badline 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/CLAUDE.md +1 -1
- data/README.md +130 -17
- data/doc/media.md +14 -2
- data/doc/pinned-behaviour.md +61 -0
- data/doc/snapshot-format.md +379 -70
- data/doc/snapshots.md +185 -10
- data/lib/badline/cbm2/acia.rb +10 -0
- data/lib/badline/cbm2/bus.rb +263 -0
- data/lib/badline/cbm2/cpu.rb +58 -0
- data/lib/badline/cbm2/crtc_video.rb +133 -0
- data/lib/badline/cbm2/ieee_drive.rb +18 -0
- data/lib/badline/cbm2/ieee_layout.rb +69 -0
- data/lib/badline/cbm2/ieee_ports.rb +104 -0
- data/lib/badline/cbm2/ieee_traps.rb +81 -0
- data/lib/badline/cbm2/keyboard_ports.rb +60 -0
- data/lib/badline/cbm2/keyed_modules.rb +67 -0
- data/lib/badline/cbm2/keyed_state.rb +91 -0
- data/lib/badline/cbm2/model.rb +44 -0
- data/lib/badline/cbm2/p500/control_ports.rb +80 -0
- data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
- data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
- data/lib/badline/cbm2/p500/model.rb +39 -0
- data/lib/badline/cbm2/p500/pages.rb +57 -0
- data/lib/badline/cbm2/p500.rb +262 -0
- data/lib/badline/cbm2/sid_page.rb +19 -0
- data/lib/badline/cbm2.rb +267 -0
- data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
- data/lib/badline/chips/acia/keyed_state.rb +47 -0
- data/lib/badline/chips/acia.rb +307 -0
- data/lib/badline/chips/crtc.rb +3 -2
- data/lib/badline/chips/riot/fast_forward.rb +72 -0
- data/lib/badline/chips/riot/keyed_parts.rb +69 -0
- data/lib/badline/chips/riot/timer.rb +80 -0
- data/lib/badline/chips/riot.rb +162 -0
- data/lib/badline/chips/rriot/keyed_state.rb +30 -0
- data/lib/badline/chips/rriot.rb +132 -0
- data/lib/badline/chips/tpi/keyed_state.rb +38 -0
- data/lib/badline/chips/tpi.rb +233 -0
- data/lib/badline/chips/vic/bank.rb +60 -5
- data/lib/badline/chips/vic/sprite.rb +4 -3
- data/lib/badline/drives/datasette/keyed_state.rb +9 -1
- data/lib/badline/drives/datasette.rb +27 -3
- data/lib/badline/drives/ieee_bus.rb +50 -4
- data/lib/badline/drives/model2031/ieee_port.rb +133 -0
- data/lib/badline/drives/model2031/keyed_state.rb +66 -0
- data/lib/badline/drives/model2031/parts.rb +25 -0
- data/lib/badline/drives/model2031.rb +92 -0
- data/lib/badline/drives/model8050/disk.rb +163 -0
- data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
- data/lib/badline/drives/model8050/idle.rb +177 -0
- data/lib/badline/drives/model8050/ieee_port.rb +147 -0
- data/lib/badline/drives/model8050/ip_bus.rb +93 -0
- data/lib/badline/drives/model8050/keyed_state.rb +160 -0
- data/lib/badline/drives/model8050/mechanism.rb +230 -0
- data/lib/badline/drives/model8050/parts.rb +37 -0
- data/lib/badline/drives/model8050.rb +243 -0
- data/lib/badline/frontend/app.rb +10 -3
- data/lib/badline/frontend/boot.rb +1 -1
- data/lib/badline/frontend/controls.rb +51 -15
- data/lib/badline/frontend/gamepads.rb +3 -3
- data/lib/badline/frontend/keys.rb +61 -2
- data/lib/badline/frontend/media_slots.rb +1 -1
- data/lib/badline/frontend/menu/menu_media.rb +14 -11
- data/lib/badline/frontend/menu/menu_pages.rb +29 -6
- data/lib/badline/frontend/menu/menu_ports.rb +40 -4
- data/lib/badline/frontend/menu/pause_menu.rb +1 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -7
- data/lib/badline/frontend/timeline.rb +19 -10
- data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
- data/lib/badline/kernal_trap/dos/status.rb +3 -0
- data/lib/badline/kernal_trap/dos.rb +18 -5
- data/lib/badline/kernal_trap/ieee.rb +7 -2
- data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
- data/lib/badline/machine/ieee_drives.rb +194 -0
- data/lib/badline/machine/settling.rb +4 -0
- data/lib/badline/machine.rb +26 -8
- data/lib/badline/media/cbm2_media.rb +29 -0
- data/lib/badline/media/pet_media.rb +5 -5
- data/lib/badline/media/program.rb +6 -3
- data/lib/badline/media/true_drive.rb +50 -8
- data/lib/badline/media.rb +21 -8
- data/lib/badline/options/family.rb +15 -3
- data/lib/badline/options/help.rb +24 -6
- data/lib/badline/options/table.rb +25 -5
- data/lib/badline/options/validation.rb +47 -7
- data/lib/badline/options.rb +48 -15
- data/lib/badline/pet/bus/expansion.rb +99 -5
- data/lib/badline/pet/bus.rb +30 -12
- data/lib/badline/pet/cassettes.rb +22 -1
- data/lib/badline/pet/characters.rb +8 -2
- data/lib/badline/pet/crtc_video.rb +20 -7
- data/lib/badline/pet/ieee_drive.rb +17 -0
- data/lib/badline/pet/ieee_ports.rb +19 -1
- data/lib/badline/pet/ieee_traps.rb +10 -5
- data/lib/badline/pet/keyed_modules.rb +4 -1
- data/lib/badline/pet/keyed_parts.rb +10 -6
- data/lib/badline/pet/model.rb +56 -6
- data/lib/badline/pet/rom_sets.rb +30 -6
- data/lib/badline/pet.rb +45 -16
- data/lib/badline/plus4/acia.rb +4 -298
- data/lib/badline/plus4/keyed_parts.rb +0 -44
- data/lib/badline/plus4.rb +1 -0
- data/lib/badline/roms/8050/dos-2.7.rom +0 -0
- data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
- data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
- data/lib/badline/roms/README +144 -1
- data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
- data/lib/badline/roms/cbm2/basic128.rom +0 -0
- data/lib/badline/roms/cbm2/basic256.rom +0 -0
- data/lib/badline/roms/cbm2/characters-600.rom +0 -0
- data/lib/badline/roms/cbm2/characters-700.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
- data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
- data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
- data/lib/badline/roms/dos2031.rom +0 -0
- data/lib/badline/roms/pet/characters-din.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
- data/lib/badline/snapshot/cbm2_setup.rb +53 -0
- data/lib/badline/snapshot/container.rb +9 -1
- data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
- data/lib/badline/snapshot/field_doc.rb +39 -3
- data/lib/badline/snapshot/image.rb +79 -15
- data/lib/badline/snapshot/pet_setup.rb +30 -13
- data/lib/badline/snapshot/vice/cbm2.rb +93 -0
- data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
- data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
- data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
- data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
- data/lib/badline/snapshot/vice/cias.rb +13 -9
- data/lib/badline/snapshot/vice/p500.rb +96 -0
- data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
- data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
- data/lib/badline/snapshot/vice/pet.rb +121 -0
- data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
- data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
- data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
- data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
- data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
- data/lib/badline/snapshot/vice/pet_via.rb +165 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
- data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
- data/lib/badline/snapshot/vice.rb +4 -0
- data/lib/badline/snapshot.rb +11 -3
- data/lib/badline/storage/crt_file.rb +7 -1
- data/lib/badline/storage/d64_image.rb +7 -0
- data/lib/badline/storage/d80_image.rb +17 -1
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image/bam.rb +18 -3
- data/lib/badline/storage/disk_image/formatting.rb +69 -0
- data/lib/badline/storage/disk_image/writing.rb +35 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage.rb +1 -0
- data/lib/badline/support/cbm2_checkpoint.rb +48 -0
- data/lib/badline/support/p500_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline.rb +1 -0
- metadata +87 -2
data/lib/badline/cbm2.rb
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/chips/acia"
|
|
4
|
+
require "badline/chips/tpi"
|
|
5
|
+
require "badline/cbm2/model"
|
|
6
|
+
require "badline/cbm2/p500/model"
|
|
7
|
+
require "badline/cbm2/bus"
|
|
8
|
+
require "badline/cbm2/cpu"
|
|
9
|
+
require "badline/cbm2/acia"
|
|
10
|
+
require "badline/cbm2/keyboard_ports"
|
|
11
|
+
require "badline/cbm2/crtc_video"
|
|
12
|
+
require "badline/cbm2/sid_page"
|
|
13
|
+
require "badline/cbm2/ieee_layout"
|
|
14
|
+
require "badline/cbm2/ieee_traps"
|
|
15
|
+
require "badline/cbm2/ieee_ports"
|
|
16
|
+
require "badline/cbm2/ieee_drive"
|
|
17
|
+
require "badline/snapshot/cbm2_setup"
|
|
18
|
+
require "badline/cbm2/keyed_state"
|
|
19
|
+
require "badline/cbm2/keyed_modules"
|
|
20
|
+
require "badline/cbm2/p500"
|
|
21
|
+
|
|
22
|
+
module Badline
|
|
23
|
+
# The Commodore CBM-II's B series, the 610, 620, 710 and 720: a 6509 at
|
|
24
|
+
# 2 MHz on a bus of sixteen 64K segments (Bus, CPU), a 6845 CRTC at
|
|
25
|
+
# $D800, a 6581 SID at $DA00, a 6526 CIA at $DC00, a 6551 ACIA at $DD00
|
|
26
|
+
# and two 6525 TPIs at $DE00 and $DF00. Nothing halts the CPU: the video
|
|
27
|
+
# reads its own screen RAM between the CPU's accesses.
|
|
28
|
+
#
|
|
29
|
+
# TPI1 is the interrupt controller, and its IRQ the CPU's only one. Its
|
|
30
|
+
# inputs are the mains on I0, the IEEE-488 bus's SRQ on I1, the CIA on
|
|
31
|
+
# I2, the coprocessor board on I3 and the ACIA on I4, each pulled low to
|
|
32
|
+
# interrupt. The mains, 50 or 60 Hz, is the system tick, and feeds the
|
|
33
|
+
# CIA's TOD pin too. TPI2 scans the key matrix and reads the model's
|
|
34
|
+
# jumpers (KeyboardPorts), and type_text types through the editor's
|
|
35
|
+
# keyboard buffer in segment 15.
|
|
36
|
+
#
|
|
37
|
+
# The DRAM's refresh takes the CPU's clock away for a cycle (Refresh),
|
|
38
|
+
# every 20 cycles or so: about 4.5% of its time.
|
|
39
|
+
#
|
|
40
|
+
# The CRTC paints its picture from the screen RAM and the character ROM
|
|
41
|
+
# (CRTCVideo). Device 8 answers through traps on the KERNAL's IEEE-488
|
|
42
|
+
# primitives (IEEETraps), or a true 2031, 8050 or 8250 on the bus
|
|
43
|
+
# (IEEEPorts, IEEEDrive) answers. It runs headless.
|
|
44
|
+
class CBM2
|
|
45
|
+
include Machine::KeyboardBuffer
|
|
46
|
+
include Machine::Clocking
|
|
47
|
+
include Machine::Settling
|
|
48
|
+
include IEEETraps
|
|
49
|
+
include IEEEDrive
|
|
50
|
+
|
|
51
|
+
CLOCK_HZ = 2_000_000
|
|
52
|
+
|
|
53
|
+
# The DRAM refresh (sheet 3 of schematic 8256043, U89 and U55): a
|
|
54
|
+
# counter of the CPU's cycles asks for a refresh once it reaches 20,
|
|
55
|
+
# stops there, and the refresh comes on the next opcode fetch (SYNC),
|
|
56
|
+
# which it holds off a cycle with RDY while it stops the CPU's clock,
|
|
57
|
+
# and clears the counter.
|
|
58
|
+
REFRESH_COUNT = 20
|
|
59
|
+
|
|
60
|
+
# The segment BASIC keeps its program text in.
|
|
61
|
+
TEXT_SEGMENT = 1
|
|
62
|
+
|
|
63
|
+
# The editor's keyboard buffer in segment 15, KEYD at $03AB and NDX at
|
|
64
|
+
# $D1, the same in 901244-03b and -04a.
|
|
65
|
+
KEYBOARD_LAYOUT = Machine::KeyboardBuffer::Layout.new(address: 0x03ab, count: 0xd1, capacity: 10)
|
|
66
|
+
|
|
67
|
+
attr_reader :model, :bus, :cpu, :crtc, :sid, :cia, :acia, :tpi1, :tpi2, :keyboard, :ieee, :mains_hz, :kernal,
|
|
68
|
+
:cycles, :init_threshold
|
|
69
|
+
|
|
70
|
+
def ram = @bus.ram
|
|
71
|
+
|
|
72
|
+
# Whether the DRAM refresh takes its cycles from the CPU.
|
|
73
|
+
def refresh? = @refreshing
|
|
74
|
+
|
|
75
|
+
def family = :cbm2
|
|
76
|
+
|
|
77
|
+
# Reads and writes memory as the CPU sees it, in its execution segment.
|
|
78
|
+
def peek(addr) = @bus.peek(addr)
|
|
79
|
+
|
|
80
|
+
def poke(addr, value) = @bus.poke(addr, value)
|
|
81
|
+
|
|
82
|
+
def install_debug_register(&) = @bus.install_debug_register(&)
|
|
83
|
+
|
|
84
|
+
def keyboard_buffer = KEYBOARD_LAYOUT
|
|
85
|
+
|
|
86
|
+
def sound_source = @sid
|
|
87
|
+
|
|
88
|
+
# The Canvas a front end shows (CRTCVideo).
|
|
89
|
+
def video = @video.canvas
|
|
90
|
+
|
|
91
|
+
# What paints the picture.
|
|
92
|
+
def crtc_video = @video
|
|
93
|
+
|
|
94
|
+
# +model+ names one of Model::ALL, +kernal+ one of Model::KERNALS, and
|
|
95
|
+
# +mains_hz+ the mains, 50 or 60, which also sets a 600's jumper for
|
|
96
|
+
# the editor's 60 Hz CRTC table. +refresh+ false leaves out the DRAM
|
|
97
|
+
# refresh's stolen cycles.
|
|
98
|
+
def initialize(model: Model::DEFAULT, kernal: "04a", mains_hz: 50, refresh: true, debug: false)
|
|
99
|
+
@model = Model.named(model)
|
|
100
|
+
raise ArgumentError, "no CBM-II KERNAL named #{kernal}" unless Model::KERNALS.key?(kernal)
|
|
101
|
+
raise ArgumentError, "the mains is 50 or 60 Hz, not #{mains_hz}" unless [50, 60].include?(mains_hz)
|
|
102
|
+
|
|
103
|
+
@kernal = kernal
|
|
104
|
+
@mains_hz = mains_hz
|
|
105
|
+
@bus = Bus.new(basic: Model.basic(@model, kernal), kernal: Model::KERNALS.fetch(kernal),
|
|
106
|
+
ram_segments: @model.ram_segments)
|
|
107
|
+
@ieee = IEEEPorts.new(Drives::IEEEBus.new)
|
|
108
|
+
@tpi1 = Chips::TPI.new(peripheral: @ieee)
|
|
109
|
+
registers = Array.new(Chips::CRTC::MASKS.length, 0)
|
|
110
|
+
@video = CRTCVideo.new(ROM.read(@model.characters), @bus.screen, @tpi1, registers,
|
|
111
|
+
high_profile: @model.high_profile)
|
|
112
|
+
@crtc = Chips::CRTC.new(@video, registers:)
|
|
113
|
+
@sid = Chips::SID.new(model: :mos6581, at: 0xda00)
|
|
114
|
+
@cia = Chips::CIA.new(start: 0xdc00, peripheral: @ieee.data, region: Region::PAL.with(clock_hz: CLOCK_HZ),
|
|
115
|
+
tod_hz: mains_hz)
|
|
116
|
+
@ieee.connect(@cia, @tpi1)
|
|
117
|
+
@acia = ACIA.new(self, CLOCK_HZ)
|
|
118
|
+
@keyboard = Input::Keyboard.new(matrix: KeyboardPorts::MATRIX)
|
|
119
|
+
keyboard_ports = KeyboardPorts.new(@keyboard, jumpers)
|
|
120
|
+
@tpi2 = Chips::TPI.new(peripheral: keyboard_ports)
|
|
121
|
+
keyboard_ports.connect(@tpi2)
|
|
122
|
+
map_io
|
|
123
|
+
@cpu = CPU.new(@bus, debug:)
|
|
124
|
+
@cycles = 0
|
|
125
|
+
@mains_half_period = CLOCK_HZ / (2 * mains_hz)
|
|
126
|
+
@mains_countdown = @mains_half_period
|
|
127
|
+
@mains_high = true
|
|
128
|
+
@refresh = 0
|
|
129
|
+
@refreshing = refresh
|
|
130
|
+
@acia_irq = false
|
|
131
|
+
@acia_due = Chips::ACIA::NEVER
|
|
132
|
+
@init_handlers = []
|
|
133
|
+
@pending_keys = nil
|
|
134
|
+
@dos = nil
|
|
135
|
+
@ieee_trap = nil
|
|
136
|
+
@capture_output = nil
|
|
137
|
+
init_ieee_drives
|
|
138
|
+
@init_threshold = boot_cycles
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The model, its RAM and its mains as the pause menu names them, such
|
|
142
|
+
# as "610, 128K RAM, 50 HZ".
|
|
143
|
+
def model_label = "#{@model.name}, #{@model.ram_kb}K RAM, #{@mains_hz} HZ"
|
|
144
|
+
|
|
145
|
+
# The 2 MHz clock and the frame of the editor's CRTC table, nominal,
|
|
146
|
+
# since a program can program the CRTC otherwise, and all of the
|
|
147
|
+
# Canvas, two dots to a window pixel.
|
|
148
|
+
def timing
|
|
149
|
+
cycles_per_line, lines_per_frame = crtc_frame
|
|
150
|
+
canvas = @video.canvas
|
|
151
|
+
Timing.new(clock_hz: CLOCK_HZ, cycles_per_line:, lines_per_frame:, crop: [0, 0, canvas.width, canvas.height],
|
|
152
|
+
pixel_width: 1, dots: 2)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The CRTC and the mains ahead of the chips, then TPI1's inputs from
|
|
156
|
+
# the CIA and the ACIA, and the CPU last.
|
|
157
|
+
def cycle!
|
|
158
|
+
handle_init if @cycles == @init_threshold
|
|
159
|
+
feed_keyboard if @pending_keys
|
|
160
|
+
|
|
161
|
+
@crtc.cycle!
|
|
162
|
+
tick_mains
|
|
163
|
+
@cia.cycle!
|
|
164
|
+
@sid.cycle!
|
|
165
|
+
@acia.run!(@cycles) if @cycles >= @acia_due
|
|
166
|
+
tpi1 = @tpi1
|
|
167
|
+
tpi1.interrupt_line(2, !@cia.interrupted?)
|
|
168
|
+
tpi1.interrupt_line(4, !@acia_irq)
|
|
169
|
+
@cpu.irq = tpi1.irq?
|
|
170
|
+
if @refreshing && @refresh >= REFRESH_COUNT && @cpu.boundary?
|
|
171
|
+
@refresh = 0
|
|
172
|
+
@cpu.stall!
|
|
173
|
+
else
|
|
174
|
+
@refresh += 1 if @refresh < REFRESH_COUNT
|
|
175
|
+
@cpu.cycle!
|
|
176
|
+
end
|
|
177
|
+
clock_ieee_drives
|
|
178
|
+
|
|
179
|
+
@cycles += 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The ACIA's IRQ line, and the cycle it next has something to do at.
|
|
183
|
+
def acia_changed(irq, due)
|
|
184
|
+
@acia_irq = irq
|
|
185
|
+
@acia_due = due
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The RES line reaches the CPU and its segment registers, the CRTC, the
|
|
189
|
+
# SID, the CIA, the ACIA and both TPIs.
|
|
190
|
+
def reset!
|
|
191
|
+
@crtc.reset!
|
|
192
|
+
@sid.reset!
|
|
193
|
+
@cia.reset!
|
|
194
|
+
@acia.reset!
|
|
195
|
+
@tpi1.reset!
|
|
196
|
+
@tpi2.reset!
|
|
197
|
+
@bus.reset!
|
|
198
|
+
@cpu.reset!
|
|
199
|
+
reset_ieee_drives
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Puts a program file's bytes where its first two say in BASIC's text
|
|
203
|
+
# segment, segment 1, and returns that address.
|
|
204
|
+
def load_prg(data)
|
|
205
|
+
load_addr = data[0] | (data[1] << 8)
|
|
206
|
+
bytes = data[2..] || []
|
|
207
|
+
bytes = bytes[0, 0x10000 - load_addr] if load_addr + bytes.length > 0x10000
|
|
208
|
+
bytes.each_with_index { |byte, i| @bus.poke_bank(TEXT_SEGMENT, load_addr + i, byte) }
|
|
209
|
+
load_addr
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Puts a cartridge's ROM chips in (Bus#attach_cartridge) and switches
|
|
213
|
+
# the machine off and on, for the KERNAL to find it as it starts.
|
|
214
|
+
def attach_cartridge(chips)
|
|
215
|
+
@bus.attach_cartridge(chips)
|
|
216
|
+
power_cycle!
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Switches the machine off and on: RAM starts cleared, and the RES
|
|
220
|
+
# line resets everything else.
|
|
221
|
+
def power_cycle!
|
|
222
|
+
@bus.power_on!
|
|
223
|
+
reset!
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
# PC6 and PC7 as the board's jumpers drive them.
|
|
229
|
+
def jumpers
|
|
230
|
+
return 0x80 if @model.high_profile
|
|
231
|
+
|
|
232
|
+
@mains_hz == 60 ? 0x40 : 0x00
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def map_io
|
|
236
|
+
@bus.map_io(0xd8, @crtc)
|
|
237
|
+
@bus.map_io(0xda, SIDPage.new(@sid))
|
|
238
|
+
@bus.map_io(0xdc, @cia)
|
|
239
|
+
@bus.map_io(0xdd, @acia)
|
|
240
|
+
@bus.map_io(0xde, @tpi1)
|
|
241
|
+
@bus.map_io(0xdf, @tpi2)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The mains, low for half its period and high for the other half, on
|
|
245
|
+
# TPI1's I0, whose falling edge latches the tick.
|
|
246
|
+
def tick_mains
|
|
247
|
+
@mains_countdown -= 1
|
|
248
|
+
return unless @mains_countdown.zero?
|
|
249
|
+
|
|
250
|
+
@mains_countdown = @mains_half_period
|
|
251
|
+
@mains_high = !@mains_high
|
|
252
|
+
@tpi1.interrupt_line(0, @mains_high)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# The characters a line and the lines a frame of the editor's table:
|
|
256
|
+
# the 700's, or the 600's 50 or 60 Hz one.
|
|
257
|
+
def crtc_frame
|
|
258
|
+
return [108, 367] if @model.high_profile
|
|
259
|
+
|
|
260
|
+
@mains_hz == 60 ? [127, 262] : [128, 313]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The cycle by which the KERNAL has tested the RAM board, about 3.7
|
|
264
|
+
# million cycles a segment with the refresh, and BASIC printed READY, with a margin.
|
|
265
|
+
def boot_cycles = 1_000_000 + (3_800_000 * @model.ram_segments.length)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Chips
|
|
5
|
+
class ACIA
|
|
6
|
+
# The registers, the status, the shift register and when the
|
|
7
|
+
# transmitter's character ends, in a keyed snapshot module (BL-ACIA).
|
|
8
|
+
# What the device on the line has still to send stays with the
|
|
9
|
+
# host, and the receiver starts on it again once loaded (#restored!).
|
|
10
|
+
# A snapshot from before the transmitter's timing was saved starts
|
|
11
|
+
# its character clock on load.
|
|
12
|
+
module KeyedState
|
|
13
|
+
def save_fields(out)
|
|
14
|
+
out.int("command", @command).int("control", @control).int("errors", @errors)
|
|
15
|
+
out.bool("receiver_full", @receiver_full).bool("transmitter_empty", @transmitter_empty).bool("irq", @irq)
|
|
16
|
+
out.int("receive_data", @receive_data).int("transmit_data", @transmit_data).int("shifting", @shifting)
|
|
17
|
+
out.int("transmit_at", @transmit_at).int("transmit_remainder", @transmit_remainder)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load_fields(input)
|
|
21
|
+
@command = input.byte("command", @command)
|
|
22
|
+
@control = input.byte("control", @control)
|
|
23
|
+
@errors = input.masked("errors", 0, PARITY_ERROR | FRAMING_ERROR | OVERRUN)
|
|
24
|
+
@receiver_full = input.bool("receiver_full", false)
|
|
25
|
+
@transmitter_empty = input.bool("transmitter_empty", true)
|
|
26
|
+
@irq = input.bool("irq", false)
|
|
27
|
+
@receive_data = input.byte("receive_data", 0)
|
|
28
|
+
@transmit_data = input.byte("transmit_data", 0)
|
|
29
|
+
@shifting = input.int("shifting", IDLE)
|
|
30
|
+
@transmit_remainder = input.int("transmit_remainder", 0)
|
|
31
|
+
transmit_at = input.optional_int("transmit_at")
|
|
32
|
+
@transmit_at = transmit_at || NEVER
|
|
33
|
+
@clock_lost = transmit_at.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def restored!
|
|
37
|
+
@receive_at = NEVER
|
|
38
|
+
@lines = modem_lines
|
|
39
|
+
start_transmitting if @clock_lost && transmitter_on?
|
|
40
|
+
@clock_lost = false
|
|
41
|
+
start_receiving
|
|
42
|
+
changed
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/chips/acia/frames"
|
|
4
|
+
require "badline/chips/acia/keyed_state"
|
|
5
|
+
|
|
6
|
+
module Badline
|
|
7
|
+
module Chips
|
|
8
|
+
# The 6551 ACIA, its four registers repeated through the page it is
|
|
9
|
+
# decoded in:
|
|
10
|
+
#
|
|
11
|
+
# 0 - read the receiver's data, write the transmitter's
|
|
12
|
+
# 1 - read the status, write a programmed reset
|
|
13
|
+
# 2 - the command register
|
|
14
|
+
# 3 - the control register
|
|
15
|
+
#
|
|
16
|
+
# Status: bit 0 a parity error, 1 a framing error, 2 an overrun, 3 the
|
|
17
|
+
# receiver full, 4 the transmitter empty, 5 DCD and 6 DSR as their pins
|
|
18
|
+
# read (1 high, not asserted), 7 an interrupt. Reading it clears bit 7.
|
|
19
|
+
#
|
|
20
|
+
# Command: bit 0 DTR, which also enables the receiver and every
|
|
21
|
+
# interrupt, bit 1 set disables the receiver's interrupt, bits 2-3 the
|
|
22
|
+
# transmitter (00 off with RTS high, 01 on with its interrupt, 10 on
|
|
23
|
+
# without, 11 sending a break), bit 4 echoes what is received, bits 5-7
|
|
24
|
+
# the parity.
|
|
25
|
+
#
|
|
26
|
+
# Control: bits 0-3 the baud rate from the 1.8432 MHz crystal (0, the
|
|
27
|
+
# 16x external clock, runs straight from the crystal, 115,200 baud),
|
|
28
|
+
# bit 4 the receiver's clock (0 the RxC pin, which nothing drives, so
|
|
29
|
+
# the receiver hears nothing), bits 5-6 the word length (8 down to 5
|
|
30
|
+
# bits) and bit 7 the stop bits (Frames).
|
|
31
|
+
#
|
|
32
|
+
# The transmitter runs in whole characters while it is on: at the end
|
|
33
|
+
# of each it hands the character it shifted out to the line, moves a
|
|
34
|
+
# waiting byte from the data register into the shift register and sets
|
|
35
|
+
# the transmitter empty, and with its interrupt on interrupts while the
|
|
36
|
+
# register is empty, every character until a byte comes. The first
|
|
37
|
+
# character ends a character's time after the transmitter comes on.
|
|
38
|
+
# The receiver takes a character's time for each byte the line sends,
|
|
39
|
+
# and a byte that arrives while the last is unread is lost to an
|
|
40
|
+
# overrun. The receiver full, and DCD or DSR changing, interrupt unless
|
|
41
|
+
# bit 1 disables it. CTS is held asserted, so the transmitter is always
|
|
42
|
+
# clear to send.
|
|
43
|
+
#
|
|
44
|
+
# What is on the other end of the line (#plug): nothing, where DCD and
|
|
45
|
+
# DSR read high and what is sent goes nowhere; a terminal, which
|
|
46
|
+
# asserts DCD and DSR, sends what the host feeds it (#feed) while DTR is
|
|
47
|
+
# asserted and keeps what it receives for the host to drain (#drain);
|
|
48
|
+
# or a loopback plug, which returns TxD to RxD and DTR to DSR and DCD.
|
|
49
|
+
#
|
|
50
|
+
# The machine runs the ACIA at the cycle it next has something to do
|
|
51
|
+
# (#due) through #run!, and learns its IRQ line and that cycle from
|
|
52
|
+
# `clock`'s #acia_changed, whose #cycles is the time, counted at
|
|
53
|
+
# `clock_hz`. Each machine has a subclass of its own, so `clock` keeps
|
|
54
|
+
# one type in each.
|
|
55
|
+
class ACIA
|
|
56
|
+
include Frames
|
|
57
|
+
include KeyedState
|
|
58
|
+
|
|
59
|
+
PARITY_ERROR = 0x01
|
|
60
|
+
FRAMING_ERROR = 0x02
|
|
61
|
+
OVERRUN = 0x04
|
|
62
|
+
RECEIVER_FULL = 0x08
|
|
63
|
+
TRANSMITTER_EMPTY = 0x10
|
|
64
|
+
DCD = 0x20
|
|
65
|
+
DSR = 0x40
|
|
66
|
+
IRQ = 0x80
|
|
67
|
+
|
|
68
|
+
NOTHING = 0
|
|
69
|
+
TERMINAL = 1
|
|
70
|
+
LOOPBACK = 2
|
|
71
|
+
|
|
72
|
+
# No shift in progress.
|
|
73
|
+
IDLE = -1
|
|
74
|
+
|
|
75
|
+
# Far enough ahead that it never comes.
|
|
76
|
+
NEVER = 1 << 60
|
|
77
|
+
|
|
78
|
+
attr_reader :command, :control, :device, :due
|
|
79
|
+
|
|
80
|
+
def initialize(clock, clock_hz)
|
|
81
|
+
@clock = clock
|
|
82
|
+
@clock_hz = clock_hz
|
|
83
|
+
@device = NOTHING
|
|
84
|
+
@incoming = []
|
|
85
|
+
@received = []
|
|
86
|
+
reset!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The RES line: every register cleared, the transmitter empty and
|
|
90
|
+
# both shift registers stopped.
|
|
91
|
+
def reset!
|
|
92
|
+
@command = 0
|
|
93
|
+
@control = 0
|
|
94
|
+
@errors = 0
|
|
95
|
+
@receiver_full = false
|
|
96
|
+
@transmitter_empty = true
|
|
97
|
+
@irq = false
|
|
98
|
+
@receive_data = 0
|
|
99
|
+
@transmit_data = 0
|
|
100
|
+
@shifting = IDLE
|
|
101
|
+
@transmit_at = NEVER
|
|
102
|
+
@transmit_remainder = 0
|
|
103
|
+
@receive_at = NEVER
|
|
104
|
+
@receive_remainder = 0
|
|
105
|
+
@clock_lost = false
|
|
106
|
+
@lines = modem_lines
|
|
107
|
+
changed
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def irq? = @irq
|
|
111
|
+
|
|
112
|
+
# DTR and RTS as asserted, low on the pins.
|
|
113
|
+
def dtr? = @command.anybits?(0x01)
|
|
114
|
+
|
|
115
|
+
def rts? = @command.anybits?(0x0c)
|
|
116
|
+
|
|
117
|
+
# Whether the transmitter sends a break, holding TxD low.
|
|
118
|
+
def break? = @command & 0x0c == 0x0c
|
|
119
|
+
|
|
120
|
+
# Plugs NOTHING, a TERMINAL or a LOOPBACK plug into the user port.
|
|
121
|
+
def plug(device)
|
|
122
|
+
@device = device
|
|
123
|
+
@incoming.clear
|
|
124
|
+
lines_changed
|
|
125
|
+
start_receiving
|
|
126
|
+
changed
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Bytes the terminal sends, one a character's time while DTR is
|
|
130
|
+
# asserted.
|
|
131
|
+
def feed(bytes)
|
|
132
|
+
return unless @device == TERMINAL
|
|
133
|
+
|
|
134
|
+
@incoming.concat(bytes)
|
|
135
|
+
start_receiving
|
|
136
|
+
changed
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# What the terminal has received since the last drain.
|
|
140
|
+
def drain
|
|
141
|
+
received = @received
|
|
142
|
+
@received = []
|
|
143
|
+
received
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Runs what falls due up to cycle `now`: the transmitter's character
|
|
147
|
+
# ends, then the receiver's.
|
|
148
|
+
def run!(now)
|
|
149
|
+
end_character while @transmit_at <= now
|
|
150
|
+
receive_character while @receive_at <= now
|
|
151
|
+
changed
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def peek(addr)
|
|
155
|
+
case addr & 0x03
|
|
156
|
+
when 0
|
|
157
|
+
@receiver_full = false
|
|
158
|
+
@receive_data
|
|
159
|
+
when 1 then read_status
|
|
160
|
+
when 2 then @command
|
|
161
|
+
else @control
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def poke(addr, value)
|
|
166
|
+
case addr & 0x03
|
|
167
|
+
when 0 then write_data(value)
|
|
168
|
+
when 1 then write_command(@command & 0xe0, clear_overrun: true)
|
|
169
|
+
when 2 then write_command(value)
|
|
170
|
+
else @control = value
|
|
171
|
+
end
|
|
172
|
+
changed
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The status as a read finds it, without clearing the interrupt.
|
|
176
|
+
def status
|
|
177
|
+
status = @errors | (@lines & (DCD | DSR))
|
|
178
|
+
status |= RECEIVER_FULL if @receiver_full
|
|
179
|
+
status |= TRANSMITTER_EMPTY if @transmitter_empty
|
|
180
|
+
@irq ? status | IRQ : status
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def read_status
|
|
186
|
+
value = status
|
|
187
|
+
@irq = false
|
|
188
|
+
changed
|
|
189
|
+
value
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def write_data(value)
|
|
193
|
+
@transmit_data = value
|
|
194
|
+
@transmitter_empty = false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def write_command(value, clear_overrun: false)
|
|
198
|
+
was_on = transmitter_on?
|
|
199
|
+
@command = value
|
|
200
|
+
@errors &= ~OVERRUN if clear_overrun
|
|
201
|
+
start_transmitting if transmitter_on? && !was_on
|
|
202
|
+
stop_transmitting unless transmitter_on? || @shifting != IDLE
|
|
203
|
+
lines_changed
|
|
204
|
+
start_receiving
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def transmitter_on?
|
|
208
|
+
control = @command & 0x0c
|
|
209
|
+
control != 0 && control != 0x0c
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def interrupts? = @command.anybits?(0x01)
|
|
213
|
+
|
|
214
|
+
def receiver_interrupts? = @command & 0x03 == 0x01
|
|
215
|
+
|
|
216
|
+
def transmitter_interrupts? = @command & 0x0d == 0x05
|
|
217
|
+
|
|
218
|
+
def receiving? = dtr? && @control.anybits?(0x10)
|
|
219
|
+
|
|
220
|
+
def start_transmitting
|
|
221
|
+
@transmit_remainder = 0
|
|
222
|
+
@transmit_at = after(@clock.cycles, true)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def stop_transmitting
|
|
226
|
+
@transmit_at = NEVER
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A character's end: the character shifted out goes to the line, and
|
|
230
|
+
# the transmitter takes the next byte or interrupts while it has none.
|
|
231
|
+
def end_character
|
|
232
|
+
sent = @shifting
|
|
233
|
+
@shifting = IDLE
|
|
234
|
+
deliver(sent) unless sent == IDLE
|
|
235
|
+
on = transmitter_on?
|
|
236
|
+
if on && !@transmitter_empty
|
|
237
|
+
@shifting = @transmit_data & word_mask
|
|
238
|
+
@transmitter_empty = true
|
|
239
|
+
end
|
|
240
|
+
@irq = true if on && @transmitter_empty && transmitter_interrupts?
|
|
241
|
+
if on || @shifting != IDLE
|
|
242
|
+
@transmit_at = after(@transmit_at, true)
|
|
243
|
+
else
|
|
244
|
+
stop_transmitting
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def deliver(byte)
|
|
249
|
+
case @device
|
|
250
|
+
when TERMINAL then @received << byte
|
|
251
|
+
when LOOPBACK then take(byte) if receiving?
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def start_receiving
|
|
256
|
+
return unless @receive_at == NEVER && !@incoming.empty? && receiving?
|
|
257
|
+
|
|
258
|
+
@receive_remainder = 0
|
|
259
|
+
@receive_at = after(@clock.cycles, false)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def receive_character
|
|
263
|
+
byte = @incoming[0]
|
|
264
|
+
@incoming.delete_at(0)
|
|
265
|
+
take(byte)
|
|
266
|
+
@receive_at = !@incoming.empty? && receiving? ? after(@receive_at, false) : NEVER
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# A whole character in from RxD: into the data register, or lost to
|
|
270
|
+
# an overrun while the last is unread. In echo mode it goes back out
|
|
271
|
+
# to the terminal.
|
|
272
|
+
def take(byte)
|
|
273
|
+
if @receiver_full
|
|
274
|
+
@errors |= OVERRUN
|
|
275
|
+
else
|
|
276
|
+
@receive_data = byte & word_mask
|
|
277
|
+
@receiver_full = true
|
|
278
|
+
@errors = 0
|
|
279
|
+
end
|
|
280
|
+
@received << byte if @device == TERMINAL && @command & 0x1c == 0x10
|
|
281
|
+
@irq = true if receiver_interrupts?
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# DCD and DSR as the device drives them, interrupting on a change.
|
|
285
|
+
def lines_changed
|
|
286
|
+
lines = modem_lines
|
|
287
|
+
return if lines == @lines
|
|
288
|
+
|
|
289
|
+
@lines = lines
|
|
290
|
+
@irq = true if receiver_interrupts?
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def modem_lines
|
|
294
|
+
case @device
|
|
295
|
+
when TERMINAL then 0
|
|
296
|
+
when LOOPBACK then dtr? ? 0 : DCD | DSR
|
|
297
|
+
else DCD | DSR
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def changed
|
|
302
|
+
@due = [@transmit_at, @receive_at].min
|
|
303
|
+
@clock.acia_changed(@irq && interrupts?, @due)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
data/lib/badline/chips/crtc.rb
CHANGED
|
@@ -23,7 +23,8 @@ module Badline
|
|
|
23
23
|
# row ends after the line where it equals R9; past R9 it runs on to
|
|
24
24
|
# 31 and wraps.
|
|
25
25
|
# - The row counter (seven bits) counts rows to R4. R5 more lines of
|
|
26
|
-
# vertical adjust follow, then the frame starts again. The
|
|
26
|
+
# vertical adjust follow, then the frame starts again. The line
|
|
27
|
+
# counter counts them, and they end no row, past R9 or not. The display
|
|
27
28
|
# stops at the start of the row the counter equals R6, and comes back
|
|
28
29
|
# at the next frame; vertical sync starts with row R7 and lasts
|
|
29
30
|
# VSYNC_LINES lines.
|
|
@@ -91,7 +92,7 @@ module Badline
|
|
|
91
92
|
column = @column
|
|
92
93
|
if column == regs[1]
|
|
93
94
|
@column_display = false
|
|
94
|
-
@row_start = @address if @line == regs[9]
|
|
95
|
+
@row_start = @address if @line == regs[9] && !@adjusting
|
|
95
96
|
end
|
|
96
97
|
@output.character(@address, @line, @since_hsync) if @column_display && @row_display
|
|
97
98
|
if column == regs[2]
|