badline 0.11.0 → 0.11.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 +8 -0
- data/CLAUDE.md +20 -3
- data/CONTRIBUTING.md +8 -3
- data/README.md +6 -2
- data/lib/badline/c128/drives.rb +95 -0
- data/lib/badline/c128/fast_serial.rb +62 -0
- data/lib/badline/c128/mmu.rb +15 -0
- data/lib/badline/c128/saved_state.rb +1 -0
- data/lib/badline/c128.rb +9 -7
- data/lib/badline/cia/fast_forward.rb +42 -0
- data/lib/badline/cia/serial.rb +12 -0
- data/lib/badline/cia/timer.rb +39 -0
- data/lib/badline/cia.rb +2 -0
- data/lib/badline/computer/attachments.rb +6 -0
- data/lib/badline/drive/core.rb +172 -0
- data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
- data/lib/badline/drive/idle_cpu.rb +28 -0
- data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
- data/lib/badline/drive/serial_lines.rb +73 -0
- data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
- data/lib/badline/drive/watch.rb +150 -0
- data/lib/badline/drive.rb +9 -0
- data/lib/badline/drive1541/bus.rb +5 -129
- data/lib/badline/drive1541/byte_ready.rb +48 -0
- data/lib/badline/drive1541/cpu.rb +1 -19
- data/lib/badline/drive1541/disk.rb +77 -20
- data/lib/badline/drive1541/disk_state.rb +7 -2
- data/lib/badline/drive1541/disk_via.rb +3 -0
- data/lib/badline/drive1541/mechanism.rb +48 -14
- data/lib/badline/drive1541/mechanism_state.rb +5 -4
- data/lib/badline/drive1541/rotation.rb +5 -5
- data/lib/badline/drive1541/serial_port.rb +4 -60
- data/lib/badline/drive1541.rb +13 -172
- data/lib/badline/drive1571/bus.rb +111 -0
- data/lib/badline/drive1571/cpu.rb +11 -0
- data/lib/badline/drive1571/serial_port.rb +37 -0
- data/lib/badline/drive1571/serial_via.rb +25 -0
- data/lib/badline/drive1571/wd1770.rb +34 -0
- data/lib/badline/drive1571.rb +225 -0
- data/lib/badline/frontend/app.rb +1 -1
- data/lib/badline/frontend/drive_led.rb +1 -1
- data/lib/badline/frontend/menu_media.rb +3 -3
- data/lib/badline/frontend/menu_pages.rb +2 -1
- data/lib/badline/iec_bus.rb +54 -4
- data/lib/badline/machine.rb +7 -5
- data/lib/badline/media/true_drive.rb +26 -23
- data/lib/badline/options/help.rb +2 -1
- data/lib/badline/options/table.rb +1 -1
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1571.rom +0 -0
- data/lib/badline/storage/d64_image.rb +3 -0
- data/lib/badline/storage/d71_image.rb +9 -4
- data/lib/badline/storage/g64_image.rb +7 -2
- data/lib/badline/time_of_day.rb +9 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/fast_forward.rb +3 -3
- data/lib/badline/vic20/attachments.rb +6 -0
- data/lib/badline/z80/arithmetic.rb +117 -0
- data/lib/badline/z80/bit_page.rb +55 -0
- data/lib/badline/z80/block_io.rb +58 -0
- data/lib/badline/z80/block_transfers.rb +59 -0
- data/lib/badline/z80/bus_cycles.rb +111 -0
- data/lib/badline/z80/core.rb +111 -0
- data/lib/badline/z80/exchanges.rb +50 -0
- data/lib/badline/z80/extended_page.rb +89 -0
- data/lib/badline/z80/flags.rb +37 -0
- data/lib/badline/z80/interrupt_acceptance.rb +46 -0
- data/lib/badline/z80/jumps.rb +82 -0
- data/lib/badline/z80/loads.rb +98 -0
- data/lib/badline/z80/main_page.rb +134 -0
- data/lib/badline/z80/registers.rb +120 -0
- data/lib/badline/z80/shifts.rb +64 -0
- data/lib/badline/z80.rb +11 -0
- metadata +36 -4
|
@@ -18,41 +18,15 @@ module Badline
|
|
|
18
18
|
# page is what its dummy read found. VICE-testprogs drive/openbus pins
|
|
19
19
|
# both on a real 1541.
|
|
20
20
|
#
|
|
21
|
-
# While Idle records a pass of the DOS's idle loop, the bus watches it
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# While Idle records a pass of the DOS's idle loop, the bus watches it,
|
|
22
|
+
# and while Idle looks for an orbit, it guards the drive (see
|
|
23
|
+
# Drive::Watch).
|
|
24
24
|
class Bus
|
|
25
25
|
include Addressable
|
|
26
|
-
|
|
27
|
-
# VIA registers, as bits by offset, whose access makes a watched
|
|
28
|
-
# stretch volatile: reads of either VIA's counters and shift register
|
|
29
|
-
# and of VIA 1's port B, and writes to VIA 2's timers, shift register
|
|
30
|
-
# and ACR. Any write to VIA 1 does too (watch_write).
|
|
31
|
-
VOLATILE_VIA1_READS = 0x0731
|
|
32
|
-
VOLATILE_VIA2_READS = 0x0730
|
|
33
|
-
VOLATILE_VIA2_WRITES = 0x0ff0
|
|
34
|
-
|
|
35
|
-
# The timer each VIA register reaches, by offset, as bits: 1 for
|
|
36
|
-
# timer 1 and 2 for timer 2.
|
|
37
|
-
TIMER_REGISTERS = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0].freeze
|
|
26
|
+
include Drive::Watch
|
|
38
27
|
|
|
39
28
|
attr_reader :ram, :rom, :via1, :via2, :data
|
|
40
29
|
|
|
41
|
-
# Whether a watched stretch did something that makes it unrepeatable.
|
|
42
|
-
attr_reader :volatile
|
|
43
|
-
|
|
44
|
-
# The RAM a watched stretch read or wrote, as each address's value
|
|
45
|
-
# from before the first access.
|
|
46
|
-
attr_reader :touched
|
|
47
|
-
|
|
48
|
-
# Whether the drive did something since guard! that reached outside
|
|
49
|
-
# it, or took something in from outside.
|
|
50
|
-
attr_reader :tainted
|
|
51
|
-
|
|
52
|
-
# The timers the drive read or wrote since guard!, as bits: VIA 1's
|
|
53
|
-
# timer 1 and timer 2 in bits 0 and 1, and VIA 2's in bits 2 and 3.
|
|
54
|
-
attr_reader :touched_timers
|
|
55
|
-
|
|
56
30
|
def initialize(rom:, via1:, via2:)
|
|
57
31
|
addressable_at(0, length: 2**16)
|
|
58
32
|
@ram = Memory.new([], length: 0x0800, start: 0)
|
|
@@ -60,38 +34,7 @@ module Badline
|
|
|
60
34
|
@via1 = via1
|
|
61
35
|
@via2 = via2
|
|
62
36
|
@data = 0
|
|
63
|
-
|
|
64
|
-
@volatile = @tainted = false
|
|
65
|
-
@touched = {}
|
|
66
|
-
@touched_timers = 0
|
|
67
|
-
@led = 0
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Starts watching: see volatile and touched. The bus stops watching
|
|
71
|
-
# once the stretch turns volatile.
|
|
72
|
-
def watch!
|
|
73
|
-
@recording = true
|
|
74
|
-
@volatile = false
|
|
75
|
-
@touched = {}
|
|
76
|
-
watching!
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def unwatch!
|
|
80
|
-
@recording = false
|
|
81
|
-
watching!
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Starts guarding: see tainted and touched_timers. Reading VIA 1's
|
|
85
|
-
# port B, which reads the serial bus, taints the drive, and so do
|
|
86
|
-
# any write to VIA 1, whose port B drives the serial bus, a write to
|
|
87
|
-
# VIA 2's shift register or ACR, and VIA 2 turning the motor on or
|
|
88
|
-
# changing the LED. The bus stops guarding once the drive is tainted.
|
|
89
|
-
def guard!
|
|
90
|
-
@guarding = true
|
|
91
|
-
@tainted = false
|
|
92
|
-
@touched_timers = 0
|
|
93
|
-
@led = @via2.port_b_output & Mechanism::LED
|
|
94
|
-
watching!
|
|
37
|
+
init_watch
|
|
95
38
|
end
|
|
96
39
|
|
|
97
40
|
# The last byte on the data bus and the RAM. What a watched stretch
|
|
@@ -138,73 +81,6 @@ module Badline
|
|
|
138
81
|
end
|
|
139
82
|
watch_write(addr) if @watching
|
|
140
83
|
end
|
|
141
|
-
|
|
142
|
-
private
|
|
143
|
-
|
|
144
|
-
def watching!
|
|
145
|
-
@watching = @recording || @guarding
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def touch(addr)
|
|
149
|
-
@touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def watch_read(addr)
|
|
153
|
-
if addr < 0x0800
|
|
154
|
-
touch(addr) if @recording
|
|
155
|
-
elsif addr >= 0x1800
|
|
156
|
-
via1 = addr < 0x1c00
|
|
157
|
-
watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
|
|
158
|
-
guard_read(via1, addr & 0x0f) if @guarding
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def watch_write(addr)
|
|
163
|
-
if addr >= 0x1c00
|
|
164
|
-
watch_via2_write(addr) if @recording
|
|
165
|
-
guard_via2_write(addr & 0x0f) if @guarding
|
|
166
|
-
elsif addr >= 0x1800
|
|
167
|
-
volatile! if @recording
|
|
168
|
-
taint! if @guarding
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Any write to VIA 1 makes the stretch volatile (watch_write), and so
|
|
173
|
-
# does a write to VIA 2 that leaves the motor on.
|
|
174
|
-
def watch_via2_write(addr)
|
|
175
|
-
watch(VOLATILE_VIA2_WRITES, addr)
|
|
176
|
-
volatile! if @recording && @via2.port_b_output.anybits?(Mechanism::MOTOR)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def watch(volatile, addr)
|
|
180
|
-
volatile! if volatile.anybits?(1 << (addr & 0x0f))
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def volatile!
|
|
184
|
-
@volatile = true
|
|
185
|
-
@recording = false
|
|
186
|
-
watching!
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def guard_read(via1, offset)
|
|
190
|
-
return taint! if via1 && offset.zero?
|
|
191
|
-
|
|
192
|
-
@touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def guard_via2_write(offset)
|
|
196
|
-
return taint! if offset.between?(0x0a, 0x0b)
|
|
197
|
-
|
|
198
|
-
@touched_timers |= TIMER_REGISTERS[offset] << 2
|
|
199
|
-
lines = @via2.port_b_output
|
|
200
|
-
taint! if lines.anybits?(Mechanism::MOTOR) || (lines & Mechanism::LED) != @led
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def taint!
|
|
204
|
-
@tainted = true
|
|
205
|
-
@guarding = false
|
|
206
|
-
watching!
|
|
207
|
-
end
|
|
208
84
|
end
|
|
209
85
|
end
|
|
210
86
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# The Mechanism's BYTE READY line: to VIA 2's CA1, to the CPU's SO pin
|
|
6
|
+
# through SOE, and on the 1571 to a latch VIA 1's PA7 reads.
|
|
7
|
+
module ByteReady
|
|
8
|
+
# VIA 2, which DiskVIA plugs in.
|
|
9
|
+
attr_writer :via
|
|
10
|
+
|
|
11
|
+
# Whether BYTE READY reached SO for the CPU to sample on the drive's
|
|
12
|
+
# next cycle.
|
|
13
|
+
attr_accessor :so_pending
|
|
14
|
+
|
|
15
|
+
# Whether SO is pending, which the drive's cycle samples and clears.
|
|
16
|
+
def take_so
|
|
17
|
+
so = @so_pending
|
|
18
|
+
@so_pending = false
|
|
19
|
+
so
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The read electronics signal a whole GCR byte. BYTE READY pulls VIA
|
|
23
|
+
# 2's CA1 low, a falling edge that sets its flag and, with latching
|
|
24
|
+
# on, latches port A. It reaches the CPU's SO pin while VIA 2's CA2
|
|
25
|
+
# (SOE) is high, which lets the DOS spin on BVC for each byte. The CPU
|
|
26
|
+
# samples SO on the next cycle.
|
|
27
|
+
def byte_ready!
|
|
28
|
+
@byte_latched = true
|
|
29
|
+
@via.ca1 = false
|
|
30
|
+
@so_pending = true if @via.ca2_output
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# BYTE READY lets go of CA1 with the next bit.
|
|
34
|
+
def byte_ready_ended!
|
|
35
|
+
@via.ca1 = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether BYTE READY has come since the CPU last read VIA 2, as the
|
|
39
|
+
# 1571 latches it for VIA 1's PA7.
|
|
40
|
+
def byte_latched? = @byte_latched
|
|
41
|
+
|
|
42
|
+
# A read of VIA 2 clears the 1571's BYTE READY latch.
|
|
43
|
+
def release_byte_latch
|
|
44
|
+
@byte_latched = false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -5,25 +5,7 @@ module Badline
|
|
|
5
5
|
# The drive's 6502, on the drive's Bus.
|
|
6
6
|
class CPU
|
|
7
7
|
include Badline::CPU::Core
|
|
8
|
-
|
|
9
|
-
# Everything the CPU holds but its cycle and instruction counts: the
|
|
10
|
-
# registers, the interrupt lines and pipeline, and what the last
|
|
11
|
-
# instruction left in its working registers. Drive1541::Idle compares
|
|
12
|
-
# it at two instruction boundaries.
|
|
13
|
-
def idle_state
|
|
14
|
-
[@program_counter, @stack_pointer, @a, @x, @y, @status.value,
|
|
15
|
-
@irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
|
|
16
|
-
@boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
|
|
17
|
-
@operation, @optional_dummy, @boundary_crossed, @branch_taken,
|
|
18
|
-
@address, @pointer, @dummy_address, @value, @rmw_result]
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Counts +cycles+ cycles and +instructions+ instructions that ran
|
|
22
|
-
# without changing anything idle_state holds.
|
|
23
|
-
def fast_forward(cycles, instructions)
|
|
24
|
-
@cycles += cycles
|
|
25
|
-
@instructions += instructions
|
|
26
|
-
end
|
|
8
|
+
include Drive::IdleCPU
|
|
27
9
|
end
|
|
28
10
|
end
|
|
29
11
|
end
|
|
@@ -10,6 +10,11 @@ module Badline
|
|
|
10
10
|
# nothing between them. A .d64 image fills the whole tracks; a .g64
|
|
11
11
|
# can fill any of them, with tracks of any length.
|
|
12
12
|
#
|
|
13
|
+
# The disk has a second side, for the 1571's second head, its half
|
|
14
|
+
# tracks numbered from SIDE on. A .d71 fills both sides, its tracks
|
|
15
|
+
# 36-70 being tracks 1-35 of the second, and a .g71 holds 84 half
|
|
16
|
+
# tracks a side. A 1541 reads the first side only.
|
|
17
|
+
#
|
|
13
18
|
# The head writes into the tracks' bytes, and a write to a half track
|
|
14
19
|
# without data gives it a blank Track first. A disk made from an image
|
|
15
20
|
# remembers it: flush reads the sectors back off the tracks written
|
|
@@ -21,6 +26,15 @@ module Badline
|
|
|
21
26
|
|
|
22
27
|
MAX_HALF_TRACK = 84
|
|
23
28
|
|
|
29
|
+
# Where the second side's half tracks start: its track 1 is SIDE + 2.
|
|
30
|
+
SIDE = MAX_HALF_TRACK + 1
|
|
31
|
+
|
|
32
|
+
# The tracks a side of a .d71 holds.
|
|
33
|
+
D71_SIDE_TRACKS = 35
|
|
34
|
+
|
|
35
|
+
# The half track entries a side of a .g71 holds.
|
|
36
|
+
G71_SIDE_ENTRIES = 84
|
|
37
|
+
|
|
24
38
|
# GCR bytes around a track in each speed zone: 200 ms of rotation at
|
|
25
39
|
# 300 rpm, in bytes of 32, 30, 28 and 26 µs.
|
|
26
40
|
TRACK_LENGTHS = [6250, 6666, 7142, 7692].freeze
|
|
@@ -51,27 +65,63 @@ module Badline
|
|
|
51
65
|
# the 325-byte GCR data block. The gap after it takes up the rest.
|
|
52
66
|
SECTOR_LENGTH = SYNC_LENGTH + 10 + HEADER_GAP + SYNC_LENGTH + 325
|
|
53
67
|
|
|
54
|
-
# A disk from the image at +path+: a .g64 as its tracks are,
|
|
55
|
-
# anything else as a .d64,
|
|
56
|
-
# write-protected.
|
|
68
|
+
# A disk from the image at +path+: a .g64 or .g71 as its tracks are,
|
|
69
|
+
# a .d71 formatted on both sides, and anything else as a .d64,
|
|
70
|
+
# formatted. `read_only` opens the image write-protected.
|
|
57
71
|
def self.open(path, read_only: false)
|
|
58
72
|
return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
|
|
73
|
+
return from_d64(Storage::D71Image.new(path, read_only:)).opened(path, read_only) if State.d71?(path)
|
|
59
74
|
|
|
60
75
|
from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
|
|
61
76
|
end
|
|
62
77
|
|
|
63
|
-
# A disk from a G64 image, each half track as the image stores
|
|
64
|
-
# with its speed map. Flushing it writes the tracks back as they
|
|
78
|
+
# A disk from a G64 or G71 image, each half track as the image stores
|
|
79
|
+
# it, with its speed map. Flushing it writes the tracks back as they
|
|
80
|
+
# are.
|
|
65
81
|
def self.from_g64(image)
|
|
66
82
|
disk = new(image)
|
|
67
|
-
|
|
68
|
-
entry = half_track - Mechanism::MIN_HALF_TRACK
|
|
83
|
+
image.half_tracks.times do |entry|
|
|
69
84
|
track = image.track(entry)
|
|
70
|
-
disk.write(
|
|
85
|
+
disk.write(half_track_of(entry), Track.new(*track, image.speeds(entry))) if track
|
|
71
86
|
end
|
|
72
87
|
disk
|
|
73
88
|
end
|
|
74
89
|
|
|
90
|
+
# The half track a G64 or G71 table entry holds: entry 0 is track 1,
|
|
91
|
+
# and a .g71's entries from G71_SIDE_ENTRIES on are the second
|
|
92
|
+
# side's.
|
|
93
|
+
def self.half_track_of(entry)
|
|
94
|
+
side = entry / G71_SIDE_ENTRIES
|
|
95
|
+
(side * SIDE) + (entry % G71_SIDE_ENTRIES) + Mechanism::MIN_HALF_TRACK
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The table entry of a half track, or nil for one a G64 or G71 has
|
|
99
|
+
# no entry for.
|
|
100
|
+
def self.entry_of(half_track)
|
|
101
|
+
side = half_track / SIDE
|
|
102
|
+
entry = (half_track % SIDE) - Mechanism::MIN_HALF_TRACK
|
|
103
|
+
(side * G71_SIDE_ENTRIES) + entry if entry.between?(0, G71_SIDE_ENTRIES - 1)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The half track an image's track number is on: a .d71's tracks past
|
|
107
|
+
# D71_SIDE_TRACKS are on the second side.
|
|
108
|
+
def self.d64_half_track(image, track)
|
|
109
|
+
return track * 2 unless image.sides == 2 && track > D71_SIDE_TRACKS
|
|
110
|
+
|
|
111
|
+
SIDE + ((track - D71_SIDE_TRACKS) * 2)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The image's track number a half track holds, or nil for a half
|
|
115
|
+
# track or one past the image's last.
|
|
116
|
+
def self.d64_track(image, half_track)
|
|
117
|
+
side = half_track / SIDE
|
|
118
|
+
return unless (half_track % SIDE).even?
|
|
119
|
+
|
|
120
|
+
track = (half_track % SIDE) / 2
|
|
121
|
+
track += D71_SIDE_TRACKS if side == 1
|
|
122
|
+
track if track.positive? && track <= image.track_count && side < image.sides
|
|
123
|
+
end
|
|
124
|
+
|
|
75
125
|
# A disk formatted from a D64 image, each sector laid out as the DOS
|
|
76
126
|
# formats it, with the disk ID from the header block. A block the
|
|
77
127
|
# image's error table marks 20, 21, 22, 23, 27 or 29 is written the
|
|
@@ -81,7 +131,8 @@ module Badline
|
|
|
81
131
|
#
|
|
82
132
|
# Each track starts sector 0 at the angle the DOS's N: leaves it: track
|
|
83
133
|
# 1 at the index angle, and each track after it SKEWS[zone] of a turn
|
|
84
|
-
# round from the last.
|
|
134
|
+
# round from the last. A .d71's second side starts over at the index
|
|
135
|
+
# angle.
|
|
85
136
|
def self.from_d64(image)
|
|
86
137
|
track, sector = image.header_block
|
|
87
138
|
header = image.read_block(track, sector)
|
|
@@ -89,16 +140,18 @@ module Badline
|
|
|
89
140
|
disk = new(image)
|
|
90
141
|
angle = 0
|
|
91
142
|
(1..image.track_count).each do |track|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
143
|
+
half_track = d64_half_track(image, track)
|
|
144
|
+
zone = zone((half_track % SIDE) / 2)
|
|
145
|
+
angle = half_track == SIDE + 2 ? 0 : (angle + SKEWS[zone]) % SKEW_TURN if track > 1
|
|
146
|
+
bytes = format_track(image, track, id, zone)
|
|
147
|
+
disk.write(half_track, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone))
|
|
95
148
|
end
|
|
96
149
|
disk
|
|
97
150
|
end
|
|
98
151
|
|
|
99
|
-
def self.format_track(image, track, id)
|
|
152
|
+
def self.format_track(image, track, id, zone = zone(track))
|
|
100
153
|
sectors = image.sectors_in(track)
|
|
101
|
-
length = TRACK_LENGTHS[zone
|
|
154
|
+
length = TRACK_LENGTHS[zone]
|
|
102
155
|
gap = (length - (sectors * SECTOR_LENGTH)) / sectors
|
|
103
156
|
bytes = []
|
|
104
157
|
sectors.times do |sector|
|
|
@@ -146,7 +199,7 @@ module Badline
|
|
|
146
199
|
|
|
147
200
|
def initialize(image = nil)
|
|
148
201
|
@image = image
|
|
149
|
-
@tracks = Array.new(
|
|
202
|
+
@tracks = Array.new(SIDE * 2)
|
|
150
203
|
@written = {}
|
|
151
204
|
@path = nil
|
|
152
205
|
@read_only = false
|
|
@@ -188,11 +241,14 @@ module Badline
|
|
|
188
241
|
def flush
|
|
189
242
|
return flush_tracks if @image.respond_to?(:store_tracks)
|
|
190
243
|
|
|
191
|
-
tracks = @written.keys.
|
|
244
|
+
tracks = @written.keys.filter_map do |half|
|
|
245
|
+
track = @image && Disk.d64_track(@image, half)
|
|
246
|
+
[half, track] if track
|
|
247
|
+
end
|
|
192
248
|
@written.clear
|
|
193
249
|
return if tracks.empty?
|
|
194
250
|
|
|
195
|
-
sectors = tracks.to_h { |half| [
|
|
251
|
+
sectors = tracks.to_h { |half, track| [track, SectorReader.read(@tracks[half].bytes, track)] }
|
|
196
252
|
id = disk_id(sectors)
|
|
197
253
|
warn_lost(sectors)
|
|
198
254
|
@image.store_blocks(sectors.flat_map do |track, found|
|
|
@@ -203,9 +259,10 @@ module Badline
|
|
|
203
259
|
private
|
|
204
260
|
|
|
205
261
|
def flush_tracks
|
|
206
|
-
tracks = @written.keys.sort.
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
tracks = @written.keys.sort.filter_map do |half|
|
|
263
|
+
entry = Disk.entry_of(half)
|
|
264
|
+
[entry, [@tracks[half].bytes, @tracks[half].zone]] if entry
|
|
265
|
+
end.to_h
|
|
209
266
|
@written.clear
|
|
210
267
|
@image.store_tracks(tracks) unless tracks.empty?
|
|
211
268
|
end
|
|
@@ -12,13 +12,18 @@ module Badline
|
|
|
12
12
|
# for a disk from Disk.open. Nil for a disk made another way.
|
|
13
13
|
attr_reader :path, :read_only
|
|
14
14
|
|
|
15
|
-
# A .g64 image by its name,
|
|
16
|
-
|
|
15
|
+
# A .g64 or .g71 image by its name, a .d71 by its name, and anything
|
|
16
|
+
# else a .d64.
|
|
17
|
+
def self.g64?(path) = %w[.g64 .g71].include?(File.extname(path).downcase)
|
|
18
|
+
|
|
19
|
+
def self.d71?(path) = File.extname(path).casecmp?(".d71")
|
|
17
20
|
|
|
18
21
|
# A disk for the image at the path, its tracks left for the state.
|
|
19
22
|
def self.reopen(path, read_only)
|
|
20
23
|
if g64?(path)
|
|
21
24
|
Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
|
|
25
|
+
elsif d71?(path)
|
|
26
|
+
Disk.new(Storage::D71Image.new(path, read_only:)).opened(path, read_only)
|
|
22
27
|
else
|
|
23
28
|
Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
|
|
24
29
|
end
|
|
@@ -7,8 +7,11 @@ module Badline
|
|
|
7
7
|
# hears both after every write and reset, since ORB, DDRB, the ACR and
|
|
8
8
|
# the PCR can all change them.
|
|
9
9
|
class DiskVIA < VIA
|
|
10
|
+
attr_reader :mechanism
|
|
11
|
+
|
|
10
12
|
def initialize(start:, mechanism:)
|
|
11
13
|
@mechanism = mechanism
|
|
14
|
+
mechanism.via = self
|
|
12
15
|
super(start:, peripheral: mechanism)
|
|
13
16
|
end
|
|
14
17
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/drive1541/rotation"
|
|
4
4
|
require "badline/drive1541/mechanism_state"
|
|
5
|
+
require "badline/drive1541/byte_ready"
|
|
5
6
|
|
|
6
7
|
module Badline
|
|
7
8
|
class Drive1541
|
|
@@ -26,7 +27,7 @@ module Badline
|
|
|
26
27
|
# reads it, and it holds the bit counter at zero, so the first 0 bit
|
|
27
28
|
# after it starts a byte. Every eighth bit after that is BYTE READY,
|
|
28
29
|
# which pulls VIA 2's CA1 low until the next bit and reaches the CPU's
|
|
29
|
-
# SO pin (see
|
|
30
|
+
# SO pin (see byte_ready!).
|
|
30
31
|
#
|
|
31
32
|
# CB2 low selects write mode, where SYNC detection and the flux are
|
|
32
33
|
# off and the clock runs on at the selected rate. At each BYTE READY
|
|
@@ -64,9 +65,13 @@ module Badline
|
|
|
64
65
|
# becomes track 1's, so the DOS's bump leaves the head on track 1 in
|
|
65
66
|
# line with the phase it ends on, and its first step in reaches the
|
|
66
67
|
# half track after it.
|
|
68
|
+
#
|
|
69
|
+
# The 1571's stepper is lined up so that phase 0 holds track 1, where
|
|
70
|
+
# its track 0 sensor looks for the head (Mechanism.new's +slip+).
|
|
67
71
|
class Mechanism
|
|
68
72
|
include Rotation
|
|
69
73
|
include MechanismState
|
|
74
|
+
include ByteReady
|
|
70
75
|
|
|
71
76
|
MOTOR = 0x04
|
|
72
77
|
LED = 0x08
|
|
@@ -81,11 +86,15 @@ module Badline
|
|
|
81
86
|
|
|
82
87
|
attr_reader :disk, :half_track, :zone
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
# +slip+ is the stepper's phase offset at power-on: 0, where each half
|
|
90
|
+
# track's phase is the low two bits of its number, or 2 for track 1
|
|
91
|
+
# on phase 0.
|
|
92
|
+
def initialize(slip: 0)
|
|
93
|
+
@via = nil
|
|
94
|
+
@so_pending = false
|
|
86
95
|
@disk = nil
|
|
87
96
|
@half_track = START_HALF_TRACK
|
|
88
|
-
@slip =
|
|
97
|
+
@slip = slip
|
|
89
98
|
@motor = false
|
|
90
99
|
@led = false
|
|
91
100
|
@zone = 0
|
|
@@ -98,10 +107,30 @@ module Badline
|
|
|
98
107
|
@bits = 0
|
|
99
108
|
@sync = false
|
|
100
109
|
@byte_ready = false
|
|
110
|
+
@byte_latched = false
|
|
101
111
|
@writing = false
|
|
102
112
|
@write_shift = 0
|
|
103
113
|
@write_gate = false
|
|
104
114
|
@protected = false
|
|
115
|
+
@side_base = 0
|
|
116
|
+
@cycle_ticks = CYCLE
|
|
117
|
+
load_track
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The drive's clock rate: the disk turns on by fewer ticks each
|
|
121
|
+
# cycle at 2 MHz.
|
|
122
|
+
def clock_hz=(clock_hz)
|
|
123
|
+
@cycle_ticks = CYCLE * Drive1541::CLOCK_HZ / clock_hz
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The head that reads, 0 or 1, on a drive with two (Disk::SIDE).
|
|
127
|
+
def side = @side_base.zero? ? 0 : 1
|
|
128
|
+
|
|
129
|
+
def side=(side)
|
|
130
|
+
side_base = side * Disk::SIDE
|
|
131
|
+
return if side_base == @side_base
|
|
132
|
+
|
|
133
|
+
@side_base = side_base
|
|
105
134
|
load_track
|
|
106
135
|
end
|
|
107
136
|
|
|
@@ -117,10 +146,10 @@ module Badline
|
|
|
117
146
|
# LED, the zone and the clock's rate, the stepper, write mode and
|
|
118
147
|
# where a write starts, and the track and cell under the head. The
|
|
119
148
|
# rest moves only while the motor turns, or as a disk goes in (see
|
|
120
|
-
#
|
|
149
|
+
# Drive::Idle).
|
|
121
150
|
def idle_state
|
|
122
|
-
[@motor, @led, @zone, @clock, @half_track, @slip, @disk, @track, @index, @mask, @cell_end,
|
|
123
|
-
@writing, @write_index, @sync]
|
|
151
|
+
[@motor, @led, @zone, @clock, @half_track, @slip, @side_base, @disk, @track, @index, @mask, @cell_end,
|
|
152
|
+
@time, @writing, @write_index, @sync, @byte_latched]
|
|
124
153
|
end
|
|
125
154
|
|
|
126
155
|
# Puts a Disk in, or takes it out with nil, flushing the disk that
|
|
@@ -178,7 +207,7 @@ module Badline
|
|
|
178
207
|
def read_bit(one)
|
|
179
208
|
if @byte_ready
|
|
180
209
|
@byte_ready = false
|
|
181
|
-
|
|
210
|
+
byte_ready_ended!
|
|
182
211
|
end
|
|
183
212
|
@shift = ((@shift << 1) & 0x3fe) | (one ? 1 : 0)
|
|
184
213
|
if one
|
|
@@ -197,7 +226,7 @@ module Badline
|
|
|
197
226
|
def write_bit(at)
|
|
198
227
|
if @byte_ready
|
|
199
228
|
@byte_ready = false
|
|
200
|
-
|
|
229
|
+
byte_ready_ended!
|
|
201
230
|
end
|
|
202
231
|
one = @write_shift.anybits?(0x80)
|
|
203
232
|
@write_shift = (@write_shift << 1) & 0xff
|
|
@@ -227,10 +256,11 @@ module Badline
|
|
|
227
256
|
# changed: a half track without data gets a blank track, and a track
|
|
228
257
|
# written at another rate is laid out again at this one.
|
|
229
258
|
def track_written(at)
|
|
230
|
-
|
|
231
|
-
@disk.
|
|
259
|
+
surface = self.surface
|
|
260
|
+
track = @disk.writable_track(surface, @zone)
|
|
261
|
+
@disk.write(surface, track.relaid(@zone)) unless track.written_at?(@zone)
|
|
232
262
|
load_track(at)
|
|
233
|
-
@disk.written(
|
|
263
|
+
@disk.written(surface)
|
|
234
264
|
@track_written = true
|
|
235
265
|
end
|
|
236
266
|
|
|
@@ -246,9 +276,9 @@ module Badline
|
|
|
246
276
|
return unless @bits == 8
|
|
247
277
|
|
|
248
278
|
@bits = 0
|
|
249
|
-
@write_shift = @writing ? @
|
|
279
|
+
@write_shift = @writing ? @via.port_a_output : @shift & 0xff
|
|
250
280
|
@byte_ready = true
|
|
251
|
-
|
|
281
|
+
byte_ready!
|
|
252
282
|
end
|
|
253
283
|
|
|
254
284
|
# The stepper's rotor turns with the head, a phase to each half
|
|
@@ -267,6 +297,10 @@ module Badline
|
|
|
267
297
|
end
|
|
268
298
|
end
|
|
269
299
|
|
|
300
|
+
# The half track under the head that reads, numbered on the disk
|
|
301
|
+
# (see Disk::SIDE).
|
|
302
|
+
def surface = @half_track + @side_base
|
|
303
|
+
|
|
270
304
|
def seek(half_track)
|
|
271
305
|
half_track = half_track.clamp(MIN_HALF_TRACK, Disk::MAX_HALF_TRACK)
|
|
272
306
|
return if half_track == @half_track
|
|
@@ -10,11 +10,10 @@ module Badline
|
|
|
10
10
|
def save_state(out)
|
|
11
11
|
out.boolean(!@disk.nil?)
|
|
12
12
|
@disk&.save_state(out)
|
|
13
|
-
[@half_track, @slip, @zone, @clock, @time, @clock_at, @clocks, @shift, @ones, @bits, @write_shift,
|
|
13
|
+
[@half_track, @slip, @side_base, @zone, @clock, @time, @clock_at, @clocks, @shift, @ones, @bits, @write_shift,
|
|
14
14
|
@index, @mask, @cell_end, @width, @next].each { |value| out.int(value) }
|
|
15
|
-
[@motor, @led, @sync, @byte_ready, @writing, @write_gate, @protected,
|
|
16
|
-
|
|
17
|
-
end
|
|
15
|
+
[@motor, @led, @sync, @byte_ready, @byte_latched, @writing, @write_gate, @protected,
|
|
16
|
+
@track_written].each { |flag| out.boolean(flag) }
|
|
18
17
|
out.optional_int(@write_index)
|
|
19
18
|
end
|
|
20
19
|
|
|
@@ -36,6 +35,7 @@ module Badline
|
|
|
36
35
|
def load_head(input)
|
|
37
36
|
@half_track = input.int
|
|
38
37
|
@slip = input.int
|
|
38
|
+
@side_base = input.int
|
|
39
39
|
@zone = input.int
|
|
40
40
|
@clock = input.int
|
|
41
41
|
@time = input.int
|
|
@@ -57,6 +57,7 @@ module Badline
|
|
|
57
57
|
@led = input.boolean?
|
|
58
58
|
@sync = input.boolean?
|
|
59
59
|
@byte_ready = input.boolean?
|
|
60
|
+
@byte_latched = input.boolean?
|
|
60
61
|
@writing = input.boolean?
|
|
61
62
|
@write_gate = input.boolean?
|
|
62
63
|
@protected = input.boolean?
|
|
@@ -30,12 +30,12 @@ module Badline
|
|
|
30
30
|
BLANK = Track.new(Array.new(Disk::TRACK_LENGTHS[0], 0).freeze, 0)
|
|
31
31
|
|
|
32
32
|
# One drive cycle: nothing with the motor off, and otherwise the
|
|
33
|
-
# disk turns on by
|
|
34
|
-
# in them.
|
|
33
|
+
# disk turns on by a cycle's ticks, 16 at 1 MHz and 8 at 2 MHz,
|
|
34
|
+
# through whatever cells and clocks fall in them.
|
|
35
35
|
def cycle!
|
|
36
36
|
return unless @motor
|
|
37
37
|
|
|
38
|
-
@time +=
|
|
38
|
+
@time += @cycle_ticks
|
|
39
39
|
pass_time if @time >= @next
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -95,7 +95,7 @@ module Badline
|
|
|
95
95
|
# The track under the head and its layout, leaving the head's place
|
|
96
96
|
# on it as it stands.
|
|
97
97
|
def head_on_track
|
|
98
|
-
@track = @disk&.track(
|
|
98
|
+
@track = @disk&.track(surface) || BLANK
|
|
99
99
|
@bytes = @track.bytes
|
|
100
100
|
@widths = @track.widths
|
|
101
101
|
@length = @track.length
|
|
@@ -105,7 +105,7 @@ module Badline
|
|
|
105
105
|
# The track under the head, from the cell that's under it +time+
|
|
106
106
|
# into the turn.
|
|
107
107
|
def load_track(time = @time)
|
|
108
|
-
@track = @disk&.track(
|
|
108
|
+
@track = @disk&.track(surface) || BLANK
|
|
109
109
|
@bytes = @track.bytes
|
|
110
110
|
@widths = @track.widths
|
|
111
111
|
@length = @track.length
|