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
@@ -18,41 +18,15 @@ module Badline
18
18
  # page is what its dummy read found. VICE-testprogs drive/openbus pins
19
19
  # both on a real 1541.
20
20
  #
21
- # While Idle records a pass of the DOS's idle loop, the bus watches it
22
- # (see watch!). While Idle looks for an orbit, the bus guards the drive
23
- # (see guard!).
21
+ # While Idle records a pass of the DOS's idle loop, the bus watches it,
22
+ # and while Idle looks for an orbit, it guards the drive (see
23
+ # Drive::Watch).
24
24
  class Bus
25
25
  include Addressable
26
-
27
- # VIA registers, as bits by offset, whose access makes a watched
28
- # stretch volatile: reads of either VIA's counters and shift register
29
- # and of VIA 1's port B, and writes to VIA 2's timers, shift register
30
- # and ACR. Any write to VIA 1 does too (watch_write).
31
- VOLATILE_VIA1_READS = 0x0731
32
- VOLATILE_VIA2_READS = 0x0730
33
- VOLATILE_VIA2_WRITES = 0x0ff0
34
-
35
- # The timer each VIA register reaches, by offset, as bits: 1 for
36
- # timer 1 and 2 for timer 2.
37
- TIMER_REGISTERS = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0].freeze
26
+ include Drive::Watch
38
27
 
39
28
  attr_reader :ram, :rom, :via1, :via2, :data
40
29
 
41
- # Whether a watched stretch did something that makes it unrepeatable.
42
- attr_reader :volatile
43
-
44
- # The RAM a watched stretch read or wrote, as each address's value
45
- # from before the first access.
46
- attr_reader :touched
47
-
48
- # Whether the drive did something since guard! that reached outside
49
- # it, or took something in from outside.
50
- attr_reader :tainted
51
-
52
- # The timers the drive read or wrote since guard!, as bits: VIA 1's
53
- # timer 1 and timer 2 in bits 0 and 1, and VIA 2's in bits 2 and 3.
54
- attr_reader :touched_timers
55
-
56
30
  def initialize(rom:, via1:, via2:)
57
31
  addressable_at(0, length: 2**16)
58
32
  @ram = Memory.new([], length: 0x0800, start: 0)
@@ -60,38 +34,7 @@ module Badline
60
34
  @via1 = via1
61
35
  @via2 = via2
62
36
  @data = 0
63
- @recording = @guarding = @watching = false
64
- @volatile = @tainted = false
65
- @touched = {}
66
- @touched_timers = 0
67
- @led = 0
68
- end
69
-
70
- # Starts watching: see volatile and touched. The bus stops watching
71
- # once the stretch turns volatile.
72
- def watch!
73
- @recording = true
74
- @volatile = false
75
- @touched = {}
76
- watching!
77
- end
78
-
79
- def unwatch!
80
- @recording = false
81
- watching!
82
- end
83
-
84
- # Starts guarding: see tainted and touched_timers. Reading VIA 1's
85
- # port B, which reads the serial bus, taints the drive, and so do
86
- # any write to VIA 1, whose port B drives the serial bus, a write to
87
- # VIA 2's shift register or ACR, and VIA 2 turning the motor on or
88
- # changing the LED. The bus stops guarding once the drive is tainted.
89
- def guard!
90
- @guarding = true
91
- @tainted = false
92
- @touched_timers = 0
93
- @led = @via2.port_b_output & Mechanism::LED
94
- watching!
37
+ init_watch
95
38
  end
96
39
 
97
40
  # The last byte on the data bus and the RAM. What a watched stretch
@@ -138,73 +81,6 @@ module Badline
138
81
  end
139
82
  watch_write(addr) if @watching
140
83
  end
141
-
142
- private
143
-
144
- def watching!
145
- @watching = @recording || @guarding
146
- end
147
-
148
- def touch(addr)
149
- @touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
150
- end
151
-
152
- def watch_read(addr)
153
- if addr < 0x0800
154
- touch(addr) if @recording
155
- elsif addr >= 0x1800
156
- via1 = addr < 0x1c00
157
- watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
158
- guard_read(via1, addr & 0x0f) if @guarding
159
- end
160
- end
161
-
162
- def watch_write(addr)
163
- if addr >= 0x1c00
164
- watch_via2_write(addr) if @recording
165
- guard_via2_write(addr & 0x0f) if @guarding
166
- elsif addr >= 0x1800
167
- volatile! if @recording
168
- taint! if @guarding
169
- end
170
- end
171
-
172
- # Any write to VIA 1 makes the stretch volatile (watch_write), and so
173
- # does a write to VIA 2 that leaves the motor on.
174
- def watch_via2_write(addr)
175
- watch(VOLATILE_VIA2_WRITES, addr)
176
- volatile! if @recording && @via2.port_b_output.anybits?(Mechanism::MOTOR)
177
- end
178
-
179
- def watch(volatile, addr)
180
- volatile! if volatile.anybits?(1 << (addr & 0x0f))
181
- end
182
-
183
- def volatile!
184
- @volatile = true
185
- @recording = false
186
- watching!
187
- end
188
-
189
- def guard_read(via1, offset)
190
- return taint! if via1 && offset.zero?
191
-
192
- @touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
193
- end
194
-
195
- def guard_via2_write(offset)
196
- return taint! if offset.between?(0x0a, 0x0b)
197
-
198
- @touched_timers |= TIMER_REGISTERS[offset] << 2
199
- lines = @via2.port_b_output
200
- taint! if lines.anybits?(Mechanism::MOTOR) || (lines & Mechanism::LED) != @led
201
- end
202
-
203
- def taint!
204
- @tainted = true
205
- @guarding = false
206
- watching!
207
- end
208
84
  end
209
85
  end
210
86
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # The Mechanism's BYTE READY line: to VIA 2's CA1, to the CPU's SO pin
6
+ # through SOE, and on the 1571 to a latch VIA 1's PA7 reads.
7
+ module ByteReady
8
+ # VIA 2, which DiskVIA plugs in.
9
+ attr_writer :via
10
+
11
+ # Whether BYTE READY reached SO for the CPU to sample on the drive's
12
+ # next cycle.
13
+ attr_accessor :so_pending
14
+
15
+ # Whether SO is pending, which the drive's cycle samples and clears.
16
+ def take_so
17
+ so = @so_pending
18
+ @so_pending = false
19
+ so
20
+ end
21
+
22
+ # The read electronics signal a whole GCR byte. BYTE READY pulls VIA
23
+ # 2's CA1 low, a falling edge that sets its flag and, with latching
24
+ # on, latches port A. It reaches the CPU's SO pin while VIA 2's CA2
25
+ # (SOE) is high, which lets the DOS spin on BVC for each byte. The CPU
26
+ # samples SO on the next cycle.
27
+ def byte_ready!
28
+ @byte_latched = true
29
+ @via.ca1 = false
30
+ @so_pending = true if @via.ca2_output
31
+ end
32
+
33
+ # BYTE READY lets go of CA1 with the next bit.
34
+ def byte_ready_ended!
35
+ @via.ca1 = true
36
+ end
37
+
38
+ # Whether BYTE READY has come since the CPU last read VIA 2, as the
39
+ # 1571 latches it for VIA 1's PA7.
40
+ def byte_latched? = @byte_latched
41
+
42
+ # A read of VIA 2 clears the 1571's BYTE READY latch.
43
+ def release_byte_latch
44
+ @byte_latched = false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -5,25 +5,7 @@ module Badline
5
5
  # The drive's 6502, on the drive's Bus.
6
6
  class CPU
7
7
  include Badline::CPU::Core
8
-
9
- # Everything the CPU holds but its cycle and instruction counts: the
10
- # registers, the interrupt lines and pipeline, and what the last
11
- # instruction left in its working registers. Drive1541::Idle compares
12
- # it at two instruction boundaries.
13
- def idle_state
14
- [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
15
- @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
16
- @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
17
- @operation, @optional_dummy, @boundary_crossed, @branch_taken,
18
- @address, @pointer, @dummy_address, @value, @rmw_result]
19
- end
20
-
21
- # Counts +cycles+ cycles and +instructions+ instructions that ran
22
- # without changing anything idle_state holds.
23
- def fast_forward(cycles, instructions)
24
- @cycles += cycles
25
- @instructions += instructions
26
- end
8
+ include Drive::IdleCPU
27
9
  end
28
10
  end
29
11
  end
@@ -10,6 +10,11 @@ module Badline
10
10
  # nothing between them. A .d64 image fills the whole tracks; a .g64
11
11
  # can fill any of them, with tracks of any length.
12
12
  #
13
+ # The disk has a second side, for the 1571's second head, its half
14
+ # tracks numbered from SIDE on. A .d71 fills both sides, its tracks
15
+ # 36-70 being tracks 1-35 of the second, and a .g71 holds 84 half
16
+ # tracks a side. A 1541 reads the first side only.
17
+ #
13
18
  # The head writes into the tracks' bytes, and a write to a half track
14
19
  # without data gives it a blank Track first. A disk made from an image
15
20
  # remembers it: flush reads the sectors back off the tracks written
@@ -21,6 +26,15 @@ module Badline
21
26
 
22
27
  MAX_HALF_TRACK = 84
23
28
 
29
+ # Where the second side's half tracks start: its track 1 is SIDE + 2.
30
+ SIDE = MAX_HALF_TRACK + 1
31
+
32
+ # The tracks a side of a .d71 holds.
33
+ D71_SIDE_TRACKS = 35
34
+
35
+ # The half track entries a side of a .g71 holds.
36
+ G71_SIDE_ENTRIES = 84
37
+
24
38
  # GCR bytes around a track in each speed zone: 200 ms of rotation at
25
39
  # 300 rpm, in bytes of 32, 30, 28 and 26 µs.
26
40
  TRACK_LENGTHS = [6250, 6666, 7142, 7692].freeze
@@ -51,27 +65,63 @@ module Badline
51
65
  # the 325-byte GCR data block. The gap after it takes up the rest.
52
66
  SECTOR_LENGTH = SYNC_LENGTH + 10 + HEADER_GAP + SYNC_LENGTH + 325
53
67
 
54
- # A disk from the image at +path+: a .g64 as its tracks are, and
55
- # anything else as a .d64, formatted. `read_only` opens the image
56
- # write-protected.
68
+ # A disk from the image at +path+: a .g64 or .g71 as its tracks are,
69
+ # a .d71 formatted on both sides, and anything else as a .d64,
70
+ # formatted. `read_only` opens the image write-protected.
57
71
  def self.open(path, read_only: false)
58
72
  return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
73
+ return from_d64(Storage::D71Image.new(path, read_only:)).opened(path, read_only) if State.d71?(path)
59
74
 
60
75
  from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
61
76
  end
62
77
 
63
- # A disk from a G64 image, each half track as the image stores it,
64
- # with its speed map. Flushing it writes the tracks back as they are.
78
+ # A disk from a G64 or G71 image, each half track as the image stores
79
+ # it, with its speed map. Flushing it writes the tracks back as they
80
+ # are.
65
81
  def self.from_g64(image)
66
82
  disk = new(image)
67
- (Mechanism::MIN_HALF_TRACK..MAX_HALF_TRACK).each do |half_track|
68
- entry = half_track - Mechanism::MIN_HALF_TRACK
83
+ image.half_tracks.times do |entry|
69
84
  track = image.track(entry)
70
- disk.write(half_track, Track.new(*track, image.speeds(entry))) if track
85
+ disk.write(half_track_of(entry), Track.new(*track, image.speeds(entry))) if track
71
86
  end
72
87
  disk
73
88
  end
74
89
 
90
+ # The half track a G64 or G71 table entry holds: entry 0 is track 1,
91
+ # and a .g71's entries from G71_SIDE_ENTRIES on are the second
92
+ # side's.
93
+ def self.half_track_of(entry)
94
+ side = entry / G71_SIDE_ENTRIES
95
+ (side * SIDE) + (entry % G71_SIDE_ENTRIES) + Mechanism::MIN_HALF_TRACK
96
+ end
97
+
98
+ # The table entry of a half track, or nil for one a G64 or G71 has
99
+ # no entry for.
100
+ def self.entry_of(half_track)
101
+ side = half_track / SIDE
102
+ entry = (half_track % SIDE) - Mechanism::MIN_HALF_TRACK
103
+ (side * G71_SIDE_ENTRIES) + entry if entry.between?(0, G71_SIDE_ENTRIES - 1)
104
+ end
105
+
106
+ # The half track an image's track number is on: a .d71's tracks past
107
+ # D71_SIDE_TRACKS are on the second side.
108
+ def self.d64_half_track(image, track)
109
+ return track * 2 unless image.sides == 2 && track > D71_SIDE_TRACKS
110
+
111
+ SIDE + ((track - D71_SIDE_TRACKS) * 2)
112
+ end
113
+
114
+ # The image's track number a half track holds, or nil for a half
115
+ # track or one past the image's last.
116
+ def self.d64_track(image, half_track)
117
+ side = half_track / SIDE
118
+ return unless (half_track % SIDE).even?
119
+
120
+ track = (half_track % SIDE) / 2
121
+ track += D71_SIDE_TRACKS if side == 1
122
+ track if track.positive? && track <= image.track_count && side < image.sides
123
+ end
124
+
75
125
  # A disk formatted from a D64 image, each sector laid out as the DOS
76
126
  # formats it, with the disk ID from the header block. A block the
77
127
  # image's error table marks 20, 21, 22, 23, 27 or 29 is written the
@@ -81,7 +131,8 @@ module Badline
81
131
  #
82
132
  # Each track starts sector 0 at the angle the DOS's N: leaves it: track
83
133
  # 1 at the index angle, and each track after it SKEWS[zone] of a turn
84
- # round from the last.
134
+ # round from the last. A .d71's second side starts over at the index
135
+ # angle.
85
136
  def self.from_d64(image)
86
137
  track, sector = image.header_block
87
138
  header = image.read_block(track, sector)
@@ -89,16 +140,18 @@ module Badline
89
140
  disk = new(image)
90
141
  angle = 0
91
142
  (1..image.track_count).each do |track|
92
- angle = (angle + SKEWS[zone(track)]) % SKEW_TURN if track > 1
93
- bytes = format_track(image, track, id)
94
- disk.write(track * 2, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone(track)))
143
+ half_track = d64_half_track(image, track)
144
+ zone = zone((half_track % SIDE) / 2)
145
+ angle = half_track == SIDE + 2 ? 0 : (angle + SKEWS[zone]) % SKEW_TURN if track > 1
146
+ bytes = format_track(image, track, id, zone)
147
+ disk.write(half_track, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone))
95
148
  end
96
149
  disk
97
150
  end
98
151
 
99
- def self.format_track(image, track, id)
152
+ def self.format_track(image, track, id, zone = zone(track))
100
153
  sectors = image.sectors_in(track)
101
- length = TRACK_LENGTHS[zone(track)]
154
+ length = TRACK_LENGTHS[zone]
102
155
  gap = (length - (sectors * SECTOR_LENGTH)) / sectors
103
156
  bytes = []
104
157
  sectors.times do |sector|
@@ -146,7 +199,7 @@ module Badline
146
199
 
147
200
  def initialize(image = nil)
148
201
  @image = image
149
- @tracks = Array.new(MAX_HALF_TRACK + 1)
202
+ @tracks = Array.new(SIDE * 2)
150
203
  @written = {}
151
204
  @path = nil
152
205
  @read_only = false
@@ -188,11 +241,14 @@ module Badline
188
241
  def flush
189
242
  return flush_tracks if @image.respond_to?(:store_tracks)
190
243
 
191
- tracks = @written.keys.select { |half| half.even? && (1..@image&.track_count.to_i).cover?(half / 2) }
244
+ tracks = @written.keys.filter_map do |half|
245
+ track = @image && Disk.d64_track(@image, half)
246
+ [half, track] if track
247
+ end
192
248
  @written.clear
193
249
  return if tracks.empty?
194
250
 
195
- sectors = tracks.to_h { |half| [half / 2, SectorReader.read(@tracks[half].bytes, half / 2)] }
251
+ sectors = tracks.to_h { |half, track| [track, SectorReader.read(@tracks[half].bytes, track)] }
196
252
  id = disk_id(sectors)
197
253
  warn_lost(sectors)
198
254
  @image.store_blocks(sectors.flat_map do |track, found|
@@ -203,9 +259,10 @@ module Badline
203
259
  private
204
260
 
205
261
  def flush_tracks
206
- tracks = @written.keys.sort.to_h do |half|
207
- [half - Mechanism::MIN_HALF_TRACK, [@tracks[half].bytes, @tracks[half].zone]]
208
- end
262
+ tracks = @written.keys.sort.filter_map do |half|
263
+ entry = Disk.entry_of(half)
264
+ [entry, [@tracks[half].bytes, @tracks[half].zone]] if entry
265
+ end.to_h
209
266
  @written.clear
210
267
  @image.store_tracks(tracks) unless tracks.empty?
211
268
  end
@@ -12,13 +12,18 @@ module Badline
12
12
  # for a disk from Disk.open. Nil for a disk made another way.
13
13
  attr_reader :path, :read_only
14
14
 
15
- # A .g64 image by its name, and anything else a .d64.
16
- def self.g64?(path) = File.extname(path).casecmp?(".g64")
15
+ # A .g64 or .g71 image by its name, a .d71 by its name, and anything
16
+ # else a .d64.
17
+ def self.g64?(path) = %w[.g64 .g71].include?(File.extname(path).downcase)
18
+
19
+ def self.d71?(path) = File.extname(path).casecmp?(".d71")
17
20
 
18
21
  # A disk for the image at the path, its tracks left for the state.
19
22
  def self.reopen(path, read_only)
20
23
  if g64?(path)
21
24
  Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
25
+ elsif d71?(path)
26
+ Disk.new(Storage::D71Image.new(path, read_only:)).opened(path, read_only)
22
27
  else
23
28
  Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
24
29
  end
@@ -7,8 +7,11 @@ module Badline
7
7
  # hears both after every write and reset, since ORB, DDRB, the ACR and
8
8
  # the PCR can all change them.
9
9
  class DiskVIA < VIA
10
+ attr_reader :mechanism
11
+
10
12
  def initialize(start:, mechanism:)
11
13
  @mechanism = mechanism
14
+ mechanism.via = self
12
15
  super(start:, peripheral: mechanism)
13
16
  end
14
17
 
@@ -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 Drive1541#byte_ready!).
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
- def initialize(drive)
85
- @drive = drive
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 = 0
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
- # Drive1541::Idle).
149
+ # Drive::Idle).
121
150
  def idle_state
122
- [@motor, @led, @zone, @clock, @half_track, @slip, @disk, @track, @index, @mask, @cell_end, @time,
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
- @drive.byte_ready_ended!
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
- @drive.byte_ready_ended!
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
- track = @disk.writable_track(@half_track, @zone)
231
- @disk.write(@half_track, track.relaid(@zone)) unless track.written_at?(@zone)
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(@half_track)
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 ? @drive.via2.port_a_output : @shift & 0xff
279
+ @write_shift = @writing ? @via.port_a_output : @shift & 0xff
250
280
  @byte_ready = true
251
- @drive.byte_ready!
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, @track_written].each do |flag|
16
- out.boolean(flag)
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 16 ticks, through whatever cells and clocks fall
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 += CYCLE
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(@half_track) || BLANK
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(@half_track) || BLANK
108
+ @track = @disk&.track(surface) || BLANK
109
109
  @bytes = @track.bytes
110
110
  @widths = @track.widths
111
111
  @length = @track.length