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,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ class Bus
6
+ # The RAM, the expansion's too, and the last byte on the bus, in a
7
+ # keyed snapshot module (BL-RAM). The 6530s keep their own 64 bytes.
8
+ module KeyedState
9
+ def save_fields(out)
10
+ @ram.save_fields(out, "ram")
11
+ out.int("data", @data)
12
+ end
13
+
14
+ def load_fields(input)
15
+ @ram.load_fields(input, "ram")
16
+ @data = input.byte("data", @data)
17
+ end
18
+ end
19
+
20
+ include KeyedState
21
+ end
22
+
23
+ class Panel
24
+ # The panel's fields in a keyed snapshot module (BL-PANEL): the
25
+ # TTY/KB jumper, the key held down, the digit lit and its segments
26
+ # since the cycle they lit, the cycles each segment has been lit,
27
+ # and what each digit last showed and when. The teletype restores
28
+ # the line in.
29
+ module KeyedState
30
+ def save_fields(out)
31
+ out.bool("tty", @tty).int("held", @held).int("digit", @digit).int("segments", @segments)
32
+ out.int("since", @since).ints("lit", @lit).ints("shown", @shown).ints("shown_at", @shown_at)
33
+ end
34
+
35
+ def load_fields(input)
36
+ @tty = input.bool("tty", false)
37
+ held = input.int("held", -1)
38
+ @held = held.between?(0, KEYS.length - 1) ? held : -1
39
+ digit = input.int("digit", -1)
40
+ @digit = digit.between?(0, DIGITS - 1) ? digit : -1
41
+ @segments = input.masked("segments", 0, 0x7f)
42
+ @since = input.int("since", 0)
43
+ input.ints_into("lit", @lit)
44
+ input.ints_into("shown", @shown)
45
+ input.ints_into("shown_at", @shown_at)
46
+ end
47
+ end
48
+
49
+ include KeyedState
50
+ end
51
+
52
+ class Tape
53
+ # The deck's fields in a keyed snapshot module (BL-DECK): whether
54
+ # PLAY or RECORD is down, how far into the tape the deck is, the next
55
+ # of the tape's edges to play and PB7's level from it, the level U2
56
+ # last drove PB7 to, and the tape itself under tape.*: its file's
57
+ # path, and the time from each crossing to the next, from the start.
58
+ # The tape goes back in from the module, and STOP after a recording
59
+ # writes it to that path.
60
+ module KeyedState
61
+ def save_fields(out)
62
+ out.bool("playing", @playing).bool("recording", @recording).int("position", position)
63
+ out.int("next_edge", @next_edge).bool("high", @high).bool("output_high", @output_high)
64
+ crossings = @tape.crossings
65
+ gaps = crossings.each_with_index.map { |time, index| index.zero? ? time : time - crossings[index - 1] }
66
+ out.text("tape.path", @tape.path).ints("tape.gaps", gaps)
67
+ end
68
+
69
+ def load_fields(input)
70
+ time = 0
71
+ crossings = input.ints("tape.gaps", []).map { |gap| time += gap }
72
+ @tape = Storage::KIMTape.new(input.text("tape.path", ""), crossings:)
73
+ @playing = input.bool("playing", false)
74
+ @recording = input.bool("recording", false) && !@playing
75
+ @stopped_at = input.int("position", 0)
76
+ @started = @clock.cycles
77
+ @edges = @playing ? @tape.edges : []
78
+ @next_edge = input.int("next_edge", 0).clamp(0, @edges.length)
79
+ @high = input.bool("high", true)
80
+ @output_high = input.bool("output_high", true)
81
+ end
82
+ end
83
+
84
+ include KeyedState
85
+ end
86
+
87
+ # The machine's own fields in a keyed snapshot module (BL-MACHINE):
88
+ # the family, its setup (Snapshot::KIM1Setup), the cycle count, the
89
+ # SST switch and U3's PB7 wiring, and under host.* whether type_keys
90
+ # is typing, the keys it has still to press, by their place in KEY_NAMES, the one held, when
91
+ # the next goes, and the on_init handlers still to run. The badline
92
+ # version that wrote it is written for reports and never read.
93
+ module KeyedState
94
+ # The keys type_keys presses, as host.pending_keys numbers them.
95
+ KEY_NAMES = (Panel::KEYS + %w[ST RS]).freeze
96
+
97
+ # How many more on_init handlers the machine a snapshot was taken of
98
+ # had yet to run than this one has. Nil until the machine is restored.
99
+ attr_reader :init_handlers_lost
100
+
101
+ # Whether a snapshot saves now (Machine::Settling): the CPU between
102
+ # instructions or jammed.
103
+ def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
104
+
105
+ # Nothing on the board defers its work.
106
+ def settle_chips_for_snapshot = nil
107
+
108
+ def save_machine_fields(out)
109
+ out.text("family", "kim1").text("written_by", VERSION)
110
+ Snapshot::KIM1Setup.of(self).write_fields(out)
111
+ out.int("cycles", @cycles).bool("sst", @sst).bool("irq_wired", @irq_wired)
112
+ pending = @pending_keys
113
+ out.bool("host.typing", !pending.nil?)
114
+ out.ints("host.pending_keys", pending.map { |name| KEY_NAMES.index(name) }) if pending
115
+ out.bool("host.key_down", @key_down).int("host.next_key_at", @next_key_at)
116
+ out.int("host.init_pending", @cycles <= @init_threshold ? @init_handlers.length : 0)
117
+ end
118
+
119
+ def load_machine_fields(input)
120
+ @cycles = input.int("cycles", @cycles)
121
+ @sst = input.bool("sst", false)
122
+ @irq_wired = input.bool("irq_wired", false)
123
+ pending = input.ints("host.pending_keys", []).filter_map { |index| KEY_NAMES[index] if index >= 0 }
124
+ @pending_keys = input.bool("host.typing", false) ? pending : nil
125
+ @key_down = input.bool("host.key_down", false)
126
+ @next_key_at = input.int("host.next_key_at", 0)
127
+ @init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
128
+ end
129
+ end
130
+
131
+ include KeyedState
132
+ end
133
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The keypad and display on U2's ports (User Manual, figures 3.5 and
6
+ # 3.6), and the TTY/KB jumper.
7
+ #
8
+ # PB1-PB4 feed a 74145 decoder, U24. Its outputs 0-2 select the
9
+ # keypad's three rows, 3 the jumper, and 4-9 the six LED digits, left
10
+ # to right. PA0-PA6 drive the selected digit's segments a-g, a high
11
+ # line lighting one, and read the selected row's keys, a pressed key
12
+ # pulling its line low: PA6 is the row's first key, PA0 its seventh.
13
+ # Fitted, the jumper pulls PA0 low while output 3 is selected, which
14
+ # the monitor reads as teletype mode, and PA7 takes the teletype's
15
+ # line in (Teletype). PB7 is the cassette's line (Tape).
16
+ #
17
+ # Nothing holds the display lit. A program lights each digit in turn
18
+ # (the monitor's SCANDS, $1F1F), so the panel times how long each
19
+ # segment is lit from the writes to U2's ports: lit_cycles hands the
20
+ # totals to a front end, and display reads back what the digits show.
21
+ class Panel
22
+ # The keys of the matrix, by the code the monitor's GETKEY ($1F6A)
23
+ # gives them: the row times 7 plus the column.
24
+ KEYS = %w[0 1 2 3 4 5 6 7 8 9 A B C D E F AD DA + GO PC].freeze
25
+
26
+ # The monitor's segment patterns for 0-F, its table at $1FE7 without
27
+ # bit 7, which PA7 doesn't drive.
28
+ HEX_SEGMENTS = [0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
29
+ 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71].freeze
30
+
31
+ DIGITS = 6
32
+
33
+ # The decoder output of the leftmost digit.
34
+ FIRST_DIGIT = 4
35
+
36
+ # A digit lit for less than this many cycles at a time doesn't count
37
+ # as showing anything: the monitor passes its digits through that
38
+ # quickly between patterns.
39
+ SHOWN_CYCLES = 100
40
+
41
+ # A digit not lit for this many cycles, some five of the monitor's
42
+ # scans, reads as dark.
43
+ DARK_CYCLES = 20_000
44
+
45
+ # Whether the TTY/KB jumper is fitted.
46
+ attr_accessor :tty
47
+
48
+ # Whether the teletype's line in is high, its idle level. Low, it
49
+ # pulls PA7 low while PB5 is low, which gates it through.
50
+ attr_accessor :tty_in_high
51
+
52
+ # The cassette deck on PB7.
53
+ attr_reader :tape
54
+
55
+ # +clock+ answers +cycles+, the cycle each write to the ports lands
56
+ # on.
57
+ def initialize(clock)
58
+ @clock = clock
59
+ @rriot = nil
60
+ @tty = false
61
+ @tty_in_high = true
62
+ @tape = Tape.new(clock)
63
+ @held = -1
64
+ @digit = -1
65
+ @segments = 0
66
+ @since = 0
67
+ @lit = Array.new(DIGITS * 7, 0)
68
+ @shown = Array.new(DIGITS, 0)
69
+ @shown_at = Array.new(DIGITS, -DARK_CYCLES - 1)
70
+ end
71
+
72
+ # U2, whose port B selects the row the keys are read from.
73
+ def connect(rriot)
74
+ @rriot = rriot
75
+ end
76
+
77
+ # Holds down the key KEYS names `name`, and lets go of any other.
78
+ def press(name)
79
+ code = KEYS.index(name)
80
+ raise ArgumentError, "no KIM-1 key named #{name}" if code.nil?
81
+
82
+ @held = code
83
+ end
84
+
85
+ def release
86
+ @held = -1
87
+ end
88
+
89
+ # The name of the key held down, or nil.
90
+ def held = @held.negative? ? nil : KEYS[@held]
91
+
92
+ def read_a(_lines)
93
+ port_b = @rriot.port_b_output
94
+ row = decoded(port_b)
95
+ mask = @tty_in_high || port_b.anybits?(0x20) ? 0xff : 0x7f
96
+ mask &= 0xfe if row == 3 && @tty
97
+ held = @held
98
+ mask &= ~(0x40 >> (held % 7)) & 0xff if held >= 0 && held / 7 == row
99
+ mask
100
+ end
101
+
102
+ def read_b(_lines) = @tape.input
103
+
104
+ # The lines U2's ports drive after a write to them.
105
+ def lines_written(port_a, port_b)
106
+ @tape.output(port_b)
107
+ digit = decoded(port_b) - FIRST_DIGIT
108
+ digit = -1 unless digit >= 0 && digit < DIGITS
109
+ segments = digit.negative? ? 0 : port_a & 0x7f
110
+ return if digit == @digit && segments == @segments
111
+
112
+ now = @clock.cycles
113
+ end_lit(now)
114
+ @digit = digit
115
+ @segments = segments
116
+ @since = now
117
+ end
118
+
119
+ # The cycles each segment has been lit since the last call, seven to
120
+ # a digit from the left, a to g, and starts counting again.
121
+ def lit_cycles
122
+ now = @clock.cycles
123
+ end_lit(now)
124
+ @since = now
125
+ totals = @lit.dup
126
+ @lit.map! { 0 }
127
+ totals
128
+ end
129
+
130
+ # The segments, a in bit 0 to g in bit 6, each digit shows: its
131
+ # pattern the last time it was lit for SHOWN_CYCLES or more, or 0
132
+ # once it has been dark for DARK_CYCLES.
133
+ def display
134
+ now = @clock.cycles
135
+ Array.new(DIGITS) do |digit|
136
+ if digit == @digit && @segments != 0 && now - @since >= SHOWN_CYCLES
137
+ @segments
138
+ elsif now - @shown_at[digit] <= DARK_CYCLES
139
+ @shown[digit]
140
+ else
141
+ 0
142
+ end
143
+ end
144
+ end
145
+
146
+ # The display as text, such as "0200 A9": a space for a dark digit
147
+ # and ? for a pattern that isn't a hex digit, with a space between
148
+ # the address and the data.
149
+ def display_text
150
+ chars = display.map do |segments|
151
+ next " " if segments.zero?
152
+
153
+ index = HEX_SEGMENTS.index(segments)
154
+ index.nil? ? "?" : index.to_s(16).upcase
155
+ end
156
+ "#{chars[0, 4].join} #{chars[4, 2].join}"
157
+ end
158
+
159
+ private
160
+
161
+ def decoded(port_b) = (port_b >> 1) & 0x0f
162
+
163
+ def end_lit(now)
164
+ digit = @digit
165
+ return if digit.negative? || @segments.zero?
166
+
167
+ lit = now - @since
168
+ 7.times { |segment| @lit[(digit * 7) + segment] += lit if @segments.anybits?(1 << segment) }
169
+ return if lit < SHOWN_CYCLES
170
+
171
+ @shown[digit] = @segments
172
+ @shown_at[digit] = now
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # Each 6530 a class of its own, so each keeps one type of peripheral.
6
+
7
+ # U2, the 6530-002: the monitor's ROM, and the ports that drive the
8
+ # keypad, the display, the teletype and the tape. Its peripheral is
9
+ # the Panel, which hears every write to the port and direction
10
+ # registers, so it can time how long each LED segment is lit.
11
+ class MonitorRRIOT < Chips::RRIOT
12
+ def reset!
13
+ super
14
+ @peripheral&.lines_written(port_a_output, port_b_output)
15
+ end
16
+
17
+ def poke(addr, value)
18
+ super
19
+ @peripheral.lines_written(port_a_output, port_b_output) if addr.nobits?(0x04)
20
+ end
21
+ end
22
+
23
+ # U3, the 6530-003: the tape routines' ROM, and the ports the
24
+ # application connector carries for the user, with nothing on them.
25
+ class UserRRIOT < Chips::RRIOT
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The KIM-1's sound: U2's PB7, the tape output, which programs such as
6
+ # the First Book of KIM's music player toggle, or with the speaker
7
+ # wired, U3's PA0, where the User Manual's chapter 5 puts a speaker.
8
+ # Machine::LineSound turns it into samples.
9
+ class Sound
10
+ include Machine::LineSound
11
+
12
+ # Whether the speaker on U3's PA0 is wired, and plays in place of PB7.
13
+ attr_accessor :speaker
14
+
15
+ def initialize(monitor, user)
16
+ @monitor = monitor
17
+ @user = user
18
+ @speaker = false
19
+ init_line_sound
20
+ end
21
+
22
+ def model = :pb7
23
+
24
+ private
25
+
26
+ def line_high? = @speaker ? @user.port_a_output.anybits?(0x01) : @monitor.port_b_output.anybits?(0x80)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The cassette deck on U2's PB7 (User Manual, appendix E). Playing, it
6
+ # drives PB7 as the board's PLL and comparator would from the tape
7
+ # (Storage::KIMTape#edges), which the monitor's LOADT ($1873) reads.
8
+ # Recording, it puts each change of PB7, which DUMPT ($1800) toggles as
9
+ # a square wave, on the tape as a crossing, in place of whatever was
10
+ # on the tape from there on. Stopping a recording writes the tape to
11
+ # its file.
12
+ #
13
+ # Nothing on the board starts or stops the deck: the tape moves while
14
+ # PLAY or RECORD is down, a microsecond, a cycle, at a time.
15
+ class Tape
16
+ attr_reader :tape
17
+
18
+ # +clock+ answers +cycles+.
19
+ def initialize(clock)
20
+ @clock = clock
21
+ @tape = Storage::KIMTape.new
22
+ @playing = false
23
+ @recording = false
24
+ @stopped_at = 0
25
+ @started = 0
26
+ @edges = []
27
+ @next_edge = 0
28
+ @high = true
29
+ @output_high = true
30
+ end
31
+
32
+ def playing? = @playing
33
+
34
+ def recording? = @recording
35
+
36
+ # How far into the tape the deck is, in microseconds.
37
+ def position = moving? ? @stopped_at + @clock.cycles - @started : @stopped_at
38
+
39
+ # Puts `tape` in the deck, wound back to its start.
40
+ def insert_tape(tape)
41
+ stop
42
+ @tape = tape
43
+ @stopped_at = 0
44
+ end
45
+
46
+ # Takes the tape out, leaving a blank one with no file.
47
+ def eject
48
+ insert_tape(Storage::KIMTape.new)
49
+ end
50
+
51
+ def rewind
52
+ stop
53
+ @stopped_at = 0
54
+ end
55
+
56
+ # Presses PLAY.
57
+ def play
58
+ stop
59
+ @edges = @tape.edges
60
+ at = @stopped_at
61
+ index = 0
62
+ index += 1 while index < @edges.length && @edges[index] <= at
63
+ @next_edge = index
64
+ @high = index.even?
65
+ start
66
+ @playing = true
67
+ end
68
+
69
+ # Presses RECORD and PLAY.
70
+ def record
71
+ stop
72
+ @tape.erase_from(@stopped_at)
73
+ start
74
+ @recording = true
75
+ end
76
+
77
+ def stop
78
+ finish_recording = @recording
79
+ @stopped_at = position
80
+ @playing = @recording = false
81
+ @tape.save if finish_recording && !@tape.path.empty?
82
+ end
83
+
84
+ # PB7 as the deck drives it, in bit 7 of a port B read: the
85
+ # comparator's level while the tape plays, and high otherwise.
86
+ def input
87
+ return 0xff unless @playing
88
+
89
+ at = position
90
+ edges = @edges
91
+ while @next_edge < edges.length && edges[@next_edge] <= at
92
+ @high = !@high
93
+ @next_edge += 1
94
+ end
95
+ @high ? 0xff : 0x7f
96
+ end
97
+
98
+ # U2's port B after a write to it, which records a change of PB7.
99
+ def output(port_b)
100
+ high = port_b.anybits?(0x80)
101
+ return if high == @output_high
102
+
103
+ @output_high = high
104
+ @tape.record(position) if @recording
105
+ end
106
+
107
+ private
108
+
109
+ def moving? = @playing || @recording
110
+
111
+ def start
112
+ @started = @clock.cycles
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # A teletype on the KIM-1's 20 mA loop, full duplex, at `baud`
6
+ # (User Manual, section 2.6 and figure 3.7).
7
+ #
8
+ # It sends what #type is given on its line to the board, a start bit,
9
+ # eight data bits from the lowest and two stop bits a character, and
10
+ # the board takes the line in on U2's PA7 while PB5 is low. It prints
11
+ # what the board's printer line carries: PB0, which the monitor drives,
12
+ # and, through the NAND U26, every bit coming in, so the hardware
13
+ # echoes what is typed. It reads each character at its own baud,
14
+ # sampling the middle of each bit from the start bit's edge, a start
15
+ # bit high again by its middle a glitch, and keeps all it has printed
16
+ # in #output, without bit 7.
17
+ #
18
+ # The monitor reads a character only while it waits for one, so the
19
+ # teletype sends the next only once both lines have been quiet for
20
+ # GAP_CHARACTERS characters' time. After a reset it sends RUBOUT, which
21
+ # the monitor times to learn the baud.
22
+ class Teletype
23
+ RUBOUT = 0x7f
24
+ GAP_CHARACTERS = 2
25
+
26
+ # The cycles from a reset to the RUBOUT, by when the monitor waits
27
+ # for its start bit.
28
+ RUBOUT_DELAY = 2_000
29
+
30
+ BAUDS = [110, 300, 1200, 2400, 4800, 9600].freeze
31
+
32
+ # The bits of a character: the start bit, eight data bits and two
33
+ # stop bits.
34
+ FRAME_BITS = 11
35
+
36
+ attr_reader :baud, :output
37
+
38
+ def initialize(panel, baud: 9600)
39
+ @panel = panel
40
+ @queue = []
41
+ @output = +""
42
+ @line_high = true
43
+ @sending = 0
44
+ @sent = FRAME_BITS
45
+ @send_start = 0
46
+ @quiet_until = 0
47
+ @receiving = false
48
+ @received = 0
49
+ @bits = 0
50
+ @edge = 0
51
+ @sample_at = 0
52
+ @last_printer = true
53
+ self.baud = baud
54
+ end
55
+
56
+ # Sets the rate both ways, which the monitor learns at the next reset.
57
+ def baud=(baud)
58
+ raise ArgumentError, "no teletype at #{baud} baud" unless BAUDS.include?(baud)
59
+
60
+ @baud = baud
61
+ end
62
+
63
+ # Queues `text`'s bytes to send, its letters in capitals, as a Model
64
+ # 33 types them.
65
+ def type(text)
66
+ text.upcase.bytes.each { |byte| @queue << byte }
67
+ end
68
+
69
+ # Whether characters are queued or on their way in or out.
70
+ def busy? = !@queue.empty? || @sent < FRAME_BITS || @receiving
71
+
72
+ # Whether the line to the board is high, its idle level.
73
+ def line_high? = @line_high
74
+
75
+ # Drops what was on its way in and sends RUBOUT once the monitor
76
+ # is ready for it, at cycle `now`.
77
+ def reset!(now)
78
+ @queue.clear
79
+ @sent = FRAME_BITS
80
+ drive(true)
81
+ @queue << RUBOUT
82
+ @quiet_until = now + RUBOUT_DELAY
83
+ end
84
+
85
+ # Runs cycle `now`, with the printer line `printer_high`.
86
+ def cycle!(now, printer_high)
87
+ send_bits(now)
88
+ receive(now, printer_high)
89
+ end
90
+
91
+ # The teletype's fields in a keyed snapshot module (BL-TTY): the
92
+ # baud, what is queued, the character going out and the one coming
93
+ # in, by the cycles they started at, and all it has printed.
94
+ def save_fields(out)
95
+ out.int("baud", @baud).ints("queue", @queue).bytes("output", @output)
96
+ out.bool("line_high", @line_high).int("send.byte", @sending).int("send.bits", @sent)
97
+ out.int("send.start", @send_start).int("quiet_until", @quiet_until)
98
+ out.bool("receive.busy", @receiving).int("receive.byte", @received).int("receive.bits", @bits)
99
+ out.int("receive.edge", @edge).int("receive.sample_at", @sample_at).bool("printer_high", @last_printer)
100
+ end
101
+
102
+ def load_fields(input)
103
+ baud = input.int("baud", @baud)
104
+ @baud = BAUDS.include?(baud) ? baud : @baud
105
+ @queue = input.ints("queue", []).map { |byte| byte & 0xff }
106
+ @output = input.bytes("output", "".b).dup
107
+ @line_high = input.bool("line_high", true)
108
+ @sending = input.int("send.byte", 0) & 0xff
109
+ @sent = input.int("send.bits", FRAME_BITS).clamp(0, FRAME_BITS)
110
+ @send_start = input.int("send.start", 0)
111
+ @quiet_until = input.int("quiet_until", 0)
112
+ @receiving = input.bool("receive.busy", false)
113
+ @received = input.int("receive.byte", 0) & 0xff
114
+ @bits = input.int("receive.bits", 0).clamp(-1, 8)
115
+ @edge = input.int("receive.edge", 0)
116
+ @sample_at = input.int("receive.sample_at", 0)
117
+ @last_printer = input.bool("printer_high", true)
118
+ @panel.tty_in_high = @line_high
119
+ end
120
+
121
+ private
122
+
123
+ # The cycles `halves` half bits take, rounded up.
124
+ def half_bits(halves) = ((halves * 500_000) + @baud - 1) / @baud
125
+
126
+ # Each bit goes out once its time from the start bit has passed.
127
+ def send_bits(now)
128
+ if @sent >= FRAME_BITS
129
+ start_character(now) if !@queue.empty? && !@receiving && now >= @quiet_until
130
+ elsif now >= @send_start + half_bits(2 * @sent)
131
+ drive(bit_high?(@sent))
132
+ @sent += 1
133
+ @quiet_until = now + half_bits(2 * GAP_CHARACTERS * FRAME_BITS) if @sent == FRAME_BITS
134
+ end
135
+ end
136
+
137
+ # The character's eleven bits, the start bit going out now.
138
+ def start_character(now)
139
+ @sending = @queue.shift
140
+ @sent = 0
141
+ @send_start = now
142
+ send_bits(now)
143
+ end
144
+
145
+ # The start bit low, the data bits from the lowest, the stop bits
146
+ # high.
147
+ def bit_high?(index)
148
+ return false if index.zero?
149
+ return true if index > 8
150
+
151
+ @sending.anybits?(1 << (index - 1))
152
+ end
153
+
154
+ def drive(high)
155
+ return if high == @line_high
156
+
157
+ @line_high = high
158
+ @panel.tty_in_high = high
159
+ end
160
+
161
+ def receive(now, printer_high)
162
+ if @receiving
163
+ sample(now, printer_high) if now >= @sample_at
164
+ elsif @last_printer && !printer_high
165
+ @receiving = true
166
+ @received = 0
167
+ @bits = -1
168
+ @edge = now
169
+ @sample_at = now + half_bits(1)
170
+ end
171
+ @last_printer = printer_high
172
+ end
173
+
174
+ # The middle of the start bit, which has to be low still, each data
175
+ # bit's, then the stop bit's.
176
+ def sample(now, printer_high)
177
+ if @bits.negative?
178
+ @receiving = !printer_high
179
+ @bits = 0
180
+ elsif @bits < 8
181
+ @received |= 1 << @bits if printer_high
182
+ @bits += 1
183
+ else
184
+ @receiving = false
185
+ @output << (@received & 0x7f).chr if printer_high
186
+ @quiet_until = [@quiet_until, now + half_bits(2 * GAP_CHARACTERS * FRAME_BITS)].max
187
+ return
188
+ end
189
+ @sample_at = @edge + half_bits((2 * @bits) + 3)
190
+ end
191
+ end
192
+ end
193
+ end