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,179 @@
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.
30
+ class Model4040
31
+ include Model8050::Board
32
+ include Model8050::Idle
33
+ include Model8050::KeyedState
34
+
35
+ CLOCK_HZ = Model8050::CLOCK_HZ
36
+
37
+ # The host cycles by which a unit switched on with the machine has
38
+ # run the IP's tests and bumped both heads, and reads its jumpers.
39
+ BOOT_CYCLES = 2_500_000
40
+
41
+ # Where DOS 2.1's idle loop and the controller's job loop start over
42
+ # (Model8050::Idle).
43
+ IP_LOOP = 0xd4b7
44
+ FDC_LOOP = 0xfc54
45
+
46
+ def model_name = "4040"
47
+
48
+ def boot_cycles = BOOT_CYCLES
49
+
50
+ def sides = 1
51
+
52
+ # +dos_rom+ covers $D000-$FFFF and +fdc_rom+ is the 6530's mask, each
53
+ # defaulting to the image in the ROM path: the revised DOS 2.1 that
54
+ # xpet's 4040 runs, and 901466-04. +device+ is the number the jumpers
55
+ # 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(dos_file, dos_start))
62
+ build_fdc(fdc_rom || ROM.read(fdc_file))
63
+ @ip_cpu = IPCPU.new(@ip_bus)
64
+ @fdc_cpu = FDCCPU.new(@fdc_bus)
65
+ init_idle(ip_loop, fdc_loop)
66
+ @ieee_bus.attach(self)
67
+ end
68
+
69
+ # Puts in the .d64 image at +path+, or the DOS 1 disk a .d67 holds,
70
+ # write-protected with `read_only`.
71
+ def insert_image(path, drive: 0, read_only: false)
72
+ image = if File.extname(path).casecmp?(".d67")
73
+ Storage::D67Image.new(path, read_only:)
74
+ else
75
+ Storage::D64Image.new(path, read_only:)
76
+ end
77
+ insert(Disk.new(image), drive:)
78
+ end
79
+
80
+ private
81
+
82
+ def dos_file = "4040/dos-2.1-rev.rom"
83
+
84
+ def dos_start = 0xd000
85
+
86
+ def fdc_file = "4040/fdc-901466-04.rom"
87
+
88
+ def ip_loop = IP_LOOP
89
+
90
+ def fdc_loop = FDC_LOOP
91
+
92
+ def build_ip(rom)
93
+ @common = Memory.new([], length: 0x1000)
94
+ @ieee_bus = IEEEBus.new
95
+ @ieee_port = Model8050::IEEEPort.new(device: @device, bus: @ieee_bus)
96
+ @data_riot = Model8050::DataRIOT.new(peripheral: Model8050::DataLines.new(@ieee_port))
97
+ @control_riot = Model8050::ControlRIOT.new(peripheral: Model8050::ControlLines.new(@ieee_port))
98
+ @ieee_port.data_riot = @data_riot
99
+ @ieee_port.control_riot = @control_riot
100
+ @ip_bus = IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
101
+ end
102
+
103
+ def build_fdc(rom)
104
+ @mechanism = Mechanism.new
105
+ @via = Model8050::ControllerVIA.new(start: 0x0040, peripheral: ReadLines.new(@mechanism))
106
+ @rriot = Model8050::ControllerRRIOT.new(rom:, peripheral: StatusLines.new(@mechanism))
107
+ @fdc_bus = Model8050::FDCBus.new(common: @common, via: @via, rriot: @rriot)
108
+ @mechanism.via = @via
109
+ @mechanism.rriot = @rriot
110
+ end
111
+
112
+ # The disk a BL-D8-DISK module holds (Model8050::KeyedState).
113
+ def disk_from_fields(input, detached) = Disk.from_fields(input, detached:)
114
+
115
+ # One cycle of the board: the drives, then the chips, then the IP and
116
+ # the FDC, each in its own half of the cycle, as on the 8050. Only the
117
+ # 6530 pulls the FDC's IRQ, and the FDC finds byte ready by polling.
118
+ def step
119
+ @mechanism.cycle!
120
+ @control_riot.pa7 = @ieee_port.atn_low?
121
+ @data_riot.cycle!
122
+ @control_riot.cycle!
123
+ @via.cycle!
124
+ @rriot.cycle!
125
+ @ip_cpu.irq = @control_riot.irq?
126
+ @ip_cpu.cycle!
127
+ @fdc_cpu.irq = @rriot.irq?
128
+ @fdc_cpu.cycle!
129
+ @cycles += 1
130
+ end
131
+ end
132
+
133
+ # The 3040: the 4040's board with the first DOS 2.1, as xpet's 3040
134
+ # runs it, 901468-11 to -13.
135
+ class Model3040 < Model4040
136
+ # Where the first DOS 2.1's idle loop starts over.
137
+ IP_LOOP = 0xd4b8
138
+
139
+ def model_name = "3040"
140
+
141
+ private
142
+
143
+ def dos_file = "4040/dos-2.1.rom"
144
+
145
+ def ip_loop = IP_LOOP
146
+ end
147
+
148
+ # The 2040: the 4040's board with DOS 1.2 in the sockets at $E000 and
149
+ # $F000, UJ1 at $D000 left empty, and 901466-02 in the controller's
150
+ # 6530, as the 3040 with DOS 1 has it too. Its disks are DOS 1's .d67,
151
+ # with 20 sectors on tracks 18 to 24 (Storage::D67Image). The
152
+ # controller starts at once, with no handshake on the common RAM, and
153
+ # the interface processor leaves the common RAM alone at boot.
154
+ class Model2040 < Model4040
155
+ # Where DOS 1.2's idle loop and its controller's job loop start over.
156
+ IP_LOOP = 0xe27e
157
+ FDC_LOOP = 0xfc47
158
+
159
+ def model_name = "2040"
160
+
161
+ # Puts in the .d67 image at +path+, write-protected with `read_only`.
162
+ def insert_image(path, drive: 0, read_only: false)
163
+ insert(Disk.new(Storage::D67Image.new(path, read_only:)), drive:)
164
+ end
165
+
166
+ private
167
+
168
+ def dos_file = "4040/dos-1.2.rom"
169
+
170
+ def dos_start = 0xe000
171
+
172
+ def fdc_file = "4040/fdc-901466-02.rom"
173
+
174
+ def ip_loop = IP_LOOP
175
+
176
+ def fdc_loop = FDC_LOOP
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,77 @@
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
+ step
72
+ watch_loops if @idle_skip
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # A .d80 or .d82 image as the floppy controller finds it on the disk:
7
+ # each track a ring of byte cells, each cell ten bit cells the GCR ROM
8
+ # turns into a data byte (Track::SYNC for a run of ones), so the
9
+ # mechanism needs no GCR. A track is laid out as the DOS's format code
10
+ # writes it ($C000-$C363 of DOS 2.7, run on the controller), sector
11
+ # after sector:
12
+ #
13
+ # 4 sync, $08, checksum, sector, track, ID 2, ID 1, $0F,
14
+ # 19 x $0F, 5 sync, $07, 256 data bytes, checksum, then $00
15
+ #
16
+ # the header checksum the EOR of the track, sector and IDs, and the
17
+ # data block's that of its bytes. The $00 gaps share out what the
18
+ # track has left over.
19
+ #
20
+ # Tracks come off the image as the head first reaches them. What the
21
+ # head writes goes back to the image when the motor stops (flush):
22
+ # every data block found on a written track whose header and checksum
23
+ # read cleanly.
24
+ class Disk
25
+ SYNC = 0x100
26
+
27
+ attr_reader :image
28
+
29
+ def initialize(image)
30
+ @image = image
31
+ @tracks = {}
32
+ @written = {}
33
+ end
34
+
35
+ def write_protected? = @image.read_only?
36
+
37
+ # The image's host file.
38
+ def path = @image.path
39
+
40
+ # How many tracks a side holds: 77.
41
+ def side_tracks = Storage::D80Image::SIDE_TRACKS
42
+
43
+ def sides = @image.is_a?(Storage::D82Image) ? 2 : 1
44
+
45
+ # The cells of +track+, 1 to 77 on the first side and 78 to 154 on
46
+ # the second, or nil past the disk's last.
47
+ def track(number)
48
+ return nil unless number.between?(1, side_tracks * sides)
49
+
50
+ @tracks[number] ||= build(number)
51
+ end
52
+
53
+ # The cells a track of the zone holds in one turn at 300 rpm: the
54
+ # 200,000 cycles of a turn over the zone's ten bit cells a byte at
55
+ # 6 MHz / (16 - zone).
56
+ def self.cells_in(zone) = 120_000 / (16 - zone)
57
+
58
+ # The zone a track is recorded in, 3 outermost to 0 innermost.
59
+ def self.zone_of(track)
60
+ cylinder = ((track - 1) % Storage::D80Image::SIDE_TRACKS) + 1
61
+ 3 - Storage::D80Image::ZONE_LAST_TRACKS.count { |last| cylinder > last }
62
+ end
63
+
64
+ # The head wrote to +track+.
65
+ def written(number)
66
+ @written[number] = true
67
+ end
68
+
69
+ # Stores the blocks the head wrote in the image.
70
+ def flush
71
+ return if @written.empty?
72
+
73
+ blocks = []
74
+ @written.each_key { |number| blocks.concat(read_blocks(@tracks[number])) }
75
+ @written.clear
76
+ @image.store_blocks(blocks) unless blocks.empty?
77
+ end
78
+
79
+ private
80
+
81
+ def build(number)
82
+ id1, id2 = disk_id
83
+ cells = []
84
+ sectors = @image.sectors_in(number)
85
+ sectors.times do |sector|
86
+ data = @image.read_block(number, sector)
87
+ cells.push(SYNC, SYNC, SYNC, SYNC, 0x08, number ^ sector ^ id1 ^ id2, sector, number, id2, id1)
88
+ cells.concat(Array.new(20, 0x0f))
89
+ cells.push(SYNC, SYNC, SYNC, SYNC, SYNC, 0x07)
90
+ cells.concat(data)
91
+ cells << checksum(data)
92
+ end
93
+ pad(cells, sectors, self.class.cells_in(self.class.zone_of(number)))
94
+ end
95
+
96
+ # Shares the $00 gap out after each sector.
97
+ def pad(cells, sectors, length)
98
+ sector_length = cells.length / sectors
99
+ gap = (length - cells.length) / sectors
100
+ padded = []
101
+ sectors.times do |sector|
102
+ padded.concat(cells[sector * sector_length, sector_length])
103
+ padded.concat(Array.new(sector == sectors - 1 ? length - padded.length : gap, 0x00))
104
+ end
105
+ padded
106
+ end
107
+
108
+ def checksum(bytes)
109
+ sum = 0
110
+ bytes.each { |byte| sum ^= byte }
111
+ sum
112
+ end
113
+
114
+ # The ID the header block holds at $18.
115
+ def disk_id
116
+ header = @image.read_block(*@image.header_block)
117
+ [header[0x18], header[0x19]]
118
+ end
119
+
120
+ # Every block whose header and data read cleanly off +cells+, as
121
+ # [track, sector, data, nil].
122
+ def read_blocks(cells)
123
+ blocks = []
124
+ index = 0
125
+ length = cells.length
126
+ while (index = next_block(cells, index, 0x08))
127
+ header = Array.new(5) { |offset| cells[(index + offset) % length] }
128
+ index = next_block(cells, index, 0x07)
129
+ break unless index
130
+
131
+ block = data_block(cells, index, header)
132
+ blocks << block if block
133
+ break if index + 260 >= length
134
+ end
135
+ blocks
136
+ end
137
+
138
+ # The cell after the next sync run followed by +mark+, from
139
+ # +index+, or nil.
140
+ def next_block(cells, index, mark)
141
+ length = cells.length
142
+ while index < length
143
+ return index + 2 if cells[index] == SYNC && cells[(index + 1) % length] == mark
144
+
145
+ index += 1
146
+ end
147
+ nil
148
+ end
149
+
150
+ def data_block(cells, index, header)
151
+ checksum, sector, track, id2, id1 = header
152
+ return nil unless checksum == (track ^ sector ^ id1 ^ id2)
153
+
154
+ length = cells.length
155
+ data = Array.new(256) { |offset| cells[(index + offset) % length] }
156
+ return nil if data.include?(SYNC) || checksum(data) != cells[(index + 256) % length]
157
+
158
+ [track, sector, data, nil]
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,91 @@
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.
19
+ module DOSBoard
20
+ attr_reader :device, :ieee_bus
21
+
22
+ def ip_cpu = settled(@ip_cpu)
23
+
24
+ def ip_bus = settled(@ip_bus)
25
+
26
+ def common = settled(@common)
27
+
28
+ def data_riot = settled(@data_riot)
29
+
30
+ def control_riot = settled(@control_riot)
31
+
32
+ def via = settled(@via)
33
+
34
+ def cycles
35
+ settle!
36
+ @cycles
37
+ end
38
+
39
+ attr_writer :host_clock_hz
40
+
41
+ # Plugs the unit into an IEEE-488 bus, leaving the one it was on.
42
+ def connect(ieee_bus)
43
+ settle!
44
+ @ieee_bus.detach(self)
45
+ @ieee_bus = ieee_bus
46
+ @ieee_port.bus = ieee_bus
47
+ ieee_bus.attach(self)
48
+ end
49
+
50
+ # The lines the unit pulls on the bus while ATN is low or not
51
+ # (IEEEPort).
52
+ def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
53
+
54
+ # The controller pushed its lines into the bus.
55
+ def host_written! = nil
56
+
57
+ # Which LEDs are lit, as the unit's port drives them. They hold
58
+ # still while the unit sleeps, so reading them leaves it asleep.
59
+ def leds = @ieee_port.leds
60
+
61
+ def power_led_on? = true
62
+
63
+ # Runs the drive cycles that fall in one host cycle, then takes the
64
+ # controller's lines for the next. Asleep, the unit owes the cycles
65
+ # instead, and wakes once the controller has moved a line.
66
+ def host_cycle!
67
+ phase = @phase + CLOCK_HZ
68
+ while phase >= @host_clock_hz
69
+ @asleep ? owe_cycle : run_cycle
70
+ phase -= @host_clock_hz
71
+ end
72
+ @phase = phase
73
+ settle! if @asleep && @ieee_port.lines_moved?
74
+ @ieee_port.latch_host
75
+ end
76
+
77
+ def cycle!
78
+ settle!
79
+ run_cycle
80
+ end
81
+
82
+ private
83
+
84
+ def settled(part)
85
+ settle!
86
+ part
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The floppy controller's bus (schematic 8050001, sheet 2), which
7
+ # decodes A0-A12 only, so it repeats every 8 KB. A 74LS42 on A10-A12
8
+ # picks a 1 KB block:
9
+ #
10
+ # $0000-$03FF - the 6530 and the VIA: with A7 set the 6530's
11
+ # registers, else with A6 set the VIA's, else the
12
+ # 6530's 64 bytes of RAM, which the controller also
13
+ # uses as its stack at $0100
14
+ # $0400-$13FF - the common RAM
15
+ # $1400-$1BFF - nothing: open bus
16
+ # $1C00-$1FFF - the 6530's ROM
17
+ #
18
+ # Writes to the ROM and to open bus go nowhere, and a read from open
19
+ # bus finds the last byte on the data bus.
20
+ class FDCBus
21
+ include Addressable
22
+
23
+ attr_reader :common, :via, :rriot
24
+ attr_accessor :data
25
+
26
+ # How many writes have changed a byte of the common RAM
27
+ # (Model8050::Idle).
28
+ attr_reader :common_changes
29
+
30
+ # Whether a counter or an interrupt flag has been read since it was
31
+ # last cleared (Model8050::Idle).
32
+ attr_accessor :volatile
33
+
34
+ def initialize(common:, via:, rriot:)
35
+ addressable_at(0, length: 2**16)
36
+ @common = common
37
+ @via = via
38
+ @rriot = rriot
39
+ @data = 0
40
+ @volatile = false
41
+ @common_changes = 0
42
+ end
43
+
44
+ def peek(addr)
45
+ addr &= 0x1fff
46
+ @data = if addr < 0x0400
47
+ peek_chips(addr)
48
+ elsif addr < 0x1400
49
+ @common.peek(addr - 0x0400)
50
+ elsif addr >= 0x1c00
51
+ @rriot.read_rom(addr)
52
+ else
53
+ @data
54
+ end
55
+ end
56
+
57
+ def poke(addr, value)
58
+ @data = value
59
+ addr &= 0x1fff
60
+ if addr < 0x0400
61
+ poke_chips(addr, value)
62
+ elsif addr < 0x1400
63
+ poke_common(addr - 0x0400, value)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def poke_common(index, value)
70
+ @common_changes += 1 if @common.peek(index) != value
71
+ @common.poke(index, value)
72
+ end
73
+
74
+ def peek_chips(addr)
75
+ if addr.anybits?(0x80)
76
+ @volatile = true if addr.anybits?(0x04)
77
+ return @rriot.peek(addr & 0x0f)
78
+ end
79
+ return peek_via(addr & 0x0f) if addr.anybits?(0x40)
80
+
81
+ @rriot.read_ram(addr)
82
+ end
83
+
84
+ # The VIA's counters, shift register and flags move on their own.
85
+ def peek_via(register)
86
+ @volatile = true if register.between?(0x04, 0x0a) || register == 0x0d
87
+ @via.peek(0x40 | register)
88
+ end
89
+
90
+ def poke_chips(addr, value)
91
+ if addr.anybits?(0x80)
92
+ @rriot.poke(addr & 0x0f, value)
93
+ elsif addr.anybits?(0x40)
94
+ @via.poke(0x40 | (addr & 0x0f), value)
95
+ else
96
+ @rriot.write_ram(addr, value)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end