badline 0.17.0 → 0.19.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +193 -0
  3. data/CLAUDE.md +19 -7
  4. data/CONTRIBUTING.md +11 -1
  5. data/README.md +178 -26
  6. data/doc/library.md +5 -1
  7. data/doc/media.md +3 -2
  8. data/doc/pinned-behaviour.md +169 -1
  9. data/doc/snapshot-format.md +302 -35
  10. data/doc/snapshots.md +97 -26
  11. data/doc/vice.md +344 -0
  12. data/exe/badline-ruby +5 -0
  13. data/lib/badline/audio/renderer.rb +4 -8
  14. data/lib/badline/audio/terminal.rb +34 -2
  15. data/lib/badline/c128/keyed_state.rb +2 -0
  16. data/lib/badline/c128.rb +5 -0
  17. data/lib/badline/c64/bus/fittings.rb +6 -0
  18. data/lib/badline/c64/keyed_state.rb +2 -0
  19. data/lib/badline/c64.rb +7 -0
  20. data/lib/badline/cbm2/keyed_state.rb +4 -0
  21. data/lib/badline/cbm2/p500.rb +9 -3
  22. data/lib/badline/cbm2.rb +10 -3
  23. data/lib/badline/chips/cia/timer.rb +2 -2
  24. data/lib/badline/chips/cpu/core.rb +29 -4
  25. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  26. data/lib/badline/chips/cpu/interrupts.rb +9 -0
  27. data/lib/badline/chips/cpu/keyed_state.rb +15 -9
  28. data/lib/badline/chips/cpu/microcode.rb +10 -0
  29. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  30. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  31. data/lib/badline/chips/m6809/core.rb +149 -0
  32. data/lib/badline/chips/m6809/indexed.rb +82 -0
  33. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  34. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  35. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  36. data/lib/badline/chips/m6809/operations.rb +258 -0
  37. data/lib/badline/chips/m6809/plans.rb +143 -0
  38. data/lib/badline/chips/m6809/registers.rb +82 -0
  39. data/lib/badline/chips/m6809/stack.rb +140 -0
  40. data/lib/badline/chips/m6809.rb +13 -0
  41. data/lib/badline/chips/riot/timer.rb +14 -3
  42. data/lib/badline/chips/rriot.rb +8 -3
  43. data/lib/badline/chips/sid/decimator.rb +1 -1
  44. data/lib/badline/chips/sid/filter.rb +4 -4
  45. data/lib/badline/chips/sid/keyed_parts.rb +51 -18
  46. data/lib/badline/chips/sid/waveform.rb +6 -6
  47. data/lib/badline/chips/sid.rb +6 -2
  48. data/lib/badline/chips/vic_i/keyed_parts.rb +12 -2
  49. data/lib/badline/chips/vic_i/light_pen.rb +89 -0
  50. data/lib/badline/chips/vic_i.rb +91 -23
  51. data/lib/badline/data_path.rb +3 -3
  52. data/lib/badline/drives/ieee_models.rb +54 -0
  53. data/lib/badline/drives/model1001.rb +37 -0
  54. data/lib/badline/drives/model1541/sector_reader.rb +20 -3
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551.rb +1 -0
  57. data/lib/badline/drives/model1571.rb +1 -0
  58. data/lib/badline/drives/model1581.rb +1 -0
  59. data/lib/badline/drives/model2031.rb +7 -0
  60. data/lib/badline/drives/model4040/disk.rb +62 -0
  61. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  62. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  63. data/lib/badline/drives/model4040/mechanism.rb +195 -0
  64. data/lib/badline/drives/model4040/parts.rb +20 -0
  65. data/lib/badline/drives/model4040.rb +195 -0
  66. data/lib/badline/drives/model8050/board.rb +79 -0
  67. data/lib/badline/drives/model8050/dos_board.rb +134 -0
  68. data/lib/badline/drives/model8050/fdc_bus.rb +7 -0
  69. data/lib/badline/drives/model8050/idle.rb +148 -31
  70. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  71. data/lib/badline/drives/model8050.rb +32 -130
  72. data/lib/badline/drives/model9060/idle.rb +169 -0
  73. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  74. data/lib/badline/drives/model9060/parts.rb +98 -0
  75. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  76. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  77. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  78. data/lib/badline/drives/model9060.rb +197 -0
  79. data/lib/badline/frontend/app.rb +8 -0
  80. data/lib/badline/frontend/boot.rb +3 -2
  81. data/lib/badline/frontend/controls.rb +62 -6
  82. data/lib/badline/frontend/keys.rb +36 -0
  83. data/lib/badline/frontend/kim1_panel.rb +208 -0
  84. data/lib/badline/frontend/media_slots.rb +8 -1
  85. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  86. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  87. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  88. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  89. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  90. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  91. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  92. data/lib/badline/frontend/screen.rb +1 -1
  93. data/lib/badline/frontend/screens.rb +40 -2
  94. data/lib/badline/frontend/teletype_pane.rb +152 -0
  95. data/lib/badline/frontend/timeline.rb +31 -2
  96. data/lib/badline/frontend.rb +3 -0
  97. data/lib/badline/input/joystick.rb +15 -2
  98. data/lib/badline/kim1/bus.rb +117 -0
  99. data/lib/badline/kim1/cpu.rb +25 -0
  100. data/lib/badline/kim1/host_terminal.rb +74 -0
  101. data/lib/badline/kim1/key_entry.rb +74 -0
  102. data/lib/badline/kim1/keyed_modules.rb +53 -0
  103. data/lib/badline/kim1/keyed_state.rb +133 -0
  104. data/lib/badline/kim1/panel.rb +176 -0
  105. data/lib/badline/kim1/rriots.rb +28 -0
  106. data/lib/badline/kim1/sound.rb +29 -0
  107. data/lib/badline/kim1/tape.rb +118 -0
  108. data/lib/badline/kim1/teletype.rb +193 -0
  109. data/lib/badline/kim1.rb +194 -0
  110. data/lib/badline/machine/ieee_drives.rb +94 -117
  111. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  112. data/lib/badline/machine/line_sound.rb +122 -0
  113. data/lib/badline/machine.rb +49 -15
  114. data/lib/badline/media/extensions.rb +8 -4
  115. data/lib/badline/media/kim1_media.rb +150 -0
  116. data/lib/badline/media/pet_media.rb +3 -1
  117. data/lib/badline/media/true_drive.rb +52 -45
  118. data/lib/badline/media/vic20_media.rb +4 -2
  119. data/lib/badline/media.rb +16 -6
  120. data/lib/badline/memory.rb +2 -4
  121. data/lib/badline/options/family.rb +98 -8
  122. data/lib/badline/options/help.rb +44 -12
  123. data/lib/badline/options/table.rb +44 -12
  124. data/lib/badline/options/validation.rb +44 -8
  125. data/lib/badline/options.rb +27 -8
  126. data/lib/badline/pet/bus.rb +16 -2
  127. data/lib/badline/pet/keyboard_ports.rb +9 -2
  128. data/lib/badline/pet/keyed_state.rb +4 -0
  129. data/lib/badline/pet/model.rb +11 -2
  130. data/lib/badline/pet/rom_sets.rb +1 -0
  131. data/lib/badline/pet/sound.rb +5 -105
  132. data/lib/badline/pet/superpet/acia.rb +12 -0
  133. data/lib/badline/pet/superpet/board.rb +221 -0
  134. data/lib/badline/pet/superpet/dongle.rb +92 -0
  135. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  136. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  137. data/lib/badline/pet/superpet/m6809.rb +32 -0
  138. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  139. data/lib/badline/pet/superpet.rb +221 -0
  140. data/lib/badline/pet.rb +23 -3
  141. data/lib/badline/plus4/keyed_state.rb +2 -0
  142. data/lib/badline/plus4.rb +5 -0
  143. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  144. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  145. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  146. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  147. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  148. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  149. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  150. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  151. data/lib/badline/roms/README +176 -0
  152. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  153. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  154. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  155. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  156. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  157. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  158. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  159. data/lib/badline/snapshot/container.rb +8 -4
  160. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  161. data/lib/badline/snapshot/field_doc.rb +35 -10
  162. data/lib/badline/snapshot/image.rb +19 -7
  163. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  164. data/lib/badline/snapshot/pet_setup.rb +27 -11
  165. data/lib/badline/snapshot/storage_setup.rb +4 -1
  166. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  167. data/lib/badline/snapshot/vice/pet.rb +14 -7
  168. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  169. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  170. data/lib/badline/snapshot/vice/pet_dongle.rb +72 -0
  171. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  172. data/lib/badline/snapshot/vice/pet_mem.rb +55 -5
  173. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  174. data/lib/badline/snapshot/vice/vic20.rb +70 -0
  175. data/lib/badline/snapshot/vice/vic20_cpu.rb +61 -0
  176. data/lib/badline/snapshot/vice/vic20_mem.rb +85 -0
  177. data/lib/badline/snapshot/vice/vic20_restore.rb +124 -0
  178. data/lib/badline/snapshot/vice/vic20_vici.rb +200 -0
  179. data/lib/badline/snapshot/vice.rb +1 -0
  180. data/lib/badline/snapshot.rb +8 -4
  181. data/lib/badline/status.rb +30 -30
  182. data/lib/badline/storage/d67_image.rb +55 -0
  183. data/lib/badline/storage/d90_image.rb +180 -0
  184. data/lib/badline/storage/kim_tape.rb +254 -0
  185. data/lib/badline/storage/papertape.rb +90 -0
  186. data/lib/badline/storage.rb +4 -0
  187. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  188. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  189. data/lib/badline/version.rb +1 -1
  190. data/lib/badline/vic20/keyed_modules.rb +6 -3
  191. data/lib/badline/vic20/keyed_state.rb +6 -4
  192. data/lib/badline/vic20/port_wiring.rb +11 -0
  193. data/lib/badline/vic20.rb +30 -12
  194. data/lib/badline.rb +1 -0
  195. metadata +85 -1
@@ -0,0 +1,169 @@
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!). Asleep, a host cycle
23
+ # only counts towards the one planned for the counter
24
+ # (DOSBoard#plan_sleep), and a moved line wakes the unit as the
25
+ # controller pushes it (DOSBoard#host_written!).
26
+ module Idle
27
+ IP_LOOP = 0xc131
28
+ SASI_LOOP = 0xf8f1
29
+
30
+ # Whether the unit may sleep through its idle loops.
31
+ attr_reader :idle_skip
32
+
33
+ def idle_skip=(on)
34
+ settle!
35
+ @idle_skip = on
36
+ end
37
+
38
+ def asleep? = @asleep
39
+
40
+ # Whether the unit owes cycles.
41
+ def dozing? = @asleep
42
+
43
+ # Brings the unit up to the present, cycle for cycle, and drops the
44
+ # passes it found, since whoever called may change it.
45
+ def settle!
46
+ wake! if @asleep
47
+ forget_passes
48
+ end
49
+
50
+ private
51
+
52
+ def init_idle
53
+ @idle_skip = true
54
+ @asleep = false
55
+ @owed = 0
56
+ @budget = 0
57
+ @planned = false
58
+ @slept = 0
59
+ @wake_at = 0
60
+ forget_passes
61
+ end
62
+
63
+ def forget_passes
64
+ @ip_mark = nil
65
+ @sasi_mark = nil
66
+ @ip_pass = nil
67
+ @sasi_pass = nil
68
+ end
69
+
70
+ def owe_cycle
71
+ @owed += 1
72
+ wake! if @owed >= @budget
73
+ end
74
+
75
+ def watch_loops
76
+ ip = @ip_cpu
77
+ @ip_pass = nil if ip.irq
78
+ @sasi_pass = nil if @sasi_cpu.irq
79
+ ip_pass_found if ip.program_counter == IP_LOOP && ip.boundary?
80
+ host = @sasi_cpu
81
+ sasi_pass_found if host.program_counter == SASI_LOOP && host.boundary?
82
+ end
83
+
84
+ def ip_pass_found
85
+ state = ip_state
86
+ @ip_pass = repeated(@ip_mark, state, @ip_cpu, @ip_bus.volatile)
87
+ @ip_mark = [state, @ip_cpu.cycles, @ip_cpu.instructions]
88
+ @ip_bus.volatile = false
89
+ fall_asleep if @ip_pass && @sasi_pass
90
+ end
91
+
92
+ def sasi_pass_found
93
+ state = sasi_state
94
+ @sasi_pass = repeated(@sasi_mark, state, @sasi_cpu, @sasi_bus.volatile)
95
+ @sasi_mark = [state, @sasi_cpu.cycles, @sasi_cpu.instructions]
96
+ @sasi_bus.volatile = false
97
+ fall_asleep if @ip_pass && @sasi_pass
98
+ end
99
+
100
+ def repeated(mark, state, cpu, volatile)
101
+ return nil if mark.nil? || volatile || mark[0] != state
102
+
103
+ [cpu.cycles - mark[1], cpu.instructions - mark[2]]
104
+ end
105
+
106
+ def fall_asleep
107
+ target = @target
108
+ return unless target.quiet? && !target.busy? && !@ip_cpu.irq && !@sasi_cpu.irq
109
+
110
+ budget = quiet_cycles
111
+ return if budget < 2
112
+
113
+ @asleep = true
114
+ @owed = 0
115
+ @budget = budget
116
+ end
117
+
118
+ def quiet_cycles
119
+ [@data_riot.quiet_cycles, @control_riot.quiet_cycles, @via.quiet_cycles].min
120
+ end
121
+
122
+ # The budget is spent (DOSBoard#slept_through?).
123
+ def doze = wake!
124
+
125
+ # Runs the cycles owed: the counters in bulk, then each processor's
126
+ # whole passes and the rest of a pass, and on a host cycle that ran
127
+ # past the budget, the rest cycle by cycle.
128
+ def wake!
129
+ owe_slept
130
+ owed = @owed
131
+ @asleep = false
132
+ @owed = 0
133
+ quiet = [owed, @budget].min
134
+ skip_passes(quiet) if quiet.positive?
135
+ (owed - quiet).times { @asleep ? owe_cycle : run_cycle }
136
+ end
137
+
138
+ def skip_passes(cycles)
139
+ @data_riot.fast_forward(cycles)
140
+ @control_riot.fast_forward(cycles)
141
+ @via.fast_forward(cycles)
142
+ @target.fast_forward(cycles)
143
+ skip(@ip_cpu, @ip_pass, @ip_mark, cycles)
144
+ skip(@sasi_cpu, @sasi_pass, @sasi_mark, cycles)
145
+ @cycles += cycles
146
+ end
147
+
148
+ def skip(cpu, pass, mark, owed)
149
+ passes = owed / pass[0]
150
+ cpu.fast_forward(passes * pass[0], passes * pass[1])
151
+ mark[1] += passes * pass[0]
152
+ mark[2] += passes * pass[1]
153
+ (owed - (passes * pass[0])).times { cpu.cycle! }
154
+ end
155
+
156
+ def ip_state
157
+ [*@ip_cpu.idle_state, *@data_riot.idle_state, *@control_riot.idle_state, @ip_bus.data, common_changes]
158
+ end
159
+
160
+ def sasi_state
161
+ [*@sasi_cpu.idle_state, *@via.idle_state, *@lines.state, *@sasi_bus.ram.read(0, 0x80), @sasi_bus.data,
162
+ common_changes]
163
+ end
164
+
165
+ def common_changes = @ip_bus.common_changes + @sasi_bus.common_changes
166
+ end
167
+ end
168
+ end
169
+ 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
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The commands the DOS board's SASI ROM (300515) sends, against the
7
+ # disk's blocks. A command's second to fourth bytes hold the drive
8
+ # (bits 5-7) and the block's LBA, its fifth the blocks to move.
9
+ #
10
+ # | Op | Command | Data |
11
+ # | --- | --- | --- |
12
+ # | $00 | Test Unit Ready | |
13
+ # | $01 | Recalibrate: seek to cylinder 0 | |
14
+ # | $03 | Request Sense: the last command's error and its LBA | 4 in |
15
+ # | $04 | Format Unit | |
16
+ # | $08 | Read | 256 a block in |
17
+ # | $0A | Write | 256 a block out |
18
+ # | $0B | Seek | |
19
+ # | $0C | Vendor: read the controller's block buffer (unverified) | 256 in |
20
+ # | $0E | Vendor: write the controller's block buffer (unverified) | 256 out |
21
+ # | $E3 | Vendor: four zero bytes from a drive that is there (unverified) | 4 in |
22
+ # | $C0 | Vendor: set the geometry (unverified) | |
23
+ #
24
+ # DOS 3.0 sends $E3 before it first reads a drive, and leaves a drive
25
+ # alone that fails it as DRIVE NOT READY, as drive 1 does here.
26
+ #
27
+ # $C0's fifth and sixth bytes are taken as the last head and the last
28
+ # sector of a head, as the ROM sends them (5 and 31 on a D9090 at
29
+ # boot). Its other bytes aren't used.
30
+ #
31
+ # An error's sense byte has its class in bits 4-5 and its code in bits
32
+ # 0-3, with bit 7 set when the LBA after it names the block. The ROM
33
+ # reads that byte from the message phase after a check condition,
34
+ # and the controller sends it there.
35
+ #
36
+ # Format Unit erases the disk at once and puts the controller in a
37
+ # fast mode, where Read, Write and Seek take no seek or rotation time,
38
+ # for the DOS's passes over the whole disk. Any other command ends
39
+ # it. Outside it a seek takes 3 ms a cylinder and 15 ms to settle, and
40
+ # the disk turns 32 blocks in 16.7 ms.
41
+ module SASICommands
42
+ DRIVE_NOT_READY = 0x04
43
+ WRITE_FAULT = 0x03
44
+ INVALID_COMMAND = 0x20
45
+ ILLEGAL_ADDRESS = 0xa1
46
+
47
+ BLOCK = 256
48
+ SECTOR_CYCLES = 521
49
+ STEP_CYCLES = 3_000
50
+ SETTLE_CYCLES = 15_000
51
+
52
+ DISK_COMMANDS = [0x00, 0x01, 0x04, 0x08, 0x0a, 0x0b, 0xe3].freeze
53
+ FAST_COMMANDS = [0x08, 0x0a, 0x0b].freeze
54
+
55
+ attr_reader :heads, :sectors, :cylinder
56
+
57
+ def fast? = @fast
58
+
59
+ private
60
+
61
+ def reset_geometry
62
+ @heads = 4
63
+ @sectors = 32
64
+ @block_buffer = Array.new(BLOCK, 0)
65
+ end
66
+
67
+ def execute
68
+ @fast = false unless FAST_COMMANDS.include?(@command[0])
69
+ case @command[0]
70
+ when 0x03 then enter(SASITarget::DATA_IN, @last_sense.dup, 1)
71
+ when 0x0c then enter(SASITarget::DATA_IN, @block_buffer.dup, 1)
72
+ when 0x0e then enter(SASITarget::DATA_OUT, Array.new(BLOCK, 0), 1)
73
+ when 0xc0 then set_geometry
74
+ else disk_command
75
+ end
76
+ end
77
+
78
+ def disk_command
79
+ return refuse(INVALID_COMMAND) unless DISK_COMMANDS.include?(@command[0])
80
+ return refuse(DRIVE_NOT_READY) unless @disk && (@command[1] >> 5).zero?
81
+
82
+ case @command[0]
83
+ when 0x00 then report
84
+ when 0x01 then report(travel(0))
85
+ when 0x04 then format_unit
86
+ when 0xe3 then enter(SASITarget::DATA_IN, [0, 0, 0, 0], 1)
87
+ else block_command
88
+ end
89
+ end
90
+
91
+ def block_command
92
+ return refuse(ILLEGAL_ADDRESS) unless @disk.blocks?(lba, @command[0] == 0x0b ? 1 : count)
93
+
94
+ case @command[0]
95
+ when 0x08 then read
96
+ when 0x0a then enter(SASITarget::DATA_OUT, Array.new(count * BLOCK, 0), 1)
97
+ else report(travel(cylinder_of(lba)))
98
+ end
99
+ end
100
+
101
+ def lba = ((@command[1] & 0x1f) << 16) | (@command[2] << 8) | @command[3]
102
+
103
+ def count = @command[4].zero? ? 256 : @command[4]
104
+
105
+ def cylinder_of(block) = block / (@heads * @sectors)
106
+
107
+ def set_geometry
108
+ @heads = @command[4] + 1
109
+ @sectors = @command[5] + 1
110
+ report
111
+ end
112
+
113
+ def read
114
+ bytes = @disk.read_blocks(lba, count)
115
+ @block_buffer = bytes.last(BLOCK)
116
+ enter(SASITarget::DATA_IN, bytes, access_cycles)
117
+ end
118
+
119
+ def data_received
120
+ if @command[0] == 0x0e
121
+ @block_buffer = @buffer.dup
122
+ report
123
+ else
124
+ @writing = true
125
+ @delay = access_cycles
126
+ end
127
+ end
128
+
129
+ def store_buffer
130
+ @disk.write_blocks(lba, @buffer)
131
+ @block_buffer = @buffer.last(BLOCK)
132
+ report
133
+ rescue Storage::WriteError
134
+ refuse(WRITE_FAULT)
135
+ end
136
+
137
+ def format_unit
138
+ blocks = @heads * @sectors
139
+ (0...@disk.block_count).step(blocks) do |first|
140
+ @disk.write_blocks(first, Array.new([blocks, @disk.block_count - first].min * BLOCK, 0))
141
+ end
142
+ @fast = true
143
+ report
144
+ rescue Storage::WriteError
145
+ refuse(WRITE_FAULT)
146
+ end
147
+
148
+ # The seek to the block's cylinder, the turn until the block comes
149
+ # under the head, and the blocks passing under it.
150
+ def access_cycles
151
+ return 1 if @fast
152
+
153
+ seek = travel(cylinder_of(lba))
154
+ slot = ((@cycle + seek) / SECTOR_CYCLES) % @sectors
155
+ seek + ((((lba % @sectors) - slot) % @sectors) * SECTOR_CYCLES) + (count * SECTOR_CYCLES)
156
+ end
157
+
158
+ def travel(target)
159
+ steps = (target - @cylinder).abs
160
+ @cylinder = target
161
+ return 1 if @fast || steps.zero?
162
+
163
+ (steps * STEP_CYCLES) + SETTLE_CYCLES
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end