badline 0.11.0 → 0.11.1

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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/CLAUDE.md +20 -3
  4. data/CONTRIBUTING.md +8 -3
  5. data/README.md +6 -2
  6. data/lib/badline/c128/drives.rb +95 -0
  7. data/lib/badline/c128/fast_serial.rb +62 -0
  8. data/lib/badline/c128/mmu.rb +15 -0
  9. data/lib/badline/c128/saved_state.rb +1 -0
  10. data/lib/badline/c128.rb +9 -7
  11. data/lib/badline/cia/fast_forward.rb +42 -0
  12. data/lib/badline/cia/serial.rb +12 -0
  13. data/lib/badline/cia/timer.rb +39 -0
  14. data/lib/badline/cia.rb +2 -0
  15. data/lib/badline/computer/attachments.rb +6 -0
  16. data/lib/badline/drive/core.rb +172 -0
  17. data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
  18. data/lib/badline/drive/idle_cpu.rb +28 -0
  19. data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
  20. data/lib/badline/drive/serial_lines.rb +73 -0
  21. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
  22. data/lib/badline/drive/watch.rb +150 -0
  23. data/lib/badline/drive.rb +9 -0
  24. data/lib/badline/drive1541/bus.rb +5 -129
  25. data/lib/badline/drive1541/byte_ready.rb +48 -0
  26. data/lib/badline/drive1541/cpu.rb +1 -19
  27. data/lib/badline/drive1541/disk.rb +77 -20
  28. data/lib/badline/drive1541/disk_state.rb +7 -2
  29. data/lib/badline/drive1541/disk_via.rb +3 -0
  30. data/lib/badline/drive1541/mechanism.rb +48 -14
  31. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  32. data/lib/badline/drive1541/rotation.rb +5 -5
  33. data/lib/badline/drive1541/serial_port.rb +4 -60
  34. data/lib/badline/drive1541.rb +13 -172
  35. data/lib/badline/drive1571/bus.rb +111 -0
  36. data/lib/badline/drive1571/cpu.rb +11 -0
  37. data/lib/badline/drive1571/serial_port.rb +37 -0
  38. data/lib/badline/drive1571/serial_via.rb +25 -0
  39. data/lib/badline/drive1571/wd1770.rb +34 -0
  40. data/lib/badline/drive1571.rb +225 -0
  41. data/lib/badline/frontend/app.rb +1 -1
  42. data/lib/badline/frontend/drive_led.rb +1 -1
  43. data/lib/badline/frontend/menu_media.rb +3 -3
  44. data/lib/badline/frontend/menu_pages.rb +2 -1
  45. data/lib/badline/iec_bus.rb +54 -4
  46. data/lib/badline/machine.rb +7 -5
  47. data/lib/badline/media/true_drive.rb +26 -23
  48. data/lib/badline/options/help.rb +2 -1
  49. data/lib/badline/options/table.rb +1 -1
  50. data/lib/badline/roms/README +9 -0
  51. data/lib/badline/roms/dos1571.rom +0 -0
  52. data/lib/badline/storage/d64_image.rb +3 -0
  53. data/lib/badline/storage/d71_image.rb +9 -4
  54. data/lib/badline/storage/g64_image.rb +7 -2
  55. data/lib/badline/time_of_day.rb +9 -0
  56. data/lib/badline/version.rb +1 -1
  57. data/lib/badline/via/fast_forward.rb +3 -3
  58. data/lib/badline/vic20/attachments.rb +6 -0
  59. data/lib/badline/z80/arithmetic.rb +117 -0
  60. data/lib/badline/z80/bit_page.rb +55 -0
  61. data/lib/badline/z80/block_io.rb +58 -0
  62. data/lib/badline/z80/block_transfers.rb +59 -0
  63. data/lib/badline/z80/bus_cycles.rb +111 -0
  64. data/lib/badline/z80/core.rb +111 -0
  65. data/lib/badline/z80/exchanges.rb +50 -0
  66. data/lib/badline/z80/extended_page.rb +89 -0
  67. data/lib/badline/z80/flags.rb +37 -0
  68. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  69. data/lib/badline/z80/jumps.rb +82 -0
  70. data/lib/badline/z80/loads.rb +98 -0
  71. data/lib/badline/z80/main_page.rb +134 -0
  72. data/lib/badline/z80/registers.rb +120 -0
  73. data/lib/badline/z80/shifts.rb +64 -0
  74. data/lib/badline/z80.rb +11 -0
  75. metadata +36 -4
@@ -3,72 +3,16 @@
3
3
  module Badline
4
4
  class Drive1541
5
5
  # What VIA 1's ports read from outside the chip. Port B carries the
6
- # serial bus inputs and the device number jumpers; port A goes to the
7
- # unused parallel header and floats high.
8
- #
9
- # DATA IN (PB0), CLK IN (PB2) and ATN IN (PB7) come through inverters,
10
- # so a released line reads 0 and a pulled one 1. The jumpers on PB5 and
11
- # PB6 pull a line low when closed, and both closed make device 8.
12
- #
13
- # The drive sees the C64's side of the bus a host cycle late. The C64's
14
- # CIA changes its pins at the end of the cycle that writes them, and
15
- # the drive's VIA samples its own late in each of its cycles, so a
16
- # drive cycle that runs alongside the write reads the lines from
17
- # before it. The drive runs after the C64 in each host cycle (see
18
- # Computer#cycle!), so the port reads the C64's lines as latch_host
19
- # took them at the end of the last one. Fast loaders that time their
20
- # transfer from an ATN edge, as VICE's drive tests do, depend on it.
6
+ # serial bus inputs and the device number jumpers (Drive::SerialLines);
7
+ # port A goes to the unused parallel header and floats high.
21
8
  class SerialPort
22
- DATA_IN = 0x01
23
- CLK_IN = 0x04
24
- ATN_IN = 0x80
25
- SERIAL_INPUTS = DATA_IN | CLK_IN | ATN_IN
26
- JUMPERS = 0b0110_0000
27
-
28
- attr_reader :bus
9
+ include Drive::SerialLines
29
10
 
30
11
  def initialize(device: 8, bus: IECBus.new)
31
- @port_b = (0xff & ~(SERIAL_INPUTS | JUMPERS)) | (((device - 8) & 0x03) << 5)
32
- @host = 0
33
- self.bus = bus
34
- end
35
-
36
- def bus=(bus)
37
- @bus = bus
38
- latch_host
39
- end
40
-
41
- # Takes the C64's lines as the machine last pushed them into the bus,
42
- # for the drive cycles of the next host cycle.
43
- def latch_host
44
- @host = @bus.host_lines
45
- end
46
-
47
- # The C64's lines as last latched. The jumpers and the bus are the
48
- # drive's wiring.
49
- def save_state(out)
50
- out.int(@host)
12
+ init_serial_lines(device, bus)
51
13
  end
52
14
 
53
- def load_state(input)
54
- @host = input.int
55
- end
56
-
57
- # Whether the C64 has moved ATN since the last latch_host.
58
- def atn_moved? = (@bus.host_lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
59
-
60
- def atn_low? = @bus.atn_low?(@host)
61
-
62
15
  def read_a(_lines) = 0xff
63
-
64
- def read_b(_lines)
65
- low = @bus.low_lines(@host)
66
- value = @port_b
67
- value |= DATA_IN if low.anybits?(IECBus::DATA)
68
- value |= CLK_IN if low.anybits?(IECBus::CLK)
69
- value |= ATN_IN if low.anybits?(IECBus::ATN)
70
- value
71
- end
72
16
  end
73
17
  end
74
18
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/drive"
3
4
  require "badline/drive1541/bus"
4
5
  require "badline/drive1541/cpu"
5
6
  require "badline/drive1541/serial_port"
@@ -8,75 +9,20 @@ require "badline/drive1541/track"
8
9
  require "badline/drive1541/disk"
9
10
  require "badline/drive1541/mechanism"
10
11
  require "badline/drive1541/disk_via"
11
- require "badline/drive1541/sleep"
12
- require "badline/drive1541/idle"
13
- require "badline/drive1541/orbit"
14
12
 
15
13
  module Badline
16
14
  # The 1541 disk drive as a machine of its own: a 6502 running the DOS
17
- # ROM at 1 MHz, with 2 KB of RAM and two VIAs on its bus. VIA 1 faces
18
- # the serial bus and VIA 2 the disk mechanism.
19
- #
20
- # The drive runs on its own crystal, so it clocks against the host's
21
- # clock through a fractional accumulator: each host cycle adds 1 MHz
22
- # worth of phase, and every whole host period in it runs a drive cycle.
23
- # Against the PAL C64's 985,248 Hz that's one drive cycle per host cycle
24
- # and a second one about every 67. The host sets its clock on attaching
25
- # the drive, and until then the drive runs one cycle per host cycle.
26
- #
27
- # VIA 2's port B runs the Mechanism, which reads a Disk put in with
28
- # insert.
29
- #
30
- # While the DOS idles, host_cycle! skips the drive's cycles and catches
31
- # up on them later, exactly (see Idle). The readers of the drive's parts
32
- # catch up first, so they find the drive as running every cycle would
33
- # have left it. Once the DOS's timer interrupts come round, the drive
34
- # sleeps through them too (see Orbit).
15
+ # ROM at 1 MHz, with 2 KB of RAM and two VIAs on its bus. Drive::Core
16
+ # clocks it against the host and sleeps through the DOS's idle loop.
35
17
  class Drive1541
36
- include Sleep
37
- include Idle
38
- include Orbit
18
+ include Drive::Core
39
19
 
40
20
  CLOCK_HZ = 1_000_000
41
21
 
42
- attr_reader :device, :serial_bus
22
+ # Where the DOS 2.6 idle loop starts over (see Drive::Idle).
23
+ IDLE_LOOP = 0xebff
43
24
 
44
- def cpu
45
- settle!
46
- @cpu
47
- end
48
-
49
- def bus
50
- settle!
51
- @bus
52
- end
53
-
54
- def via1
55
- settle!
56
- @via1
57
- end
58
-
59
- def via2
60
- settle!
61
- @via2
62
- end
63
-
64
- def mechanism
65
- settle!
66
- @mechanism
67
- end
68
-
69
- def cycles
70
- settle!
71
- @cycles
72
- end
73
-
74
- def ram = bus.ram
75
-
76
- def host_clock_hz=(clock_hz)
77
- settle!
78
- @host_clock_hz = clock_hz
79
- end
25
+ def model_name = "1541"
80
26
 
81
27
  # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
82
28
  # path. +device+ is the number the jumpers on VIA 1's PB5 and PB6 set,
@@ -85,14 +31,15 @@ module Badline
85
31
  @device = device
86
32
  @serial_port = SerialPort.new(device:)
87
33
  @via1 = VIA.new(start: 0x1800, peripheral: @serial_port)
88
- @mechanism = Mechanism.new(self)
34
+ @mechanism = Mechanism.new
89
35
  @via2 = DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
90
36
  @bus = Bus.new(rom: rom || ROM.load("dos1541.rom", 0xc000), via1: @via1, via2: @via2)
91
37
  @cpu = CPU.new(@bus, debug:)
38
+ @clock_hz = CLOCK_HZ
39
+ @idle_loop = IDLE_LOOP
92
40
  @host_clock_hz = host_clock_hz
93
41
  @phase = 0
94
42
  @cycles = 0
95
- @so_pending = false
96
43
  @serial_bus = nil
97
44
  init_idle(debug)
98
45
  # CA1 powers up at the level of a released ATN, without an edge.
@@ -101,86 +48,6 @@ module Badline
101
48
  connect(IECBus.new)
102
49
  end
103
50
 
104
- # Plugs the drive into a serial bus, leaving the one it was on. A drive
105
- # starts out on a bus of its own, with nothing else on it.
106
- def connect(serial_bus)
107
- settle!
108
- @serial_bus&.detach(self)
109
- @serial_bus = serial_bus
110
- @serial_port.bus = serial_bus
111
- serial_bus.attach(self)
112
- @via1.ca1 = @serial_port.atn_low?
113
- end
114
-
115
- # Puts a Disk in the drive (Disk.from_d64 makes one from an image).
116
- # Nil takes the disk out.
117
- def insert(disk)
118
- settle!
119
- @mechanism.insert(disk)
120
- end
121
-
122
- def disk = mechanism.disk
123
-
124
- # Whether the LED is lit. Reading it leaves the drive asleep: the LED
125
- # is the same at the end of every pass the drive sleeps through.
126
- def led_on? = @mechanism.led_on?
127
-
128
- # VIA 1's port B as it drives the serial bus. It holds still while the
129
- # drive sleeps, so reading it leaves the drive asleep.
130
- def serial_output = @via1.port_b_output
131
-
132
- # Stores what the head wrote since the motor last stopped in the
133
- # disk's image, as the motor stopping does.
134
- def flush
135
- settle!
136
- @mechanism.flush
137
- end
138
-
139
- # The serial bus's RESET line reaches the CPU and both VIAs. RAM keeps
140
- # its contents.
141
- def reset!
142
- settle!
143
- @via1.reset!
144
- @via2.reset!
145
- @cpu.reset!
146
- end
147
-
148
- # Runs the drive cycles that fall in one host cycle: none, one or two.
149
- # The host has run this cycle already, and the drive sees what it did
150
- # from the next one on (see SerialPort). Asleep, the drive owes the
151
- # cycles instead, until ATN moves or its counters are due (see Idle).
152
- # It looks at ATN only after the machine pushed its lines into the bus
153
- # (see host_written!).
154
- def host_cycle!
155
- if @asleep
156
- return if (@slept += 1) != @wake_at && @host_still
157
-
158
- return doze
159
- end
160
-
161
- phase = @phase + CLOCK_HZ
162
- while phase >= @host_clock_hz
163
- @asleep ? @owed += 1 : run_cycle
164
- phase -= @host_clock_hz
165
- end
166
- @phase = phase
167
- settle! if @serial_port.atn_moved?
168
- @serial_port.latch_host
169
- plan_wake if @asleep
170
- end
171
-
172
- # The machine pushed its lines into the bus, so ATN may have moved.
173
- def host_written!
174
- @host_still = false
175
- end
176
-
177
- # Runs one drive cycle, catching up first on any the drive owes (see
178
- # step).
179
- def cycle!
180
- settle!
181
- step
182
- end
183
-
184
51
  # The drive's whole state: its clock phase, the CPU, RAM, both VIAs,
185
52
  # the serial port's latch of the C64's lines and the mechanism with its
186
53
  # disk. The drive settles first, so it is awake and not recording a
@@ -191,7 +58,7 @@ module Badline
191
58
  def save_state(out)
192
59
  settle!
193
60
  out.marker("DRIVE1541")
194
- out.int(@phase).int(@cycles).boolean(@so_pending)
61
+ out.int(@phase).int(@cycles).boolean(@mechanism.so_pending)
195
62
  @serial_port.save_state(out)
196
63
  @via1.save_state(out)
197
64
  @via2.save_state(out)
@@ -205,7 +72,7 @@ module Badline
205
72
  input.marker("DRIVE1541")
206
73
  @phase = input.int
207
74
  @cycles = input.int
208
- @so_pending = input.boolean?
75
+ @mechanism.so_pending = input.boolean?
209
76
  @serial_port.load_state(input)
210
77
  @via1.load_state(input)
211
78
  @via2.load_state(input)
@@ -215,33 +82,8 @@ module Badline
215
82
  forget_orbits
216
83
  end
217
84
 
218
- def inspect
219
- "#<#{self.class.name} cycles=#{cycles} cpu=(#{cpu.inspect})>"
220
- end
221
-
222
- # The read electronics signal a whole GCR byte. BYTE READY pulls VIA 2's
223
- # CA1 low, a falling edge that sets its flag and, with latching on,
224
- # latches port A. It reaches the CPU's SO pin while VIA 2's CA2 (SOE)
225
- # is high, which lets the DOS spin on BVC for each byte. The CPU
226
- # samples SO on the next cycle (see step).
227
- def byte_ready!
228
- @via2.ca1 = false
229
- @so_pending = true if @via2.ca2_output
230
- end
231
-
232
- # BYTE READY lets go of CA1 with the next bit.
233
- def byte_ready_ended!
234
- @via2.ca1 = true
235
- end
236
-
237
85
  private
238
86
 
239
- # A drive cycle from host_cycle!, which may find the idle loop.
240
- def run_cycle
241
- step
242
- idle_loop_reached if @cpu.program_counter == IDLE_LOOP && @idle_skip
243
- end
244
-
245
87
  # One drive cycle. The VIAs clock ahead of the CPU, as the C64's chips
246
88
  # do, and either one pulls IRQ. Nothing drives NMI on the 1541. The CPU
247
89
  # runs whether or not the motor turns, since it answers ATN.
@@ -254,8 +96,7 @@ module Badline
254
96
  # high as the C64 asserts ATN, as the serial port sees it: from the
255
97
  # host cycle after the one that asserts it.
256
98
  def step
257
- so = @so_pending
258
- @so_pending = false
99
+ so = @mechanism.take_so
259
100
  @mechanism.cycle!
260
101
  @via1.ca1 = @serial_port.atn_low?
261
102
  @via1.cycle!
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # The drive CPU's bus:
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-$3FFF - the WD1770, its four registers mirrored
12
+ # $4000-$7FFF - the CIA, its sixteen registers mirrored
13
+ # $8000-$FFFF - the DOS ROM
14
+ #
15
+ # Writes to the ROM and to open bus go nowhere, and a read from open
16
+ # bus finds the last byte on the data bus, as on the 1541. Any access
17
+ # to VIA 2 clears the BYTE READY latch VIA 1's PA7 reads (SerialPort).
18
+ #
19
+ # While Idle records a pass of the DOS's idle loop, the bus watches it,
20
+ # and while Idle looks for an orbit, it guards the drive (see
21
+ # Drive::Watch). Any access to the CIA or the WD1770 makes the pass
22
+ # volatile and taints the drive.
23
+ class Bus
24
+ include Addressable
25
+ include Drive::Watch
26
+
27
+ attr_reader :ram, :rom, :via1, :via2, :cia, :fdc, :data
28
+
29
+ def initialize(rom:, via1:, via2:, cia:, fdc:)
30
+ addressable_at(0, length: 2**16)
31
+ @ram = Memory.new([], length: 0x0800, start: 0)
32
+ @rom = rom
33
+ @via1 = via1
34
+ @via2 = via2
35
+ @cia = cia
36
+ @fdc = fdc
37
+ @mechanism = via2.mechanism
38
+ @data = 0
39
+ init_watch
40
+ end
41
+
42
+ # The last byte on the data bus and the RAM. What a watched stretch
43
+ # touched is Idle's, and a restored bus isn't watching.
44
+ def save_state(out)
45
+ out.int(@data)
46
+ @ram.save_state(out)
47
+ end
48
+
49
+ def load_state(input)
50
+ @data = input.int
51
+ @ram.load_state(input)
52
+ @watching = false
53
+ end
54
+
55
+ def peek(addr)
56
+ return @data = @rom.peek(addr) if addr >= 0x8000
57
+ return @data = chip_peek(addr) if addr >= 0x2000
58
+
59
+ watch_read(addr) if @watching
60
+ @data = if addr < 0x0800
61
+ @ram.peek(addr)
62
+ elsif addr < 0x1800
63
+ @data
64
+ elsif addr < 0x1c00
65
+ @via1.peek(addr)
66
+ else
67
+ @mechanism.release_byte_latch
68
+ @via2.peek(addr)
69
+ end
70
+ end
71
+
72
+ def poke(addr, value)
73
+ @data = value
74
+ return if addr >= 0x8000
75
+ return chip_poke(addr, value) if addr >= 0x2000
76
+
77
+ low_poke(addr, value)
78
+ end
79
+
80
+ private
81
+
82
+ # A write below $2000: RAM or a VIA.
83
+ def low_poke(addr, value)
84
+ if addr < 0x0800
85
+ touch(addr) if @recording
86
+ @ram.poke(addr, value)
87
+ elsif addr >= 0x1c00
88
+ @mechanism.release_byte_latch
89
+ @via2.poke(addr, value)
90
+ elsif addr >= 0x1800
91
+ @via1.poke(addr, value)
92
+ end
93
+ watch_write(addr) if @watching
94
+ end
95
+
96
+ def chip_peek(addr)
97
+ watch_chip if @watching
98
+ addr >= 0x4000 ? @cia.peek(0x4000 | (addr & 0x0f)) : @fdc.peek(addr & 0x03)
99
+ end
100
+
101
+ def chip_poke(addr, value)
102
+ if addr >= 0x4000
103
+ @cia.poke(0x4000 | (addr & 0x0f), value)
104
+ else
105
+ @fdc.poke(addr & 0x03, value)
106
+ end
107
+ watch_chip if @watching
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # The drive's 6502, on the drive's Bus.
6
+ class CPU
7
+ include Badline::CPU::Core
8
+ include Drive::IdleCPU
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # What VIA 1's ports read from outside the chip. Port B carries the
6
+ # serial bus inputs and the device number jumpers, as on the 1541
7
+ # (Drive::SerialLines). Port A reads the mechanism on two inputs:
8
+ #
9
+ # PA0 track 0 sensor, low while the head sits on track 1
10
+ # PA7 BYTE READY, latched low from a whole byte until the CPU next
11
+ # reads or writes VIA 2
12
+ #
13
+ # The DOS steps the head out until PA0 reads low with the stepper on
14
+ # phase 0, and polls PA7 for each byte at 2 MHz, taking the byte from
15
+ # VIA 2's port A or putting the next one there after each. PA1 (fast serial
16
+ # direction), PA2 (side) and PA5 (1 or 2 MHz) are outputs, which
17
+ # Drive1571 follows (SerialVIA).
18
+ class SerialPort
19
+ include Drive::SerialLines
20
+
21
+ TRACK0 = 0x01
22
+ BYTE_READY = 0x80
23
+
24
+ def initialize(mechanism, device: 8, bus: IECBus.new)
25
+ @mechanism = mechanism
26
+ init_serial_lines(device, bus)
27
+ end
28
+
29
+ def read_a(_lines)
30
+ value = 0xff
31
+ value &= ~TRACK0 if @mechanism.half_track == Drive1541::Mechanism::MIN_HALF_TRACK
32
+ value &= ~BYTE_READY if @mechanism.byte_latched?
33
+ value
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # VIA 1, the one facing the serial bus. Its port A also selects the
6
+ # side, the clock rate and the fast serial direction, so the drive
7
+ # hears port A after every write and reset (Drive1571#port_a_written).
8
+ class SerialVIA < VIA
9
+ def initialize(start:, peripheral:, drive:)
10
+ @drive = drive
11
+ super(start:, peripheral:)
12
+ end
13
+
14
+ def reset!
15
+ super
16
+ @drive.port_a_written(port_a_output)
17
+ end
18
+
19
+ def poke(addr, value)
20
+ super
21
+ @drive.port_a_written(port_a_output)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1571
5
+ # The WD1770 floppy controller at $2000, which the DOS needs only for
6
+ # MFM disks. Its four registers hold what's written to them and it
7
+ # runs no commands: the status reads idle, with the motor off.
8
+ #
9
+ # $2000 status (read), command (write)
10
+ # $2001 track
11
+ # $2002 sector
12
+ # $2003 data
13
+ class WD1770
14
+ def initialize
15
+ @registers = [0, 0, 0, 0]
16
+ end
17
+
18
+ # Register +offset+, 0 to 3.
19
+ def peek(offset) = offset.zero? ? 0 : @registers[offset]
20
+
21
+ def poke(offset, value)
22
+ @registers[offset] = value
23
+ end
24
+
25
+ def save_state(out)
26
+ out.ints(@registers)
27
+ end
28
+
29
+ def load_state(input)
30
+ @registers = input.ints
31
+ end
32
+ end
33
+ end
34
+ end