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,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# The interface processor's two 6532s on the IEEE-488 bus
|
|
7
|
+
# (Drives::IEEEBus), through MC3446 transceivers (schematic 8050001,
|
|
8
|
+
# sheet 1). UC1 carries the data lines, its port A reading them and
|
|
9
|
+
# its port B driving them. UE1 carries the rest:
|
|
10
|
+
#
|
|
11
|
+
# PA0 ATN acknowledge (ATNA) PB0-2 the device number jumpers
|
|
12
|
+
# PA1 NDAC out, low while set PB3 drive 1's LED
|
|
13
|
+
# PA2 NRFD out PB4 drive 0's LED
|
|
14
|
+
# PA3 EOI out PB5 the error LED
|
|
15
|
+
# PA4 DAV out PB6 NDAC in
|
|
16
|
+
# PA5 EOI in PB7 NRFD in
|
|
17
|
+
# PA6 DAV in
|
|
18
|
+
# PA7 ATN in, high while ATN is low
|
|
19
|
+
#
|
|
20
|
+
# The ports read and drive the lines' levels, 0 for a low line, but
|
|
21
|
+
# for NDAC out, which pulls NDAC low while set. The DOS sets it as it
|
|
22
|
+
# waits for a byte and clears it once it has taken one ($C627-$C650).
|
|
23
|
+
# Every input reads the bus, the drive's own lines included.
|
|
24
|
+
#
|
|
25
|
+
# ATN in also reaches UE1's PA7 edge detector, which the DOS arms for
|
|
26
|
+
# the rising edge ($C58A), so ATN interrupts the interface processor.
|
|
27
|
+
# While ATN in and ATNA differ, the board pulls NRFD and NDAC low, so
|
|
28
|
+
# the drive answers ATN before the DOS gets to it, as the 2031 does.
|
|
29
|
+
#
|
|
30
|
+
# The device number jumpers ground PB0-2: cutting one adds its weight
|
|
31
|
+
# to 8.
|
|
32
|
+
#
|
|
33
|
+
# The drive takes the controller's lines a host cycle late, as the
|
|
34
|
+
# 2031 does: latch_host takes them at the end of each host cycle.
|
|
35
|
+
class IEEEPort
|
|
36
|
+
ATNA = 0x01
|
|
37
|
+
NDAC_OUT = 0x02
|
|
38
|
+
NRFD_OUT = 0x04
|
|
39
|
+
EOI_OUT = 0x08
|
|
40
|
+
DAV_OUT = 0x10
|
|
41
|
+
EOI_IN = 0x20
|
|
42
|
+
DAV_IN = 0x40
|
|
43
|
+
ATN_IN = 0x80
|
|
44
|
+
NDAC_IN = 0x40
|
|
45
|
+
NRFD_IN = 0x80
|
|
46
|
+
|
|
47
|
+
attr_reader :bus
|
|
48
|
+
|
|
49
|
+
# The lines the controller pulled, as latch_host took them.
|
|
50
|
+
attr_accessor :host
|
|
51
|
+
|
|
52
|
+
# The RIOTs on the data lines and on the control lines.
|
|
53
|
+
attr_accessor :data_riot, :control_riot
|
|
54
|
+
|
|
55
|
+
def initialize(device:, bus:)
|
|
56
|
+
raise ArgumentError, "an 8050 is device 8 to 15, not #{device}" unless device.between?(8, 15)
|
|
57
|
+
|
|
58
|
+
@jumpers = (device - 8) | 0xf8
|
|
59
|
+
@host = 0
|
|
60
|
+
@data_riot = nil
|
|
61
|
+
@control_riot = nil
|
|
62
|
+
self.bus = bus
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def bus=(bus)
|
|
66
|
+
@bus = bus
|
|
67
|
+
latch_host
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Takes the controller's lines for the drive cycles of the next host
|
|
71
|
+
# cycle.
|
|
72
|
+
def latch_host
|
|
73
|
+
@host = @bus.host_lines
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether the controller has moved ATN since the last latch_host.
|
|
77
|
+
def atn_moved? = (@bus.host_lines ^ @host).anybits?(IEEEBus::ATN)
|
|
78
|
+
|
|
79
|
+
# Whether the controller has moved any line since the last
|
|
80
|
+
# latch_host.
|
|
81
|
+
def lines_moved? = @bus.host_lines != @host
|
|
82
|
+
|
|
83
|
+
def atn_low? = @host.anybits?(IEEEBus::ATN)
|
|
84
|
+
|
|
85
|
+
# UC1's port A: the data lines.
|
|
86
|
+
def data_lines = ~@bus.low_lines(@host) & 0xff
|
|
87
|
+
|
|
88
|
+
# UE1's port A inputs. ATN in comes through the edge detector's own
|
|
89
|
+
# pin (RIOT#pa7=), so bit 7 is left high here.
|
|
90
|
+
def control_a
|
|
91
|
+
low = @bus.low_lines(@host)
|
|
92
|
+
value = 0xff
|
|
93
|
+
value &= ~EOI_IN if low.anybits?(IEEEBus::EOI)
|
|
94
|
+
value &= ~DAV_IN if low.anybits?(IEEEBus::DAV)
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# UE1's port B inputs: the jumpers, NDAC and NRFD.
|
|
99
|
+
def control_b
|
|
100
|
+
low = @bus.low_lines(@host)
|
|
101
|
+
value = @jumpers
|
|
102
|
+
value &= ~NDAC_IN if low.anybits?(IEEEBus::NDAC)
|
|
103
|
+
value &= ~NRFD_IN if low.anybits?(IEEEBus::NRFD)
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The lines the drive pulls low while ATN is low or not, as bus bits.
|
|
108
|
+
def ieee_low_lines(atn)
|
|
109
|
+
control = @control_riot.port_a_output
|
|
110
|
+
low = ~@data_riot.port_b_output & IEEEBus::DIO
|
|
111
|
+
low |= IEEEBus::EOI if control.nobits?(EOI_OUT)
|
|
112
|
+
low |= IEEEBus::DAV if control.nobits?(DAV_OUT)
|
|
113
|
+
low |= IEEEBus::NRFD if control.nobits?(NRFD_OUT)
|
|
114
|
+
low |= IEEEBus::NDAC if control.anybits?(NDAC_OUT)
|
|
115
|
+
low |= IEEEBus::NRFD | IEEEBus::NDAC if atn != control.anybits?(ATNA)
|
|
116
|
+
low
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Which LEDs UE1 lights: bit 0 drive 1's, bit 1 drive 0's, bit 2 the
|
|
120
|
+
# error LED.
|
|
121
|
+
def leds = (@control_riot.port_b_output >> 3) & 0x07
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# UC1's peripheral: the data lines on port A, port B only driving.
|
|
125
|
+
class DataLines
|
|
126
|
+
def initialize(port)
|
|
127
|
+
@port = port
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def read_a(_lines) = @port.data_lines
|
|
131
|
+
|
|
132
|
+
def read_b(_lines) = 0xff
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# UE1's peripheral: the control lines and the jumpers.
|
|
136
|
+
class ControlLines
|
|
137
|
+
def initialize(port)
|
|
138
|
+
@port = port
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def read_a(_lines) = @port.control_a
|
|
142
|
+
|
|
143
|
+
def read_b(_lines) = @port.control_b
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# The interface processor's bus (schematic 8050001, sheet 1). A
|
|
7
|
+
# 74LS42 on A12-A15 picks a 4 KB block:
|
|
8
|
+
#
|
|
9
|
+
# $0000-$0FFF - the two 6532s. A9 is their RS pin and A7 picks one:
|
|
10
|
+
# UC1's RAM at $00-$7F and UE1's at $80-$FF, mirrored at
|
|
11
|
+
# $0100 for the stack since A8 isn't decoded, and their
|
|
12
|
+
# registers at $0200 (UC1) and $0280 (UE1)
|
|
13
|
+
# $1000-$4FFF - the common RAM, 1 KB in each block, mirrored through it
|
|
14
|
+
# $5000-$BFFF - nothing: open bus
|
|
15
|
+
# $C000-$FFFF - DOS 2.7, 901887-01 and 901888-01
|
|
16
|
+
#
|
|
17
|
+
# Writes to the ROM and to open bus go nowhere, and a read from open
|
|
18
|
+
# bus finds the last byte on the data bus.
|
|
19
|
+
class IPBus
|
|
20
|
+
include Addressable
|
|
21
|
+
|
|
22
|
+
attr_reader :common, :rom, :data_riot, :control_riot
|
|
23
|
+
attr_accessor :data
|
|
24
|
+
|
|
25
|
+
# How many writes have changed a byte of the common RAM
|
|
26
|
+
# (Model8050::Idle).
|
|
27
|
+
attr_reader :common_changes
|
|
28
|
+
|
|
29
|
+
# Whether a timer or the interrupt flags have been read since it was
|
|
30
|
+
# last cleared (Model8050::Idle).
|
|
31
|
+
attr_accessor :volatile
|
|
32
|
+
|
|
33
|
+
def initialize(rom:, common:, data_riot:, control_riot:)
|
|
34
|
+
addressable_at(0, length: 2**16)
|
|
35
|
+
@rom = rom
|
|
36
|
+
@common = common
|
|
37
|
+
@data_riot = data_riot
|
|
38
|
+
@control_riot = control_riot
|
|
39
|
+
@data = 0
|
|
40
|
+
@volatile = false
|
|
41
|
+
@common_changes = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def peek(addr)
|
|
45
|
+
return @data = @rom.peek(addr) if addr >= 0xc000
|
|
46
|
+
|
|
47
|
+
@data = if addr < 0x1000
|
|
48
|
+
peek_riots(addr)
|
|
49
|
+
elsif addr < 0x5000
|
|
50
|
+
@common.peek((((addr >> 12) - 1) << 10) | (addr & 0x03ff))
|
|
51
|
+
else
|
|
52
|
+
@data
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def poke(addr, value)
|
|
57
|
+
@data = value
|
|
58
|
+
return if addr >= 0x5000
|
|
59
|
+
|
|
60
|
+
if addr < 0x1000
|
|
61
|
+
poke_riots(addr, value)
|
|
62
|
+
else
|
|
63
|
+
poke_common((((addr >> 12) - 1) << 10) | (addr & 0x03ff), 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_riots(addr)
|
|
75
|
+
@volatile = true if addr & 0x204 == 0x204
|
|
76
|
+
if addr.anybits?(0x80)
|
|
77
|
+
addr.anybits?(0x200) ? @control_riot.peek(addr & 0x1f) : @control_riot.read_ram(addr)
|
|
78
|
+
else
|
|
79
|
+
addr.anybits?(0x200) ? @data_riot.peek(addr & 0x1f) : @data_riot.read_ram(addr)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def poke_riots(addr, value)
|
|
84
|
+
if addr.anybits?(0x80)
|
|
85
|
+
addr.anybits?(0x200) ? @control_riot.poke(addr & 0x1f, value) : @control_riot.write_ram(addr, value)
|
|
86
|
+
else
|
|
87
|
+
addr.anybits?(0x200) ? @data_riot.poke(addr & 0x1f, value) : @data_riot.write_ram(addr, value)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
class Disk
|
|
7
|
+
# A disk's fields in a keyed snapshot module (BL-D8-DISK, BL-D8-DISK1
|
|
8
|
+
# for drive 1): the image file's path and how it was opened, the
|
|
9
|
+
# image as last flushed, embedded under image and image.format, each
|
|
10
|
+
# track whose cells differ from what the image lays out, as
|
|
11
|
+
# track<N>, a cell per byte or 256 for a sync, and the tracks
|
|
12
|
+
# written since the last flush.
|
|
13
|
+
module KeyedState
|
|
14
|
+
IMAGE_FORMATS = %w[d80 d82].freeze
|
|
15
|
+
|
|
16
|
+
def save_fields(out)
|
|
17
|
+
image = @image
|
|
18
|
+
out.text("path", image.path.to_s).bool("read_only", image.read_only?)
|
|
19
|
+
out.text("image.format", image.is_a?(Storage::D82Image) ? "d82" : "d80")
|
|
20
|
+
out.bytes("image", image.contents)
|
|
21
|
+
@tracks.each do |number, cells|
|
|
22
|
+
out.ints("track#{number}", cells) unless cells == build(number)
|
|
23
|
+
end
|
|
24
|
+
out.ints("written", @written.keys)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Puts back the tracks and the writes not yet flushed.
|
|
28
|
+
def load_track_fields(input)
|
|
29
|
+
(1..(side_tracks * sides)).each do |number|
|
|
30
|
+
cells = input.ints("track#{number}", [])
|
|
31
|
+
@tracks[number] = cells.dup unless cells.empty?
|
|
32
|
+
end
|
|
33
|
+
input.ints("written", []).each { |number| @written[number] = true }
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
include KeyedState
|
|
39
|
+
|
|
40
|
+
# The disk a BL-D8-DISK module holds. A detached reader gets the
|
|
41
|
+
# image write-protected, and it and a host file that is gone leave
|
|
42
|
+
# it no path to write back to.
|
|
43
|
+
def self.from_fields(input, detached: false)
|
|
44
|
+
path = input.text("path", "")
|
|
45
|
+
read_only = input.bool("read_only", false) || detached
|
|
46
|
+
kind = input.choice("image.format", KeyedState::IMAGE_FORMATS, "d80")
|
|
47
|
+
bytes = input.bytes("image", "".b).bytes
|
|
48
|
+
path = "" if detached || !File.file?(path)
|
|
49
|
+
image_class = kind == "d82" ? Storage::D82Image : Storage::D80Image
|
|
50
|
+
new(image_class.new(path, read_only:, bytes:)).load_track_fields(input)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Mechanism
|
|
55
|
+
# The drives' fields in a keyed snapshot module (BL-D8-MECH), a value
|
|
56
|
+
# a drive in each list: the heads' cylinders, the stepper phases,
|
|
57
|
+
# each disk's place in its turn and its bit clock, the disk change
|
|
58
|
+
# under way and the motors, then the byte ready pulse, the byte on
|
|
59
|
+
# VIA port A and SYNC. The disks load from their own modules.
|
|
60
|
+
module KeyedState
|
|
61
|
+
def save_fields(out)
|
|
62
|
+
out.ints("cylinders", @cylinders).ints("phases", @phases).ints("positions", @positions)
|
|
63
|
+
out.ints("bit_clocks", @clocks).ints("changing", @changing)
|
|
64
|
+
out.ints("motors", @motors.map { |on| on ? 1 : 0 }).ints("syncs", @syncs.map { |low| low ? 1 : 0 })
|
|
65
|
+
out.bool("so_pending", @so_pending).bool("ca1_low", @ca1_low).int("data_in", @data_in)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load_fields(input, disks)
|
|
69
|
+
@disks = disks
|
|
70
|
+
input.ints_into("cylinders", @cylinders)
|
|
71
|
+
input.ints_into("phases", @phases)
|
|
72
|
+
input.ints_into("positions", @positions)
|
|
73
|
+
input.ints_into("bit_clocks", @clocks)
|
|
74
|
+
input.ints_into("changing", @changing)
|
|
75
|
+
@motors = input.ints("motors", [0, 0]).map { |on| on == 1 }
|
|
76
|
+
@syncs = input.ints("syncs", [0, 0]).map { |low| low == 1 }
|
|
77
|
+
@so_pending = input.bool("so_pending", @so_pending)
|
|
78
|
+
@ca1_low = input.bool("ca1_low", @ca1_low)
|
|
79
|
+
@data_in = input.byte("data_in", @data_in)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
include KeyedState
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The unit's own fields in a keyed snapshot module (BL-D8): its model,
|
|
87
|
+
# its clock phase and cycle count, each bus's last byte, and the
|
|
88
|
+
# controller's lines as the port latched them, under ieee.*. A save
|
|
89
|
+
# settles the unit first, so it is awake; a load finds its passes
|
|
90
|
+
# afresh.
|
|
91
|
+
module KeyedState
|
|
92
|
+
def save_fields(out)
|
|
93
|
+
settle!
|
|
94
|
+
out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
|
|
95
|
+
out.int("ip_data", @ip_bus.data).int("fdc_data", @fdc_bus.data)
|
|
96
|
+
out.int("ieee.host", @ieee_port.host)
|
|
97
|
+
out
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def load_fields(input)
|
|
101
|
+
settle!
|
|
102
|
+
input.choice("model", [model_name], model_name)
|
|
103
|
+
@phase = input.int("phase", @phase)
|
|
104
|
+
@cycles = input.int("cycles", @cycles)
|
|
105
|
+
@ip_bus.data = input.byte("ip_data", @ip_bus.data)
|
|
106
|
+
@fdc_bus.data = input.byte("fdc_data", @fdc_bus.data)
|
|
107
|
+
@ieee_port.host = input.int("ieee.host", @ieee_port.host)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The unit as modules of its own, named from `name` (BL-D8 for
|
|
111
|
+
# device 8): its own fields, the common RAM, UC1 and UE1 with their
|
|
112
|
+
# RAM, each processor, the VIA, the 6530, the drives and each disk
|
|
113
|
+
# in, drive 1's as -DISK1.
|
|
114
|
+
def save_modules(sections, name)
|
|
115
|
+
sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
|
|
116
|
+
sections << Snapshot::Keyed.section("#{name}-COMMON") { |out| @common.save_fields(out, "ram") }
|
|
117
|
+
sections << Snapshot::Keyed.section("#{name}-RIOT1") { |out| @data_riot.save_fields(out) }
|
|
118
|
+
sections << Snapshot::Keyed.section("#{name}-RIOT2") { |out| @control_riot.save_fields(out) }
|
|
119
|
+
sections << Snapshot::Keyed.section("#{name}-IPCPU") { |out| @ip_cpu.save_fields(out) }
|
|
120
|
+
sections << Snapshot::Keyed.section("#{name}-FDCCPU") { |out| @fdc_cpu.save_fields(out) }
|
|
121
|
+
sections << Snapshot::Keyed.section("#{name}-VIA") { |out| @via.save_fields(out) }
|
|
122
|
+
sections << Snapshot::Keyed.section("#{name}-RRIOT") { |out| @rriot.save_fields(out) }
|
|
123
|
+
sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
|
|
124
|
+
[0, 1].each do |drive|
|
|
125
|
+
disk = @mechanism.disk(drive)
|
|
126
|
+
sections << Snapshot::Keyed.section(disk_module(name, drive)) { |out| disk.save_fields(out) } if disk
|
|
127
|
+
end
|
|
128
|
+
sections
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The unit from its modules, `input` being its own. No disk module
|
|
132
|
+
# means no disk in that drive.
|
|
133
|
+
def load_modules(modules, input, name)
|
|
134
|
+
load_fields(input)
|
|
135
|
+
@common.load_fields(modules.required("#{name}-COMMON"), "ram")
|
|
136
|
+
@data_riot.load_fields(modules.required("#{name}-RIOT1"))
|
|
137
|
+
@control_riot.load_fields(modules.required("#{name}-RIOT2"))
|
|
138
|
+
@ip_cpu.load_fields(modules.required("#{name}-IPCPU"))
|
|
139
|
+
@fdc_cpu.load_fields(modules.required("#{name}-FDCCPU"))
|
|
140
|
+
@via.load_fields(modules.required("#{name}-VIA"))
|
|
141
|
+
@rriot.load_fields(modules.required("#{name}-RRIOT"))
|
|
142
|
+
disks = [0, 1].map do |drive|
|
|
143
|
+
disk = modules.optional(disk_module(name, drive))
|
|
144
|
+
disk.nil? ? nil : Disk.from_fields(disk, detached: modules.detached?)
|
|
145
|
+
end
|
|
146
|
+
@mechanism.load_fields(modules.required("#{name}-MECH"), disks)
|
|
147
|
+
@via.restored!
|
|
148
|
+
@ip_cpu.restored!
|
|
149
|
+
@fdc_cpu.restored!
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def disk_module(name, drive) = drive.zero? ? "#{name}-DISK" : "#{name}-DISK1"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
include KeyedState
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# The unit's two drives as the floppy controller sees them through
|
|
7
|
+
# the VIA and the 6530 (schematic 8050001, sheets 2 and 3).
|
|
8
|
+
#
|
|
9
|
+
# VIA port B runs the drives: PB5 drive 0's motor and PB4 drive 1's,
|
|
10
|
+
# each on while low, PB2-3 drive 0's stepper phases and PB0-1 drive
|
|
11
|
+
# 1's, and PB7 reads SYNC, low while ten one bits sit in the shift
|
|
12
|
+
# register. The 6530's port B picks the drive on PB0, the zone on
|
|
13
|
+
# PB1-2 and the side on PB4, low for the second, and reads write
|
|
14
|
+
# protect on PB3, high while the disk can't be written, and the sides
|
|
15
|
+
# on PB6, low for two, which jumper E3 grounds on the 8250. Its port
|
|
16
|
+
# A holds the byte to write.
|
|
17
|
+
#
|
|
18
|
+
# Each phase change moves the head a track: the controller steps
|
|
19
|
+
# through the Gray code 00, 10, 11, 01 outward and back the other way
|
|
20
|
+
# inward ($FF9C), and bumps the head against track 1 with 92 steps
|
|
21
|
+
# out.
|
|
22
|
+
#
|
|
23
|
+
# A turning disk passes one byte cell under the head every ten bit
|
|
24
|
+
# cells, at 6 MHz divided by 16 less the track's zone, and the byte
|
|
25
|
+
# cells come round 300 times a minute (Disk). Each data cell ends in
|
|
26
|
+
# byte ready, which latches the GCR ROM's byte into VIA port A through
|
|
27
|
+
# CA1 and pulses the controller's SO pin; a sync cell holds SYNC low
|
|
28
|
+
# for the cell after it instead. While the VIA's CB2 is low the drive
|
|
29
|
+
# writes: each cell takes the 6530's port A, or a sync while CA2 is
|
|
30
|
+
# high, and still ends in byte ready.
|
|
31
|
+
#
|
|
32
|
+
# A disk going in or out passes the write-protect sensor, which the
|
|
33
|
+
# controller watches for a disk change: it reads high for a while.
|
|
34
|
+
class Mechanism
|
|
35
|
+
WRITE_PROTECT = 0x08
|
|
36
|
+
ONE_SIDE = 0x40
|
|
37
|
+
|
|
38
|
+
# The cycles the write-protect sensor reads high as a disk goes in
|
|
39
|
+
# or out, ten of the controller's 5 ms interrupts.
|
|
40
|
+
CHANGE_CYCLES = 50_000
|
|
41
|
+
|
|
42
|
+
attr_reader :sides, :data_in, :cylinders, :so_pending
|
|
43
|
+
attr_accessor :via, :rriot
|
|
44
|
+
|
|
45
|
+
def initialize(sides:)
|
|
46
|
+
@sides = sides
|
|
47
|
+
@via = nil
|
|
48
|
+
@rriot = nil
|
|
49
|
+
@disks = [nil, nil]
|
|
50
|
+
@cylinders = [0, 0]
|
|
51
|
+
@phases = [0, 0]
|
|
52
|
+
@positions = [0, 0]
|
|
53
|
+
@clocks = [0, 0]
|
|
54
|
+
@changing = [0, 0]
|
|
55
|
+
@motors = [false, false]
|
|
56
|
+
@so_pending = false
|
|
57
|
+
@data_in = 0xff
|
|
58
|
+
@syncs = [false, false]
|
|
59
|
+
@ca1_low = false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The Disk in +drive+, 0 or 1, or nil.
|
|
63
|
+
def disk(drive = 0) = @disks[drive]
|
|
64
|
+
|
|
65
|
+
def insert(disk, drive = 0)
|
|
66
|
+
@disks[drive]&.flush
|
|
67
|
+
@disks[drive] = disk
|
|
68
|
+
@changing[drive] = CHANGE_CYCLES
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def flush
|
|
72
|
+
@disks.each { |disk| disk&.flush }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def motor_on?(drive = 0) = @motors[drive]
|
|
76
|
+
|
|
77
|
+
# The track under +drive+'s head on the side the 6530 picks.
|
|
78
|
+
def track(drive = 0)
|
|
79
|
+
side = drive == selected_drive ? selected_side : 0
|
|
80
|
+
@cylinders[drive] + 1 + (side * Storage::D80Image::SIDE_TRACKS)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cycle!
|
|
84
|
+
if @ca1_low
|
|
85
|
+
@ca1_low = false
|
|
86
|
+
@via.ca1 = true
|
|
87
|
+
end
|
|
88
|
+
drives = @via.port_b_output
|
|
89
|
+
turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
|
|
90
|
+
turn(1, drives.nobits?(0x10), drives & 0x03)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether a cycle would change nothing: both motors off, no disk
|
|
94
|
+
# going in or out, and no byte ready under way.
|
|
95
|
+
def quiet?
|
|
96
|
+
!@motors[0] && !@motors[1] && @changing[0].zero? && @changing[1].zero? && !@ca1_low && !@so_pending
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# What the drives hold, for comparing them at two points.
|
|
100
|
+
def idle_state = [*@cylinders, *@phases, *@motors, *@syncs, @data_in]
|
|
101
|
+
|
|
102
|
+
# Whether byte ready pulses the controller's SO pin this cycle,
|
|
103
|
+
# which takes the pulse.
|
|
104
|
+
def take_so
|
|
105
|
+
so = @so_pending
|
|
106
|
+
@so_pending = false
|
|
107
|
+
so
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Whether ten one bits sit in the shift register, off the selected
|
|
111
|
+
# drive's head.
|
|
112
|
+
def sync? = @syncs[selected_drive]
|
|
113
|
+
|
|
114
|
+
# The 6530's port B inputs.
|
|
115
|
+
def status_lines
|
|
116
|
+
lines = 0xff
|
|
117
|
+
lines &= ~WRITE_PROTECT unless write_protected?(selected_drive)
|
|
118
|
+
@sides == 2 ? lines & ~ONE_SIDE : lines
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def selected_drive = @rriot.port_b_output & 0x01
|
|
124
|
+
|
|
125
|
+
def selected_side = @sides == 2 && @rriot.port_b_output.nobits?(0x10) ? 1 : 0
|
|
126
|
+
|
|
127
|
+
def write_protected?(drive)
|
|
128
|
+
return true if @changing[drive].positive?
|
|
129
|
+
|
|
130
|
+
disk = @disks[drive]
|
|
131
|
+
disk ? disk.write_protected? : false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def turn(drive, motor, phase)
|
|
135
|
+
@changing[drive] -= 1 if @changing[drive].positive?
|
|
136
|
+
step(drive, phase) if phase != @phases[drive]
|
|
137
|
+
motor_off(drive) if @motors[drive] && !motor
|
|
138
|
+
@motors[drive] = motor
|
|
139
|
+
return @syncs[drive] = false unless motor && @disks[drive]
|
|
140
|
+
|
|
141
|
+
clock = @clocks[drive] + 6
|
|
142
|
+
byte = 10 * (16 - Disk.zone_of(track(drive)))
|
|
143
|
+
if clock >= byte
|
|
144
|
+
clock -= byte
|
|
145
|
+
pass_cell(drive)
|
|
146
|
+
end
|
|
147
|
+
@clocks[drive] = clock
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Gray code index of a phase pair: 00, 10, 11, 01.
|
|
151
|
+
def phase_index(phase) = [0, 3, 1, 2][phase]
|
|
152
|
+
|
|
153
|
+
def step(drive, phase)
|
|
154
|
+
change = (phase_index(phase) - phase_index(@phases[drive])) & 0x03
|
|
155
|
+
@phases[drive] = phase
|
|
156
|
+
cylinder = @cylinders[drive]
|
|
157
|
+
if change == 1
|
|
158
|
+
cylinder -= 1
|
|
159
|
+
elsif change == 3
|
|
160
|
+
cylinder += 1
|
|
161
|
+
end
|
|
162
|
+
@cylinders[drive] = cylinder.clamp(0, Storage::D80Image::SIDE_TRACKS - 1)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def motor_off(drive)
|
|
166
|
+
@disks[drive]&.flush
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A cell has passed under +drive+'s head.
|
|
170
|
+
def pass_cell(drive)
|
|
171
|
+
disk = @disks[drive]
|
|
172
|
+
number = track(drive)
|
|
173
|
+
cells = disk.track(number)
|
|
174
|
+
return unless cells
|
|
175
|
+
|
|
176
|
+
position = @positions[drive] % cells.length
|
|
177
|
+
@positions[drive] = position + 1
|
|
178
|
+
return unless drive == selected_drive
|
|
179
|
+
|
|
180
|
+
writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def writing? = (@via.pcr & 0xe0) == 0xc0
|
|
184
|
+
|
|
185
|
+
def read_cell(drive, cells, position)
|
|
186
|
+
cell = cells[position]
|
|
187
|
+
@syncs[drive] = cell == Disk::SYNC
|
|
188
|
+
byte_ready(cell) unless cell == Disk::SYNC
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def write_cell(drive, disk, number, cells, position)
|
|
192
|
+
cells[position] = @via.pcr.allbits?(0x0e) ? Disk::SYNC : @rriot.port_a_output
|
|
193
|
+
disk.written(number)
|
|
194
|
+
@syncs[drive] = false
|
|
195
|
+
byte_ready(@data_in)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def byte_ready(byte)
|
|
199
|
+
@data_in = byte
|
|
200
|
+
@via.ca1 = false
|
|
201
|
+
@ca1_low = true
|
|
202
|
+
@so_pending = true
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# VIA port A reads the decoded byte, and PB7 SYNC, low while the
|
|
207
|
+
# shift register holds a sync.
|
|
208
|
+
class ReadLines
|
|
209
|
+
def initialize(mechanism)
|
|
210
|
+
@mechanism = mechanism
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def read_a(_lines) = @mechanism.data_in
|
|
214
|
+
|
|
215
|
+
def read_b(_lines) = @mechanism.sync? ? 0x7f : 0xff
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The 6530's port B inputs from the drives.
|
|
219
|
+
class StatusLines
|
|
220
|
+
def initialize(mechanism)
|
|
221
|
+
@mechanism = mechanism
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def read_a(_lines) = 0xff
|
|
225
|
+
|
|
226
|
+
def read_b(_lines) = @mechanism.status_lines
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model8050
|
|
6
|
+
# Each chip a class of its own, so each keeps one type of peripheral.
|
|
7
|
+
|
|
8
|
+
# UC1, on the IEEE-488 data lines.
|
|
9
|
+
class DataRIOT < Chips::RIOT
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# UE1, on the control lines, the jumpers and the LEDs.
|
|
13
|
+
class ControlRIOT < Chips::RIOT
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# UM3, the controller's VIA, at $0040.
|
|
17
|
+
class ControllerVIA < Chips::VIA
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# UK3, the controller's 6530, its ROM the controller's program.
|
|
21
|
+
class ControllerRRIOT < Chips::RRIOT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The interface processor's 6502, UN1, on the IPBus.
|
|
25
|
+
class IPCPU
|
|
26
|
+
include Badline::Chips::CPU::Core
|
|
27
|
+
include Engine::IdleCPU
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The floppy controller's 6502, UH3, on the FDCBus.
|
|
31
|
+
class FDCCPU
|
|
32
|
+
include Badline::Chips::CPU::Core
|
|
33
|
+
include Engine::IdleCPU
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|