badline 0.16.0 → 0.17.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 +76 -0
- data/CLAUDE.md +1 -1
- data/README.md +130 -17
- data/doc/media.md +14 -2
- data/doc/pinned-behaviour.md +61 -0
- data/doc/snapshot-format.md +379 -70
- data/doc/snapshots.md +185 -10
- data/lib/badline/cbm2/acia.rb +10 -0
- data/lib/badline/cbm2/bus.rb +263 -0
- data/lib/badline/cbm2/cpu.rb +58 -0
- data/lib/badline/cbm2/crtc_video.rb +133 -0
- data/lib/badline/cbm2/ieee_drive.rb +18 -0
- data/lib/badline/cbm2/ieee_layout.rb +69 -0
- data/lib/badline/cbm2/ieee_ports.rb +104 -0
- data/lib/badline/cbm2/ieee_traps.rb +81 -0
- data/lib/badline/cbm2/keyboard_ports.rb +60 -0
- data/lib/badline/cbm2/keyed_modules.rb +67 -0
- data/lib/badline/cbm2/keyed_state.rb +91 -0
- data/lib/badline/cbm2/model.rb +44 -0
- data/lib/badline/cbm2/p500/control_ports.rb +80 -0
- data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
- data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
- data/lib/badline/cbm2/p500/model.rb +39 -0
- data/lib/badline/cbm2/p500/pages.rb +57 -0
- data/lib/badline/cbm2/p500.rb +262 -0
- data/lib/badline/cbm2/sid_page.rb +19 -0
- data/lib/badline/cbm2.rb +267 -0
- data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
- data/lib/badline/chips/acia/keyed_state.rb +47 -0
- data/lib/badline/chips/acia.rb +307 -0
- data/lib/badline/chips/crtc.rb +3 -2
- data/lib/badline/chips/riot/fast_forward.rb +72 -0
- data/lib/badline/chips/riot/keyed_parts.rb +69 -0
- data/lib/badline/chips/riot/timer.rb +80 -0
- data/lib/badline/chips/riot.rb +162 -0
- data/lib/badline/chips/rriot/keyed_state.rb +30 -0
- data/lib/badline/chips/rriot.rb +132 -0
- data/lib/badline/chips/tpi/keyed_state.rb +38 -0
- data/lib/badline/chips/tpi.rb +233 -0
- data/lib/badline/chips/vic/bank.rb +60 -5
- data/lib/badline/chips/vic/sprite.rb +4 -3
- data/lib/badline/drives/datasette/keyed_state.rb +9 -1
- data/lib/badline/drives/datasette.rb +27 -3
- data/lib/badline/drives/ieee_bus.rb +50 -4
- data/lib/badline/drives/model2031/ieee_port.rb +133 -0
- data/lib/badline/drives/model2031/keyed_state.rb +66 -0
- data/lib/badline/drives/model2031/parts.rb +25 -0
- data/lib/badline/drives/model2031.rb +92 -0
- data/lib/badline/drives/model8050/disk.rb +163 -0
- data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
- data/lib/badline/drives/model8050/idle.rb +177 -0
- data/lib/badline/drives/model8050/ieee_port.rb +147 -0
- data/lib/badline/drives/model8050/ip_bus.rb +93 -0
- data/lib/badline/drives/model8050/keyed_state.rb +160 -0
- data/lib/badline/drives/model8050/mechanism.rb +230 -0
- data/lib/badline/drives/model8050/parts.rb +37 -0
- data/lib/badline/drives/model8050.rb +243 -0
- data/lib/badline/frontend/app.rb +10 -3
- data/lib/badline/frontend/boot.rb +1 -1
- data/lib/badline/frontend/controls.rb +51 -15
- data/lib/badline/frontend/gamepads.rb +3 -3
- data/lib/badline/frontend/keys.rb +61 -2
- data/lib/badline/frontend/media_slots.rb +1 -1
- data/lib/badline/frontend/menu/menu_media.rb +14 -11
- data/lib/badline/frontend/menu/menu_pages.rb +29 -6
- data/lib/badline/frontend/menu/menu_ports.rb +40 -4
- data/lib/badline/frontend/menu/pause_menu.rb +1 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -7
- data/lib/badline/frontend/timeline.rb +19 -10
- data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
- data/lib/badline/kernal_trap/dos/status.rb +3 -0
- data/lib/badline/kernal_trap/dos.rb +18 -5
- data/lib/badline/kernal_trap/ieee.rb +7 -2
- data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
- data/lib/badline/machine/ieee_drives.rb +194 -0
- data/lib/badline/machine/settling.rb +4 -0
- data/lib/badline/machine.rb +26 -8
- data/lib/badline/media/cbm2_media.rb +29 -0
- data/lib/badline/media/pet_media.rb +5 -5
- data/lib/badline/media/program.rb +6 -3
- data/lib/badline/media/true_drive.rb +50 -8
- data/lib/badline/media.rb +21 -8
- data/lib/badline/options/family.rb +15 -3
- data/lib/badline/options/help.rb +24 -6
- data/lib/badline/options/table.rb +25 -5
- data/lib/badline/options/validation.rb +47 -7
- data/lib/badline/options.rb +48 -15
- data/lib/badline/pet/bus/expansion.rb +99 -5
- data/lib/badline/pet/bus.rb +30 -12
- data/lib/badline/pet/cassettes.rb +22 -1
- data/lib/badline/pet/characters.rb +8 -2
- data/lib/badline/pet/crtc_video.rb +20 -7
- data/lib/badline/pet/ieee_drive.rb +17 -0
- data/lib/badline/pet/ieee_ports.rb +19 -1
- data/lib/badline/pet/ieee_traps.rb +10 -5
- data/lib/badline/pet/keyed_modules.rb +4 -1
- data/lib/badline/pet/keyed_parts.rb +10 -6
- data/lib/badline/pet/model.rb +56 -6
- data/lib/badline/pet/rom_sets.rb +30 -6
- data/lib/badline/pet.rb +45 -16
- data/lib/badline/plus4/acia.rb +4 -298
- data/lib/badline/plus4/keyed_parts.rb +0 -44
- data/lib/badline/plus4.rb +1 -0
- data/lib/badline/roms/8050/dos-2.7.rom +0 -0
- data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
- data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
- data/lib/badline/roms/README +144 -1
- data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
- data/lib/badline/roms/cbm2/basic128.rom +0 -0
- data/lib/badline/roms/cbm2/basic256.rom +0 -0
- data/lib/badline/roms/cbm2/characters-600.rom +0 -0
- data/lib/badline/roms/cbm2/characters-700.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
- data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
- data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
- data/lib/badline/roms/dos2031.rom +0 -0
- data/lib/badline/roms/pet/characters-din.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
- data/lib/badline/snapshot/cbm2_setup.rb +53 -0
- data/lib/badline/snapshot/container.rb +9 -1
- data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
- data/lib/badline/snapshot/field_doc.rb +39 -3
- data/lib/badline/snapshot/image.rb +79 -15
- data/lib/badline/snapshot/pet_setup.rb +30 -13
- data/lib/badline/snapshot/vice/cbm2.rb +93 -0
- data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
- data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
- data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
- data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
- data/lib/badline/snapshot/vice/cias.rb +13 -9
- data/lib/badline/snapshot/vice/p500.rb +96 -0
- data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
- data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
- data/lib/badline/snapshot/vice/pet.rb +121 -0
- data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
- data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
- data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
- data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
- data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
- data/lib/badline/snapshot/vice/pet_via.rb +165 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
- data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
- data/lib/badline/snapshot/vice.rb +4 -0
- data/lib/badline/snapshot.rb +11 -3
- data/lib/badline/storage/crt_file.rb +7 -1
- data/lib/badline/storage/d64_image.rb +7 -0
- data/lib/badline/storage/d80_image.rb +17 -1
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image/bam.rb +18 -3
- data/lib/badline/storage/disk_image/formatting.rb +69 -0
- data/lib/badline/storage/disk_image/writing.rb +35 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage.rb +1 -0
- data/lib/badline/support/cbm2_checkpoint.rb +48 -0
- data/lib/badline/support/p500_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline.rb +1 -0
- metadata +87 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model2031
|
|
6
|
+
# The drive's own fields in a keyed snapshot module (BL-D8): its
|
|
7
|
+
# model, its clock phase and cycle count, the SO pulse due, and the
|
|
8
|
+
# controller's lines as the IEEE-488 port latched them under ieee.*. A
|
|
9
|
+
# save settles the drive first, so it is awake; a load forgets its
|
|
10
|
+
# orbits.
|
|
11
|
+
module KeyedState
|
|
12
|
+
def save_fields(out)
|
|
13
|
+
settle!
|
|
14
|
+
out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
|
|
15
|
+
out.bool("so_pending", @mechanism.so_pending)
|
|
16
|
+
@serial_port.save_fields(out, "ieee.")
|
|
17
|
+
out
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load_fields(input)
|
|
21
|
+
settle!
|
|
22
|
+
input.choice("model", [model_name], model_name)
|
|
23
|
+
@phase = input.int("phase", @phase)
|
|
24
|
+
@cycles = input.int("cycles", @cycles)
|
|
25
|
+
@mechanism.so_pending = input.bool("so_pending", @mechanism.so_pending)
|
|
26
|
+
@serial_port.load_fields(input, "ieee.")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The drive as modules of its own, named from `name` (BL-D8 for
|
|
30
|
+
# device 8): its own fields, its RAM, each VIA, the CPU, the
|
|
31
|
+
# mechanism and the disk, when one is in, as the 1541's.
|
|
32
|
+
def save_modules(sections, name)
|
|
33
|
+
sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
|
|
34
|
+
sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
|
|
35
|
+
sections << Snapshot::Keyed.section("#{name}-VIA1") { |out| @via1.save_fields(out) }
|
|
36
|
+
sections << Snapshot::Keyed.section("#{name}-VIA2") { |out| @via2.save_fields(out) }
|
|
37
|
+
sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
|
|
38
|
+
sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
|
|
39
|
+
disk = @mechanism.disk
|
|
40
|
+
sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
|
|
41
|
+
sections
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The drive from its modules, `input` being its own. No disk module
|
|
45
|
+
# means no disk in the drive.
|
|
46
|
+
def load_modules(modules, input, name)
|
|
47
|
+
load_fields(input)
|
|
48
|
+
@bus.load_fields(modules.required("#{name}-RAM"))
|
|
49
|
+
@via1.load_fields(modules.required("#{name}-VIA1"))
|
|
50
|
+
@via2.load_fields(modules.required("#{name}-VIA2"))
|
|
51
|
+
@cpu.load_fields(modules.required("#{name}-CPU"))
|
|
52
|
+
disk = modules.optional("#{name}-DISK")
|
|
53
|
+
mechanism = modules.required("#{name}-MECH")
|
|
54
|
+
@mechanism.load_fields(mechanism,
|
|
55
|
+
disk.nil? ? nil : Model1541::Disk.from_fields(disk, detached: modules.detached?))
|
|
56
|
+
@via1.restored!
|
|
57
|
+
@via2.restored!
|
|
58
|
+
@cpu.restored!
|
|
59
|
+
forget_orbits
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
include KeyedState
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model2031
|
|
6
|
+
# VIA 1, facing the IEEE-488 bus (IEEEPort), a class of its own so
|
|
7
|
+
# its peripheral keeps one type, apart from the serial drives'.
|
|
8
|
+
class IEEEVIA < Chips::VIA
|
|
9
|
+
# DDRB, whose PB0 says whether the VIA drives ATNA (IEEEPort).
|
|
10
|
+
def port_b_directions = @ddrb
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The drive CPU's bus, decoded as the 1541's (Model1541::Bus), with
|
|
14
|
+
# VIA 1 an IEEEVIA.
|
|
15
|
+
class Bus < Model1541::Bus
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The drive's 6502, on the drive's Bus.
|
|
19
|
+
class CPU
|
|
20
|
+
include Badline::Chips::CPU::Core
|
|
21
|
+
include Engine::IdleCPU
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/drives/model1541"
|
|
4
|
+
require "badline/drives/ieee_bus"
|
|
5
|
+
require "badline/drives/model2031/parts"
|
|
6
|
+
require "badline/drives/model2031/ieee_port"
|
|
7
|
+
require "badline/drives/model2031/keyed_state"
|
|
8
|
+
|
|
9
|
+
module Badline
|
|
10
|
+
module Drives
|
|
11
|
+
# The 2031 disk drive of the PET and CBM: the 1541's board with an
|
|
12
|
+
# IEEE-488 interface in place of the serial one (Commodore schematic
|
|
13
|
+
# 2031038). A 6502 at 1 MHz runs DOS 2.6 from 16 KB of ROM, with 2 KB
|
|
14
|
+
# of RAM, VIA 1 at $1800 on the IEEE-488 bus (IEEEPort) and VIA 2 at
|
|
15
|
+
# $1C00 running the 1541's GCR Mechanism, decoded as the 1541's.
|
|
16
|
+
#
|
|
17
|
+
# Drives::Engine::Core clocks it against the host and sleeps through
|
|
18
|
+
# the DOS's idle loop, as the 1541's, until ATN moves or a timer is due
|
|
19
|
+
# (Engine::VIAs). The bus reads the lines it pulls without waking it.
|
|
20
|
+
class Model2031
|
|
21
|
+
include Engine::Core
|
|
22
|
+
include Engine::VIAs
|
|
23
|
+
|
|
24
|
+
CLOCK_HZ = 1_000_000
|
|
25
|
+
|
|
26
|
+
# Where the DOS's idle loop starts over (see Drives::Engine::Idle).
|
|
27
|
+
IDLE_LOOP = 0xec4d
|
|
28
|
+
|
|
29
|
+
# The host cycles by which a 2031 switched on with the machine has run
|
|
30
|
+
# its DOS's RAM test and ROM checksums and reads its jumpers.
|
|
31
|
+
BOOT_CYCLES = 1_300_000
|
|
32
|
+
|
|
33
|
+
attr_reader :ieee_bus
|
|
34
|
+
|
|
35
|
+
def model_name = "2031"
|
|
36
|
+
|
|
37
|
+
# +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
|
|
38
|
+
# path. +device+ is the number the jumpers set, 8 to 11.
|
|
39
|
+
def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
|
|
40
|
+
@device = device
|
|
41
|
+
@ieee_bus = IEEEBus.new
|
|
42
|
+
@serial_port = IEEEPort.new(device:, bus: @ieee_bus)
|
|
43
|
+
@via1 = IEEEVIA.new(start: 0x1800, peripheral: @serial_port)
|
|
44
|
+
@serial_port.via = @via1
|
|
45
|
+
@mechanism = Model1541::Mechanism.new
|
|
46
|
+
@via2 = Model1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
|
|
47
|
+
@bus = Bus.new(rom: rom || ROM.load("dos2031.rom", 0xc000), via1: @via1, via2: @via2)
|
|
48
|
+
@cpu = CPU.new(@bus, debug:)
|
|
49
|
+
@clock_hz = CLOCK_HZ
|
|
50
|
+
@idle_loop = IDLE_LOOP
|
|
51
|
+
@host_clock_hz = host_clock_hz
|
|
52
|
+
@phase = 0
|
|
53
|
+
@cycles = 0
|
|
54
|
+
init_idle(debug)
|
|
55
|
+
@via1.ca1 = false
|
|
56
|
+
@via1.reset!
|
|
57
|
+
connect(@ieee_bus)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Plugs the drive into an IEEE-488 bus, leaving the one it was on.
|
|
61
|
+
def connect(ieee_bus)
|
|
62
|
+
settle!
|
|
63
|
+
@ieee_bus.detach(self)
|
|
64
|
+
@ieee_bus = ieee_bus
|
|
65
|
+
@serial_port.bus = ieee_bus
|
|
66
|
+
ieee_bus.attach(self)
|
|
67
|
+
hear_atn
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The lines the drive pulls on the bus while ATN is low or not
|
|
71
|
+
# (IEEEPort). They hold still while it sleeps, so reading them leaves
|
|
72
|
+
# it asleep.
|
|
73
|
+
def ieee_low_lines(atn) = @serial_port.ieee_low_lines(atn)
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# One drive cycle, as the 1541's (Model1541#step): the mechanism, then
|
|
78
|
+
# ATN on VIA 1's CA1, the VIAs and the CPU.
|
|
79
|
+
def step
|
|
80
|
+
so = @mechanism.take_so
|
|
81
|
+
@mechanism.cycle!
|
|
82
|
+
@via1.ca1 = @serial_port.atn_low?
|
|
83
|
+
@via1.cycle!
|
|
84
|
+
@via2.cycle!
|
|
85
|
+
@cpu.irq = @via1.irq? || @via2.irq?
|
|
86
|
+
@cpu.so! if so
|
|
87
|
+
@cpu.cycle!
|
|
88
|
+
@cycles += 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# A .d80 or .d82 image as the floppy controller finds it on the disk:
|
|
7
|
+
# each track a ring of byte cells, each cell ten bit cells the GCR ROM
|
|
8
|
+
# turns into a data byte (Track::SYNC for a run of ones), so the
|
|
9
|
+
# mechanism needs no GCR. A track is laid out as the DOS's format code
|
|
10
|
+
# writes it ($C000-$C363 of DOS 2.7, run on the controller), sector
|
|
11
|
+
# after sector:
|
|
12
|
+
#
|
|
13
|
+
# 4 sync, $08, checksum, sector, track, ID 2, ID 1, $0F,
|
|
14
|
+
# 19 x $0F, 5 sync, $07, 256 data bytes, checksum, then $00
|
|
15
|
+
#
|
|
16
|
+
# the header checksum the EOR of the track, sector and IDs, and the
|
|
17
|
+
# data block's that of its bytes. The $00 gaps share out what the
|
|
18
|
+
# track has left over.
|
|
19
|
+
#
|
|
20
|
+
# Tracks come off the image as the head first reaches them. What the
|
|
21
|
+
# head writes goes back to the image when the motor stops (flush):
|
|
22
|
+
# every data block found on a written track whose header and checksum
|
|
23
|
+
# read cleanly.
|
|
24
|
+
class Disk
|
|
25
|
+
SYNC = 0x100
|
|
26
|
+
|
|
27
|
+
attr_reader :image
|
|
28
|
+
|
|
29
|
+
def initialize(image)
|
|
30
|
+
@image = image
|
|
31
|
+
@tracks = {}
|
|
32
|
+
@written = {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def write_protected? = @image.read_only?
|
|
36
|
+
|
|
37
|
+
# The image's host file.
|
|
38
|
+
def path = @image.path
|
|
39
|
+
|
|
40
|
+
# How many tracks a side holds: 77.
|
|
41
|
+
def side_tracks = Storage::D80Image::SIDE_TRACKS
|
|
42
|
+
|
|
43
|
+
def sides = @image.is_a?(Storage::D82Image) ? 2 : 1
|
|
44
|
+
|
|
45
|
+
# The cells of +track+, 1 to 77 on the first side and 78 to 154 on
|
|
46
|
+
# the second, or nil past the disk's last.
|
|
47
|
+
def track(number)
|
|
48
|
+
return nil unless number.between?(1, side_tracks * sides)
|
|
49
|
+
|
|
50
|
+
@tracks[number] ||= build(number)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The cells a track of the zone holds in one turn at 300 rpm: the
|
|
54
|
+
# 200,000 cycles of a turn over the zone's ten bit cells a byte at
|
|
55
|
+
# 6 MHz / (16 - zone).
|
|
56
|
+
def self.cells_in(zone) = 120_000 / (16 - zone)
|
|
57
|
+
|
|
58
|
+
# The zone a track is recorded in, 3 outermost to 0 innermost.
|
|
59
|
+
def self.zone_of(track)
|
|
60
|
+
cylinder = ((track - 1) % Storage::D80Image::SIDE_TRACKS) + 1
|
|
61
|
+
3 - Storage::D80Image::ZONE_LAST_TRACKS.count { |last| cylinder > last }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The head wrote to +track+.
|
|
65
|
+
def written(number)
|
|
66
|
+
@written[number] = true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Stores the blocks the head wrote in the image.
|
|
70
|
+
def flush
|
|
71
|
+
return if @written.empty?
|
|
72
|
+
|
|
73
|
+
blocks = []
|
|
74
|
+
@written.each_key { |number| blocks.concat(read_blocks(@tracks[number])) }
|
|
75
|
+
@written.clear
|
|
76
|
+
@image.store_blocks(blocks) unless blocks.empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def build(number)
|
|
82
|
+
id1, id2 = disk_id
|
|
83
|
+
cells = []
|
|
84
|
+
sectors = @image.sectors_in(number)
|
|
85
|
+
sectors.times do |sector|
|
|
86
|
+
data = @image.read_block(number, sector)
|
|
87
|
+
cells.push(SYNC, SYNC, SYNC, SYNC, 0x08, number ^ sector ^ id1 ^ id2, sector, number, id2, id1)
|
|
88
|
+
cells.concat(Array.new(20, 0x0f))
|
|
89
|
+
cells.push(SYNC, SYNC, SYNC, SYNC, SYNC, 0x07)
|
|
90
|
+
cells.concat(data)
|
|
91
|
+
cells << checksum(data)
|
|
92
|
+
end
|
|
93
|
+
pad(cells, sectors, self.class.cells_in(self.class.zone_of(number)))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Shares the $00 gap out after each sector.
|
|
97
|
+
def pad(cells, sectors, length)
|
|
98
|
+
sector_length = cells.length / sectors
|
|
99
|
+
gap = (length - cells.length) / sectors
|
|
100
|
+
padded = []
|
|
101
|
+
sectors.times do |sector|
|
|
102
|
+
padded.concat(cells[sector * sector_length, sector_length])
|
|
103
|
+
padded.concat(Array.new(sector == sectors - 1 ? length - padded.length : gap, 0x00))
|
|
104
|
+
end
|
|
105
|
+
padded
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def checksum(bytes)
|
|
109
|
+
sum = 0
|
|
110
|
+
bytes.each { |byte| sum ^= byte }
|
|
111
|
+
sum
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The ID the header block holds at $18.
|
|
115
|
+
def disk_id
|
|
116
|
+
header = @image.read_block(*@image.header_block)
|
|
117
|
+
[header[0x18], header[0x19]]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Every block whose header and data read cleanly off +cells+, as
|
|
121
|
+
# [track, sector, data, nil].
|
|
122
|
+
def read_blocks(cells)
|
|
123
|
+
blocks = []
|
|
124
|
+
index = 0
|
|
125
|
+
length = cells.length
|
|
126
|
+
while (index = next_block(cells, index, 0x08))
|
|
127
|
+
header = Array.new(5) { |offset| cells[(index + offset) % length] }
|
|
128
|
+
index = next_block(cells, index, 0x07)
|
|
129
|
+
break unless index
|
|
130
|
+
|
|
131
|
+
block = data_block(cells, index, header)
|
|
132
|
+
blocks << block if block
|
|
133
|
+
break if index + 260 >= length
|
|
134
|
+
end
|
|
135
|
+
blocks
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The cell after the next sync run followed by +mark+, from
|
|
139
|
+
# +index+, or nil.
|
|
140
|
+
def next_block(cells, index, mark)
|
|
141
|
+
length = cells.length
|
|
142
|
+
while index < length
|
|
143
|
+
return index + 2 if cells[index] == SYNC && cells[(index + 1) % length] == mark
|
|
144
|
+
|
|
145
|
+
index += 1
|
|
146
|
+
end
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def data_block(cells, index, header)
|
|
151
|
+
checksum, sector, track, id2, id1 = header
|
|
152
|
+
return nil unless checksum == (track ^ sector ^ id1 ^ id2)
|
|
153
|
+
|
|
154
|
+
length = cells.length
|
|
155
|
+
data = Array.new(256) { |offset| cells[(index + offset) % length] }
|
|
156
|
+
return nil if data.include?(SYNC) || checksum(data) != cells[(index + 256) % length]
|
|
157
|
+
|
|
158
|
+
[track, sector, data, nil]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# The floppy controller's bus (schematic 8050001, sheet 2), which
|
|
7
|
+
# decodes A0-A12 only, so it repeats every 8 KB. A 74LS42 on A10-A12
|
|
8
|
+
# picks a 1 KB block:
|
|
9
|
+
#
|
|
10
|
+
# $0000-$03FF - the 6530 and the VIA: with A7 set the 6530's
|
|
11
|
+
# registers, else with A6 set the VIA's, else the
|
|
12
|
+
# 6530's 64 bytes of RAM, which the controller also
|
|
13
|
+
# uses as its stack at $0100
|
|
14
|
+
# $0400-$13FF - the common RAM
|
|
15
|
+
# $1400-$1BFF - nothing: open bus
|
|
16
|
+
# $1C00-$1FFF - the 6530's ROM
|
|
17
|
+
#
|
|
18
|
+
# Writes to the ROM and to open bus go nowhere, and a read from open
|
|
19
|
+
# bus finds the last byte on the data bus.
|
|
20
|
+
class FDCBus
|
|
21
|
+
include Addressable
|
|
22
|
+
|
|
23
|
+
attr_reader :common, :via, :rriot
|
|
24
|
+
attr_accessor :data
|
|
25
|
+
|
|
26
|
+
# How many writes have changed a byte of the common RAM
|
|
27
|
+
# (Model8050::Idle).
|
|
28
|
+
attr_reader :common_changes
|
|
29
|
+
|
|
30
|
+
# Whether a counter or an interrupt flag has been read since it was
|
|
31
|
+
# last cleared (Model8050::Idle).
|
|
32
|
+
attr_accessor :volatile
|
|
33
|
+
|
|
34
|
+
def initialize(common:, via:, rriot:)
|
|
35
|
+
addressable_at(0, length: 2**16)
|
|
36
|
+
@common = common
|
|
37
|
+
@via = via
|
|
38
|
+
@rriot = rriot
|
|
39
|
+
@data = 0
|
|
40
|
+
@volatile = false
|
|
41
|
+
@common_changes = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def peek(addr)
|
|
45
|
+
addr &= 0x1fff
|
|
46
|
+
@data = if addr < 0x0400
|
|
47
|
+
peek_chips(addr)
|
|
48
|
+
elsif addr < 0x1400
|
|
49
|
+
@common.peek(addr - 0x0400)
|
|
50
|
+
elsif addr >= 0x1c00
|
|
51
|
+
@rriot.read_rom(addr)
|
|
52
|
+
else
|
|
53
|
+
@data
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def poke(addr, value)
|
|
58
|
+
@data = value
|
|
59
|
+
addr &= 0x1fff
|
|
60
|
+
if addr < 0x0400
|
|
61
|
+
poke_chips(addr, value)
|
|
62
|
+
elsif addr < 0x1400
|
|
63
|
+
poke_common(addr - 0x0400, value)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def poke_common(index, value)
|
|
70
|
+
@common_changes += 1 if @common.peek(index) != value
|
|
71
|
+
@common.poke(index, value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def peek_chips(addr)
|
|
75
|
+
if addr.anybits?(0x80)
|
|
76
|
+
@volatile = true if addr.anybits?(0x04)
|
|
77
|
+
return @rriot.peek(addr & 0x0f)
|
|
78
|
+
end
|
|
79
|
+
return peek_via(addr & 0x0f) if addr.anybits?(0x40)
|
|
80
|
+
|
|
81
|
+
@rriot.read_ram(addr)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The VIA's counters, shift register and flags move on their own.
|
|
85
|
+
def peek_via(register)
|
|
86
|
+
@volatile = true if register.between?(0x04, 0x0a) || register == 0x0d
|
|
87
|
+
@via.peek(0x40 | register)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def poke_chips(addr, value)
|
|
91
|
+
if addr.anybits?(0x80)
|
|
92
|
+
@rriot.poke(addr & 0x0f, value)
|
|
93
|
+
elsif addr.anybits?(0x40)
|
|
94
|
+
@via.poke(0x40 | (addr & 0x0f), value)
|
|
95
|
+
else
|
|
96
|
+
@rriot.write_ram(addr, value)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# Sleeping through the two processors' idle loops, exactly.
|
|
7
|
+
#
|
|
8
|
+
# With nothing to do, the IP goes round DOS 2.7's idle loop from
|
|
9
|
+
# IP_LOOP, and the FDC round its job loop from FDC_LOOP, each pass
|
|
10
|
+
# finding the job queue empty, until ATN interrupts the IP or the
|
|
11
|
+
# 6530's timer the FDC, every 5 ms. A pass puts back what it found,
|
|
12
|
+
# so each processor comes back to its loop's start as it left it, and
|
|
13
|
+
# only the chips' counters and the cycle counts move.
|
|
14
|
+
#
|
|
15
|
+
# Each time a processor comes to its loop's start, the unit compares
|
|
16
|
+
# what that processor sees with what it saw last time round: the CPU
|
|
17
|
+
# (Engine::IdleCPU#idle_state), its chips but for their counters, its
|
|
18
|
+
# RAM and the common RAM, and on the FDC's side the drives. The same,
|
|
19
|
+
# with no read of a counter or an interrupt flag between (the buses'
|
|
20
|
+
# volatile), makes the pass repeatable. Once both processors have a
|
|
21
|
+
# repeatable pass, with the motors off, no head stepping, no disk
|
|
22
|
+
# going in or out and no interrupt pulled, the unit sleeps:
|
|
23
|
+
# host_cycle! only counts the cycles it owes, until one of the chips'
|
|
24
|
+
# counters is due to pull IRQ (quiet_cycles), the controller moves a
|
|
25
|
+
# line of the bus, or something outside reads or changes the unit
|
|
26
|
+
# (settle!).
|
|
27
|
+
#
|
|
28
|
+
# Waking runs the counters on in bulk, counts off each processor's
|
|
29
|
+
# whole passes, and runs the rest of a pass on that processor alone.
|
|
30
|
+
# Its passes touch nothing the other one or the counters move, so the
|
|
31
|
+
# order the two run in doesn't matter. Then the unit runs cycle by
|
|
32
|
+
# cycle again. Woken by a counter, it keeps the passes it found, and
|
|
33
|
+
# sleeps again as soon as both repeat: the IP's, which the timer's
|
|
34
|
+
# interrupt doesn't reach, at once, and the FDC's a pass after its
|
|
35
|
+
# interrupt. Woken from outside, it finds both afresh.
|
|
36
|
+
module Idle
|
|
37
|
+
IP_LOOP = 0xc5a1
|
|
38
|
+
FDC_LOOP = 0xfc17
|
|
39
|
+
|
|
40
|
+
# Whether the unit may sleep through its idle loops.
|
|
41
|
+
attr_reader :idle_skip
|
|
42
|
+
|
|
43
|
+
def idle_skip=(on)
|
|
44
|
+
settle!
|
|
45
|
+
@idle_skip = on
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def asleep? = @asleep
|
|
49
|
+
|
|
50
|
+
# Brings the unit up to the present, cycle for cycle, and drops the
|
|
51
|
+
# passes it found, since whoever called may change it.
|
|
52
|
+
def settle!
|
|
53
|
+
wake! if @asleep
|
|
54
|
+
forget_passes
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def init_idle
|
|
60
|
+
@idle_skip = true
|
|
61
|
+
@asleep = false
|
|
62
|
+
@owed = 0
|
|
63
|
+
@budget = 0
|
|
64
|
+
forget_passes
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def forget_passes
|
|
68
|
+
@ip_mark = nil
|
|
69
|
+
@fdc_mark = nil
|
|
70
|
+
@ip_pass = nil
|
|
71
|
+
@fdc_pass = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A cycle owed while asleep, which wakes the unit once the budget
|
|
75
|
+
# is spent.
|
|
76
|
+
def owe_cycle
|
|
77
|
+
@owed += 1
|
|
78
|
+
wake! if @owed >= @budget
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Looks for the loops' starts after a cycle run awake. An interrupt
|
|
82
|
+
# pulled breaks the processor's pass.
|
|
83
|
+
def watch_loops
|
|
84
|
+
ip = @ip_cpu
|
|
85
|
+
@ip_pass = nil if ip.irq
|
|
86
|
+
@fdc_pass = nil if @fdc_cpu.irq
|
|
87
|
+
ip_pass_found if ip.program_counter == IP_LOOP && ip.boundary?
|
|
88
|
+
fdc = @fdc_cpu
|
|
89
|
+
fdc_pass_found if fdc.program_counter == FDC_LOOP && fdc.boundary?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ip_pass_found
|
|
93
|
+
state = ip_state
|
|
94
|
+
@ip_pass = repeated(@ip_mark, state, @ip_cpu, @ip_bus.volatile)
|
|
95
|
+
@ip_mark = [state, @ip_cpu.cycles, @ip_cpu.instructions]
|
|
96
|
+
@ip_bus.volatile = false
|
|
97
|
+
fall_asleep if @ip_pass && @fdc_pass
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def fdc_pass_found
|
|
101
|
+
state = fdc_state
|
|
102
|
+
@fdc_pass = repeated(@fdc_mark, state, @fdc_cpu, @fdc_bus.volatile)
|
|
103
|
+
@fdc_mark = [state, @fdc_cpu.cycles, @fdc_cpu.instructions]
|
|
104
|
+
@fdc_bus.volatile = false
|
|
105
|
+
fall_asleep if @ip_pass && @fdc_pass
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The pass since +mark+ as [cycles, instructions] if it left the
|
|
109
|
+
# processor as it found it, or nil.
|
|
110
|
+
def repeated(mark, state, cpu, volatile)
|
|
111
|
+
return nil if mark.nil? || volatile || mark[0] != state
|
|
112
|
+
|
|
113
|
+
[cpu.cycles - mark[1], cpu.instructions - mark[2]]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def fall_asleep
|
|
117
|
+
return unless @mechanism.quiet? && !@ip_cpu.irq && !@fdc_cpu.irq
|
|
118
|
+
|
|
119
|
+
budget = quiet_cycles
|
|
120
|
+
return if budget < 2
|
|
121
|
+
|
|
122
|
+
@asleep = true
|
|
123
|
+
@owed = 0
|
|
124
|
+
@budget = budget
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# How many cycles the chips can run with no counter pulling IRQ.
|
|
128
|
+
def quiet_cycles
|
|
129
|
+
[@data_riot.quiet_cycles, @control_riot.quiet_cycles, @rriot.quiet_cycles, @via.quiet_cycles].min
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Runs the cycles owed: the counters in bulk, then each processor's
|
|
133
|
+
# whole passes and the rest of a pass.
|
|
134
|
+
def wake!
|
|
135
|
+
owed = @owed
|
|
136
|
+
@asleep = false
|
|
137
|
+
@owed = 0
|
|
138
|
+
return if owed.zero?
|
|
139
|
+
|
|
140
|
+
@data_riot.fast_forward(owed)
|
|
141
|
+
@control_riot.fast_forward(owed)
|
|
142
|
+
@rriot.fast_forward(owed)
|
|
143
|
+
@via.fast_forward(owed)
|
|
144
|
+
skip(@ip_cpu, @ip_pass, @ip_mark, owed)
|
|
145
|
+
skip(@fdc_cpu, @fdc_pass, @fdc_mark, owed)
|
|
146
|
+
@cycles += owed
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Counts off the processor's whole passes, moving its mark on with
|
|
150
|
+
# them, and runs the rest on the processor alone.
|
|
151
|
+
def skip(cpu, pass, mark, owed)
|
|
152
|
+
passes = owed / pass[0]
|
|
153
|
+
cpu.fast_forward(passes * pass[0], passes * pass[1])
|
|
154
|
+
mark[1] += passes * pass[0]
|
|
155
|
+
mark[2] += passes * pass[1]
|
|
156
|
+
(owed - (passes * pass[0])).times { cpu.cycle! }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# What the IP sees: its CPU, its RIOTs but for their counters, its
|
|
160
|
+
# bus's last byte and the common RAM, as the count of the writes
|
|
161
|
+
# that changed it.
|
|
162
|
+
def ip_state
|
|
163
|
+
[*@ip_cpu.idle_state, *@data_riot.idle_state, *@control_riot.idle_state, @ip_bus.data, common_changes]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# What the FDC sees: its CPU, the VIA and the 6530 but for their
|
|
167
|
+
# counters, its bus's last byte, the drives and the common RAM.
|
|
168
|
+
def fdc_state
|
|
169
|
+
[*@fdc_cpu.idle_state, *@via.idle_state, *@rriot.idle_state, @fdc_bus.data, *@mechanism.idle_state,
|
|
170
|
+
common_changes]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def common_changes = @ip_bus.common_changes + @fdc_bus.common_changes
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|