badline 0.13.0 → 0.14.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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/CLAUDE.md +27 -7
  4. data/CONTRIBUTING.md +4 -3
  5. data/README.md +8 -5
  6. data/doc/media.md +2 -1
  7. data/doc/pinned-behaviour.md +29 -0
  8. data/doc/snapshot-format.md +1047 -0
  9. data/doc/snapshots.md +72 -39
  10. data/lib/badline/audio/bare_player.rb +50 -17
  11. data/lib/badline/audio/machine_player.rb +15 -9
  12. data/lib/badline/audio/renderer.rb +32 -21
  13. data/lib/badline/c128/banks.rb +6 -3
  14. data/lib/badline/c128/bus/keyed_state.rb +45 -0
  15. data/lib/badline/c128/bus.rb +17 -3
  16. data/lib/badline/c128/drives.rb +6 -25
  17. data/lib/badline/c128/keyed_modules.rb +117 -0
  18. data/lib/badline/c128/keyed_state.rb +91 -0
  19. data/lib/badline/c128/keys.rb +9 -0
  20. data/lib/badline/c128/mmu.rb +9 -8
  21. data/lib/badline/c128/mmu_pages.rb +20 -0
  22. data/lib/badline/c128/modes.rb +19 -9
  23. data/lib/badline/c128/z80_turns.rb +1 -1
  24. data/lib/badline/c128.rb +8 -8
  25. data/lib/badline/c64/attachments.rb +0 -44
  26. data/lib/badline/c64/banked_ram/plus256k.rb +18 -10
  27. data/lib/badline/c64/banked_ram/plus60k.rb +8 -6
  28. data/lib/badline/c64/banked_ram/unexpanded.rb +2 -2
  29. data/lib/badline/c64/bus/debug_register.rb +32 -0
  30. data/lib/badline/c64/bus/keyed_state.rb +41 -0
  31. data/lib/badline/c64/bus.rb +2 -1
  32. data/lib/badline/c64/cartridge/action_replay.rb +0 -13
  33. data/lib/badline/c64/cartridge/easy_flash.rb +0 -15
  34. data/lib/badline/c64/cartridge/epyx_fastload.rb +0 -8
  35. data/lib/badline/c64/cartridge/final_cartridge3.rb +0 -12
  36. data/lib/badline/c64/cartridge/flash.rb +3 -3
  37. data/lib/badline/c64/cartridge/g_mod2.rb +0 -12
  38. data/lib/badline/c64/cartridge/geo_ram.rb +0 -16
  39. data/lib/badline/c64/cartridge/kcs_power.rb +0 -8
  40. data/lib/badline/c64/cartridge/keyed_state.rb +393 -0
  41. data/lib/badline/c64/cartridge/pagefox.rb +0 -10
  42. data/lib/badline/c64/cartridge/retro_replay.rb +0 -8
  43. data/lib/badline/c64/cartridge/rgcd.rb +0 -10
  44. data/lib/badline/c64/cartridge/super_games.rb +0 -10
  45. data/lib/badline/c64/cartridge/zaxxon.rb +0 -16
  46. data/lib/badline/c64/cartridge.rb +1 -27
  47. data/lib/badline/c64/kernal_traps.rb +28 -25
  48. data/lib/badline/c64/keyed_modules.rb +111 -0
  49. data/lib/badline/c64/keyed_state.rb +68 -0
  50. data/lib/badline/c64/reu/dma.rb +0 -4
  51. data/lib/badline/c64/reu/keyed_state.rb +140 -0
  52. data/lib/badline/c64/reu/ram.rb +0 -15
  53. data/lib/badline/c64/reu.rb +1 -3
  54. data/lib/badline/c64/true_drives.rb +56 -5
  55. data/lib/badline/c64.rb +7 -3
  56. data/lib/badline/chips/cia/interrupt_register.rb +0 -17
  57. data/lib/badline/chips/cia/keyed_parts.rb +213 -0
  58. data/lib/badline/chips/cia/serial.rb +0 -4
  59. data/lib/badline/chips/cia/time_of_day.rb +0 -22
  60. data/lib/badline/chips/cia/timer.rb +0 -20
  61. data/lib/badline/chips/cia.rb +2 -2
  62. data/lib/badline/chips/cpu/core.rb +18 -2
  63. data/lib/badline/chips/cpu/keyed_state.rb +177 -0
  64. data/lib/badline/chips/cpu/microcode.rb +16 -0
  65. data/lib/badline/chips/cpu/port.rb +20 -3
  66. data/lib/badline/chips/cpu.rb +5 -0
  67. data/lib/badline/chips/sid/decimator.rb +10 -8
  68. data/lib/badline/chips/sid/envelope.rb +1 -6
  69. data/lib/badline/chips/sid/filter.rb +0 -14
  70. data/lib/badline/chips/sid/keyed_parts.rb +286 -0
  71. data/lib/badline/chips/sid/voice.rb +0 -10
  72. data/lib/badline/chips/sid/waveform.rb +0 -2
  73. data/lib/badline/chips/sid.rb +1 -2
  74. data/lib/badline/chips/ted/beam.rb +119 -0
  75. data/lib/badline/chips/ted/timers.rb +66 -0
  76. data/lib/badline/chips/ted.rb +143 -0
  77. data/lib/badline/chips/tia.rb +49 -0
  78. data/lib/badline/chips/vdc/keyed_parts.rb +105 -0
  79. data/lib/badline/chips/vdc/painter.rb +0 -12
  80. data/lib/badline/chips/vdc/raster.rb +0 -16
  81. data/lib/badline/chips/vdc/window.rb +0 -9
  82. data/lib/badline/chips/vdc.rb +1 -2
  83. data/lib/badline/chips/via/control_lines.rb +0 -15
  84. data/lib/badline/chips/via/interrupt_register.rb +0 -9
  85. data/lib/badline/chips/via/keyed_parts.rb +163 -0
  86. data/lib/badline/chips/via/shift_register.rb +0 -14
  87. data/lib/badline/chips/via/timer1.rb +0 -13
  88. data/lib/badline/chips/via/timer2.rb +0 -18
  89. data/lib/badline/chips/via.rb +1 -2
  90. data/lib/badline/chips/vic/border_mask.rb +0 -10
  91. data/lib/badline/chips/vic/collisions.rb +0 -11
  92. data/lib/badline/chips/vic/color_patches.rb +0 -11
  93. data/lib/badline/chips/vic/display_state.rb +0 -19
  94. data/lib/badline/chips/vic/graphics_shifter.rb +0 -4
  95. data/lib/badline/chips/vic/keyed_parts.rb +228 -0
  96. data/lib/badline/chips/vic/keyed_sprites.rb +225 -0
  97. data/lib/badline/chips/vic/keyed_state.rb +102 -0
  98. data/lib/badline/chips/vic/register_log.rb +0 -18
  99. data/lib/badline/chips/vic/registers.rb +0 -9
  100. data/lib/badline/chips/vic/sequencer.rb +0 -2
  101. data/lib/badline/chips/vic/sprite/internal_bus.rb +0 -11
  102. data/lib/badline/chips/vic/sprite.rb +0 -2
  103. data/lib/badline/chips/vic/sprites.rb +0 -3
  104. data/lib/badline/chips/vic.rb +6 -2
  105. data/lib/badline/chips/vic_i/keyed_parts.rb +142 -0
  106. data/lib/badline/chips/vic_i/painter.rb +0 -4
  107. data/lib/badline/chips/vic_i/sound.rb +38 -46
  108. data/lib/badline/chips/vic_i.rb +2 -2
  109. data/lib/badline/chips/z80/core.rb +2 -2
  110. data/lib/badline/chips/z80/keyed_state.rb +73 -0
  111. data/lib/badline/chips.rb +10 -0
  112. data/lib/badline/core.rb +4 -21
  113. data/lib/badline/drives/datasette/keyed_state.rb +38 -0
  114. data/lib/badline/drives/datasette.rb +40 -7
  115. data/lib/badline/drives/engine/core.rb +3 -0
  116. data/lib/badline/drives/engine/serial_lines.rb +4 -7
  117. data/lib/badline/drives/model1541/bus.rb +0 -13
  118. data/lib/badline/drives/model1541/disk.rb +9 -8
  119. data/lib/badline/drives/model1541/keyed_state.rb +251 -0
  120. data/lib/badline/drives/model1541/mechanism.rb +0 -2
  121. data/lib/badline/drives/model1541.rb +1 -2
  122. data/lib/badline/drives/model1571/bus.rb +0 -13
  123. data/lib/badline/drives/model1571/keyed_state.rb +108 -0
  124. data/lib/badline/drives/model1571/wd1770.rb +0 -14
  125. data/lib/badline/drives/model1571.rb +1 -2
  126. data/lib/badline/drives/model1581/bus.rb +0 -13
  127. data/lib/badline/drives/model1581/disk.rb +0 -4
  128. data/lib/badline/drives/model1581/keyed_state.rb +280 -0
  129. data/lib/badline/drives/model1581/mechanism.rb +0 -21
  130. data/lib/badline/drives/model1581/slot.rb +0 -18
  131. data/lib/badline/drives/model1581/wd1772.rb +0 -2
  132. data/lib/badline/drives/model1581.rb +2 -2
  133. data/lib/badline/drives.rb +8 -0
  134. data/lib/badline/frontend/app.rb +3 -3
  135. data/lib/badline/frontend/boot.rb +1 -1
  136. data/lib/badline/frontend/controls.rb +19 -3
  137. data/lib/badline/frontend/drive_led.rb +44 -21
  138. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -2
  139. data/lib/badline/frontend/snapshots.rb +12 -1
  140. data/lib/badline/frontend/timeline.rb +28 -10
  141. data/lib/badline/input/matrix_typist.rb +98 -0
  142. data/lib/badline/input.rb +1 -0
  143. data/lib/badline/kernal_trap/chrout.rb +45 -0
  144. data/lib/badline/kernal_trap/dos/channel.rb +0 -27
  145. data/lib/badline/kernal_trap/dos/channels.rb +0 -25
  146. data/lib/badline/kernal_trap/dos/memory.rb +0 -9
  147. data/lib/badline/kernal_trap/dos/status.rb +0 -8
  148. data/lib/badline/kernal_trap/dos/write_file.rb +0 -18
  149. data/lib/badline/kernal_trap/dos.rb +0 -19
  150. data/lib/badline/kernal_trap/file_routine.rb +4 -4
  151. data/lib/badline/kernal_trap/keyed_state.rb +150 -0
  152. data/lib/badline/kernal_trap/layout.rb +75 -11
  153. data/lib/badline/kernal_trap/load.rb +14 -16
  154. data/lib/badline/kernal_trap/routine.rb +12 -0
  155. data/lib/badline/kernal_trap/save.rb +8 -18
  156. data/lib/badline/kernal_trap/serial.rb +1 -17
  157. data/lib/badline/kernal_trap.rb +2 -0
  158. data/lib/badline/machine/keyboard_buffer.rb +49 -0
  159. data/lib/badline/machine/settling.rb +29 -0
  160. data/lib/badline/machine.rb +5 -2
  161. data/lib/badline/media/boot_disk.rb +14 -2
  162. data/lib/badline/media/extensions.rb +10 -0
  163. data/lib/badline/memory.rb +3 -6
  164. data/lib/badline/options/family.rb +5 -2
  165. data/lib/badline/options/help.rb +42 -16
  166. data/lib/badline/options/table.rb +2 -0
  167. data/lib/badline/options/validation.rb +2 -0
  168. data/lib/badline/options.rb +6 -1
  169. data/lib/badline/plus4/acia.rb +45 -0
  170. data/lib/badline/plus4/bus.rb +312 -0
  171. data/lib/badline/plus4/cpu.rb +10 -0
  172. data/lib/badline/plus4/cpu_port.rb +93 -0
  173. data/lib/badline/plus4/keyboard_port.rb +51 -0
  174. data/lib/badline/plus4/model.rb +38 -0
  175. data/lib/badline/plus4/user_port.rb +31 -0
  176. data/lib/badline/plus4.rb +164 -0
  177. data/lib/badline/roms/README +38 -0
  178. data/lib/badline/roms/plus4/basic.rom +0 -0
  179. data/lib/badline/roms/plus4/function-high.rom +0 -0
  180. data/lib/badline/roms/plus4/function-low.rom +0 -0
  181. data/lib/badline/roms/plus4/kernal-ntsc.rom +0 -0
  182. data/lib/badline/roms/plus4/kernal-pal.rom +0 -0
  183. data/lib/badline/snapshot/c128_setup.rb +9 -20
  184. data/lib/badline/snapshot/c64_setup.rb +28 -24
  185. data/lib/badline/snapshot/container.rb +4 -0
  186. data/lib/badline/snapshot/field_doc/markdown.rb +112 -0
  187. data/lib/badline/snapshot/field_doc.rb +109 -0
  188. data/lib/badline/snapshot/image.rb +134 -28
  189. data/lib/badline/snapshot/keyed/cursor.rb +40 -0
  190. data/lib/badline/snapshot/keyed/modules.rb +66 -0
  191. data/lib/badline/snapshot/keyed/reader.rb +188 -0
  192. data/lib/badline/snapshot/keyed/recorder.rb +88 -0
  193. data/lib/badline/snapshot/keyed/report.rb +26 -0
  194. data/lib/badline/snapshot/keyed/writer.rb +55 -0
  195. data/lib/badline/snapshot/keyed.rb +64 -0
  196. data/lib/badline/snapshot/machine_state.rb +17 -64
  197. data/lib/badline/snapshot/storage_setup.rb +29 -34
  198. data/lib/badline/snapshot/vic20_setup.rb +6 -15
  199. data/lib/badline/snapshot/vice/reu1764.rb +2 -2
  200. data/lib/badline/snapshot/vice.rb +7 -31
  201. data/lib/badline/snapshot.rb +43 -20
  202. data/lib/badline/storage/crt_file.rb +3 -1
  203. data/lib/badline/storage/g64_image.rb +8 -3
  204. data/lib/badline/storage/tap.rb +43 -15
  205. data/lib/badline/version.rb +1 -1
  206. data/lib/badline/vic20/attachments.rb +3 -0
  207. data/lib/badline/vic20/bus/keyed_state.rb +52 -0
  208. data/lib/badline/vic20/bus.rb +1 -2
  209. data/lib/badline/vic20/keyed_modules.rb +73 -0
  210. data/lib/badline/vic20/keyed_state.rb +45 -0
  211. data/lib/badline/vic20.rb +5 -5
  212. data/lib/badline.rb +1 -0
  213. metadata +61 -42
  214. data/lib/badline/c128/bus/saved_state.rb +0 -53
  215. data/lib/badline/c128/saved_state.rb +0 -117
  216. data/lib/badline/c64/bus/saved_state.rb +0 -68
  217. data/lib/badline/c64/cartridge/flash/saved_state.rb +0 -40
  218. data/lib/badline/c64/cartridge/retro_replay/saved_state.rb +0 -47
  219. data/lib/badline/c64/cartridge/saved_state.rb +0 -63
  220. data/lib/badline/c64/reu/dma/saved_state.rb +0 -52
  221. data/lib/badline/c64/reu/saved_state.rb +0 -55
  222. data/lib/badline/c64/saved_state.rb +0 -112
  223. data/lib/badline/chips/cia/saved_state.rb +0 -49
  224. data/lib/badline/chips/cia/serial/saved_state.rb +0 -45
  225. data/lib/badline/chips/cpu/saved_state.rb +0 -117
  226. data/lib/badline/chips/sid/envelope/saved_state.rb +0 -70
  227. data/lib/badline/chips/sid/filter/saved_state.rb +0 -42
  228. data/lib/badline/chips/sid/saved_state.rb +0 -52
  229. data/lib/badline/chips/sid/waveform/saved_state.rb +0 -68
  230. data/lib/badline/chips/vdc/saved_state.rb +0 -36
  231. data/lib/badline/chips/via/saved_state.rb +0 -44
  232. data/lib/badline/chips/vic/graphics_shifter/saved_state.rb +0 -40
  233. data/lib/badline/chips/vic/saved_state.rb +0 -82
  234. data/lib/badline/chips/vic/sequencer/saved_state.rb +0 -91
  235. data/lib/badline/chips/vic/sprite/saved_state.rb +0 -66
  236. data/lib/badline/chips/vic/sprites/saved_state.rb +0 -59
  237. data/lib/badline/chips/vic_i/painter/saved_state.rb +0 -40
  238. data/lib/badline/chips/vic_i/saved_state.rb +0 -55
  239. data/lib/badline/chips/z80/saved_state.rb +0 -71
  240. data/lib/badline/chrout_trap.rb +0 -43
  241. data/lib/badline/debug_register.rb +0 -28
  242. data/lib/badline/drives/datasette/saved_state.rb +0 -37
  243. data/lib/badline/drives/model1541/disk/saved_state.rb +0 -88
  244. data/lib/badline/drives/model1541/mechanism/saved_state.rb +0 -72
  245. data/lib/badline/drives/model1541/saved_state.rb +0 -44
  246. data/lib/badline/drives/model1571/saved_state.rb +0 -47
  247. data/lib/badline/drives/model1581/disk/saved_state.rb +0 -89
  248. data/lib/badline/drives/model1581/saved_state.rb +0 -42
  249. data/lib/badline/drives/model1581/wd1772/saved_state.rb +0 -68
  250. data/lib/badline/keyboard_buffer.rb +0 -33
  251. data/lib/badline/snapshot/setup.rb +0 -31
  252. data/lib/badline/snapshot/state.rb +0 -179
  253. data/lib/badline/vic20/bus/saved_state.rb +0 -35
  254. data/lib/badline/vic20/saved_state.rb +0 -110
@@ -14,7 +14,7 @@ module Badline
14
14
  # first, plays .sid tunes and directories of them in the terminal.
15
15
  # `c64`, `vic20` or `c128`, given first, names the machine family, and
16
16
  # without one the family is the C64, or the C128 for a program that loads
17
- # where C128 BASIC starts. The native build adds --no-sound and
17
+ # where C128 BASIC starts. `plus4` is refused until the Plus/4 runs. The native build adds --no-sound and
18
18
  # --version, and badline-ruby adds --disable-jit.
19
19
  #
20
20
  # TABLE lists the options, and both the parser and the help read it. It
@@ -94,6 +94,7 @@ module Badline
94
94
  @true_drive = false
95
95
  @screenshot = ""
96
96
  @save_snapshot = ""
97
+ @vice = false
97
98
  @timeline = []
98
99
  @jit = true
99
100
  @help = false
@@ -171,6 +172,9 @@ module Badline
171
172
 
172
173
  def true_drive? = @true_drive
173
174
 
175
+ # Whether --save-snapshot adds VICE's modules, so x64sc opens it.
176
+ def vice? = @vice
177
+
174
178
  def jit? = @jit
175
179
 
176
180
  # The name of the model to build: a C64 Badline::Model names, a VIC-20
@@ -272,6 +276,7 @@ module Badline
272
276
  when "--no-vsync" then @vsync = false
273
277
  when "--verbose" then @verbose = true
274
278
  when "--true-drive" then @true_drive = true
279
+ when "--vice" then @vice = true
275
280
  when "--ntsc" then @models << "ntsc"
276
281
  when "--c64" then @c64_mode = true
277
282
  when "--unpaced" then @paced = false
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The Plus/4's 6551 ACIA at $FD00-$FD0F, its four registers repeated,
6
+ # as a stub with nothing on the line: it never receives, its
7
+ # transmitter is always empty, and it never interrupts. The command and
8
+ # control registers keep what is written to them, and a write to the
9
+ # status register, a programmed reset, clears the command register's
10
+ # low five bits.
11
+ class ACIA
12
+ STATUS = 0x10
13
+
14
+ attr_reader :command, :control
15
+
16
+ def initialize
17
+ reset!
18
+ end
19
+
20
+ def reset!
21
+ @command = 0
22
+ @control = 0
23
+ end
24
+
25
+ def irq? = false
26
+
27
+ def peek(addr)
28
+ case addr & 0x03
29
+ when 1 then STATUS
30
+ when 2 then @command
31
+ when 3 then @control
32
+ else 0
33
+ end
34
+ end
35
+
36
+ def poke(addr, value)
37
+ case addr & 0x03
38
+ when 1 then @command &= 0xe0
39
+ when 2 then @command = value
40
+ when 3 then @control = value
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The CPU's bus:
6
+ #
7
+ # $0000-$0001 - the 7501's port (CPUPort), over RAM
8
+ # $0002-$7FFF - RAM
9
+ # $8000-$BFFF - the low ROM bank, or RAM
10
+ # $C000-$FCFF - the high ROM bank, or RAM; $FC00-$FCFF is always the
11
+ # KERNAL's while ROM is selected
12
+ # $FD00-$FD0F - the 6551 ACIA (Plus/4 only)
13
+ # $FD10-$FD1F - the user port's 6529 (Plus/4 only)
14
+ # $FD30-$FD3F - the keyboard's 6529 (KeyboardPort)
15
+ # $FDD0-$FDDF - the ROM bank latch
16
+ # $FF00-$FF1F - the TED
17
+ # $FF3E/$FF3F - a write selects ROM or RAM at $8000-$FFFF
18
+ # $FF40-$FFFF - the high ROM bank, or RAM
19
+ #
20
+ # A write to $FF3E selects ROM and one to $FF3F RAM, everywhere in
21
+ # $8000-$FFFF but $FD00-$FF3F, which holds I/O either way. ROM is
22
+ # selected at power-on and reset. A write to ROM goes to the RAM under
23
+ # it. The ROM bank latch takes the address written to, not the byte:
24
+ # bits 0-1 pick the low bank and bits 2-3 the high bank, 0 for BASIC
25
+ # and the KERNAL, 1 for the Plus/4's function ROMs, 2 and 3 for the two
26
+ # cartridges.
27
+ #
28
+ # RAM smaller than 64K repeats through the 64K, as the C16's 16K does
29
+ # without A14 and A15. Where nothing answers, a read finds the last byte
30
+ # on the bus.
31
+ class Bus
32
+ include Addressable
33
+
34
+ RAM_POWER_ON = 0x00
35
+
36
+ # RAM through a size it repeats at.
37
+ class MirroredRAM
38
+ def initialize(ram, mask)
39
+ @ram = ram
40
+ @mask = mask
41
+ end
42
+
43
+ def peek(addr) = @ram.peek(addr & @mask)
44
+
45
+ def poke(addr, value)
46
+ @ram.poke(addr & @mask, value)
47
+ end
48
+ end
49
+
50
+ # The last byte the CPU moved, which an empty spot reads back.
51
+ class OpenBus
52
+ def initialize(bus)
53
+ @bus = bus
54
+ end
55
+
56
+ def peek(_addr) = @bus.data
57
+ def poke(_addr, _value); end
58
+ end
59
+
60
+ # Page 0: the port at $00 and $01, RAM above it.
61
+ class ZeroPage
62
+ def initialize(port, ram)
63
+ @port = port
64
+ @ram = ram
65
+ end
66
+
67
+ def peek(addr) = addr < 2 ? @port.peek(addr) : @ram.peek(addr)
68
+
69
+ def poke(addr, value)
70
+ return @port.poke(addr, value) if addr < 2
71
+
72
+ @ram.poke(addr, value)
73
+ end
74
+ end
75
+
76
+ # Page $FD: the ACIA, the user port, the keyboard port and the ROM
77
+ # bank latch, by the address's bits 4-7. +acia+ and +user_port+ are
78
+ # nil where the machine has none.
79
+ class IOPage
80
+ def initialize(keyboard_port:, acia: nil, user_port: nil)
81
+ @bus = nil
82
+ @acia = acia
83
+ @user_port = user_port
84
+ @keyboard_port = keyboard_port
85
+ end
86
+
87
+ # The bus whose last byte an empty spot reads and whose bank latch
88
+ # a write to $FDD0-$FDDF sets.
89
+ def connect(bus)
90
+ @bus = bus
91
+ end
92
+
93
+ def peek(addr)
94
+ case addr & 0xf0
95
+ when 0x00 then @acia ? @acia.peek(addr) : @bus.data
96
+ when 0x10 then @user_port ? @user_port.peek(addr) : @bus.data
97
+ when 0x30 then @keyboard_port.peek(addr)
98
+ else @bus.data
99
+ end
100
+ end
101
+
102
+ def poke(addr, value)
103
+ case addr & 0xf0
104
+ when 0x00 then @acia&.poke(addr, value)
105
+ when 0x10 then @user_port&.poke(addr, value)
106
+ when 0x30 then @keyboard_port.poke(addr, value)
107
+ when 0xd0 then @bus.select_banks(addr)
108
+ end
109
+ end
110
+ end
111
+
112
+ # VICE's debug cartridge register at $FDCF, a spot in the I/O page
113
+ # that nothing answers (plus4world forum post 31417). A write there
114
+ # calls the handler with the byte, the exit code the VICE testprogs
115
+ # report. Every other access falls through to the I/O page.
116
+ class DebugRegister
117
+ ADDRESS = 0xfdcf
118
+
119
+ def initialize(io, &handler)
120
+ @io = io
121
+ @handler = handler
122
+ end
123
+
124
+ def peek(addr) = @io.peek(addr)
125
+
126
+ def poke(addr, value)
127
+ return @handler.call(value) if addr == ADDRESS
128
+
129
+ @io.poke(addr, value)
130
+ end
131
+ end
132
+
133
+ # Page $FE with a 1551's TCBM interface plugged in: the 6523 TIA
134
+ # that talks to drive 9 at $FEC0-$FEDF and drive 8's at $FEE0-$FEFF.
135
+ # The rest of the page is open.
136
+ class TCBMPage
137
+ def initialize(bus, drive8:, drive9:)
138
+ @bus = bus
139
+ @drive8 = drive8
140
+ @drive9 = drive9
141
+ end
142
+
143
+ def peek(addr)
144
+ return @bus.data if addr < 0xfec0
145
+
146
+ addr < 0xfee0 ? @drive9.peek(addr) : @drive8.peek(addr)
147
+ end
148
+
149
+ def poke(addr, value)
150
+ return if addr < 0xfec0
151
+
152
+ addr < 0xfee0 ? @drive9.poke(addr, value) : @drive8.poke(addr, value)
153
+ end
154
+ end
155
+
156
+ # Page $FF: the TED in $FF00-$FF3F, its two phantom registers, and
157
+ # ROM or RAM above.
158
+ class TopPage
159
+ def initialize(bus, ted)
160
+ @bus = bus
161
+ @ted = ted
162
+ end
163
+
164
+ def peek(addr)
165
+ return @ted.peek(addr) if addr < 0xff40
166
+
167
+ @bus.top_memory.peek(addr)
168
+ end
169
+
170
+ def poke(addr, value)
171
+ return @bus.ram_view.poke(addr, value) if addr >= 0xff40
172
+
173
+ case addr
174
+ when 0xff3e then @bus.rom_selected = true
175
+ when 0xff3f then @bus.rom_selected = false
176
+ else @ted.poke(addr, value)
177
+ end
178
+ end
179
+ end
180
+
181
+ attr_reader :ram, :ram_view, :data, :low_bank, :high_bank
182
+
183
+ # What the top page reads above $FF3F: the high ROM bank or RAM.
184
+ attr_reader :top_memory
185
+
186
+ # The CPU sees ROM at $8000-$FFFF.
187
+ attr_reader :rom_selected
188
+
189
+ # +io+ is the IOPage, +model+ a Model profile, whose KERNAL it maps
190
+ # and whose function ROMs it maps where the model has them, and
191
+ # +ram_kb+ the RAM fitted.
192
+ def initialize(ted:, port:, io:, model: Model::PLUS4, ram_kb: model.ram_kb)
193
+ addressable_at(0, length: 2**16)
194
+ size = ram_kb * 1024
195
+ @ram = Memory.new(Array.new(size, RAM_POWER_ON), length: size, start: 0)
196
+ @ram_view = MirroredRAM.new(@ram, size - 1)
197
+ @ted = ted
198
+ @open_bus = OpenBus.new(self)
199
+ @low_roms = [ROM.load("plus4/basic.rom", 0x8000), nil, nil, nil]
200
+ @high_roms = [ROM.load(model.kernal, 0xc000), nil, nil, nil]
201
+ if model.plus4_parts
202
+ @low_roms[1] = ROM.load("plus4/function-low.rom", 0x8000)
203
+ @high_roms[1] = ROM.load("plus4/function-high.rom", 0xc000)
204
+ end
205
+ @zero_page = ZeroPage.new(port, @ram_view)
206
+ @io_page = io
207
+ io.connect(self)
208
+ @top_page = TopPage.new(self, ted)
209
+ @tcbm_page = @open_bus
210
+ @debug_register = nil
211
+ @data = 0
212
+ @read_pages = Array.new(256, @open_bus)
213
+ @write_pages = Array.new(256, @ram_view)
214
+ @low_bank = @high_bank = 0
215
+ @rom_selected = true
216
+ @top_memory = @ram_view
217
+ map_writes
218
+ map_reads
219
+ end
220
+
221
+ def power_on!
222
+ @ram.clear!(Array.new(@ram.length, RAM_POWER_ON))
223
+ reset!
224
+ end
225
+
226
+ # The RESET line selects ROM and the first bank of each.
227
+ def reset!
228
+ @low_bank = @high_bank = 0
229
+ self.rom_selected = true
230
+ end
231
+
232
+ def rom_selected=(selected)
233
+ @rom_selected = selected
234
+ @ted.rom_selected = selected
235
+ map_reads
236
+ end
237
+
238
+ # Whether the CPU reads the KERNAL at $C000-$FFFF: ROM selected, with
239
+ # the KERNAL's bank, high bank 0.
240
+ def kernal_selected? = @rom_selected && @high_bank.zero?
241
+
242
+ # A write to the ROM bank latch at +addr+.
243
+ def select_banks(addr)
244
+ @low_bank = addr & 0x03
245
+ @high_bank = (addr >> 2) & 0x03
246
+ map_reads
247
+ end
248
+
249
+ # Calls the block with each byte written to $FDCF (DebugRegister).
250
+ def install_debug_register(&)
251
+ @debug_register = DebugRegister.new(@io_page, &)
252
+ map_writes
253
+ end
254
+
255
+ # Plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF.
256
+ def plug_tcbm(drive8:, drive9:)
257
+ @tcbm_page = TCBMPage.new(self, drive8:, drive9:)
258
+ map_writes
259
+ map_reads
260
+ end
261
+
262
+ # Puts a cartridge's ROM chips in the slots of cartridge 1: a chip at
263
+ # $8000 in low bank 2 and one at $C000 in high bank 2, each repeated
264
+ # through the bank's 16K.
265
+ def attach_cartridge(chips)
266
+ chips.each do |chip|
267
+ data = chip.data * (0x4000 / chip.data.length)
268
+ if chip.address < 0xc000
269
+ @low_roms[2] = ROM.new(data, length: 0x4000, start: 0x8000)
270
+ else
271
+ @high_roms[2] = ROM.new(data, length: 0x4000, start: 0xc000)
272
+ end
273
+ end
274
+ map_reads
275
+ end
276
+
277
+ def peek(addr)
278
+ @data = @read_pages[addr >> 8].peek(addr)
279
+ end
280
+
281
+ def poke(addr, value)
282
+ @data = value
283
+ @write_pages[addr >> 8].poke(addr, value)
284
+ end
285
+
286
+ private
287
+
288
+ def map_writes
289
+ @write_pages[0x00] = @zero_page
290
+ @write_pages[0xfd] = @debug_register || @io_page
291
+ @write_pages[0xfe] = @tcbm_page
292
+ @write_pages[0xff] = @top_page
293
+ end
294
+
295
+ def map_reads
296
+ @read_pages.fill(@ram_view, 0x00, 0xfd)
297
+ @read_pages[0x00] = @zero_page
298
+ map_roms if @rom_selected
299
+ @top_memory = @rom_selected ? @high_roms[@high_bank] || @open_bus : @ram_view
300
+ @read_pages[0xfd] = @io_page
301
+ @read_pages[0xfe] = @tcbm_page
302
+ @read_pages[0xff] = @top_page
303
+ end
304
+
305
+ def map_roms
306
+ @read_pages.fill(@low_roms[@low_bank] || @open_bus, 0x80, 0x40)
307
+ @read_pages.fill(@high_roms[@high_bank] || @open_bus, 0xc0, 0x3c)
308
+ @read_pages[0xfc] = @high_roms[0]
309
+ end
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The 7501 or 8501, a 6510 core without an NMI pin, on the Plus4::Bus.
6
+ class CPU
7
+ include Badline::Chips::CPU::Core
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The 7501's I/O port at $00 (direction) and $01 (data). Its seven
6
+ # pins carry the serial bus and the cassette port, and none of them
7
+ # banks memory:
8
+ #
9
+ # - P0, P1 and P2 drive the serial bus's DATA, CLK and ATN through
10
+ # inverters, so a 1 pulls the line low, and P1 is the cassette write
11
+ # line too.
12
+ # - P3 runs the cassette motor while it is low.
13
+ # - P4 reads the cassette read line, P6 the serial CLK line and P7 the
14
+ # DATA line, 1 while the line is high.
15
+ # - P5 has no pin and reads 1.
16
+ #
17
+ # An output pin reads back what it drives. A pin left as an input reads
18
+ # what drives it from outside, and the serial outputs, P0-P2, float high
19
+ # there, so they pull their lines, as an output of 1 does.
20
+ class CPUPort
21
+ DATA_OUT = 0x01
22
+ CLK_OUT = 0x02
23
+ ATN_OUT = 0x04
24
+ MOTOR = 0x08
25
+ TAPE_READ = 0x10
26
+ CLK_IN = 0x40
27
+ DATA_IN = 0x80
28
+
29
+ attr_reader :ddr, :data
30
+
31
+ def initialize(serial_bus:, datasette:)
32
+ @serial_bus = serial_bus
33
+ @datasette = datasette
34
+ @ddr = 0
35
+ @data = 0
36
+ end
37
+
38
+ def reset!
39
+ @ddr = 0
40
+ @data = 0
41
+ changed
42
+ end
43
+
44
+ def peek(addr)
45
+ return @ddr if addr.zero?
46
+
47
+ (@data & @ddr) | (inputs & ~@ddr & 0xff)
48
+ end
49
+
50
+ def poke(addr, value)
51
+ if addr.zero?
52
+ @ddr = value
53
+ else
54
+ @data = value
55
+ end
56
+ changed
57
+ end
58
+
59
+ # The pins' levels as they leave the chip, inputs floating high.
60
+ def levels = (@data & @ddr) | (~@ddr & 0xff)
61
+
62
+ # The serial bus lines the port pulls low, as Drives::SerialBus host
63
+ # lines.
64
+ def serial_lines
65
+ out = levels
66
+ lines = 0
67
+ lines |= Drives::SerialBus::HOST_ATN_OUT if out.anybits?(ATN_OUT)
68
+ lines |= Drives::SerialBus::HOST_CLK_OUT if out.anybits?(CLK_OUT)
69
+ lines |= Drives::SerialBus::HOST_DATA_OUT if out.anybits?(DATA_OUT)
70
+ lines
71
+ end
72
+
73
+ private
74
+
75
+ def inputs
76
+ value = 0x2f
77
+ value |= TAPE_READ if @datasette.read_high?
78
+ low = @serial_bus.low_lines
79
+ value |= CLK_IN unless low.anybits?(Drives::SerialBus::CLK)
80
+ value |= DATA_IN unless low.anybits?(Drives::SerialBus::DATA)
81
+ value
82
+ end
83
+
84
+ def changed
85
+ lines = serial_lines
86
+ @serial_bus.host_lines = lines unless lines == @serial_bus.host_lines
87
+ out = levels
88
+ @datasette.motor = out.nobits?(MOTOR)
89
+ @datasette.write_line = out.anybits?(CLK_OUT)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The keyboard: the 6529 at $FD30-$FD3F, whose eight outputs drive the
6
+ # key matrix's lines, and the TED's K0-K7 inputs, which $FF08 latches
7
+ # (Chips::TED#keyboard_port=). The KERNAL's scan ($DB11) writes each
8
+ # line's mask to $FD30 and $FF08 and reads the keys back from $FF08,
9
+ # and its key table at $E026 puts each key at line times 8 plus input
10
+ # bit.
11
+ #
12
+ # The 6529 is a quasi-bidirectional latch: a read returns what was
13
+ # written to it.
14
+ class KeyboardPort
15
+ # One row of keys per 6529 output, in K0-K7 order, after the
16
+ # KERNAL's key table. HELP is the KERNAL's F7, and the two SHIFT keys
17
+ # share one place.
18
+ MATRIX = [
19
+ %i[delete return £ help f1 f2 f3 @],
20
+ %i[3 w a 4 z s e lshift],
21
+ %i[5 r d 6 c f t x],
22
+ %i[7 y g 8 b h u v],
23
+ %i[9 i j 0 m k o n],
24
+ %i[crsr_down p l crsr_up . : - ,],
25
+ %i[crsr_left * ; crsr_right esc = + /],
26
+ %i[1 clr_home control 2 space cbm q run_stop]
27
+ ].freeze
28
+
29
+ attr_reader :keyboard, :output
30
+
31
+ def initialize(keyboard)
32
+ @keyboard = keyboard
33
+ @output = 0xff
34
+ end
35
+
36
+ def reset!
37
+ @output = 0xff
38
+ end
39
+
40
+ def peek(_addr) = @output
41
+
42
+ def poke(_addr, value)
43
+ @output = value
44
+ end
45
+
46
+ # The K0-K7 inputs the TED latches on a write of +_value+ to $FF08:
47
+ # high but where a held key joins a line the 6529 pulls low.
48
+ def latch(_value) = @keyboard.scan(@output, 0xff).last
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The 264 series machines badline builds by name, after xplus4's
6
+ # -model names, with the C116 as another name for the C16: one board
7
+ # design with its RAM, its video standard and KERNAL, and whether the
8
+ # Plus/4's own parts are fitted (the 3-plus-1 function ROMs, the 6551
9
+ # ACIA and the user port's 6529).
10
+ module Model
11
+ Profile = Data.define(:name, :ram_kb, :ted, :kernal, :plus4_parts)
12
+
13
+ PLUS4 = Profile.new(name: "plus4", ram_kb: 64, ted: Chips::TED::PAL, kernal: "plus4/kernal-pal.rom",
14
+ plus4_parts: true)
15
+ PLUS4_NTSC = Profile.new(name: "plus4ntsc", ram_kb: 64, ted: Chips::TED::NTSC,
16
+ kernal: "plus4/kernal-ntsc.rom", plus4_parts: true)
17
+ C16 = Profile.new(name: "c16", ram_kb: 16, ted: Chips::TED::PAL, kernal: "plus4/kernal-pal.rom",
18
+ plus4_parts: false)
19
+ C16_NTSC = Profile.new(name: "c16ntsc", ram_kb: 16, ted: Chips::TED::NTSC, kernal: "plus4/kernal-ntsc.rom",
20
+ plus4_parts: false)
21
+
22
+ ALL = [PLUS4, PLUS4_NTSC, C16, C16_NTSC].freeze
23
+
24
+ # Other names a model goes by.
25
+ ALIASES = { "c116" => "c16" }.freeze
26
+
27
+ # The RAM sizes a machine can have, in K.
28
+ RAM_SIZES = [16, 32, 64].freeze
29
+
30
+ def self.named(name)
31
+ model = ALL.find { |candidate| candidate.name == ALIASES.fetch(name, name) }
32
+ raise ArgumentError, "no Plus/4 model named #{name}" if model.nil?
33
+
34
+ model
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The Plus/4's user port 6529 at $FD10-$FD1F, a quasi-bidirectional
6
+ # latch: a line reads low where the latch or the outside pulls it. With
7
+ # nothing plugged into the user port, the outside pulls only bit 2, the
8
+ # cassette sense switch, low while a key is down on the deck. The
9
+ # KERNAL's IRQ reads it there ($CFBF).
10
+ class UserPort
11
+ TAPE_SENSE = 0x04
12
+
13
+ attr_reader :output
14
+
15
+ def initialize(datasette)
16
+ @datasette = datasette
17
+ @output = 0xff
18
+ end
19
+
20
+ def reset!
21
+ @output = 0xff
22
+ end
23
+
24
+ def peek(_addr) = @datasette.sense_low? ? @output & ~TAPE_SENSE : @output
25
+
26
+ def poke(_addr, value)
27
+ @output = value
28
+ end
29
+ end
30
+ end
31
+ end