badline 0.16.1 → 0.17.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The unit's two drives as the floppy controller sees them through
7
+ # the VIA and the 6530 (schematic 8050001, sheets 2 and 3).
8
+ #
9
+ # VIA port B runs the drives: PB5 drive 0's motor and PB4 drive 1's,
10
+ # each on while low, PB2-3 drive 0's stepper phases and PB0-1 drive
11
+ # 1's, and PB7 reads SYNC, low while ten one bits sit in the shift
12
+ # register. The 6530's port B picks the drive on PB0, the zone on
13
+ # PB1-2 and the side on PB4, low for the second, and reads write
14
+ # protect on PB3, high while the disk can't be written, and the sides
15
+ # on PB6, low for two, which jumper E3 grounds on the 8250. Its port
16
+ # A holds the byte to write.
17
+ #
18
+ # Each phase change moves the head a track: the controller steps
19
+ # through the Gray code 00, 10, 11, 01 outward and back the other way
20
+ # inward ($FF9C), and bumps the head against track 1 with 92 steps
21
+ # out.
22
+ #
23
+ # A turning disk passes one byte cell under the head every ten bit
24
+ # cells, at 6 MHz divided by 16 less the track's zone, and the byte
25
+ # cells come round 300 times a minute (Disk). Each data cell ends in
26
+ # byte ready, which latches the GCR ROM's byte into VIA port A through
27
+ # CA1 and pulses the controller's SO pin; a sync cell holds SYNC low
28
+ # for the cell after it instead. While the VIA's CB2 is low the drive
29
+ # writes: each cell takes the 6530's port A, or a sync while CA2 is
30
+ # high, and still ends in byte ready.
31
+ #
32
+ # A disk going in or out passes the write-protect sensor, which the
33
+ # controller watches for a disk change: it reads high for a while.
34
+ class Mechanism
35
+ WRITE_PROTECT = 0x08
36
+ ONE_SIDE = 0x40
37
+
38
+ # The cycles the write-protect sensor reads high as a disk goes in
39
+ # or out, ten of the controller's 5 ms interrupts.
40
+ CHANGE_CYCLES = 50_000
41
+
42
+ attr_reader :sides, :data_in, :cylinders, :so_pending
43
+ attr_accessor :via, :rriot
44
+
45
+ def initialize(sides:)
46
+ @sides = sides
47
+ @via = nil
48
+ @rriot = nil
49
+ @disks = [nil, nil]
50
+ @cylinders = [0, 0]
51
+ @phases = [0, 0]
52
+ @positions = [0, 0]
53
+ @clocks = [0, 0]
54
+ @changing = [0, 0]
55
+ @motors = [false, false]
56
+ @so_pending = false
57
+ @data_in = 0xff
58
+ @syncs = [false, false]
59
+ @ca1_low = false
60
+ end
61
+
62
+ # The Disk in +drive+, 0 or 1, or nil.
63
+ def disk(drive = 0) = @disks[drive]
64
+
65
+ def insert(disk, drive = 0)
66
+ @disks[drive]&.flush
67
+ @disks[drive] = disk
68
+ @changing[drive] = CHANGE_CYCLES
69
+ end
70
+
71
+ def flush
72
+ @disks.each { |disk| disk&.flush }
73
+ end
74
+
75
+ def motor_on?(drive = 0) = @motors[drive]
76
+
77
+ # The track under +drive+'s head on the side the 6530 picks.
78
+ def track(drive = 0)
79
+ side = drive == selected_drive ? selected_side : 0
80
+ @cylinders[drive] + 1 + (side * Storage::D80Image::SIDE_TRACKS)
81
+ end
82
+
83
+ def cycle!
84
+ if @ca1_low
85
+ @ca1_low = false
86
+ @via.ca1 = true
87
+ end
88
+ drives = @via.port_b_output
89
+ turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
90
+ turn(1, drives.nobits?(0x10), drives & 0x03)
91
+ end
92
+
93
+ # Whether a cycle would change nothing: both motors off, no disk
94
+ # going in or out, and no byte ready under way.
95
+ def quiet?
96
+ !@motors[0] && !@motors[1] && @changing[0].zero? && @changing[1].zero? && !@ca1_low && !@so_pending
97
+ end
98
+
99
+ # What the drives hold, for comparing them at two points.
100
+ def idle_state = [*@cylinders, *@phases, *@motors, *@syncs, @data_in]
101
+
102
+ # Whether byte ready pulses the controller's SO pin this cycle,
103
+ # which takes the pulse.
104
+ def take_so
105
+ so = @so_pending
106
+ @so_pending = false
107
+ so
108
+ end
109
+
110
+ # Whether ten one bits sit in the shift register, off the selected
111
+ # drive's head.
112
+ def sync? = @syncs[selected_drive]
113
+
114
+ # The 6530's port B inputs.
115
+ def status_lines
116
+ lines = 0xff
117
+ lines &= ~WRITE_PROTECT unless write_protected?(selected_drive)
118
+ @sides == 2 ? lines & ~ONE_SIDE : lines
119
+ end
120
+
121
+ private
122
+
123
+ def selected_drive = @rriot.port_b_output & 0x01
124
+
125
+ def selected_side = @sides == 2 && @rriot.port_b_output.nobits?(0x10) ? 1 : 0
126
+
127
+ def write_protected?(drive)
128
+ return true if @changing[drive].positive?
129
+
130
+ disk = @disks[drive]
131
+ disk ? disk.write_protected? : false
132
+ end
133
+
134
+ def turn(drive, motor, phase)
135
+ @changing[drive] -= 1 if @changing[drive].positive?
136
+ step(drive, phase) if phase != @phases[drive]
137
+ motor_off(drive) if @motors[drive] && !motor
138
+ @motors[drive] = motor
139
+ return @syncs[drive] = false unless motor && @disks[drive]
140
+
141
+ clock = @clocks[drive] + 6
142
+ byte = 10 * (16 - Disk.zone_of(track(drive)))
143
+ if clock >= byte
144
+ clock -= byte
145
+ pass_cell(drive)
146
+ end
147
+ @clocks[drive] = clock
148
+ end
149
+
150
+ # Gray code index of a phase pair: 00, 10, 11, 01.
151
+ def phase_index(phase) = [0, 3, 1, 2][phase]
152
+
153
+ def step(drive, phase)
154
+ change = (phase_index(phase) - phase_index(@phases[drive])) & 0x03
155
+ @phases[drive] = phase
156
+ cylinder = @cylinders[drive]
157
+ if change == 1
158
+ cylinder -= 1
159
+ elsif change == 3
160
+ cylinder += 1
161
+ end
162
+ @cylinders[drive] = cylinder.clamp(0, Storage::D80Image::SIDE_TRACKS - 1)
163
+ end
164
+
165
+ def motor_off(drive)
166
+ @disks[drive]&.flush
167
+ end
168
+
169
+ # A cell has passed under +drive+'s head.
170
+ def pass_cell(drive)
171
+ disk = @disks[drive]
172
+ number = track(drive)
173
+ cells = disk.track(number)
174
+ return unless cells
175
+
176
+ position = @positions[drive] % cells.length
177
+ @positions[drive] = position + 1
178
+ return unless drive == selected_drive
179
+
180
+ writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
181
+ end
182
+
183
+ def writing? = (@via.pcr & 0xe0) == 0xc0
184
+
185
+ def read_cell(drive, cells, position)
186
+ cell = cells[position]
187
+ @syncs[drive] = cell == Disk::SYNC
188
+ byte_ready(cell) unless cell == Disk::SYNC
189
+ end
190
+
191
+ def write_cell(drive, disk, number, cells, position)
192
+ cells[position] = @via.pcr.allbits?(0x0e) ? Disk::SYNC : @rriot.port_a_output
193
+ disk.written(number)
194
+ @syncs[drive] = false
195
+ byte_ready(@data_in)
196
+ end
197
+
198
+ def byte_ready(byte)
199
+ @data_in = byte
200
+ @via.ca1 = false
201
+ @ca1_low = true
202
+ @so_pending = true
203
+ end
204
+ end
205
+
206
+ # VIA port A reads the decoded byte, and PB7 SYNC, low while the
207
+ # shift register holds a sync.
208
+ class ReadLines
209
+ def initialize(mechanism)
210
+ @mechanism = mechanism
211
+ end
212
+
213
+ def read_a(_lines) = @mechanism.data_in
214
+
215
+ def read_b(_lines) = @mechanism.sync? ? 0x7f : 0xff
216
+ end
217
+
218
+ # The 6530's port B inputs from the drives.
219
+ class StatusLines
220
+ def initialize(mechanism)
221
+ @mechanism = mechanism
222
+ end
223
+
224
+ def read_a(_lines) = 0xff
225
+
226
+ def read_b(_lines) = @mechanism.status_lines
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # Each chip a class of its own, so each keeps one type of peripheral.
7
+
8
+ # UC1, on the IEEE-488 data lines.
9
+ class DataRIOT < Chips::RIOT
10
+ end
11
+
12
+ # UE1, on the control lines, the jumpers and the LEDs.
13
+ class ControlRIOT < Chips::RIOT
14
+ end
15
+
16
+ # UM3, the controller's VIA, at $0040.
17
+ class ControllerVIA < Chips::VIA
18
+ end
19
+
20
+ # UK3, the controller's 6530, its ROM the controller's program.
21
+ class ControllerRRIOT < Chips::RRIOT
22
+ end
23
+
24
+ # The interface processor's 6502, UN1, on the IPBus.
25
+ class IPCPU
26
+ include Badline::Chips::CPU::Core
27
+ include Engine::IdleCPU
28
+ end
29
+
30
+ # The floppy controller's 6502, UH3, on the FDCBus.
31
+ class FDCCPU
32
+ include Badline::Chips::CPU::Core
33
+ include Engine::IdleCPU
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/riot"
4
+ require "badline/chips/rriot"
5
+ require "badline/drives/ieee_bus"
6
+ require "badline/drives/engine"
7
+ require "badline/drives/model8050/ip_bus"
8
+ require "badline/drives/model8050/fdc_bus"
9
+ require "badline/drives/model8050/parts"
10
+ require "badline/drives/model8050/ieee_port"
11
+ require "badline/drives/model8050/disk"
12
+ require "badline/drives/model8050/mechanism"
13
+ require "badline/drives/model8050/idle"
14
+ require "badline/drives/model8050/keyed_state"
15
+
16
+ module Badline
17
+ module Drives
18
+ # The 8050 dual disk drive of the PET and CBM-II, two drives on one
19
+ # IEEE-488 unit (schematic 8050001; the 8050/8250 service manual).
20
+ #
21
+ # Two 6502s share the board, clocked at 1 MHz from one 12 MHz crystal
22
+ # on opposite phases. The interface processor (IP) runs DOS 2.7 from
23
+ # 16 KB of ROM at $C000, with two 6532s (IPBus): their RAM is its zero
24
+ # page and stack, and their ports face the IEEE-488 bus (IEEEPort). The
25
+ # floppy controller (FDC) runs the 1 KB program in its 6530's ROM, with
26
+ # a VIA and the 6530 facing the drives (FDCBus, Mechanism). The two
27
+ # meet in 4 KB of common RAM, which each reaches in its own half of
28
+ # the cycle: the IP leaves jobs there, and the FDC runs them.
29
+ #
30
+ # Reset starts both. The FDC waits on the common RAM for the IP, which
31
+ # tests it, copies the FDC's start-up code into it ($C370) and waits for
32
+ # the FDC to answer, before both settle into their idle loops.
33
+ #
34
+ # The drive clocks against the host through a phase accumulator, as
35
+ # Drives::Engine::Core does: each host cycle adds 1 MHz of phase, and
36
+ # every whole host period in it runs a cycle of both processors. While
37
+ # both processors idle, it sleeps instead (Idle), and the readers of its
38
+ # parts catch it up first.
39
+ class Model8050
40
+ include Idle
41
+
42
+ CLOCK_HZ = 1_000_000
43
+
44
+ # The host cycles by which a unit switched on with the machine has
45
+ # run the IP's RAM test and the FDC's start-up and reads its jumpers.
46
+ BOOT_CYCLES = 1_500_000
47
+
48
+ attr_reader :device, :ieee_bus
49
+
50
+ def ip_cpu = settled(@ip_cpu)
51
+
52
+ def fdc_cpu = settled(@fdc_cpu)
53
+
54
+ def ip_bus = settled(@ip_bus)
55
+
56
+ def fdc_bus = settled(@fdc_bus)
57
+
58
+ def common = settled(@common)
59
+
60
+ def data_riot = settled(@data_riot)
61
+
62
+ def control_riot = settled(@control_riot)
63
+
64
+ def via = settled(@via)
65
+
66
+ def rriot = settled(@rriot)
67
+
68
+ def mechanism = settled(@mechanism)
69
+
70
+ def cycles
71
+ settle!
72
+ @cycles
73
+ end
74
+
75
+ attr_writer :host_clock_hz
76
+
77
+ def model_name = "8050"
78
+
79
+ # The sides each drive's disk has.
80
+ def sides = 1
81
+
82
+ # +dos_rom+ covers $C000-$FFFF and +fdc_rom+ is the 6530's mask, each
83
+ # defaulting to the image in the ROM path: DOS 2.7 and the mask for
84
+ # MPI mechanisms. +device+ is the number the jumpers set, 8 to 15.
85
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
86
+ @device = device
87
+ @host_clock_hz = host_clock_hz
88
+ @phase = 0
89
+ @cycles = 0
90
+ build_ip(dos_rom || ROM.load("8050/dos-2.7.rom", 0xc000))
91
+ build_fdc(fdc_rom || ROM.read("8050/fdc-901869-01.rom"))
92
+ @ip_cpu = IPCPU.new(@ip_bus)
93
+ @fdc_cpu = FDCCPU.new(@fdc_bus)
94
+ init_idle
95
+ @ieee_bus.attach(self)
96
+ end
97
+
98
+ # Plugs the unit into an IEEE-488 bus, leaving the one it was on.
99
+ def connect(ieee_bus)
100
+ settle!
101
+ @ieee_bus.detach(self)
102
+ @ieee_bus = ieee_bus
103
+ @ieee_port.bus = ieee_bus
104
+ ieee_bus.attach(self)
105
+ end
106
+
107
+ # The lines the unit pulls on the bus while ATN is low or not
108
+ # (IEEEPort).
109
+ def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
110
+
111
+ # The controller pushed its lines into the bus.
112
+ def host_written! = nil
113
+
114
+ # Which LEDs are lit: bit 0 drive 1's, bit 1 drive 0's, bit 2 the
115
+ # error LED. They hold still while the unit sleeps, so reading them
116
+ # leaves it asleep.
117
+ def leds = @ieee_port.leds
118
+
119
+ def led_on? = leds.anybits?(0x03)
120
+
121
+ # Puts a Disk in +drive+, 0 or 1. Nil takes the disk out.
122
+ def insert(disk, drive: 0)
123
+ settle!
124
+ @mechanism.insert(disk, drive)
125
+ end
126
+
127
+ # Puts in the .d80 or .d82 image at +path+, write-protected with
128
+ # `read_only`.
129
+ def insert_image(path, drive: 0, read_only: false)
130
+ image_class = File.extname(path).casecmp?(".d82") ? Storage::D82Image : Storage::D80Image
131
+ insert(Disk.new(image_class.new(path, read_only:)), drive:)
132
+ end
133
+
134
+ # The Disk in +drive+, or nil.
135
+ def disk(drive = 0) = @mechanism.disk(drive)
136
+
137
+ # Stores what the heads wrote in the disks' images, as the motors
138
+ # stopping do.
139
+ def flush
140
+ settle!
141
+ @mechanism.flush
142
+ end
143
+
144
+ def power_led_on? = true
145
+
146
+ # Runs the drive cycles that fall in one host cycle, then takes the
147
+ # controller's lines for the next. Asleep, the unit owes the cycles
148
+ # instead, and wakes once the controller has moved a line.
149
+ def host_cycle!
150
+ phase = @phase + CLOCK_HZ
151
+ while phase >= @host_clock_hz
152
+ @asleep ? owe_cycle : run_cycle
153
+ phase -= @host_clock_hz
154
+ end
155
+ @phase = phase
156
+ settle! if @asleep && @ieee_port.lines_moved?
157
+ @ieee_port.latch_host
158
+ end
159
+
160
+ def cycle!
161
+ settle!
162
+ run_cycle
163
+ end
164
+
165
+ # The bus's IFC line resets both processors and every chip. RAM keeps
166
+ # its contents.
167
+ def reset!
168
+ settle!
169
+ @data_riot.reset!
170
+ @control_riot.reset!
171
+ @via.reset!
172
+ @rriot.reset!
173
+ @ip_cpu.reset!
174
+ @fdc_cpu.reset!
175
+ end
176
+
177
+ def inspect
178
+ "#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) fdc=(#{@fdc_cpu.inspect})>"
179
+ end
180
+
181
+ private
182
+
183
+ def settled(part)
184
+ settle!
185
+ part
186
+ end
187
+
188
+ def run_cycle
189
+ step
190
+ watch_loops if @idle_skip
191
+ end
192
+
193
+ def build_ip(rom)
194
+ @common = Memory.new([], length: 0x1000)
195
+ @ieee_bus = IEEEBus.new
196
+ @ieee_port = IEEEPort.new(device: @device, bus: @ieee_bus)
197
+ @data_riot = DataRIOT.new(peripheral: DataLines.new(@ieee_port))
198
+ @control_riot = ControlRIOT.new(peripheral: ControlLines.new(@ieee_port))
199
+ @ieee_port.data_riot = @data_riot
200
+ @ieee_port.control_riot = @control_riot
201
+ @ip_bus = IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
202
+ end
203
+
204
+ def build_fdc(rom)
205
+ @mechanism = Mechanism.new(sides:)
206
+ @via = ControllerVIA.new(start: 0x0040, peripheral: ReadLines.new(@mechanism))
207
+ @rriot = ControllerRRIOT.new(rom:, peripheral: StatusLines.new(@mechanism))
208
+ @fdc_bus = FDCBus.new(common: @common, via: @via, rriot: @rriot)
209
+ @mechanism.via = @via
210
+ @mechanism.rriot = @rriot
211
+ end
212
+
213
+ # One cycle of the board: the drives, then the chips, then the IP and
214
+ # the FDC, each in its own half of the cycle. ATN reaches UE1's PA7 as
215
+ # the port latched it. UE1 pulls the IP's IRQ, and the VIA and the
216
+ # 6530 the FDC's. Byte ready sets the FDC's V flag through SO for the
217
+ # next cycle's instruction step, as on the 1541.
218
+ def step
219
+ so = @mechanism.take_so
220
+ @mechanism.cycle!
221
+ @control_riot.pa7 = @ieee_port.atn_low?
222
+ @data_riot.cycle!
223
+ @control_riot.cycle!
224
+ @via.cycle!
225
+ @rriot.cycle!
226
+ @ip_cpu.irq = @control_riot.irq?
227
+ @ip_cpu.cycle!
228
+ @fdc_cpu.irq = @via.irq? || @rriot.irq?
229
+ @fdc_cpu.so! if so
230
+ @fdc_cpu.cycle!
231
+ @cycles += 1
232
+ end
233
+ end
234
+
235
+ # The 8250: the 8050's board driving double-sided mechanisms, 154 tracks
236
+ # a disk, with jumper E3 grounding the 6530's PB6 to say so.
237
+ class Model8250 < Model8050
238
+ def model_name = "8250"
239
+
240
+ def sides = 2
241
+ end
242
+ end
243
+ end
@@ -111,16 +111,23 @@ module Badline
111
111
  end
112
112
  end
113
113
 
114
+ # The window keeps F8, F9, F10, Tab and the snapshot keys for itself,
115
+ # but for the keys the machine claims (Controls#claims?).
114
116
  def handle_key(scancode, down)
115
- if [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
117
+ claimed = @controls.claims?(scancode)
118
+ if !claimed && toggle?(scancode)
116
119
  handle_toggle(scancode) if down
117
- elsif down && @snapshots.key(scancode)
120
+ elsif !claimed && down && @snapshots.key(scancode)
118
121
  swap(@snapshots.computer)
119
- elsif !Snapshots::KEYS.include?(scancode)
122
+ elsif claimed || !Snapshots::KEYS.include?(scancode)
120
123
  @controls.key(scancode, down)
121
124
  end
122
125
  end
123
126
 
127
+ def toggle?(scancode)
128
+ [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
129
+ end
130
+
124
131
  # Runs the restored machine in place of the one before: the screen,
125
132
  # the drive LED, the controls, the gamepads and the sound go over to
126
133
  # it.
@@ -10,7 +10,7 @@ module Badline
10
10
  # attach, an --at key names nothing or an event fails, which it warns
11
11
  # about.
12
12
  def self.run(options)
13
- error = Timeline.error(options.timeline, options.family)
13
+ error = Timeline.error(options.timeline, options.p500? ? :p500 : options.family)
14
14
  unless error.empty?
15
15
  warn "#{options.program}: #{error}"
16
16
  return 1
@@ -8,10 +8,12 @@ module Badline
8
8
  # switches, the arrow cluster and space drive one joystick and WASD
9
9
  # and left shift the other. The arrows start on joystick 2, and
10
10
  # #swap_ports swaps them. The VIC-20's one joystick takes both. The
11
- # PET has no joystick, and its keys go by the character they type
12
- # (PETKeyboard).
13
- # - A pot device the pause menu plugs into a C64 control port: a 1351
14
- # mouse or a pair of paddles, which the host mouse drives. Motion
11
+ # PET's and the CBM-II's keys go by the character they type
12
+ # (PETKeyboard), and the PET and the CBM-II's B series have no
13
+ # joystick.
14
+ # - A pot device the pause menu plugs into a control port of a C64, a
15
+ # C128 or a P500: a 1351 mouse or a pair of paddles, which the host
16
+ # mouse drives. Motion
15
17
  # turns the paddles or moves the mouse, and the host's left and right
16
18
  # buttons go to whichever lines the device puts them on.
17
19
  class Controls
@@ -24,6 +26,21 @@ module Badline
24
26
  # The pot device plugged in, one of POTS.
25
27
  attr_reader :pot, :arrows_port
26
28
 
29
+ # Whether the machine has joysticks: all but the PET and the CBM-II's
30
+ # B series.
31
+ def self.joysticks?(computer)
32
+ family = computer.family
33
+ family != :pet && (family != :cbm2 || p500?(computer))
34
+ end
35
+
36
+ # Whether the machine is the CBM-II's P500, which has the B series'
37
+ # keyboard and two joysticks.
38
+ def self.p500?(computer) = computer.is_a?(CBM2::P500)
39
+
40
+ # Whether the machine's keys go by the character they type: the PET's
41
+ # and the CBM-II's.
42
+ def self.symbolic?(computer) = %i[pet cbm2].include?(computer.family)
43
+
27
44
  def initialize(computer)
28
45
  @computer = computer
29
46
  @joystick = false
@@ -58,9 +75,19 @@ module Badline
58
75
  self.joystick_mode = !@joystick if tab_toggles?
59
76
  end
60
77
 
61
- # Whether Tab switches the keys to the joysticks, or on the PET, which
62
- # has none, is a key of its own.
63
- def tab_toggles? = @computer.family != :pet
78
+ # Whether Tab switches the keys to the joysticks, or on a machine
79
+ # without them is a key of its own.
80
+ def tab_toggles? = Controls.joysticks?(@computer)
81
+
82
+ # Whether the machine takes a key the window would otherwise keep for
83
+ # itself: on the CBM-II, F8, RUN/STOP on F12, Alt with F9 or F10, and
84
+ # the release of any key it pressed.
85
+ def claims?(scancode)
86
+ return false unless @computer.family == :cbm2
87
+ return true if @pet_keyboard.holds?(scancode) || [Keys::F8, Keys::F12].include?(scancode)
88
+
89
+ @pet_keyboard.alt? && [Keys::F9, Keys::F10].include?(scancode)
90
+ end
64
91
 
65
92
  # Plugs in one of POTS, in place of the one before.
66
93
  def plug(pot)
@@ -116,7 +143,7 @@ module Badline
116
143
  # A host key lets go of the key it pressed, though the C128 has
117
144
  # switched between C64 and C128 mode since and maps it elsewhere.
118
145
  def machine_key(scancode, down)
119
- return @pet_keyboard.key(scancode, down) if @computer.family == :pet
146
+ return @pet_keyboard.key(scancode, down) if Controls.symbolic?(@computer)
120
147
 
121
148
  @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
122
149
  key = @held[scancode]
@@ -159,7 +186,7 @@ module Badline
159
186
  # A fresh pot device on its port, and none on the other. A machine
160
187
  # without the ports takes none.
161
188
  def attach_pot_device
162
- ports = @computer.family == :pet ? nil : @computer.control_ports
189
+ ports = Controls.p500?(@computer) || !Controls.symbolic?(@computer) ? @computer.control_ports : nil
163
190
  if ports.nil?
164
191
  @pot = :none
165
192
  return
@@ -211,17 +238,26 @@ module Badline
211
238
  down ? joystick.press(direction) : joystick.release(direction)
212
239
  end
213
240
 
214
- # The PET's keyboard for a PET, and for any other machine one it never
215
- # uses.
241
+ # The PET's keyboard for a PET or a CBM-II, and for any other machine
242
+ # one it never uses.
216
243
  def pet_keyboard(computer)
217
- PETKeyboard.new(computer.keyboard, computer.family == :pet ? computer.model.keyboard : :graphics)
244
+ layout = case computer.family
245
+ when :pet then computer.model.keyboard
246
+ when :cbm2 then :cbm2
247
+ else :graphics
248
+ end
249
+ PETKeyboard.new(computer.keyboard, layout)
218
250
  end
219
251
 
220
252
  def release_all
221
- return @pet_keyboard.release_all if @computer.family == :pet
253
+ if Controls.symbolic?(@computer)
254
+ @pet_keyboard.release_all
255
+ else
256
+ Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
257
+ Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
258
+ end
259
+ return unless Controls.joysticks?(@computer)
222
260
 
223
- Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
224
- Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
225
261
  Keys::ARROWS.each_value do |direction|
226
262
  @computer.joystick1.release(direction)
227
263
  @computer.joystick2.release(direction)
@@ -7,8 +7,8 @@ module Badline
7
7
  # second one joystick 1. PadPort holds the mapping.
8
8
  #
9
9
  # The controllers are polled every frame, and rescanned whenever SDL
10
- # reports one added or removed. The PET has no joysticks, and the
11
- # controllers steer two that nothing reads.
10
+ # reports one added or removed. The PET and the CBM-II's B series have
11
+ # no joysticks, and the controllers steer two that nothing reads.
12
12
  class Gamepads
13
13
  PORTS = [2, 1].freeze
14
14
 
@@ -53,7 +53,7 @@ module Badline
53
53
  private
54
54
 
55
55
  def joystick(computer, port)
56
- return Input::Joystick.new if computer.family == :pet
56
+ return Input::Joystick.new unless Controls.joysticks?(computer)
57
57
 
58
58
  port == 1 ? computer.joystick1 : computer.joystick2
59
59
  end