badline 0.17.0 → 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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class SuperPET < PET
6
+ # The Waterloo board, after Levak's PET-Interfaces.txt, André
7
+ # Fachat's SuperPET page and Commodore's 324037 schematic: 64K of
8
+ # RAM in 16 banks of 4K, one at a time at $9000-$9FFF, and the I/O
9
+ # page at $EF00 (IOPage), which both CPUs reach.
10
+ #
11
+ # The bank latch at $EFFC-$EFFD picks the bank with bits 0-3, and its
12
+ # bit 7 lets writes reach the system latch at $EFF8-$EFFB:
13
+ #
14
+ # bit 0 the CPU while SW2 is at PROG: 1 the 6502, 0 the 6809
15
+ # bit 1 the bank RAM while SW1 is at PROG: 1 read/write, 0 read only
16
+ # bit 3 pulls the user port's diagnostic sense, PIA 1's PA7, low
17
+ #
18
+ # Bit 0 of $EFFE-$EFFF puts the board's own ROM socket at $9000 in
19
+ # place of the RAM, which badline leaves empty, while the 6502 runs.
20
+ # All three are write-only, and the RES line clears them, so the
21
+ # board starts at bank 0, and with SW2 at PROG with the 6809.
22
+ #
23
+ # SW1 is READ, R/W or PROG, the bank RAM's write protect, and SW2
24
+ # PROG, 6502 or 6809, the CPU. A write to the bank RAM while it is
25
+ # protected goes nowhere.
26
+ #
27
+ # While the 6809 runs, the 8032's ROMs step aside through its NO ROM
28
+ # line, and the Waterloo ROMs answer at $A000-$E7FF and $F000-$FFFF,
29
+ # the $E000 one with the CRTC table for the frame rate.
30
+ class Board
31
+ CPU_SWITCH = %w[6502 6809 prog].freeze
32
+
33
+ RAM_SWITCH = %w[rw read prog].freeze
34
+
35
+ # $EF00-$EFFF: the 6702 at $EFE0-$EFE3, the ACIA at $EFF0-$EFF3
36
+ # and the latches above. A read of a latch finds the last byte on
37
+ # the bus, and below $EFF0, where neither chip answers, $FF.
38
+ class IOPage
39
+ def initialize(board, bus, dongle, acia)
40
+ @board = board
41
+ @bus = bus
42
+ @dongle = dongle
43
+ @acia = acia
44
+ end
45
+
46
+ def peek(addr)
47
+ low = addr & 0xff
48
+ return @bus.data if low >= 0xf4
49
+ return @acia.peek(addr) if low >= 0xf0
50
+ return @dongle.peek(addr) if low >= 0xe0 && low < 0xe4
51
+
52
+ 0xff
53
+ end
54
+
55
+ def poke(addr, value)
56
+ low = addr & 0xff
57
+ if low >= 0xfe
58
+ @board.socket = value
59
+ elsif low >= 0xfc
60
+ @board.bank_latch = value
61
+ elsif low >= 0xf8
62
+ @board.system_latch = value
63
+ elsif low >= 0xf0
64
+ @acia.poke(addr, value) if low < 0xf4
65
+ elsif low >= 0xe0
66
+ @dongle.poke(addr, value) if low < 0xe4
67
+ end
68
+ end
69
+ end
70
+
71
+ attr_reader :cells, :bank_latch, :system_latch, :cpu_switch, :ram_switch
72
+
73
+ # Bit 0 of $EFFE: 1 for the ROM socket at $9000.
74
+ attr_reader :socket
75
+
76
+ # +bus+ is the PET's, whose pages the board maps, and
77
+ # +keyboard_ports+ PIA 1's, whose PA7 the diagnostic sense is.
78
+ # +cpu_switch+ and +ram_switch+ are SW2's and SW1's positions, one
79
+ # of CPU_SWITCH's and RAM_SWITCH's, and +refresh_hz+ picks the
80
+ # Waterloo $E000 ROM, 50 or 60.
81
+ def initialize(bus, dongle:, acia:, keyboard_ports:, cpu_switch: "6502", ram_switch: "rw",
82
+ refresh_hz: 50)
83
+ raise ArgumentError, "the CPU switch has no position #{cpu_switch}" unless CPU_SWITCH.include?(cpu_switch)
84
+ raise ArgumentError, "the RAM switch has no position #{ram_switch}" unless RAM_SWITCH.include?(ram_switch)
85
+
86
+ @bus = bus
87
+ @dongle = dongle
88
+ @keyboard_ports = keyboard_ports
89
+ @cpu_switch = cpu_switch
90
+ @ram_switch = ram_switch
91
+ @cells = Array.new(0x10000, 0)
92
+ @banks = Array.new(16) { |bank| Bus::Expansion::Window.new(@cells, bank << 12, 0x9000) }
93
+ @io = IOPage.new(self, bus, dongle, acia)
94
+ @bank_latch = 0
95
+ @system_latch = 0
96
+ @socket = 0
97
+ @rom_a = ROM.load("pet/superpet-a000.rom", 0xa000)
98
+ @rom_c = ROM.load("pet/superpet-c000.rom", 0xc000)
99
+ @rom_e = ROM.load(refresh_hz == 50 ? "pet/superpet-e000-50hz.rom" : "pet/superpet-e000.rom", 0xe000)
100
+ @m6809 = m6809_selected?
101
+ @select_handler = nil
102
+ end
103
+
104
+ # Calls the block when the CPU the switch and the system latch pick
105
+ # changes.
106
+ def on_select(&handler)
107
+ @select_handler = handler
108
+ end
109
+
110
+ # Whether the 6809 has the bus.
111
+ def m6809? = @m6809
112
+
113
+ def bank = @bank_latch & 0x0f
114
+
115
+ def bank_latch=(value)
116
+ @bank_latch = value
117
+ @bus.map_pages
118
+ end
119
+
120
+ def system_latch=(value)
121
+ return if @bank_latch.nobits?(0x80)
122
+
123
+ @system_latch = value
124
+ @keyboard_ports.diagnostic_low = value.anybits?(0x08)
125
+ select
126
+ end
127
+
128
+ def socket=(value)
129
+ @socket = @m6809 ? 0 : value & 0x01
130
+ @bus.map_pages
131
+ end
132
+
133
+ # Moves SW2, which picks the CPU at the next RES.
134
+ def cpu_switch=(position)
135
+ raise ArgumentError, "the CPU switch has no position #{position}" unless CPU_SWITCH.include?(position)
136
+
137
+ @cpu_switch = position
138
+ end
139
+
140
+ # Moves SW1, which protects the bank RAM from the next write on.
141
+ def ram_switch=(position)
142
+ raise ArgumentError, "the RAM switch has no position #{position}" unless RAM_SWITCH.include?(position)
143
+
144
+ @ram_switch = position
145
+ @bus.map_pages
146
+ end
147
+
148
+ # Whether writes to the bank RAM go nowhere.
149
+ def write_protected?
150
+ case @ram_switch
151
+ when "read" then true
152
+ when "rw" then false
153
+ else @system_latch.nobits?(0x02)
154
+ end
155
+ end
156
+
157
+ # Whether the CPU switch, or the system latch with it at PROG,
158
+ # picks the 6809.
159
+ def m6809_selected?
160
+ case @cpu_switch
161
+ when "6502" then false
162
+ when "6809" then true
163
+ else @system_latch.nobits?(0x01)
164
+ end
165
+ end
166
+
167
+ # The RES line clears the latches, and the 6702.
168
+ def reset!
169
+ @bank_latch = 0
170
+ @system_latch = 0
171
+ @socket = 0
172
+ @keyboard_ports.diagnostic_low = false
173
+ @dongle.reset!
174
+ select
175
+ end
176
+
177
+ def power_on!
178
+ @cells.fill(0)
179
+ end
180
+
181
+ # Lays the bank, or the empty socket, over $9000-$9FFF, the I/O
182
+ # page over $EF00, and the Waterloo ROMs over the 8032's while the
183
+ # 6809 runs.
184
+ def map(reads, writes)
185
+ if @socket.zero?
186
+ window = @banks[bank]
187
+ window.read_only = write_protected?
188
+ reads.fill(window, 0x90, 0x10)
189
+ writes.fill(window, 0x90, 0x10)
190
+ else
191
+ reads.fill(@bus.open_bus, 0x90, 0x10)
192
+ writes.fill(@bus.open_bus, 0x90, 0x10)
193
+ end
194
+ reads[0xef] = @io
195
+ writes[0xef] = @io
196
+ map_waterloo(reads) if @m6809
197
+ end
198
+
199
+ private
200
+
201
+ # Picks the CPU again, and hands the bus over if that changed it.
202
+ # The 6809 taking over clears the socket bit.
203
+ def select
204
+ m6809 = m6809_selected?
205
+ changed = m6809 != @m6809
206
+ @m6809 = m6809
207
+ @socket = 0 if m6809
208
+ @bus.map_pages
209
+ @select_handler&.call if changed
210
+ end
211
+
212
+ def map_waterloo(reads)
213
+ reads.fill(@rom_a, 0xa0, 0x20)
214
+ reads.fill(@rom_c, 0xc0, 0x20)
215
+ reads.fill(@rom_e, 0xe0, 0x08)
216
+ reads.fill(@rom_e, 0xf0, 0x10)
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class SuperPET < PET
6
+ # The 6702 at $EFE0-$EFE3, the protection chip the Waterloo languages
7
+ # check for. Its behaviour comes from David Roberts' Visual 6702, a
8
+ # switch-level simulation of the decapped chip's netlist, driven a
9
+ # bus cycle at a time.
10
+ #
11
+ # Each data line has a shift register of its own, LENGTHS long, whose
12
+ # first stage the line reads, inverted where INVERTED has the bit
13
+ # set. A latch keeps the last byte written, all ones from the RES
14
+ # line. A write that sets D0 where the last one left it clear shifts
15
+ # every register one stage at the end of the next cycle, whichever
16
+ # cycle that is, so a read in it still finds the old byte. Each
17
+ # register takes in its first stage XORed with the latch's bit,
18
+ # inverted where FEEDBACK has the bit clear, the latch as that cycle
19
+ # leaves it. The RES line clears the registers.
20
+ class Dongle
21
+ LENGTHS = [6, 3, 7, 8, 1, 3, 5, 2].freeze
22
+
23
+ INVERTED = 0xd6
24
+
25
+ FEEDBACK = 0x29
26
+
27
+ # No shift waiting.
28
+ NONE = -1
29
+
30
+ # The registers, bit 0 of each the stage its line reads, the latch,
31
+ # and the cycle at whose end the registers shift, or NONE.
32
+ attr_reader :registers, :latch, :shift_at
33
+
34
+ # +clock+ answers #cycles, the cycle the CPU's access falls in.
35
+ def initialize(clock)
36
+ @clock = clock
37
+ @registers = Array.new(8, 0)
38
+ reset!
39
+ end
40
+
41
+ def reset!
42
+ @registers.fill(0)
43
+ @latch = 0xff
44
+ @shift_at = NONE
45
+ end
46
+
47
+ def peek(_addr)
48
+ now = @clock.cycles
49
+ shift if @shift_at != NONE && @shift_at < now
50
+ value = 0
51
+ 8.times { |bit| value |= (@registers[bit] & 1) << bit }
52
+ value ^ INVERTED
53
+ end
54
+
55
+ def poke(_addr, value)
56
+ now = @clock.cycles
57
+ shift if @shift_at != NONE && @shift_at < now
58
+ raising = value.anybits?(0x01) && @latch.nobits?(0x01)
59
+ @latch = value
60
+ if @shift_at == now
61
+ shift
62
+ elsif raising
63
+ @shift_at = now + 1
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def shift
70
+ @shift_at = NONE
71
+ incoming = @latch ^ FEEDBACK ^ 0xff
72
+ 8.times do |bit|
73
+ register = @registers[bit]
74
+ stage = (register ^ (incoming >> bit)) & 1
75
+ @registers[bit] = (register >> 1) | (stage << (LENGTHS[bit] - 1))
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class SuperPET < PET
6
+ # The SuperPET as keyed snapshot modules: the 8032's (PET::KeyedModules),
7
+ # then BL-SUPERPET with the board and the 6702 under dongle.*, BL-ACIA
8
+ # and BL-M6809. The traps on the Waterloo ROM's routines go in
9
+ # BL-TRAPDOS under waterloo.*, beside the KERNAL's. Without BL-ACIA or
10
+ # BL-M6809, as from xpet, the unit stays as it is. The CPU switch and
11
+ # the latches say which CPU has the bus, and the CPU that has it
12
+ # settles on its instruction boundary, or in SYNC or CWAI.
13
+ module KeyedModules
14
+ def settled_for_snapshot?
15
+ return super unless @m6809_running
16
+
17
+ @m6809.boundary? || @m6809.waiting?
18
+ end
19
+
20
+ def save_modules
21
+ sections = super
22
+ board = @board
23
+ dongle = @dongle
24
+ sections << Snapshot::Keyed.section("BL-SUPERPET") do |out|
25
+ board.save_fields(out)
26
+ dongle.save_fields(out, "dongle.")
27
+ end
28
+ sections << Snapshot::Keyed.section("BL-ACIA") { |out| @acia.save_fields(out) }
29
+ sections << Snapshot::Keyed.section("BL-M6809") { |out| @m6809.save_fields(out) }
30
+ sections
31
+ end
32
+
33
+ def load_modules(modules)
34
+ super
35
+ board = modules.required("BL-SUPERPET")
36
+ @board.load_fields(board)
37
+ @dongle.load_fields(board, "dongle.")
38
+ acia = modules.optional("BL-ACIA")
39
+ @acia.load_fields(acia) if acia
40
+ m6809 = modules.optional("BL-M6809")
41
+ @m6809.load_fields(m6809) if m6809
42
+ @m6809_running = @board.m6809?
43
+ @board.restored!
44
+ @acia.restored!
45
+ @m6809.restored!
46
+ self
47
+ end
48
+
49
+ def save_trap_dos_fields(out)
50
+ super
51
+ @waterloo_traps&.save_fields(out, "waterloo.")
52
+ end
53
+
54
+ def load_trap_dos_fields(input, detached: false)
55
+ super
56
+ @waterloo_traps&.load_fields(input, "waterloo.")
57
+ end
58
+ end
59
+
60
+ include KeyedModules
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class SuperPET < PET
6
+ class Board
7
+ # The latches, the socket bit and the 64K of bank RAM, in a keyed
8
+ # snapshot module (BL-SUPERPET). The switches come with the setup
9
+ # (Snapshot::PETSetup). Which CPU has the bus, and the diagnostic
10
+ # sense line, follow from the latches once loaded, and the pages
11
+ # are laid out again (#restored!).
12
+ module KeyedState
13
+ def save_fields(out)
14
+ out.int("bank_latch", @bank_latch).int("system_latch", @system_latch).int("socket", @socket)
15
+ out.bytes("ram", @cells)
16
+ end
17
+
18
+ def load_fields(input)
19
+ @bank_latch = input.byte("bank_latch", 0)
20
+ @system_latch = input.byte("system_latch", 0)
21
+ @socket = input.int("socket", 0) & 0x01
22
+ input.bytes_into("ram", @cells)
23
+ @m6809 = m6809_selected?
24
+ @keyboard_ports.diagnostic_low = @system_latch.anybits?(0x08)
25
+ end
26
+
27
+ def restored! = @bus.map_pages
28
+ end
29
+
30
+ include KeyedState
31
+ end
32
+
33
+ class Dongle
34
+ # The shift registers, the latch and a shift waiting, as the cycles
35
+ # from now to the one it ends, under `prefix`.
36
+ module KeyedState
37
+ def save_fields(out, prefix)
38
+ out.ints("#{prefix}registers", @registers).int("#{prefix}latch", @latch)
39
+ out.optional_int("#{prefix}shift_in", @shift_at == NONE ? nil : @shift_at - @clock.cycles)
40
+ end
41
+
42
+ def load_fields(input, prefix)
43
+ input.ints_into("#{prefix}registers", @registers)
44
+ @latch = input.byte("#{prefix}latch", 0xff)
45
+ shift_in = input.optional_int("#{prefix}shift_in")
46
+ @shift_at = shift_in.nil? ? NONE : @clock.cycles + shift_in
47
+ end
48
+ end
49
+
50
+ include KeyedState
51
+ end
52
+
53
+ class WaterlooTraps
54
+ # Whether a served device listens or talks, the channel and the
55
+ # command it was addressed with, and the bytes sent it so far,
56
+ # under `prefix`.
57
+ module KeyedState
58
+ def save_fields(out, prefix)
59
+ out.bool("#{prefix}listening", @listening).bool("#{prefix}talking", @talking)
60
+ out.int("#{prefix}channel", @channel).int("#{prefix}frame", @frame).ints("#{prefix}buffer", @buffer)
61
+ end
62
+
63
+ def load_fields(input, prefix)
64
+ @listening = input.bool("#{prefix}listening", false)
65
+ @talking = input.bool("#{prefix}talking", false)
66
+ @channel = input.int("#{prefix}channel", 0) & 0x0f
67
+ @frame = input.byte("#{prefix}frame", 0)
68
+ @buffer = input.ints("#{prefix}buffer", []).map { |byte| byte & 0xff }
69
+ end
70
+ end
71
+
72
+ include KeyedState
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class SuperPET < PET
6
+ # The board's 6809E, on the PET's bus, as the 6502 is. A trap
7
+ # installed on an address runs as the 6809 is about to fetch an
8
+ # opcode there, and may move the PC on.
9
+ class M6809
10
+ include Chips::M6809::Core
11
+
12
+ def initialize(bus)
13
+ super
14
+ @traps = nil
15
+ end
16
+
17
+ def install_trap(addr, &handler)
18
+ (@traps ||= {})[addr] = handler
19
+ end
20
+
21
+ def remove_trap(addr)
22
+ @traps&.delete(addr)
23
+ end
24
+
25
+ def cycle!
26
+ @traps[@pc]&.call if @traps && boundary?
27
+ super
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class PET
5
+ class SuperPET < PET
6
+ # The traps on the Waterloo 6809 ROM's IEEE-488 routines, which serve
7
+ # device 8 from the virtual drive (KernalTrap::DOS) the 6502's traps
8
+ # serve it from, after the ROM's source as Mike Naberezny hosts it
9
+ # (opsys-all-clean-20101210-src.txt). Other devices fall through to
10
+ # the ROM, which drives the bus.
11
+ #
12
+ # The routines take their argument in D and return with RTS: SetLstnr
13
+ # and SetTalkr a file control block, whose byte 9 is the device, byte
14
+ # 10 the secondary address and byte 0 the status, 3 after a timeout,
15
+ # SetLstnr with the command's offset pushed before the call ($F0 to
16
+ # open, $E0 to close, $60 for data), ByteOut_ the byte to send and
17
+ # the byte-in routine none, returning the byte in D and setting bit 0
18
+ # of the block's byte 12 at EOI. Each keeps the block's address at
19
+ # $02 and the bus at $77-$7A: $20 listening or $40 talking, the
20
+ # offset, the device and the secondary address. A language that hooks
21
+ # a routine through its flag at $0580 gets the ROM's routine, which
22
+ # calls the hook.
23
+ #
24
+ # The menu loads from "disk/1.", drive 1 of a dual drive. The virtual
25
+ # drive has one, so a name on drive 1 opens the file on drive 0.
26
+ class WaterlooTraps
27
+ BYTE_OUT = 0xbe6f
28
+ BYTE_IN = 0xbefb
29
+ SET_LISTENER = 0xc072
30
+ UNLISTEN = 0xc0d0
31
+ SET_TALKER = 0xc0dd
32
+ UNTALK = 0xc13c
33
+
34
+ # The routines' user hook numbers, whose flags sit at $0580 on.
35
+ HOOKS = { BYTE_OUT => 23, BYTE_IN => 24, SET_LISTENER => 27, UNLISTEN => 28, SET_TALKER => 29,
36
+ UNTALK => 30 }.freeze
37
+
38
+ BLOCK = 0x02
39
+ ACTIVE = 0x77
40
+ LISTENING = 0x20
41
+ TALKING = 0x40
42
+ TIMED_OUT = 3
43
+
44
+ OPEN = 0xf0
45
+ CLOSE = 0xe0
46
+
47
+ # The device the traps serve, or nil to serve none.
48
+ attr_accessor :device
49
+
50
+ def initialize(cpu:, bus:, dos:)
51
+ @cpu = cpu
52
+ @bus = bus
53
+ @dos = dos
54
+ @device = KernalTrap::Routine::DEVICE
55
+ @listening = false
56
+ @talking = false
57
+ @channel = 0
58
+ @frame = 0
59
+ @buffer = []
60
+ end
61
+
62
+ def install
63
+ HOOKS.each_key { |address| @cpu.install_trap(address) { call(address) } }
64
+ self
65
+ end
66
+
67
+ def uninstall
68
+ HOOKS.each_key { |address| @cpu.remove_trap(address) }
69
+ end
70
+
71
+ # Whether a served device listens and talks, the channel and the
72
+ # command, and the bytes sent so far, for a checkpoint.
73
+ def state = [@listening ? 1 : 0, @talking ? 1 : 0, @channel, @frame, *@buffer]
74
+
75
+ private
76
+
77
+ def call(address)
78
+ return unless @bus.peek(0x0580 + HOOKS.fetch(address)).zero?
79
+
80
+ case address
81
+ when SET_LISTENER then set_listener
82
+ when BYTE_OUT then byte_out
83
+ when UNLISTEN then unlisten
84
+ when SET_TALKER then set_talker
85
+ when BYTE_IN then byte_in
86
+ else untalk
87
+ end
88
+ end
89
+
90
+ def served?(block) = !@device.nil? && @bus.peek(block + 9) == @device
91
+
92
+ def set_listener
93
+ block = @cpu.d
94
+ return unless served?(block)
95
+
96
+ offset = @bus.peek(@cpu.s + 3)
97
+ select(block, LISTENING, offset)
98
+ @listening = true
99
+ @frame = offset & 0xf0
100
+ @buffer = []
101
+ @dos.close(@channel) if @frame == CLOSE
102
+ return_from_routine
103
+ end
104
+
105
+ def set_talker
106
+ block = @cpu.d
107
+ return unless served?(block)
108
+
109
+ select(block, TALKING, 0x60)
110
+ @talking = true
111
+ return_from_routine
112
+ end
113
+
114
+ # Ends what the bus was doing, then addresses the block's device as
115
+ # the routine does.
116
+ def select(block, active, offset)
117
+ finish_listening if @listening
118
+ @talking = false
119
+ @bus.poke(BLOCK, block >> 8)
120
+ @bus.poke(BLOCK + 1, block & 0xff)
121
+ @bus.poke(ACTIVE, active)
122
+ @bus.poke(ACTIVE + 1, offset)
123
+ @bus.poke(ACTIVE + 2, @bus.peek(block + 9))
124
+ secondary = @bus.peek(block + 10)
125
+ @bus.poke(ACTIVE + 3, secondary)
126
+ @channel = secondary & 0x0f
127
+ end
128
+
129
+ def byte_out
130
+ return unless @listening
131
+
132
+ block = current_block
133
+ if @bus.peek(block).zero?
134
+ @buffer << (@cpu.d & 0xff)
135
+ @bus.poke(block, TIMED_OUT) unless accepted?
136
+ end
137
+ @cpu.d = 0x00ff
138
+ return_from_routine
139
+ end
140
+
141
+ def accepted? = [OPEN, CLOSE].include?(@frame) || @dos.listening?(@channel)
142
+
143
+ def unlisten
144
+ return unless @listening
145
+
146
+ finish_listening
147
+ @bus.poke(ACTIVE, 0)
148
+ return_from_routine
149
+ end
150
+
151
+ def finish_listening
152
+ @listening = false
153
+ case @frame
154
+ when OPEN then @dos.open(@channel, on_drive_zero(@buffer.pack("C*")))
155
+ when CLOSE then nil
156
+ else @dos.write(@channel, @buffer)
157
+ end
158
+ @buffer = []
159
+ end
160
+
161
+ def on_drive_zero(name) = name.start_with?("1:") ? "0:#{name[2..]}" : name
162
+
163
+ def byte_in
164
+ return unless @talking
165
+
166
+ block = current_block
167
+ byte = 0x0d
168
+ if @bus.peek(block).zero?
169
+ read, eoi = @dos.read(@channel)
170
+ if read.nil?
171
+ @bus.poke(block, TIMED_OUT)
172
+ else
173
+ byte = read
174
+ @bus.poke(block + 12, @bus.peek(block + 12) | 0x01) if eoi
175
+ end
176
+ end
177
+ @cpu.d = byte
178
+ return_from_routine
179
+ end
180
+
181
+ def untalk
182
+ return unless @talking
183
+
184
+ @talking = false
185
+ @bus.poke(ACTIVE, 0)
186
+ return_from_routine
187
+ end
188
+
189
+ def current_block = (@bus.peek(BLOCK) << 8) | @bus.peek(BLOCK + 1)
190
+
191
+ def return_from_routine
192
+ s = @cpu.s
193
+ @cpu.pc = (@bus.peek(s) << 8) | @bus.peek(s + 1)
194
+ @cpu.s = (s + 2) & 0xffff
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end