badline 0.14.0 → 0.15.1
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 +86 -0
- data/CLAUDE.md +22 -5
- data/README.md +101 -11
- data/doc/media.md +19 -0
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +244 -46
- data/doc/snapshots.md +18 -1
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +4 -26
- data/lib/badline/c128/banks.rb +1 -3
- data/lib/badline/c128/bus.rb +19 -1
- data/lib/badline/c128/mmu_pages.rb +7 -0
- data/lib/badline/c64/bus/extra_sids.rb +80 -0
- data/lib/badline/c64/bus.rb +1 -0
- data/lib/badline/c64/cartridge/action_replay.rb +6 -2
- data/lib/badline/c64/cartridge/comal80.rb +1 -0
- data/lib/badline/c64/cartridge/dinamic.rb +3 -1
- data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
- data/lib/badline/c64/cartridge/fun_play.rb +2 -1
- data/lib/badline/c64/cartridge/game_system.rb +2 -1
- data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
- data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
- data/lib/badline/c64/cartridge/ocean.rb +1 -0
- data/lib/badline/c64/cartridge/pagefox.rb +1 -0
- data/lib/badline/c64/cartridge/rgcd.rb +2 -1
- data/lib/badline/c64/cartridge/super_games.rb +3 -2
- data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
- data/lib/badline/c64/cartridge.rb +3 -3
- data/lib/badline/c64/keyed_modules.rb +6 -2
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/ted/beam.rb +13 -14
- data/lib/badline/chips/ted/fetches.rb +199 -0
- data/lib/badline/chips/ted/keyed_state.rb +156 -0
- data/lib/badline/chips/ted/modes.rb +134 -0
- data/lib/badline/chips/ted/palette.rb +43 -0
- data/lib/badline/chips/ted/picture.rb +182 -0
- data/lib/badline/chips/ted/sound/output.rb +86 -0
- data/lib/badline/chips/ted/sound.rb +284 -0
- data/lib/badline/chips/ted.rb +53 -6
- data/lib/badline/chips/tia.rb +28 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +14 -4
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/drives/engine/core.rb +6 -14
- data/lib/badline/drives/engine/idle.rb +42 -4
- data/lib/badline/drives/engine/idle_cpu.rb +18 -0
- data/lib/badline/drives/engine/serial_connection.rb +25 -0
- data/lib/badline/drives/engine/sleep.rb +1 -0
- data/lib/badline/drives/engine.rb +1 -0
- data/lib/badline/drives/model1541/byte_ready.rb +19 -3
- data/lib/badline/drives/model1541/mechanism.rb +10 -1
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551/bus.rb +138 -0
- data/lib/badline/drives/model1551/cable.rb +104 -0
- data/lib/badline/drives/model1551/cpu.rb +14 -0
- data/lib/badline/drives/model1551/keyed_state.rb +65 -0
- data/lib/badline/drives/model1551/port.rb +74 -0
- data/lib/badline/drives/model1551/tia.rb +92 -0
- data/lib/badline/drives/model1551.rb +170 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/frontend/boot.rb +23 -2
- data/lib/badline/frontend/controls.rb +7 -3
- data/lib/badline/frontend/keys.rb +14 -2
- data/lib/badline/frontend/media_slots.rb +11 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
- data/lib/badline/frontend/menu/menu_media.rb +102 -14
- data/lib/badline/frontend/menu/menu_pages.rb +16 -11
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/frontend/screen.rb +33 -12
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/sound.rb +4 -4
- data/lib/badline/frontend/timeline.rb +19 -6
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +30 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +32 -6
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +29 -4
- data/lib/badline/options/validation.rb +66 -4
- data/lib/badline/options.rb +71 -11
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +245 -46
- data/lib/badline/plus4/cassette_sense.rb +21 -0
- data/lib/badline/plus4/keyboard_port.rb +35 -10
- data/lib/badline/plus4/keyed_modules.rb +116 -0
- data/lib/badline/plus4/keyed_parts.rb +172 -0
- data/lib/badline/plus4/keyed_state.rb +44 -0
- data/lib/badline/plus4/ram_expansion.rb +145 -0
- data/lib/badline/plus4/sid_card.rb +95 -0
- data/lib/badline/plus4/sound_mix.rb +51 -0
- data/lib/badline/plus4/tcbm_drive.rb +120 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +184 -26
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
- data/lib/badline/snapshot/field_doc.rb +48 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +81 -0
- data/lib/badline/snapshot.rb +9 -4
- data/lib/badline/storage/tap.rb +21 -12
- data/lib/badline/support/checkpoint.rb +3 -2
- data/lib/badline/support/plus4_checkpoint.rb +42 -0
- data/lib/badline/version.rb +1 -1
- metadata +29 -1
data/lib/badline/chips/tia.rb
CHANGED
|
@@ -9,20 +9,28 @@ module Badline
|
|
|
9
9
|
#
|
|
10
10
|
# A pin read as an output returns what the port drives, and one read as
|
|
11
11
|
# an input returns what the other side drives, #inputs, per port.
|
|
12
|
+
#
|
|
13
|
+
# A cable plugged in (Drives::Model1551::Cable) hears every write and
|
|
14
|
+
# reset, which can move the pins it carries.
|
|
12
15
|
class TIA
|
|
13
16
|
attr_reader :ports, :directions
|
|
14
17
|
|
|
15
18
|
# What the other side drives onto each port's pins.
|
|
16
19
|
attr_reader :inputs
|
|
17
20
|
|
|
21
|
+
# The cable to the other side, or nil.
|
|
22
|
+
attr_accessor :cable
|
|
23
|
+
|
|
18
24
|
def initialize
|
|
19
25
|
@inputs = [0, 0, 0]
|
|
26
|
+
@cable = nil
|
|
20
27
|
reset!
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
def reset!
|
|
24
31
|
@ports = [0, 0, 0]
|
|
25
32
|
@directions = [0, 0, 0]
|
|
33
|
+
@cable&.moved
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def peek(addr)
|
|
@@ -43,6 +51,26 @@ module Badline
|
|
|
43
51
|
else
|
|
44
52
|
@ports[register] = value
|
|
45
53
|
end
|
|
54
|
+
@cable&.moved
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The levels on +port+'s pins as the chip leaves them: an output's
|
|
58
|
+
# level, and 1 for an input, which the chip doesn't drive.
|
|
59
|
+
def output(port)
|
|
60
|
+
direction = @directions[port]
|
|
61
|
+
(@ports[port] & direction) | (~direction & 0xff)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The ports, their directions and what the other side drives, in a
|
|
65
|
+
# keyed snapshot module (Snapshot::Keyed, BL-TIA1 and BL-TIA2).
|
|
66
|
+
def save_fields(out)
|
|
67
|
+
out.ints("ports", @ports).ints("directions", @directions).ints("inputs", @inputs)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load_fields(input)
|
|
71
|
+
input.ints_into("ports", @ports)
|
|
72
|
+
input.ints_into("directions", @directions)
|
|
73
|
+
input.ints_into("inputs", @inputs)
|
|
46
74
|
end
|
|
47
75
|
end
|
|
48
76
|
end
|
|
@@ -43,7 +43,10 @@ module Badline
|
|
|
43
43
|
# register back at the value it held when the line started.
|
|
44
44
|
def rewind
|
|
45
45
|
@values.length.times { |reg| @values[reg] = @registers[reg] }
|
|
46
|
-
@seed.
|
|
46
|
+
@seed.each_index do |reg|
|
|
47
|
+
value = @seed[reg]
|
|
48
|
+
@values[reg] = value unless value.nil?
|
|
49
|
+
end
|
|
47
50
|
@cursor = 0
|
|
48
51
|
@next_x = @entries.empty? ? Float::INFINITY : @entries[0][0]
|
|
49
52
|
end
|
|
@@ -4,13 +4,18 @@ module Badline
|
|
|
4
4
|
module Drives
|
|
5
5
|
class Datasette
|
|
6
6
|
# The datasette's fields in a keyed snapshot module (Snapshot::Keyed,
|
|
7
|
-
# BL-TAPE): the keys, the motor, the countdown to the next pulse,
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# BL-TAPE): the keys, the motor, the countdown to the next pulse,
|
|
8
|
+
# whether it records, the cycles since the write line's last edge and
|
|
9
|
+
# that line's level, and a tape in it under tape.*: its path, its
|
|
10
|
+
# bytes and how far it has played. A tape that isn't in leaves tape.* out. A datasette that
|
|
11
|
+
# times the read line's level adds when in the countdown the line
|
|
12
|
+
# rises, and whether a recording's low half wave is on the tape. The
|
|
13
|
+
# sense and flag handlers don't fire on load.
|
|
11
14
|
module KeyedState
|
|
12
15
|
def save_fields(out)
|
|
13
16
|
out.bool("playing", @playing).bool("motor", @motor).int("countdown", @countdown)
|
|
17
|
+
out.bool("recording", @recording).int("since", @since).bool("write_high", @write_high)
|
|
18
|
+
out.int("rise_at", @rise_at).bool("low_recorded", @low_recorded) if @level
|
|
14
19
|
tape = @tape
|
|
15
20
|
return out unless tape
|
|
16
21
|
|
|
@@ -23,6 +28,11 @@ module Badline
|
|
|
23
28
|
@playing = input.bool("playing", @playing)
|
|
24
29
|
@motor = input.bool("motor", @motor)
|
|
25
30
|
@countdown = input.int("countdown", @countdown)
|
|
31
|
+
@recording = input.bool("recording", @recording)
|
|
32
|
+
@since = input.int("since", @since)
|
|
33
|
+
@write_high = input.bool("write_high", @write_high)
|
|
34
|
+
@rise_at = input.int("rise_at", @rise_at)
|
|
35
|
+
@low_recorded = input.bool("low_recorded", @low_recorded)
|
|
26
36
|
bytes = input.bytes("tape.bytes", "".b).bytes
|
|
27
37
|
return @tape = nil if bytes.empty?
|
|
28
38
|
|
|
@@ -27,6 +27,7 @@ module Badline
|
|
|
27
27
|
@motor = false
|
|
28
28
|
@countdown = 0
|
|
29
29
|
@rise_at = 0
|
|
30
|
+
@low_recorded = false
|
|
30
31
|
@flag_handler = nil
|
|
31
32
|
@sense_handler = nil
|
|
32
33
|
end
|
|
@@ -78,6 +79,7 @@ module Badline
|
|
|
78
79
|
press(true)
|
|
79
80
|
@recording = true
|
|
80
81
|
@since = 0
|
|
82
|
+
@low_recorded = false
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
def recording? = @recording
|
|
@@ -85,14 +87,21 @@ module Badline
|
|
|
85
87
|
# The machine's tape write line. While the deck records with its motor
|
|
86
88
|
# running, each rising edge ends a pulse on the tape, as long as the
|
|
87
89
|
# time since the edge before, and plays back as a falling edge on the
|
|
88
|
-
# read line.
|
|
90
|
+
# read line. A datasette built with +level: true+ records a version 2
|
|
91
|
+
# tape's half waves instead: a rising edge ends the low half and a
|
|
92
|
+
# falling edge the high half after it, so the tape plays back the
|
|
93
|
+
# levels it recorded (#read_high?).
|
|
89
94
|
def write_line=(high)
|
|
90
|
-
|
|
95
|
+
edge = high != @write_high
|
|
91
96
|
@write_high = high
|
|
92
|
-
return unless
|
|
97
|
+
return unless edge && @recording && running?
|
|
93
98
|
|
|
94
|
-
@tape.
|
|
95
|
-
|
|
99
|
+
if @level && @tape.half_waves?
|
|
100
|
+
record_half_wave(high)
|
|
101
|
+
elsif high
|
|
102
|
+
@tape.record_pulse(@since)
|
|
103
|
+
@since = 0
|
|
104
|
+
end
|
|
96
105
|
end
|
|
97
106
|
|
|
98
107
|
# Sets the write line's level without recording an edge, as a
|
|
@@ -153,6 +162,14 @@ module Badline
|
|
|
153
162
|
@countdown = first + @rise_at
|
|
154
163
|
end
|
|
155
164
|
|
|
165
|
+
# A high half wave goes on the tape only after the low half before it,
|
|
166
|
+
# so the tape starts with a whole pulse.
|
|
167
|
+
def record_half_wave(rose)
|
|
168
|
+
@tape.record_half_wave(@since) if rose || @low_recorded
|
|
169
|
+
@low_recorded = rose
|
|
170
|
+
@since = 0
|
|
171
|
+
end
|
|
172
|
+
|
|
156
173
|
def press(down)
|
|
157
174
|
return if down == @playing || !@connected
|
|
158
175
|
|
|
@@ -24,8 +24,11 @@ module Badline
|
|
|
24
24
|
# sleeps through them too (see Orbit).
|
|
25
25
|
#
|
|
26
26
|
# A model includes Core and builds its parts, sets @clock_hz and
|
|
27
|
-
# @idle_loop, and runs one drive cycle in step.
|
|
28
|
-
#
|
|
27
|
+
# @idle_loop, and runs one drive cycle in step. Its @serial_port faces
|
|
28
|
+
# the host: it says whether the host moved the lines the drive
|
|
29
|
+
# listens to (atn_moved?) and takes them for the next host cycle
|
|
30
|
+
# (latch_host), as the serial port does for ATN on the serial bus
|
|
31
|
+
# (SerialConnection) and the TCBM cable does on the 1551. The model
|
|
29
32
|
# answers Idle's and Orbit's questions about its chips, as VIAs does
|
|
30
33
|
# for the 1541 and the 1571.
|
|
31
34
|
module Core
|
|
@@ -33,7 +36,7 @@ module Badline
|
|
|
33
36
|
include Idle
|
|
34
37
|
include Orbit
|
|
35
38
|
|
|
36
|
-
attr_reader :device
|
|
39
|
+
attr_reader :device
|
|
37
40
|
|
|
38
41
|
def cpu
|
|
39
42
|
settle!
|
|
@@ -62,17 +65,6 @@ module Badline
|
|
|
62
65
|
@host_clock_hz = clock_hz
|
|
63
66
|
end
|
|
64
67
|
|
|
65
|
-
# Plugs the drive into a serial bus, leaving the one it was on. A drive
|
|
66
|
-
# starts out on a bus of its own, with nothing else on it.
|
|
67
|
-
def connect(serial_bus)
|
|
68
|
-
settle!
|
|
69
|
-
@serial_bus&.detach(self)
|
|
70
|
-
@serial_bus = serial_bus
|
|
71
|
-
@serial_port.bus = serial_bus
|
|
72
|
-
serial_bus.attach(self)
|
|
73
|
-
hear_atn
|
|
74
|
-
end
|
|
75
|
-
|
|
76
68
|
# Puts a Disk in the drive (Disk.from_d64 makes one from an image).
|
|
77
69
|
# Nil takes the disk out.
|
|
78
70
|
def insert(disk)
|
|
@@ -47,6 +47,14 @@ module Badline
|
|
|
47
47
|
#
|
|
48
48
|
# The timer's interrupts come round too, and once they do, the drive
|
|
49
49
|
# sleeps on through them (see Orbit).
|
|
50
|
+
#
|
|
51
|
+
# A model whose loop counts its passes in a byte of RAM, as the 1551's
|
|
52
|
+
# increments $61, names it in @pass_counter. A pass may then leave
|
|
53
|
+
# that byte one higher than it found it, and the drive sleeps only
|
|
54
|
+
# until the count would wrap, where the loop goes another way. The
|
|
55
|
+
# model compares the CPU without the increment's working registers
|
|
56
|
+
# (IdleCPU#counting_state), which skipping the passes sets as the
|
|
57
|
+
# last one would have left them (IdleCPU#counted).
|
|
50
58
|
module Idle
|
|
51
59
|
# Whether the drive may sleep through its idle loop. On by default,
|
|
52
60
|
# but for a drive whose CPU logs each instruction.
|
|
@@ -64,6 +72,7 @@ module Badline
|
|
|
64
72
|
@recording = false
|
|
65
73
|
@pass_cycles = 0
|
|
66
74
|
@pass_instructions = 0
|
|
75
|
+
@pass_counter = -1
|
|
67
76
|
init_sleep
|
|
68
77
|
init_orbits
|
|
69
78
|
end
|
|
@@ -104,22 +113,51 @@ module Badline
|
|
|
104
113
|
end
|
|
105
114
|
|
|
106
115
|
# Whether the counters leave room for a whole pass.
|
|
107
|
-
def room_for_pass? = quiet_cycles >= @cycles - @record_cycles
|
|
116
|
+
def room_for_pass? = quiet_cycles >= @cycles - @record_cycles && passes_to_count.positive?
|
|
108
117
|
|
|
109
118
|
# Sleeps through the pass just recorded, over and over.
|
|
110
119
|
def fall_asleep
|
|
111
|
-
|
|
120
|
+
pass_cycles = @cycles - @record_cycles
|
|
121
|
+
@pass_cycles = pass_cycles
|
|
112
122
|
@pass_instructions = @cpu.instructions - @record_instructions
|
|
113
123
|
@asleep = true
|
|
114
124
|
@host_still = false
|
|
115
125
|
@owed = 0
|
|
116
|
-
|
|
126
|
+
budget = quiet_cycles
|
|
127
|
+
unless @pass_counter.negative?
|
|
128
|
+
counted = passes_to_count * pass_cycles
|
|
129
|
+
budget = counted if counted < budget
|
|
130
|
+
end
|
|
131
|
+
@budget = budget
|
|
117
132
|
anchor
|
|
118
133
|
end
|
|
119
134
|
|
|
135
|
+
# The RAM the pass read or wrote as it found it, but for the pass
|
|
136
|
+
# counter, one higher.
|
|
120
137
|
def ram_as_found?
|
|
121
138
|
ram = @bus.ram
|
|
122
|
-
|
|
139
|
+
counter = @pass_counter
|
|
140
|
+
@bus.touched.all? do |addr, value|
|
|
141
|
+
ram.peek(addr) == (addr == counter ? value + 1 : value)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# How many more passes the pass counter can count before it wraps,
|
|
146
|
+
# and 1 without a pass counter.
|
|
147
|
+
def passes_to_count
|
|
148
|
+
counter = @pass_counter
|
|
149
|
+
counter.negative? ? 1 : 0xff - @bus.ram.peek(counter)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Counts +passes+ more passes in the pass counter.
|
|
153
|
+
def count_passes(passes)
|
|
154
|
+
counter = @pass_counter
|
|
155
|
+
return if counter.negative?
|
|
156
|
+
|
|
157
|
+
ram = @bus.ram
|
|
158
|
+
count = ram.peek(counter) + passes
|
|
159
|
+
ram.poke(counter, count)
|
|
160
|
+
@cpu.counted(count)
|
|
123
161
|
end
|
|
124
162
|
end
|
|
125
163
|
end
|
|
@@ -18,6 +18,24 @@ module Badline
|
|
|
18
18
|
@address, @pointer, @dummy_address, @value, @rmw_result]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# idle_state but for the byte a read-modify-write read and the one
|
|
22
|
+
# it wrote, which a pass that counts in RAM leaves one higher each
|
|
23
|
+
# time (Drives::Engine::Idle's pass counter).
|
|
24
|
+
def counting_state
|
|
25
|
+
[@program_counter, @stack_pointer, @a, @x, @y, @status.value,
|
|
26
|
+
@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
|
|
27
|
+
@boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
|
|
28
|
+
@operation, @optional_dummy, @boundary_crossed, @branch_taken,
|
|
29
|
+
@address, @pointer, @dummy_address]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The working registers as the increment that brought a counter to
|
|
33
|
+
# +count+ left them.
|
|
34
|
+
def counted(count)
|
|
35
|
+
@value = (count - 1) & 0xff
|
|
36
|
+
@rmw_result = count
|
|
37
|
+
end
|
|
38
|
+
|
|
21
39
|
# Counts +cycles+ cycles and +instructions+ instructions that ran
|
|
22
40
|
# without changing anything idle_state holds.
|
|
23
41
|
def fast_forward(cycles, instructions)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
module Engine
|
|
6
|
+
# A drive on the serial bus, beside Core: the 1541, the 1571 and the
|
|
7
|
+
# 1581. It hears ATN on connecting (hear_atn) and drives the bus
|
|
8
|
+
# (serial_output).
|
|
9
|
+
module SerialConnection
|
|
10
|
+
attr_reader :serial_bus
|
|
11
|
+
|
|
12
|
+
# Plugs the drive into a serial bus, leaving the one it was on. A drive
|
|
13
|
+
# starts out on a bus of its own, with nothing else on it.
|
|
14
|
+
def connect(serial_bus)
|
|
15
|
+
settle!
|
|
16
|
+
@serial_bus&.detach(self)
|
|
17
|
+
@serial_bus = serial_bus
|
|
18
|
+
@serial_port.bus = serial_bus
|
|
19
|
+
serial_bus.attach(self)
|
|
20
|
+
hear_atn
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -4,6 +4,7 @@ require "badline/drives/engine/sleep"
|
|
|
4
4
|
require "badline/drives/engine/idle"
|
|
5
5
|
require "badline/drives/engine/orbit"
|
|
6
6
|
require "badline/drives/engine/core"
|
|
7
|
+
require "badline/drives/engine/serial_connection"
|
|
7
8
|
require "badline/drives/engine/vias"
|
|
8
9
|
require "badline/drives/engine/fast_serial"
|
|
9
10
|
require "badline/drives/engine/idle_cpu"
|
|
@@ -9,6 +9,14 @@ module Badline
|
|
|
9
9
|
# VIA 2, which DiskVIA plugs in.
|
|
10
10
|
attr_writer :via
|
|
11
11
|
|
|
12
|
+
# The 1551's TIA, whose port B drives the byte to write
|
|
13
|
+
# (Drives::Model1551::TIA), where there is no VIA 2.
|
|
14
|
+
attr_writer :tia
|
|
15
|
+
|
|
16
|
+
# The byte the last BYTE READY read, which a 1551's gate array
|
|
17
|
+
# latches.
|
|
18
|
+
attr_accessor :latched_byte
|
|
19
|
+
|
|
12
20
|
# Whether BYTE READY reached SO for the CPU to sample on the drive's
|
|
13
21
|
# next cycle.
|
|
14
22
|
attr_accessor :so_pending
|
|
@@ -25,15 +33,23 @@ module Badline
|
|
|
25
33
|
# on, latches port A. It reaches the CPU's SO pin while VIA 2's CA2
|
|
26
34
|
# (SOE) is high, which lets the DOS spin on BVC for each byte. The CPU
|
|
27
35
|
# samples SO on the next cycle.
|
|
36
|
+
#
|
|
37
|
+
# A 1551 has no VIA there: its gate array latches the byte read for
|
|
38
|
+
# its TIA's port B (latched_byte), and BYTE LATCHED reaches its
|
|
39
|
+
# CPU's port.
|
|
28
40
|
def byte_ready!
|
|
29
41
|
@byte_latched = true
|
|
30
|
-
@
|
|
31
|
-
|
|
42
|
+
@latched_byte = @shift & 0xff
|
|
43
|
+
via = @via
|
|
44
|
+
return unless via
|
|
45
|
+
|
|
46
|
+
via.ca1 = false
|
|
47
|
+
@so_pending = true if via.ca2_output
|
|
32
48
|
end
|
|
33
49
|
|
|
34
50
|
# BYTE READY lets go of CA1 with the next bit.
|
|
35
51
|
def byte_ready_ended!
|
|
36
|
-
@via
|
|
52
|
+
@via&.ca1 = true
|
|
37
53
|
end
|
|
38
54
|
|
|
39
55
|
# Whether BYTE READY has come since the CPU last read VIA 2, as the
|
|
@@ -90,6 +90,8 @@ module Badline
|
|
|
90
90
|
# on phase 0.
|
|
91
91
|
def initialize(slip: 0)
|
|
92
92
|
@via = nil
|
|
93
|
+
@tia = nil
|
|
94
|
+
@latched_byte = 0
|
|
93
95
|
@so_pending = false
|
|
94
96
|
@disk = nil
|
|
95
97
|
@half_track = START_HALF_TRACK
|
|
@@ -275,11 +277,18 @@ module Badline
|
|
|
275
277
|
return unless @bits == 8
|
|
276
278
|
|
|
277
279
|
@bits = 0
|
|
278
|
-
@write_shift = @writing ?
|
|
280
|
+
@write_shift = @writing ? write_byte : @shift & 0xff
|
|
279
281
|
@byte_ready = true
|
|
280
282
|
byte_ready!
|
|
281
283
|
end
|
|
282
284
|
|
|
285
|
+
# The byte the write shift register loads in write mode: what VIA 2
|
|
286
|
+
# drives on port A, or on a 1551 what its TIA drives on port B.
|
|
287
|
+
def write_byte
|
|
288
|
+
via = @via
|
|
289
|
+
via ? via.port_a_output : @tia.port_b_output
|
|
290
|
+
end
|
|
291
|
+
|
|
283
292
|
# The stepper's rotor turns with the head, a phase to each half
|
|
284
293
|
# track, so the energized phase pulls the head to the neighbouring
|
|
285
294
|
# half track it belongs to. A phase two away pulls both ways, and the
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The drive CPU's bus, as the gate array decodes it (schematic 251860):
|
|
7
|
+
#
|
|
8
|
+
# $0000-$0001 - the 6510T's port (Port)
|
|
9
|
+
# $0002-$07FF - 2 KB of RAM
|
|
10
|
+
# $4000-$7FFF - the TIA, its eight registers mirrored
|
|
11
|
+
# $C000-$FFFF - the DOS ROM
|
|
12
|
+
#
|
|
13
|
+
# Writes to the ROM and to the rest go nowhere, and a read from the
|
|
14
|
+
# rest finds the last byte on the data bus.
|
|
15
|
+
#
|
|
16
|
+
# While Engine::Idle records a pass of the DOS's idle loop, the bus
|
|
17
|
+
# watches it (watch!): the RAM it reads and writes, and whether it
|
|
18
|
+
# reached the port or the TIA, which makes the pass volatile, but for
|
|
19
|
+
# reads of the TIA's port A, the cable's data lines, which hold still
|
|
20
|
+
# until the computer moves them. While Engine::Orbit looks for an
|
|
21
|
+
# orbit, the bus guards the drive (guard!), and any access to the
|
|
22
|
+
# port or the TIA taints it.
|
|
23
|
+
class Bus
|
|
24
|
+
include Addressable
|
|
25
|
+
|
|
26
|
+
attr_reader :ram, :rom, :port, :tia, :data, :volatile, :touched, :tainted, :touched_timers
|
|
27
|
+
|
|
28
|
+
def initialize(rom:, port:, tia:)
|
|
29
|
+
addressable_at(0, length: 2**16)
|
|
30
|
+
@ram = Memory.new([], length: 0x0800, start: 0)
|
|
31
|
+
@rom = rom
|
|
32
|
+
@port = port
|
|
33
|
+
@tia = tia
|
|
34
|
+
@data = 0
|
|
35
|
+
@recording = @guarding = @watching = false
|
|
36
|
+
@volatile = @tainted = false
|
|
37
|
+
@touched = {}
|
|
38
|
+
@touched_timers = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def peek(addr)
|
|
42
|
+
return @data = @rom.peek(addr) if addr >= 0xc000
|
|
43
|
+
|
|
44
|
+
if addr < 0x0800
|
|
45
|
+
return @data = port_peek(addr) if addr < 2
|
|
46
|
+
|
|
47
|
+
touch(addr) if @recording
|
|
48
|
+
return @data = @ram.peek(addr)
|
|
49
|
+
end
|
|
50
|
+
return @data unless addr.between?(0x4000, 0x7fff)
|
|
51
|
+
|
|
52
|
+
watch_chip(addr.nobits?(0x07)) if @watching
|
|
53
|
+
@data = @tia.peek(addr)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def poke(addr, value)
|
|
57
|
+
@data = value
|
|
58
|
+
if addr < 0x0800
|
|
59
|
+
return port_poke(addr, value) if addr < 2
|
|
60
|
+
|
|
61
|
+
touch(addr) if @recording
|
|
62
|
+
@ram.poke(addr, value)
|
|
63
|
+
elsif addr.between?(0x4000, 0x7fff)
|
|
64
|
+
watch_chip(false) if @watching
|
|
65
|
+
@tia.poke(addr, value)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Starts watching: see volatile and touched.
|
|
70
|
+
def watch!
|
|
71
|
+
@recording = true
|
|
72
|
+
@volatile = false
|
|
73
|
+
@touched = {}
|
|
74
|
+
watching!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def unwatch!
|
|
78
|
+
@recording = false
|
|
79
|
+
watching!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Starts guarding: see tainted.
|
|
83
|
+
def guard!
|
|
84
|
+
@guarding = true
|
|
85
|
+
@tainted = false
|
|
86
|
+
watching!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The drive's RAM and the byte last on its data bus, in a keyed
|
|
90
|
+
# snapshot module (BL-D8-RAM).
|
|
91
|
+
def save_fields(out)
|
|
92
|
+
out.int("data", @data).bytes("ram", @ram.read(0, @ram.length))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def load_fields(input)
|
|
96
|
+
@data = input.byte("data", @data)
|
|
97
|
+
ram = @ram.read(0, @ram.length)
|
|
98
|
+
@ram.write(0, input.bytes_into("ram", ram))
|
|
99
|
+
@watching = false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def port_peek(addr)
|
|
105
|
+
watch_chip(false) if @watching
|
|
106
|
+
@port.peek(addr)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def port_poke(addr, value)
|
|
110
|
+
watch_chip(false) if @watching
|
|
111
|
+
@port.poke(addr, value)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def touch(addr)
|
|
115
|
+
@touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def watching!
|
|
119
|
+
@watching = @recording || @guarding
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The CPU reached the port or the TIA: +steady+ for a read of the
|
|
123
|
+
# TIA's port A.
|
|
124
|
+
def watch_chip(steady)
|
|
125
|
+
if @recording && !steady
|
|
126
|
+
@volatile = true
|
|
127
|
+
@recording = false
|
|
128
|
+
end
|
|
129
|
+
if @guarding
|
|
130
|
+
@tainted = true
|
|
131
|
+
@guarding = false
|
|
132
|
+
end
|
|
133
|
+
watching!
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# TCBM, the cable between the 1551's TIA and the 6523 TIA in its
|
|
7
|
+
# interface in the computer's expansion port (zimmers.net's 1551
|
|
8
|
+
# memory map, schematic 251860):
|
|
9
|
+
#
|
|
10
|
+
# - the eight data lines join the two port As;
|
|
11
|
+
# - the drive's STATUS0 and STATUS1 (PC0-1) reach the computer's PB0-1;
|
|
12
|
+
# - the drive's ACK (PC3) reaches the computer's PC7;
|
|
13
|
+
# - the computer's DAV (PC6) reaches the drive's PC7.
|
|
14
|
+
#
|
|
15
|
+
# The drive pulls every line up, so a line neither side drives low
|
|
16
|
+
# reads 1. The computer's other port B and C pins read 0.
|
|
17
|
+
#
|
|
18
|
+
# The computer sees the drive's lines as soon as the drive writes
|
|
19
|
+
# them. The drive sees the computer's a host cycle late, as the
|
|
20
|
+
# cable latched them at the end of the last (latch_host), as a drive
|
|
21
|
+
# on the serial bus sees ATN: the cable is the 1551's serial port to
|
|
22
|
+
# Drives::Engine::Core, and a write that moves the computer's lines
|
|
23
|
+
# wakes a drive asleep in its idle loop (atn_moved?).
|
|
24
|
+
class Cable
|
|
25
|
+
STATUS = 0x03
|
|
26
|
+
ACK = 0x08
|
|
27
|
+
DAV = 0x40
|
|
28
|
+
|
|
29
|
+
attr_reader :host, :drive
|
|
30
|
+
|
|
31
|
+
# Plugs the cable into +host+, the computer's Chips::TIA, and
|
|
32
|
+
# +drive+, the drive's (Model1551::TIA#chip), for +unit+, the drive.
|
|
33
|
+
def initialize(host, drive, unit)
|
|
34
|
+
@host = host
|
|
35
|
+
@drive = drive
|
|
36
|
+
@unit = unit
|
|
37
|
+
@data = 0xff
|
|
38
|
+
@dav = DAV
|
|
39
|
+
@moved = false
|
|
40
|
+
host.cable = self
|
|
41
|
+
drive.cable = self
|
|
42
|
+
relatch
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Pulls the cable out: the computer's TIA reads 0 on every input and
|
|
46
|
+
# the drive's lines float high.
|
|
47
|
+
def unplug
|
|
48
|
+
@host.cable = nil
|
|
49
|
+
@drive.cable = nil
|
|
50
|
+
host_inputs = @host.inputs
|
|
51
|
+
drive_inputs = @drive.inputs
|
|
52
|
+
3.times do |port|
|
|
53
|
+
host_inputs[port] = 0
|
|
54
|
+
drive_inputs[port] = 0xff
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether the computer moved its lines since latch_host.
|
|
59
|
+
def atn_moved? = @moved
|
|
60
|
+
|
|
61
|
+
# Takes the computer's lines for the drive's cycles in the next host
|
|
62
|
+
# cycle.
|
|
63
|
+
def latch_host
|
|
64
|
+
return unless @moved
|
|
65
|
+
|
|
66
|
+
@moved = false
|
|
67
|
+
relatch
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Takes the computer's lines as they stand, as a snapshot leaves
|
|
71
|
+
# them.
|
|
72
|
+
def relatch
|
|
73
|
+
host = @host
|
|
74
|
+
@data = host.output(0)
|
|
75
|
+
@dav = host.output(2) & DAV
|
|
76
|
+
@moved = false
|
|
77
|
+
moved
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Either side wrote its TIA: the computer's inputs follow the drive
|
|
81
|
+
# at once, and the drive's follow the lines last latched.
|
|
82
|
+
def moved
|
|
83
|
+
host = @host
|
|
84
|
+
drive = @drive
|
|
85
|
+
drive_data = drive.output(0)
|
|
86
|
+
drive_lines = drive.output(2)
|
|
87
|
+
host_lines = host.output(2)
|
|
88
|
+
host_data = host.output(0)
|
|
89
|
+
host_inputs = host.inputs
|
|
90
|
+
host_inputs[0] = host_data & drive_data
|
|
91
|
+
host_inputs[1] = drive_lines & STATUS
|
|
92
|
+
host_inputs[2] = ((drive_lines & ACK) << 4) | (host_lines & DAV)
|
|
93
|
+
drive_inputs = drive.inputs
|
|
94
|
+
drive_inputs[0] = @data & drive_data
|
|
95
|
+
drive_inputs[2] = (drive_inputs[2] & 0x7f) | (@dav << 1)
|
|
96
|
+
return if @moved || (host_data == @data && (host_lines & DAV) == @dav)
|
|
97
|
+
|
|
98
|
+
@moved = true
|
|
99
|
+
@unit.host_written!
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|