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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/CLAUDE.md +10 -5
  4. data/README.md +54 -23
  5. data/doc/pinned-behaviour.md +221 -0
  6. data/exe/badline-ruby +7 -4
  7. data/lib/badline/address_bus/ultimax_pages.rb +38 -0
  8. data/lib/badline/address_bus.rb +19 -29
  9. data/lib/badline/audio/jukebox.rb +6 -4
  10. data/lib/badline/audio/machine_player.rb +10 -6
  11. data/lib/badline/cartridge/kcs_power.rb +3 -3
  12. data/lib/badline/cia.rb +5 -1
  13. data/lib/badline/computer.rb +86 -7
  14. data/lib/badline/cpu.rb +39 -0
  15. data/lib/badline/drive1541/bus.rb +134 -0
  16. data/lib/badline/drive1541/disk.rb +256 -0
  17. data/lib/badline/drive1541/disk_via.rb +33 -0
  18. data/lib/badline/drive1541/gcr.rb +57 -0
  19. data/lib/badline/drive1541/idle.rb +184 -0
  20. data/lib/badline/drive1541/mechanism.rb +277 -0
  21. data/lib/badline/drive1541/rotation.rb +111 -0
  22. data/lib/badline/drive1541/sector_reader.rb +76 -0
  23. data/lib/badline/drive1541/serial_port.rb +67 -0
  24. data/lib/badline/drive1541/track.rb +98 -0
  25. data/lib/badline/drive1541.rb +218 -0
  26. data/lib/badline/gui/application.rb +15 -3
  27. data/lib/badline/gui/drive_led_pane.rb +29 -0
  28. data/lib/badline/gui.rb +1 -0
  29. data/lib/badline/iec_bus.rb +93 -0
  30. data/lib/badline/kernal_trap/channel.rb +9 -1
  31. data/lib/badline/kernal_trap/serial.rb +9 -3
  32. data/lib/badline/media/true_drive.rb +54 -0
  33. data/lib/badline/media.rb +22 -1
  34. data/lib/badline/options.rb +24 -4
  35. data/lib/badline/region.rb +35 -10
  36. data/lib/badline/reu/dma.rb +228 -0
  37. data/lib/badline/reu/ram.rb +65 -0
  38. data/lib/badline/reu/trigger.rb +32 -0
  39. data/lib/badline/reu.rb +276 -0
  40. data/lib/badline/roms/README +10 -0
  41. data/lib/badline/roms/dos1541.rom +0 -0
  42. data/lib/badline/sdl/functions.rb +1 -0
  43. data/lib/badline/storage/d64_image.rb +18 -9
  44. data/lib/badline/storage/d81_image.rb +1 -0
  45. data/lib/badline/storage/disk_image/catalog.rb +52 -0
  46. data/lib/badline/storage/disk_image/writing.rb +16 -0
  47. data/lib/badline/storage/disk_image.rb +2 -0
  48. data/lib/badline/storage/g64_image.rb +222 -0
  49. data/lib/badline/storage/host_directory.rb +21 -1
  50. data/lib/badline/storage/listing.rb +118 -0
  51. data/lib/badline/storage/t64.rb +14 -0
  52. data/lib/badline/storage.rb +2 -0
  53. data/lib/badline/traps.rb +4 -0
  54. data/lib/badline/version.rb +1 -1
  55. data/lib/badline/via/control_lines.rb +3 -0
  56. data/lib/badline/via/fast_forward.rb +38 -0
  57. data/lib/badline/via/shift_register.rb +8 -3
  58. data/lib/badline/via/timer1.rb +30 -0
  59. data/lib/badline/via/timer2.rb +25 -0
  60. data/lib/badline/via.rb +2 -0
  61. data/lib/badline/vic/display_state.rb +7 -7
  62. data/lib/badline/vic/sprite/internal_bus.rb +11 -7
  63. data/lib/badline/vic/sprite/shifter.rb +10 -15
  64. data/lib/badline/vic/sprite/timing.rb +60 -0
  65. data/lib/badline/vic/sprite.rb +14 -12
  66. data/lib/badline/vic/sprites.rb +2 -2
  67. data/lib/badline/vic.rb +86 -39
  68. data/lib/badline.rb +3 -0
  69. metadata +26 -1
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drive1541/rotation"
4
+
5
+ module Badline
6
+ class Drive1541
7
+ # The disk mechanism behind VIA 2: the spindle motor, the head on its
8
+ # stepper, the LED, the read and write electronics and the
9
+ # write-protect sensor.
10
+ #
11
+ # Port B drives it and reads it back:
12
+ #
13
+ # PB0-1 stepper phase (out) PB4 write protect (in, 1 = writable)
14
+ # PB2 motor on (out) PB5-6 bit rate, speed zone 0-3 (out)
15
+ # PB3 LED on (out) PB7 SYNC (in, 0 while in a SYNC mark)
16
+ #
17
+ # Port A reads the read shift register's low eight bits, which CA1
18
+ # latches on BYTE READY. In write mode it drives the write shift
19
+ # register, and the read shift register takes the bits written.
20
+ #
21
+ # The disk turns at 300 rpm under the head, and a read clock at the
22
+ # bit rate PB5-6 select clocks the bits off it (see Rotation).
23
+ #
24
+ # Ten 1 bits in a row are a SYNC mark while CB2 selects reading: PB7
25
+ # reads it, and it holds the bit counter at zero, so the first 0 bit
26
+ # after it starts a byte. Every eighth bit after that is BYTE READY,
27
+ # which pulls VIA 2's CA1 low until the next bit and reaches the CPU's
28
+ # SO pin (see Drive1541#byte_ready!).
29
+ #
30
+ # CB2 low selects write mode, where SYNC detection and the flux are
31
+ # off and the clock runs on at the selected rate. At each BYTE READY
32
+ # the write shift register loads what the VIA drives on port A, and
33
+ # the next eight clocks put that byte's bits on the track, most
34
+ # significant first, in place of what it held. While reading, the load
35
+ # takes the byte just read, so a switch to writing mid-byte writes the
36
+ # rest of it. A write lays its bits one to a cell from the cell under
37
+ # the head, so on a track written at the selected rate each bit fills
38
+ # the cell it passes over. A write to a half track without data gives
39
+ # it a blank track first, a turn long at the selected rate, and a
40
+ # write to a track written at another rate lays the whole track out
41
+ # again as a turn at the selected rate first (Track#relaid).
42
+ #
43
+ # PB4 reads the write-protect sensor: high with no disk in, or with
44
+ # the notch of a writable one open. With the disk write-protected the
45
+ # write gate stays shut: the bits and BYTE READY go on, and nothing
46
+ # reaches the disk. The DOS reads PB4 first and refuses to write, so
47
+ # only a program that writes without asking finds that out.
48
+ #
49
+ # The motor stopping flushes what was written to the disk's image
50
+ # (see Disk#flush), which is when the DOS is done with a job, and so
51
+ # does taking the disk out.
52
+ #
53
+ # The stepper moves the head a half track each time the phase steps
54
+ # by one, inwards for +1 and outwards for -1. Each half track has a
55
+ # phase of its own, from power-on the low two bits of its number, so
56
+ # the head sits on a whole track when phase 0 or 2 holds it. The
57
+ # stepper's coils are powered only while PB2 runs the motor, as VICE
58
+ # reads the 1541's schematic, so a phase set with the motor off moves
59
+ # nothing until the motor comes on and it pulls the head. Tracks run
60
+ # from half track 2 (track 1), where the head stops against the end of
61
+ # its rail, to Disk::MAX_HALF_TRACK. A step out against the stop slips:
62
+ # the head stays on track 1 and the phase that tried to pull it out
63
+ # becomes track 1's, so the DOS's bump leaves the head on track 1 in
64
+ # line with the phase it ends on, and its first step in reaches the
65
+ # half track after it.
66
+ class Mechanism
67
+ include Rotation
68
+
69
+ MOTOR = 0x04
70
+ LED = 0x08
71
+ WRITE_PROTECT = 0x10
72
+ SYNC = 0x80
73
+
74
+ MIN_HALF_TRACK = 2
75
+ # Where the head sits at power-on: track 18, the directory.
76
+ START_HALF_TRACK = 36
77
+
78
+ SYNC_BITS = 10
79
+
80
+ attr_reader :disk, :half_track, :zone
81
+
82
+ def initialize(drive)
83
+ @drive = drive
84
+ @disk = nil
85
+ @half_track = START_HALF_TRACK
86
+ @slip = 0
87
+ @motor = false
88
+ @led = false
89
+ @zone = 0
90
+ @clock = Track.cell(0)
91
+ @time = 0
92
+ @clock_at = @clock / 2
93
+ @clocks = 0
94
+ @shift = 0
95
+ @ones = 0
96
+ @bits = 0
97
+ @sync = false
98
+ @byte_ready = false
99
+ @writing = false
100
+ @write_shift = 0
101
+ @write_gate = false
102
+ @protected = false
103
+ load_track
104
+ end
105
+
106
+ def motor_on? = @motor
107
+
108
+ def led_on? = @led
109
+
110
+ def sync? = @sync
111
+
112
+ def writing? = @writing
113
+
114
+ # What port B and CB2 can change with the motor off: the motor, the
115
+ # LED, the zone and the clock's rate, the stepper, write mode and
116
+ # where a write starts, and the track and cell under the head. The
117
+ # rest moves only while the motor turns, or as a disk goes in (see
118
+ # Drive1541::Idle).
119
+ def idle_state
120
+ [@motor, @led, @zone, @clock, @half_track, @slip, @disk, @track, @index, @mask, @cell_end, @time,
121
+ @writing, @write_index, @sync]
122
+ end
123
+
124
+ # Puts a Disk in, or takes it out with nil, flushing the disk that
125
+ # was in. The head goes on from the same point in the turn.
126
+ def insert(disk)
127
+ flush
128
+ @disk = disk
129
+ @protected = disk&.write_protected? || false
130
+ @write_gate = !disk.nil? && !@protected
131
+ load_track
132
+ end
133
+
134
+ # Stores what the head wrote in the disk's image.
135
+ def flush
136
+ @disk.flush if @disk&.written?
137
+ rescue Storage::WriteError => e
138
+ warn "1541: couldn't write the disk back: #{e.message}"
139
+ end
140
+
141
+ # VIA 2 drives CB2 to +high+: low selects write mode, which stops
142
+ # SYNC detection at once.
143
+ def cb2_written(high)
144
+ @write_index = nil if high || !@writing
145
+ @writing = !high
146
+ @sync = !@writing && @ones >= SYNC_BITS
147
+ end
148
+
149
+ # VIA 2 drives port B's lines to +lines+.
150
+ def port_b_written(lines)
151
+ motor = @motor
152
+ @motor = lines.anybits?(MOTOR)
153
+ flush if motor && !@motor
154
+ @led = lines.anybits?(LED)
155
+ zone = (lines >> 5) & 0x03
156
+ if zone != @zone
157
+ @zone = zone
158
+ @clock = Track.cell(zone)
159
+ @track_written = false
160
+ end
161
+ step(lines & 0x03) if @motor
162
+ end
163
+
164
+ # Port A is the read shift register.
165
+ def read_a(_lines) = @shift & 0xff
166
+
167
+ # SYNC low in a SYNC mark, and write protect low for a protected
168
+ # disk.
169
+ def read_b(_lines)
170
+ lines = @sync ? 0xff & ~SYNC : 0xff
171
+ @protected ? lines & ~WRITE_PROTECT : lines
172
+ end
173
+
174
+ private
175
+
176
+ def read_bit(one)
177
+ if @byte_ready
178
+ @byte_ready = false
179
+ @drive.byte_ready_ended!
180
+ end
181
+ @shift = ((@shift << 1) & 0x3fe) | (one ? 1 : 0)
182
+ if one
183
+ @ones += 1
184
+ @sync = true if @ones >= SYNC_BITS
185
+ else
186
+ @ones = 0
187
+ @sync = false
188
+ end
189
+ count_bit
190
+ end
191
+
192
+ # The write shift register's top bit goes onto the track through the
193
+ # write gate, and into the read shift register, as the head's own
194
+ # signal.
195
+ def write_bit(at)
196
+ if @byte_ready
197
+ @byte_ready = false
198
+ @drive.byte_ready_ended!
199
+ end
200
+ one = @write_shift.anybits?(0x80)
201
+ @write_shift = (@write_shift << 1) & 0xff
202
+ store_bit(one, at) if @write_gate
203
+ @shift = ((@shift << 1) & 0x3fe) | (one ? 1 : 0)
204
+ @ones = one ? @ones + 1 : 0
205
+ count_bit
206
+ end
207
+
208
+ # Each bit written goes in the cell after the last one's, from the
209
+ # cell under the head when the write started.
210
+ def store_bit(one, at)
211
+ track_written(at) unless @track_written
212
+ @write_index ||= (@index * 8) + 8 - @mask.bit_length
213
+ index = @write_index >> 3
214
+ mask = 0x80 >> (@write_index & 7)
215
+ if one
216
+ @bytes[index] |= mask
217
+ else
218
+ @bytes[index] &= ~mask
219
+ end
220
+ @write_index += 1
221
+ @write_index = 0 if @write_index == @length * 8
222
+ end
223
+
224
+ # The first write to the track since the head came to it or the rate
225
+ # changed: a half track without data gets a blank track, and a track
226
+ # written at another rate is laid out again at this one.
227
+ def track_written(at)
228
+ track = @disk.writable_track(@half_track, @zone)
229
+ @disk.write(@half_track, track.relaid(@zone)) unless track.written_at?(@zone)
230
+ load_track(at)
231
+ @disk.written(@half_track)
232
+ @track_written = true
233
+ end
234
+
235
+ # Every eighth bit outside a SYNC mark is BYTE READY, which loads the
236
+ # write shift register: from what the VIA drives on port A in write
237
+ # mode, and with the byte just read otherwise.
238
+ def count_bit
239
+ if @sync
240
+ @bits = 0
241
+ return
242
+ end
243
+ @bits += 1
244
+ return unless @bits == 8
245
+
246
+ @bits = 0
247
+ @write_shift = @writing ? @drive.via2.port_a_output : @shift & 0xff
248
+ @byte_ready = true
249
+ @drive.byte_ready!
250
+ end
251
+
252
+ # The stepper's rotor turns with the head, a phase to each half
253
+ # track, so the energized phase pulls the head to the neighbouring
254
+ # half track it belongs to. A phase two away pulls both ways, and the
255
+ # head stays. Against the stop at track 1 the head can't follow, and
256
+ # the phases slip round to where the head is.
257
+ def step(phase)
258
+ move = (phase - @half_track - @slip) & 0x03
259
+ if move == 1
260
+ seek(@half_track + 1)
261
+ elsif move == 3 && @half_track == MIN_HALF_TRACK
262
+ @slip = (phase - MIN_HALF_TRACK) & 0x03
263
+ elsif move == 3
264
+ seek(@half_track - 1)
265
+ end
266
+ end
267
+
268
+ def seek(half_track)
269
+ half_track = half_track.clamp(MIN_HALF_TRACK, Disk::MAX_HALF_TRACK)
270
+ return if half_track == @half_track
271
+
272
+ @half_track = half_track
273
+ load_track
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # The disk turning under the Mechanism's head, and its read clock.
6
+ #
7
+ # The disk turns at 300 rpm, whatever the bit rate: once in 200 ms, or
8
+ # 200,000 drive cycles. Each track's bits pass under the head at the
9
+ # rate they were written at (see Track), and a 1 bit is a flux
10
+ # transition at the start of its cell.
11
+ #
12
+ # The read circuit clocks bits out at the rate PB5-6 select, from a
13
+ # counter the 16 MHz crystal steps every 16 - zone ticks. A flux
14
+ # transition clears the counter. The counter clocks a bit into the
15
+ # read shift register as it reaches 2 and every four steps after
16
+ # that, and the bit is a 1 only for the first clock after the counter
17
+ # passed 0. A bit is 4 * (16 - zone) ticks, so a byte takes 32, 30, 28
18
+ # or 26 drive cycles in zones 0 to 3, and each transition brings the
19
+ # clock back into step with the flux. A track read at the rate it was
20
+ # written reads its bits. At another rate the clock and the cells
21
+ # drift apart between transitions: a faster clock slips an extra 0
22
+ # bit into some runs of 0 bits, a slower one drops one from some, and
23
+ # the bytes come out garbled. Without flux the counter runs round on
24
+ # its own and reads a 1 bit in every four.
25
+ module Rotation
26
+ CYCLE = 16 * Track::TICK
27
+
28
+ # What the head reads with no disk in, or off the tracks a disk has:
29
+ # no flux.
30
+ BLANK = Track.new(Array.new(Disk::TRACK_LENGTHS[0], 0).freeze, 0)
31
+
32
+ # One drive cycle: nothing with the motor off, and otherwise the
33
+ # disk turns on by 16 ticks, through whatever cells and clocks fall
34
+ # in them.
35
+ def cycle!
36
+ return unless @motor
37
+
38
+ @time += CYCLE
39
+ pass_time if @time >= @next
40
+ end
41
+
42
+ private
43
+
44
+ # The cell edges and clocks up to now, in the order they come. A
45
+ # clock that falls on a cell edge comes after it.
46
+ def pass_time
47
+ while @cell_end <= @time || @clock_at <= @time
48
+ if @cell_end <= @clock_at
49
+ next_cell
50
+ else
51
+ clock
52
+ end
53
+ end
54
+ @next = [@cell_end, @clock_at].min
55
+ end
56
+
57
+ # The head comes to the next cell, and a 1 bit's flux clears the
58
+ # read clock while reading.
59
+ def next_cell
60
+ start = @cell_end
61
+ @mask >>= 1
62
+ start = next_byte(start) if @mask.zero?
63
+ @cell_end = @index == @last && @mask == 1 ? Track::TURN : start + @width
64
+ return if @writing || @bytes[@index].nobits?(@mask)
65
+
66
+ @clock_at = start + (@clock / 2)
67
+ @clocks = 0
68
+ end
69
+
70
+ # The head comes to the next byte, and at the end of the track back
71
+ # round to the first, where the turn starts over.
72
+ def next_byte(start)
73
+ @mask = 0x80
74
+ @index += 1
75
+ if @index == @length
76
+ @index = 0
77
+ @time -= Track::TURN
78
+ @clock_at -= Track::TURN
79
+ start = 0
80
+ end
81
+ @width = @widths[@index] if @widths
82
+ start
83
+ end
84
+
85
+ # The read clock ticks a bit into the shift register: a 1 the first
86
+ # time after a flux transition or the counter's wrap.
87
+ def clock
88
+ at = @clock_at
89
+ one = @clocks.zero?
90
+ @clocks = (@clocks + 1) & 3
91
+ @clock_at = at + @clock
92
+ @writing ? write_bit(at) : read_bit(one)
93
+ end
94
+
95
+ # The track under the head, from the cell that's under it +time+
96
+ # into the turn.
97
+ def load_track(time = @time)
98
+ @track = @disk&.track(@half_track) || BLANK
99
+ @bytes = @track.bytes
100
+ @widths = @track.widths
101
+ @length = @track.length
102
+ @last = @length - 1
103
+ @index, @mask, @cell_end = @track.cell_at(time)
104
+ @width = @widths ? @widths[@index] : @track.width
105
+ @write_index = nil
106
+ @track_written = false
107
+ @next = [@cell_end, @clock_at].min
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # Reads a track's blocks back out of its GCR, as the DOS finds them:
6
+ # each SYNC mark is ten or more 1 bits, and the block after it starts
7
+ # at the first 0 bit. Writes land wherever the head was, so a block
8
+ # can start at any bit of the stored bytes, and the reader works on
9
+ # the bits.
10
+ #
11
+ # A header block starts with $08, and the next block after a header is
12
+ # that sector's data block, whatever it holds.
13
+ module SectorReader
14
+ HEADER_BITS = 10 * 8
15
+ DATA_BITS = 325 * 8
16
+ SYNC = /1{10,}/
17
+
18
+ # A sector as read: its decoded header, [ID, checksum, sector, track,
19
+ # the second ID byte, the first, ...], and its decoded data block,
20
+ # [ID, 256 data bytes, checksum, ...], nil when there isn't one that
21
+ # decodes.
22
+ Sector = Struct.new(:header, :data)
23
+
24
+ module_function
25
+
26
+ # The sectors found on the track's bytes, by sector number. Headers
27
+ # for another track are left out.
28
+ def read(bytes, track)
29
+ ring = ring(bytes)
30
+ return {} unless ring
31
+
32
+ sectors = {}
33
+ each_block(ring) do |header, data|
34
+ next unless header[3] == track
35
+
36
+ sectors[header[2]] ||= Sector.new(header, data)
37
+ end
38
+ sectors
39
+ end
40
+
41
+ # The track's bits, starting at a 0 bit so no SYNC mark straddles the
42
+ # start, twice over so a block can run on past the end. Nil for a
43
+ # track without a 0 bit.
44
+ def ring(bytes)
45
+ bits = bytes.pack("C*").unpack1("B*")
46
+ zero = bits.index("0")
47
+ return unless zero
48
+
49
+ bits = bits[zero..] + bits[0, zero]
50
+ [bits * 2, bits.length]
51
+ end
52
+
53
+ # Yields each header whose SYNC mark starts in the first turn, with
54
+ # the data block after it.
55
+ def each_block(ring)
56
+ bits, length = ring
57
+ pos = 0
58
+ while (sync = SYNC.match(bits, pos)) && sync.begin(0) < length
59
+ pos = sync.end(0)
60
+ header = decode(bits, pos, HEADER_BITS)
61
+ next unless header && header[0] == Disk::HEADER_ID
62
+
63
+ data_sync = SYNC.match(bits, pos + HEADER_BITS)
64
+ yield header, data_sync && decode(bits, data_sync.end(0), DATA_BITS)
65
+ end
66
+ end
67
+
68
+ def decode(bits, pos, count)
69
+ slice = bits[pos, count]
70
+ return unless slice.length == count
71
+
72
+ GCR.decode([slice].pack("B*").bytes)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
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.
21
+ 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
29
+
30
+ def initialize(device: 8, bus: nil)
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 they stand, for the drive cycles of the
42
+ # next host cycle.
43
+ def latch_host
44
+ @host = @bus ? @bus.host_lines : 0
45
+ end
46
+
47
+ # Whether the C64 has moved ATN since the last latch_host.
48
+ def atn_moved?
49
+ lines = @bus ? @bus.host_lines : 0
50
+ (lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
51
+ end
52
+
53
+ def atn_low? = @bus.atn_low?(@host)
54
+
55
+ def read_a(_lines) = 0xff
56
+
57
+ def read_b(_lines)
58
+ low = @bus.low_lines(@host)
59
+ value = @port_b
60
+ value |= DATA_IN if low.anybits?(IECBus::DATA)
61
+ value |= CLK_IN if low.anybits?(IECBus::CLK)
62
+ value |= ATN_IN if low.anybits?(IECBus::ATN)
63
+ value
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # One ring of flux on the disk, as the GCR bytes the head reads from it,
6
+ # most significant bit first, and the speed zone it was written in
7
+ # (0-3). The bytes wrap around: the last one's bits run into the
8
+ # first's. A track is as long as its bytes, which is what a .g64 image
9
+ # stores for each half track.
10
+ #
11
+ # The disk turns at 300 rpm, so every track passes under the head in
12
+ # the same 200 ms, and its bits pass at the rate they were written at.
13
+ # Each bit is a cell of time, starting at the track's index angle with
14
+ # the first byte's top bit, and a 1 bit is a flux transition at the
15
+ # start of its cell. The cells are spread evenly over the turn, so a
16
+ # track as long as a turn holds at its zone's rate
17
+ # (Disk::TRACK_LENGTHS) passes at that rate, within the part of a byte
18
+ # the length rounds off, and a longer or shorter one faster or slower.
19
+ # A .g64 speed map gives each byte the zone it was written in, and the
20
+ # cells of each byte are as wide as their zone's, scaled together to
21
+ # fill the turn.
22
+ #
23
+ # Times are in TICKs, 1/65536 of a tick of the drive's 16 MHz crystal,
24
+ # so the cells add up to a turn within a tick.
25
+ class Track
26
+ TICK = 1 << 16
27
+ TURN = 200_000 * 16 * TICK
28
+
29
+ # A bit cell at the zone's rate, in TICKs.
30
+ def self.cell(zone) = 4 * (16 - zone) * TICK
31
+
32
+ attr_reader :bytes, :zone, :speeds, :width, :widths
33
+
34
+ # +speeds+ holds each byte's zone, where the track's rate changes
35
+ # along it.
36
+ def initialize(bytes, zone, speeds = nil)
37
+ @bytes = bytes
38
+ @zone = zone
39
+ @speeds = speeds
40
+ @widths = speeds && scaled_widths(speeds)
41
+ @width = speeds ? @widths[0] : TURN / (bytes.length * 8)
42
+ end
43
+
44
+ def length = @bytes.length
45
+
46
+ # Whether the whole track was written at the +zone+'s rate.
47
+ def written_at?(zone) = @widths.nil? && @zone == zone
48
+
49
+ # The bit cell under the head +time+ TICKs into the turn, as
50
+ # [byte, mask, the time the cell ends].
51
+ def cell_at(time)
52
+ start = 0
53
+ index = 0
54
+ if @widths
55
+ while index < length - 1 && time >= start + (@widths[index] * 8)
56
+ start += @widths[index] * 8
57
+ index += 1
58
+ end
59
+ else
60
+ index = [time / (@width * 8), length - 1].min
61
+ start = index * @width * 8
62
+ end
63
+ width = @widths ? @widths[index] : @width
64
+ bit = [(time - start) / width, 7].min
65
+ cell_end = index == length - 1 && bit == 7 ? TURN : start + ((bit + 1) * width)
66
+ [index, 0x80 >> bit, cell_end]
67
+ end
68
+
69
+ # The track laid out again as one turn at the +zone+'s rate, each
70
+ # flux transition in the cell under it.
71
+ def relaid(zone)
72
+ bits = Disk::TRACK_LENGTHS[zone] * 8
73
+ cell = TURN / bits
74
+ relaid = Array.new(bits / 8, 0)
75
+ time = 0
76
+ @bytes.each_with_index do |byte, index|
77
+ width = @widths ? @widths[index] : @width
78
+ 8.times do |bit|
79
+ if byte.anybits?(0x80 >> bit)
80
+ cell_index = [time / cell, bits - 1].min
81
+ relaid[cell_index >> 3] |= 0x80 >> (cell_index & 7)
82
+ end
83
+ time += width
84
+ end
85
+ end
86
+ Track.new(relaid, zone)
87
+ end
88
+
89
+ private
90
+
91
+ def scaled_widths(speeds)
92
+ units = 0
93
+ speeds.each { |zone| units += 16 - zone }
94
+ speeds.map { |zone| TURN * (16 - zone) / (units * 8) }
95
+ end
96
+ end
97
+ end
98
+ end