badline 0.4.2 → 0.5.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 +25 -0
- data/CLAUDE.md +10 -5
- data/README.md +54 -23
- data/doc/pinned-behaviour.md +221 -0
- data/exe/badline-ruby +7 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/jukebox.rb +6 -4
- data/lib/badline/audio/machine_player.rb +10 -6
- data/lib/badline/cartridge/kcs_power.rb +3 -3
- data/lib/badline/cia.rb +5 -1
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +134 -0
- data/lib/badline/drive1541/disk.rb +256 -0
- data/lib/badline/drive1541/disk_via.rb +33 -0
- data/lib/badline/drive1541/gcr.rb +57 -0
- data/lib/badline/drive1541/idle.rb +184 -0
- data/lib/badline/drive1541/mechanism.rb +277 -0
- data/lib/badline/drive1541/rotation.rb +111 -0
- data/lib/badline/drive1541/sector_reader.rb +76 -0
- data/lib/badline/drive1541/serial_port.rb +67 -0
- data/lib/badline/drive1541/track.rb +98 -0
- data/lib/badline/drive1541.rb +218 -0
- data/lib/badline/gui/application.rb +15 -3
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +93 -0
- data/lib/badline/kernal_trap/channel.rb +9 -1
- data/lib/badline/kernal_trap/serial.rb +9 -3
- data/lib/badline/media/true_drive.rb +54 -0
- data/lib/badline/media.rb +22 -1
- data/lib/badline/options.rb +24 -4
- data/lib/badline/region.rb +35 -10
- data/lib/badline/reu/dma.rb +228 -0
- data/lib/badline/reu/ram.rb +65 -0
- data/lib/badline/reu/trigger.rb +32 -0
- data/lib/badline/reu.rb +276 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/dos1541.rom +0 -0
- data/lib/badline/sdl/functions.rb +1 -0
- data/lib/badline/storage/d64_image.rb +18 -9
- data/lib/badline/storage/d81_image.rb +1 -0
- data/lib/badline/storage/disk_image/catalog.rb +52 -0
- data/lib/badline/storage/disk_image/writing.rb +16 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/g64_image.rb +222 -0
- data/lib/badline/storage/host_directory.rb +21 -1
- data/lib/badline/storage/listing.rb +118 -0
- data/lib/badline/storage/t64.rb +14 -0
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/traps.rb +4 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +3 -0
- data/lib/badline/via/fast_forward.rb +38 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +30 -0
- data/lib/badline/via/timer2.rb +25 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/display_state.rb +7 -7
- data/lib/badline/vic/sprite/internal_bus.rb +11 -7
- data/lib/badline/vic/sprite/shifter.rb +10 -15
- data/lib/badline/vic/sprite/timing.rb +60 -0
- data/lib/badline/vic/sprite.rb +14 -12
- data/lib/badline/vic/sprites.rb +2 -2
- data/lib/badline/vic.rb +86 -39
- data/lib/badline.rb +3 -0
- metadata +26 -1
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Audio
|
|
5
5
|
# Plays a tune's songs interactively: the console's keys step between
|
|
6
|
-
# songs, pause and quit, and the status line follows along.
|
|
7
|
-
#
|
|
6
|
+
# songs, pause and quit, and the status line follows along. A song that
|
|
7
|
+
# plays to its end moves on to the next one. It stops after the last
|
|
8
|
+
# song, on q, or on Ctrl-C.
|
|
8
9
|
#
|
|
9
10
|
# `renderer` builds the renderer for a song at the sink's rate, and
|
|
10
11
|
# `length` gives a song's length in seconds.
|
|
@@ -23,7 +24,7 @@ module Badline
|
|
|
23
24
|
loop do
|
|
24
25
|
@action = nil
|
|
25
26
|
result = play(song)
|
|
26
|
-
song = following(song)
|
|
27
|
+
song = following(song, result)
|
|
27
28
|
return result unless song
|
|
28
29
|
end
|
|
29
30
|
end
|
|
@@ -43,10 +44,11 @@ module Badline
|
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
def following(song)
|
|
47
|
+
def following(song, result)
|
|
47
48
|
case @action
|
|
48
49
|
when :next then song + 1
|
|
49
50
|
when :previous then song - 1
|
|
51
|
+
else song + 1 if result == :finished && song < @songs
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
|
|
@@ -5,7 +5,9 @@ module Badline
|
|
|
5
5
|
# Runs a tune on the whole machine, for the RSID tunes that install their
|
|
6
6
|
# own interrupts and expect a booted C64 underneath. The tune goes in
|
|
7
7
|
# the way `Media` puts it in: BASIC SYSes the driver stub, or RUNs a
|
|
8
|
-
# BASIC tune. Rendering starts when the CPU gets there.
|
|
8
|
+
# BASIC tune. Rendering starts when the CPU gets there. The machine is
|
|
9
|
+
# PAL whatever the tune asks for, and the frames and the clock follow
|
|
10
|
+
# its region.
|
|
9
11
|
class MachinePlayer
|
|
10
12
|
FRAME_CYCLES = BarePlayer::FRAME_CYCLES
|
|
11
13
|
|
|
@@ -16,6 +18,8 @@ module Badline
|
|
|
16
18
|
@tune = tune
|
|
17
19
|
@song = song || tune.start_song
|
|
18
20
|
@computer = Computer.new(sid_model:)
|
|
21
|
+
region = @computer.region
|
|
22
|
+
@frame_cycles = region.cycles_per_line * region.lines_per_frame
|
|
19
23
|
@injected = false
|
|
20
24
|
@started = false
|
|
21
25
|
end
|
|
@@ -28,13 +32,13 @@ module Badline
|
|
|
28
32
|
|
|
29
33
|
def sid = @computer.sid
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
def clock_hz = TimeOfDay::CLOCK_HZ
|
|
35
|
+
def clock_hz = @computer.region.clock_hz
|
|
33
36
|
|
|
34
|
-
# Advances one
|
|
35
|
-
# yields whatever the SID recorded over it. Returns the
|
|
37
|
+
# Advances one frame of the machine, or `budget` cycles if that is
|
|
38
|
+
# shorter, then yields whatever the SID recorded over it. Returns the
|
|
39
|
+
# cycles advanced.
|
|
36
40
|
def frame(budget, &)
|
|
37
|
-
cycles = [budget,
|
|
41
|
+
cycles = [budget, @frame_cycles].min
|
|
38
42
|
cycles.times { @computer.cycle! }
|
|
39
43
|
sid.drain_samples.each(&)
|
|
40
44
|
cycles
|
|
@@ -31,7 +31,7 @@ module Badline
|
|
|
31
31
|
elsif addr.anybits?(0x80)
|
|
32
32
|
(@exrom << 7) | (@game << 6) | (open_bus(addr) & 0x3f)
|
|
33
33
|
else
|
|
34
|
-
@
|
|
34
|
+
@io_ram[addr & 0x7f]
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -39,7 +39,7 @@ module Badline
|
|
|
39
39
|
if addr < 0xdf00
|
|
40
40
|
switch(addr.anybits?(0x02) ? :ultimax : :rom16k)
|
|
41
41
|
elsif addr.nobits?(0x80)
|
|
42
|
-
@
|
|
42
|
+
@io_ram[addr & 0x7f] = value
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -64,7 +64,7 @@ module Badline
|
|
|
64
64
|
roml, romh = banks_from(chips)
|
|
65
65
|
@roml = roml.first || EMPTY_BANK
|
|
66
66
|
@romh = romh.first || EMPTY_BANK
|
|
67
|
-
@
|
|
67
|
+
@io_ram = Array.new(0x80, 0xff)
|
|
68
68
|
reset
|
|
69
69
|
end
|
|
70
70
|
end
|
data/lib/badline/cia.rb
CHANGED
|
@@ -13,7 +13,11 @@ module Badline
|
|
|
13
13
|
# C64C, which differs from it only in its interrupt control register.
|
|
14
14
|
MODELS = %i[mos6526 mos6526a].freeze
|
|
15
15
|
|
|
16
|
-
attr_reader :start, :control_a, :control_b, :
|
|
16
|
+
attr_reader :start, :control_a, :control_b, :serial, :model
|
|
17
|
+
|
|
18
|
+
# What's on the ports outside the chip. It pulls port lines low through
|
|
19
|
+
# read_a and read_b, and on CIA 1 drives PB4 through port_b4_high?.
|
|
20
|
+
attr_accessor :peripheral
|
|
17
21
|
|
|
18
22
|
def interrupt_status = @icr.status
|
|
19
23
|
|
data/lib/badline/computer.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Badline
|
|
|
5
5
|
include IntegerHelper
|
|
6
6
|
include KeyboardBuffer
|
|
7
7
|
|
|
8
|
-
attr_reader :address_bus, :cpu, :cycles
|
|
8
|
+
attr_reader :address_bus, :cpu, :cycles, :drive1541
|
|
9
9
|
|
|
10
10
|
def region = address_bus.region
|
|
11
11
|
|
|
@@ -33,9 +33,12 @@ module Badline
|
|
|
33
33
|
|
|
34
34
|
# The machine options (sid_model:, cia_model:, vic_model:, region: and
|
|
35
35
|
# ram_expansion:) configure the AddressBus. The region sets the clock,
|
|
36
|
-
# the VIC's raster and the mains frequency the CIAs'
|
|
37
|
-
#
|
|
38
|
-
|
|
36
|
+
# the VIC's raster and sprite timing, and the mains frequency the CIAs'
|
|
37
|
+
# TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
|
|
38
|
+
# every region boots the same ROMs. reu plugs an REU of that many K
|
|
39
|
+
# into the expansion port, where it drives the IRQ line and takes the
|
|
40
|
+
# bus for its transfers.
|
|
41
|
+
def initialize(debug: false, reu: nil, **machine)
|
|
39
42
|
@address_bus = AddressBus.new(**machine)
|
|
40
43
|
@cpu = CPU.new(@address_bus, debug:)
|
|
41
44
|
@vic = @address_bus.vic
|
|
@@ -48,11 +51,17 @@ module Badline
|
|
|
48
51
|
@nmi_asserted = false
|
|
49
52
|
@cartridge_nmi = false
|
|
50
53
|
@restore_pulse = false
|
|
54
|
+
@reu_irq = false
|
|
55
|
+
@dma = false
|
|
51
56
|
@freezing = false
|
|
52
57
|
@freeze_writes = 0
|
|
53
58
|
@init_handlers = []
|
|
54
59
|
@pending_keys = nil
|
|
55
60
|
@drive = nil
|
|
61
|
+
@serial_trap = nil
|
|
62
|
+
@drive1541 = nil
|
|
63
|
+
@iec_bus = nil
|
|
64
|
+
@reu = reu ? plug_reu(reu) : nil
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
INIT_THRESHOLD = 2_500_000
|
|
@@ -69,11 +78,12 @@ module Badline
|
|
|
69
78
|
@sid.cycle!
|
|
70
79
|
@datasette.cycle!
|
|
71
80
|
|
|
72
|
-
@cpu.irq = @cia1.interrupted? || @vic.interrupted?
|
|
81
|
+
@cpu.irq = @cia1.interrupted? || @vic.interrupted? || @reu_irq
|
|
73
82
|
|
|
74
83
|
drive_nmi
|
|
75
84
|
watch_freeze if @freezing
|
|
76
|
-
|
|
85
|
+
clock_cpu
|
|
86
|
+
@drive1541&.host_cycle!
|
|
77
87
|
|
|
78
88
|
@cycles += 1
|
|
79
89
|
end
|
|
@@ -91,6 +101,8 @@ module Badline
|
|
|
91
101
|
power_cycle!
|
|
92
102
|
end
|
|
93
103
|
|
|
104
|
+
def reu = address_bus.reu
|
|
105
|
+
|
|
94
106
|
# A cartridge goes in with the power off, so attaching one switches the
|
|
95
107
|
# machine off and on: the VIC and RAM start from their power-on state,
|
|
96
108
|
# and the RES line resets everything else.
|
|
@@ -109,7 +121,10 @@ module Badline
|
|
|
109
121
|
@cia2.reset!
|
|
110
122
|
@sid.reset!
|
|
111
123
|
address_bus.cartridge&.reset
|
|
124
|
+
@reu&.reset!
|
|
125
|
+
@dma = false
|
|
112
126
|
@drive&.reset!
|
|
127
|
+
@drive1541&.reset!
|
|
113
128
|
@freezing = false
|
|
114
129
|
@nmi_asserted = false
|
|
115
130
|
cpu.reset!
|
|
@@ -145,11 +160,44 @@ module Badline
|
|
|
145
160
|
@drive = KernalTrap::Drive.new(storage)
|
|
146
161
|
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
|
|
147
162
|
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
148
|
-
KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
|
|
163
|
+
@serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
|
|
149
164
|
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
|
|
150
165
|
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
151
166
|
end
|
|
152
167
|
|
|
168
|
+
# Takes device 8's mounted storage out, and with it the LOAD, SAVE and
|
|
169
|
+
# serial traps, so the KERNAL's routines go out over the serial bus.
|
|
170
|
+
# Mounting again starts a new drive, with its RAM cleared.
|
|
171
|
+
def unmount
|
|
172
|
+
return unless @drive
|
|
173
|
+
|
|
174
|
+
cpu.remove_trap(KernalTrap::Load::ADDRESS)
|
|
175
|
+
cpu.remove_trap(KernalTrap::Save::ADDRESS)
|
|
176
|
+
@serial_trap.device = nil
|
|
177
|
+
@serial_trap = nil
|
|
178
|
+
@drive = nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Plugs in a Drive1541, which then runs alongside the C64 on its own
|
|
182
|
+
# clock and talks to it over the serial bus. The serial traps stop
|
|
183
|
+
# answering the drive's device number, so the KERNAL's TALK, LISTEN and
|
|
184
|
+
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
185
|
+
# still serve a mounted image.
|
|
186
|
+
def attach_drive1541(drive)
|
|
187
|
+
@iec_bus.detach(@drive1541) if @iec_bus && @drive1541
|
|
188
|
+
drive.host_clock_hz = region.clock_hz
|
|
189
|
+
@drive1541 = drive
|
|
190
|
+
drive.connect(iec_bus)
|
|
191
|
+
@serial_trap&.device = serial_trap_device
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The serial bus. CIA 2's port A joins it the first time it's asked
|
|
195
|
+
# for, which plugging in a drive does. Until then port A's serial
|
|
196
|
+
# inputs float high.
|
|
197
|
+
def iec_bus
|
|
198
|
+
@iec_bus ||= IECBus.new(host: @cia2).tap { |bus| @cia2.peripheral = bus }
|
|
199
|
+
end
|
|
200
|
+
|
|
153
201
|
def capture_output
|
|
154
202
|
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
|
|
155
203
|
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
@@ -170,6 +218,12 @@ module Badline
|
|
|
170
218
|
|
|
171
219
|
private
|
|
172
220
|
|
|
221
|
+
# The device number the serial traps answer: device 8, unless a true
|
|
222
|
+
# drive is on the bus as device 8.
|
|
223
|
+
def serial_trap_device
|
|
224
|
+
@drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
|
|
225
|
+
end
|
|
226
|
+
|
|
173
227
|
# The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
|
|
174
228
|
# key, and the CPU takes an interrupt on its falling edge.
|
|
175
229
|
def drive_nmi
|
|
@@ -179,6 +233,31 @@ module Badline
|
|
|
179
233
|
@nmi_asserted = nmi
|
|
180
234
|
end
|
|
181
235
|
|
|
236
|
+
def plug_reu(size_kb)
|
|
237
|
+
reu = REU.new(size_kb, bus: @address_bus, vic: @vic)
|
|
238
|
+
reu.on_irq_change { |level| @reu_irq = level }
|
|
239
|
+
reu.on_dma { @dma = true }
|
|
240
|
+
@address_bus.attach_reu(reu)
|
|
241
|
+
reu
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# BA halts the CPU on a read cycle, and so does an REU holding the bus
|
|
245
|
+
# for a transfer.
|
|
246
|
+
def clock_cpu
|
|
247
|
+
return dma_cycle! if @dma
|
|
248
|
+
|
|
249
|
+
@cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def dma_cycle!
|
|
253
|
+
writing = @cpu.pending_write?
|
|
254
|
+
@reu.dma_cycle!(@vic.reu_ba_low?, writing)
|
|
255
|
+
return @cpu.stall! if @reu.holds_bus?
|
|
256
|
+
|
|
257
|
+
@dma = @reu.dma?
|
|
258
|
+
writing || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
259
|
+
end
|
|
260
|
+
|
|
182
261
|
# A freezer counts the CPU's write cycles once it pulls NMI and switches
|
|
183
262
|
# to Ultimax on the third in a row, the last push of the interrupt
|
|
184
263
|
# sequence, so the vector fetch that follows reads the cartridge.
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -40,6 +40,7 @@ module Badline
|
|
|
40
40
|
@brk = false
|
|
41
41
|
@pending_write = false
|
|
42
42
|
@stalled_at = nil
|
|
43
|
+
@so_high = true
|
|
43
44
|
|
|
44
45
|
@cycles = 0
|
|
45
46
|
@instructions = 0
|
|
@@ -88,6 +89,20 @@ module Badline
|
|
|
88
89
|
sample_while_stalled
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
# The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
|
|
93
|
+
# C64 never drives it. A falling edge sets V and a held level does
|
|
94
|
+
# nothing more.
|
|
95
|
+
def so=(high)
|
|
96
|
+
@status.overflow = true if @so_high && !high
|
|
97
|
+
@so_high = high
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Pulses SO: pulls it low, which sets V, and releases it.
|
|
101
|
+
def so!
|
|
102
|
+
@status.overflow = true
|
|
103
|
+
@so_high = true
|
|
104
|
+
end
|
|
105
|
+
|
|
91
106
|
def jammed?
|
|
92
107
|
@plan.equal?(JAMMED_PLAN)
|
|
93
108
|
end
|
|
@@ -99,6 +114,30 @@ module Badline
|
|
|
99
114
|
nil
|
|
100
115
|
end
|
|
101
116
|
|
|
117
|
+
# Whether the next cycle fetches an opcode.
|
|
118
|
+
def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
|
|
119
|
+
|
|
120
|
+
def trapped? = !@traps.nil?
|
|
121
|
+
|
|
122
|
+
# Everything the CPU holds but its cycle and instruction counts: the
|
|
123
|
+
# registers, the interrupt lines and pipeline, and what the last
|
|
124
|
+
# instruction left in its working registers. Drive1541::Idle compares
|
|
125
|
+
# it at two instruction boundaries.
|
|
126
|
+
def idle_state
|
|
127
|
+
[@program_counter, @stack_pointer, @a, @x, @y, @status.value,
|
|
128
|
+
@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
|
|
129
|
+
@boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
|
|
130
|
+
@operation, @optional_dummy, @boundary_crossed, @branch_taken,
|
|
131
|
+
@address, @pointer, @dummy_address, @value, @rmw_result]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Counts +cycles+ cycles and +instructions+ instructions that ran
|
|
135
|
+
# without changing anything idle_state holds.
|
|
136
|
+
def fast_forward(cycles, instructions)
|
|
137
|
+
@cycles += cycles
|
|
138
|
+
@instructions += instructions
|
|
139
|
+
end
|
|
140
|
+
|
|
102
141
|
def inspect
|
|
103
142
|
"Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
|
|
104
143
|
"SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# The drive CPU's bus. The decoder looks at A15, A12, A11 and A10 only:
|
|
6
|
+
#
|
|
7
|
+
# $0000-$07FF - 2 KB of RAM
|
|
8
|
+
# $0800-$17FF - nothing: open bus
|
|
9
|
+
# $1800-$1BFF - VIA 1, its sixteen registers mirrored
|
|
10
|
+
# $1C00-$1FFF - VIA 2, its sixteen registers mirrored
|
|
11
|
+
# $2000-$7FFF - $0000-$1FFF again, since A13 and A14 aren't decoded
|
|
12
|
+
# $8000-$BFFF - the ROM again, since A14 isn't decoded
|
|
13
|
+
# $C000-$FFFF - the DOS ROM
|
|
14
|
+
#
|
|
15
|
+
# Writes to the ROM and to open bus go nowhere. A read from open bus
|
|
16
|
+
# finds the last byte on the data bus, which for LDA abs or LDA (zp),Y
|
|
17
|
+
# is the address's high byte, and for an indexed read that crosses a
|
|
18
|
+
# page is what its dummy read found. VICE-testprogs drive/openbus pins
|
|
19
|
+
# both on a real 1541.
|
|
20
|
+
#
|
|
21
|
+
# While Idle records a pass of the DOS's idle loop, the bus watches it
|
|
22
|
+
# (see watch!).
|
|
23
|
+
class Bus
|
|
24
|
+
include Addressable
|
|
25
|
+
|
|
26
|
+
# VIA registers, as bits by offset, whose access makes a watched
|
|
27
|
+
# stretch volatile: reads of either VIA's counters and shift register
|
|
28
|
+
# and of VIA 1's port B, and writes to VIA 2's timers, shift register
|
|
29
|
+
# and ACR. Any write to VIA 1 does too (watch_write).
|
|
30
|
+
VOLATILE_VIA1_READS = 0x0731
|
|
31
|
+
VOLATILE_VIA2_READS = 0x0730
|
|
32
|
+
VOLATILE_VIA2_WRITES = 0x0ff0
|
|
33
|
+
|
|
34
|
+
attr_reader :ram, :rom, :via1, :via2, :data
|
|
35
|
+
|
|
36
|
+
# Whether a watched stretch did something that makes it unrepeatable.
|
|
37
|
+
attr_reader :volatile
|
|
38
|
+
|
|
39
|
+
# The RAM a watched stretch read or wrote, as each address's value
|
|
40
|
+
# from before the first access.
|
|
41
|
+
attr_reader :touched
|
|
42
|
+
|
|
43
|
+
def initialize(rom:, via1:, via2:)
|
|
44
|
+
addressable_at(0, length: 2**16)
|
|
45
|
+
@ram = Memory.new([], length: 0x0800, start: 0)
|
|
46
|
+
@rom = rom
|
|
47
|
+
@via1 = via1
|
|
48
|
+
@via2 = via2
|
|
49
|
+
@data = 0
|
|
50
|
+
@watching = false
|
|
51
|
+
@volatile = false
|
|
52
|
+
@touched = {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Starts watching: see volatile and touched. The bus stops watching
|
|
56
|
+
# once the stretch turns volatile.
|
|
57
|
+
def watch!
|
|
58
|
+
@watching = true
|
|
59
|
+
@volatile = false
|
|
60
|
+
@touched = {}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unwatch!
|
|
64
|
+
@watching = false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def peek(addr)
|
|
68
|
+
return @data = @rom.peek(addr | 0x4000) if addr >= 0x8000
|
|
69
|
+
|
|
70
|
+
addr &= 0x1fff
|
|
71
|
+
watch_read(addr) if @watching
|
|
72
|
+
@data = if addr < 0x0800
|
|
73
|
+
@ram.peek(addr)
|
|
74
|
+
elsif addr < 0x1800
|
|
75
|
+
@data
|
|
76
|
+
elsif addr < 0x1c00
|
|
77
|
+
@via1.peek(addr)
|
|
78
|
+
else
|
|
79
|
+
@via2.peek(addr)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def poke(addr, value)
|
|
84
|
+
@data = value
|
|
85
|
+
return if addr >= 0x8000
|
|
86
|
+
|
|
87
|
+
addr &= 0x1fff
|
|
88
|
+
if addr < 0x0800
|
|
89
|
+
touch(addr) if @watching
|
|
90
|
+
@ram.poke(addr, value)
|
|
91
|
+
elsif addr >= 0x1c00
|
|
92
|
+
@via2.poke(addr, value)
|
|
93
|
+
elsif addr >= 0x1800
|
|
94
|
+
@via1.poke(addr, value)
|
|
95
|
+
end
|
|
96
|
+
watch_write(addr) if @watching
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def touch(addr)
|
|
102
|
+
@touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def watch_read(addr)
|
|
106
|
+
if addr < 0x0800
|
|
107
|
+
touch(addr)
|
|
108
|
+
elsif addr >= 0x1800
|
|
109
|
+
watch(addr < 0x1c00 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Any write to VIA 1 makes the stretch volatile, and so does a write
|
|
114
|
+
# to VIA 2 that leaves the motor on.
|
|
115
|
+
def watch_write(addr)
|
|
116
|
+
if addr >= 0x1c00
|
|
117
|
+
watch(VOLATILE_VIA2_WRITES, addr)
|
|
118
|
+
volatile! if @watching && @via2.port_b_output.anybits?(Mechanism::MOTOR)
|
|
119
|
+
elsif addr >= 0x1800
|
|
120
|
+
volatile!
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def watch(volatile, addr)
|
|
125
|
+
volatile! if volatile.anybits?(1 << (addr & 0x0f))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def volatile!
|
|
129
|
+
@volatile = true
|
|
130
|
+
@watching = false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|