badline 0.15.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 +63 -0
- data/CLAUDE.md +7 -0
- data/README.md +40 -10
- data/doc/media.md +19 -0
- data/doc/snapshot-format.md +61 -13
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/machine_player.rb +2 -14
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/tia.rb +16 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +8 -3
- 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 +20 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +13 -5
- data/lib/badline/frontend/menu/menu_media.rb +92 -14
- data/lib/badline/frontend/menu/menu_pages.rb +11 -6
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/machine.rb +9 -5
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/options/help.rb +4 -2
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +14 -2
- data/lib/badline/options/validation.rb +49 -9
- data/lib/badline/options.rb +34 -2
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +181 -45
- data/lib/badline/plus4/keyed_modules.rb +22 -5
- data/lib/badline/plus4/keyed_parts.rb +33 -4
- data/lib/badline/plus4/keyed_state.rb +5 -2
- 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 +103 -21
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/snapshot/field_doc.rb +7 -5
- data/lib/badline/snapshot/plus4_setup.rb +52 -10
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/version.rb +1 -1
- metadata +15 -1
|
@@ -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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The drive's 6510T, a 6510 with an 8-bit port (Port), on the drive's
|
|
7
|
+
# Bus.
|
|
8
|
+
class CPU
|
|
9
|
+
include Badline::Chips::CPU::Core
|
|
10
|
+
include Engine::IdleCPU
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The drive's own fields in a keyed snapshot module (BL-D8): its
|
|
7
|
+
# model, its clock phase, its cycle count, the 555's count to its
|
|
8
|
+
# next pulse and the byte the gate array latched. A save settles the
|
|
9
|
+
# drive first, so it is awake; a load forgets its orbits.
|
|
10
|
+
module KeyedState
|
|
11
|
+
def save_fields(out)
|
|
12
|
+
settle!
|
|
13
|
+
out.text("model", model_name).int("phase", @phase).int("cycles", @cycles).int("irq_timer", @irq_timer)
|
|
14
|
+
out.int("latched_byte", @mechanism.latched_byte)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_fields(input)
|
|
18
|
+
settle!
|
|
19
|
+
input.choice("model", [model_name], model_name)
|
|
20
|
+
@phase = input.int("phase", @phase)
|
|
21
|
+
@cycles = input.int("cycles", @cycles)
|
|
22
|
+
@irq_timer = input.int("irq_timer", @irq_timer).clamp(1, IRQ_PERIOD)
|
|
23
|
+
@mechanism.latched_byte = input.byte("latched_byte", @mechanism.latched_byte)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The drive as modules of its own, named from `name` (BL-D8 for
|
|
27
|
+
# device 8): its own fields, its RAM, the CPU's port, the drive's
|
|
28
|
+
# TIA, its interface's TIA in the computer, the CPU, the mechanism
|
|
29
|
+
# and the disk, when one is in.
|
|
30
|
+
def save_modules(sections, name)
|
|
31
|
+
sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
|
|
32
|
+
sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
|
|
33
|
+
sections << Snapshot::Keyed.section("#{name}-PORT") { |out| @port.save_fields(out) }
|
|
34
|
+
sections << Snapshot::Keyed.section("#{name}-TIA") { |out| @tia.save_fields(out) }
|
|
35
|
+
sections << Snapshot::Keyed.section("#{name}-HOSTTIA") { |out| @host_tia.save_fields(out) }
|
|
36
|
+
sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
|
|
37
|
+
sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
|
|
38
|
+
disk = @mechanism.disk
|
|
39
|
+
sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
|
|
40
|
+
sections
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The drive from its modules, `input` being its own. No disk module
|
|
44
|
+
# means no disk in the drive. The cable's lines and the mechanism's
|
|
45
|
+
# mode follow the TIAs once both are loaded.
|
|
46
|
+
def load_modules(modules, input, name)
|
|
47
|
+
load_fields(input)
|
|
48
|
+
@bus.load_fields(modules.required("#{name}-RAM"))
|
|
49
|
+
@port.load_fields(modules.required("#{name}-PORT"))
|
|
50
|
+
@tia.load_fields(modules.required("#{name}-TIA"))
|
|
51
|
+
@host_tia.load_fields(modules.required("#{name}-HOSTTIA"))
|
|
52
|
+
@cpu.load_fields(modules.required("#{name}-CPU"))
|
|
53
|
+
disk_input = modules.optional("#{name}-DISK")
|
|
54
|
+
disk = disk_input.nil? ? nil : Model1541::Disk.from_fields(disk_input, detached: modules.detached?)
|
|
55
|
+
@mechanism.load_fields(modules.required("#{name}-MECH"), disk)
|
|
56
|
+
@cable.relatch
|
|
57
|
+
@cpu.restored!
|
|
58
|
+
forget_orbits
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
include KeyedState
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The 6510T's port at $00 (direction) and $01 (data), wired to the
|
|
7
|
+
# mechanism in the bits of the 1541's VIA 2 port B (zimmers.net's 1551
|
|
8
|
+
# memory map, schematic 251860):
|
|
9
|
+
#
|
|
10
|
+
# P0-1 stepper phase (out) P4 write protect (in, 1 = writable)
|
|
11
|
+
# P2 motor on (out) P5-6 bit rate, speed zone 0-3 (out)
|
|
12
|
+
# P3 activity LED (out) P7 BYTE LATCHED (in)
|
|
13
|
+
#
|
|
14
|
+
# An output pin reads back what it drives, and a pin left as an input
|
|
15
|
+
# drives nothing into the mechanism.
|
|
16
|
+
class Port
|
|
17
|
+
WRITE_PROTECT = 0x10
|
|
18
|
+
BYTE_LATCHED = 0x80
|
|
19
|
+
|
|
20
|
+
attr_reader :ddr, :data
|
|
21
|
+
|
|
22
|
+
def initialize(mechanism)
|
|
23
|
+
@mechanism = mechanism
|
|
24
|
+
@ddr = 0
|
|
25
|
+
@data = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
@ddr = 0
|
|
30
|
+
@data = 0
|
|
31
|
+
changed
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def peek(addr)
|
|
35
|
+
return @ddr if addr.zero?
|
|
36
|
+
|
|
37
|
+
ddr = @ddr
|
|
38
|
+
(@data & ddr) | (inputs & ~ddr & 0xff)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def poke(addr, value)
|
|
42
|
+
if addr.zero?
|
|
43
|
+
@ddr = value
|
|
44
|
+
else
|
|
45
|
+
@data = value
|
|
46
|
+
end
|
|
47
|
+
changed
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The registers, in a keyed snapshot module (BL-D8-PORT).
|
|
51
|
+
def save_fields(out)
|
|
52
|
+
out.int("ddr", @ddr).int("data", @data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_fields(input)
|
|
56
|
+
@ddr = input.byte("ddr", @ddr)
|
|
57
|
+
@data = input.byte("data", @data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def inputs
|
|
63
|
+
mechanism = @mechanism
|
|
64
|
+
value = (mechanism.read_b(0xff) & WRITE_PROTECT) | (0xff & ~(WRITE_PROTECT | BYTE_LATCHED))
|
|
65
|
+
mechanism.byte_latched? ? value | BYTE_LATCHED : value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def changed
|
|
69
|
+
@mechanism.port_b_written(@data & @ddr)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|