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
@@ -14,7 +14,6 @@ module Badline
14
14
  # What inserting a tape raises on a machine without a cassette port.
15
15
  class Missing < ArgumentError; end
16
16
  attr_reader :tape
17
- attr_writer :motor
18
17
 
19
18
  def initialize(tape = nil, level: false)
20
19
  @tape = tape
@@ -30,6 +29,8 @@ module Badline
30
29
  @low_recorded = false
31
30
  @flag_handler = nil
32
31
  @sense_handler = nil
32
+ @running_handler = nil
33
+ @running = false
33
34
  end
34
35
 
35
36
  # Falling edges on the tape read line.
@@ -42,6 +43,17 @@ module Badline
42
43
  @sense_handler = handler
43
44
  end
44
45
 
46
+ # Whether the tape runs (#running?), each time that changes, a
47
+ # snapshot's load included.
48
+ def on_running_change(&handler)
49
+ @running_handler = handler
50
+ end
51
+
52
+ def motor=(on)
53
+ @motor = on
54
+ update_running
55
+ end
56
+
45
57
  # Whether the machine has a datasette at its cassette port. The SX-64
46
58
  # has none: its keys stay up and it takes no tape.
47
59
  def connected? = @connected
@@ -56,11 +68,13 @@ module Badline
56
68
 
57
69
  @tape = tape
58
70
  rewind
71
+ update_running
59
72
  end
60
73
 
61
74
  def eject
62
75
  stop!
63
76
  @tape = nil
77
+ update_running
64
78
  end
65
79
 
66
80
  def rewind
@@ -117,7 +131,8 @@ module Badline
117
131
 
118
132
  def motor? = @motor
119
133
 
120
- def running? = @playing && @motor && !@tape.nil?
134
+ # Whether the tape moves: a key down, the motor on and a tape in.
135
+ def running? = @running
121
136
 
122
137
  # The tape read line's level, on a datasette built with +level: true+.
123
138
  # Each pulse holds it low for its first half and high for its second,
@@ -131,7 +146,7 @@ module Badline
131
146
  end
132
147
 
133
148
  def cycle!
134
- return unless running?
149
+ return unless @running
135
150
  return @since += 1 if @recording
136
151
 
137
152
  next_pulse if @countdown.zero?
@@ -175,9 +190,18 @@ module Badline
175
190
 
176
191
  finish_recording unless down
177
192
  @playing = down
193
+ update_running
178
194
  @sense_handler&.call
179
195
  end
180
196
 
197
+ def update_running
198
+ running = @playing && @motor && !@tape.nil?
199
+ return if running == @running
200
+
201
+ @running = running
202
+ @running_handler&.call
203
+ end
204
+
181
205
  # Releasing the keys ends a recording, and writes the tape out.
182
206
  def finish_recording
183
207
  return unless @recording
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ # The true drives the PET's and the CBM-II's IEEE-488 bus takes, by the
6
+ # names --drive-type gives them, xpet's drive types, and the disk images
7
+ # each reads. Options reads it to check --drive-type, and
8
+ # Media::TrueDrive to pick the drive an image goes in, so it loads
9
+ # nothing else. Machine::IEEEDrives builds each.
10
+ module IEEEModels
11
+ # The images each model reads, by extension.
12
+ MEDIA = {
13
+ "2031" => %w[.d64 .g64], "8050" => %w[.d80], "8250" => %w[.d82], "1001" => %w[.d82], "4040" => %w[.d64 .d67],
14
+ "3040" => %w[.d64 .d67], "2040" => %w[.d67], "9060" => %w[.d90], "9090" => %w[.d90]
15
+ }.freeze
16
+
17
+ # The model --true-drive plugs in without --drive-type.
18
+ DEFAULT = "2031"
19
+
20
+ # The model an image goes in when neither the drive on device 8 nor
21
+ # the machine's own model reads it. A .d90 goes by its size
22
+ # (hard_disk_model).
23
+ FOR_IMAGES = { ".d64" => "2031", ".g64" => "2031", ".d80" => "8050", ".d82" => "8250", ".d67" => "2040" }.freeze
24
+
25
+ NAMES = MEDIA.keys.freeze
26
+
27
+ module_function
28
+
29
+ # Whether the image at path is a hard disk's, which only a true drive
30
+ # reads.
31
+ def hard_disk?(path) = File.extname(path).casecmp?(".d90")
32
+
33
+ # The hard disk a .d90 image belongs to, by its size: a D9090 for a
34
+ # D9090's, and otherwise a D9060.
35
+ def hard_disk_model(path) = Storage::D90Image.heads_for(File.size?(path).to_i) == 6 ? "9090" : "9060"
36
+
37
+ # Whether the model reads the image at path.
38
+ def reads?(model, path) = MEDIA.fetch(model, []).include?(File.extname(path).downcase)
39
+
40
+ # The model the image at path goes in: the drive's on device 8,
41
+ # `present`, if it reads it, or else the machine's own, `chosen`, or
42
+ # else the image's own, or nil for an image none reads. A .d90 goes
43
+ # in the machine's own model if that is a hard disk, and otherwise in
44
+ # the one its size names.
45
+ def taking(path, present, chosen)
46
+ return reads?(chosen, path) ? chosen : hard_disk_model(path) if hard_disk?(path)
47
+ return present if present && reads?(present, path)
48
+ return chosen if reads?(chosen, path)
49
+
50
+ FOR_IMAGES[File.extname(path).downcase]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model8050"
4
+
5
+ module Badline
6
+ module Drives
7
+ # The SFD-1001: the 8250LP's board redrawn as one PCB with one
8
+ # double-sided drive, a Matsushita JU-570-2 (schematic 251406). Both
9
+ # processors see an 8250. The interface processor runs DOS 2.7, which
10
+ # doesn't know there is one drive, and the controller runs 251257-02A
11
+ # from the EPROM on its 6530 adaptor (251252-01): the 8250LP's program
12
+ # with an interrupt every 3 ms for the JU-570's step rate, a longer head
13
+ # settle, and every job for drive 1 ended with an error. Drive 1's
14
+ # stepper, motor and LED aren't wired, so the unit takes no disk there,
15
+ # and its LED never lights.
16
+ class Model1001 < Model8250
17
+ # The LED bit of drive 1, which the board leaves unwired.
18
+ DRIVE1_LED = 0x01
19
+
20
+ def model_name = "1001"
21
+
22
+ # +fdc_rom+ defaults to 251257-02A, the rest as Model8050's.
23
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
24
+ super(dos_rom:, fdc_rom: fdc_rom || ROM.read("1001/fdc-251257-02a.rom"), host_clock_hz:, device:)
25
+ end
26
+
27
+ def leds = super & ~DRIVE1_LED
28
+
29
+ # Puts a Disk in drive 0, the only one. Drive 1 raises ArgumentError.
30
+ def insert(disk, drive: 0)
31
+ raise ArgumentError, "the 1001 has one drive, not drive #{drive}" unless drive.zero?
32
+
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
@@ -34,6 +34,12 @@ module Badline
34
34
 
35
35
  def model_name = "2031"
36
36
 
37
+ def boot_cycles = BOOT_CYCLES
38
+
39
+ # Whether the drive is where VICE saves it: its CPU between
40
+ # instructions.
41
+ def settled_for_vice? = cpu.boundary?
42
+
37
43
  # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
38
44
  # path. +device+ is the number the jumpers set, 8 to 11.
39
45
  def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # A .d64 image as the 4040's controller finds it on the disk, each
7
+ # track a ring of byte cells as on the 8050 (Model8050::Disk), laid
8
+ # out as DOS 2.1's format writes it, sector after sector:
9
+ #
10
+ # 4 sync, $08, checksum, sector, track, ID 2, ID 1,
11
+ # 9 x $00, 4 sync, $07, 256 data bytes, checksum, then $00
12
+ #
13
+ # nine cells being the header gap the interface processor gives the
14
+ # controller ($049D, from DOS 2.1's SECTRK table), past which a write
15
+ # puts its syncs. A .d67, DOS 1's disk, is laid out as DOS 1 writes a
16
+ # sector: 2 sync, the header, one $08, 2 sync, $07, the data and its
17
+ # checksum. DOS 1's format writes headers only, so this is the track as
18
+ # if every sector had been written. The disk's ID is the BAM's, at
19
+ # 18/0 $A2. A turn passes 80,000 / (16 - zone) cells, the 16 MHz bit
20
+ # clock's.
21
+ class Disk < Model8050::Disk
22
+ # How many tracks a disk holds: 35.
23
+ def side_tracks = 35
24
+
25
+ def sides = 1
26
+
27
+ # The cells a track of the zone holds in one turn at 300 rpm: the
28
+ # 200,000 µs of a turn over 2.5 µs times 16 less the zone a byte.
29
+ def self.cells_in(zone) = 80_000 / (16 - zone)
30
+
31
+ # The zone a track is recorded in, 3 outermost to 0 innermost.
32
+ def self.zone_of(track) = Storage::D64Image.zone(track)
33
+
34
+ private
35
+
36
+ def build(number)
37
+ id1, id2 = disk_id
38
+ cells = []
39
+ sectors = @image.sectors_in(number)
40
+ dos1 = @image.is_a?(Storage::D67Image)
41
+ sectors.times do |sector|
42
+ data = @image.read_block(number, sector)
43
+ header = [0x08, number ^ sector ^ id1 ^ id2, sector, number, id2, id1]
44
+ if dos1
45
+ cells.push(SYNC, SYNC, *header, 0x08, SYNC, SYNC, 0x07)
46
+ else
47
+ cells.push(SYNC, SYNC, SYNC, SYNC, *header, *Array.new(9, 0x00), SYNC, SYNC, SYNC, SYNC, 0x07)
48
+ end
49
+ cells.concat(data)
50
+ cells << checksum(data)
51
+ end
52
+ pad(cells, sectors, self.class.cells_in(self.class.zone_of(number)))
53
+ end
54
+
55
+ def disk_id
56
+ bam = @image.read_block(18, 0)
57
+ [bam[0xa2], bam[0xa3]]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The interface processor's bus (schematic 320806, sheet 1). UB3
7
+ # decodes A12-A14 only, so the map repeats every 32 KB:
8
+ #
9
+ # $0000-$0FFF - the two 6532s, as on the 8050 (Model8050::IPBus)
10
+ # $1000-$4FFF - the common RAM, 1 KB in each block, mirrored through it
11
+ # $5000-$7FFF - the ROM sockets UJ1, UL1 and UH1, seen again at
12
+ # $D000-$FFFF, where the processor finds its vectors
13
+ #
14
+ # A socket left empty, as UJ1 is under DOS 1, reads as open bus: the
15
+ # last byte on the data bus.
16
+ class IPBus < Model8050::IPBus
17
+ def peek(addr)
18
+ addr &= 0x7fff
19
+ return @data = rom_peek(addr | 0x8000) if addr >= 0x5000
20
+
21
+ @data = if addr < 0x1000
22
+ peek_riots(addr)
23
+ else
24
+ @common.peek((((addr >> 12) - 1) << 10) | (addr & 0x03ff))
25
+ end
26
+ end
27
+
28
+ def poke(addr, value)
29
+ @data = value
30
+ addr &= 0x7fff
31
+ return if addr >= 0x5000
32
+
33
+ if addr < 0x1000
34
+ poke_riots(addr, value)
35
+ else
36
+ poke_common((((addr >> 12) - 1) << 10) | (addr & 0x03ff), value)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def rom_peek(addr) = addr >= @rom.start ? @rom.peek(addr) : @data
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ class Disk
7
+ def image_format = @image.is_a?(Storage::D67Image) ? "d67" : "d64"
8
+
9
+ # The disk a BL-D8-DISK module holds, as Model8050::Disk's, from a
10
+ # .d64 or a .d67.
11
+ def self.from_fields(input, detached: false)
12
+ path = input.text("path", "")
13
+ read_only = input.bool("read_only", false) || detached
14
+ kind = input.choice("image.format", %w[d64 d67], "d64")
15
+ bytes = input.bytes("image", "".b).bytes
16
+ path = "" if detached || !File.file?(path)
17
+ image = if kind == "d67"
18
+ Storage::D67Image.new(path, read_only:, bytes:)
19
+ else
20
+ Storage::D64Image.new(path, read_only:, bytes:)
21
+ end
22
+ new(image).load_track_fields(input)
23
+ end
24
+ end
25
+
26
+ class Mechanism
27
+ # The drives' fields in a keyed snapshot module (BL-D8-MECH), a value
28
+ # a drive in each list: the heads' half tracks, the stepper counts,
29
+ # each disk's place in its turn and its bit clock and the motors,
30
+ # then byte ready, the byte on VIA port A and SYNC. The disks load
31
+ # from their own modules.
32
+ module KeyedState
33
+ def save_fields(out)
34
+ out.ints("half_tracks", @half_tracks).ints("phases", @phases).ints("positions", @positions)
35
+ out.ints("bit_clocks", @clocks)
36
+ out.ints("motors", @motors.map { |on| on ? 1 : 0 }).ints("syncs", @syncs.map { |low| low ? 1 : 0 })
37
+ out.bool("ca1_low", @ca1_low).int("data_in", @data_in)
38
+ end
39
+
40
+ def load_fields(input, disks)
41
+ @disks = disks
42
+ input.ints_into("half_tracks", @half_tracks)
43
+ input.ints_into("phases", @phases)
44
+ input.ints_into("positions", @positions)
45
+ input.ints_into("bit_clocks", @clocks)
46
+ @motors = input.ints("motors", [0, 0]).map { |on| on == 1 }
47
+ @syncs = input.ints("syncs", [0, 0]).map { |low| low == 1 }
48
+ @ca1_low = input.bool("ca1_low", @ca1_low)
49
+ @data_in = input.byte("data_in", @data_in)
50
+ end
51
+ end
52
+
53
+ include KeyedState
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The unit's two SA390 drives as the 6504 sees them through the VIA
7
+ # and the 6530 (schematic 320806, sheets 2 and 3).
8
+ #
9
+ # VIA port B runs the drives: PB5 drive 0's motor and PB4 drive 1's,
10
+ # each on while low, PB2-3 drive 0's stepper and PB0-1 drive 1's. The
11
+ # 6530's port B picks the drive on PB0 and the zone on PB1-2, and
12
+ # reads write protect on PB3, high while the disk can't be written,
13
+ # and SYNC on PB6, low while ten one bits sit in the shift register.
14
+ # Its port A holds the byte to write.
15
+ #
16
+ # Each stepper's two bits count in binary: one up moves the head half
17
+ # a track in, one down half a track out, and the controller steps
18
+ # once an interrupt, twice a track ($FF82), bumping the head against
19
+ # track 1 with 116 steps out.
20
+ #
21
+ # A turning disk passes one byte cell under the head every ten bit
22
+ # cells, at 16 MHz divided by 16 less the track's zone and then by 4,
23
+ # and the byte cells come round 300 times a minute (Disk). Each data
24
+ # cell ends in byte ready, which latches the byte into VIA port A
25
+ # through CA1; the controller has no SO pin and polls the VIA's
26
+ # interrupt flags for it. A sync cell holds SYNC low instead. While
27
+ # the VIA's CB2 is low the drive writes: each cell takes the 6530's
28
+ # port A, or a sync while CA2 is high, and still ends in byte ready.
29
+ # The format code holds CA2 high in pulse mode, which reading port A
30
+ # pulls low for a cycle.
31
+ class Mechanism
32
+ WRITE_PROTECT = 0x08
33
+ SYNC_LINE = 0x40
34
+
35
+ # The half tracks a head can reach: track 1 to 35.
36
+ HALF_TRACKS = 70
37
+
38
+ attr_reader :data_in, :half_tracks
39
+ attr_accessor :via, :rriot
40
+
41
+ def initialize
42
+ @via = nil
43
+ @rriot = nil
44
+ @disks = [nil, nil]
45
+ @half_tracks = [0, 0]
46
+ @phases = [0, 0]
47
+ @positions = [0, 0]
48
+ @clocks = [0, 0]
49
+ @motors = [false, false]
50
+ @data_in = 0xff
51
+ @syncs = [false, false]
52
+ @ca1_low = false
53
+ end
54
+
55
+ # The Disk in +drive+, 0 or 1, or nil.
56
+ def disk(drive = 0) = @disks[drive]
57
+
58
+ def insert(disk, drive = 0)
59
+ @disks[drive]&.flush
60
+ @disks[drive] = disk
61
+ end
62
+
63
+ def flush
64
+ @disks.each { |disk| disk&.flush }
65
+ end
66
+
67
+ def motor_on?(drive = 0) = @motors[drive]
68
+
69
+ # The track under +drive+'s head, 1 to 35.
70
+ def track(drive = 0) = (@half_tracks[drive] >> 1) + 1
71
+
72
+ def cycle!
73
+ if @ca1_low
74
+ @ca1_low = false
75
+ @via.ca1 = true
76
+ end
77
+ drives = @via.port_b_output
78
+ turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
79
+ turn(1, drives.nobits?(0x10), drives & 0x03)
80
+ end
81
+
82
+ # Whether a cycle would change nothing: both motors off and no byte
83
+ # ready under way.
84
+ def quiet? = !@motors[0] && !@motors[1] && !@ca1_low
85
+
86
+ # What the drives hold, for comparing them at two points.
87
+ def idle_state = [*@half_tracks, *@phases, *@motors, *@syncs, @data_in]
88
+
89
+ # The 6530's port B inputs.
90
+ def status_lines
91
+ drive = selected_drive
92
+ lines = 0xff
93
+ lines &= ~WRITE_PROTECT unless @disks[drive]&.write_protected?
94
+ @syncs[drive] ? lines & ~SYNC_LINE : lines
95
+ end
96
+
97
+ private
98
+
99
+ def selected_drive = @rriot.port_b_output & 0x01
100
+
101
+ def turn(drive, motor, phase)
102
+ step(drive, phase) if phase != @phases[drive]
103
+ motor_off(drive) if @motors[drive] && !motor
104
+ @motors[drive] = motor
105
+ return @syncs[drive] = false unless motor && @disks[drive]
106
+
107
+ clock = @clocks[drive] + 2
108
+ byte = 5 * (16 - Disk.zone_of(track(drive)))
109
+ if clock >= byte
110
+ clock -= byte
111
+ pass_cell(drive)
112
+ end
113
+ @clocks[drive] = clock
114
+ end
115
+
116
+ def step(drive, phase)
117
+ change = (phase - @phases[drive]) & 0x03
118
+ @phases[drive] = phase
119
+ half_track = @half_tracks[drive]
120
+ if change == 1
121
+ half_track += 1
122
+ elsif change == 3
123
+ half_track -= 1
124
+ end
125
+ @half_tracks[drive] = half_track.clamp(0, HALF_TRACKS - 1)
126
+ end
127
+
128
+ def motor_off(drive)
129
+ @disks[drive]&.flush
130
+ end
131
+
132
+ # A cell has passed under +drive+'s head.
133
+ def pass_cell(drive)
134
+ disk = @disks[drive]
135
+ number = track(drive)
136
+ cells = disk.track(number)
137
+ return unless cells
138
+
139
+ position = @positions[drive] % cells.length
140
+ @positions[drive] = position + 1
141
+ return unless drive == selected_drive
142
+
143
+ writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
144
+ end
145
+
146
+ def writing? = !@via.cb2_output
147
+
148
+ def read_cell(drive, cells, position)
149
+ cell = cells[position]
150
+ @syncs[drive] = cell == Disk::SYNC
151
+ byte_ready(cell) unless cell == Disk::SYNC
152
+ end
153
+
154
+ def write_cell(drive, disk, number, cells, position)
155
+ cells[position] = @via.ca2_output ? Disk::SYNC : @rriot.port_a_output
156
+ disk.written(number)
157
+ @syncs[drive] = false
158
+ byte_ready(@data_in)
159
+ end
160
+
161
+ def byte_ready(byte)
162
+ @data_in = byte
163
+ @via.ca1 = false
164
+ @ca1_low = true
165
+ end
166
+ end
167
+
168
+ # VIA port A reads the byte the last cell latched. Port B drives the
169
+ # drives only.
170
+ class ReadLines
171
+ def initialize(mechanism)
172
+ @mechanism = mechanism
173
+ end
174
+
175
+ def read_a(_lines) = @mechanism.data_in
176
+
177
+ def read_b(_lines) = 0xff
178
+ end
179
+
180
+ # The 6530's port B inputs from the drives.
181
+ class StatusLines
182
+ def initialize(mechanism)
183
+ @mechanism = mechanism
184
+ end
185
+
186
+ def read_a(_lines) = 0xff
187
+
188
+ def read_b(_lines) = @mechanism.status_lines
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The interface processor's 6502, UN1, on the 4040's IPBus.
7
+ class IPCPU
8
+ include Badline::Chips::CPU::Core
9
+ include Engine::IdleCPU
10
+ end
11
+
12
+ # The floppy controller's 6504, UH3, on the 8050's FDCBus. It has no
13
+ # SO pin.
14
+ class FDCCPU
15
+ include Badline::Chips::CPU::Core
16
+ include Engine::IdleCPU
17
+ end
18
+ end
19
+ end
20
+ end