badline 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/CLAUDE.md +22 -5
- data/README.md +101 -11
- data/doc/media.md +19 -0
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +244 -46
- data/doc/snapshots.md +18 -1
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +4 -26
- data/lib/badline/c128/banks.rb +1 -3
- data/lib/badline/c128/bus.rb +19 -1
- data/lib/badline/c128/mmu_pages.rb +7 -0
- data/lib/badline/c64/bus/extra_sids.rb +80 -0
- data/lib/badline/c64/bus.rb +1 -0
- data/lib/badline/c64/cartridge/action_replay.rb +6 -2
- data/lib/badline/c64/cartridge/comal80.rb +1 -0
- data/lib/badline/c64/cartridge/dinamic.rb +3 -1
- data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
- data/lib/badline/c64/cartridge/fun_play.rb +2 -1
- data/lib/badline/c64/cartridge/game_system.rb +2 -1
- data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
- data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
- data/lib/badline/c64/cartridge/ocean.rb +1 -0
- data/lib/badline/c64/cartridge/pagefox.rb +1 -0
- data/lib/badline/c64/cartridge/rgcd.rb +2 -1
- data/lib/badline/c64/cartridge/super_games.rb +3 -2
- data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
- data/lib/badline/c64/cartridge.rb +3 -3
- data/lib/badline/c64/keyed_modules.rb +6 -2
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/ted/beam.rb +13 -14
- data/lib/badline/chips/ted/fetches.rb +199 -0
- data/lib/badline/chips/ted/keyed_state.rb +156 -0
- data/lib/badline/chips/ted/modes.rb +134 -0
- data/lib/badline/chips/ted/palette.rb +43 -0
- data/lib/badline/chips/ted/picture.rb +182 -0
- data/lib/badline/chips/ted/sound/output.rb +86 -0
- data/lib/badline/chips/ted/sound.rb +284 -0
- data/lib/badline/chips/ted.rb +53 -6
- data/lib/badline/chips/tia.rb +28 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +14 -4
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/drives/engine/core.rb +6 -14
- data/lib/badline/drives/engine/idle.rb +42 -4
- data/lib/badline/drives/engine/idle_cpu.rb +18 -0
- data/lib/badline/drives/engine/serial_connection.rb +25 -0
- data/lib/badline/drives/engine/sleep.rb +1 -0
- data/lib/badline/drives/engine.rb +1 -0
- data/lib/badline/drives/model1541/byte_ready.rb +19 -3
- data/lib/badline/drives/model1541/mechanism.rb +10 -1
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551/bus.rb +138 -0
- data/lib/badline/drives/model1551/cable.rb +104 -0
- data/lib/badline/drives/model1551/cpu.rb +14 -0
- data/lib/badline/drives/model1551/keyed_state.rb +65 -0
- data/lib/badline/drives/model1551/port.rb +74 -0
- data/lib/badline/drives/model1551/tia.rb +92 -0
- data/lib/badline/drives/model1551.rb +170 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/frontend/boot.rb +23 -2
- data/lib/badline/frontend/controls.rb +7 -3
- data/lib/badline/frontend/keys.rb +14 -2
- data/lib/badline/frontend/media_slots.rb +11 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
- data/lib/badline/frontend/menu/menu_media.rb +102 -14
- data/lib/badline/frontend/menu/menu_pages.rb +16 -11
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/frontend/screen.rb +33 -12
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/sound.rb +4 -4
- data/lib/badline/frontend/timeline.rb +19 -6
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +30 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +32 -6
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +29 -4
- data/lib/badline/options/validation.rb +66 -4
- data/lib/badline/options.rb +71 -11
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +245 -46
- data/lib/badline/plus4/cassette_sense.rb +21 -0
- data/lib/badline/plus4/keyboard_port.rb +35 -10
- data/lib/badline/plus4/keyed_modules.rb +116 -0
- data/lib/badline/plus4/keyed_parts.rb +172 -0
- data/lib/badline/plus4/keyed_state.rb +44 -0
- data/lib/badline/plus4/ram_expansion.rb +145 -0
- data/lib/badline/plus4/sid_card.rb +95 -0
- data/lib/badline/plus4/sound_mix.rb +51 -0
- data/lib/badline/plus4/tcbm_drive.rb +120 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +184 -26
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
- data/lib/badline/snapshot/field_doc.rb +48 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +81 -0
- data/lib/badline/snapshot.rb +9 -4
- data/lib/badline/storage/tap.rb +21 -12
- data/lib/badline/support/checkpoint.rb +3 -2
- data/lib/badline/support/plus4_checkpoint.rb +42 -0
- data/lib/badline/version.rb +1 -1
- metadata +29 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The drive's 6510T, a 6510 with an 8-bit port (Port), on the drive's
|
|
7
|
+
# Bus.
|
|
8
|
+
class CPU
|
|
9
|
+
include Badline::Chips::CPU::Core
|
|
10
|
+
include Engine::IdleCPU
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The drive's own fields in a keyed snapshot module (BL-D8): its
|
|
7
|
+
# model, its clock phase, its cycle count, the 555's count to its
|
|
8
|
+
# next pulse and the byte the gate array latched. A save settles the
|
|
9
|
+
# drive first, so it is awake; a load forgets its orbits.
|
|
10
|
+
module KeyedState
|
|
11
|
+
def save_fields(out)
|
|
12
|
+
settle!
|
|
13
|
+
out.text("model", model_name).int("phase", @phase).int("cycles", @cycles).int("irq_timer", @irq_timer)
|
|
14
|
+
out.int("latched_byte", @mechanism.latched_byte)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_fields(input)
|
|
18
|
+
settle!
|
|
19
|
+
input.choice("model", [model_name], model_name)
|
|
20
|
+
@phase = input.int("phase", @phase)
|
|
21
|
+
@cycles = input.int("cycles", @cycles)
|
|
22
|
+
@irq_timer = input.int("irq_timer", @irq_timer).clamp(1, IRQ_PERIOD)
|
|
23
|
+
@mechanism.latched_byte = input.byte("latched_byte", @mechanism.latched_byte)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The drive as modules of its own, named from `name` (BL-D8 for
|
|
27
|
+
# device 8): its own fields, its RAM, the CPU's port, the drive's
|
|
28
|
+
# TIA, its interface's TIA in the computer, the CPU, the mechanism
|
|
29
|
+
# and the disk, when one is in.
|
|
30
|
+
def save_modules(sections, name)
|
|
31
|
+
sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
|
|
32
|
+
sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
|
|
33
|
+
sections << Snapshot::Keyed.section("#{name}-PORT") { |out| @port.save_fields(out) }
|
|
34
|
+
sections << Snapshot::Keyed.section("#{name}-TIA") { |out| @tia.save_fields(out) }
|
|
35
|
+
sections << Snapshot::Keyed.section("#{name}-HOSTTIA") { |out| @host_tia.save_fields(out) }
|
|
36
|
+
sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
|
|
37
|
+
sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
|
|
38
|
+
disk = @mechanism.disk
|
|
39
|
+
sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
|
|
40
|
+
sections
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The drive from its modules, `input` being its own. No disk module
|
|
44
|
+
# means no disk in the drive. The cable's lines and the mechanism's
|
|
45
|
+
# mode follow the TIAs once both are loaded.
|
|
46
|
+
def load_modules(modules, input, name)
|
|
47
|
+
load_fields(input)
|
|
48
|
+
@bus.load_fields(modules.required("#{name}-RAM"))
|
|
49
|
+
@port.load_fields(modules.required("#{name}-PORT"))
|
|
50
|
+
@tia.load_fields(modules.required("#{name}-TIA"))
|
|
51
|
+
@host_tia.load_fields(modules.required("#{name}-HOSTTIA"))
|
|
52
|
+
@cpu.load_fields(modules.required("#{name}-CPU"))
|
|
53
|
+
disk_input = modules.optional("#{name}-DISK")
|
|
54
|
+
disk = disk_input.nil? ? nil : Model1541::Disk.from_fields(disk_input, detached: modules.detached?)
|
|
55
|
+
@mechanism.load_fields(modules.required("#{name}-MECH"), disk)
|
|
56
|
+
@cable.relatch
|
|
57
|
+
@cpu.restored!
|
|
58
|
+
forget_orbits
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
include KeyedState
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The 6510T's port at $00 (direction) and $01 (data), wired to the
|
|
7
|
+
# mechanism in the bits of the 1541's VIA 2 port B (zimmers.net's 1551
|
|
8
|
+
# memory map, schematic 251860):
|
|
9
|
+
#
|
|
10
|
+
# P0-1 stepper phase (out) P4 write protect (in, 1 = writable)
|
|
11
|
+
# P2 motor on (out) P5-6 bit rate, speed zone 0-3 (out)
|
|
12
|
+
# P3 activity LED (out) P7 BYTE LATCHED (in)
|
|
13
|
+
#
|
|
14
|
+
# An output pin reads back what it drives, and a pin left as an input
|
|
15
|
+
# drives nothing into the mechanism.
|
|
16
|
+
class Port
|
|
17
|
+
WRITE_PROTECT = 0x10
|
|
18
|
+
BYTE_LATCHED = 0x80
|
|
19
|
+
|
|
20
|
+
attr_reader :ddr, :data
|
|
21
|
+
|
|
22
|
+
def initialize(mechanism)
|
|
23
|
+
@mechanism = mechanism
|
|
24
|
+
@ddr = 0
|
|
25
|
+
@data = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
@ddr = 0
|
|
30
|
+
@data = 0
|
|
31
|
+
changed
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def peek(addr)
|
|
35
|
+
return @ddr if addr.zero?
|
|
36
|
+
|
|
37
|
+
ddr = @ddr
|
|
38
|
+
(@data & ddr) | (inputs & ~ddr & 0xff)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def poke(addr, value)
|
|
42
|
+
if addr.zero?
|
|
43
|
+
@ddr = value
|
|
44
|
+
else
|
|
45
|
+
@data = value
|
|
46
|
+
end
|
|
47
|
+
changed
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The registers, in a keyed snapshot module (BL-D8-PORT).
|
|
51
|
+
def save_fields(out)
|
|
52
|
+
out.int("ddr", @ddr).int("data", @data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_fields(input)
|
|
56
|
+
@ddr = input.byte("ddr", @ddr)
|
|
57
|
+
@data = input.byte("data", @data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def inputs
|
|
63
|
+
mechanism = @mechanism
|
|
64
|
+
value = (mechanism.read_b(0xff) & WRITE_PROTECT) | (0xff & ~(WRITE_PROTECT | BYTE_LATCHED))
|
|
65
|
+
mechanism.byte_latched? ? value | BYTE_LATCHED : value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def changed
|
|
69
|
+
@mechanism.port_b_written(@data & @ddr)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Drives
|
|
5
|
+
class Model1551
|
|
6
|
+
# The drive's 6523 TIA at $4000-$7FFF, its eight registers mirrored
|
|
7
|
+
# (zimmers.net's 1551 memory map, schematic 251860), a Chips::TIA
|
|
8
|
+
# with what the drive's side wires to it:
|
|
9
|
+
#
|
|
10
|
+
# - Port A carries the cable's eight data lines, pulled up.
|
|
11
|
+
# - Port B reads the GCR byte the gate array latched at BYTE READY
|
|
12
|
+
# and drives the byte the head writes.
|
|
13
|
+
# - Port C drives STATUS0 and STATUS1 (PC0-1), DEV (PC2), which picks
|
|
14
|
+
# where the computer's TIA answers, ACK (PC3) and the head's MODE
|
|
15
|
+
# (PC4, low to write), and reads the device number jumper (PC5,
|
|
16
|
+
# high for device 9), SYNC (PC6, low in a SYNC mark) and DAV (PC7).
|
|
17
|
+
#
|
|
18
|
+
# The DOS sets port C's directions to $1F. The cable's lines are
|
|
19
|
+
# pulled up, so an input nothing drives reads 1.
|
|
20
|
+
#
|
|
21
|
+
# Any access to the TIA clears BYTE LATCHED.
|
|
22
|
+
class TIA
|
|
23
|
+
DAV = 0x80
|
|
24
|
+
SYNC = 0x40
|
|
25
|
+
JUMPER = 0x20
|
|
26
|
+
MODE = 0x10
|
|
27
|
+
|
|
28
|
+
# The chip, whose inputs the drive's side sets before each read.
|
|
29
|
+
attr_reader :chip
|
|
30
|
+
|
|
31
|
+
def initialize(mechanism, device:)
|
|
32
|
+
@mechanism = mechanism
|
|
33
|
+
@jumper = device == 9 ? JUMPER : 0
|
|
34
|
+
@chip = Chips::TIA.new
|
|
35
|
+
inputs = @chip.inputs
|
|
36
|
+
inputs[0] = 0xff
|
|
37
|
+
inputs[1] = 0xff
|
|
38
|
+
inputs[2] = 0xff
|
|
39
|
+
mechanism.tia = self
|
|
40
|
+
mode_written
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ports = @chip.ports
|
|
44
|
+
|
|
45
|
+
def directions = @chip.directions
|
|
46
|
+
|
|
47
|
+
def inputs = @chip.inputs
|
|
48
|
+
|
|
49
|
+
def reset!
|
|
50
|
+
@chip.reset!
|
|
51
|
+
mode_written
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def peek(addr)
|
|
55
|
+
mechanism = @mechanism
|
|
56
|
+
mechanism.release_byte_latch
|
|
57
|
+
chip = @chip
|
|
58
|
+
inputs = chip.inputs
|
|
59
|
+
case addr & 0x07
|
|
60
|
+
when 1 then inputs[1] = mechanism.latched_byte
|
|
61
|
+
when 2 then inputs[2] = (inputs[2] & DAV) | (mechanism.sync? ? 0 : SYNC) | @jumper | 0x1f
|
|
62
|
+
end
|
|
63
|
+
chip.peek(addr)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def poke(addr, value)
|
|
67
|
+
@mechanism.release_byte_latch
|
|
68
|
+
@chip.poke(addr, value)
|
|
69
|
+
register = addr & 0x07
|
|
70
|
+
mode_written if [2, 5].include?(register)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# What port B drives onto the head's write shift register.
|
|
74
|
+
def port_b_output = @chip.output(1)
|
|
75
|
+
|
|
76
|
+
# The chip's registers, in a keyed snapshot module (BL-D8-TIA).
|
|
77
|
+
def save_fields(out) = @chip.save_fields(out)
|
|
78
|
+
|
|
79
|
+
def load_fields(input)
|
|
80
|
+
@chip.load_fields(input)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# MODE selects reading while high, as the 1541's CB2 does.
|
|
86
|
+
def mode_written
|
|
87
|
+
@mechanism.cb2_written(@chip.output(2).anybits?(MODE))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/chips/tia"
|
|
4
|
+
require "badline/drives/model1541"
|
|
5
|
+
require "badline/drives/model1551/cpu"
|
|
6
|
+
require "badline/drives/model1551/port"
|
|
7
|
+
require "badline/drives/model1551/tia"
|
|
8
|
+
require "badline/drives/model1551/bus"
|
|
9
|
+
require "badline/drives/model1551/cable"
|
|
10
|
+
require "badline/drives/model1551/keyed_state"
|
|
11
|
+
|
|
12
|
+
module Badline
|
|
13
|
+
module Drives
|
|
14
|
+
# The 1551 disk drive of the 264 series: a 6510T at 2 MHz running DOS
|
|
15
|
+
# 2.6 from 16 KB of ROM, with 2 KB of RAM, the 1541's GCR mechanism
|
|
16
|
+
# behind a gate array, and a 6523 TIA facing the computer over TCBM, a
|
|
17
|
+
# parallel cable to a second 6523 in the computer's expansion port
|
|
18
|
+
# (Commodore schematic 251860; zimmers.net's 1551 memory map).
|
|
19
|
+
#
|
|
20
|
+
# The 6510T's port drives the mechanism as the 1541's VIA 2 port B does
|
|
21
|
+
# and reads the write-protect sensor and the gate array's BYTE LATCHED
|
|
22
|
+
# (Port). The TIA's port B reads the GCR byte the gate array latched and
|
|
23
|
+
# drives the byte to write, and its port C selects reading or writing
|
|
24
|
+
# and reads SYNC (TIA).
|
|
25
|
+
#
|
|
26
|
+
# The drive comes with its interface for the computer's expansion port,
|
|
27
|
+
# a second 6523 (#host_tia), which the cable joins to the drive's
|
|
28
|
+
# (Cable). The computer puts it at $FEE0 for device 8 and at $FEC0 for
|
|
29
|
+
# device 9.
|
|
30
|
+
#
|
|
31
|
+
# A 555 timer pulls IRQ low for about 7 µs every 8.3 ms, its R1 120K,
|
|
32
|
+
# R2 100R and C1 0.1 µF, which runs the DOS's job loop. Nothing
|
|
33
|
+
# acknowledges it: the line lets go on its own.
|
|
34
|
+
#
|
|
35
|
+
# Drives::Engine::Core clocks it against the host and sleeps through
|
|
36
|
+
# the DOS's idle loop, which counts its passes in $61 (Engine::Idle's
|
|
37
|
+
# pass counter), until the 555 is due to pull IRQ, the count wraps or
|
|
38
|
+
# the computer moves the cable's lines.
|
|
39
|
+
class Model1551
|
|
40
|
+
include Engine::Core
|
|
41
|
+
|
|
42
|
+
CLOCK_HZ = 2_000_000
|
|
43
|
+
|
|
44
|
+
# The 555's cycle in drive cycles: high for 0.693 (R1 + R2) C and low,
|
|
45
|
+
# pulling IRQ, for 0.693 R2 C.
|
|
46
|
+
IRQ_PERIOD = 16_660
|
|
47
|
+
IRQ_LOW = 14
|
|
48
|
+
|
|
49
|
+
# The host cycles, at about 1 MHz, by which a 1551 switched on with the
|
|
50
|
+
# machine has run its DOS's reset, the RAM test and the ROM checksum,
|
|
51
|
+
# and answers TCBM: half a second, with a margin.
|
|
52
|
+
BOOT_CYCLES = 600_000
|
|
53
|
+
|
|
54
|
+
# Where the DOS's idle loop starts over, polling the TIA's port A for
|
|
55
|
+
# a command from the computer.
|
|
56
|
+
IDLE_LOOP = 0xeabd
|
|
57
|
+
|
|
58
|
+
# The byte the idle loop increments on each pass.
|
|
59
|
+
PASS_COUNTER = 0x61
|
|
60
|
+
|
|
61
|
+
def port
|
|
62
|
+
settle!
|
|
63
|
+
@port
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tia
|
|
67
|
+
settle!
|
|
68
|
+
@tia
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The TIA in the computer's expansion port, which the computer reads
|
|
72
|
+
# and writes without waking the drive.
|
|
73
|
+
attr_reader :host_tia
|
|
74
|
+
|
|
75
|
+
# The cable between the two TIAs.
|
|
76
|
+
def cable
|
|
77
|
+
settle!
|
|
78
|
+
@cable
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def model_name = "1551"
|
|
82
|
+
|
|
83
|
+
# +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
|
|
84
|
+
# path. +device+ is 8 or 9, as the jumper on the TIA's PC5 sets it.
|
|
85
|
+
def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
|
|
86
|
+
raise ArgumentError, "a 1551 is device 8 or 9, not #{device}" unless [8, 9].include?(device)
|
|
87
|
+
|
|
88
|
+
@device = device
|
|
89
|
+
@clock_hz = CLOCK_HZ
|
|
90
|
+
@mechanism = Model1541::Mechanism.new
|
|
91
|
+
@mechanism.clock_hz = CLOCK_HZ
|
|
92
|
+
@port = Port.new(@mechanism)
|
|
93
|
+
@tia = TIA.new(@mechanism, device:)
|
|
94
|
+
@host_tia = Chips::TIA.new
|
|
95
|
+
@bus = Bus.new(rom: rom || ROM.load("dos1551.rom", 0xc000), port: @port, tia: @tia)
|
|
96
|
+
@cpu = CPU.new(@bus, debug:)
|
|
97
|
+
@idle_loop = IDLE_LOOP
|
|
98
|
+
@host_clock_hz = host_clock_hz
|
|
99
|
+
@phase = 0
|
|
100
|
+
@cycles = 0
|
|
101
|
+
@irq_timer = IRQ_PERIOD
|
|
102
|
+
init_idle(debug)
|
|
103
|
+
@pass_counter = PASS_COUNTER
|
|
104
|
+
@cable = Cable.new(@host_tia, @tia.chip, self)
|
|
105
|
+
@serial_port = @cable
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Puts in a Disk from the .d64 or .g64 image at +path+, write-protected
|
|
109
|
+
# with `read_only`.
|
|
110
|
+
def insert_image(path, read_only: false)
|
|
111
|
+
insert(Model1541::Disk.open(path, read_only:))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The computer's RESET line reaches the interface's TIA and, through
|
|
115
|
+
# the cable, the CPU, its port and the drive's TIA. RAM keeps its
|
|
116
|
+
# contents, and the 555 runs on.
|
|
117
|
+
def reset!
|
|
118
|
+
settle!
|
|
119
|
+
@host_tia.reset!
|
|
120
|
+
@port.reset!
|
|
121
|
+
@tia.reset!
|
|
122
|
+
@cpu.reset!
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# One drive cycle: the mechanism turns the disk ahead of the CPU, and
|
|
128
|
+
# the 555 pulls IRQ while its output is low.
|
|
129
|
+
def step
|
|
130
|
+
@mechanism.cycle!
|
|
131
|
+
timer = @irq_timer - 1
|
|
132
|
+
timer = IRQ_PERIOD if timer.zero?
|
|
133
|
+
@irq_timer = timer
|
|
134
|
+
@cpu.irq = timer <= IRQ_LOW
|
|
135
|
+
@cpu.cycle!
|
|
136
|
+
@cycles += 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# A pass of the idle loop reads the TIA's port A, which holds still
|
|
140
|
+
# until the computer moves the cable's lines, and RAM. It repeats
|
|
141
|
+
# while the motor is off and the 555 leaves IRQ alone, for as long as
|
|
142
|
+
# the 555 leaves room (see Engine::Idle).
|
|
143
|
+
def busy_for_pass? = @mechanism.motor_on? || @irq_timer <= IRQ_LOW
|
|
144
|
+
|
|
145
|
+
def quiet_cycles
|
|
146
|
+
quiet = @irq_timer - IRQ_LOW - 1
|
|
147
|
+
quiet.negative? ? 0 : quiet
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def idle_state
|
|
151
|
+
chip = @tia.chip
|
|
152
|
+
[*@cpu.counting_state, @bus.data, *@mechanism.idle_state, *chip.ports, *chip.directions, *chip.inputs,
|
|
153
|
+
@port.ddr, @port.data]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def fast_forward_chips(cycles)
|
|
157
|
+
@irq_timer -= cycles
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Every 555 pulse runs the job loop, which reads the port and the TIA,
|
|
161
|
+
# so no orbit (Engine::Orbit) comes round, and the drive keeps no
|
|
162
|
+
# anchors.
|
|
163
|
+
def anchor = nil
|
|
164
|
+
|
|
165
|
+
def counter_state(_touched) = []
|
|
166
|
+
|
|
167
|
+
def skip_chip_orbits(_cycles, _touched) = nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -17,7 +17,11 @@ module Badline
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
computer = nil
|
|
20
|
-
problem =
|
|
20
|
+
problem = if cartridge?(options)
|
|
21
|
+
cartridge_problem { computer = boot(options) }
|
|
22
|
+
else
|
|
23
|
+
media_problem { computer = boot(options) }
|
|
24
|
+
end
|
|
21
25
|
unless problem.empty?
|
|
22
26
|
warn "#{options.program}: #{options.media_path}: #{problem}"
|
|
23
27
|
return 1
|
|
@@ -40,6 +44,21 @@ module Badline
|
|
|
40
44
|
e.message
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
# Runs the block that puts a cartridge in, as media_problem does, and
|
|
48
|
+
# also returns the message of the ArgumentError a machine raises over a
|
|
49
|
+
# cartridge with a chip in a bank or slot it doesn't map.
|
|
50
|
+
def self.cartridge_problem(&)
|
|
51
|
+
media_problem(&)
|
|
52
|
+
rescue ArgumentError => e
|
|
53
|
+
e.message
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether the media the options start is a .crt.
|
|
57
|
+
def self.cartridge?(options)
|
|
58
|
+
media = options.media_path
|
|
59
|
+
!media.nil? && Media::Extensions.kind(media) == :cartridge
|
|
60
|
+
end
|
|
61
|
+
|
|
43
62
|
# The machine a .vsf snapshot holds, built as the saved one was, or a
|
|
44
63
|
# new one built from the options with the media attached.
|
|
45
64
|
def self.boot(options)
|
|
@@ -55,11 +74,13 @@ module Badline
|
|
|
55
74
|
|
|
56
75
|
# A new machine built from the options (Machine.for_options), with
|
|
57
76
|
# `media`, if not nil, attached and started as the command line does,
|
|
58
|
-
# its disk writable if `writable
|
|
77
|
+
# its disk writable if `writable`, after the Plus/4's cartridges the
|
|
78
|
+
# options put in its slots. The VIC-20 and the Plus/4 raise
|
|
59
79
|
# Machine::Unsupported for a .sid tune.
|
|
60
80
|
def self.start(options, media, writable)
|
|
61
81
|
computer = build(options, media)
|
|
62
82
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
83
|
+
options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
|
|
63
84
|
unless media.nil?
|
|
64
85
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
65
86
|
disk: { read_only: !writable })
|
|
@@ -117,7 +117,11 @@ module Badline
|
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def host_key(scancode)
|
|
120
|
-
@computer.family
|
|
120
|
+
case @computer.family
|
|
121
|
+
when :c128 then Keys.c128_key(scancode, @computer.mode)
|
|
122
|
+
when :plus4 then Keys.plus4_key(scancode)
|
|
123
|
+
else Keys.c64_key(scancode)
|
|
124
|
+
end
|
|
121
125
|
end
|
|
122
126
|
|
|
123
127
|
# RESTORE isn't in the key matrix, so it goes to the machine instead.
|
|
@@ -196,8 +200,8 @@ module Badline
|
|
|
196
200
|
end
|
|
197
201
|
|
|
198
202
|
def release_all
|
|
199
|
-
Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(
|
|
200
|
-
Keys::WASD.each_key { |scancode| @computer.keyboard.release(
|
|
203
|
+
Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
|
|
204
|
+
Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
|
|
201
205
|
Keys::ARROWS.each_value do |direction|
|
|
202
206
|
@computer.joystick1.release(direction)
|
|
203
207
|
@computer.joystick2.release(direction)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# SDL scancodes follow key positions on a US layout. They map onto the C64
|
|
6
|
-
# keys, the C128's own keys and, in joystick mode, the
|
|
7
|
-
# directions.
|
|
6
|
+
# keys, the C128's and the 264's own keys and, in joystick mode, the
|
|
7
|
+
# joystick directions.
|
|
8
8
|
module Keys
|
|
9
9
|
RETURN = 40
|
|
10
10
|
ESCAPE = 41
|
|
@@ -56,6 +56,15 @@ module Badline
|
|
|
56
56
|
63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
|
|
57
57
|
}.freeze
|
|
58
58
|
|
|
59
|
+
# The 264's keys where they differ from the C64's: ESC on Esc, with
|
|
60
|
+
# RUN/STOP on Page Down as on the C128, F1-F3 on F1-F3, HELP on F4,
|
|
61
|
+
# Insert or Help, a cursor key on each arrow, and either SHIFT on the
|
|
62
|
+
# one SHIFT. Page Up, RESTORE on the C64, presses nothing.
|
|
63
|
+
PLUS4_OTHERS = {
|
|
64
|
+
41 => :esc, 78 => :run_stop, 58 => :f1, 59 => :f2, 60 => :f3, 61 => :help, 73 => :help, 117 => :help,
|
|
65
|
+
79 => :crsr_right, 80 => :crsr_left, 81 => :crsr_down, 82 => :crsr_up, 229 => :lshift, 75 => :none
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
59
68
|
ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
|
|
60
69
|
WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
|
|
61
70
|
|
|
@@ -77,6 +86,9 @@ module Badline
|
|
|
77
86
|
C128_OTHERS[scancode] || c64_key(scancode)
|
|
78
87
|
end
|
|
79
88
|
|
|
89
|
+
# The 264's key for the scancode: one of PLUS4_OTHERS or the C64's.
|
|
90
|
+
def self.plus4_key(scancode) = PLUS4_OTHERS[scancode] || c64_key(scancode)
|
|
91
|
+
|
|
80
92
|
# The character the key types on a US layout: a letter, a capital
|
|
81
93
|
# with `shift`, or a digit, and "" for any other key.
|
|
82
94
|
def self.character(scancode, shift)
|
|
@@ -31,8 +31,18 @@ module Badline
|
|
|
31
31
|
drive.nil? ? computer.unmount : drive.insert(nil)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# Whether the expansion port has a cartridge in, on the Plus/4 in
|
|
35
|
+
# either cartridge's slots.
|
|
36
|
+
def cartridge?(computer)
|
|
37
|
+
return !computer.cartridge_slots.empty? if computer.family == :plus4
|
|
38
|
+
|
|
39
|
+
!computer.address_bus.cartridge.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Takes the cartridge out with the power off, on the Plus/4 both.
|
|
35
43
|
def remove_cartridge(computer)
|
|
44
|
+
return computer.detach_cartridges if computer.family == :plus4
|
|
45
|
+
|
|
36
46
|
computer.address_bus.detach_cartridge
|
|
37
47
|
computer.power_cycle!
|
|
38
48
|
end
|
|
@@ -37,7 +37,12 @@ module Badline
|
|
|
37
37
|
# :program, or :snapshot, which starts in the saves folder.
|
|
38
38
|
def browse(kind)
|
|
39
39
|
@browsing = kind
|
|
40
|
-
|
|
40
|
+
extensions = case kind
|
|
41
|
+
when :cartridge then @media.cartridge_extensions
|
|
42
|
+
when :program then programs
|
|
43
|
+
else EXTENSIONS.fetch(kind)
|
|
44
|
+
end
|
|
45
|
+
@browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
# Asks for a save's name, starting from `name`.
|
|
@@ -55,15 +60,15 @@ module Badline
|
|
|
55
60
|
def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
|
|
56
61
|
|
|
57
62
|
# Puts a dropped disk or tape in its device, or asks before a
|
|
58
|
-
# cartridge
|
|
59
|
-
# files are left alone.
|
|
63
|
+
# cartridge, a Plus/4's raw ROM image among them, or a program
|
|
64
|
+
# starts, and says whether it asks. Other files are left alone.
|
|
60
65
|
def drop(path)
|
|
61
66
|
extension = File.extname(path).downcase
|
|
62
67
|
return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
|
|
63
68
|
return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
|
|
64
|
-
return false unless
|
|
69
|
+
return false unless programs.include?(extension)
|
|
65
70
|
|
|
66
|
-
@confirmation.ask(extension
|
|
71
|
+
@confirmation.ask(@media.cartridge_extensions.include?(extension) ? :cartridge : :start, path)
|
|
67
72
|
true
|
|
68
73
|
end
|
|
69
74
|
|
|
@@ -97,6 +102,10 @@ module Badline
|
|
|
97
102
|
|
|
98
103
|
private
|
|
99
104
|
|
|
105
|
+
# What Quick open and a drop start: every medium, and the cartridges
|
|
106
|
+
# the machine takes.
|
|
107
|
+
def programs = (EXTENSIONS[:program] + @media.cartridge_extensions).uniq
|
|
108
|
+
|
|
100
109
|
# Puts the disk or tape picked in, or asks before a cartridge or
|
|
101
110
|
# Quick open power cycles the machine, and leaves the browser, as Esc
|
|
102
111
|
# does.
|