badline 0.15.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 +63 -0
- data/CLAUDE.md +7 -0
- data/README.md +40 -10
- data/doc/media.md +19 -0
- data/doc/snapshot-format.md +61 -13
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/machine_player.rb +2 -14
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/tia.rb +16 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +8 -3
- 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 +20 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +13 -5
- data/lib/badline/frontend/menu/menu_media.rb +92 -14
- data/lib/badline/frontend/menu/menu_pages.rb +11 -6
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/machine.rb +9 -5
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/options/help.rb +4 -2
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +14 -2
- data/lib/badline/options/validation.rb +49 -9
- data/lib/badline/options.rb +34 -2
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +181 -45
- data/lib/badline/plus4/keyed_modules.rb +22 -5
- data/lib/badline/plus4/keyed_parts.rb +33 -4
- data/lib/badline/plus4/keyed_state.rb +5 -2
- 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 +103 -21
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/snapshot/field_doc.rb +7 -5
- data/lib/badline/snapshot/plus4_setup.rb +52 -10
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/version.rb +1 -1
- metadata +15 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Plus4
|
|
5
|
+
# A SID card's SID at $FD40-$FD5F, or at $FE80-$FE9F as Csory's card
|
|
6
|
+
# puts it, clocked once a TED cycle, as the cards clock it from the
|
|
7
|
+
# TED's single clock. The SID counts the
|
|
8
|
+
# cycles that have passed whenever something touches it, rather than
|
|
9
|
+
# every cycle: a CPU access runs it through the cycle the access is in,
|
|
10
|
+
# and the host, between cycles, up to the cycle about to start.
|
|
11
|
+
# `clock` answers the machine's cycles.
|
|
12
|
+
class SIDCard
|
|
13
|
+
# Where a card can put its SID.
|
|
14
|
+
ADDRESSES = [0xfd40, 0xfe80].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :sid, :address
|
|
17
|
+
|
|
18
|
+
def initialize(clock, model, address = ADDRESSES.first)
|
|
19
|
+
raise ArgumentError, format("no SID card at $%04X", address) unless ADDRESSES.include?(address)
|
|
20
|
+
|
|
21
|
+
@clock = clock
|
|
22
|
+
@address = address
|
|
23
|
+
@sid = Chips::SID.new(model:, at: address)
|
|
24
|
+
@clocked = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def model = @sid.model
|
|
28
|
+
|
|
29
|
+
def model=(model)
|
|
30
|
+
catch_up!
|
|
31
|
+
@sid.model = model
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def peek(addr)
|
|
35
|
+
run_to(@clock.cycles + 1)
|
|
36
|
+
@sid.peek(addr)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def poke(addr, value)
|
|
40
|
+
run_to(@clock.cycles + 1)
|
|
41
|
+
@sid.poke(addr, value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The RES line, from the host between cycles.
|
|
45
|
+
def reset!
|
|
46
|
+
run_to(@clock.cycles)
|
|
47
|
+
@sid.reset!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Runs the SID, its voices and its filter, up to the current cycle.
|
|
51
|
+
def catch_up!
|
|
52
|
+
run_to(@clock.cycles)
|
|
53
|
+
@sid.catch_up!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record(rate:, clock_hz:)
|
|
57
|
+
run_to(@clock.cycles)
|
|
58
|
+
@sid.record(rate:, clock_hz:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def drain_samples
|
|
62
|
+
run_to(@clock.cycles)
|
|
63
|
+
@sid.drain_samples
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The SID's fields in a keyed snapshot module (BL-SID), saved once the
|
|
67
|
+
# machine has settled it (#catch_up!).
|
|
68
|
+
def save_fields(out) = @sid.save_fields(out)
|
|
69
|
+
|
|
70
|
+
def load_fields(input)
|
|
71
|
+
@sid.load_fields(input)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The SID counts on from the machine's cycle the snapshot was taken at.
|
|
75
|
+
def restored!
|
|
76
|
+
@sid.restored!
|
|
77
|
+
@clocked = @clock.cycles
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def run_to(target)
|
|
83
|
+
clocked = @clocked
|
|
84
|
+
return if clocked >= target
|
|
85
|
+
|
|
86
|
+
sid = @sid
|
|
87
|
+
while clocked < target
|
|
88
|
+
sid.cycle!
|
|
89
|
+
clocked += 1
|
|
90
|
+
end
|
|
91
|
+
@clocked = clocked
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Plus4
|
|
5
|
+
# What a front end plays from a 264 with a SID card fitted: the TED's
|
|
6
|
+
# sound and the card's SID, each recorded at the audio rate and added
|
|
7
|
+
# together, clipped to a signed 16-bit sample. A sample one source has
|
|
8
|
+
# closed waits for the other's. The SID's model is the mix's, as it
|
|
9
|
+
# is a C64's sound source's, for the pause menu's Sound page.
|
|
10
|
+
class SoundMix
|
|
11
|
+
def initialize(ted_sound, sid)
|
|
12
|
+
@ted_sound = ted_sound
|
|
13
|
+
@sid = sid
|
|
14
|
+
@ted_waiting = []
|
|
15
|
+
@sid_waiting = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def model = @sid.model
|
|
19
|
+
|
|
20
|
+
def model=(model)
|
|
21
|
+
@sid.model = model
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Starts both sources recording at `rate` samples a second, their
|
|
25
|
+
# cycles taken to run at `clock_hz`.
|
|
26
|
+
def record(rate:, clock_hz:)
|
|
27
|
+
@ted_sound.record(rate:, clock_hz:)
|
|
28
|
+
@sid.record(rate:, clock_hz:)
|
|
29
|
+
@ted_waiting = []
|
|
30
|
+
@sid_waiting = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Runs both sources up to the current cycle.
|
|
34
|
+
def catch_up!
|
|
35
|
+
@ted_sound.catch_up!
|
|
36
|
+
@sid.catch_up!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The mixed samples both sources have closed since the last drain.
|
|
40
|
+
def drain_samples
|
|
41
|
+
ted = @ted_waiting.concat(@ted_sound.drain_samples)
|
|
42
|
+
sid = @sid_waiting.concat(@sid.drain_samples)
|
|
43
|
+
count = [ted.length, sid.length].min
|
|
44
|
+
mixed = Array.new(count) { |index| (ted[index] + sid[index]).clamp(-0x8000, 0x7fff) }
|
|
45
|
+
@ted_waiting = ted[count, ted.length - count] || []
|
|
46
|
+
@sid_waiting = sid[count, sid.length - count] || []
|
|
47
|
+
mixed
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Plus4
|
|
5
|
+
# The 264 series' true drives: a Drives::Model1541 and a
|
|
6
|
+
# Drives::Model1581 on the serial bus (C64::TrueDrives), and a
|
|
7
|
+
# Drives::Model1551 on TCBM, its interface's TIA in the expansion port
|
|
8
|
+
# at $FEE0 for device 8 or $FEC0 for device 9. Each sits in a field of
|
|
9
|
+
# its own, so none makes another's per-cycle call a union.
|
|
10
|
+
#
|
|
11
|
+
# A device number has one drive at a time: a 1551 takes the place of a
|
|
12
|
+
# serial drive with its number, and the other way round.
|
|
13
|
+
module TCBMDrive
|
|
14
|
+
# The true drive models plug_true_drive builds.
|
|
15
|
+
TRUE_DRIVE_MODELS = %w[1541 1551].freeze
|
|
16
|
+
|
|
17
|
+
# The 1551 on TCBM, or nil.
|
|
18
|
+
attr_reader :drive1551
|
|
19
|
+
|
|
20
|
+
# The drive plug_true_drive plugs in, "1541" or "1551".
|
|
21
|
+
attr_reader :true_drive_model
|
|
22
|
+
|
|
23
|
+
# Plugs in a Drives::Model1551, its interface's TIA where its device
|
|
24
|
+
# number puts it, in place of the TCBM interface's own there, and in
|
|
25
|
+
# place of a serial drive with its number. The traps stop answering
|
|
26
|
+
# device 8 when the drive is device 8.
|
|
27
|
+
def attach_drive1551(drive)
|
|
28
|
+
detach_drive1551
|
|
29
|
+
device = drive.device
|
|
30
|
+
detach_drive1541 if @drive1541&.device == device
|
|
31
|
+
detach_drive1581 if @drive1581&.device == device
|
|
32
|
+
drive.host_clock_hz = timing.clock_hz
|
|
33
|
+
@drive1551 = drive
|
|
34
|
+
@bus.plug_tia(drive.device, drive.host_tia)
|
|
35
|
+
@serial_trap&.device = serial_trap_device
|
|
36
|
+
@init_threshold = [@init_threshold, Badline::Drives::Model1551::BOOT_CYCLES].max if @cycles < @init_threshold
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Unplugs the Drives::Model1551, leaving the TCBM interface's own TIA,
|
|
40
|
+
# if there is one, in its place.
|
|
41
|
+
def detach_drive1551
|
|
42
|
+
drive = @drive1551
|
|
43
|
+
return unless drive
|
|
44
|
+
|
|
45
|
+
@bus.plug_tia(drive.device, tcbm_tia(drive.device))
|
|
46
|
+
@drive1551 = nil
|
|
47
|
+
@serial_trap&.device = serial_trap_device
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The true drive: a 1581 ahead of a 1551 ahead of a 1541.
|
|
51
|
+
def true_drive = @drive1581 || @drive1551 || @drive1541
|
|
52
|
+
|
|
53
|
+
# Plugs in the model true_drive_model names as device 8, in place of
|
|
54
|
+
# the drive there, and returns it.
|
|
55
|
+
def plug_true_drive
|
|
56
|
+
return plug_drive1551 if @true_drive_model == "1551"
|
|
57
|
+
|
|
58
|
+
unplug_device8
|
|
59
|
+
detach_drive1581 if drive1581_on_device8?
|
|
60
|
+
Badline::Drives::Model1541.new.tap { |drive| attach_drive1541(drive) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Plugs in a 1551 as device 8, in place of the drive there, and
|
|
64
|
+
# returns it.
|
|
65
|
+
def plug_drive1551
|
|
66
|
+
Badline::Drives::Model1551.new.tap { |drive| attach_drive1551(drive) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def init_drives(true_drive_model)
|
|
72
|
+
unless TRUE_DRIVE_MODELS.include?(true_drive_model)
|
|
73
|
+
raise ArgumentError, "no true drive model #{true_drive_model} for the Plus/4"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@true_drive_model = true_drive_model
|
|
77
|
+
@drive1541 = nil
|
|
78
|
+
@drive1551 = nil
|
|
79
|
+
@drive1581 = nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def save_drive1551_modules(sections)
|
|
83
|
+
drive = @drive1551
|
|
84
|
+
drive&.save_modules(sections, "BL-D#{drive.device}")
|
|
85
|
+
sections
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_drive1551_modules(modules, input, device)
|
|
89
|
+
return input.adjust("a 1551 as device #{device}, left out") unless [8, 9].include?(device)
|
|
90
|
+
|
|
91
|
+
detach_drive1551 if @drive1551 && @drive1551.device != device
|
|
92
|
+
attach_drive1551(Badline::Drives::Model1551.new(device:)) unless @drive1551
|
|
93
|
+
@drive1551.load_modules(modules, input, "BL-D#{device}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The TCBM interface's own TIA for +device+, or nil without one.
|
|
97
|
+
def tcbm_tia(device)
|
|
98
|
+
tias = @tias
|
|
99
|
+
return nil if tias.empty?
|
|
100
|
+
|
|
101
|
+
device == 9 ? tias[1] : tias[0]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Device 8, unless a true drive is device 8.
|
|
105
|
+
def serial_trap_device
|
|
106
|
+
device = KernalTrap::Routine::DEVICE
|
|
107
|
+
on_device8 = @drive1551&.device == device || @drive1541&.device == device || drive1581_on_device8?
|
|
108
|
+
on_device8 ? nil : device
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Unplugs the 1541 and the 1551 if either is device 8, for a 1581 to
|
|
112
|
+
# take its place.
|
|
113
|
+
def unplug_device8
|
|
114
|
+
device = KernalTrap::Routine::DEVICE
|
|
115
|
+
detach_drive1541 if @drive1541&.device == device
|
|
116
|
+
detach_drive1551 if @drive1551&.device == device
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -6,22 +6,31 @@ module Badline
|
|
|
6
6
|
# latch: a line reads low where the latch or the outside pulls it. With
|
|
7
7
|
# nothing plugged into the user port, the outside pulls only bit 2, the
|
|
8
8
|
# cassette sense switch, low while a key is down on the deck. The
|
|
9
|
-
# KERNAL's IRQ reads it there ($CFBF)
|
|
9
|
+
# KERNAL's IRQ reads it there ($CFBF), and takes P1 as RS-232's clear
|
|
10
|
+
# to send, sending through the ACIA only while it reads high.
|
|
11
|
+
#
|
|
12
|
+
# A device on the user port drives P0-P7 through #inputs, low where its
|
|
13
|
+
# bit is clear.
|
|
10
14
|
class UserPort
|
|
11
15
|
TAPE_SENSE = 0x04
|
|
12
16
|
|
|
13
17
|
attr_reader :output
|
|
18
|
+
attr_accessor :inputs
|
|
14
19
|
|
|
15
20
|
def initialize(datasette)
|
|
16
21
|
@datasette = datasette
|
|
17
22
|
@output = 0xff
|
|
23
|
+
@inputs = 0xff
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def reset!
|
|
21
27
|
@output = 0xff
|
|
22
28
|
end
|
|
23
29
|
|
|
24
|
-
def peek(_addr)
|
|
30
|
+
def peek(_addr)
|
|
31
|
+
lines = @output & @inputs
|
|
32
|
+
@datasette.sense_low? ? lines & ~TAPE_SENSE : lines
|
|
33
|
+
end
|
|
25
34
|
|
|
26
35
|
def poke(_addr, value)
|
|
27
36
|
@output = value
|
data/lib/badline/plus4.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/chips/ted"
|
|
4
4
|
require "badline/chips/tia"
|
|
5
|
+
require "badline/drives/model1551"
|
|
5
6
|
require "badline/plus4/model"
|
|
6
7
|
require "badline/plus4/cpu"
|
|
7
8
|
require "badline/plus4/cpu_port"
|
|
@@ -9,7 +10,11 @@ require "badline/plus4/keyboard_port"
|
|
|
9
10
|
require "badline/plus4/user_port"
|
|
10
11
|
require "badline/plus4/cassette_sense"
|
|
11
12
|
require "badline/plus4/acia"
|
|
13
|
+
require "badline/plus4/sid_card"
|
|
14
|
+
require "badline/plus4/sound_mix"
|
|
15
|
+
require "badline/plus4/ram_expansion"
|
|
12
16
|
require "badline/plus4/bus"
|
|
17
|
+
require "badline/plus4/tcbm_drive"
|
|
13
18
|
require "badline/snapshot/plus4_setup"
|
|
14
19
|
require "badline/plus4/keyed_parts"
|
|
15
20
|
require "badline/plus4/keyed_state"
|
|
@@ -19,20 +24,31 @@ module Badline
|
|
|
19
24
|
# The Commodore 264 series: the Plus/4, the C16 and the C116. A 7501 on
|
|
20
25
|
# the TED's clock, which runs it once or twice a TED cycle (Chips::TED's
|
|
21
26
|
# Beam), the TED and, on the Plus/4, a 6551 ACIA and the user port's
|
|
22
|
-
# 6529.
|
|
23
|
-
# doesn't have.
|
|
27
|
+
# 6529. The TED and the ACIA pull IRQ, and nothing pulls NMI, which the
|
|
28
|
+
# 7501 doesn't have.
|
|
24
29
|
#
|
|
25
30
|
# The TED paints the picture a front end shows (#video, #timing). Device 8
|
|
26
31
|
# serves disks through the KERNAL traps (C64::KernalTraps), or a true
|
|
27
|
-
# 1541 or 1581 on the serial bus does (
|
|
32
|
+
# 1541 or 1581 on the serial bus or a 1551 on TCBM does (TCBMDrive),
|
|
33
|
+
# cartridges go
|
|
28
34
|
# in the two cartridges' slots, and tapes play on the datasette, whose read
|
|
29
35
|
# line's level the KERNAL times with the TED's timers.
|
|
36
|
+
#
|
|
37
|
+
# A SID card, as third parties built for the 264s, puts a SID at
|
|
38
|
+
# $FD40-$FD5F, or at $FE80-$FE9F as Csory's does. The cards clock it from the TED's single clock, one SID
|
|
39
|
+
# cycle a TED cycle, 886,724 Hz on PAL, below the C64's 985,248 Hz, so a
|
|
40
|
+
# C64 tune plays about a semitone flat (plus4world forum threads 38287
|
|
41
|
+
# and 20130).
|
|
42
|
+
#
|
|
43
|
+
# A RAM expansion, Hannes's 256K or 1M or Csory's 256K, banks more RAM
|
|
44
|
+
# in through a register at $FD16 (RAMExpansion).
|
|
30
45
|
class Plus4
|
|
31
46
|
include IntegerHelper
|
|
32
47
|
include Machine::KeyboardBuffer
|
|
33
48
|
include Machine::Clocking
|
|
34
49
|
include Machine::Settling
|
|
35
50
|
include C64::TrueDrives
|
|
51
|
+
include TCBMDrive
|
|
36
52
|
include C64::KernalTraps
|
|
37
53
|
|
|
38
54
|
# The cycle by which the KERNAL has booted and BASIC printed READY,
|
|
@@ -46,7 +62,7 @@ module Badline
|
|
|
46
62
|
DRIVE_BOOT_CYCLES = 1_200_000
|
|
47
63
|
|
|
48
64
|
attr_reader :model, :bus, :cpu, :ted, :sound, :port, :keyboard, :keyboard_port, :joystick1, :joystick2, :acia,
|
|
49
|
-
:user_port, :datasette, :tias, :iec_bus, :cycles, :ram_kb, :drive1541, :init_threshold
|
|
65
|
+
:user_port, :datasette, :tias, :iec_bus, :cycles, :ram_kb, :drive1541, :init_threshold, :sid_card
|
|
50
66
|
|
|
51
67
|
def family = :plus4
|
|
52
68
|
|
|
@@ -57,8 +73,8 @@ module Badline
|
|
|
57
73
|
def ram = @bus.ram
|
|
58
74
|
|
|
59
75
|
# What a front end plays: the TED's sound, which answers #record and
|
|
60
|
-
# #drain_samples.
|
|
61
|
-
def sound_source = @sound
|
|
76
|
+
# #drain_samples, mixed with the SID card's where one is fitted.
|
|
77
|
+
def sound_source = @sound_mix || @sound
|
|
62
78
|
|
|
63
79
|
# The chip whose #display a front end shows.
|
|
64
80
|
def video = @ted
|
|
@@ -84,11 +100,16 @@ module Badline
|
|
|
84
100
|
# +model+ names one of Model::ALL, or c116 for the C16, and +ram+ the
|
|
85
101
|
# RAM in K, one of Model::RAM_SIZES, when not the model's own. +tcbm+
|
|
86
102
|
# plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF, with no
|
|
87
|
-
# drive answering on the other side.
|
|
88
|
-
|
|
103
|
+
# drive answering on the other side. +true_drive_model+ names the drive
|
|
104
|
+
# plug_true_drive plugs in, "1541" or "1551". +sid_card+ fits a SID
|
|
105
|
+
# card with a SID of that model, :mos6581 or :mos8580, at
|
|
106
|
+
# +sid_card_address+, $FD40 or $FE80. +ram_expansion+
|
|
107
|
+
# fits a RAMExpansion of that kind, :hannes256, :hannes1m or
|
|
108
|
+
# :csory256, over 64K of the machine's own.
|
|
109
|
+
def initialize(model: "plus4", ram: nil, tcbm: false, true_drive_model: "1551", sid_card: nil,
|
|
110
|
+
sid_card_address: SIDCard::ADDRESSES.first, ram_expansion: nil, debug: false)
|
|
89
111
|
@model = Model.named(model)
|
|
90
|
-
@ram_kb = ram
|
|
91
|
-
raise ArgumentError, "no Plus/4 RAM size of #{@ram_kb}K" unless Model::RAM_SIZES.include?(@ram_kb)
|
|
112
|
+
@ram_kb = fitted_ram(ram, ram_expansion)
|
|
92
113
|
|
|
93
114
|
@ted = Chips::TED.new(@model.ted)
|
|
94
115
|
@keyboard = Input::Keyboard.new(matrix: KeyboardPort::MATRIX)
|
|
@@ -100,39 +121,45 @@ module Badline
|
|
|
100
121
|
@datasette = Drives::Datasette.new(level: true)
|
|
101
122
|
@port = CPUPort.new(serial_bus: @iec_bus, datasette: @datasette)
|
|
102
123
|
plus4_parts = @model.plus4_parts
|
|
103
|
-
@
|
|
124
|
+
@cycles = 0
|
|
125
|
+
@init_threshold = INIT_THRESHOLD
|
|
126
|
+
@acia_irq = false
|
|
127
|
+
@acia_due = ACIA::NEVER
|
|
128
|
+
@due = INIT_THRESHOLD
|
|
129
|
+
@sid_card = sid_card ? SIDCard.new(self, sid_card, sid_card_address) : nil
|
|
130
|
+
@acia = plus4_parts ? ACIA.new(self, @model.ted.clock_hz) : nil
|
|
104
131
|
@user_port = plus4_parts ? UserPort.new(@datasette) : nil
|
|
105
132
|
io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port,
|
|
106
|
-
cassette_sense: plus4_parts ? nil : CassetteSense.new(@datasette)
|
|
107
|
-
|
|
133
|
+
cassette_sense: plus4_parts ? nil : CassetteSense.new(@datasette),
|
|
134
|
+
sid_card: sid_card_at(0xfd40))
|
|
135
|
+
@bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb, ram_expansion:)
|
|
108
136
|
@ted.connect(@bus)
|
|
137
|
+
@bus.plug_sid_card(sid_card_at(0xfe80))
|
|
109
138
|
@tias = tcbm ? [Chips::TIA.new, Chips::TIA.new] : []
|
|
110
139
|
@bus.plug_tcbm(drive8: @tias[0], drive9: @tias[1]) if tcbm
|
|
111
140
|
@cpu = CPU.new(@bus, debug:)
|
|
112
|
-
@cycles = 0
|
|
113
141
|
@sound = Chips::TED::Sound.new(self, @model.ted.clock_hz)
|
|
114
142
|
@ted.sound = @sound
|
|
143
|
+
@sound_mix = @sid_card ? SoundMix.new(@sound, @sid_card) : nil
|
|
115
144
|
@init_handlers = []
|
|
116
145
|
@pending_keys = nil
|
|
117
146
|
@dos = nil
|
|
118
147
|
@serial_trap = nil
|
|
119
148
|
@save_trap = nil
|
|
120
149
|
@capture_output = nil
|
|
121
|
-
|
|
122
|
-
@drive1581 = nil
|
|
123
|
-
@init_threshold = INIT_THRESHOLD
|
|
150
|
+
init_drives(true_drive_model)
|
|
124
151
|
end
|
|
125
152
|
|
|
126
153
|
# One TED cycle: the TED moves on and decides the CPU's clock, then the
|
|
127
154
|
# CPU runs its cycles in it, none while the TED holds the bus.
|
|
128
155
|
def cycle!
|
|
129
|
-
|
|
156
|
+
handle_due if @cycles >= @due
|
|
130
157
|
feed_keyboard if @pending_keys
|
|
131
158
|
|
|
132
159
|
ted = @ted
|
|
133
160
|
ted.cycle!
|
|
134
161
|
@datasette.cycle!
|
|
135
|
-
@cpu.irq = ted.irq?
|
|
162
|
+
@cpu.irq = ted.irq? || @acia_irq
|
|
136
163
|
slots = ted.cpu_slots
|
|
137
164
|
if slots.zero?
|
|
138
165
|
@cpu.stall!
|
|
@@ -142,13 +169,15 @@ module Badline
|
|
|
142
169
|
end
|
|
143
170
|
@drive1541&.host_cycle!
|
|
144
171
|
@drive1581&.host_cycle!
|
|
172
|
+
@drive1551&.host_cycle!
|
|
145
173
|
|
|
146
174
|
@cycles += 1
|
|
147
175
|
end
|
|
148
176
|
|
|
149
177
|
# The RESET line: the CPU, the port, the 6529s, the ACIA, the bank
|
|
150
|
-
# latch and ROM selection,
|
|
151
|
-
# drives. The TED has no
|
|
178
|
+
# latch and ROM selection, a RAM expansion's register, the SID card,
|
|
179
|
+
# and through the serial bus's RESET line, the drives. The TED has no
|
|
180
|
+
# reset pin.
|
|
152
181
|
def reset!
|
|
153
182
|
@port.reset!
|
|
154
183
|
@keyboard_port.reset!
|
|
@@ -156,7 +185,9 @@ module Badline
|
|
|
156
185
|
@dos&.reset!
|
|
157
186
|
@drive1541&.reset!
|
|
158
187
|
@drive1581&.reset!
|
|
188
|
+
@drive1551&.reset!
|
|
159
189
|
@acia&.reset!
|
|
190
|
+
@sid_card&.reset!
|
|
160
191
|
@tias.each(&:reset!)
|
|
161
192
|
@bus.reset!
|
|
162
193
|
@cpu.reset!
|
|
@@ -171,6 +202,13 @@ module Badline
|
|
|
171
202
|
reset!
|
|
172
203
|
end
|
|
173
204
|
|
|
205
|
+
# The ACIA's IRQ line, and the cycle it next has something to do at.
|
|
206
|
+
def acia_changed(irq, due)
|
|
207
|
+
@acia_irq = irq
|
|
208
|
+
@acia_due = due
|
|
209
|
+
schedule
|
|
210
|
+
end
|
|
211
|
+
|
|
174
212
|
# Calls the block with the exit code a VICE testprog writes to $FDCF.
|
|
175
213
|
def install_debug_register(&) = @bus.install_debug_register(&)
|
|
176
214
|
|
|
@@ -201,6 +239,18 @@ module Badline
|
|
|
201
239
|
"#{name} #{standard}, #{@ram_kb}K RAM"
|
|
202
240
|
end
|
|
203
241
|
|
|
242
|
+
# The RAMExpansion fitted, or nil.
|
|
243
|
+
def ram_expansion = @bus.ram_expansion
|
|
244
|
+
|
|
245
|
+
# The RAM expansion as the pause menu names it, such as "HANNES, 256
|
|
246
|
+
# KB", or nil without one.
|
|
247
|
+
def ram_expansion_label
|
|
248
|
+
expansion = @bus.ram_expansion
|
|
249
|
+
return if expansion.nil?
|
|
250
|
+
|
|
251
|
+
"#{RAMExpansion::NAMES.fetch(expansion.kind)}, #{expansion.size_kb} KB"
|
|
252
|
+
end
|
|
253
|
+
|
|
204
254
|
# Stores a PRG's bytes from its load address on, into RAM, and returns
|
|
205
255
|
# that address.
|
|
206
256
|
def load_prg(data)
|
|
@@ -213,6 +263,22 @@ module Badline
|
|
|
213
263
|
|
|
214
264
|
private
|
|
215
265
|
|
|
266
|
+
# The SID card if it is at +address+, or nil.
|
|
267
|
+
def sid_card_at(address)
|
|
268
|
+
card = @sid_card
|
|
269
|
+
card if card && card.address == address
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The RAM in K: +ram+, or the model's own, or 64K under a RAM
|
|
273
|
+
# expansion, which needs that much.
|
|
274
|
+
def fitted_ram(ram, ram_expansion)
|
|
275
|
+
ram_kb = ram || (ram_expansion ? 64 : @model.ram_kb)
|
|
276
|
+
raise ArgumentError, "no Plus/4 RAM size of #{ram_kb}K" unless Model::RAM_SIZES.include?(ram_kb)
|
|
277
|
+
raise ArgumentError, "a RAM expansion needs 64K, not #{ram_kb}K" if ram_expansion && ram_kb != 64
|
|
278
|
+
|
|
279
|
+
ram_kb
|
|
280
|
+
end
|
|
281
|
+
|
|
216
282
|
# The 264 KERNAL's keyboard buffer.
|
|
217
283
|
def keyboard_buffer = Machine::KeyboardBuffer::PLUS4_LAYOUT
|
|
218
284
|
|
|
@@ -232,6 +298,22 @@ module Badline
|
|
|
232
298
|
@init_threshold = [@init_threshold, Drives::Model1581::BOOT_CYCLES].max if @cycles < @init_threshold
|
|
233
299
|
end
|
|
234
300
|
|
|
301
|
+
# What falls due at the start of a cycle: the on_init handlers at the
|
|
302
|
+
# init threshold, and the ACIA's next character.
|
|
303
|
+
def handle_due
|
|
304
|
+
handle_init if @cycles == @init_threshold
|
|
305
|
+
acia = @acia
|
|
306
|
+
acia.run!(@cycles) if acia && @cycles >= @acia_due
|
|
307
|
+
schedule
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The next cycle anything falls due at. A drive that raises the init
|
|
311
|
+
# threshold leaves it early, and handle_due looks again then.
|
|
312
|
+
def schedule
|
|
313
|
+
init = @cycles < @init_threshold ? @init_threshold : ACIA::NEVER
|
|
314
|
+
@due = [@acia_due, init].min
|
|
315
|
+
end
|
|
316
|
+
|
|
235
317
|
# BA stops the CPU at a read, and a write goes ahead.
|
|
236
318
|
def clock_cpu(ted)
|
|
237
319
|
ted.ba_low? && !@cpu.pending_write? ? @cpu.stall! : @cpu.cycle!
|
data/lib/badline/roms/README
CHANGED
|
@@ -79,6 +79,15 @@ Downloaded from
|
|
|
79
79
|
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
|
|
80
80
|
SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
|
|
81
81
|
|
|
82
|
+
dos1551.rom
|
|
83
|
+
-----------
|
|
84
|
+
The 1551 disk drive's DOS 2.6, 318008-01, 16 KB for $C000-$FFFF.
|
|
85
|
+
Drives::Model1551 loads it.
|
|
86
|
+
|
|
87
|
+
Downloaded from
|
|
88
|
+
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1551/1551.318008-01.bin
|
|
89
|
+
SHA-1 fae3c788ad9a6cc2dbdfbcf6c0264b2ca921d55e.
|
|
90
|
+
|
|
82
91
|
Commodore VIC-20 ROMs
|
|
83
92
|
=====================
|
|
84
93
|
|
|
Binary file
|
|
@@ -37,7 +37,7 @@ module Badline
|
|
|
37
37
|
# The machines the doc is made from: each family at power-on, the C64
|
|
38
38
|
# with each kind of expansion, cartridge and media, and a second SID,
|
|
39
39
|
# and the 264 series' models, its TCBM interface, cartridge, tape and
|
|
40
|
-
# true
|
|
40
|
+
# true drives, the 1541 and the 1551.
|
|
41
41
|
def self.machines
|
|
42
42
|
{
|
|
43
43
|
"c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512),
|
|
@@ -45,8 +45,10 @@ module Badline
|
|
|
45
45
|
with_drive(:d81), with_extra_sids],
|
|
46
46
|
"vic20" => [Vic20.new, with_rom],
|
|
47
47
|
"c128" => [C128.new(mode: :c128), with_drive(:d71)],
|
|
48
|
-
"plus4" => [Plus4.new, Plus4.new(model: "c16"), Plus4.new(tcbm: true),
|
|
49
|
-
|
|
48
|
+
"plus4" => [Plus4.new, Plus4.new(model: "c16"), Plus4.new(tcbm: true), Plus4.new(sid_card: :mos6581),
|
|
49
|
+
Plus4.new(sid_card: :mos8580, sid_card_address: 0xfe80),
|
|
50
|
+
Plus4.new(ram_expansion: :csory256),
|
|
51
|
+
plus4_with_cartridge, plus4_with_tape, plus4_with_drive, plus4_with_drive("1551")]
|
|
50
52
|
}
|
|
51
53
|
end
|
|
52
54
|
|
|
@@ -110,8 +112,8 @@ module Badline
|
|
|
110
112
|
Plus4.new(model: "c16").tap { |machine| machine.datasette.insert(Storage::TAP.new("", bytes:)) }
|
|
111
113
|
end
|
|
112
114
|
|
|
113
|
-
def self.plus4_with_drive
|
|
114
|
-
Plus4.new.tap do |machine|
|
|
115
|
+
def self.plus4_with_drive(model = "1541")
|
|
116
|
+
Plus4.new(true_drive_model: model).tap do |machine|
|
|
115
117
|
image = Storage::D64Image.new("", bytes: blank(174_848))
|
|
116
118
|
machine.plug_true_drive.insert(Drives::Model1541::Disk.decode(image))
|
|
117
119
|
end
|