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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +193 -0
- data/CLAUDE.md +19 -7
- data/CONTRIBUTING.md +11 -1
- data/README.md +178 -26
- data/doc/library.md +5 -1
- data/doc/media.md +3 -2
- data/doc/pinned-behaviour.md +169 -1
- data/doc/snapshot-format.md +302 -35
- data/doc/snapshots.md +97 -26
- data/doc/vice.md +344 -0
- data/exe/badline-ruby +5 -0
- data/lib/badline/audio/renderer.rb +4 -8
- data/lib/badline/audio/terminal.rb +34 -2
- data/lib/badline/c128/keyed_state.rb +2 -0
- data/lib/badline/c128.rb +5 -0
- data/lib/badline/c64/bus/fittings.rb +6 -0
- data/lib/badline/c64/keyed_state.rb +2 -0
- data/lib/badline/c64.rb +7 -0
- data/lib/badline/cbm2/keyed_state.rb +4 -0
- data/lib/badline/cbm2/p500.rb +9 -3
- data/lib/badline/cbm2.rb +10 -3
- data/lib/badline/chips/cia/timer.rb +2 -2
- data/lib/badline/chips/cpu/core.rb +29 -4
- data/lib/badline/chips/cpu/instruction_set.rb +1 -1
- data/lib/badline/chips/cpu/interrupts.rb +9 -0
- data/lib/badline/chips/cpu/keyed_state.rb +15 -9
- data/lib/badline/chips/cpu/microcode.rb +10 -0
- data/lib/badline/chips/m6809/arithmetic.rb +140 -0
- data/lib/badline/chips/m6809/bus_steps.rb +223 -0
- data/lib/badline/chips/m6809/core.rb +149 -0
- data/lib/badline/chips/m6809/indexed.rb +82 -0
- data/lib/badline/chips/m6809/interrupts.rb +159 -0
- data/lib/badline/chips/m6809/keyed_state.rb +147 -0
- data/lib/badline/chips/m6809/opcodes.rb +159 -0
- data/lib/badline/chips/m6809/operations.rb +258 -0
- data/lib/badline/chips/m6809/plans.rb +143 -0
- data/lib/badline/chips/m6809/registers.rb +82 -0
- data/lib/badline/chips/m6809/stack.rb +140 -0
- data/lib/badline/chips/m6809.rb +13 -0
- data/lib/badline/chips/riot/timer.rb +14 -3
- data/lib/badline/chips/rriot.rb +8 -3
- data/lib/badline/chips/sid/decimator.rb +1 -1
- data/lib/badline/chips/sid/filter.rb +4 -4
- data/lib/badline/chips/sid/keyed_parts.rb +51 -18
- data/lib/badline/chips/sid/waveform.rb +6 -6
- data/lib/badline/chips/sid.rb +6 -2
- data/lib/badline/chips/vic_i/keyed_parts.rb +12 -2
- data/lib/badline/chips/vic_i/light_pen.rb +89 -0
- data/lib/badline/chips/vic_i.rb +91 -23
- data/lib/badline/data_path.rb +3 -3
- data/lib/badline/drives/ieee_models.rb +54 -0
- data/lib/badline/drives/model1001.rb +37 -0
- data/lib/badline/drives/model1541/sector_reader.rb +20 -3
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551.rb +1 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/drives/model2031.rb +7 -0
- data/lib/badline/drives/model4040/disk.rb +62 -0
- data/lib/badline/drives/model4040/ip_bus.rb +46 -0
- data/lib/badline/drives/model4040/keyed_state.rb +57 -0
- data/lib/badline/drives/model4040/mechanism.rb +195 -0
- data/lib/badline/drives/model4040/parts.rb +20 -0
- data/lib/badline/drives/model4040.rb +195 -0
- data/lib/badline/drives/model8050/board.rb +79 -0
- data/lib/badline/drives/model8050/dos_board.rb +134 -0
- data/lib/badline/drives/model8050/fdc_bus.rb +7 -0
- data/lib/badline/drives/model8050/idle.rb +148 -31
- data/lib/badline/drives/model8050/keyed_state.rb +5 -2
- data/lib/badline/drives/model8050.rb +32 -130
- data/lib/badline/drives/model9060/idle.rb +169 -0
- data/lib/badline/drives/model9060/keyed_state.rb +123 -0
- data/lib/badline/drives/model9060/parts.rb +98 -0
- data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
- data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
- data/lib/badline/drives/model9060/sasi_target.rb +161 -0
- data/lib/badline/drives/model9060.rb +197 -0
- data/lib/badline/frontend/app.rb +8 -0
- data/lib/badline/frontend/boot.rb +3 -2
- data/lib/badline/frontend/controls.rb +62 -6
- data/lib/badline/frontend/keys.rb +36 -0
- data/lib/badline/frontend/kim1_panel.rb +208 -0
- data/lib/badline/frontend/media_slots.rb +8 -1
- data/lib/badline/frontend/menu/drive_types.rb +56 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
- data/lib/badline/frontend/menu/menu_media.rb +42 -12
- data/lib/badline/frontend/menu/menu_pages.rb +97 -2
- data/lib/badline/frontend/menu/menu_ports.rb +38 -1
- data/lib/badline/frontend/menu/pause_menu.rb +5 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -1
- data/lib/badline/frontend/screen.rb +1 -1
- data/lib/badline/frontend/screens.rb +40 -2
- data/lib/badline/frontend/teletype_pane.rb +152 -0
- data/lib/badline/frontend/timeline.rb +31 -2
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/input/joystick.rb +15 -2
- data/lib/badline/kim1/bus.rb +117 -0
- data/lib/badline/kim1/cpu.rb +25 -0
- data/lib/badline/kim1/host_terminal.rb +74 -0
- data/lib/badline/kim1/key_entry.rb +74 -0
- data/lib/badline/kim1/keyed_modules.rb +53 -0
- data/lib/badline/kim1/keyed_state.rb +133 -0
- data/lib/badline/kim1/panel.rb +176 -0
- data/lib/badline/kim1/rriots.rb +28 -0
- data/lib/badline/kim1/sound.rb +29 -0
- data/lib/badline/kim1/tape.rb +118 -0
- data/lib/badline/kim1/teletype.rb +193 -0
- data/lib/badline/kim1.rb +194 -0
- data/lib/badline/machine/ieee_drives.rb +94 -117
- data/lib/badline/machine/keyboard_buffer.rb +20 -3
- data/lib/badline/machine/line_sound.rb +122 -0
- data/lib/badline/machine.rb +49 -15
- data/lib/badline/media/extensions.rb +8 -4
- data/lib/badline/media/kim1_media.rb +150 -0
- data/lib/badline/media/pet_media.rb +3 -1
- data/lib/badline/media/true_drive.rb +52 -45
- data/lib/badline/media/vic20_media.rb +4 -2
- data/lib/badline/media.rb +16 -6
- data/lib/badline/memory.rb +2 -4
- data/lib/badline/options/family.rb +98 -8
- data/lib/badline/options/help.rb +44 -12
- data/lib/badline/options/table.rb +44 -12
- data/lib/badline/options/validation.rb +44 -8
- data/lib/badline/options.rb +27 -8
- data/lib/badline/pet/bus.rb +16 -2
- data/lib/badline/pet/keyboard_ports.rb +9 -2
- data/lib/badline/pet/keyed_state.rb +4 -0
- data/lib/badline/pet/model.rb +11 -2
- data/lib/badline/pet/rom_sets.rb +1 -0
- data/lib/badline/pet/sound.rb +5 -105
- data/lib/badline/pet/superpet/acia.rb +12 -0
- data/lib/badline/pet/superpet/board.rb +221 -0
- data/lib/badline/pet/superpet/dongle.rb +92 -0
- data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
- data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
- data/lib/badline/pet/superpet/m6809.rb +32 -0
- data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
- data/lib/badline/pet/superpet.rb +221 -0
- data/lib/badline/pet.rb +23 -3
- data/lib/badline/plus4/keyed_state.rb +2 -0
- data/lib/badline/plus4.rb +5 -0
- data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
- data/lib/badline/roms/4040/dos-1.2.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
- data/lib/badline/roms/9060/dos-3.0.rom +0 -0
- data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
- data/lib/badline/roms/README +176 -0
- data/lib/badline/roms/kim1/6530-002.rom +0 -0
- data/lib/badline/roms/kim1/6530-003.rom +0 -0
- data/lib/badline/roms/pet/characters-superpet.rom +0 -0
- data/lib/badline/roms/pet/superpet-a000.rom +0 -0
- data/lib/badline/roms/pet/superpet-c000.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000.rom +0 -0
- data/lib/badline/snapshot/container.rb +8 -4
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +35 -10
- data/lib/badline/snapshot/image.rb +19 -7
- data/lib/badline/snapshot/kim1_setup.rb +28 -0
- data/lib/badline/snapshot/pet_setup.rb +27 -11
- data/lib/badline/snapshot/storage_setup.rb +4 -1
- data/lib/badline/snapshot/vic20_setup.rb +11 -7
- data/lib/badline/snapshot/vice/pet.rb +14 -7
- data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
- data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
- data/lib/badline/snapshot/vice/pet_dongle.rb +72 -0
- data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
- data/lib/badline/snapshot/vice/pet_mem.rb +55 -5
- data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
- data/lib/badline/snapshot/vice/vic20.rb +70 -0
- data/lib/badline/snapshot/vice/vic20_cpu.rb +61 -0
- data/lib/badline/snapshot/vice/vic20_mem.rb +85 -0
- data/lib/badline/snapshot/vice/vic20_restore.rb +124 -0
- data/lib/badline/snapshot/vice/vic20_vici.rb +200 -0
- data/lib/badline/snapshot/vice.rb +1 -0
- data/lib/badline/snapshot.rb +8 -4
- data/lib/badline/status.rb +30 -30
- data/lib/badline/storage/d67_image.rb +55 -0
- data/lib/badline/storage/d90_image.rb +180 -0
- data/lib/badline/storage/kim_tape.rb +254 -0
- data/lib/badline/storage/papertape.rb +90 -0
- data/lib/badline/storage.rb +4 -0
- data/lib/badline/support/kim1_checkpoint.rb +41 -0
- data/lib/badline/support/superpet_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/keyed_modules.rb +6 -3
- data/lib/badline/vic20/keyed_state.rb +6 -4
- data/lib/badline/vic20/port_wiring.rb +11 -0
- data/lib/badline/vic20.rb +30 -12
- data/lib/badline.rb +1 -0
- 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
|