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,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The unit's two SA390 drives as the 6504 sees them through the VIA
7
+ # and the 6530 (schematic 320806, sheets 2 and 3).
8
+ #
9
+ # VIA port B runs the drives: PB5 drive 0's motor and PB4 drive 1's,
10
+ # each on while low, PB2-3 drive 0's stepper and PB0-1 drive 1's. The
11
+ # 6530's port B picks the drive on PB0 and the zone on PB1-2, and
12
+ # reads write protect on PB3, high while the disk can't be written,
13
+ # and SYNC on PB6, low while ten one bits sit in the shift register.
14
+ # Its port A holds the byte to write.
15
+ #
16
+ # Each stepper's two bits count in binary: one up moves the head half
17
+ # a track in, one down half a track out, and the controller steps
18
+ # once an interrupt, twice a track ($FF82), bumping the head against
19
+ # track 1 with 116 steps out.
20
+ #
21
+ # A turning disk passes one byte cell under the head every ten bit
22
+ # cells, at 16 MHz divided by 16 less the track's zone and then by 4,
23
+ # and the byte cells come round 300 times a minute (Disk). Each data
24
+ # cell ends in byte ready, which latches the byte into VIA port A
25
+ # through CA1; the controller has no SO pin and polls the VIA's
26
+ # interrupt flags for it. A sync cell holds SYNC low instead. While
27
+ # the VIA's CB2 is low the drive writes: each cell takes the 6530's
28
+ # port A, or a sync while CA2 is high, and still ends in byte ready.
29
+ # The format code holds CA2 high in pulse mode, which reading port A
30
+ # pulls low for a cycle.
31
+ class Mechanism
32
+ WRITE_PROTECT = 0x08
33
+ SYNC_LINE = 0x40
34
+
35
+ # The half tracks a head can reach: track 1 to 35.
36
+ HALF_TRACKS = 70
37
+
38
+ attr_reader :data_in, :half_tracks
39
+ attr_accessor :via, :rriot
40
+
41
+ def initialize
42
+ @via = nil
43
+ @rriot = nil
44
+ @disks = [nil, nil]
45
+ @half_tracks = [0, 0]
46
+ @phases = [0, 0]
47
+ @positions = [0, 0]
48
+ @clocks = [0, 0]
49
+ @motors = [false, false]
50
+ @data_in = 0xff
51
+ @syncs = [false, false]
52
+ @ca1_low = false
53
+ end
54
+
55
+ # The Disk in +drive+, 0 or 1, or nil.
56
+ def disk(drive = 0) = @disks[drive]
57
+
58
+ def insert(disk, drive = 0)
59
+ @disks[drive]&.flush
60
+ @disks[drive] = disk
61
+ end
62
+
63
+ def flush
64
+ @disks.each { |disk| disk&.flush }
65
+ end
66
+
67
+ def motor_on?(drive = 0) = @motors[drive]
68
+
69
+ # The track under +drive+'s head, 1 to 35.
70
+ def track(drive = 0) = (@half_tracks[drive] >> 1) + 1
71
+
72
+ def cycle!
73
+ if @ca1_low
74
+ @ca1_low = false
75
+ @via.ca1 = true
76
+ end
77
+ drives = @via.port_b_output
78
+ turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
79
+ turn(1, drives.nobits?(0x10), drives & 0x03)
80
+ end
81
+
82
+ # Whether a cycle would change nothing: both motors off and no byte
83
+ # ready under way.
84
+ def quiet? = !@motors[0] && !@motors[1] && !@ca1_low
85
+
86
+ # What the drives hold, for comparing them at two points.
87
+ def idle_state = [*@half_tracks, *@phases, *@motors, *@syncs, @data_in]
88
+
89
+ # The 6530's port B inputs.
90
+ def status_lines
91
+ drive = selected_drive
92
+ lines = 0xff
93
+ lines &= ~WRITE_PROTECT unless @disks[drive]&.write_protected?
94
+ @syncs[drive] ? lines & ~SYNC_LINE : lines
95
+ end
96
+
97
+ private
98
+
99
+ def selected_drive = @rriot.port_b_output & 0x01
100
+
101
+ def turn(drive, motor, phase)
102
+ step(drive, phase) if phase != @phases[drive]
103
+ motor_off(drive) if @motors[drive] && !motor
104
+ @motors[drive] = motor
105
+ return @syncs[drive] = false unless motor && @disks[drive]
106
+
107
+ clock = @clocks[drive] + 2
108
+ byte = 5 * (16 - Disk.zone_of(track(drive)))
109
+ if clock >= byte
110
+ clock -= byte
111
+ pass_cell(drive)
112
+ end
113
+ @clocks[drive] = clock
114
+ end
115
+
116
+ def step(drive, phase)
117
+ change = (phase - @phases[drive]) & 0x03
118
+ @phases[drive] = phase
119
+ half_track = @half_tracks[drive]
120
+ if change == 1
121
+ half_track += 1
122
+ elsif change == 3
123
+ half_track -= 1
124
+ end
125
+ @half_tracks[drive] = half_track.clamp(0, HALF_TRACKS - 1)
126
+ end
127
+
128
+ def motor_off(drive)
129
+ @disks[drive]&.flush
130
+ end
131
+
132
+ # A cell has passed under +drive+'s head.
133
+ def pass_cell(drive)
134
+ disk = @disks[drive]
135
+ number = track(drive)
136
+ cells = disk.track(number)
137
+ return unless cells
138
+
139
+ position = @positions[drive] % cells.length
140
+ @positions[drive] = position + 1
141
+ return unless drive == selected_drive
142
+
143
+ writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
144
+ end
145
+
146
+ def writing? = !@via.cb2_output
147
+
148
+ def read_cell(drive, cells, position)
149
+ cell = cells[position]
150
+ @syncs[drive] = cell == Disk::SYNC
151
+ byte_ready(cell) unless cell == Disk::SYNC
152
+ end
153
+
154
+ def write_cell(drive, disk, number, cells, position)
155
+ cells[position] = @via.ca2_output ? Disk::SYNC : @rriot.port_a_output
156
+ disk.written(number)
157
+ @syncs[drive] = false
158
+ byte_ready(@data_in)
159
+ end
160
+
161
+ def byte_ready(byte)
162
+ @data_in = byte
163
+ @via.ca1 = false
164
+ @ca1_low = true
165
+ end
166
+ end
167
+
168
+ # VIA port A reads the byte the last cell latched. Port B drives the
169
+ # drives only.
170
+ class ReadLines
171
+ def initialize(mechanism)
172
+ @mechanism = mechanism
173
+ end
174
+
175
+ def read_a(_lines) = @mechanism.data_in
176
+
177
+ def read_b(_lines) = 0xff
178
+ end
179
+
180
+ # The 6530's port B inputs from the drives.
181
+ class StatusLines
182
+ def initialize(mechanism)
183
+ @mechanism = mechanism
184
+ end
185
+
186
+ def read_a(_lines) = 0xff
187
+
188
+ def read_b(_lines) = @mechanism.status_lines
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model4040
6
+ # The interface processor's 6502, UN1, on the 4040's IPBus.
7
+ class IPCPU
8
+ include Badline::Chips::CPU::Core
9
+ include Engine::IdleCPU
10
+ end
11
+
12
+ # The floppy controller's 6504, UH3, on the 8050's FDCBus. It has no
13
+ # SO pin.
14
+ class FDCCPU
15
+ include Badline::Chips::CPU::Core
16
+ include Engine::IdleCPU
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model8050"
4
+ require "badline/drives/model4040/ip_bus"
5
+ require "badline/drives/model4040/parts"
6
+ require "badline/drives/model4040/disk"
7
+ require "badline/drives/model4040/mechanism"
8
+ require "badline/drives/model4040/keyed_state"
9
+
10
+ module Badline
11
+ module Drives
12
+ # The 4040 dual disk drive of the PET and CBM-II, two Shugart SA390
13
+ # drives on one IEEE-488 unit (schematic 320806, the 8050's parent).
14
+ #
15
+ # The board is the 8050's (Model8050::Board) with an older floppy side.
16
+ # A 16 MHz crystal clocks both processors at 1 MHz on opposite phases.
17
+ # The interface processor runs DOS 2.1 from 12 KB of ROM at $D000, with
18
+ # the 8050's 6532s on the bus and the same common RAM, on a bus that
19
+ # doesn't decode A15 (IPBus). The floppy controller is a 6504, with no
20
+ # SO pin, on the 8050's map (Model8050::FDCBus): it runs the program in
21
+ # its 6530 (901466-04) and polls the VIA for byte ready, whose own
22
+ # interrupt line goes nowhere, so only the 6530's timer interrupts it
23
+ # (Mechanism). The disks are the 1541's, 35 tracks of 17 to 21
24
+ # sectors, a .d64 each (Disk).
25
+ #
26
+ # At reset the controller waits for the common RAM's $0400 to read 0,
27
+ # starts, and writes its timer's period there. The interface processor
28
+ # tests its RAM and ROMs, has the controller reset its drives, and
29
+ # bumps both heads to track 1.
30
+ class Model4040
31
+ include Model8050::Board
32
+ include Model8050::Idle
33
+ include Model8050::KeyedState
34
+
35
+ CLOCK_HZ = Model8050::CLOCK_HZ
36
+
37
+ # The host cycles by which a unit switched on with the machine has
38
+ # run the IP's tests and bumped both heads, and reads its jumpers.
39
+ BOOT_CYCLES = 2_500_000
40
+
41
+ # Where DOS 2.1's idle loop and the controller's job loop start over
42
+ # (Model8050::Idle).
43
+ IP_LOOP = 0xd4b7
44
+ FDC_LOOP = 0xfc54
45
+
46
+ def model_name = "4040"
47
+
48
+ def boot_cycles = BOOT_CYCLES
49
+
50
+ def sides = 1
51
+
52
+ # +dos_rom+ covers $D000-$FFFF and +fdc_rom+ is the 6530's mask, each
53
+ # defaulting to the image in the ROM path: the revised DOS 2.1 that
54
+ # xpet's 4040 runs, and 901466-04. +device+ is the number the jumpers
55
+ # set, 8 to 15.
56
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
57
+ @device = device
58
+ @host_clock_hz = host_clock_hz
59
+ @phase = 0
60
+ @cycles = 0
61
+ build_ip(dos_rom || ROM.load(dos_file, dos_start))
62
+ build_fdc(fdc_rom || ROM.read(fdc_file))
63
+ @ip_cpu = IPCPU.new(@ip_bus)
64
+ @fdc_cpu = FDCCPU.new(@fdc_bus)
65
+ init_idle(ip_loop, fdc_loop)
66
+ @ieee_bus.attach(self)
67
+ end
68
+
69
+ # Puts in the .d64 image at +path+, or the DOS 1 disk a .d67 holds,
70
+ # write-protected with `read_only`.
71
+ def insert_image(path, drive: 0, read_only: false)
72
+ image = if File.extname(path).casecmp?(".d67")
73
+ Storage::D67Image.new(path, read_only:)
74
+ else
75
+ Storage::D64Image.new(path, read_only:)
76
+ end
77
+ insert(Disk.new(image), drive:)
78
+ end
79
+
80
+ private
81
+
82
+ def dos_file = "4040/dos-2.1-rev.rom"
83
+
84
+ def dos_start = 0xd000
85
+
86
+ def fdc_file = "4040/fdc-901466-04.rom"
87
+
88
+ def ip_loop = IP_LOOP
89
+
90
+ def fdc_loop = FDC_LOOP
91
+
92
+ def build_ip(rom)
93
+ @common = Memory.new([], length: 0x1000)
94
+ @ieee_bus = IEEEBus.new
95
+ @ieee_port = Model8050::IEEEPort.new(device: @device, bus: @ieee_bus)
96
+ @data_riot = Model8050::DataRIOT.new(peripheral: Model8050::DataLines.new(@ieee_port))
97
+ @control_riot = Model8050::ControlRIOT.new(peripheral: Model8050::ControlLines.new(@ieee_port))
98
+ @ieee_port.data_riot = @data_riot
99
+ @ieee_port.control_riot = @control_riot
100
+ @ip_bus = IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
101
+ end
102
+
103
+ def build_fdc(rom)
104
+ @mechanism = Mechanism.new
105
+ @via = Model8050::ControllerVIA.new(start: 0x0040, peripheral: ReadLines.new(@mechanism))
106
+ @rriot = Model8050::ControllerRRIOT.new(rom:, peripheral: StatusLines.new(@mechanism))
107
+ @fdc_bus = Model8050::FDCBus.new(common: @common, via: @via, rriot: @rriot)
108
+ @mechanism.via = @via
109
+ @mechanism.rriot = @rriot
110
+ end
111
+
112
+ # The disk a BL-D8-DISK module holds (Model8050::KeyedState).
113
+ def disk_from_fields(input, detached) = Disk.from_fields(input, detached:)
114
+
115
+ # One cycle of the board: the drives, then the chips, then the IP and
116
+ # the FDC, each in its own half of the cycle, as on the 8050. Only the
117
+ # 6530 pulls the FDC's IRQ, and the FDC finds byte ready by polling.
118
+ def step
119
+ @mechanism.cycle!
120
+ @control_riot.pa7 = @ieee_port.atn_low?
121
+ @data_riot.cycle!
122
+ @control_riot.cycle!
123
+ @via.cycle!
124
+ @rriot.cycle!
125
+ @ip_cpu.irq = @control_riot.irq?
126
+ @ip_cpu.cycle!
127
+ @fdc_cpu.irq = @rriot.irq?
128
+ @fdc_cpu.cycle!
129
+ @cycles += 1
130
+ end
131
+ end
132
+
133
+ # The 3040: the 4040's board with the first DOS 2.1, as xpet's 3040
134
+ # runs it, 901468-11 to -13.
135
+ class Model3040 < Model4040
136
+ # Where the first DOS 2.1's idle loop starts over.
137
+ IP_LOOP = 0xd4b8
138
+
139
+ def model_name = "3040"
140
+
141
+ private
142
+
143
+ def dos_file = "4040/dos-2.1.rom"
144
+
145
+ def ip_loop = IP_LOOP
146
+ end
147
+
148
+ # The 2040: the 4040's board with DOS 1.2 in the sockets at $E000 and
149
+ # $F000, UJ1 at $D000 left empty, and 901466-02 in the controller's
150
+ # 6530, as the 3040 with DOS 1 has it too. Its disks are DOS 1's .d67,
151
+ # with 20 sectors on tracks 18 to 24 (Storage::D67Image). The
152
+ # controller starts at once, with no handshake on the common RAM, and
153
+ # the interface processor leaves the common RAM alone at boot.
154
+ class Model2040 < Model4040
155
+ # Where DOS 1.2's idle loop and its controller's job loop start over.
156
+ IP_LOOP = 0xe27e
157
+ FDC_LOOP = 0xfc47
158
+
159
+ def model_name = "2040"
160
+
161
+ # Puts in the .d67 image at +path+, write-protected with `read_only`.
162
+ def insert_image(path, drive: 0, read_only: false)
163
+ insert(Disk.new(Storage::D67Image.new(path, read_only:)), drive:)
164
+ end
165
+
166
+ private
167
+
168
+ def dos_file = "4040/dos-1.2.rom"
169
+
170
+ def dos_start = 0xe000
171
+
172
+ def fdc_file = "4040/fdc-901466-02.rom"
173
+
174
+ def ip_loop = IP_LOOP
175
+
176
+ def fdc_loop = FDC_LOOP
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The board of a unit with two processors on the IEEE-488 bus, as the
7
+ # 8050 family and the 4040 family share it: the DOS board's interface
8
+ # processor side (DOSBoard), and the floppy controller with its VIA
9
+ # and 6530 facing the drives. Each unit's class includes it, with
10
+ # Idle and KeyedState, and builds its own chips, so no call it makes
11
+ # on them is a union: the class sets DOSBoard's fields and @fdc_cpu,
12
+ # @fdc_bus, @rriot and @mechanism, and runs one cycle of the board in
13
+ # its own step. While both processors idle, it sleeps (Idle).
14
+ module Board
15
+ include DOSBoard
16
+
17
+ def fdc_cpu = settled(@fdc_cpu)
18
+
19
+ def fdc_bus = settled(@fdc_bus)
20
+
21
+ def rriot = settled(@rriot)
22
+
23
+ def mechanism = settled(@mechanism)
24
+
25
+ # Whether the unit is where VICE saves it: both processors between
26
+ # instructions, the FDC at the start of its idle loop.
27
+ def settled_for_vice?
28
+ fdc = fdc_cpu
29
+ ip_cpu.boundary? && fdc.boundary? && fdc.program_counter == @fdc_loop
30
+ end
31
+
32
+ # Whether a drive's LED is lit: of the LEDs, bit 0 is drive 1's,
33
+ # bit 1 drive 0's, and bit 2 the error LED.
34
+ def led_on? = leds.anybits?(0x03)
35
+
36
+ # Puts a Disk in +drive+, 0 or 1. Nil takes the disk out.
37
+ def insert(disk, drive: 0)
38
+ settle!
39
+ @mechanism.insert(disk, drive)
40
+ end
41
+
42
+ # The Disk in +drive+, or nil.
43
+ def disk(drive = 0) = @mechanism.disk(drive)
44
+
45
+ # Stores what the heads wrote in the disks' images, as the motors
46
+ # stopping do.
47
+ def flush
48
+ settle!
49
+ @mechanism.flush
50
+ end
51
+
52
+ # The bus's IFC line resets both processors and every chip. RAM keeps
53
+ # its contents.
54
+ def reset!
55
+ settle!
56
+ @data_riot.reset!
57
+ @control_riot.reset!
58
+ @via.reset!
59
+ @rriot.reset!
60
+ @ip_cpu.reset!
61
+ @fdc_cpu.reset!
62
+ end
63
+
64
+ def inspect
65
+ "#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) fdc=(#{@fdc_cpu.inspect})>"
66
+ end
67
+
68
+ private
69
+
70
+ def run_cycle
71
+ step
72
+ watch_loops if @idle_skip
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The half of the DOS board every unit with an interface processor
7
+ # shares: the interface processor with its two 6532s on the IEEE-488
8
+ # bus, the common RAM, and the second processor's VIA, whichever side
9
+ # the second processor runs. Board adds the floppy controller's side
10
+ # to it, and Model9060 the SASI host's. Each unit's class sets
11
+ # @ip_cpu, @ip_bus, @common, @data_riot, @control_riot, @via,
12
+ # @ieee_bus and @ieee_port, and has its own run_cycle and settle!.
13
+ #
14
+ # The unit clocks against the host through a phase accumulator, as
15
+ # Drives::Engine::Core does: each host cycle adds 1 MHz of phase, and
16
+ # every whole host period in it runs a cycle of both processors. While
17
+ # both processors idle, it sleeps instead, and the readers of its
18
+ # parts catch it up first.
19
+ module DOSBoard
20
+ attr_reader :device, :ieee_bus
21
+
22
+ def ip_cpu = settled(@ip_cpu)
23
+
24
+ def ip_bus = settled(@ip_bus)
25
+
26
+ def common = settled(@common)
27
+
28
+ def data_riot = settled(@data_riot)
29
+
30
+ def control_riot = settled(@control_riot)
31
+
32
+ def via = settled(@via)
33
+
34
+ def cycles
35
+ settle!
36
+ @cycles
37
+ end
38
+
39
+ attr_writer :host_clock_hz
40
+
41
+ # Plugs the unit into an IEEE-488 bus, leaving the one it was on.
42
+ def connect(ieee_bus)
43
+ settle!
44
+ @ieee_bus.detach(self)
45
+ @ieee_bus = ieee_bus
46
+ @ieee_port.bus = ieee_bus
47
+ ieee_bus.attach(self)
48
+ end
49
+
50
+ # The lines the unit pulls on the bus while ATN is low or not
51
+ # (IEEEPort).
52
+ def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
53
+
54
+ # The controller pushed its lines into the bus.
55
+ def host_written! = nil
56
+
57
+ # Which LEDs are lit, as the unit's port drives them. They hold
58
+ # still while the unit sleeps, so reading them leaves it asleep.
59
+ def leds = @ieee_port.leds
60
+
61
+ def power_led_on? = true
62
+
63
+ # Runs the drive cycles that fall in one host cycle, then takes the
64
+ # controller's lines for the next. Asleep, the unit owes the cycles
65
+ # instead, and wakes once the controller has moved a line.
66
+ def host_cycle!
67
+ phase = @phase + CLOCK_HZ
68
+ while phase >= @host_clock_hz
69
+ @asleep ? owe_cycle : run_cycle
70
+ phase -= @host_clock_hz
71
+ end
72
+ @phase = phase
73
+ settle! if @asleep && @ieee_port.lines_moved?
74
+ @ieee_port.latch_host
75
+ end
76
+
77
+ def cycle!
78
+ settle!
79
+ run_cycle
80
+ end
81
+
82
+ private
83
+
84
+ def settled(part)
85
+ settle!
86
+ part
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -5,10 +5,11 @@ module Badline
5
5
  class Model8050
6
6
  # Sleeping through the two processors' idle loops, exactly.
7
7
  #
8
- # With nothing to do, the IP goes round DOS 2.7's idle loop from
9
- # IP_LOOP, and the FDC round its job loop from FDC_LOOP, each pass
10
- # finding the job queue empty, until ATN interrupts the IP or the
11
- # 6530's timer the FDC, every 5 ms. A pass puts back what it found,
8
+ # With nothing to do, the IP goes round its DOS's idle loop from
9
+ # @ip_loop, and the FDC round its job loop from @fdc_loop (IP_LOOP and
10
+ # FDC_LOOP on the 8050), each pass finding the job queue empty, until
11
+ # ATN interrupts the IP or the 6530's timer the FDC, every 5 ms on the
12
+ # 8050. A pass puts back what it found,
12
13
  # so each processor comes back to its loop's start as it left it, and
13
14
  # only the chips' counters and the cycle counts move.
14
15
  #
@@ -56,7 +57,10 @@ module Badline
56
57
 
57
58
  private
58
59
 
59
- def init_idle
60
+ # The unit's loops start at +ip_loop+ and +fdc_loop+.
61
+ def init_idle(ip_loop, fdc_loop)
62
+ @ip_loop = ip_loop
63
+ @fdc_loop = fdc_loop
60
64
  @idle_skip = true
61
65
  @asleep = false
62
66
  @owed = 0
@@ -84,9 +88,9 @@ module Badline
84
88
  ip = @ip_cpu
85
89
  @ip_pass = nil if ip.irq
86
90
  @fdc_pass = nil if @fdc_cpu.irq
87
- ip_pass_found if ip.program_counter == IP_LOOP && ip.boundary?
91
+ ip_pass_found if ip.program_counter == @ip_loop && ip.boundary?
88
92
  fdc = @fdc_cpu
89
- fdc_pass_found if fdc.program_counter == FDC_LOOP && fdc.boundary?
93
+ fdc_pass_found if fdc.program_counter == @fdc_loop && fdc.boundary?
90
94
  end
91
95
 
92
96
  def ip_pass_found
@@ -16,7 +16,7 @@ module Badline
16
16
  def save_fields(out)
17
17
  image = @image
18
18
  out.text("path", image.path.to_s).bool("read_only", image.read_only?)
19
- out.text("image.format", image.is_a?(Storage::D82Image) ? "d82" : "d80")
19
+ out.text("image.format", image_format)
20
20
  out.bytes("image", image.contents)
21
21
  @tracks.each do |number, cells|
22
22
  out.ints("track#{number}", cells) unless cells == build(number)
@@ -37,6 +37,9 @@ module Badline
37
37
 
38
38
  include KeyedState
39
39
 
40
+ # The kind of image the disk lays out, as image.format names it.
41
+ def image_format = @image.is_a?(Storage::D82Image) ? "d82" : "d80"
42
+
40
43
  # The disk a BL-D8-DISK module holds. A detached reader gets the
41
44
  # image write-protected, and it and a host file that is gone leave
42
45
  # it no path to write back to.
@@ -141,7 +144,7 @@ module Badline
141
144
  @rriot.load_fields(modules.required("#{name}-RRIOT"))
142
145
  disks = [0, 1].map do |drive|
143
146
  disk = modules.optional(disk_module(name, drive))
144
- disk.nil? ? nil : Disk.from_fields(disk, detached: modules.detached?)
147
+ disk.nil? ? nil : disk_from_fields(disk, modules.detached?)
145
148
  end
146
149
  @mechanism.load_fields(modules.required("#{name}-MECH"), disks)
147
150
  @via.restored!