badline 0.4.2 → 0.5.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 +25 -0
- data/CLAUDE.md +10 -5
- data/README.md +54 -23
- data/doc/pinned-behaviour.md +221 -0
- data/exe/badline-ruby +7 -4
- data/lib/badline/address_bus/ultimax_pages.rb +38 -0
- data/lib/badline/address_bus.rb +19 -29
- data/lib/badline/audio/jukebox.rb +6 -4
- data/lib/badline/audio/machine_player.rb +10 -6
- data/lib/badline/cartridge/kcs_power.rb +3 -3
- data/lib/badline/cia.rb +5 -1
- data/lib/badline/computer.rb +86 -7
- data/lib/badline/cpu.rb +39 -0
- data/lib/badline/drive1541/bus.rb +134 -0
- data/lib/badline/drive1541/disk.rb +256 -0
- data/lib/badline/drive1541/disk_via.rb +33 -0
- data/lib/badline/drive1541/gcr.rb +57 -0
- data/lib/badline/drive1541/idle.rb +184 -0
- data/lib/badline/drive1541/mechanism.rb +277 -0
- data/lib/badline/drive1541/rotation.rb +111 -0
- data/lib/badline/drive1541/sector_reader.rb +76 -0
- data/lib/badline/drive1541/serial_port.rb +67 -0
- data/lib/badline/drive1541/track.rb +98 -0
- data/lib/badline/drive1541.rb +218 -0
- data/lib/badline/gui/application.rb +15 -3
- data/lib/badline/gui/drive_led_pane.rb +29 -0
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/iec_bus.rb +93 -0
- data/lib/badline/kernal_trap/channel.rb +9 -1
- data/lib/badline/kernal_trap/serial.rb +9 -3
- data/lib/badline/media/true_drive.rb +54 -0
- data/lib/badline/media.rb +22 -1
- data/lib/badline/options.rb +24 -4
- data/lib/badline/region.rb +35 -10
- data/lib/badline/reu/dma.rb +228 -0
- data/lib/badline/reu/ram.rb +65 -0
- data/lib/badline/reu/trigger.rb +32 -0
- data/lib/badline/reu.rb +276 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/dos1541.rom +0 -0
- data/lib/badline/sdl/functions.rb +1 -0
- data/lib/badline/storage/d64_image.rb +18 -9
- data/lib/badline/storage/d81_image.rb +1 -0
- data/lib/badline/storage/disk_image/catalog.rb +52 -0
- data/lib/badline/storage/disk_image/writing.rb +16 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage/g64_image.rb +222 -0
- data/lib/badline/storage/host_directory.rb +21 -1
- data/lib/badline/storage/listing.rb +118 -0
- data/lib/badline/storage/t64.rb +14 -0
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/traps.rb +4 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +3 -0
- data/lib/badline/via/fast_forward.rb +38 -0
- data/lib/badline/via/shift_register.rb +8 -3
- data/lib/badline/via/timer1.rb +30 -0
- data/lib/badline/via/timer2.rb +25 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/display_state.rb +7 -7
- data/lib/badline/vic/sprite/internal_bus.rb +11 -7
- data/lib/badline/vic/sprite/shifter.rb +10 -15
- data/lib/badline/vic/sprite/timing.rb +60 -0
- data/lib/badline/vic/sprite.rb +14 -12
- data/lib/badline/vic/sprites.rb +2 -2
- data/lib/badline/vic.rb +86 -39
- data/lib/badline.rb +3 -0
- metadata +26 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drive1541/rotation"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class Drive1541
|
|
7
|
+
# The disk mechanism behind VIA 2: the spindle motor, the head on its
|
|
8
|
+
# stepper, the LED, the read and write electronics and the
|
|
9
|
+
# write-protect sensor.
|
|
10
|
+
#
|
|
11
|
+
# Port B drives it and reads it back:
|
|
12
|
+
#
|
|
13
|
+
# PB0-1 stepper phase (out) PB4 write protect (in, 1 = writable)
|
|
14
|
+
# PB2 motor on (out) PB5-6 bit rate, speed zone 0-3 (out)
|
|
15
|
+
# PB3 LED on (out) PB7 SYNC (in, 0 while in a SYNC mark)
|
|
16
|
+
#
|
|
17
|
+
# Port A reads the read shift register's low eight bits, which CA1
|
|
18
|
+
# latches on BYTE READY. In write mode it drives the write shift
|
|
19
|
+
# register, and the read shift register takes the bits written.
|
|
20
|
+
#
|
|
21
|
+
# The disk turns at 300 rpm under the head, and a read clock at the
|
|
22
|
+
# bit rate PB5-6 select clocks the bits off it (see Rotation).
|
|
23
|
+
#
|
|
24
|
+
# Ten 1 bits in a row are a SYNC mark while CB2 selects reading: PB7
|
|
25
|
+
# reads it, and it holds the bit counter at zero, so the first 0 bit
|
|
26
|
+
# after it starts a byte. Every eighth bit after that is BYTE READY,
|
|
27
|
+
# which pulls VIA 2's CA1 low until the next bit and reaches the CPU's
|
|
28
|
+
# SO pin (see Drive1541#byte_ready!).
|
|
29
|
+
#
|
|
30
|
+
# CB2 low selects write mode, where SYNC detection and the flux are
|
|
31
|
+
# off and the clock runs on at the selected rate. At each BYTE READY
|
|
32
|
+
# the write shift register loads what the VIA drives on port A, and
|
|
33
|
+
# the next eight clocks put that byte's bits on the track, most
|
|
34
|
+
# significant first, in place of what it held. While reading, the load
|
|
35
|
+
# takes the byte just read, so a switch to writing mid-byte writes the
|
|
36
|
+
# rest of it. A write lays its bits one to a cell from the cell under
|
|
37
|
+
# the head, so on a track written at the selected rate each bit fills
|
|
38
|
+
# the cell it passes over. A write to a half track without data gives
|
|
39
|
+
# it a blank track first, a turn long at the selected rate, and a
|
|
40
|
+
# write to a track written at another rate lays the whole track out
|
|
41
|
+
# again as a turn at the selected rate first (Track#relaid).
|
|
42
|
+
#
|
|
43
|
+
# PB4 reads the write-protect sensor: high with no disk in, or with
|
|
44
|
+
# the notch of a writable one open. With the disk write-protected the
|
|
45
|
+
# write gate stays shut: the bits and BYTE READY go on, and nothing
|
|
46
|
+
# reaches the disk. The DOS reads PB4 first and refuses to write, so
|
|
47
|
+
# only a program that writes without asking finds that out.
|
|
48
|
+
#
|
|
49
|
+
# The motor stopping flushes what was written to the disk's image
|
|
50
|
+
# (see Disk#flush), which is when the DOS is done with a job, and so
|
|
51
|
+
# does taking the disk out.
|
|
52
|
+
#
|
|
53
|
+
# The stepper moves the head a half track each time the phase steps
|
|
54
|
+
# by one, inwards for +1 and outwards for -1. Each half track has a
|
|
55
|
+
# phase of its own, from power-on the low two bits of its number, so
|
|
56
|
+
# the head sits on a whole track when phase 0 or 2 holds it. The
|
|
57
|
+
# stepper's coils are powered only while PB2 runs the motor, as VICE
|
|
58
|
+
# reads the 1541's schematic, so a phase set with the motor off moves
|
|
59
|
+
# nothing until the motor comes on and it pulls the head. Tracks run
|
|
60
|
+
# from half track 2 (track 1), where the head stops against the end of
|
|
61
|
+
# its rail, to Disk::MAX_HALF_TRACK. A step out against the stop slips:
|
|
62
|
+
# the head stays on track 1 and the phase that tried to pull it out
|
|
63
|
+
# becomes track 1's, so the DOS's bump leaves the head on track 1 in
|
|
64
|
+
# line with the phase it ends on, and its first step in reaches the
|
|
65
|
+
# half track after it.
|
|
66
|
+
class Mechanism
|
|
67
|
+
include Rotation
|
|
68
|
+
|
|
69
|
+
MOTOR = 0x04
|
|
70
|
+
LED = 0x08
|
|
71
|
+
WRITE_PROTECT = 0x10
|
|
72
|
+
SYNC = 0x80
|
|
73
|
+
|
|
74
|
+
MIN_HALF_TRACK = 2
|
|
75
|
+
# Where the head sits at power-on: track 18, the directory.
|
|
76
|
+
START_HALF_TRACK = 36
|
|
77
|
+
|
|
78
|
+
SYNC_BITS = 10
|
|
79
|
+
|
|
80
|
+
attr_reader :disk, :half_track, :zone
|
|
81
|
+
|
|
82
|
+
def initialize(drive)
|
|
83
|
+
@drive = drive
|
|
84
|
+
@disk = nil
|
|
85
|
+
@half_track = START_HALF_TRACK
|
|
86
|
+
@slip = 0
|
|
87
|
+
@motor = false
|
|
88
|
+
@led = false
|
|
89
|
+
@zone = 0
|
|
90
|
+
@clock = Track.cell(0)
|
|
91
|
+
@time = 0
|
|
92
|
+
@clock_at = @clock / 2
|
|
93
|
+
@clocks = 0
|
|
94
|
+
@shift = 0
|
|
95
|
+
@ones = 0
|
|
96
|
+
@bits = 0
|
|
97
|
+
@sync = false
|
|
98
|
+
@byte_ready = false
|
|
99
|
+
@writing = false
|
|
100
|
+
@write_shift = 0
|
|
101
|
+
@write_gate = false
|
|
102
|
+
@protected = false
|
|
103
|
+
load_track
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def motor_on? = @motor
|
|
107
|
+
|
|
108
|
+
def led_on? = @led
|
|
109
|
+
|
|
110
|
+
def sync? = @sync
|
|
111
|
+
|
|
112
|
+
def writing? = @writing
|
|
113
|
+
|
|
114
|
+
# What port B and CB2 can change with the motor off: the motor, the
|
|
115
|
+
# LED, the zone and the clock's rate, the stepper, write mode and
|
|
116
|
+
# where a write starts, and the track and cell under the head. The
|
|
117
|
+
# rest moves only while the motor turns, or as a disk goes in (see
|
|
118
|
+
# Drive1541::Idle).
|
|
119
|
+
def idle_state
|
|
120
|
+
[@motor, @led, @zone, @clock, @half_track, @slip, @disk, @track, @index, @mask, @cell_end, @time,
|
|
121
|
+
@writing, @write_index, @sync]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Puts a Disk in, or takes it out with nil, flushing the disk that
|
|
125
|
+
# was in. The head goes on from the same point in the turn.
|
|
126
|
+
def insert(disk)
|
|
127
|
+
flush
|
|
128
|
+
@disk = disk
|
|
129
|
+
@protected = disk&.write_protected? || false
|
|
130
|
+
@write_gate = !disk.nil? && !@protected
|
|
131
|
+
load_track
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Stores what the head wrote in the disk's image.
|
|
135
|
+
def flush
|
|
136
|
+
@disk.flush if @disk&.written?
|
|
137
|
+
rescue Storage::WriteError => e
|
|
138
|
+
warn "1541: couldn't write the disk back: #{e.message}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# VIA 2 drives CB2 to +high+: low selects write mode, which stops
|
|
142
|
+
# SYNC detection at once.
|
|
143
|
+
def cb2_written(high)
|
|
144
|
+
@write_index = nil if high || !@writing
|
|
145
|
+
@writing = !high
|
|
146
|
+
@sync = !@writing && @ones >= SYNC_BITS
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# VIA 2 drives port B's lines to +lines+.
|
|
150
|
+
def port_b_written(lines)
|
|
151
|
+
motor = @motor
|
|
152
|
+
@motor = lines.anybits?(MOTOR)
|
|
153
|
+
flush if motor && !@motor
|
|
154
|
+
@led = lines.anybits?(LED)
|
|
155
|
+
zone = (lines >> 5) & 0x03
|
|
156
|
+
if zone != @zone
|
|
157
|
+
@zone = zone
|
|
158
|
+
@clock = Track.cell(zone)
|
|
159
|
+
@track_written = false
|
|
160
|
+
end
|
|
161
|
+
step(lines & 0x03) if @motor
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Port A is the read shift register.
|
|
165
|
+
def read_a(_lines) = @shift & 0xff
|
|
166
|
+
|
|
167
|
+
# SYNC low in a SYNC mark, and write protect low for a protected
|
|
168
|
+
# disk.
|
|
169
|
+
def read_b(_lines)
|
|
170
|
+
lines = @sync ? 0xff & ~SYNC : 0xff
|
|
171
|
+
@protected ? lines & ~WRITE_PROTECT : lines
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def read_bit(one)
|
|
177
|
+
if @byte_ready
|
|
178
|
+
@byte_ready = false
|
|
179
|
+
@drive.byte_ready_ended!
|
|
180
|
+
end
|
|
181
|
+
@shift = ((@shift << 1) & 0x3fe) | (one ? 1 : 0)
|
|
182
|
+
if one
|
|
183
|
+
@ones += 1
|
|
184
|
+
@sync = true if @ones >= SYNC_BITS
|
|
185
|
+
else
|
|
186
|
+
@ones = 0
|
|
187
|
+
@sync = false
|
|
188
|
+
end
|
|
189
|
+
count_bit
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The write shift register's top bit goes onto the track through the
|
|
193
|
+
# write gate, and into the read shift register, as the head's own
|
|
194
|
+
# signal.
|
|
195
|
+
def write_bit(at)
|
|
196
|
+
if @byte_ready
|
|
197
|
+
@byte_ready = false
|
|
198
|
+
@drive.byte_ready_ended!
|
|
199
|
+
end
|
|
200
|
+
one = @write_shift.anybits?(0x80)
|
|
201
|
+
@write_shift = (@write_shift << 1) & 0xff
|
|
202
|
+
store_bit(one, at) if @write_gate
|
|
203
|
+
@shift = ((@shift << 1) & 0x3fe) | (one ? 1 : 0)
|
|
204
|
+
@ones = one ? @ones + 1 : 0
|
|
205
|
+
count_bit
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Each bit written goes in the cell after the last one's, from the
|
|
209
|
+
# cell under the head when the write started.
|
|
210
|
+
def store_bit(one, at)
|
|
211
|
+
track_written(at) unless @track_written
|
|
212
|
+
@write_index ||= (@index * 8) + 8 - @mask.bit_length
|
|
213
|
+
index = @write_index >> 3
|
|
214
|
+
mask = 0x80 >> (@write_index & 7)
|
|
215
|
+
if one
|
|
216
|
+
@bytes[index] |= mask
|
|
217
|
+
else
|
|
218
|
+
@bytes[index] &= ~mask
|
|
219
|
+
end
|
|
220
|
+
@write_index += 1
|
|
221
|
+
@write_index = 0 if @write_index == @length * 8
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The first write to the track since the head came to it or the rate
|
|
225
|
+
# changed: a half track without data gets a blank track, and a track
|
|
226
|
+
# written at another rate is laid out again at this one.
|
|
227
|
+
def track_written(at)
|
|
228
|
+
track = @disk.writable_track(@half_track, @zone)
|
|
229
|
+
@disk.write(@half_track, track.relaid(@zone)) unless track.written_at?(@zone)
|
|
230
|
+
load_track(at)
|
|
231
|
+
@disk.written(@half_track)
|
|
232
|
+
@track_written = true
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Every eighth bit outside a SYNC mark is BYTE READY, which loads the
|
|
236
|
+
# write shift register: from what the VIA drives on port A in write
|
|
237
|
+
# mode, and with the byte just read otherwise.
|
|
238
|
+
def count_bit
|
|
239
|
+
if @sync
|
|
240
|
+
@bits = 0
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
@bits += 1
|
|
244
|
+
return unless @bits == 8
|
|
245
|
+
|
|
246
|
+
@bits = 0
|
|
247
|
+
@write_shift = @writing ? @drive.via2.port_a_output : @shift & 0xff
|
|
248
|
+
@byte_ready = true
|
|
249
|
+
@drive.byte_ready!
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# The stepper's rotor turns with the head, a phase to each half
|
|
253
|
+
# track, so the energized phase pulls the head to the neighbouring
|
|
254
|
+
# half track it belongs to. A phase two away pulls both ways, and the
|
|
255
|
+
# head stays. Against the stop at track 1 the head can't follow, and
|
|
256
|
+
# the phases slip round to where the head is.
|
|
257
|
+
def step(phase)
|
|
258
|
+
move = (phase - @half_track - @slip) & 0x03
|
|
259
|
+
if move == 1
|
|
260
|
+
seek(@half_track + 1)
|
|
261
|
+
elsif move == 3 && @half_track == MIN_HALF_TRACK
|
|
262
|
+
@slip = (phase - MIN_HALF_TRACK) & 0x03
|
|
263
|
+
elsif move == 3
|
|
264
|
+
seek(@half_track - 1)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def seek(half_track)
|
|
269
|
+
half_track = half_track.clamp(MIN_HALF_TRACK, Disk::MAX_HALF_TRACK)
|
|
270
|
+
return if half_track == @half_track
|
|
271
|
+
|
|
272
|
+
@half_track = half_track
|
|
273
|
+
load_track
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# The disk turning under the Mechanism's head, and its read clock.
|
|
6
|
+
#
|
|
7
|
+
# The disk turns at 300 rpm, whatever the bit rate: once in 200 ms, or
|
|
8
|
+
# 200,000 drive cycles. Each track's bits pass under the head at the
|
|
9
|
+
# rate they were written at (see Track), and a 1 bit is a flux
|
|
10
|
+
# transition at the start of its cell.
|
|
11
|
+
#
|
|
12
|
+
# The read circuit clocks bits out at the rate PB5-6 select, from a
|
|
13
|
+
# counter the 16 MHz crystal steps every 16 - zone ticks. A flux
|
|
14
|
+
# transition clears the counter. The counter clocks a bit into the
|
|
15
|
+
# read shift register as it reaches 2 and every four steps after
|
|
16
|
+
# that, and the bit is a 1 only for the first clock after the counter
|
|
17
|
+
# passed 0. A bit is 4 * (16 - zone) ticks, so a byte takes 32, 30, 28
|
|
18
|
+
# or 26 drive cycles in zones 0 to 3, and each transition brings the
|
|
19
|
+
# clock back into step with the flux. A track read at the rate it was
|
|
20
|
+
# written reads its bits. At another rate the clock and the cells
|
|
21
|
+
# drift apart between transitions: a faster clock slips an extra 0
|
|
22
|
+
# bit into some runs of 0 bits, a slower one drops one from some, and
|
|
23
|
+
# the bytes come out garbled. Without flux the counter runs round on
|
|
24
|
+
# its own and reads a 1 bit in every four.
|
|
25
|
+
module Rotation
|
|
26
|
+
CYCLE = 16 * Track::TICK
|
|
27
|
+
|
|
28
|
+
# What the head reads with no disk in, or off the tracks a disk has:
|
|
29
|
+
# no flux.
|
|
30
|
+
BLANK = Track.new(Array.new(Disk::TRACK_LENGTHS[0], 0).freeze, 0)
|
|
31
|
+
|
|
32
|
+
# One drive cycle: nothing with the motor off, and otherwise the
|
|
33
|
+
# disk turns on by 16 ticks, through whatever cells and clocks fall
|
|
34
|
+
# in them.
|
|
35
|
+
def cycle!
|
|
36
|
+
return unless @motor
|
|
37
|
+
|
|
38
|
+
@time += CYCLE
|
|
39
|
+
pass_time if @time >= @next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# The cell edges and clocks up to now, in the order they come. A
|
|
45
|
+
# clock that falls on a cell edge comes after it.
|
|
46
|
+
def pass_time
|
|
47
|
+
while @cell_end <= @time || @clock_at <= @time
|
|
48
|
+
if @cell_end <= @clock_at
|
|
49
|
+
next_cell
|
|
50
|
+
else
|
|
51
|
+
clock
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
@next = [@cell_end, @clock_at].min
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The head comes to the next cell, and a 1 bit's flux clears the
|
|
58
|
+
# read clock while reading.
|
|
59
|
+
def next_cell
|
|
60
|
+
start = @cell_end
|
|
61
|
+
@mask >>= 1
|
|
62
|
+
start = next_byte(start) if @mask.zero?
|
|
63
|
+
@cell_end = @index == @last && @mask == 1 ? Track::TURN : start + @width
|
|
64
|
+
return if @writing || @bytes[@index].nobits?(@mask)
|
|
65
|
+
|
|
66
|
+
@clock_at = start + (@clock / 2)
|
|
67
|
+
@clocks = 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The head comes to the next byte, and at the end of the track back
|
|
71
|
+
# round to the first, where the turn starts over.
|
|
72
|
+
def next_byte(start)
|
|
73
|
+
@mask = 0x80
|
|
74
|
+
@index += 1
|
|
75
|
+
if @index == @length
|
|
76
|
+
@index = 0
|
|
77
|
+
@time -= Track::TURN
|
|
78
|
+
@clock_at -= Track::TURN
|
|
79
|
+
start = 0
|
|
80
|
+
end
|
|
81
|
+
@width = @widths[@index] if @widths
|
|
82
|
+
start
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The read clock ticks a bit into the shift register: a 1 the first
|
|
86
|
+
# time after a flux transition or the counter's wrap.
|
|
87
|
+
def clock
|
|
88
|
+
at = @clock_at
|
|
89
|
+
one = @clocks.zero?
|
|
90
|
+
@clocks = (@clocks + 1) & 3
|
|
91
|
+
@clock_at = at + @clock
|
|
92
|
+
@writing ? write_bit(at) : read_bit(one)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The track under the head, from the cell that's under it +time+
|
|
96
|
+
# into the turn.
|
|
97
|
+
def load_track(time = @time)
|
|
98
|
+
@track = @disk&.track(@half_track) || BLANK
|
|
99
|
+
@bytes = @track.bytes
|
|
100
|
+
@widths = @track.widths
|
|
101
|
+
@length = @track.length
|
|
102
|
+
@last = @length - 1
|
|
103
|
+
@index, @mask, @cell_end = @track.cell_at(time)
|
|
104
|
+
@width = @widths ? @widths[@index] : @track.width
|
|
105
|
+
@write_index = nil
|
|
106
|
+
@track_written = false
|
|
107
|
+
@next = [@cell_end, @clock_at].min
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# Reads a track's blocks back out of its GCR, as the DOS finds them:
|
|
6
|
+
# each SYNC mark is ten or more 1 bits, and the block after it starts
|
|
7
|
+
# at the first 0 bit. Writes land wherever the head was, so a block
|
|
8
|
+
# can start at any bit of the stored bytes, and the reader works on
|
|
9
|
+
# the bits.
|
|
10
|
+
#
|
|
11
|
+
# A header block starts with $08, and the next block after a header is
|
|
12
|
+
# that sector's data block, whatever it holds.
|
|
13
|
+
module SectorReader
|
|
14
|
+
HEADER_BITS = 10 * 8
|
|
15
|
+
DATA_BITS = 325 * 8
|
|
16
|
+
SYNC = /1{10,}/
|
|
17
|
+
|
|
18
|
+
# A sector as read: its decoded header, [ID, checksum, sector, track,
|
|
19
|
+
# the second ID byte, the first, ...], and its decoded data block,
|
|
20
|
+
# [ID, 256 data bytes, checksum, ...], nil when there isn't one that
|
|
21
|
+
# decodes.
|
|
22
|
+
Sector = Struct.new(:header, :data)
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# The sectors found on the track's bytes, by sector number. Headers
|
|
27
|
+
# for another track are left out.
|
|
28
|
+
def read(bytes, track)
|
|
29
|
+
ring = ring(bytes)
|
|
30
|
+
return {} unless ring
|
|
31
|
+
|
|
32
|
+
sectors = {}
|
|
33
|
+
each_block(ring) do |header, data|
|
|
34
|
+
next unless header[3] == track
|
|
35
|
+
|
|
36
|
+
sectors[header[2]] ||= Sector.new(header, data)
|
|
37
|
+
end
|
|
38
|
+
sectors
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The track's bits, starting at a 0 bit so no SYNC mark straddles the
|
|
42
|
+
# start, twice over so a block can run on past the end. Nil for a
|
|
43
|
+
# track without a 0 bit.
|
|
44
|
+
def ring(bytes)
|
|
45
|
+
bits = bytes.pack("C*").unpack1("B*")
|
|
46
|
+
zero = bits.index("0")
|
|
47
|
+
return unless zero
|
|
48
|
+
|
|
49
|
+
bits = bits[zero..] + bits[0, zero]
|
|
50
|
+
[bits * 2, bits.length]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Yields each header whose SYNC mark starts in the first turn, with
|
|
54
|
+
# the data block after it.
|
|
55
|
+
def each_block(ring)
|
|
56
|
+
bits, length = ring
|
|
57
|
+
pos = 0
|
|
58
|
+
while (sync = SYNC.match(bits, pos)) && sync.begin(0) < length
|
|
59
|
+
pos = sync.end(0)
|
|
60
|
+
header = decode(bits, pos, HEADER_BITS)
|
|
61
|
+
next unless header && header[0] == Disk::HEADER_ID
|
|
62
|
+
|
|
63
|
+
data_sync = SYNC.match(bits, pos + HEADER_BITS)
|
|
64
|
+
yield header, data_sync && decode(bits, data_sync.end(0), DATA_BITS)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def decode(bits, pos, count)
|
|
69
|
+
slice = bits[pos, count]
|
|
70
|
+
return unless slice.length == count
|
|
71
|
+
|
|
72
|
+
GCR.decode([slice].pack("B*").bytes)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# What VIA 1's ports read from outside the chip. Port B carries the
|
|
6
|
+
# serial bus inputs and the device number jumpers; port A goes to the
|
|
7
|
+
# unused parallel header and floats high.
|
|
8
|
+
#
|
|
9
|
+
# DATA IN (PB0), CLK IN (PB2) and ATN IN (PB7) come through inverters,
|
|
10
|
+
# so a released line reads 0 and a pulled one 1. The jumpers on PB5 and
|
|
11
|
+
# PB6 pull a line low when closed, and both closed make device 8.
|
|
12
|
+
#
|
|
13
|
+
# The drive sees the C64's side of the bus a host cycle late. The C64's
|
|
14
|
+
# CIA changes its pins at the end of the cycle that writes them, and
|
|
15
|
+
# the drive's VIA samples its own late in each of its cycles, so a
|
|
16
|
+
# drive cycle that runs alongside the write reads the lines from
|
|
17
|
+
# before it. The drive runs after the C64 in each host cycle (see
|
|
18
|
+
# Computer#cycle!), so the port reads the C64's lines as latch_host
|
|
19
|
+
# took them at the end of the last one. Fast loaders that time their
|
|
20
|
+
# transfer from an ATN edge, as VICE's drive tests do, depend on it.
|
|
21
|
+
class SerialPort
|
|
22
|
+
DATA_IN = 0x01
|
|
23
|
+
CLK_IN = 0x04
|
|
24
|
+
ATN_IN = 0x80
|
|
25
|
+
SERIAL_INPUTS = DATA_IN | CLK_IN | ATN_IN
|
|
26
|
+
JUMPERS = 0b0110_0000
|
|
27
|
+
|
|
28
|
+
attr_reader :bus
|
|
29
|
+
|
|
30
|
+
def initialize(device: 8, bus: nil)
|
|
31
|
+
@port_b = (0xff & ~(SERIAL_INPUTS | JUMPERS)) | (((device - 8) & 0x03) << 5)
|
|
32
|
+
@host = 0
|
|
33
|
+
self.bus = bus
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def bus=(bus)
|
|
37
|
+
@bus = bus
|
|
38
|
+
latch_host
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Takes the C64's lines as they stand, for the drive cycles of the
|
|
42
|
+
# next host cycle.
|
|
43
|
+
def latch_host
|
|
44
|
+
@host = @bus ? @bus.host_lines : 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Whether the C64 has moved ATN since the last latch_host.
|
|
48
|
+
def atn_moved?
|
|
49
|
+
lines = @bus ? @bus.host_lines : 0
|
|
50
|
+
(lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def atn_low? = @bus.atn_low?(@host)
|
|
54
|
+
|
|
55
|
+
def read_a(_lines) = 0xff
|
|
56
|
+
|
|
57
|
+
def read_b(_lines)
|
|
58
|
+
low = @bus.low_lines(@host)
|
|
59
|
+
value = @port_b
|
|
60
|
+
value |= DATA_IN if low.anybits?(IECBus::DATA)
|
|
61
|
+
value |= CLK_IN if low.anybits?(IECBus::CLK)
|
|
62
|
+
value |= ATN_IN if low.anybits?(IECBus::ATN)
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1541
|
|
5
|
+
# One ring of flux on the disk, as the GCR bytes the head reads from it,
|
|
6
|
+
# most significant bit first, and the speed zone it was written in
|
|
7
|
+
# (0-3). The bytes wrap around: the last one's bits run into the
|
|
8
|
+
# first's. A track is as long as its bytes, which is what a .g64 image
|
|
9
|
+
# stores for each half track.
|
|
10
|
+
#
|
|
11
|
+
# The disk turns at 300 rpm, so every track passes under the head in
|
|
12
|
+
# the same 200 ms, and its bits pass at the rate they were written at.
|
|
13
|
+
# Each bit is a cell of time, starting at the track's index angle with
|
|
14
|
+
# the first byte's top bit, and a 1 bit is a flux transition at the
|
|
15
|
+
# start of its cell. The cells are spread evenly over the turn, so a
|
|
16
|
+
# track as long as a turn holds at its zone's rate
|
|
17
|
+
# (Disk::TRACK_LENGTHS) passes at that rate, within the part of a byte
|
|
18
|
+
# the length rounds off, and a longer or shorter one faster or slower.
|
|
19
|
+
# A .g64 speed map gives each byte the zone it was written in, and the
|
|
20
|
+
# cells of each byte are as wide as their zone's, scaled together to
|
|
21
|
+
# fill the turn.
|
|
22
|
+
#
|
|
23
|
+
# Times are in TICKs, 1/65536 of a tick of the drive's 16 MHz crystal,
|
|
24
|
+
# so the cells add up to a turn within a tick.
|
|
25
|
+
class Track
|
|
26
|
+
TICK = 1 << 16
|
|
27
|
+
TURN = 200_000 * 16 * TICK
|
|
28
|
+
|
|
29
|
+
# A bit cell at the zone's rate, in TICKs.
|
|
30
|
+
def self.cell(zone) = 4 * (16 - zone) * TICK
|
|
31
|
+
|
|
32
|
+
attr_reader :bytes, :zone, :speeds, :width, :widths
|
|
33
|
+
|
|
34
|
+
# +speeds+ holds each byte's zone, where the track's rate changes
|
|
35
|
+
# along it.
|
|
36
|
+
def initialize(bytes, zone, speeds = nil)
|
|
37
|
+
@bytes = bytes
|
|
38
|
+
@zone = zone
|
|
39
|
+
@speeds = speeds
|
|
40
|
+
@widths = speeds && scaled_widths(speeds)
|
|
41
|
+
@width = speeds ? @widths[0] : TURN / (bytes.length * 8)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def length = @bytes.length
|
|
45
|
+
|
|
46
|
+
# Whether the whole track was written at the +zone+'s rate.
|
|
47
|
+
def written_at?(zone) = @widths.nil? && @zone == zone
|
|
48
|
+
|
|
49
|
+
# The bit cell under the head +time+ TICKs into the turn, as
|
|
50
|
+
# [byte, mask, the time the cell ends].
|
|
51
|
+
def cell_at(time)
|
|
52
|
+
start = 0
|
|
53
|
+
index = 0
|
|
54
|
+
if @widths
|
|
55
|
+
while index < length - 1 && time >= start + (@widths[index] * 8)
|
|
56
|
+
start += @widths[index] * 8
|
|
57
|
+
index += 1
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
index = [time / (@width * 8), length - 1].min
|
|
61
|
+
start = index * @width * 8
|
|
62
|
+
end
|
|
63
|
+
width = @widths ? @widths[index] : @width
|
|
64
|
+
bit = [(time - start) / width, 7].min
|
|
65
|
+
cell_end = index == length - 1 && bit == 7 ? TURN : start + ((bit + 1) * width)
|
|
66
|
+
[index, 0x80 >> bit, cell_end]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The track laid out again as one turn at the +zone+'s rate, each
|
|
70
|
+
# flux transition in the cell under it.
|
|
71
|
+
def relaid(zone)
|
|
72
|
+
bits = Disk::TRACK_LENGTHS[zone] * 8
|
|
73
|
+
cell = TURN / bits
|
|
74
|
+
relaid = Array.new(bits / 8, 0)
|
|
75
|
+
time = 0
|
|
76
|
+
@bytes.each_with_index do |byte, index|
|
|
77
|
+
width = @widths ? @widths[index] : @width
|
|
78
|
+
8.times do |bit|
|
|
79
|
+
if byte.anybits?(0x80 >> bit)
|
|
80
|
+
cell_index = [time / cell, bits - 1].min
|
|
81
|
+
relaid[cell_index >> 3] |= 0x80 >> (cell_index & 7)
|
|
82
|
+
end
|
|
83
|
+
time += width
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
Track.new(relaid, zone)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def scaled_widths(speeds)
|
|
92
|
+
units = 0
|
|
93
|
+
speeds.each { |zone| units += 16 - zone }
|
|
94
|
+
speeds.map { |zone| TURN * (16 - zone) / (units * 8) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|