badline 0.16.1 → 0.18.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +213 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +265 -26
  6. data/doc/media.md +5 -2
  7. data/doc/pinned-behaviour.md +189 -0
  8. data/doc/snapshot-format.md +616 -74
  9. data/doc/snapshots.md +192 -24
  10. data/exe/badline-ruby +5 -0
  11. data/lib/badline/audio/terminal.rb +34 -2
  12. data/lib/badline/c128/keyed_state.rb +2 -0
  13. data/lib/badline/c128.rb +1 -0
  14. data/lib/badline/c64/keyed_state.rb +2 -0
  15. data/lib/badline/c64.rb +1 -0
  16. data/lib/badline/cbm2/acia.rb +10 -0
  17. data/lib/badline/cbm2/bus.rb +263 -0
  18. data/lib/badline/cbm2/cpu.rb +58 -0
  19. data/lib/badline/cbm2/crtc_video.rb +133 -0
  20. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  21. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  22. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  23. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  24. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  25. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  26. data/lib/badline/cbm2/keyed_state.rb +95 -0
  27. data/lib/badline/cbm2/model.rb +44 -0
  28. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  29. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  30. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  31. data/lib/badline/cbm2/p500/model.rb +39 -0
  32. data/lib/badline/cbm2/p500/pages.rb +57 -0
  33. data/lib/badline/cbm2/p500.rb +264 -0
  34. data/lib/badline/cbm2/sid_page.rb +19 -0
  35. data/lib/badline/cbm2.rb +270 -0
  36. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  37. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  38. data/lib/badline/chips/acia.rb +307 -0
  39. data/lib/badline/chips/cia/timer.rb +2 -2
  40. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  41. data/lib/badline/chips/crtc.rb +3 -2
  42. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  43. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  44. data/lib/badline/chips/m6809/core.rb +149 -0
  45. data/lib/badline/chips/m6809/indexed.rb +82 -0
  46. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  47. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  48. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  49. data/lib/badline/chips/m6809/operations.rb +258 -0
  50. data/lib/badline/chips/m6809/plans.rb +143 -0
  51. data/lib/badline/chips/m6809/registers.rb +82 -0
  52. data/lib/badline/chips/m6809/stack.rb +140 -0
  53. data/lib/badline/chips/m6809.rb +13 -0
  54. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  55. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  56. data/lib/badline/chips/riot/timer.rb +91 -0
  57. data/lib/badline/chips/riot.rb +162 -0
  58. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  59. data/lib/badline/chips/rriot.rb +134 -0
  60. data/lib/badline/chips/sid/filter.rb +4 -4
  61. data/lib/badline/chips/sid/waveform.rb +6 -6
  62. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  63. data/lib/badline/chips/tpi.rb +233 -0
  64. data/lib/badline/chips/vic/bank.rb +60 -5
  65. data/lib/badline/chips/vic/sprite.rb +4 -3
  66. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  67. data/lib/badline/chips/vic_i.rb +84 -21
  68. data/lib/badline/data_path.rb +3 -3
  69. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  70. data/lib/badline/drives/datasette.rb +27 -3
  71. data/lib/badline/drives/ieee_models.rb +54 -0
  72. data/lib/badline/drives/model1001.rb +37 -0
  73. data/lib/badline/drives/model2031.rb +6 -0
  74. data/lib/badline/drives/model4040/disk.rb +62 -0
  75. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  76. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  77. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  78. data/lib/badline/drives/model4040/parts.rb +20 -0
  79. data/lib/badline/drives/model4040.rb +179 -0
  80. data/lib/badline/drives/model8050/board.rb +77 -0
  81. data/lib/badline/drives/model8050/disk.rb +163 -0
  82. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  83. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  84. data/lib/badline/drives/model8050/idle.rb +181 -0
  85. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  86. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  87. data/lib/badline/drives/model8050/keyed_state.rb +163 -0
  88. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  89. data/lib/badline/drives/model8050/parts.rb +37 -0
  90. data/lib/badline/drives/model8050.rb +131 -0
  91. data/lib/badline/drives/model9060/idle.rb +149 -0
  92. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  93. data/lib/badline/drives/model9060/parts.rb +98 -0
  94. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  95. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  96. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  97. data/lib/badline/drives/model9060.rb +195 -0
  98. data/lib/badline/frontend/app.rb +18 -3
  99. data/lib/badline/frontend/boot.rb +4 -3
  100. data/lib/badline/frontend/controls.rb +107 -15
  101. data/lib/badline/frontend/gamepads.rb +3 -3
  102. data/lib/badline/frontend/keys.rb +97 -2
  103. data/lib/badline/frontend/kim1_panel.rb +208 -0
  104. data/lib/badline/frontend/media_slots.rb +8 -1
  105. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  106. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  107. data/lib/badline/frontend/menu/menu_media.rb +47 -14
  108. data/lib/badline/frontend/menu/menu_pages.rb +113 -4
  109. data/lib/badline/frontend/menu/menu_ports.rb +77 -4
  110. data/lib/badline/frontend/menu/pause_menu.rb +6 -2
  111. data/lib/badline/frontend/pet_keyboard.rb +45 -7
  112. data/lib/badline/frontend/screen.rb +1 -1
  113. data/lib/badline/frontend/screens.rb +40 -2
  114. data/lib/badline/frontend/teletype_pane.rb +152 -0
  115. data/lib/badline/frontend/timeline.rb +49 -11
  116. data/lib/badline/frontend.rb +3 -0
  117. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  118. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  119. data/lib/badline/kernal_trap/dos.rb +18 -5
  120. data/lib/badline/kernal_trap/ieee.rb +4 -2
  121. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  122. data/lib/badline/kim1/bus.rb +117 -0
  123. data/lib/badline/kim1/cpu.rb +25 -0
  124. data/lib/badline/kim1/host_terminal.rb +74 -0
  125. data/lib/badline/kim1/key_entry.rb +74 -0
  126. data/lib/badline/kim1/keyed_modules.rb +53 -0
  127. data/lib/badline/kim1/keyed_state.rb +133 -0
  128. data/lib/badline/kim1/panel.rb +176 -0
  129. data/lib/badline/kim1/rriots.rb +28 -0
  130. data/lib/badline/kim1/sound.rb +29 -0
  131. data/lib/badline/kim1/tape.rb +116 -0
  132. data/lib/badline/kim1/teletype.rb +193 -0
  133. data/lib/badline/kim1.rb +190 -0
  134. data/lib/badline/machine/ieee_drives.rb +171 -0
  135. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  136. data/lib/badline/machine/line_sound.rb +122 -0
  137. data/lib/badline/machine/settling.rb +4 -0
  138. data/lib/badline/machine.rb +62 -14
  139. data/lib/badline/media/cbm2_media.rb +29 -0
  140. data/lib/badline/media/extensions.rb +8 -4
  141. data/lib/badline/media/kim1_media.rb +150 -0
  142. data/lib/badline/media/pet_media.rb +3 -1
  143. data/lib/badline/media/program.rb +6 -3
  144. data/lib/badline/media/true_drive.rb +51 -25
  145. data/lib/badline/media.rb +23 -8
  146. data/lib/badline/options/family.rb +108 -6
  147. data/lib/badline/options/help.rb +58 -12
  148. data/lib/badline/options/table.rb +60 -14
  149. data/lib/badline/options/validation.rb +71 -11
  150. data/lib/badline/options.rb +46 -13
  151. data/lib/badline/pet/bus.rb +16 -2
  152. data/lib/badline/pet/cassettes.rb +22 -1
  153. data/lib/badline/pet/ieee_drive.rb +5 -64
  154. data/lib/badline/pet/ieee_ports.rb +7 -0
  155. data/lib/badline/pet/keyboard_ports.rb +9 -2
  156. data/lib/badline/pet/keyed_state.rb +4 -0
  157. data/lib/badline/pet/model.rb +11 -2
  158. data/lib/badline/pet/rom_sets.rb +1 -0
  159. data/lib/badline/pet/sound.rb +5 -105
  160. data/lib/badline/pet/superpet/acia.rb +12 -0
  161. data/lib/badline/pet/superpet/board.rb +221 -0
  162. data/lib/badline/pet/superpet/dongle.rb +81 -0
  163. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  164. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  165. data/lib/badline/pet/superpet/m6809.rb +32 -0
  166. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  167. data/lib/badline/pet/superpet.rb +216 -0
  168. data/lib/badline/pet.rb +33 -6
  169. data/lib/badline/plus4/acia.rb +4 -298
  170. data/lib/badline/plus4/keyed_parts.rb +0 -44
  171. data/lib/badline/plus4/keyed_state.rb +2 -0
  172. data/lib/badline/plus4.rb +2 -0
  173. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  174. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  175. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  176. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  177. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  178. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  179. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  180. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  181. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  182. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  183. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  184. data/lib/badline/roms/README +277 -0
  185. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  186. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  187. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  188. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  189. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  190. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  191. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  192. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  193. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  194. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  195. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  196. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  197. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  198. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  199. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  200. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  201. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  202. data/lib/badline/snapshot/container.rb +9 -2
  203. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  204. data/lib/badline/snapshot/field_doc.rb +59 -3
  205. data/lib/badline/snapshot/image.rb +65 -9
  206. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  207. data/lib/badline/snapshot/pet_setup.rb +27 -11
  208. data/lib/badline/snapshot/storage_setup.rb +4 -1
  209. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  210. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  211. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  212. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  213. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  214. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  215. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  216. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  217. data/lib/badline/snapshot/vice/cias.rb +13 -9
  218. data/lib/badline/snapshot/vice/p500.rb +96 -0
  219. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  220. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  221. data/lib/badline/snapshot/vice/pet.rb +20 -10
  222. data/lib/badline/snapshot/vice/pet_cpu.rb +42 -5
  223. data/lib/badline/snapshot/vice/pet_crtc.rb +25 -17
  224. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  225. data/lib/badline/snapshot/vice/pet_drive8050.rb +336 -0
  226. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  227. data/lib/badline/snapshot/vice/pet_restore.rb +100 -7
  228. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  229. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  230. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  231. data/lib/badline/snapshot/vice.rb +3 -0
  232. data/lib/badline/snapshot.rb +15 -3
  233. data/lib/badline/status.rb +30 -30
  234. data/lib/badline/storage/crt_file.rb +7 -1
  235. data/lib/badline/storage/d64_image.rb +7 -0
  236. data/lib/badline/storage/d67_image.rb +55 -0
  237. data/lib/badline/storage/d80_image.rb +17 -1
  238. data/lib/badline/storage/d81_image.rb +14 -0
  239. data/lib/badline/storage/d90_image.rb +180 -0
  240. data/lib/badline/storage/disk_image/bam.rb +18 -3
  241. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  242. data/lib/badline/storage/disk_image/writing.rb +37 -2
  243. data/lib/badline/storage/disk_image.rb +2 -0
  244. data/lib/badline/storage/kim_tape.rb +254 -0
  245. data/lib/badline/storage/papertape.rb +90 -0
  246. data/lib/badline/storage.rb +5 -0
  247. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  248. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  249. data/lib/badline/support/p500_checkpoint.rb +35 -0
  250. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  251. data/lib/badline/version.rb +1 -1
  252. data/lib/badline/vic20/keyed_modules.rb +6 -3
  253. data/lib/badline/vic20/keyed_state.rb +6 -4
  254. data/lib/badline/vic20.rb +26 -12
  255. data/lib/badline.rb +2 -0
  256. metadata +148 -2
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The unit's two drives as the floppy controller sees them through
7
+ # the VIA and the 6530 (schematic 8050001, 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 phases and PB0-1 drive
11
+ # 1's, and PB7 reads SYNC, low while ten one bits sit in the shift
12
+ # register. The 6530's port B picks the drive on PB0, the zone on
13
+ # PB1-2 and the side on PB4, low for the second, and reads write
14
+ # protect on PB3, high while the disk can't be written, and the sides
15
+ # on PB6, low for two, which jumper E3 grounds on the 8250. Its port
16
+ # A holds the byte to write.
17
+ #
18
+ # Each phase change moves the head a track: the controller steps
19
+ # through the Gray code 00, 10, 11, 01 outward and back the other way
20
+ # inward ($FF9C), and bumps the head against track 1 with 92 steps
21
+ # out.
22
+ #
23
+ # A turning disk passes one byte cell under the head every ten bit
24
+ # cells, at 6 MHz divided by 16 less the track's zone, and the byte
25
+ # cells come round 300 times a minute (Disk). Each data cell ends in
26
+ # byte ready, which latches the GCR ROM's byte into VIA port A through
27
+ # CA1 and pulses the controller's SO pin; a sync cell holds SYNC low
28
+ # for the cell after it instead. While the VIA's CB2 is low the drive
29
+ # writes: each cell takes the 6530's port A, or a sync while CA2 is
30
+ # high, and still ends in byte ready.
31
+ #
32
+ # A disk going in or out passes the write-protect sensor, which the
33
+ # controller watches for a disk change: it reads high for a while.
34
+ class Mechanism
35
+ WRITE_PROTECT = 0x08
36
+ ONE_SIDE = 0x40
37
+
38
+ # The cycles the write-protect sensor reads high as a disk goes in
39
+ # or out, ten of the controller's 5 ms interrupts.
40
+ CHANGE_CYCLES = 50_000
41
+
42
+ attr_reader :sides, :data_in, :cylinders, :so_pending
43
+ attr_accessor :via, :rriot
44
+
45
+ def initialize(sides:)
46
+ @sides = sides
47
+ @via = nil
48
+ @rriot = nil
49
+ @disks = [nil, nil]
50
+ @cylinders = [0, 0]
51
+ @phases = [0, 0]
52
+ @positions = [0, 0]
53
+ @clocks = [0, 0]
54
+ @changing = [0, 0]
55
+ @motors = [false, false]
56
+ @so_pending = false
57
+ @data_in = 0xff
58
+ @syncs = [false, false]
59
+ @ca1_low = false
60
+ end
61
+
62
+ # The Disk in +drive+, 0 or 1, or nil.
63
+ def disk(drive = 0) = @disks[drive]
64
+
65
+ def insert(disk, drive = 0)
66
+ @disks[drive]&.flush
67
+ @disks[drive] = disk
68
+ @changing[drive] = CHANGE_CYCLES
69
+ end
70
+
71
+ def flush
72
+ @disks.each { |disk| disk&.flush }
73
+ end
74
+
75
+ def motor_on?(drive = 0) = @motors[drive]
76
+
77
+ # The track under +drive+'s head on the side the 6530 picks.
78
+ def track(drive = 0)
79
+ side = drive == selected_drive ? selected_side : 0
80
+ @cylinders[drive] + 1 + (side * Storage::D80Image::SIDE_TRACKS)
81
+ end
82
+
83
+ def cycle!
84
+ if @ca1_low
85
+ @ca1_low = false
86
+ @via.ca1 = true
87
+ end
88
+ drives = @via.port_b_output
89
+ turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
90
+ turn(1, drives.nobits?(0x10), drives & 0x03)
91
+ end
92
+
93
+ # Whether a cycle would change nothing: both motors off, no disk
94
+ # going in or out, and no byte ready under way.
95
+ def quiet?
96
+ !@motors[0] && !@motors[1] && @changing[0].zero? && @changing[1].zero? && !@ca1_low && !@so_pending
97
+ end
98
+
99
+ # What the drives hold, for comparing them at two points.
100
+ def idle_state = [*@cylinders, *@phases, *@motors, *@syncs, @data_in]
101
+
102
+ # Whether byte ready pulses the controller's SO pin this cycle,
103
+ # which takes the pulse.
104
+ def take_so
105
+ so = @so_pending
106
+ @so_pending = false
107
+ so
108
+ end
109
+
110
+ # Whether ten one bits sit in the shift register, off the selected
111
+ # drive's head.
112
+ def sync? = @syncs[selected_drive]
113
+
114
+ # The 6530's port B inputs.
115
+ def status_lines
116
+ lines = 0xff
117
+ lines &= ~WRITE_PROTECT unless write_protected?(selected_drive)
118
+ @sides == 2 ? lines & ~ONE_SIDE : lines
119
+ end
120
+
121
+ private
122
+
123
+ def selected_drive = @rriot.port_b_output & 0x01
124
+
125
+ def selected_side = @sides == 2 && @rriot.port_b_output.nobits?(0x10) ? 1 : 0
126
+
127
+ def write_protected?(drive)
128
+ return true if @changing[drive].positive?
129
+
130
+ disk = @disks[drive]
131
+ disk ? disk.write_protected? : false
132
+ end
133
+
134
+ def turn(drive, motor, phase)
135
+ @changing[drive] -= 1 if @changing[drive].positive?
136
+ step(drive, phase) if phase != @phases[drive]
137
+ motor_off(drive) if @motors[drive] && !motor
138
+ @motors[drive] = motor
139
+ return @syncs[drive] = false unless motor && @disks[drive]
140
+
141
+ clock = @clocks[drive] + 6
142
+ byte = 10 * (16 - Disk.zone_of(track(drive)))
143
+ if clock >= byte
144
+ clock -= byte
145
+ pass_cell(drive)
146
+ end
147
+ @clocks[drive] = clock
148
+ end
149
+
150
+ # Gray code index of a phase pair: 00, 10, 11, 01.
151
+ def phase_index(phase) = [0, 3, 1, 2][phase]
152
+
153
+ def step(drive, phase)
154
+ change = (phase_index(phase) - phase_index(@phases[drive])) & 0x03
155
+ @phases[drive] = phase
156
+ cylinder = @cylinders[drive]
157
+ if change == 1
158
+ cylinder -= 1
159
+ elsif change == 3
160
+ cylinder += 1
161
+ end
162
+ @cylinders[drive] = cylinder.clamp(0, Storage::D80Image::SIDE_TRACKS - 1)
163
+ end
164
+
165
+ def motor_off(drive)
166
+ @disks[drive]&.flush
167
+ end
168
+
169
+ # A cell has passed under +drive+'s head.
170
+ def pass_cell(drive)
171
+ disk = @disks[drive]
172
+ number = track(drive)
173
+ cells = disk.track(number)
174
+ return unless cells
175
+
176
+ position = @positions[drive] % cells.length
177
+ @positions[drive] = position + 1
178
+ return unless drive == selected_drive
179
+
180
+ writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
181
+ end
182
+
183
+ def writing? = (@via.pcr & 0xe0) == 0xc0
184
+
185
+ def read_cell(drive, cells, position)
186
+ cell = cells[position]
187
+ @syncs[drive] = cell == Disk::SYNC
188
+ byte_ready(cell) unless cell == Disk::SYNC
189
+ end
190
+
191
+ def write_cell(drive, disk, number, cells, position)
192
+ cells[position] = @via.pcr.allbits?(0x0e) ? Disk::SYNC : @rriot.port_a_output
193
+ disk.written(number)
194
+ @syncs[drive] = false
195
+ byte_ready(@data_in)
196
+ end
197
+
198
+ def byte_ready(byte)
199
+ @data_in = byte
200
+ @via.ca1 = false
201
+ @ca1_low = true
202
+ @so_pending = true
203
+ end
204
+ end
205
+
206
+ # VIA port A reads the decoded byte, and PB7 SYNC, low while the
207
+ # shift register holds a sync.
208
+ class ReadLines
209
+ def initialize(mechanism)
210
+ @mechanism = mechanism
211
+ end
212
+
213
+ def read_a(_lines) = @mechanism.data_in
214
+
215
+ def read_b(_lines) = @mechanism.sync? ? 0x7f : 0xff
216
+ end
217
+
218
+ # The 6530's port B inputs from the drives.
219
+ class StatusLines
220
+ def initialize(mechanism)
221
+ @mechanism = mechanism
222
+ end
223
+
224
+ def read_a(_lines) = 0xff
225
+
226
+ def read_b(_lines) = @mechanism.status_lines
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # Each chip a class of its own, so each keeps one type of peripheral.
7
+
8
+ # UC1, on the IEEE-488 data lines.
9
+ class DataRIOT < Chips::RIOT
10
+ end
11
+
12
+ # UE1, on the control lines, the jumpers and the LEDs.
13
+ class ControlRIOT < Chips::RIOT
14
+ end
15
+
16
+ # UM3, the controller's VIA, at $0040.
17
+ class ControllerVIA < Chips::VIA
18
+ end
19
+
20
+ # UK3, the controller's 6530, its ROM the controller's program.
21
+ class ControllerRRIOT < Chips::RRIOT
22
+ end
23
+
24
+ # The interface processor's 6502, UN1, on the IPBus.
25
+ class IPCPU
26
+ include Badline::Chips::CPU::Core
27
+ include Engine::IdleCPU
28
+ end
29
+
30
+ # The floppy controller's 6502, UH3, on the FDCBus.
31
+ class FDCCPU
32
+ include Badline::Chips::CPU::Core
33
+ include Engine::IdleCPU
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/riot"
4
+ require "badline/chips/rriot"
5
+ require "badline/drives/ieee_bus"
6
+ require "badline/drives/engine"
7
+ require "badline/drives/model8050/ip_bus"
8
+ require "badline/drives/model8050/fdc_bus"
9
+ require "badline/drives/model8050/parts"
10
+ require "badline/drives/model8050/ieee_port"
11
+ require "badline/drives/model8050/disk"
12
+ require "badline/drives/model8050/mechanism"
13
+ require "badline/drives/model8050/dos_board"
14
+ require "badline/drives/model8050/board"
15
+ require "badline/drives/model8050/idle"
16
+ require "badline/drives/model8050/keyed_state"
17
+
18
+ module Badline
19
+ module Drives
20
+ # The 8050 dual disk drive of the PET and CBM-II, two drives on one
21
+ # IEEE-488 unit (schematic 8050001; the 8050/8250 service manual).
22
+ #
23
+ # Two 6502s share the board (Board), clocked at 1 MHz from one 12 MHz
24
+ # crystal on opposite phases. The interface processor (IP) runs DOS 2.7
25
+ # from 16 KB of ROM at $C000, with two 6532s (IPBus): their RAM is its
26
+ # zero page and stack, and their ports face the IEEE-488 bus
27
+ # (IEEEPort). The floppy controller (FDC) runs the 1 KB program in its
28
+ # 6530's ROM, with a VIA and the 6530 facing the drives (FDCBus,
29
+ # Mechanism). The two meet in 4 KB of common RAM, which each reaches in
30
+ # its own half of the cycle: the IP leaves jobs there, and the FDC runs
31
+ # them.
32
+ #
33
+ # Reset starts both. The FDC waits on the common RAM for the IP, which
34
+ # tests it, copies the FDC's start-up code into it ($C370) and waits for
35
+ # the FDC to answer, before both settle into their idle loops.
36
+ class Model8050
37
+ include Board
38
+ include Idle
39
+
40
+ CLOCK_HZ = 1_000_000
41
+
42
+ # The host cycles by which a unit switched on with the machine has
43
+ # run the IP's RAM test and the FDC's start-up and reads its jumpers.
44
+ BOOT_CYCLES = 1_500_000
45
+
46
+ def model_name = "8050"
47
+
48
+ def boot_cycles = BOOT_CYCLES
49
+
50
+ # The sides each drive's disk has.
51
+ def sides = 1
52
+
53
+ # +dos_rom+ covers $C000-$FFFF and +fdc_rom+ is the 6530's mask, each
54
+ # defaulting to the image in the ROM path: DOS 2.7 and the mask for
55
+ # MPI mechanisms. +device+ is the number the jumpers set, 8 to 15.
56
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
57
+ @device = device
58
+ @host_clock_hz = host_clock_hz
59
+ @phase = 0
60
+ @cycles = 0
61
+ build_ip(dos_rom || ROM.load("8050/dos-2.7.rom", 0xc000))
62
+ build_fdc(fdc_rom || ROM.read("8050/fdc-901869-01.rom"))
63
+ @ip_cpu = IPCPU.new(@ip_bus)
64
+ @fdc_cpu = FDCCPU.new(@fdc_bus)
65
+ init_idle(Idle::IP_LOOP, Idle::FDC_LOOP)
66
+ @ieee_bus.attach(self)
67
+ end
68
+
69
+ # Puts in the .d80 or .d82 image at +path+, write-protected with
70
+ # `read_only`.
71
+ def insert_image(path, drive: 0, read_only: false)
72
+ image_class = File.extname(path).casecmp?(".d82") ? Storage::D82Image : Storage::D80Image
73
+ insert(Disk.new(image_class.new(path, read_only:)), drive:)
74
+ end
75
+
76
+ private
77
+
78
+ def build_ip(rom)
79
+ @common = Memory.new([], length: 0x1000)
80
+ @ieee_bus = IEEEBus.new
81
+ @ieee_port = IEEEPort.new(device: @device, bus: @ieee_bus)
82
+ @data_riot = DataRIOT.new(peripheral: DataLines.new(@ieee_port))
83
+ @control_riot = ControlRIOT.new(peripheral: ControlLines.new(@ieee_port))
84
+ @ieee_port.data_riot = @data_riot
85
+ @ieee_port.control_riot = @control_riot
86
+ @ip_bus = IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
87
+ end
88
+
89
+ def build_fdc(rom)
90
+ @mechanism = Mechanism.new(sides:)
91
+ @via = ControllerVIA.new(start: 0x0040, peripheral: ReadLines.new(@mechanism))
92
+ @rriot = ControllerRRIOT.new(rom:, peripheral: StatusLines.new(@mechanism))
93
+ @fdc_bus = FDCBus.new(common: @common, via: @via, rriot: @rriot)
94
+ @mechanism.via = @via
95
+ @mechanism.rriot = @rriot
96
+ end
97
+
98
+ # The disk a BL-D8-DISK module holds (KeyedState).
99
+ def disk_from_fields(input, detached) = Disk.from_fields(input, detached:)
100
+
101
+ # One cycle of the board: the drives, then the chips, then the IP and
102
+ # the FDC, each in its own half of the cycle. ATN reaches UE1's PA7 as
103
+ # the port latched it. UE1 pulls the IP's IRQ, and the VIA and the
104
+ # 6530 the FDC's. Byte ready sets the FDC's V flag through SO for the
105
+ # next cycle's instruction step, as on the 1541.
106
+ def step
107
+ so = @mechanism.take_so
108
+ @mechanism.cycle!
109
+ @control_riot.pa7 = @ieee_port.atn_low?
110
+ @data_riot.cycle!
111
+ @control_riot.cycle!
112
+ @via.cycle!
113
+ @rriot.cycle!
114
+ @ip_cpu.irq = @control_riot.irq?
115
+ @ip_cpu.cycle!
116
+ @fdc_cpu.irq = @via.irq? || @rriot.irq?
117
+ @fdc_cpu.so! if so
118
+ @fdc_cpu.cycle!
119
+ @cycles += 1
120
+ end
121
+ end
122
+
123
+ # The 8250: the 8050's board driving double-sided mechanisms, 154 tracks
124
+ # a disk, with jumper E3 grounding the 6530's PB6 to say so.
125
+ class Model8250 < Model8050
126
+ def model_name = "8250"
127
+
128
+ def sides = 2
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # Sleeping through the two processors' idle loops, exactly, as
7
+ # Model8050::Idle does.
8
+ #
9
+ # With nothing to do, the IP goes round DOS 3.0's idle loop from
10
+ # IP_LOOP, and the SASI host round its job loop from SASI_LOOP, each
11
+ # pass finding the job queue empty, until ATN interrupts the IP. The
12
+ # host has no interrupt of its own. A pass puts back what it found, so
13
+ # each processor comes back to its loop's start as it left it.
14
+ #
15
+ # Each time a processor comes to its loop's start, the unit compares
16
+ # what that processor sees with what it saw last time round: the CPU,
17
+ # its chips but for their counters, its RAM and the common RAM. The
18
+ # same, with no read of a counter or an interrupt flag between, makes
19
+ # the pass repeatable. Once both are, with the hard disk's controller
20
+ # idle and no interrupt pulled, the unit sleeps until a counter is due
21
+ # to pull IRQ, the IEEE-488 controller moves a line, or something
22
+ # outside reads or changes the unit (settle!).
23
+ module Idle
24
+ IP_LOOP = 0xc131
25
+ SASI_LOOP = 0xf8f1
26
+
27
+ # Whether the unit may sleep through its idle loops.
28
+ attr_reader :idle_skip
29
+
30
+ def idle_skip=(on)
31
+ settle!
32
+ @idle_skip = on
33
+ end
34
+
35
+ def asleep? = @asleep
36
+
37
+ # Brings the unit up to the present, cycle for cycle, and drops the
38
+ # passes it found, since whoever called may change it.
39
+ def settle!
40
+ wake! if @asleep
41
+ forget_passes
42
+ end
43
+
44
+ private
45
+
46
+ def init_idle
47
+ @idle_skip = true
48
+ @asleep = false
49
+ @owed = 0
50
+ @budget = 0
51
+ forget_passes
52
+ end
53
+
54
+ def forget_passes
55
+ @ip_mark = nil
56
+ @sasi_mark = nil
57
+ @ip_pass = nil
58
+ @sasi_pass = nil
59
+ end
60
+
61
+ def owe_cycle
62
+ @owed += 1
63
+ wake! if @owed >= @budget
64
+ end
65
+
66
+ def watch_loops
67
+ ip = @ip_cpu
68
+ @ip_pass = nil if ip.irq
69
+ @sasi_pass = nil if @sasi_cpu.irq
70
+ ip_pass_found if ip.program_counter == IP_LOOP && ip.boundary?
71
+ host = @sasi_cpu
72
+ sasi_pass_found if host.program_counter == SASI_LOOP && host.boundary?
73
+ end
74
+
75
+ def ip_pass_found
76
+ state = ip_state
77
+ @ip_pass = repeated(@ip_mark, state, @ip_cpu, @ip_bus.volatile)
78
+ @ip_mark = [state, @ip_cpu.cycles, @ip_cpu.instructions]
79
+ @ip_bus.volatile = false
80
+ fall_asleep if @ip_pass && @sasi_pass
81
+ end
82
+
83
+ def sasi_pass_found
84
+ state = sasi_state
85
+ @sasi_pass = repeated(@sasi_mark, state, @sasi_cpu, @sasi_bus.volatile)
86
+ @sasi_mark = [state, @sasi_cpu.cycles, @sasi_cpu.instructions]
87
+ @sasi_bus.volatile = false
88
+ fall_asleep if @ip_pass && @sasi_pass
89
+ end
90
+
91
+ def repeated(mark, state, cpu, volatile)
92
+ return nil if mark.nil? || volatile || mark[0] != state
93
+
94
+ [cpu.cycles - mark[1], cpu.instructions - mark[2]]
95
+ end
96
+
97
+ def fall_asleep
98
+ target = @target
99
+ return unless target.quiet? && !target.busy? && !@ip_cpu.irq && !@sasi_cpu.irq
100
+
101
+ budget = quiet_cycles
102
+ return if budget < 2
103
+
104
+ @asleep = true
105
+ @owed = 0
106
+ @budget = budget
107
+ end
108
+
109
+ def quiet_cycles
110
+ [@data_riot.quiet_cycles, @control_riot.quiet_cycles, @via.quiet_cycles].min
111
+ end
112
+
113
+ def wake!
114
+ owed = @owed
115
+ @asleep = false
116
+ @owed = 0
117
+ return if owed.zero?
118
+
119
+ @data_riot.fast_forward(owed)
120
+ @control_riot.fast_forward(owed)
121
+ @via.fast_forward(owed)
122
+ @target.fast_forward(owed)
123
+ skip(@ip_cpu, @ip_pass, @ip_mark, owed)
124
+ skip(@sasi_cpu, @sasi_pass, @sasi_mark, owed)
125
+ @cycles += owed
126
+ end
127
+
128
+ def skip(cpu, pass, mark, owed)
129
+ passes = owed / pass[0]
130
+ cpu.fast_forward(passes * pass[0], passes * pass[1])
131
+ mark[1] += passes * pass[0]
132
+ mark[2] += passes * pass[1]
133
+ (owed - (passes * pass[0])).times { cpu.cycle! }
134
+ end
135
+
136
+ def ip_state
137
+ [*@ip_cpu.idle_state, *@data_riot.idle_state, *@control_riot.idle_state, @ip_bus.data, common_changes]
138
+ end
139
+
140
+ def sasi_state
141
+ [*@sasi_cpu.idle_state, *@via.idle_state, *@lines.state, *@sasi_bus.ram.read(0, 0x80), @sasi_bus.data,
142
+ common_changes]
143
+ end
144
+
145
+ def common_changes = @ip_bus.common_changes + @sasi_bus.common_changes
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ class SASITarget
7
+ # The controller's fields in a keyed snapshot module (BL-D8-SASI):
8
+ # the bus phase, REQ, the cycles left to the next, the command, the
9
+ # bytes of the phase and the place in them, the write under way,
10
+ # the sense bytes, the fast mode, its clock, the heads' cylinder, the
11
+ # geometry and the block buffer, then the image's path and how it
12
+ # was opened. The image itself stays in its file, which each write
13
+ # reached as it ended.
14
+ module KeyedState
15
+ def save_fields(out)
16
+ out.int("phase", @phase).bool("request", @request).int("delay", @delay)
17
+ out.ints("command", @command).ints("buffer", @buffer).int("index", @index)
18
+ out.bool("writing", @writing).ints("sense", @sense).ints("last_sense", @last_sense)
19
+ out.bool("fast", @fast).int("cycle", @cycle).int("cylinder", @cylinder)
20
+ out.int("heads", @heads).int("sectors", @sectors).ints("block_buffer", @block_buffer)
21
+ disk = @disk
22
+ out.text("path", disk.nil? ? "" : disk.path.to_s).bool("read_only", disk.nil? || disk.read_only?)
23
+ out.int("disk_heads", disk.nil? ? 0 : disk.heads)
24
+ end
25
+
26
+ # A detached reader, or a host file that is gone, leaves the drive
27
+ # empty.
28
+ def load_fields(input, detached: false)
29
+ load_bus_fields(input)
30
+ path = input.text("path", "")
31
+ read_only = input.bool("read_only", false)
32
+ heads = input.int("disk_heads", 4)
33
+ @disk = detached || !File.file?(path) ? nil : Storage::D90Image.new(path, read_only:, heads:)
34
+ end
35
+
36
+ private
37
+
38
+ def load_bus_fields(input)
39
+ @phase = input.int("phase", @phase)
40
+ @request = input.bool("request", @request)
41
+ @delay = input.int("delay", @delay)
42
+ @command = input.ints("command", @command).dup
43
+ @buffer = input.ints("buffer", @buffer).dup
44
+ @index = input.int("index", @index)
45
+ @writing = input.bool("writing", @writing)
46
+ @sense = input.ints("sense", @sense).dup
47
+ @last_sense = input.ints("last_sense", @last_sense).dup
48
+ @fast = input.bool("fast", @fast)
49
+ @cycle = input.int("cycle", @cycle)
50
+ @cylinder = input.int("cylinder", @cylinder)
51
+ @heads = input.int("heads", @heads)
52
+ @sectors = input.int("sectors", @sectors)
53
+ @block_buffer = input.ints("block_buffer", @block_buffer).dup
54
+ end
55
+ end
56
+
57
+ include KeyedState
58
+ end
59
+
60
+ # The unit's own fields in a keyed snapshot module (BL-D8): its model,
61
+ # its clock phase and cycle count, each bus's last byte, the
62
+ # controller's lines as the port latched them, under ieee.*, and the
63
+ # SASI lines' ACK and RST levels. A save settles the unit first, so it
64
+ # is awake; a load finds its passes afresh.
65
+ module KeyedState
66
+ def save_fields(out)
67
+ settle!
68
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
69
+ out.int("ip_data", @ip_bus.data).int("sasi_data", @sasi_bus.data)
70
+ out.int("ieee.host", @ieee_port.host)
71
+ ack_high, reset_high = @lines.state
72
+ out.bool("sasi.ack_high", ack_high).bool("sasi.reset_high", reset_high)
73
+ out
74
+ end
75
+
76
+ def load_fields(input)
77
+ settle!
78
+ input.choice("model", [model_name], model_name)
79
+ @phase = input.int("phase", @phase)
80
+ @cycles = input.int("cycles", @cycles)
81
+ @ip_bus.data = input.byte("ip_data", @ip_bus.data)
82
+ @sasi_bus.data = input.byte("sasi_data", @sasi_bus.data)
83
+ @ieee_port.host = input.int("ieee.host", @ieee_port.host)
84
+ @lines.state = [input.bool("sasi.ack_high", true), input.bool("sasi.reset_high", false)]
85
+ end
86
+
87
+ # The unit as modules of its own, named from `name` (BL-D8 for
88
+ # device 8): its own fields, the common RAM, UC1 and UE1 with their
89
+ # RAM, each processor, the host's RAM and VIA, and the hard disk's
90
+ # controller.
91
+ def save_modules(sections, name)
92
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
93
+ sections << Snapshot::Keyed.section("#{name}-COMMON") { |out| @common.save_fields(out, "ram") }
94
+ sections << Snapshot::Keyed.section("#{name}-RIOT1") { |out| @data_riot.save_fields(out) }
95
+ sections << Snapshot::Keyed.section("#{name}-RIOT2") { |out| @control_riot.save_fields(out) }
96
+ sections << Snapshot::Keyed.section("#{name}-IPCPU") { |out| @ip_cpu.save_fields(out) }
97
+ sections << Snapshot::Keyed.section("#{name}-SASICPU") { |out| @sasi_cpu.save_fields(out) }
98
+ sections << Snapshot::Keyed.section("#{name}-SASIRAM") { |out| @sasi_bus.ram.save_fields(out, "ram") }
99
+ sections << Snapshot::Keyed.section("#{name}-VIA") { |out| @via.save_fields(out) }
100
+ sections << Snapshot::Keyed.section("#{name}-SASI") { |out| @target.save_fields(out) }
101
+ end
102
+
103
+ # The unit from its modules, `input` being its own.
104
+ def load_modules(modules, input, name)
105
+ load_fields(input)
106
+ @common.load_fields(modules.required("#{name}-COMMON"), "ram")
107
+ @data_riot.load_fields(modules.required("#{name}-RIOT1"))
108
+ @control_riot.load_fields(modules.required("#{name}-RIOT2"))
109
+ @ip_cpu.load_fields(modules.required("#{name}-IPCPU"))
110
+ @sasi_cpu.load_fields(modules.required("#{name}-SASICPU"))
111
+ @sasi_bus.ram.load_fields(modules.required("#{name}-SASIRAM"), "ram")
112
+ @via.load_fields(modules.required("#{name}-VIA"))
113
+ @target.load_fields(modules.required("#{name}-SASI"), detached: modules.detached?)
114
+ @via.restored!
115
+ @ip_cpu.restored!
116
+ @sasi_cpu.restored!
117
+ end
118
+ end
119
+
120
+ include KeyedState
121
+ end
122
+ end
123
+ end