badline 0.17.0 → 0.19.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +193 -0
  3. data/CLAUDE.md +19 -7
  4. data/CONTRIBUTING.md +11 -1
  5. data/README.md +178 -26
  6. data/doc/library.md +5 -1
  7. data/doc/media.md +3 -2
  8. data/doc/pinned-behaviour.md +169 -1
  9. data/doc/snapshot-format.md +302 -35
  10. data/doc/snapshots.md +97 -26
  11. data/doc/vice.md +344 -0
  12. data/exe/badline-ruby +5 -0
  13. data/lib/badline/audio/renderer.rb +4 -8
  14. data/lib/badline/audio/terminal.rb +34 -2
  15. data/lib/badline/c128/keyed_state.rb +2 -0
  16. data/lib/badline/c128.rb +5 -0
  17. data/lib/badline/c64/bus/fittings.rb +6 -0
  18. data/lib/badline/c64/keyed_state.rb +2 -0
  19. data/lib/badline/c64.rb +7 -0
  20. data/lib/badline/cbm2/keyed_state.rb +4 -0
  21. data/lib/badline/cbm2/p500.rb +9 -3
  22. data/lib/badline/cbm2.rb +10 -3
  23. data/lib/badline/chips/cia/timer.rb +2 -2
  24. data/lib/badline/chips/cpu/core.rb +29 -4
  25. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  26. data/lib/badline/chips/cpu/interrupts.rb +9 -0
  27. data/lib/badline/chips/cpu/keyed_state.rb +15 -9
  28. data/lib/badline/chips/cpu/microcode.rb +10 -0
  29. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  30. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  31. data/lib/badline/chips/m6809/core.rb +149 -0
  32. data/lib/badline/chips/m6809/indexed.rb +82 -0
  33. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  34. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  35. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  36. data/lib/badline/chips/m6809/operations.rb +258 -0
  37. data/lib/badline/chips/m6809/plans.rb +143 -0
  38. data/lib/badline/chips/m6809/registers.rb +82 -0
  39. data/lib/badline/chips/m6809/stack.rb +140 -0
  40. data/lib/badline/chips/m6809.rb +13 -0
  41. data/lib/badline/chips/riot/timer.rb +14 -3
  42. data/lib/badline/chips/rriot.rb +8 -3
  43. data/lib/badline/chips/sid/decimator.rb +1 -1
  44. data/lib/badline/chips/sid/filter.rb +4 -4
  45. data/lib/badline/chips/sid/keyed_parts.rb +51 -18
  46. data/lib/badline/chips/sid/waveform.rb +6 -6
  47. data/lib/badline/chips/sid.rb +6 -2
  48. data/lib/badline/chips/vic_i/keyed_parts.rb +12 -2
  49. data/lib/badline/chips/vic_i/light_pen.rb +89 -0
  50. data/lib/badline/chips/vic_i.rb +91 -23
  51. data/lib/badline/data_path.rb +3 -3
  52. data/lib/badline/drives/ieee_models.rb +54 -0
  53. data/lib/badline/drives/model1001.rb +37 -0
  54. data/lib/badline/drives/model1541/sector_reader.rb +20 -3
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551.rb +1 -0
  57. data/lib/badline/drives/model1571.rb +1 -0
  58. data/lib/badline/drives/model1581.rb +1 -0
  59. data/lib/badline/drives/model2031.rb +7 -0
  60. data/lib/badline/drives/model4040/disk.rb +62 -0
  61. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  62. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  63. data/lib/badline/drives/model4040/mechanism.rb +195 -0
  64. data/lib/badline/drives/model4040/parts.rb +20 -0
  65. data/lib/badline/drives/model4040.rb +195 -0
  66. data/lib/badline/drives/model8050/board.rb +79 -0
  67. data/lib/badline/drives/model8050/dos_board.rb +134 -0
  68. data/lib/badline/drives/model8050/fdc_bus.rb +7 -0
  69. data/lib/badline/drives/model8050/idle.rb +148 -31
  70. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  71. data/lib/badline/drives/model8050.rb +32 -130
  72. data/lib/badline/drives/model9060/idle.rb +169 -0
  73. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  74. data/lib/badline/drives/model9060/parts.rb +98 -0
  75. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  76. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  77. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  78. data/lib/badline/drives/model9060.rb +197 -0
  79. data/lib/badline/frontend/app.rb +8 -0
  80. data/lib/badline/frontend/boot.rb +3 -2
  81. data/lib/badline/frontend/controls.rb +62 -6
  82. data/lib/badline/frontend/keys.rb +36 -0
  83. data/lib/badline/frontend/kim1_panel.rb +208 -0
  84. data/lib/badline/frontend/media_slots.rb +8 -1
  85. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  86. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  87. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  88. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  89. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  90. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  91. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  92. data/lib/badline/frontend/screen.rb +1 -1
  93. data/lib/badline/frontend/screens.rb +40 -2
  94. data/lib/badline/frontend/teletype_pane.rb +152 -0
  95. data/lib/badline/frontend/timeline.rb +31 -2
  96. data/lib/badline/frontend.rb +3 -0
  97. data/lib/badline/input/joystick.rb +15 -2
  98. data/lib/badline/kim1/bus.rb +117 -0
  99. data/lib/badline/kim1/cpu.rb +25 -0
  100. data/lib/badline/kim1/host_terminal.rb +74 -0
  101. data/lib/badline/kim1/key_entry.rb +74 -0
  102. data/lib/badline/kim1/keyed_modules.rb +53 -0
  103. data/lib/badline/kim1/keyed_state.rb +133 -0
  104. data/lib/badline/kim1/panel.rb +176 -0
  105. data/lib/badline/kim1/rriots.rb +28 -0
  106. data/lib/badline/kim1/sound.rb +29 -0
  107. data/lib/badline/kim1/tape.rb +118 -0
  108. data/lib/badline/kim1/teletype.rb +193 -0
  109. data/lib/badline/kim1.rb +194 -0
  110. data/lib/badline/machine/ieee_drives.rb +94 -117
  111. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  112. data/lib/badline/machine/line_sound.rb +122 -0
  113. data/lib/badline/machine.rb +49 -15
  114. data/lib/badline/media/extensions.rb +8 -4
  115. data/lib/badline/media/kim1_media.rb +150 -0
  116. data/lib/badline/media/pet_media.rb +3 -1
  117. data/lib/badline/media/true_drive.rb +52 -45
  118. data/lib/badline/media/vic20_media.rb +4 -2
  119. data/lib/badline/media.rb +16 -6
  120. data/lib/badline/memory.rb +2 -4
  121. data/lib/badline/options/family.rb +98 -8
  122. data/lib/badline/options/help.rb +44 -12
  123. data/lib/badline/options/table.rb +44 -12
  124. data/lib/badline/options/validation.rb +44 -8
  125. data/lib/badline/options.rb +27 -8
  126. data/lib/badline/pet/bus.rb +16 -2
  127. data/lib/badline/pet/keyboard_ports.rb +9 -2
  128. data/lib/badline/pet/keyed_state.rb +4 -0
  129. data/lib/badline/pet/model.rb +11 -2
  130. data/lib/badline/pet/rom_sets.rb +1 -0
  131. data/lib/badline/pet/sound.rb +5 -105
  132. data/lib/badline/pet/superpet/acia.rb +12 -0
  133. data/lib/badline/pet/superpet/board.rb +221 -0
  134. data/lib/badline/pet/superpet/dongle.rb +92 -0
  135. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  136. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  137. data/lib/badline/pet/superpet/m6809.rb +32 -0
  138. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  139. data/lib/badline/pet/superpet.rb +221 -0
  140. data/lib/badline/pet.rb +23 -3
  141. data/lib/badline/plus4/keyed_state.rb +2 -0
  142. data/lib/badline/plus4.rb +5 -0
  143. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  144. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  145. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  146. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  147. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  148. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  149. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  150. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  151. data/lib/badline/roms/README +176 -0
  152. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  153. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  154. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  155. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  156. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  157. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  158. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  159. data/lib/badline/snapshot/container.rb +8 -4
  160. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  161. data/lib/badline/snapshot/field_doc.rb +35 -10
  162. data/lib/badline/snapshot/image.rb +19 -7
  163. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  164. data/lib/badline/snapshot/pet_setup.rb +27 -11
  165. data/lib/badline/snapshot/storage_setup.rb +4 -1
  166. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  167. data/lib/badline/snapshot/vice/pet.rb +14 -7
  168. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  169. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  170. data/lib/badline/snapshot/vice/pet_dongle.rb +72 -0
  171. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  172. data/lib/badline/snapshot/vice/pet_mem.rb +55 -5
  173. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  174. data/lib/badline/snapshot/vice/vic20.rb +70 -0
  175. data/lib/badline/snapshot/vice/vic20_cpu.rb +61 -0
  176. data/lib/badline/snapshot/vice/vic20_mem.rb +85 -0
  177. data/lib/badline/snapshot/vice/vic20_restore.rb +124 -0
  178. data/lib/badline/snapshot/vice/vic20_vici.rb +200 -0
  179. data/lib/badline/snapshot/vice.rb +1 -0
  180. data/lib/badline/snapshot.rb +8 -4
  181. data/lib/badline/status.rb +30 -30
  182. data/lib/badline/storage/d67_image.rb +55 -0
  183. data/lib/badline/storage/d90_image.rb +180 -0
  184. data/lib/badline/storage/kim_tape.rb +254 -0
  185. data/lib/badline/storage/papertape.rb +90 -0
  186. data/lib/badline/storage.rb +4 -0
  187. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  188. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  189. data/lib/badline/version.rb +1 -1
  190. data/lib/badline/vic20/keyed_modules.rb +6 -3
  191. data/lib/badline/vic20/keyed_state.rb +6 -4
  192. data/lib/badline/vic20/port_wiring.rb +11 -0
  193. data/lib/badline/vic20.rb +30 -12
  194. data/lib/badline.rb +1 -0
  195. metadata +85 -1
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ class Disk
7
+ def image_format = @image.is_a?(Storage::D67Image) ? "d67" : "d64"
8
+
9
+ # The disk a BL-D8-DISK module holds, as Model8050::Disk's, from a
10
+ # .d64 or a .d67.
11
+ def self.from_fields(input, detached: false)
12
+ path = input.text("path", "")
13
+ read_only = input.bool("read_only", false) || detached
14
+ kind = input.choice("image.format", %w[d64 d67], "d64")
15
+ bytes = input.bytes("image", "".b).bytes
16
+ path = "" if detached || !File.file?(path)
17
+ image = if kind == "d67"
18
+ Storage::D67Image.new(path, read_only:, bytes:)
19
+ else
20
+ Storage::D64Image.new(path, read_only:, bytes:)
21
+ end
22
+ new(image).load_track_fields(input)
23
+ end
24
+ end
25
+
26
+ class Mechanism
27
+ # The drives' fields in a keyed snapshot module (BL-D8-MECH), a value
28
+ # a drive in each list: the heads' half tracks, the stepper counts,
29
+ # each disk's place in its turn and its bit clock and the motors,
30
+ # then byte ready, the byte on VIA port A and SYNC. The disks load
31
+ # from their own modules.
32
+ module KeyedState
33
+ def save_fields(out)
34
+ out.ints("half_tracks", @half_tracks).ints("phases", @phases).ints("positions", @positions)
35
+ out.ints("bit_clocks", @clocks)
36
+ out.ints("motors", @motors.map { |on| on ? 1 : 0 }).ints("syncs", @syncs.map { |low| low ? 1 : 0 })
37
+ out.bool("ca1_low", @ca1_low).int("data_in", @data_in)
38
+ end
39
+
40
+ def load_fields(input, disks)
41
+ @disks = disks
42
+ input.ints_into("half_tracks", @half_tracks)
43
+ input.ints_into("phases", @phases)
44
+ input.ints_into("positions", @positions)
45
+ input.ints_into("bit_clocks", @clocks)
46
+ @motors = input.ints("motors", [0, 0]).map { |on| on == 1 }
47
+ @syncs = input.ints("syncs", [0, 0]).map { |low| low == 1 }
48
+ @ca1_low = input.bool("ca1_low", @ca1_low)
49
+ @data_in = input.byte("data_in", @data_in)
50
+ end
51
+ end
52
+
53
+ include KeyedState
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The unit's two SA390 drives as the 6504 sees them through the VIA
7
+ # and the 6530 (schematic 320806, sheets 2 and 3).
8
+ #
9
+ # VIA port B runs the drives: PB5 drive 0's motor and PB4 drive 1's,
10
+ # each on while low, PB2-3 drive 0's stepper and PB0-1 drive 1's. The
11
+ # 6530's port B picks the drive on PB0 and the zone on PB1-2, and
12
+ # reads write protect on PB3, high while the disk can't be written,
13
+ # and SYNC on PB6, low while ten one bits sit in the shift register.
14
+ # Its port A holds the byte to write.
15
+ #
16
+ # Each stepper's two bits count in binary: one up moves the head half
17
+ # a track in, one down half a track out, and the controller steps
18
+ # once an interrupt, twice a track ($FF82), bumping the head against
19
+ # track 1 with 116 steps out.
20
+ #
21
+ # A turning disk passes one byte cell under the head every ten bit
22
+ # cells, at 16 MHz divided by 16 less the track's zone and then by 4,
23
+ # and the byte cells come round 300 times a minute (Disk). Each data
24
+ # cell ends in byte ready, which latches the byte into VIA port A
25
+ # through CA1; the controller has no SO pin and polls the VIA's
26
+ # interrupt flags for it. A sync cell holds SYNC low instead. While
27
+ # the VIA's CB2 is low the drive writes: each cell takes the 6530's
28
+ # port A, or a sync while CA2 is high, and still ends in byte ready.
29
+ # The format code holds CA2 high in pulse mode, which reading port A
30
+ # pulls low for a cycle.
31
+ class Mechanism
32
+ WRITE_PROTECT = 0x08
33
+ SYNC_LINE = 0x40
34
+
35
+ # The half tracks a head can reach: track 1 to 35.
36
+ HALF_TRACKS = 70
37
+
38
+ attr_reader :data_in, :half_tracks
39
+ attr_accessor :via, :rriot
40
+
41
+ def initialize
42
+ @via = nil
43
+ @rriot = nil
44
+ @disks = [nil, nil]
45
+ @half_tracks = [0, 0]
46
+ @phases = [0, 0]
47
+ @positions = [0, 0]
48
+ @clocks = [0, 0]
49
+ @motors = [false, false]
50
+ @data_in = 0xff
51
+ @syncs = [false, false]
52
+ @ca1_low = false
53
+ end
54
+
55
+ # The Disk in +drive+, 0 or 1, or nil.
56
+ def disk(drive = 0) = @disks[drive]
57
+
58
+ def insert(disk, drive = 0)
59
+ @disks[drive]&.flush
60
+ @disks[drive] = disk
61
+ end
62
+
63
+ def flush
64
+ @disks.each { |disk| disk&.flush }
65
+ end
66
+
67
+ def motor_on?(drive = 0) = @motors[drive]
68
+
69
+ # The track under +drive+'s head, 1 to 35.
70
+ def track(drive = 0) = (@half_tracks[drive] >> 1) + 1
71
+
72
+ def cycle!
73
+ if @ca1_low
74
+ @ca1_low = false
75
+ @via.ca1 = true
76
+ end
77
+ drives = @via.port_b_output
78
+ turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
79
+ turn(1, drives.nobits?(0x10), drives & 0x03)
80
+ end
81
+
82
+ # Whether a cycle would change nothing: no byte ready under way, and
83
+ # each drive's motor off or turning with no disk in it, as both turn
84
+ # from the bump at boot until the run-down.
85
+ def quiet? = !@ca1_low && still?(0) && still?(1)
86
+
87
+ # What the drives hold, for comparing them at two points.
88
+ def idle_state = [*@half_tracks, *@phases, *@motors, *@syncs, @data_in]
89
+
90
+ # The 6530's port B inputs.
91
+ def status_lines
92
+ drive = selected_drive
93
+ lines = 0xff
94
+ lines &= ~WRITE_PROTECT unless @disks[drive]&.write_protected?
95
+ @syncs[drive] ? lines & ~SYNC_LINE : lines
96
+ end
97
+
98
+ private
99
+
100
+ def selected_drive = @rriot.port_b_output & 0x01
101
+
102
+ def still?(drive) = !@motors[drive] || @disks[drive].nil?
103
+
104
+ def turn(drive, motor, phase)
105
+ step(drive, phase) if phase != @phases[drive]
106
+ motor_off(drive) if @motors[drive] && !motor
107
+ @motors[drive] = motor
108
+ return @syncs[drive] = false unless motor && @disks[drive]
109
+
110
+ clock = @clocks[drive] + 2
111
+ byte = 5 * (16 - Disk.zone_of(track(drive)))
112
+ if clock >= byte
113
+ clock -= byte
114
+ pass_cell(drive)
115
+ end
116
+ @clocks[drive] = clock
117
+ end
118
+
119
+ def step(drive, phase)
120
+ change = (phase - @phases[drive]) & 0x03
121
+ @phases[drive] = phase
122
+ half_track = @half_tracks[drive]
123
+ if change == 1
124
+ half_track += 1
125
+ elsif change == 3
126
+ half_track -= 1
127
+ end
128
+ @half_tracks[drive] = half_track.clamp(0, HALF_TRACKS - 1)
129
+ end
130
+
131
+ def motor_off(drive)
132
+ @disks[drive]&.flush
133
+ end
134
+
135
+ # A cell has passed under +drive+'s head.
136
+ def pass_cell(drive)
137
+ disk = @disks[drive]
138
+ number = track(drive)
139
+ cells = disk.track(number)
140
+ return unless cells
141
+
142
+ position = @positions[drive] % cells.length
143
+ @positions[drive] = position + 1
144
+ return unless drive == selected_drive
145
+
146
+ writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
147
+ end
148
+
149
+ def writing? = !@via.cb2_output
150
+
151
+ def read_cell(drive, cells, position)
152
+ cell = cells[position]
153
+ @syncs[drive] = cell == Disk::SYNC
154
+ byte_ready(cell) unless cell == Disk::SYNC
155
+ end
156
+
157
+ def write_cell(drive, disk, number, cells, position)
158
+ cells[position] = @via.ca2_output ? Disk::SYNC : @rriot.port_a_output
159
+ disk.written(number)
160
+ @syncs[drive] = false
161
+ byte_ready(@data_in)
162
+ end
163
+
164
+ def byte_ready(byte)
165
+ @data_in = byte
166
+ @via.ca1 = false
167
+ @ca1_low = true
168
+ end
169
+ end
170
+
171
+ # VIA port A reads the byte the last cell latched. Port B drives the
172
+ # drives only.
173
+ class ReadLines
174
+ def initialize(mechanism)
175
+ @mechanism = mechanism
176
+ end
177
+
178
+ def read_a(_lines) = @mechanism.data_in
179
+
180
+ def read_b(_lines) = 0xff
181
+ end
182
+
183
+ # The 6530's port B inputs from the drives.
184
+ class StatusLines
185
+ def initialize(mechanism)
186
+ @mechanism = mechanism
187
+ end
188
+
189
+ def read_a(_lines) = 0xff
190
+
191
+ def read_b(_lines) = @mechanism.status_lines
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The interface processor's 6502, UN1, on the 4040's IPBus.
7
+ class IPCPU
8
+ include Badline::Chips::CPU::Core
9
+ include Engine::IdleCPU
10
+ end
11
+
12
+ # The floppy controller's 6504, UH3, on the 8050's FDCBus. It has no
13
+ # SO pin.
14
+ class FDCCPU
15
+ include Badline::Chips::CPU::Core
16
+ include Engine::IdleCPU
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model8050"
4
+ require "badline/drives/model4040/ip_bus"
5
+ require "badline/drives/model4040/parts"
6
+ require "badline/drives/model4040/disk"
7
+ require "badline/drives/model4040/mechanism"
8
+ require "badline/drives/model4040/keyed_state"
9
+
10
+ module Badline
11
+ module Drives
12
+ # The 4040 dual disk drive of the PET and CBM-II, two Shugart SA390
13
+ # drives on one IEEE-488 unit (schematic 320806, the 8050's parent).
14
+ #
15
+ # The board is the 8050's (Model8050::Board) with an older floppy side.
16
+ # A 16 MHz crystal clocks both processors at 1 MHz on opposite phases.
17
+ # The interface processor runs DOS 2.1 from 12 KB of ROM at $D000, with
18
+ # the 8050's 6532s on the bus and the same common RAM, on a bus that
19
+ # doesn't decode A15 (IPBus). The floppy controller is a 6504, with no
20
+ # SO pin, on the 8050's map (Model8050::FDCBus): it runs the program in
21
+ # its 6530 (901466-04) and polls the VIA for byte ready, whose own
22
+ # interrupt line goes nowhere, so only the 6530's timer interrupts it
23
+ # (Mechanism). The disks are the 1541's, 35 tracks of 17 to 21
24
+ # sectors, a .d64 each (Disk).
25
+ #
26
+ # At reset the controller waits for the common RAM's $0400 to read 0,
27
+ # starts, and writes its timer's period there. The interface processor
28
+ # tests its RAM and ROMs, has the controller reset its drives, and
29
+ # bumps both heads to track 1. The motors run from then until the
30
+ # run-down after the first job, about 9.5 million host cycles in for a
31
+ # directory LOAD, or 8.8 million with no job. A motor turning with no
32
+ # disk in its drive changes nothing, so a unit with no disks sleeps
33
+ # from the bump on, and one with a disk only after the run-down.
34
+ class Model4040
35
+ include Model8050::Board
36
+ include Model8050::Idle
37
+ include Model8050::KeyedState
38
+
39
+ CLOCK_HZ = Model8050::CLOCK_HZ
40
+
41
+ # The host cycles by which a unit switched on with the machine has
42
+ # run the IP's tests and bumped both heads, and reads its jumpers.
43
+ BOOT_CYCLES = 2_500_000
44
+
45
+ # Where DOS 2.1's idle loop and the controller's job loop start over
46
+ # (Model8050::Idle).
47
+ IP_LOOP = 0xd4b7
48
+ FDC_LOOP = 0xfc54
49
+
50
+ def model_name = "4040"
51
+
52
+ def boot_cycles = BOOT_CYCLES
53
+
54
+ def sides = 1
55
+
56
+ # +dos_rom+ covers $D000-$FFFF and +fdc_rom+ is the 6530's mask, each
57
+ # defaulting to the image in the ROM path: the revised DOS 2.1 that
58
+ # xpet's 4040 runs, and 901466-04. +device+ is the number the jumpers
59
+ # set, 8 to 15.
60
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
61
+ @device = device
62
+ @host_clock_hz = host_clock_hz
63
+ @phase = 0
64
+ @cycles = 0
65
+ build_ip(dos_rom || ROM.load(dos_file, dos_start))
66
+ build_fdc(fdc_rom || ROM.read(fdc_file))
67
+ @ip_cpu = IPCPU.new(@ip_bus)
68
+ @ip_cpu.power_on!
69
+ @fdc_cpu = FDCCPU.new(@fdc_bus)
70
+ @fdc_cpu.power_on!
71
+ init_idle(ip_loop, fdc_loop)
72
+ @ieee_bus.attach(self)
73
+ end
74
+
75
+ # Puts in the .d64 image at +path+, or the DOS 1 disk a .d67 holds,
76
+ # write-protected with `read_only`.
77
+ def insert_image(path, drive: 0, read_only: false)
78
+ image = if File.extname(path).casecmp?(".d67")
79
+ Storage::D67Image.new(path, read_only:)
80
+ else
81
+ Storage::D64Image.new(path, read_only:)
82
+ end
83
+ insert(Disk.new(image), drive:)
84
+ end
85
+
86
+ private
87
+
88
+ def dos_file = "4040/dos-2.1-rev.rom"
89
+
90
+ def dos_start = 0xd000
91
+
92
+ def fdc_file = "4040/fdc-901466-04.rom"
93
+
94
+ def ip_loop = IP_LOOP
95
+
96
+ def fdc_loop = FDC_LOOP
97
+
98
+ def build_ip(rom)
99
+ @common = Memory.new([], length: 0x1000)
100
+ @ieee_bus = IEEEBus.new
101
+ @ieee_port = Model8050::IEEEPort.new(device: @device, bus: @ieee_bus)
102
+ @data_riot = Model8050::DataRIOT.new(peripheral: Model8050::DataLines.new(@ieee_port))
103
+ @control_riot = Model8050::ControlRIOT.new(peripheral: Model8050::ControlLines.new(@ieee_port))
104
+ @ieee_port.data_riot = @data_riot
105
+ @ieee_port.control_riot = @control_riot
106
+ @ip_bus = IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
107
+ end
108
+
109
+ def build_fdc(rom)
110
+ @mechanism = Mechanism.new
111
+ @via = Model8050::ControllerVIA.new(start: 0x0040, peripheral: ReadLines.new(@mechanism))
112
+ @rriot = Model8050::ControllerRRIOT.new(rom:, peripheral: StatusLines.new(@mechanism))
113
+ @fdc_bus = Model8050::FDCBus.new(common: @common, via: @via, rriot: @rriot)
114
+ @mechanism.via = @via
115
+ @mechanism.rriot = @rriot
116
+ end
117
+
118
+ # The disk a BL-D8-DISK module holds (Model8050::KeyedState).
119
+ def disk_from_fields(input, detached) = Disk.from_fields(input, detached:)
120
+
121
+ # One cycle of the board: the drives, then the chips, then the IP and
122
+ # the FDC, each in its own half of the cycle, as on the 8050. Only the
123
+ # 6530 pulls the FDC's IRQ, and the FDC finds byte ready by polling.
124
+ def step
125
+ @mechanism.cycle!
126
+ @control_riot.pa7 = @ieee_port.atn_low?
127
+ @data_riot.cycle!
128
+ @control_riot.cycle!
129
+ @via.cycle!
130
+ @rriot.cycle!
131
+ @ip_cpu.irq = @control_riot.irq?
132
+ @ip_cpu.cycle!
133
+ @fdc_cpu.irq = @rriot.irq?
134
+ @fdc_cpu.cycle!
135
+ @cycles += 1
136
+ end
137
+
138
+ # One cycle of the FDC's side alone, as step runs it (Model8050::Idle).
139
+ def fdc_step
140
+ @mechanism.cycle!
141
+ @via.cycle!
142
+ @rriot.cycle!
143
+ @fdc_cpu.irq = @rriot.irq?
144
+ @fdc_cpu.cycle!
145
+ @cycles += 1
146
+ end
147
+ end
148
+
149
+ # The 3040: the 4040's board with the first DOS 2.1, as xpet's 3040
150
+ # runs it, 901468-11 to -13.
151
+ class Model3040 < Model4040
152
+ # Where the first DOS 2.1's idle loop starts over.
153
+ IP_LOOP = 0xd4b8
154
+
155
+ def model_name = "3040"
156
+
157
+ private
158
+
159
+ def dos_file = "4040/dos-2.1.rom"
160
+
161
+ def ip_loop = IP_LOOP
162
+ end
163
+
164
+ # The 2040: the 4040's board with DOS 1.2 in the sockets at $E000 and
165
+ # $F000, UJ1 at $D000 left empty, and 901466-02 in the controller's
166
+ # 6530, as the 3040 with DOS 1 has it too. Its disks are DOS 1's .d67,
167
+ # with 20 sectors on tracks 18 to 24 (Storage::D67Image). The
168
+ # controller starts at once, with no handshake on the common RAM, and
169
+ # the interface processor leaves the common RAM alone at boot.
170
+ class Model2040 < Model4040
171
+ # Where DOS 1.2's idle loop and its controller's job loop start over.
172
+ IP_LOOP = 0xe27e
173
+ FDC_LOOP = 0xfc47
174
+
175
+ def model_name = "2040"
176
+
177
+ # Puts in the .d67 image at +path+, write-protected with `read_only`.
178
+ def insert_image(path, drive: 0, read_only: false)
179
+ insert(Disk.new(Storage::D67Image.new(path, read_only:)), drive:)
180
+ end
181
+
182
+ private
183
+
184
+ def dos_file = "4040/dos-1.2.rom"
185
+
186
+ def dos_start = 0xe000
187
+
188
+ def fdc_file = "4040/fdc-901466-02.rom"
189
+
190
+ def ip_loop = IP_LOOP
191
+
192
+ def fdc_loop = FDC_LOOP
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The board of a unit with two processors on the IEEE-488 bus, as the
7
+ # 8050 family and the 4040 family share it: the DOS board's interface
8
+ # processor side (DOSBoard), and the floppy controller with its VIA
9
+ # and 6530 facing the drives. Each unit's class includes it, with
10
+ # Idle and KeyedState, and builds its own chips, so no call it makes
11
+ # on them is a union: the class sets DOSBoard's fields and @fdc_cpu,
12
+ # @fdc_bus, @rriot and @mechanism, and runs one cycle of the board in
13
+ # its own step. While both processors idle, it sleeps (Idle).
14
+ module Board
15
+ include DOSBoard
16
+
17
+ def fdc_cpu = settled(@fdc_cpu)
18
+
19
+ def fdc_bus = settled(@fdc_bus)
20
+
21
+ def rriot = settled(@rriot)
22
+
23
+ def mechanism = settled(@mechanism)
24
+
25
+ # Whether the unit is where VICE saves it: both processors between
26
+ # instructions, the FDC at the start of its idle loop.
27
+ def settled_for_vice?
28
+ fdc = fdc_cpu
29
+ ip_cpu.boundary? && fdc.boundary? && fdc.program_counter == @fdc_loop
30
+ end
31
+
32
+ # Whether a drive's LED is lit: of the LEDs, bit 0 is drive 1's,
33
+ # bit 1 drive 0's, and bit 2 the error LED.
34
+ def led_on? = leds.anybits?(0x03)
35
+
36
+ # Puts a Disk in +drive+, 0 or 1. Nil takes the disk out.
37
+ def insert(disk, drive: 0)
38
+ settle!
39
+ @mechanism.insert(disk, drive)
40
+ end
41
+
42
+ # The Disk in +drive+, or nil.
43
+ def disk(drive = 0) = @mechanism.disk(drive)
44
+
45
+ # Stores what the heads wrote in the disks' images, as the motors
46
+ # stopping do.
47
+ def flush
48
+ settle!
49
+ @mechanism.flush
50
+ end
51
+
52
+ # The bus's IFC line resets both processors and every chip. RAM keeps
53
+ # its contents.
54
+ def reset!
55
+ settle!
56
+ @data_riot.reset!
57
+ @control_riot.reset!
58
+ @via.reset!
59
+ @rriot.reset!
60
+ @ip_cpu.reset!
61
+ @fdc_cpu.reset!
62
+ end
63
+
64
+ def inspect
65
+ "#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) fdc=(#{@fdc_cpu.inspect})>"
66
+ end
67
+
68
+ private
69
+
70
+ def run_cycle
71
+ return fdc_alone_cycle if @fdc_alone
72
+
73
+ step
74
+ watch_loops if @idle_skip
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The half of the DOS board every unit with an interface processor
7
+ # shares: the interface processor with its two 6532s on the IEEE-488
8
+ # bus, the common RAM, and the second processor's VIA, whichever side
9
+ # the second processor runs. Board adds the floppy controller's side
10
+ # to it, and Model9060 the SASI host's. Each unit's class sets
11
+ # @ip_cpu, @ip_bus, @common, @data_riot, @control_riot, @via,
12
+ # @ieee_bus and @ieee_port, and has its own run_cycle and settle!.
13
+ #
14
+ # The unit clocks against the host through a phase accumulator, as
15
+ # Drives::Engine::Core does: each host cycle adds 1 MHz of phase, and
16
+ # every whole host period in it runs a cycle of both processors. While
17
+ # both processors idle, it sleeps instead, and the readers of its
18
+ # parts catch it up first. Asleep, a host cycle only counts towards
19
+ # the one planned to wake the unit, which its own doze wakes, and a
20
+ # line the controller moves wakes it at once (host_written!).
21
+ module DOSBoard
22
+ attr_reader :device, :ieee_bus
23
+
24
+ def ip_cpu = settled(@ip_cpu)
25
+
26
+ def ip_bus = settled(@ip_bus)
27
+
28
+ def common = settled(@common)
29
+
30
+ def data_riot = settled(@data_riot)
31
+
32
+ def control_riot = settled(@control_riot)
33
+
34
+ def via = settled(@via)
35
+
36
+ def cycles
37
+ settle!
38
+ @cycles
39
+ end
40
+
41
+ attr_writer :host_clock_hz
42
+
43
+ # Plugs the unit into an IEEE-488 bus, leaving the one it was on.
44
+ def connect(ieee_bus)
45
+ settle!
46
+ @ieee_bus.detach(self)
47
+ @ieee_bus = ieee_bus
48
+ @ieee_port.bus = ieee_bus
49
+ ieee_bus.attach(self)
50
+ end
51
+
52
+ # The lines the unit pulls on the bus while ATN is low or not
53
+ # (IEEEPort).
54
+ def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
55
+
56
+ # Which LEDs are lit, as the unit's port drives them. They hold
57
+ # still while the unit sleeps, so reading them leaves it asleep.
58
+ def leds = @ieee_port.leds
59
+
60
+ def power_led_on? = true
61
+
62
+ # Runs the drive cycles that fall in one host cycle, then takes the
63
+ # controller's lines for the next. Asleep, the unit owes the cycles
64
+ # instead, and wakes once the controller has moved a line, as it
65
+ # does with a processor owing cycles (dozing?).
66
+ def host_cycle!
67
+ return if slept_through?
68
+
69
+ phase = @phase + CLOCK_HZ
70
+ while phase >= @host_clock_hz
71
+ @asleep ? owe_cycle : run_cycle
72
+ phase -= @host_clock_hz
73
+ end
74
+ @phase = phase
75
+ settle! if dozing? && @ieee_port.lines_moved?
76
+ @ieee_port.latch_host
77
+ plan_sleep if @asleep
78
+ end
79
+
80
+ def cycle!
81
+ settle!
82
+ run_cycle
83
+ end
84
+
85
+ # The controller pushed its lines into the bus. A line it moved
86
+ # wakes a unit that owes cycles, which saw the lines as they stood.
87
+ def host_written!
88
+ settle! if dozing? && @ieee_port.lines_moved?
89
+ end
90
+
91
+ private
92
+
93
+ def settled(part)
94
+ settle!
95
+ part
96
+ end
97
+
98
+ # Asleep, a host cycle only counts towards the one planned to wake
99
+ # the unit (plan_sleep). Returns whether it did.
100
+ def slept_through?
101
+ return false unless @planned
102
+
103
+ @slept += 1
104
+ doze if @slept == @wake_at
105
+ true
106
+ end
107
+
108
+ # Plans the host cycle that brings the cycles owed up to the budget,
109
+ # once the unit has fallen asleep and the host cycle it fell asleep
110
+ # in is over.
111
+ def plan_sleep
112
+ return if @planned
113
+
114
+ @planned = true
115
+ @slept = 0
116
+ needed = ((@budget - @owed) * @host_clock_hz) - @phase
117
+ @wake_at = needed.positive? ? (needed + CLOCK_HZ - 1) / CLOCK_HZ : 1
118
+ end
119
+
120
+ # Turns the host cycles slept into cycles owed, through the phase
121
+ # accumulator.
122
+ def owe_slept
123
+ return unless @planned
124
+
125
+ @planned = false
126
+ phase = @phase + (@slept * CLOCK_HZ)
127
+ @owed += phase / @host_clock_hz
128
+ @phase = phase % @host_clock_hz
129
+ @slept = 0
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end