badline 0.11.1 → 0.12.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 +18 -0
- data/CLAUDE.md +8 -7
- data/CONTRIBUTING.md +7 -6
- data/README.md +12 -5
- data/doc/media.md +28 -1
- data/doc/pinned-behaviour.md +31 -1
- data/exe/badline-ruby +1 -1
- data/lib/badline/c128/bus.rb +4 -17
- data/lib/badline/c128/drives.rb +35 -22
- data/lib/badline/c128/fast_serial.rb +1 -0
- data/lib/badline/c128/io_pages.rb +35 -0
- data/lib/badline/c128/mmu.rb +9 -7
- data/lib/badline/c128/modes.rb +17 -4
- data/lib/badline/c128/saved_state.rb +22 -0
- data/lib/badline/c128/z80.rb +49 -0
- data/lib/badline/c128/z80_pages.rb +118 -0
- data/lib/badline/c128/z80_turns.rb +106 -0
- data/lib/badline/c128.rb +16 -19
- data/lib/badline/cartridge/action_replay.rb +2 -2
- data/lib/badline/cartridge/comal80.rb +1 -7
- data/lib/badline/cartridge/dinamic.rb +0 -8
- data/lib/badline/cartridge/final_cartridge3.rb +6 -6
- data/lib/badline/cartridge/fun_play.rb +0 -8
- data/lib/badline/cartridge/game_system.rb +0 -8
- data/lib/badline/cartridge/kcs_power.rb +10 -7
- data/lib/badline/cartridge/mach5.rb +2 -4
- data/lib/badline/cartridge/magic_desk.rb +0 -8
- data/lib/badline/cartridge/ocean.rb +0 -8
- data/lib/badline/cartridge/pagefox.rb +2 -2
- data/lib/badline/cartridge/retro_replay.rb +7 -6
- data/lib/badline/cartridge/rex_utility.rb +2 -6
- data/lib/badline/cartridge/rgcd.rb +2 -2
- data/lib/badline/cartridge/simons_basic.rb +0 -5
- data/lib/badline/cartridge/super_games.rb +4 -2
- data/lib/badline/cartridge/westermann.rb +2 -6
- data/lib/badline/cartridge.rb +37 -9
- data/lib/badline/cia/fast_forward.rb +21 -3
- data/lib/badline/cia.rb +8 -5
- data/lib/badline/computer/attachments.rb +4 -56
- data/lib/badline/computer/true_drives.rb +80 -0
- data/lib/badline/computer.rb +4 -0
- data/lib/badline/core.rb +2 -0
- data/lib/badline/drive/core.rb +8 -33
- data/lib/badline/drive/fast_serial.rb +73 -0
- data/lib/badline/drive/idle.rb +1 -11
- data/lib/badline/drive/orbit.rb +2 -5
- data/lib/badline/drive/sleep.rb +0 -2
- data/lib/badline/drive/vias.rb +84 -0
- data/lib/badline/drive.rb +2 -0
- data/lib/badline/drive1541.rb +1 -0
- data/lib/badline/drive1571/wd1770.rb +94 -8
- data/lib/badline/drive1571.rb +8 -58
- data/lib/badline/drive1581/bus.rb +137 -0
- data/lib/badline/drive1581/cpu.rb +11 -0
- data/lib/badline/drive1581/disk.rb +135 -0
- data/lib/badline/drive1581/disk_state.rb +87 -0
- data/lib/badline/drive1581/mechanism.rb +117 -0
- data/lib/badline/drive1581/serial_port.rb +52 -0
- data/lib/badline/drive1581/slot.rb +78 -0
- data/lib/badline/drive1581/track.rb +100 -0
- data/lib/badline/drive1581/track_bytes.rb +75 -0
- data/lib/badline/drive1581/wd1772/registers.rb +89 -0
- data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
- data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
- data/lib/badline/drive1581/wd1772/state.rb +66 -0
- data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
- data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
- data/lib/badline/drive1581/wd1772/turn.rb +73 -0
- data/lib/badline/drive1581/wd1772.rb +216 -0
- data/lib/badline/drive1581.rb +233 -0
- data/lib/badline/frontend/app.rb +1 -3
- data/lib/badline/frontend/audio_device.rb +67 -0
- data/lib/badline/frontend/audio_sink.rb +16 -50
- data/lib/badline/frontend/boot.rb +18 -10
- data/lib/badline/frontend/buttons.rb +7 -7
- data/lib/badline/frontend/keys.rb +33 -8
- data/lib/badline/frontend/media_slots.rb +42 -0
- data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
- data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
- data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
- data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
- data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
- data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
- data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
- data/lib/badline/frontend/menu/theme.rb +18 -0
- data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
- data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
- data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
- data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
- data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
- data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
- data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
- data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
- data/lib/badline/frontend/player/theme.rb +18 -0
- data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
- data/lib/badline/frontend/sound.rb +19 -38
- data/lib/badline/frontend/timeline.rb +3 -14
- data/lib/badline/frontend.rb +24 -20
- data/lib/badline/iec_bus.rb +14 -2
- data/lib/badline/machine.rb +5 -4
- data/lib/badline/media/boot_disk.rb +50 -0
- data/lib/badline/media/not_disk.rb +8 -0
- data/lib/badline/media/true_drive.rb +35 -15
- data/lib/badline/media.rb +9 -3
- data/lib/badline/options/event.rb +2 -2
- data/lib/badline/options/family.rb +3 -9
- data/lib/badline/options/help.rb +2 -1
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1581.rom +0 -0
- data/lib/badline/sdl.rb +37 -20
- data/lib/badline/snapshot/state.rb +5 -4
- data/lib/badline/storage/p00.rb +8 -0
- data/lib/badline/time_of_day.rb +3 -3
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/attachments.rb +21 -7
- data/lib/badline/vic20/saved_state.rb +2 -0
- data/lib/badline/vic20.rb +3 -0
- data/lib/badline/z80/core.rb +2 -0
- data/lib/badline/z80/saved_state.rb +69 -0
- metadata +54 -21
- /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
- /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
- /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
- /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
# The 1581's 3.5" mechanism: the spindle motor, the head on one of
|
|
6
|
+
# its cylinders and sides, the index sensor, and the lines it reports
|
|
7
|
+
# on the drive connector (1581 Service Manual, schematic sheet 2).
|
|
8
|
+
#
|
|
9
|
+
# The motor runs while /MOTOR is low and turns a disk at 300 rpm, a
|
|
10
|
+
# turn every REVOLUTION drive cycles at 2 MHz. The index sensor pulses
|
|
11
|
+
# for INDEX_PULSE cycles at the start of each turn, which is where a
|
|
12
|
+
# Track's bytes start. /RDY is low while the motor turns a disk. The
|
|
13
|
+
# WD1772 steps the head, and /TR00 is low while it sits on cylinder 0.
|
|
14
|
+
# /WPRT is low while the disk's write-protect hole is open, or without
|
|
15
|
+
# a disk.
|
|
16
|
+
#
|
|
17
|
+
# /DISK CHNG goes low as a disk comes out, and at power-on, and goes
|
|
18
|
+
# high again at a step pulse with a disk in, as on any Shugart-style
|
|
19
|
+
# drive with a disk change line. The DOS steps in and out after
|
|
20
|
+
# spinning the motor up to find out whether a disk is in.
|
|
21
|
+
#
|
|
22
|
+
# Times are the WD1772's cycle count (WD1772#now), which the drive
|
|
23
|
+
# passes in: the angle under the head follows from how long the disk
|
|
24
|
+
# has turned.
|
|
25
|
+
class Mechanism
|
|
26
|
+
REVOLUTION = 400_000
|
|
27
|
+
INDEX_PULSE = 4_000
|
|
28
|
+
LAST_CYLINDER = 83
|
|
29
|
+
|
|
30
|
+
attr_reader :disk, :cylinder
|
|
31
|
+
attr_accessor :side
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@disk = nil
|
|
35
|
+
@cylinder = 0
|
|
36
|
+
@side = 0
|
|
37
|
+
@motor = false
|
|
38
|
+
@changed = true
|
|
39
|
+
@angle = 0
|
|
40
|
+
@since = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Puts a Disk in, or with nil takes it out. Whatever was in is
|
|
44
|
+
# flushed first, and the disk change line goes low.
|
|
45
|
+
def insert(disk, now)
|
|
46
|
+
@angle = angle(now)
|
|
47
|
+
@since = now
|
|
48
|
+
flush
|
|
49
|
+
@disk = disk
|
|
50
|
+
@changed = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def flush = @disk&.flush
|
|
54
|
+
|
|
55
|
+
def motor_on? = @motor
|
|
56
|
+
|
|
57
|
+
# Turns the motor on or off at +now+.
|
|
58
|
+
def motor(on, now)
|
|
59
|
+
@angle = angle(now)
|
|
60
|
+
@since = now
|
|
61
|
+
@motor = on
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Whether a disk turns.
|
|
65
|
+
def spinning? = @motor && !@disk.nil?
|
|
66
|
+
|
|
67
|
+
# How far into its turn the disk is at +now+, in cycles from the
|
|
68
|
+
# index.
|
|
69
|
+
def angle(now) = spinning? ? (@angle + now - @since) % REVOLUTION : @angle
|
|
70
|
+
|
|
71
|
+
def index?(now) = spinning? && angle(now) < INDEX_PULSE
|
|
72
|
+
|
|
73
|
+
def ready? = spinning?
|
|
74
|
+
|
|
75
|
+
def track0? = @cylinder.zero?
|
|
76
|
+
|
|
77
|
+
def disk_changed? = @changed
|
|
78
|
+
|
|
79
|
+
def write_protected? = @disk.nil? || @disk.write_protected?
|
|
80
|
+
|
|
81
|
+
# A step pulse, towards the hub with +inward+.
|
|
82
|
+
def step(inward)
|
|
83
|
+
@cylinder = (@cylinder + (inward ? 1 : -1)).clamp(0, LAST_CYLINDER)
|
|
84
|
+
@changed = false if @disk
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The Track under the head, or nil.
|
|
88
|
+
def track = @disk&.track(@cylinder, @side)
|
|
89
|
+
|
|
90
|
+
# Stores a track the WD1772 laid down under the head.
|
|
91
|
+
def write(track)
|
|
92
|
+
@disk&.write(@cylinder, @side, track)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The disk (Disk#save_state) and the motor, the head and the turn.
|
|
96
|
+
def save_state(out)
|
|
97
|
+
out.boolean(!@disk.nil?)
|
|
98
|
+
@disk&.save_state(out)
|
|
99
|
+
out.int(@cylinder).int(@side).int(@angle).int(@since).boolean(@motor).boolean(@changed)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Puts the state back, reusing the disk in when it's the one the state
|
|
103
|
+
# names (Disk::State.load). A disk taken out is flushed.
|
|
104
|
+
def load_state(input)
|
|
105
|
+
disk = input.boolean? ? Disk::State.load(input, @disk) : nil
|
|
106
|
+
flush unless disk.equal?(@disk)
|
|
107
|
+
@disk = disk
|
|
108
|
+
@cylinder = input.int
|
|
109
|
+
@side = input.int
|
|
110
|
+
@angle = input.int
|
|
111
|
+
@since = input.int
|
|
112
|
+
@motor = input.boolean?
|
|
113
|
+
@changed = input.boolean?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
# What the 8520's ports read from outside the chip (1581 Service
|
|
6
|
+
# Manual, schematic sheet 3; the DOS's I/O definitions):
|
|
7
|
+
#
|
|
8
|
+
# PA0 side select (out) PB0 DATA IN (in)
|
|
9
|
+
# PA1 /RDY from the mechanism PB1 DATA OUT (out)
|
|
10
|
+
# PA2 /MOTOR (out) PB2 CLK IN (in)
|
|
11
|
+
# PA3 device number switch 1 PB3 CLK OUT (out)
|
|
12
|
+
# PA4 device number switch 2 PB4 ATN ACK (out)
|
|
13
|
+
# PA5 power LED (out) PB5 fast serial direction (out)
|
|
14
|
+
# PA6 activity LED (out) PB6 /WPRT from the mechanism
|
|
15
|
+
# PA7 /DISK CHNG from the mech. PB7 ATN IN (in)
|
|
16
|
+
#
|
|
17
|
+
# DATA IN, CLK IN and ATN IN come through inverters, so a pulled line
|
|
18
|
+
# reads 1, as on the 1541 (Drive::SerialLines). A closed switch pulls
|
|
19
|
+
# its line low, and both closed make device 8. The ports' outputs
|
|
20
|
+
# read back as driven: the peripheral pulls only the inputs.
|
|
21
|
+
class SerialPort
|
|
22
|
+
include Drive::SerialLines
|
|
23
|
+
|
|
24
|
+
READY = 0x02
|
|
25
|
+
SWITCHES = 0x18
|
|
26
|
+
DISK_CHANGE = 0x80
|
|
27
|
+
PROTECT = 0x40
|
|
28
|
+
|
|
29
|
+
def initialize(mechanism, device: 8, bus: IECBus.new)
|
|
30
|
+
@mechanism = mechanism
|
|
31
|
+
@switches = (0xff & ~SWITCHES) | (((device - 8) & 0x03) << 3)
|
|
32
|
+
init_serial_lines(device, bus)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read_a(_port_a, _port_b)
|
|
36
|
+
value = @switches
|
|
37
|
+
value &= ~READY if @mechanism.ready?
|
|
38
|
+
value &= ~DISK_CHANGE if @mechanism.disk_changed?
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_b(_port_a, _port_b)
|
|
43
|
+
low = @bus.low_lines(@host)
|
|
44
|
+
value = 0xff & ~(SERIAL_INPUTS | PROTECT)
|
|
45
|
+
value |= DATA_IN if low.anybits?(IECBus::DATA)
|
|
46
|
+
value |= CLK_IN if low.anybits?(IECBus::CLK)
|
|
47
|
+
value |= ATN_IN if low.anybits?(IECBus::ATN)
|
|
48
|
+
@mechanism.write_protected? ? value : value | PROTECT
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
# A machine's place for a Drive1581 on its serial bus, in a field of
|
|
6
|
+
# its own beside its other true drives, so none of them makes another's
|
|
7
|
+
# per-cycle call a union. The C64, the VIC-20 and the C128 include it,
|
|
8
|
+
# and run the drive from their cycle as they run a 1541.
|
|
9
|
+
#
|
|
10
|
+
# A 1581 reads only .d81 images and the others none, so a .d81 put in
|
|
11
|
+
# device 8 swaps the true drive there for a 1581, and any other disk
|
|
12
|
+
# swaps a 1581 back for the machine's own true drive
|
|
13
|
+
# (Media::TrueDrive).
|
|
14
|
+
module Slot
|
|
15
|
+
attr_reader :drive1581
|
|
16
|
+
|
|
17
|
+
# Plugs in a Drive1581, which then runs alongside the machine on its
|
|
18
|
+
# own clock and talks to it over the serial bus, as a 1541 does.
|
|
19
|
+
def attach_drive1581(drive)
|
|
20
|
+
@iec_bus.detach(@drive1581) if @drive1581
|
|
21
|
+
drive.host_clock_hz = timing.clock_hz
|
|
22
|
+
@drive1581 = drive
|
|
23
|
+
drive.connect(@iec_bus)
|
|
24
|
+
@serial_trap&.device = serial_trap_device
|
|
25
|
+
drive1581_attached
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Unplugs the Drive1581.
|
|
29
|
+
def detach_drive1581
|
|
30
|
+
return unless @drive1581
|
|
31
|
+
|
|
32
|
+
@iec_bus.detach(@drive1581)
|
|
33
|
+
@drive1581 = nil
|
|
34
|
+
@serial_trap&.device = serial_trap_device
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Plugs in a 1581 as device 8, in place of the true drive there, and
|
|
38
|
+
# returns it.
|
|
39
|
+
def plug_drive1581
|
|
40
|
+
unplug_device8
|
|
41
|
+
Drive1581.new.tap { |drive| attach_drive1581(drive) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Unplugs the 1541 if it is device 8, for a 1581 to take its place.
|
|
47
|
+
def unplug_device8
|
|
48
|
+
detach_drive1541 if @drive1541&.device == KernalTrap::Routine::DEVICE
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# What the machine does as a 1581 goes on its bus. The VIC-20 waits
|
|
52
|
+
# for its DOS to boot before typing.
|
|
53
|
+
def drive1581_attached = nil
|
|
54
|
+
|
|
55
|
+
# Whether the 1581 is device 8.
|
|
56
|
+
def drive1581_on_device8? = @drive1581&.device == KernalTrap::Routine::DEVICE
|
|
57
|
+
|
|
58
|
+
# The 1581 on the serial bus, with its device number.
|
|
59
|
+
def save_drive1581(out)
|
|
60
|
+
out.boolean(!@drive1581.nil?)
|
|
61
|
+
return unless @drive1581
|
|
62
|
+
|
|
63
|
+
out.int(@drive1581.device)
|
|
64
|
+
@drive1581.save_state(out)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Layouts before 8 have no 1581.
|
|
68
|
+
def load_drive1581(input)
|
|
69
|
+
return detach_drive1581 if input.schema < 8 || !input.boolean?
|
|
70
|
+
|
|
71
|
+
device = input.int
|
|
72
|
+
detach_drive1581 if @drive1581 && @drive1581.device != device
|
|
73
|
+
attach_drive1581(Drive1581.new(device:)) unless @drive1581
|
|
74
|
+
@drive1581.load_state(input)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
# One side of a cylinder as the WD1772 finds it: its sectors, each an
|
|
6
|
+
# ID field and a data field at a place round the turn, counted in
|
|
7
|
+
# bytes from the index pulse. The disk turns at 300 rpm and the bits
|
|
8
|
+
# pass at 250 kbit/s, so a turn is LENGTH bytes of 32 µs. The bytes
|
|
9
|
+
# between the fields, the gaps and the sync marks, are not kept: a
|
|
10
|
+
# read track lays them out again (raw).
|
|
11
|
+
#
|
|
12
|
+
# A standard track is the one the 1581 DOS formats (its format routine,
|
|
13
|
+
# and the 1581 Service Manual's per-sector organization): 32 bytes of
|
|
14
|
+
# $4E after the index, then ten 512-byte sectors numbered from 1, each
|
|
15
|
+
#
|
|
16
|
+
# 12 x $00, 3 x $A1, $FE, track, side, sector, size code, CRC,
|
|
17
|
+
# 22 x $4E, 12 x $00, 3 x $A1, $FB, 512 data bytes, CRC,
|
|
18
|
+
# 35 x $4E
|
|
19
|
+
#
|
|
20
|
+
# and $4E to the index. A write track lays down whatever the CPU sends,
|
|
21
|
+
# and parse finds its fields again.
|
|
22
|
+
class Track
|
|
23
|
+
LENGTH = 6250
|
|
24
|
+
GAP = 0x4e
|
|
25
|
+
SYNC = 0xa1
|
|
26
|
+
ID_MARK = 0xfe
|
|
27
|
+
DATA_MARK = 0xfb
|
|
28
|
+
DELETED_MARK = 0xf8
|
|
29
|
+
|
|
30
|
+
SECTORS = 10
|
|
31
|
+
SECTOR_SIZE = 512
|
|
32
|
+
SIZE_CODE = 2
|
|
33
|
+
|
|
34
|
+
# Where the first sector's ID mark lies, and how far apart the
|
|
35
|
+
# sectors are.
|
|
36
|
+
FIRST_ID = 32 + 15
|
|
37
|
+
SECTOR_SPACING = 574 + 35
|
|
38
|
+
|
|
39
|
+
# From an ID mark to its data mark.
|
|
40
|
+
DATA_OFFSET = 44
|
|
41
|
+
|
|
42
|
+
# How far after an ID field's CRC a data mark still belongs to it
|
|
43
|
+
# (WD177x data sheet).
|
|
44
|
+
DATA_MARK_WINDOW = 43
|
|
45
|
+
|
|
46
|
+
# An ID or data field: its mark's place, its bytes, whether its CRC
|
|
47
|
+
# reads back, and for a data field whether its mark is the deleted
|
|
48
|
+
# one.
|
|
49
|
+
Field = Struct.new(:mark, :bytes, :good, :deleted)
|
|
50
|
+
|
|
51
|
+
# A sector: its ID field and its data field, or nil where none
|
|
52
|
+
# follows the ID.
|
|
53
|
+
Sector = Struct.new(:id, :data)
|
|
54
|
+
|
|
55
|
+
# The CRC-CCITT the WD1772 writes after each field, over the field
|
|
56
|
+
# from its first $A1: x^16 + x^12 + x^5 + 1 from $FFFF.
|
|
57
|
+
def self.crc(bytes, crc = 0xffff)
|
|
58
|
+
bytes.each do |byte|
|
|
59
|
+
crc ^= byte << 8
|
|
60
|
+
8.times { crc = crc.anybits?(0x8000) ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff }
|
|
61
|
+
end
|
|
62
|
+
crc
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A field's CRC as the two bytes written after it: the mark and
|
|
66
|
+
# +bytes+, behind three $A1s.
|
|
67
|
+
def self.field_crc(mark, bytes)
|
|
68
|
+
crc = crc([SYNC, SYNC, SYNC, mark] + bytes)
|
|
69
|
+
[crc >> 8, crc & 0xff]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A standard track for +cylinder+ and +side+, each sector's data the
|
|
73
|
+
# block yields for its number, 1 to 10.
|
|
74
|
+
def self.standard(cylinder, side)
|
|
75
|
+
sectors = Array.new(SECTORS) do |index|
|
|
76
|
+
mark = FIRST_ID + (index * SECTOR_SPACING)
|
|
77
|
+
id = Field.new(mark, [cylinder, side, index + 1, SIZE_CODE], true, false)
|
|
78
|
+
Sector.new(id, Field.new(mark + DATA_OFFSET, yield(index + 1), true, false))
|
|
79
|
+
end
|
|
80
|
+
new(sectors)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
attr_reader :sectors
|
|
84
|
+
|
|
85
|
+
def initialize(sectors)
|
|
86
|
+
@sectors = sectors
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The first sector whose ID mark lies at or after +position+, round
|
|
90
|
+
# from the index if none does, or nil on a track without IDs.
|
|
91
|
+
def next_id(position)
|
|
92
|
+
@sectors.find { |sector| sector.id.mark >= position } || @sectors.first
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The sector with the ID field +id+, track, side, sector and size
|
|
96
|
+
# code, or nil.
|
|
97
|
+
def sector(id) = @sectors.find { |sector| sector.id.bytes == id }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
class Track
|
|
6
|
+
# A Track as the bytes going round under the head, for the WD1772's
|
|
7
|
+
# read track: its fields with their gaps and sync marks.
|
|
8
|
+
module Bytes
|
|
9
|
+
# The track's LENGTH bytes from the index: $4E gaps, and before each
|
|
10
|
+
# field 12 x $00 and three $A1 sync marks, its mark, its bytes and
|
|
11
|
+
# its CRC, spoiled where the field's CRC doesn't read back.
|
|
12
|
+
def raw
|
|
13
|
+
bytes = Array.new(LENGTH, GAP)
|
|
14
|
+
@sectors.each do |sector|
|
|
15
|
+
lay(bytes, sector.id, ID_MARK)
|
|
16
|
+
data = sector.data
|
|
17
|
+
lay(bytes, data, data.deleted ? DELETED_MARK : DATA_MARK) if data
|
|
18
|
+
end
|
|
19
|
+
bytes
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def lay(bytes, field, mark)
|
|
25
|
+
start = field.mark - 15
|
|
26
|
+
crc = Track.field_crc(mark, field.bytes)
|
|
27
|
+
crc[1] ^= 0xff unless field.good
|
|
28
|
+
[*Array.new(12, 0), SYNC, SYNC, SYNC, mark, *field.bytes, *crc].each_with_index do |byte, i|
|
|
29
|
+
bytes[start + i] = byte if start + i < LENGTH
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Finding the fields in the bytes a write track laid down.
|
|
35
|
+
module Parse
|
|
36
|
+
# Every sector in +bytes+, a turn as a write track laid it down,
|
|
37
|
+
# where +syncs+ marks the $A1s written as sync marks: an ID mark
|
|
38
|
+
# behind three of them, its four bytes and CRC, and the data field
|
|
39
|
+
# that follows it within DATA_MARK_WINDOW bytes, its length from
|
|
40
|
+
# the ID's size code.
|
|
41
|
+
def parse(bytes, syncs)
|
|
42
|
+
sectors = []
|
|
43
|
+
position = 0
|
|
44
|
+
while (mark = find_mark(bytes, syncs, position...bytes.length, ID_MARK..ID_MARK))
|
|
45
|
+
id = read_field(bytes, mark, 4)
|
|
46
|
+
position = mark + 7
|
|
47
|
+
data_mark = find_mark(bytes, syncs, position...(position + DATA_MARK_WINDOW), DELETED_MARK..DATA_MARK)
|
|
48
|
+
sectors << Sector.new(id, data_mark && read_field(bytes, data_mark, 128 << (id.bytes[3] & 3)))
|
|
49
|
+
end
|
|
50
|
+
new(sectors)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# The first place in +range+ that holds one of +marks+ behind three
|
|
56
|
+
# sync marks.
|
|
57
|
+
def find_mark(bytes, syncs, range, marks)
|
|
58
|
+
([range.first, 3].max...[range.end, bytes.length].min).find do |at|
|
|
59
|
+
marks.cover?(bytes[at]) && syncs[at - 1] && syncs[at - 2] && syncs[at - 3]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_field(bytes, mark, length)
|
|
64
|
+
field = bytes[mark + 1, length] || []
|
|
65
|
+
field += Array.new(length - field.length, 0)
|
|
66
|
+
crc = bytes[mark + 1 + length, 2] || []
|
|
67
|
+
Field.new(mark, field, crc == Track.field_crc(bytes[mark], field), bytes[mark] < 0xfa)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
include Bytes
|
|
72
|
+
extend Parse
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
class WD1772
|
|
6
|
+
# The four registers as the CPU reaches them, and a command written
|
|
7
|
+
# to the command register.
|
|
8
|
+
module Registers
|
|
9
|
+
# Register +offset+, 0 to 3.
|
|
10
|
+
def peek(offset)
|
|
11
|
+
case offset
|
|
12
|
+
when 0 then status
|
|
13
|
+
when 1 then @track
|
|
14
|
+
when 2 then @sector
|
|
15
|
+
else read_data
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def poke(offset, value)
|
|
20
|
+
case offset
|
|
21
|
+
when 0 then write_command(value)
|
|
22
|
+
when 1 then @track = value unless busy?
|
|
23
|
+
when 2 then @sector = value unless busy?
|
|
24
|
+
else write_data(value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The status register, which clears INTRQ: after a type I command
|
|
29
|
+
# bits 1, 2, 5 and 6 follow the index sensor, the track 0 sensor, the
|
|
30
|
+
# spin-up and the write-protect line as they are now.
|
|
31
|
+
def status
|
|
32
|
+
@intrq = false
|
|
33
|
+
value = @status | (@motor_on ? MOTOR_ON : 0) | (busy? ? BUSY : 0)
|
|
34
|
+
return value | (@drq ? DATA_REQUEST : 0) unless @type1
|
|
35
|
+
|
|
36
|
+
value |= PROTECTED if @mechanism.write_protected?
|
|
37
|
+
value |= TRACK0 if @mechanism.track0?
|
|
38
|
+
value | (@mechanism.index?(@now) ? INDEX : 0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def read_data
|
|
44
|
+
@drq = false
|
|
45
|
+
@data
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write_data(value)
|
|
49
|
+
@drq = false
|
|
50
|
+
@data = value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A command while busy goes nowhere, but for a force interrupt.
|
|
54
|
+
def write_command(value)
|
|
55
|
+
@intrq = false
|
|
56
|
+
return force_interrupt(value) if value & 0xf0 == 0xd0
|
|
57
|
+
return if busy?
|
|
58
|
+
|
|
59
|
+
@command = value
|
|
60
|
+
@phase = STARTING
|
|
61
|
+
@due = @now + START
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def start_command
|
|
65
|
+
if @command < 0x80 then start_type1
|
|
66
|
+
elsif @command < 0xc0 then start_type2
|
|
67
|
+
elsif @command & 0xf0 == 0xc0 then start_read_address
|
|
68
|
+
else start_type3
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Stops a command where it is, leaving its status but for BUSY, or
|
|
73
|
+
# with none running switches the status to type I. I3 interrupts at
|
|
74
|
+
# once and I2 at the next index pulse.
|
|
75
|
+
def force_interrupt(value)
|
|
76
|
+
@type1 = true unless busy?
|
|
77
|
+
@drq = false
|
|
78
|
+
idle
|
|
79
|
+
@intrq = true if value.anybits?(0x08)
|
|
80
|
+
return unless value.anybits?(0x04)
|
|
81
|
+
|
|
82
|
+
@phase = INDEX_INTERRUPT
|
|
83
|
+
@count = 1
|
|
84
|
+
count_index
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Drive1581
|
|
5
|
+
class WD1772
|
|
6
|
+
# Write sector, once Sectors has found the ID: it asks for the first
|
|
7
|
+
# byte 2 bytes after the ID's CRC and gives up at 22 with LOST_DATA,
|
|
8
|
+
# then writes the data field where a format puts it, behind 12 x $00
|
|
9
|
+
# and three $A1s, its mark deleted with a, a byte of 0 for each one
|
|
10
|
+
# the CPU missed, and the CRC and a $FF after the last.
|
|
11
|
+
module SectorWrites
|
|
12
|
+
WRITE_REQUEST = 2
|
|
13
|
+
WRITE_DEADLINE = 22
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def write_sector(sector)
|
|
18
|
+
@write_id = sector.id.mark
|
|
19
|
+
@size = 128 << (sector.id.bytes[3] & 0x03)
|
|
20
|
+
@gate_open = false
|
|
21
|
+
@phase = WRITE_GATE
|
|
22
|
+
@due = byte_end(@write_id + 6 + WRITE_REQUEST)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Two bytes after the ID's CRC, DRQ asks for the first byte, and
|
|
26
|
+
# twenty bytes on it has to be there.
|
|
27
|
+
def write_gate
|
|
28
|
+
unless @gate_open
|
|
29
|
+
@gate_open = true
|
|
30
|
+
@drq = true
|
|
31
|
+
return @due += (WRITE_DEADLINE - WRITE_REQUEST) * BYTE
|
|
32
|
+
end
|
|
33
|
+
return lost if @drq
|
|
34
|
+
|
|
35
|
+
@bytes = []
|
|
36
|
+
@phase = WRITE_DATA
|
|
37
|
+
@due = byte_end(@write_id + Track::DATA_OFFSET)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The next byte goes under the head from the data register, a 0
|
|
41
|
+
# where DRQ went unanswered, and DRQ asks for the one after it.
|
|
42
|
+
# After the last come the CRC and a $FF.
|
|
43
|
+
def byte_written
|
|
44
|
+
return sector_written if @bytes.length == @size
|
|
45
|
+
|
|
46
|
+
@status |= LOST_DATA if @drq && !@bytes.empty?
|
|
47
|
+
@bytes << (@drq && !@bytes.empty? ? 0 : @data)
|
|
48
|
+
@drq = @bytes.length < @size
|
|
49
|
+
@due += @bytes.length == @size ? 3 * BYTE : BYTE
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sector_written
|
|
53
|
+
sector = found
|
|
54
|
+
if sector
|
|
55
|
+
sector.data = Track::Field.new(@write_id + Track::DATA_OFFSET, @bytes, true,
|
|
56
|
+
@command.anybits?(DELETED_MARK))
|
|
57
|
+
@mechanism.write(@mechanism.track)
|
|
58
|
+
end
|
|
59
|
+
return finish if @command.nobits?(MULTIPLE)
|
|
60
|
+
|
|
61
|
+
@sector = (@sector + 1) & 0xff
|
|
62
|
+
begin_search
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def lost
|
|
66
|
+
@status |= LOST_DATA
|
|
67
|
+
@drq = false
|
|
68
|
+
finish
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|