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
@@ -10,6 +10,8 @@ require "badline/drives/model8050/parts"
10
10
  require "badline/drives/model8050/ieee_port"
11
11
  require "badline/drives/model8050/disk"
12
12
  require "badline/drives/model8050/mechanism"
13
+ require "badline/drives/model8050/dos_board"
14
+ require "badline/drives/model8050/board"
13
15
  require "badline/drives/model8050/idle"
14
16
  require "badline/drives/model8050/keyed_state"
15
17
 
@@ -18,25 +20,21 @@ module Badline
18
20
  # The 8050 dual disk drive of the PET and CBM-II, two drives on one
19
21
  # IEEE-488 unit (schematic 8050001; the 8050/8250 service manual).
20
22
  #
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.
23
+ # Two 6502s share the board (Board), clocked at 1 MHz from one 12 MHz
24
+ # crystal on opposite phases. The interface processor (IP) runs DOS 2.7
25
+ # from 16 KB of ROM at $C000, with two 6532s (IPBus): their RAM is its
26
+ # zero page and stack, and their ports face the IEEE-488 bus
27
+ # (IEEEPort). The floppy controller (FDC) runs the 1 KB program in its
28
+ # 6530's ROM, with a VIA and the 6530 facing the drives (FDCBus,
29
+ # Mechanism). The two meet in 4 KB of common RAM, which each reaches in
30
+ # its own half of the cycle: the IP leaves jobs there, and the FDC runs
31
+ # them.
29
32
  #
30
33
  # Reset starts both. The FDC waits on the common RAM for the IP, which
31
34
  # tests it, copies the FDC's start-up code into it ($C370) and waits for
32
35
  # 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
36
  class Model8050
37
+ include Board
40
38
  include Idle
41
39
 
42
40
  CLOCK_HZ = 1_000_000
@@ -45,37 +43,10 @@ module Badline
45
43
  # run the IP's RAM test and the FDC's start-up and reads its jumpers.
46
44
  BOOT_CYCLES = 1_500_000
47
45
 
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
46
  def model_name = "8050"
78
47
 
48
+ def boot_cycles = BOOT_CYCLES
49
+
79
50
  # The sides each drive's disk has.
80
51
  def sides = 1
81
52
 
@@ -91,39 +62,10 @@ module Badline
91
62
  build_fdc(fdc_rom || ROM.read("8050/fdc-901869-01.rom"))
92
63
  @ip_cpu = IPCPU.new(@ip_bus)
93
64
  @fdc_cpu = FDCCPU.new(@fdc_bus)
94
- init_idle
65
+ init_idle(Idle::IP_LOOP, Idle::FDC_LOOP)
95
66
  @ieee_bus.attach(self)
96
67
  end
97
68
 
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
69
  # Puts in the .d80 or .d82 image at +path+, write-protected with
128
70
  # `read_only`.
129
71
  def insert_image(path, drive: 0, read_only: false)
@@ -131,65 +73,8 @@ module Badline
131
73
  insert(Disk.new(image_class.new(path, read_only:)), drive:)
132
74
  end
133
75
 
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
76
  private
182
77
 
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
78
  def build_ip(rom)
194
79
  @common = Memory.new([], length: 0x1000)
195
80
  @ieee_bus = IEEEBus.new
@@ -210,6 +95,9 @@ module Badline
210
95
  @mechanism.rriot = @rriot
211
96
  end
212
97
 
98
+ # The disk a BL-D8-DISK module holds (KeyedState).
99
+ def disk_from_fields(input, detached) = Disk.from_fields(input, detached:)
100
+
213
101
  # One cycle of the board: the drives, then the chips, then the IP and
214
102
  # the FDC, each in its own half of the cycle. ATN reaches UE1's PA7 as
215
103
  # the port latched it. UE1 pulls the IP's IRQ, and the VIA and the
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # Sleeping through the two processors' idle loops, exactly, as
7
+ # Model8050::Idle does.
8
+ #
9
+ # With nothing to do, the IP goes round DOS 3.0's idle loop from
10
+ # IP_LOOP, and the SASI host round its job loop from SASI_LOOP, each
11
+ # pass finding the job queue empty, until ATN interrupts the IP. The
12
+ # host has no interrupt of its own. A pass puts back what it found, so
13
+ # each processor comes back to its loop's start as it left it.
14
+ #
15
+ # Each time a processor comes to its loop's start, the unit compares
16
+ # what that processor sees with what it saw last time round: the CPU,
17
+ # its chips but for their counters, its RAM and the common RAM. The
18
+ # same, with no read of a counter or an interrupt flag between, makes
19
+ # the pass repeatable. Once both are, with the hard disk's controller
20
+ # idle and no interrupt pulled, the unit sleeps until a counter is due
21
+ # to pull IRQ, the IEEE-488 controller moves a line, or something
22
+ # outside reads or changes the unit (settle!).
23
+ module Idle
24
+ IP_LOOP = 0xc131
25
+ SASI_LOOP = 0xf8f1
26
+
27
+ # Whether the unit may sleep through its idle loops.
28
+ attr_reader :idle_skip
29
+
30
+ def idle_skip=(on)
31
+ settle!
32
+ @idle_skip = on
33
+ end
34
+
35
+ def asleep? = @asleep
36
+
37
+ # Brings the unit up to the present, cycle for cycle, and drops the
38
+ # passes it found, since whoever called may change it.
39
+ def settle!
40
+ wake! if @asleep
41
+ forget_passes
42
+ end
43
+
44
+ private
45
+
46
+ def init_idle
47
+ @idle_skip = true
48
+ @asleep = false
49
+ @owed = 0
50
+ @budget = 0
51
+ forget_passes
52
+ end
53
+
54
+ def forget_passes
55
+ @ip_mark = nil
56
+ @sasi_mark = nil
57
+ @ip_pass = nil
58
+ @sasi_pass = nil
59
+ end
60
+
61
+ def owe_cycle
62
+ @owed += 1
63
+ wake! if @owed >= @budget
64
+ end
65
+
66
+ def watch_loops
67
+ ip = @ip_cpu
68
+ @ip_pass = nil if ip.irq
69
+ @sasi_pass = nil if @sasi_cpu.irq
70
+ ip_pass_found if ip.program_counter == IP_LOOP && ip.boundary?
71
+ host = @sasi_cpu
72
+ sasi_pass_found if host.program_counter == SASI_LOOP && host.boundary?
73
+ end
74
+
75
+ def ip_pass_found
76
+ state = ip_state
77
+ @ip_pass = repeated(@ip_mark, state, @ip_cpu, @ip_bus.volatile)
78
+ @ip_mark = [state, @ip_cpu.cycles, @ip_cpu.instructions]
79
+ @ip_bus.volatile = false
80
+ fall_asleep if @ip_pass && @sasi_pass
81
+ end
82
+
83
+ def sasi_pass_found
84
+ state = sasi_state
85
+ @sasi_pass = repeated(@sasi_mark, state, @sasi_cpu, @sasi_bus.volatile)
86
+ @sasi_mark = [state, @sasi_cpu.cycles, @sasi_cpu.instructions]
87
+ @sasi_bus.volatile = false
88
+ fall_asleep if @ip_pass && @sasi_pass
89
+ end
90
+
91
+ def repeated(mark, state, cpu, volatile)
92
+ return nil if mark.nil? || volatile || mark[0] != state
93
+
94
+ [cpu.cycles - mark[1], cpu.instructions - mark[2]]
95
+ end
96
+
97
+ def fall_asleep
98
+ target = @target
99
+ return unless target.quiet? && !target.busy? && !@ip_cpu.irq && !@sasi_cpu.irq
100
+
101
+ budget = quiet_cycles
102
+ return if budget < 2
103
+
104
+ @asleep = true
105
+ @owed = 0
106
+ @budget = budget
107
+ end
108
+
109
+ def quiet_cycles
110
+ [@data_riot.quiet_cycles, @control_riot.quiet_cycles, @via.quiet_cycles].min
111
+ end
112
+
113
+ def wake!
114
+ owed = @owed
115
+ @asleep = false
116
+ @owed = 0
117
+ return if owed.zero?
118
+
119
+ @data_riot.fast_forward(owed)
120
+ @control_riot.fast_forward(owed)
121
+ @via.fast_forward(owed)
122
+ @target.fast_forward(owed)
123
+ skip(@ip_cpu, @ip_pass, @ip_mark, owed)
124
+ skip(@sasi_cpu, @sasi_pass, @sasi_mark, owed)
125
+ @cycles += owed
126
+ end
127
+
128
+ def skip(cpu, pass, mark, owed)
129
+ passes = owed / pass[0]
130
+ cpu.fast_forward(passes * pass[0], passes * pass[1])
131
+ mark[1] += passes * pass[0]
132
+ mark[2] += passes * pass[1]
133
+ (owed - (passes * pass[0])).times { cpu.cycle! }
134
+ end
135
+
136
+ def ip_state
137
+ [*@ip_cpu.idle_state, *@data_riot.idle_state, *@control_riot.idle_state, @ip_bus.data, common_changes]
138
+ end
139
+
140
+ def sasi_state
141
+ [*@sasi_cpu.idle_state, *@via.idle_state, *@lines.state, *@sasi_bus.ram.read(0, 0x80), @sasi_bus.data,
142
+ common_changes]
143
+ end
144
+
145
+ def common_changes = @ip_bus.common_changes + @sasi_bus.common_changes
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ class SASITarget
7
+ # The controller's fields in a keyed snapshot module (BL-D8-SASI):
8
+ # the bus phase, REQ, the cycles left to the next, the command, the
9
+ # bytes of the phase and the place in them, the write under way,
10
+ # the sense bytes, the fast mode, its clock, the heads' cylinder, the
11
+ # geometry and the block buffer, then the image's path and how it
12
+ # was opened. The image itself stays in its file, which each write
13
+ # reached as it ended.
14
+ module KeyedState
15
+ def save_fields(out)
16
+ out.int("phase", @phase).bool("request", @request).int("delay", @delay)
17
+ out.ints("command", @command).ints("buffer", @buffer).int("index", @index)
18
+ out.bool("writing", @writing).ints("sense", @sense).ints("last_sense", @last_sense)
19
+ out.bool("fast", @fast).int("cycle", @cycle).int("cylinder", @cylinder)
20
+ out.int("heads", @heads).int("sectors", @sectors).ints("block_buffer", @block_buffer)
21
+ disk = @disk
22
+ out.text("path", disk.nil? ? "" : disk.path.to_s).bool("read_only", disk.nil? || disk.read_only?)
23
+ out.int("disk_heads", disk.nil? ? 0 : disk.heads)
24
+ end
25
+
26
+ # A detached reader, or a host file that is gone, leaves the drive
27
+ # empty.
28
+ def load_fields(input, detached: false)
29
+ load_bus_fields(input)
30
+ path = input.text("path", "")
31
+ read_only = input.bool("read_only", false)
32
+ heads = input.int("disk_heads", 4)
33
+ @disk = detached || !File.file?(path) ? nil : Storage::D90Image.new(path, read_only:, heads:)
34
+ end
35
+
36
+ private
37
+
38
+ def load_bus_fields(input)
39
+ @phase = input.int("phase", @phase)
40
+ @request = input.bool("request", @request)
41
+ @delay = input.int("delay", @delay)
42
+ @command = input.ints("command", @command).dup
43
+ @buffer = input.ints("buffer", @buffer).dup
44
+ @index = input.int("index", @index)
45
+ @writing = input.bool("writing", @writing)
46
+ @sense = input.ints("sense", @sense).dup
47
+ @last_sense = input.ints("last_sense", @last_sense).dup
48
+ @fast = input.bool("fast", @fast)
49
+ @cycle = input.int("cycle", @cycle)
50
+ @cylinder = input.int("cylinder", @cylinder)
51
+ @heads = input.int("heads", @heads)
52
+ @sectors = input.int("sectors", @sectors)
53
+ @block_buffer = input.ints("block_buffer", @block_buffer).dup
54
+ end
55
+ end
56
+
57
+ include KeyedState
58
+ end
59
+
60
+ # The unit's own fields in a keyed snapshot module (BL-D8): its model,
61
+ # its clock phase and cycle count, each bus's last byte, the
62
+ # controller's lines as the port latched them, under ieee.*, and the
63
+ # SASI lines' ACK and RST levels. A save settles the unit first, so it
64
+ # is awake; a load finds its passes afresh.
65
+ module KeyedState
66
+ def save_fields(out)
67
+ settle!
68
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
69
+ out.int("ip_data", @ip_bus.data).int("sasi_data", @sasi_bus.data)
70
+ out.int("ieee.host", @ieee_port.host)
71
+ ack_high, reset_high = @lines.state
72
+ out.bool("sasi.ack_high", ack_high).bool("sasi.reset_high", reset_high)
73
+ out
74
+ end
75
+
76
+ def load_fields(input)
77
+ settle!
78
+ input.choice("model", [model_name], model_name)
79
+ @phase = input.int("phase", @phase)
80
+ @cycles = input.int("cycles", @cycles)
81
+ @ip_bus.data = input.byte("ip_data", @ip_bus.data)
82
+ @sasi_bus.data = input.byte("sasi_data", @sasi_bus.data)
83
+ @ieee_port.host = input.int("ieee.host", @ieee_port.host)
84
+ @lines.state = [input.bool("sasi.ack_high", true), input.bool("sasi.reset_high", false)]
85
+ end
86
+
87
+ # The unit as modules of its own, named from `name` (BL-D8 for
88
+ # device 8): its own fields, the common RAM, UC1 and UE1 with their
89
+ # RAM, each processor, the host's RAM and VIA, and the hard disk's
90
+ # controller.
91
+ def save_modules(sections, name)
92
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
93
+ sections << Snapshot::Keyed.section("#{name}-COMMON") { |out| @common.save_fields(out, "ram") }
94
+ sections << Snapshot::Keyed.section("#{name}-RIOT1") { |out| @data_riot.save_fields(out) }
95
+ sections << Snapshot::Keyed.section("#{name}-RIOT2") { |out| @control_riot.save_fields(out) }
96
+ sections << Snapshot::Keyed.section("#{name}-IPCPU") { |out| @ip_cpu.save_fields(out) }
97
+ sections << Snapshot::Keyed.section("#{name}-SASICPU") { |out| @sasi_cpu.save_fields(out) }
98
+ sections << Snapshot::Keyed.section("#{name}-SASIRAM") { |out| @sasi_bus.ram.save_fields(out, "ram") }
99
+ sections << Snapshot::Keyed.section("#{name}-VIA") { |out| @via.save_fields(out) }
100
+ sections << Snapshot::Keyed.section("#{name}-SASI") { |out| @target.save_fields(out) }
101
+ end
102
+
103
+ # The unit from its modules, `input` being its own.
104
+ def load_modules(modules, input, name)
105
+ load_fields(input)
106
+ @common.load_fields(modules.required("#{name}-COMMON"), "ram")
107
+ @data_riot.load_fields(modules.required("#{name}-RIOT1"))
108
+ @control_riot.load_fields(modules.required("#{name}-RIOT2"))
109
+ @ip_cpu.load_fields(modules.required("#{name}-IPCPU"))
110
+ @sasi_cpu.load_fields(modules.required("#{name}-SASICPU"))
111
+ @sasi_bus.ram.load_fields(modules.required("#{name}-SASIRAM"), "ram")
112
+ @via.load_fields(modules.required("#{name}-VIA"))
113
+ @target.load_fields(modules.required("#{name}-SASI"), detached: modules.detached?)
114
+ @via.restored!
115
+ @ip_cpu.restored!
116
+ @sasi_cpu.restored!
117
+ end
118
+ end
119
+
120
+ include KeyedState
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The SASI host's VIA, at $0080, a class of its own so it keeps one
7
+ # type of peripheral.
8
+ class SASIVIA < Chips::VIA
9
+ end
10
+
11
+ # The SASI host's 6502, on the SASIBus.
12
+ class SASICPU
13
+ include Badline::Chips::CPU::Core
14
+ include Engine::IdleCPU
15
+ end
16
+
17
+ # The VIA's lines to the controller (SASITarget), on the board's
18
+ # transceivers:
19
+ #
20
+ # PA0-7 the data bus PB0 SEL out
21
+ # CA1 REQ, rising as it rises PB1 RST out
22
+ # CA2 ACK, pulsed on each PB2 C/D in
23
+ # read or write of ORA PB3 BSY in
24
+ # CB2 the transceivers' PB4 jumper J14: high for four
25
+ # direction heads (D9060), low for six
26
+ # PB6 I/O in
27
+ # PB7 MSG in
28
+ #
29
+ # The ports read and drive each line high while it is asserted, SEL
30
+ # and RST only while PB0 and PB1 are outputs.
31
+ # SEL raised selects the controller with the data bus as the host
32
+ # drives it, and RST raised resets it. ACK falling hands it the byte
33
+ # on the data bus.
34
+ class SASILines
35
+ SEL = 0x01
36
+ RST = 0x02
37
+ CD = 0x04
38
+ BSY = 0x08
39
+ J14 = 0x10
40
+ IO = 0x40
41
+ MSG = 0x80
42
+
43
+ attr_reader :target
44
+ attr_writer :via
45
+
46
+ # `four_heads` is J14.
47
+ def initialize(target, four_heads:)
48
+ @target = target
49
+ @four_heads = four_heads
50
+ @via = nil
51
+ @ack_high = true
52
+ @reset_high = false
53
+ end
54
+
55
+ # The data bus: the controller's byte while it drives it.
56
+ def read_a(_lines)
57
+ target = @target
58
+ target.input? ? target.data : 0xff
59
+ end
60
+
61
+ def read_b(_lines)
62
+ target = @target
63
+ value = 0x23
64
+ value |= CD if target.control?
65
+ value |= BSY if target.busy?
66
+ value |= J14 if @four_heads
67
+ value |= IO if target.input?
68
+ value |= MSG if target.message?
69
+ value
70
+ end
71
+
72
+ # One cycle of the controller, then the host's ACK, RST and SEL to
73
+ # the controller, and REQ to CA1.
74
+ def cycle!
75
+ target = @target
76
+ via = @via
77
+ target.clock
78
+ ack_high = via.ca2_output
79
+ target.acknowledge(via.port_a_output) if @ack_high && !ack_high
80
+ @ack_high = ack_high
81
+ host = via.port_b_output & via.peek(0x82)
82
+ reset_high = host.anybits?(RST)
83
+ target.reset if reset_high && !@reset_high
84
+ @reset_high = reset_high
85
+ target.select(via.port_a_output) if host.anybits?(SEL)
86
+ via.ca1 = target.request?
87
+ end
88
+
89
+ # Everything the lines hold between cycles.
90
+ def state = [@ack_high, @reset_high]
91
+
92
+ def state=(values)
93
+ @ack_high, @reset_high = values
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The SASI host processor's bus on the DOS board (300010), where the
7
+ # 8050's floppy controller sits, with its decode and common RAM:
8
+ #
9
+ # $0000-$007F - the 6810's 128 bytes, mirrored at $0100 for the stack
10
+ # $0080-$008F - the VIA (SASILines)
11
+ # $0400-$13FF - the common RAM, 1 KB in each of the 8050's windows
12
+ # $F800-$FFFF - 300515, the host's program
13
+ #
14
+ # Writes to the ROM and to open bus go nowhere, and a read from open
15
+ # bus finds the last byte on the data bus.
16
+ class SASIBus
17
+ include Addressable
18
+
19
+ attr_reader :common, :ram, :via
20
+ attr_accessor :data
21
+
22
+ # How many writes have changed a byte of the common RAM
23
+ # (Model9060::Idle).
24
+ attr_reader :common_changes
25
+
26
+ # Whether a counter or an interrupt flag has been read since it was
27
+ # last cleared (Model9060::Idle).
28
+ attr_accessor :volatile
29
+
30
+ def initialize(rom:, common:, via:)
31
+ addressable_at(0, length: 2**16)
32
+ @rom = rom
33
+ @common = common
34
+ @via = via
35
+ @ram = Memory.new([], length: 0x80)
36
+ @data = 0
37
+ @volatile = false
38
+ @common_changes = 0
39
+ end
40
+
41
+ def peek(addr)
42
+ @data = if addr >= 0xf800
43
+ @rom.peek(addr & 0x07ff)
44
+ elsif addr < 0x0200
45
+ peek_low(addr)
46
+ elsif addr.between?(0x0400, 0x13ff)
47
+ @common.peek(addr - 0x0400)
48
+ else
49
+ @data
50
+ end
51
+ end
52
+
53
+ def poke(addr, value)
54
+ @data = value
55
+ if addr < 0x0200
56
+ poke_low(addr, value)
57
+ elsif addr.between?(0x0400, 0x13ff)
58
+ poke_common(addr - 0x0400, value)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def peek_low(addr)
65
+ return @ram.peek(addr & 0x7f) if addr.nobits?(0x80)
66
+ return @data if addr.anybits?(0x70)
67
+
68
+ register = addr & 0x0f
69
+ @volatile = true if register.between?(0x04, 0x0a) || register == 0x0d
70
+ @via.peek(0x80 | register)
71
+ end
72
+
73
+ def poke_low(addr, value)
74
+ if addr.nobits?(0x80)
75
+ @ram.poke(addr & 0x7f, value)
76
+ elsif addr.nobits?(0x70)
77
+ @via.poke(0x80 | (addr & 0x0f), value)
78
+ end
79
+ end
80
+
81
+ def poke_common(index, value)
82
+ @common_changes += 1 if @common.peek(index) != value
83
+ @common.poke(index, value)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end