badline 0.10.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +25 -6
- data/CONTRIBUTING.md +8 -3
- data/README.md +63 -4
- data/doc/pinned-behaviour.md +75 -0
- data/doc/snapshots.md +15 -0
- data/lib/badline/address_bus/pla.rb +3 -16
- data/lib/badline/address_bus.rb +14 -0
- data/lib/badline/c128/banks.rb +35 -0
- data/lib/badline/c128/bus.rb +347 -0
- data/lib/badline/c128/bus_state.rb +53 -0
- data/lib/badline/c128/color_lines.rb +39 -0
- data/lib/badline/c128/cpu.rb +10 -0
- data/lib/badline/c128/drives.rb +95 -0
- data/lib/badline/c128/fast_serial.rb +62 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +197 -0
- data/lib/badline/c128/mmu_pages.rb +105 -0
- data/lib/badline/c128/model.rb +39 -0
- data/lib/badline/c128/modes.rb +48 -0
- data/lib/badline/c128/saved_state.rb +121 -0
- data/lib/badline/c128/vdc/memory.rb +54 -0
- data/lib/badline/c128/vdc/painter.rb +143 -0
- data/lib/badline/c128/vdc/raster.rb +97 -0
- data/lib/badline/c128/vdc/window.rb +215 -0
- data/lib/badline/c128/vdc.rb +268 -0
- data/lib/badline/c128.rb +322 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/cia/fast_forward.rb +42 -0
- data/lib/badline/cia/serial.rb +12 -0
- data/lib/badline/cia/timer.rb +39 -0
- data/lib/badline/cia.rb +2 -0
- data/lib/badline/computer/attachments.rb +10 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +5 -0
- data/lib/badline/drive/core.rb +172 -0
- data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
- data/lib/badline/drive/idle_cpu.rb +28 -0
- data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
- data/lib/badline/drive/serial_lines.rb +73 -0
- data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
- data/lib/badline/drive/watch.rb +150 -0
- data/lib/badline/drive.rb +9 -0
- data/lib/badline/drive1541/bus.rb +5 -129
- data/lib/badline/drive1541/byte_ready.rb +48 -0
- data/lib/badline/drive1541/cpu.rb +1 -19
- data/lib/badline/drive1541/disk.rb +77 -20
- data/lib/badline/drive1541/disk_state.rb +7 -2
- data/lib/badline/drive1541/disk_via.rb +3 -0
- data/lib/badline/drive1541/mechanism.rb +48 -14
- data/lib/badline/drive1541/mechanism_state.rb +5 -4
- data/lib/badline/drive1541/rotation.rb +5 -5
- data/lib/badline/drive1541/serial_port.rb +4 -60
- data/lib/badline/drive1541.rb +13 -172
- data/lib/badline/drive1571/bus.rb +111 -0
- data/lib/badline/drive1571/cpu.rb +11 -0
- data/lib/badline/drive1571/serial_port.rb +37 -0
- data/lib/badline/drive1571/serial_via.rb +25 -0
- data/lib/badline/drive1571/wd1770.rb +34 -0
- data/lib/badline/drive1571.rb +225 -0
- data/lib/badline/frontend/app.rb +35 -22
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/drive_led.rb +1 -1
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_media.rb +3 -3
- data/lib/badline/frontend/menu_pages.rb +8 -2
- data/lib/badline/frontend/menu_ports.rb +1 -1
- data/lib/badline/frontend/pause_menu.rb +1 -0
- data/lib/badline/frontend/screen.rb +33 -8
- data/lib/badline/frontend/screens.rb +90 -0
- data/lib/badline/frontend/timeline.rb +15 -4
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/iec_bus.rb +54 -4
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +58 -8
- data/lib/badline/kernal_trap/load.rb +1 -1
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +5 -0
- data/lib/badline/machine.rb +20 -9
- data/lib/badline/media/true_drive.rb +26 -23
- data/lib/badline/media.rb +8 -6
- data/lib/badline/options/event.rb +5 -1
- data/lib/badline/options/family.rb +52 -0
- data/lib/badline/options/help.rb +7 -2
- data/lib/badline/options/table.rb +8 -2
- data/lib/badline/options/validation.rb +14 -1
- data/lib/badline/options.rb +20 -19
- data/lib/badline/roms/README +36 -0
- data/lib/badline/roms/c128/basic.rom +0 -0
- data/lib/badline/roms/c128/character.rom +0 -0
- data/lib/badline/roms/c128/kernal.rom +0 -0
- data/lib/badline/roms/dos1571.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +14 -0
- data/lib/badline/snapshot.rb +9 -2
- data/lib/badline/storage/d64_image.rb +3 -0
- data/lib/badline/storage/d71_image.rb +9 -4
- data/lib/badline/storage/g64_image.rb +7 -2
- data/lib/badline/time_of_day.rb +9 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/fast_forward.rb +3 -3
- data/lib/badline/vic/display_state.rb +12 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic.rb +108 -7
- data/lib/badline/vic20/attachments.rb +6 -0
- data/lib/badline/z80/arithmetic.rb +117 -0
- data/lib/badline/z80/bit_page.rb +55 -0
- data/lib/badline/z80/block_io.rb +58 -0
- data/lib/badline/z80/block_transfers.rb +59 -0
- data/lib/badline/z80/bus_cycles.rb +111 -0
- data/lib/badline/z80/core.rb +111 -0
- data/lib/badline/z80/exchanges.rb +50 -0
- data/lib/badline/z80/extended_page.rb +89 -0
- data/lib/badline/z80/flags.rb +37 -0
- data/lib/badline/z80/interrupt_acceptance.rb +46 -0
- data/lib/badline/z80/jumps.rb +82 -0
- data/lib/badline/z80/loads.rb +98 -0
- data/lib/badline/z80/main_page.rb +134 -0
- data/lib/badline/z80/registers.rb +120 -0
- data/lib/badline/z80/shifts.rb +64 -0
- data/lib/badline/z80.rb +11 -0
- data/lib/badline.rb +1 -0
- metadata +59 -4
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/rotation"
|
|
4
4
|
require "badline/drive1541/mechanism_state"
|
|
5
|
+
require "badline/drive1541/byte_ready"
|
|
5
6
|
|
|
6
7
|
module Badline
|
|
7
8
|
class Drive1541
|
|
@@ -26,7 +27,7 @@ module Badline
|
|
|
26
27
|
# reads it, and it holds the bit counter at zero, so the first 0 bit
|
|
27
28
|
# after it starts a byte. Every eighth bit after that is BYTE READY,
|
|
28
29
|
# which pulls VIA 2's CA1 low until the next bit and reaches the CPU's
|
|
29
|
-
# SO pin (see
|
|
30
|
+
# SO pin (see byte_ready!).
|
|
30
31
|
#
|
|
31
32
|
# CB2 low selects write mode, where SYNC detection and the flux are
|
|
32
33
|
# off and the clock runs on at the selected rate. At each BYTE READY
|
|
@@ -64,9 +65,13 @@ module Badline
|
|
|
64
65
|
# becomes track 1's, so the DOS's bump leaves the head on track 1 in
|
|
65
66
|
# line with the phase it ends on, and its first step in reaches the
|
|
66
67
|
# half track after it.
|
|
68
|
+
#
|
|
69
|
+
# The 1571's stepper is lined up so that phase 0 holds track 1, where
|
|
70
|
+
# its track 0 sensor looks for the head (Mechanism.new's +slip+).
|
|
67
71
|
class Mechanism
|
|
68
72
|
include Rotation
|
|
69
73
|
include MechanismState
|
|
74
|
+
include ByteReady
|
|
70
75
|
|
|
71
76
|
MOTOR = 0x04
|
|
72
77
|
LED = 0x08
|
|
@@ -81,11 +86,15 @@ module Badline
|
|
|
81
86
|
|
|
82
87
|
attr_reader :disk, :half_track, :zone
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
# +slip+ is the stepper's phase offset at power-on: 0, where each half
|
|
90
|
+
# track's phase is the low two bits of its number, or 2 for track 1
|
|
91
|
+
# on phase 0.
|
|
92
|
+
def initialize(slip: 0)
|
|
93
|
+
@via = nil
|
|
94
|
+
@so_pending = false
|
|
86
95
|
@disk = nil
|
|
87
96
|
@half_track = START_HALF_TRACK
|
|
88
|
-
@slip =
|
|
97
|
+
@slip = slip
|
|
89
98
|
@motor = false
|
|
90
99
|
@led = false
|
|
91
100
|
@zone = 0
|
|
@@ -98,10 +107,30 @@ module Badline
|
|
|
98
107
|
@bits = 0
|
|
99
108
|
@sync = false
|
|
100
109
|
@byte_ready = false
|
|
110
|
+
@byte_latched = false
|
|
101
111
|
@writing = false
|
|
102
112
|
@write_shift = 0
|
|
103
113
|
@write_gate = false
|
|
104
114
|
@protected = false
|
|
115
|
+
@side_base = 0
|
|
116
|
+
@cycle_ticks = CYCLE
|
|
117
|
+
load_track
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The drive's clock rate: the disk turns on by fewer ticks each
|
|
121
|
+
# cycle at 2 MHz.
|
|
122
|
+
def clock_hz=(clock_hz)
|
|
123
|
+
@cycle_ticks = CYCLE * Drive1541::CLOCK_HZ / clock_hz
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The head that reads, 0 or 1, on a drive with two (Disk::SIDE).
|
|
127
|
+
def side = @side_base.zero? ? 0 : 1
|
|
128
|
+
|
|
129
|
+
def side=(side)
|
|
130
|
+
side_base = side * Disk::SIDE
|
|
131
|
+
return if side_base == @side_base
|
|
132
|
+
|
|
133
|
+
@side_base = side_base
|
|
105
134
|
load_track
|
|
106
135
|
end
|
|
107
136
|
|
|
@@ -117,10 +146,10 @@ module Badline
|
|
|
117
146
|
# LED, the zone and the clock's rate, the stepper, write mode and
|
|
118
147
|
# where a write starts, and the track and cell under the head. The
|
|
119
148
|
# rest moves only while the motor turns, or as a disk goes in (see
|
|
120
|
-
#
|
|
149
|
+
# Drive::Idle).
|
|
121
150
|
def idle_state
|
|
122
|
-
[@motor, @led, @zone, @clock, @half_track, @slip, @disk, @track, @index, @mask, @cell_end,
|
|
123
|
-
@writing, @write_index, @sync]
|
|
151
|
+
[@motor, @led, @zone, @clock, @half_track, @slip, @side_base, @disk, @track, @index, @mask, @cell_end,
|
|
152
|
+
@time, @writing, @write_index, @sync, @byte_latched]
|
|
124
153
|
end
|
|
125
154
|
|
|
126
155
|
# Puts a Disk in, or takes it out with nil, flushing the disk that
|
|
@@ -178,7 +207,7 @@ module Badline
|
|
|
178
207
|
def read_bit(one)
|
|
179
208
|
if @byte_ready
|
|
180
209
|
@byte_ready = false
|
|
181
|
-
|
|
210
|
+
byte_ready_ended!
|
|
182
211
|
end
|
|
183
212
|
@shift = ((@shift << 1) & 0x3fe) | (one ? 1 : 0)
|
|
184
213
|
if one
|
|
@@ -197,7 +226,7 @@ module Badline
|
|
|
197
226
|
def write_bit(at)
|
|
198
227
|
if @byte_ready
|
|
199
228
|
@byte_ready = false
|
|
200
|
-
|
|
229
|
+
byte_ready_ended!
|
|
201
230
|
end
|
|
202
231
|
one = @write_shift.anybits?(0x80)
|
|
203
232
|
@write_shift = (@write_shift << 1) & 0xff
|
|
@@ -227,10 +256,11 @@ module Badline
|
|
|
227
256
|
# changed: a half track without data gets a blank track, and a track
|
|
228
257
|
# written at another rate is laid out again at this one.
|
|
229
258
|
def track_written(at)
|
|
230
|
-
|
|
231
|
-
@disk.
|
|
259
|
+
surface = self.surface
|
|
260
|
+
track = @disk.writable_track(surface, @zone)
|
|
261
|
+
@disk.write(surface, track.relaid(@zone)) unless track.written_at?(@zone)
|
|
232
262
|
load_track(at)
|
|
233
|
-
@disk.written(
|
|
263
|
+
@disk.written(surface)
|
|
234
264
|
@track_written = true
|
|
235
265
|
end
|
|
236
266
|
|
|
@@ -246,9 +276,9 @@ module Badline
|
|
|
246
276
|
return unless @bits == 8
|
|
247
277
|
|
|
248
278
|
@bits = 0
|
|
249
|
-
@write_shift = @writing ? @
|
|
279
|
+
@write_shift = @writing ? @via.port_a_output : @shift & 0xff
|
|
250
280
|
@byte_ready = true
|
|
251
|
-
|
|
281
|
+
byte_ready!
|
|
252
282
|
end
|
|
253
283
|
|
|
254
284
|
# The stepper's rotor turns with the head, a phase to each half
|
|
@@ -267,6 +297,10 @@ module Badline
|
|
|
267
297
|
end
|
|
268
298
|
end
|
|
269
299
|
|
|
300
|
+
# The half track under the head that reads, numbered on the disk
|
|
301
|
+
# (see Disk::SIDE).
|
|
302
|
+
def surface = @half_track + @side_base
|
|
303
|
+
|
|
270
304
|
def seek(half_track)
|
|
271
305
|
half_track = half_track.clamp(MIN_HALF_TRACK, Disk::MAX_HALF_TRACK)
|
|
272
306
|
return if half_track == @half_track
|
|
@@ -10,11 +10,10 @@ module Badline
|
|
|
10
10
|
def save_state(out)
|
|
11
11
|
out.boolean(!@disk.nil?)
|
|
12
12
|
@disk&.save_state(out)
|
|
13
|
-
[@half_track, @slip, @zone, @clock, @time, @clock_at, @clocks, @shift, @ones, @bits, @write_shift,
|
|
13
|
+
[@half_track, @slip, @side_base, @zone, @clock, @time, @clock_at, @clocks, @shift, @ones, @bits, @write_shift,
|
|
14
14
|
@index, @mask, @cell_end, @width, @next].each { |value| out.int(value) }
|
|
15
|
-
[@motor, @led, @sync, @byte_ready, @writing, @write_gate, @protected,
|
|
16
|
-
|
|
17
|
-
end
|
|
15
|
+
[@motor, @led, @sync, @byte_ready, @byte_latched, @writing, @write_gate, @protected,
|
|
16
|
+
@track_written].each { |flag| out.boolean(flag) }
|
|
18
17
|
out.optional_int(@write_index)
|
|
19
18
|
end
|
|
20
19
|
|
|
@@ -36,6 +35,7 @@ module Badline
|
|
|
36
35
|
def load_head(input)
|
|
37
36
|
@half_track = input.int
|
|
38
37
|
@slip = input.int
|
|
38
|
+
@side_base = input.int
|
|
39
39
|
@zone = input.int
|
|
40
40
|
@clock = input.int
|
|
41
41
|
@time = input.int
|
|
@@ -57,6 +57,7 @@ module Badline
|
|
|
57
57
|
@led = input.boolean?
|
|
58
58
|
@sync = input.boolean?
|
|
59
59
|
@byte_ready = input.boolean?
|
|
60
|
+
@byte_latched = input.boolean?
|
|
60
61
|
@writing = input.boolean?
|
|
61
62
|
@write_gate = input.boolean?
|
|
62
63
|
@protected = input.boolean?
|
|
@@ -30,12 +30,12 @@ module Badline
|
|
|
30
30
|
BLANK = Track.new(Array.new(Disk::TRACK_LENGTHS[0], 0).freeze, 0)
|
|
31
31
|
|
|
32
32
|
# One drive cycle: nothing with the motor off, and otherwise the
|
|
33
|
-
# disk turns on by
|
|
34
|
-
# in them.
|
|
33
|
+
# disk turns on by a cycle's ticks, 16 at 1 MHz and 8 at 2 MHz,
|
|
34
|
+
# through whatever cells and clocks fall in them.
|
|
35
35
|
def cycle!
|
|
36
36
|
return unless @motor
|
|
37
37
|
|
|
38
|
-
@time +=
|
|
38
|
+
@time += @cycle_ticks
|
|
39
39
|
pass_time if @time >= @next
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -95,7 +95,7 @@ module Badline
|
|
|
95
95
|
# The track under the head and its layout, leaving the head's place
|
|
96
96
|
# on it as it stands.
|
|
97
97
|
def head_on_track
|
|
98
|
-
@track = @disk&.track(
|
|
98
|
+
@track = @disk&.track(surface) || BLANK
|
|
99
99
|
@bytes = @track.bytes
|
|
100
100
|
@widths = @track.widths
|
|
101
101
|
@length = @track.length
|
|
@@ -105,7 +105,7 @@ module Badline
|
|
|
105
105
|
# The track under the head, from the cell that's under it +time+
|
|
106
106
|
# into the turn.
|
|
107
107
|
def load_track(time = @time)
|
|
108
|
-
@track = @disk&.track(
|
|
108
|
+
@track = @disk&.track(surface) || BLANK
|
|
109
109
|
@bytes = @track.bytes
|
|
110
110
|
@widths = @track.widths
|
|
111
111
|
@length = @track.length
|
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/badline/drive1541.rb
CHANGED
|
@@ -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.
|
|
18
|
-
# the
|
|
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
|
|
37
|
-
include Idle
|
|
38
|
-
include Orbit
|
|
18
|
+
include Drive::Core
|
|
39
19
|
|
|
40
20
|
CLOCK_HZ = 1_000_000
|
|
41
21
|
|
|
42
|
-
|
|
22
|
+
# Where the DOS 2.6 idle loop starts over (see Drive::Idle).
|
|
23
|
+
IDLE_LOOP = 0xebff
|
|
43
24
|
|
|
44
|
-
def
|
|
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
|
|
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 = @
|
|
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
|