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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The SASI host's VIA, at $0080, a class of its own so it keeps one
7
+ # type of peripheral.
8
+ class SASIVIA < Chips::VIA
9
+ end
10
+
11
+ # The SASI host's 6502, on the SASIBus.
12
+ class SASICPU
13
+ include Badline::Chips::CPU::Core
14
+ include Engine::IdleCPU
15
+ end
16
+
17
+ # The VIA's lines to the controller (SASITarget), on the board's
18
+ # transceivers:
19
+ #
20
+ # PA0-7 the data bus PB0 SEL out
21
+ # CA1 REQ, rising as it rises PB1 RST out
22
+ # CA2 ACK, pulsed on each PB2 C/D in
23
+ # read or write of ORA PB3 BSY in
24
+ # CB2 the transceivers' PB4 jumper J14: high for four
25
+ # direction heads (D9060), low for six
26
+ # PB6 I/O in
27
+ # PB7 MSG in
28
+ #
29
+ # The ports read and drive each line high while it is asserted, SEL
30
+ # and RST only while PB0 and PB1 are outputs.
31
+ # SEL raised selects the controller with the data bus as the host
32
+ # drives it, and RST raised resets it. ACK falling hands it the byte
33
+ # on the data bus.
34
+ class SASILines
35
+ SEL = 0x01
36
+ RST = 0x02
37
+ CD = 0x04
38
+ BSY = 0x08
39
+ J14 = 0x10
40
+ IO = 0x40
41
+ MSG = 0x80
42
+
43
+ attr_reader :target
44
+ attr_writer :via
45
+
46
+ # `four_heads` is J14.
47
+ def initialize(target, four_heads:)
48
+ @target = target
49
+ @four_heads = four_heads
50
+ @via = nil
51
+ @ack_high = true
52
+ @reset_high = false
53
+ end
54
+
55
+ # The data bus: the controller's byte while it drives it.
56
+ def read_a(_lines)
57
+ target = @target
58
+ target.input? ? target.data : 0xff
59
+ end
60
+
61
+ def read_b(_lines)
62
+ target = @target
63
+ value = 0x23
64
+ value |= CD if target.control?
65
+ value |= BSY if target.busy?
66
+ value |= J14 if @four_heads
67
+ value |= IO if target.input?
68
+ value |= MSG if target.message?
69
+ value
70
+ end
71
+
72
+ # One cycle of the controller, then the host's ACK, RST and SEL to
73
+ # the controller, and REQ to CA1.
74
+ def cycle!
75
+ target = @target
76
+ via = @via
77
+ target.clock
78
+ ack_high = via.ca2_output
79
+ target.acknowledge(via.port_a_output) if @ack_high && !ack_high
80
+ @ack_high = ack_high
81
+ host = via.port_b_output & via.peek(0x82)
82
+ reset_high = host.anybits?(RST)
83
+ target.reset if reset_high && !@reset_high
84
+ @reset_high = reset_high
85
+ target.select(via.port_a_output) if host.anybits?(SEL)
86
+ via.ca1 = target.request?
87
+ end
88
+
89
+ # Everything the lines hold between cycles.
90
+ def state = [@ack_high, @reset_high]
91
+
92
+ def state=(values)
93
+ @ack_high, @reset_high = values
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The SASI host processor's bus on the DOS board (300010), where the
7
+ # 8050's floppy controller sits, with its decode and common RAM:
8
+ #
9
+ # $0000-$007F - the 6810's 128 bytes, mirrored at $0100 for the stack
10
+ # $0080-$008F - the VIA (SASILines)
11
+ # $0400-$13FF - the common RAM, 1 KB in each of the 8050's windows
12
+ # $F800-$FFFF - 300515, the host's program
13
+ #
14
+ # Writes to the ROM and to open bus go nowhere, and a read from open
15
+ # bus finds the last byte on the data bus.
16
+ class SASIBus
17
+ include Addressable
18
+
19
+ attr_reader :common, :ram, :via
20
+ attr_accessor :data
21
+
22
+ # How many writes have changed a byte of the common RAM
23
+ # (Model9060::Idle).
24
+ attr_reader :common_changes
25
+
26
+ # Whether a counter or an interrupt flag has been read since it was
27
+ # last cleared (Model9060::Idle).
28
+ attr_accessor :volatile
29
+
30
+ def initialize(rom:, common:, via:)
31
+ addressable_at(0, length: 2**16)
32
+ @rom = rom
33
+ @common = common
34
+ @via = via
35
+ @ram = Memory.new([], length: 0x80)
36
+ @data = 0
37
+ @volatile = false
38
+ @common_changes = 0
39
+ end
40
+
41
+ def peek(addr)
42
+ @data = if addr >= 0xf800
43
+ @rom.peek(addr & 0x07ff)
44
+ elsif addr < 0x0200
45
+ peek_low(addr)
46
+ elsif addr.between?(0x0400, 0x13ff)
47
+ @common.peek(addr - 0x0400)
48
+ else
49
+ @data
50
+ end
51
+ end
52
+
53
+ def poke(addr, value)
54
+ @data = value
55
+ if addr < 0x0200
56
+ poke_low(addr, value)
57
+ elsif addr.between?(0x0400, 0x13ff)
58
+ poke_common(addr - 0x0400, value)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def peek_low(addr)
65
+ return @ram.peek(addr & 0x7f) if addr.nobits?(0x80)
66
+ return @data if addr.anybits?(0x70)
67
+
68
+ register = addr & 0x0f
69
+ @volatile = true if register.between?(0x04, 0x0a) || register == 0x0d
70
+ @via.peek(0x80 | register)
71
+ end
72
+
73
+ def poke_low(addr, value)
74
+ if addr.nobits?(0x80)
75
+ @ram.poke(addr & 0x7f, value)
76
+ elsif addr.nobits?(0x70)
77
+ @via.poke(0x80 | (addr & 0x0f), value)
78
+ end
79
+ end
80
+
81
+ def poke_common(index, value)
82
+ @common_changes += 1 if @common.peek(index) != value
83
+ @common.poke(index, value)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The commands the DOS board's SASI ROM (300515) sends, against the
7
+ # disk's blocks. A command's second to fourth bytes hold the drive
8
+ # (bits 5-7) and the block's LBA, its fifth the blocks to move.
9
+ #
10
+ # | Op | Command | Data |
11
+ # | --- | --- | --- |
12
+ # | $00 | Test Unit Ready | |
13
+ # | $01 | Recalibrate: seek to cylinder 0 | |
14
+ # | $03 | Request Sense: the last command's error and its LBA | 4 in |
15
+ # | $04 | Format Unit | |
16
+ # | $08 | Read | 256 a block in |
17
+ # | $0A | Write | 256 a block out |
18
+ # | $0B | Seek | |
19
+ # | $0C | Vendor: read the controller's block buffer (unverified) | 256 in |
20
+ # | $0E | Vendor: write the controller's block buffer (unverified) | 256 out |
21
+ # | $E3 | Vendor: four zero bytes from a drive that is there (unverified) | 4 in |
22
+ # | $C0 | Vendor: set the geometry (unverified) | |
23
+ #
24
+ # DOS 3.0 sends $E3 before it first reads a drive, and leaves a drive
25
+ # alone that fails it as DRIVE NOT READY, as drive 1 does here.
26
+ #
27
+ # $C0's fifth and sixth bytes are taken as the last head and the last
28
+ # sector of a head, as the ROM sends them (5 and 31 on a D9090 at
29
+ # boot). Its other bytes aren't used.
30
+ #
31
+ # An error's sense byte has its class in bits 4-5 and its code in bits
32
+ # 0-3, with bit 7 set when the LBA after it names the block. The ROM
33
+ # reads that byte from the message phase after a check condition,
34
+ # and the controller sends it there.
35
+ #
36
+ # Format Unit erases the disk at once and puts the controller in a
37
+ # fast mode, where Read, Write and Seek take no seek or rotation time,
38
+ # for the DOS's passes over the whole disk. Any other command ends
39
+ # it. Outside it a seek takes 3 ms a cylinder and 15 ms to settle, and
40
+ # the disk turns 32 blocks in 16.7 ms.
41
+ module SASICommands
42
+ DRIVE_NOT_READY = 0x04
43
+ WRITE_FAULT = 0x03
44
+ INVALID_COMMAND = 0x20
45
+ ILLEGAL_ADDRESS = 0xa1
46
+
47
+ BLOCK = 256
48
+ SECTOR_CYCLES = 521
49
+ STEP_CYCLES = 3_000
50
+ SETTLE_CYCLES = 15_000
51
+
52
+ DISK_COMMANDS = [0x00, 0x01, 0x04, 0x08, 0x0a, 0x0b, 0xe3].freeze
53
+ FAST_COMMANDS = [0x08, 0x0a, 0x0b].freeze
54
+
55
+ attr_reader :heads, :sectors, :cylinder
56
+
57
+ def fast? = @fast
58
+
59
+ private
60
+
61
+ def reset_geometry
62
+ @heads = 4
63
+ @sectors = 32
64
+ @block_buffer = Array.new(BLOCK, 0)
65
+ end
66
+
67
+ def execute
68
+ @fast = false unless FAST_COMMANDS.include?(@command[0])
69
+ case @command[0]
70
+ when 0x03 then enter(SASITarget::DATA_IN, @last_sense.dup, 1)
71
+ when 0x0c then enter(SASITarget::DATA_IN, @block_buffer.dup, 1)
72
+ when 0x0e then enter(SASITarget::DATA_OUT, Array.new(BLOCK, 0), 1)
73
+ when 0xc0 then set_geometry
74
+ else disk_command
75
+ end
76
+ end
77
+
78
+ def disk_command
79
+ return refuse(INVALID_COMMAND) unless DISK_COMMANDS.include?(@command[0])
80
+ return refuse(DRIVE_NOT_READY) unless @disk && (@command[1] >> 5).zero?
81
+
82
+ case @command[0]
83
+ when 0x00 then report
84
+ when 0x01 then report(travel(0))
85
+ when 0x04 then format_unit
86
+ when 0xe3 then enter(SASITarget::DATA_IN, [0, 0, 0, 0], 1)
87
+ else block_command
88
+ end
89
+ end
90
+
91
+ def block_command
92
+ return refuse(ILLEGAL_ADDRESS) unless @disk.blocks?(lba, @command[0] == 0x0b ? 1 : count)
93
+
94
+ case @command[0]
95
+ when 0x08 then read
96
+ when 0x0a then enter(SASITarget::DATA_OUT, Array.new(count * BLOCK, 0), 1)
97
+ else report(travel(cylinder_of(lba)))
98
+ end
99
+ end
100
+
101
+ def lba = ((@command[1] & 0x1f) << 16) | (@command[2] << 8) | @command[3]
102
+
103
+ def count = @command[4].zero? ? 256 : @command[4]
104
+
105
+ def cylinder_of(block) = block / (@heads * @sectors)
106
+
107
+ def set_geometry
108
+ @heads = @command[4] + 1
109
+ @sectors = @command[5] + 1
110
+ report
111
+ end
112
+
113
+ def read
114
+ bytes = @disk.read_blocks(lba, count)
115
+ @block_buffer = bytes.last(BLOCK)
116
+ enter(SASITarget::DATA_IN, bytes, access_cycles)
117
+ end
118
+
119
+ def data_received
120
+ if @command[0] == 0x0e
121
+ @block_buffer = @buffer.dup
122
+ report
123
+ else
124
+ @writing = true
125
+ @delay = access_cycles
126
+ end
127
+ end
128
+
129
+ def store_buffer
130
+ @disk.write_blocks(lba, @buffer)
131
+ @block_buffer = @buffer.last(BLOCK)
132
+ report
133
+ rescue Storage::WriteError
134
+ refuse(WRITE_FAULT)
135
+ end
136
+
137
+ def format_unit
138
+ blocks = @heads * @sectors
139
+ (0...@disk.block_count).step(blocks) do |first|
140
+ @disk.write_blocks(first, Array.new([blocks, @disk.block_count - first].min * BLOCK, 0))
141
+ end
142
+ @fast = true
143
+ report
144
+ rescue Storage::WriteError
145
+ refuse(WRITE_FAULT)
146
+ end
147
+
148
+ # The seek to the block's cylinder, the turn until the block comes
149
+ # under the head, and the blocks passing under it.
150
+ def access_cycles
151
+ return 1 if @fast
152
+
153
+ seek = travel(cylinder_of(lba))
154
+ slot = ((@cycle + seek) / SECTOR_CYCLES) % @sectors
155
+ seek + ((((lba % @sectors) - slot) % @sectors) * SECTOR_CYCLES) + (count * SECTOR_CYCLES)
156
+ end
157
+
158
+ def travel(target)
159
+ steps = (target - @cylinder).abs
160
+ @cylinder = target
161
+ return 1 if @fast || steps.zero?
162
+
163
+ (steps * STEP_CYCLES) + SETTLE_CYCLES
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model9060/sasi_commands"
4
+
5
+ module Badline
6
+ module Drives
7
+ class Model9060
8
+ # The D9060's and D9090's hard disk controller (Tandon 188008) as the
9
+ # DOS board's SASI host sees it, at the level of the bus's lines.
10
+ #
11
+ # The host selects it by raising SEL with bit 0 on the data bus, and
12
+ # it answers with BSY. It then runs one command through the bus's
13
+ # phases, which C/D, I/O and MSG name: six command bytes from the
14
+ # host, the data in or out, a status byte and a message byte, after
15
+ # which it lets go of BSY. Each byte waits on REQ, which the host
16
+ # answers with ACK, and REQ rises again a cycle later for the next.
17
+ #
18
+ # The controller's work between the phases (a seek, the turn to the
19
+ # block, the block under the head) delays the next REQ. `clock`
20
+ # counts that time out, one cycle of the DOS board's 1 MHz at a time,
21
+ # and `quiet?` says when there is none left to count. SASICommands
22
+ # runs the commands against the disk, a Storage::D90Image, or nil
23
+ # with none inserted.
24
+ class SASITarget
25
+ include SASICommands
26
+
27
+ BUS_FREE = 0
28
+ COMMAND = 1
29
+ DATA_IN = 2
30
+ DATA_OUT = 3
31
+ STATUS = 4
32
+ MESSAGE = 5
33
+
34
+ COMMAND_LENGTH = 6
35
+
36
+ attr_accessor :disk
37
+ attr_reader :phase
38
+
39
+ def initialize(disk = nil)
40
+ @disk = disk
41
+ @cycle = 0
42
+ @cylinder = 0
43
+ reset_geometry
44
+ reset
45
+ end
46
+
47
+ # RST: the controller drops whatever it was doing and frees the bus.
48
+ def reset
49
+ @phase = BUS_FREE
50
+ @request = false
51
+ @delay = 0
52
+ @command = []
53
+ @buffer = []
54
+ @index = 0
55
+ @writing = false
56
+ @sense = [0, 0, 0, 0]
57
+ @last_sense = @sense
58
+ @fast = false
59
+ end
60
+
61
+ def busy? = @phase != BUS_FREE
62
+
63
+ def request? = @request
64
+
65
+ # C/D: a command, status or message byte rather than data.
66
+ def control? = [COMMAND, STATUS, MESSAGE].include?(@phase)
67
+
68
+ # I/O: a byte from the controller to the host.
69
+ def input? = [DATA_IN, STATUS, MESSAGE].include?(@phase)
70
+
71
+ def message? = @phase == MESSAGE
72
+
73
+ def quiet? = @delay.zero?
74
+
75
+ # The byte the controller puts on the data bus, while I/O is set.
76
+ def data = input? ? @buffer[@index] || 0 : 0
77
+
78
+ # SEL with the host's data bus. The controller answers to bit 0
79
+ # while the bus is free.
80
+ def select(bus)
81
+ return if busy? || bus.nobits?(0x01)
82
+
83
+ enter(COMMAND, Array.new(COMMAND_LENGTH, 0), 1)
84
+ end
85
+
86
+ # ACK for the byte REQ asked for. The host's data bus carries a
87
+ # byte to the controller in the command and data out phases, and
88
+ # is ignored in the others.
89
+ def acknowledge(bus)
90
+ return unless @request
91
+
92
+ @request = false
93
+ @buffer[@index] = bus if @phase == COMMAND || @phase == DATA_OUT
94
+ @index += 1
95
+ @index < @buffer.length ? @delay = 1 : finish_phase
96
+ end
97
+
98
+ # Runs the clock on through cycles the controller has nothing to
99
+ # count down in.
100
+ def fast_forward(cycles)
101
+ @cycle += cycles
102
+ end
103
+
104
+ def clock
105
+ @cycle += 1
106
+ return if @delay.zero?
107
+
108
+ @delay -= 1
109
+ arrive if @delay.zero?
110
+ end
111
+
112
+ private
113
+
114
+ def enter(phase, bytes, delay)
115
+ @phase = phase
116
+ @buffer = bytes
117
+ @index = 0
118
+ @request = false
119
+ @delay = [delay, 1].max
120
+ end
121
+
122
+ def arrive
123
+ if @writing
124
+ @writing = false
125
+ store_buffer
126
+ else
127
+ @request = true
128
+ end
129
+ end
130
+
131
+ def finish_phase
132
+ case @phase
133
+ when COMMAND then start(@buffer.dup)
134
+ when DATA_IN then report
135
+ when DATA_OUT then data_received
136
+ when STATUS then enter(MESSAGE, [@sense[0]], 1)
137
+ else @phase = BUS_FREE
138
+ end
139
+ end
140
+
141
+ def start(command)
142
+ @command = command
143
+ @last_sense = @sense
144
+ @sense = [0, 0, 0, 0]
145
+ execute
146
+ end
147
+
148
+ # The status byte: 0 for a command that went well, $02 (check
149
+ # condition) for one that failed, whose error Request Sense reads.
150
+ def report(delay = 1)
151
+ enter(STATUS, [@sense[0].positive? ? 0x02 : 0x00], delay)
152
+ end
153
+
154
+ def refuse(error)
155
+ @sense = [error, (@command[1] & 0xe0) | ((lba >> 16) & 0x1f), (lba >> 8) & 0xff, lba & 0xff]
156
+ report
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model8050"
4
+ require "badline/drives/model9060/sasi_target"
5
+ require "badline/drives/model9060/sasi_bus"
6
+ require "badline/drives/model9060/parts"
7
+ require "badline/drives/model9060/idle"
8
+ require "badline/drives/model9060/keyed_state"
9
+
10
+ module Badline
11
+ module Drives
12
+ # The D9060 hard disk of the PET and CBM-II, one 5 MB drive on an
13
+ # IEEE-488 unit (technical manual PN 990441; DOS board 300010).
14
+ #
15
+ # The DOS board is the 8050's with the floppy half given over to a SASI
16
+ # host. The interface processor (IP) runs DOS 3.0 from 16 KB of ROM at
17
+ # $C000, with the 8050's two 6532s on the IEEE-488 bus (Model8050's
18
+ # IPBus and IEEEPort). The SASI host, a second 6502 in the floppy
19
+ # controller's place, runs 300515 from 2 KB at $F800, with a 6810 for
20
+ # its zero page and stack and a VIA facing the hard disk's controller
21
+ # (SASIBus, SASILines, SASITarget). The two meet in the 8050's 4 KB of
22
+ # common RAM, where the IP leaves jobs and the host turns each into a
23
+ # SASI command.
24
+ #
25
+ # Reset starts both. The host waits on the common RAM for the IP, which
26
+ # tests it and answers, and the DOS settles into its idle loop with the
27
+ # host polling the job queue.
28
+ #
29
+ # The interface processor's side of the board and the clocking against
30
+ # the host are the 8050's (Model8050::DOSBoard), and the unit sleeps
31
+ # through both processors' idle loops (Idle).
32
+ #
33
+ # N runs as the DOS runs it, but for the six passes that write and
34
+ # read back test patterns over every block, an hour of the hardware's
35
+ # time: once Format Unit has erased the disk, the IP goes from the first
36
+ # pass straight on to where they end, as if every block had passed.
37
+ # The DOS then writes the bad-block list, the BAM, the header and the
38
+ # directory as c1541 formats a .d90, byte for byte.
39
+ class Model9060
40
+ include Model8050::DOSBoard
41
+ include Idle
42
+
43
+ CLOCK_HZ = 1_000_000
44
+
45
+ # The host cycles by which a unit switched on with the machine has
46
+ # run its RAM tests and the host's start-up, and reads its jumpers.
47
+ BOOT_CYCLES = 1_500_000
48
+
49
+ # DOS 3.0's format: the first of its pattern passes (FORMAT_PASSES),
50
+ # where the pass number ($2D) is 0, and the code after the last
51
+ # (FORMAT_DONE), with the pass number at 6.
52
+ FORMAT_PASSES = 0xcb02
53
+ FORMAT_DONE = 0xcc15
54
+ PASS = 0x2d
55
+
56
+ def sasi_cpu = settled(@sasi_cpu)
57
+
58
+ def sasi_bus = settled(@sasi_bus)
59
+
60
+ def target = settled(@target)
61
+
62
+ def model_name = "9060"
63
+
64
+ # The heads the drive has, which J14 tells the DOS.
65
+ def heads = 4
66
+
67
+ def boot_cycles = BOOT_CYCLES
68
+
69
+ # Whether the unit is where VICE saves it: both processors between
70
+ # instructions, the host at the start of its job loop.
71
+ def settled_for_vice?
72
+ host = sasi_cpu
73
+ ip_cpu.boundary? && host.boundary? && host.program_counter == Idle::SASI_LOOP
74
+ end
75
+
76
+ # +dos_rom+ covers $C000-$FFFF and +sasi_rom+ $F800-$FFFF of the
77
+ # host's, each defaulting to the image in the ROM path. +device+ is
78
+ # the number the jumpers set, 8 to 15. Only the default DOS has its
79
+ # pattern passes skipped.
80
+ def initialize(dos_rom: nil, sasi_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
81
+ @device = device
82
+ @host_clock_hz = host_clock_hz
83
+ @phase = 0
84
+ @cycles = 0
85
+ @format_trap = dos_rom.nil?
86
+ build_ip(dos_rom || ROM.load("9060/dos-3.0.rom", 0xc000))
87
+ build_sasi(sasi_rom || ROM.read("9060/sasi-300515-revb.rom"))
88
+ @ip_cpu = Model8050::IPCPU.new(@ip_bus)
89
+ @sasi_cpu = SASICPU.new(@sasi_bus)
90
+ init_idle
91
+ @ieee_bus.attach(self)
92
+ end
93
+
94
+ # Whether the activity LED is lit: of the LEDs, bit 1 is the
95
+ # activity LED and bit 2 the status LED.
96
+ def led_on? = leds.anybits?(0x02)
97
+
98
+ # Puts the Storage::D90Image in the drive. Nil takes it out.
99
+ def insert(image)
100
+ settle!
101
+ @target.disk = image
102
+ end
103
+
104
+ # Puts in the .d90 image at +path+, write-protected with `read_only`.
105
+ # An image of neither drive's size takes this drive's heads.
106
+ def insert_image(path, read_only: false)
107
+ image_heads = Storage::D90Image.heads_for(File.size(path)) || heads
108
+ insert(Storage::D90Image.new(path, read_only:, heads: image_heads))
109
+ end
110
+
111
+ # The image in the drive, or nil.
112
+ def disk = @target.disk
113
+
114
+ # Writes reach the image as each Write ends, so there is nothing to
115
+ # store.
116
+ def flush = nil
117
+
118
+ # The bus's IFC line resets both processors and every chip. RAM keeps
119
+ # its contents, and the hard disk's controller runs on.
120
+ def reset!
121
+ settle!
122
+ @data_riot.reset!
123
+ @control_riot.reset!
124
+ @via.reset!
125
+ @ip_cpu.reset!
126
+ @sasi_cpu.reset!
127
+ end
128
+
129
+ def inspect
130
+ "#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) sasi=(#{@sasi_cpu.inspect})>"
131
+ end
132
+
133
+ private
134
+
135
+ def run_cycle
136
+ step
137
+ skip_format_passes if @format_trap && @target.fast?
138
+ watch_loops if @idle_skip
139
+ end
140
+
141
+ def build_ip(rom)
142
+ @common = Memory.new([], length: 0x1000)
143
+ @ieee_bus = IEEEBus.new
144
+ @ieee_port = Model8050::IEEEPort.new(device: @device, bus: @ieee_bus)
145
+ @data_riot = Model8050::DataRIOT.new(peripheral: Model8050::DataLines.new(@ieee_port))
146
+ @control_riot = Model8050::ControlRIOT.new(peripheral: Model8050::ControlLines.new(@ieee_port))
147
+ @ieee_port.data_riot = @data_riot
148
+ @ieee_port.control_riot = @control_riot
149
+ @ip_bus = Model8050::IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
150
+ end
151
+
152
+ def build_sasi(rom)
153
+ @target = SASITarget.new
154
+ @lines = SASILines.new(@target, four_heads: heads == 4)
155
+ @via = SASIVIA.new(start: 0x0080, peripheral: @lines)
156
+ @lines.via = @via
157
+ @sasi_bus = SASIBus.new(rom: ROM.new(rom, length: rom.length), common: @common, via: @via)
158
+ end
159
+
160
+ # One cycle of the board: the controller and the chips, then the IP
161
+ # and the host, each in its own half of the cycle. ATN reaches UE1's
162
+ # PA7 as the port latched it. UE1 pulls the IP's IRQ and the VIA the
163
+ # host's.
164
+ def step
165
+ @control_riot.pa7 = @ieee_port.atn_low?
166
+ @data_riot.cycle!
167
+ @control_riot.cycle!
168
+ @via.cycle!
169
+ @lines.cycle!
170
+ @ip_cpu.irq = @control_riot.irq?
171
+ @ip_cpu.cycle!
172
+ @sasi_cpu.irq = @via.irq?
173
+ @sasi_cpu.cycle!
174
+ @cycles += 1
175
+ end
176
+
177
+ # Sends the IP from the first pattern pass on to the end of the last.
178
+ def skip_format_passes
179
+ ip = @ip_cpu
180
+ return unless ip.program_counter == FORMAT_PASSES && ip.boundary? && @ip_bus.peek(PASS).zero?
181
+
182
+ @ip_bus.poke(PASS, 6)
183
+ ip.program_counter = FORMAT_DONE
184
+ end
185
+ end
186
+
187
+ # The D9090: the D9060's board on a six-head drive, 7.5 MB, with J14
188
+ # telling the DOS so.
189
+ class Model9090 < Model9060
190
+ def model_name = "9090"
191
+
192
+ def heads = 6
193
+ end
194
+ end
195
+ end