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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/CLAUDE.md +7 -0
  4. data/README.md +40 -10
  5. data/doc/media.md +19 -0
  6. data/doc/snapshot-format.md +61 -13
  7. data/exe/badline-ruby +1 -1
  8. data/lib/badline/audio/machine_player.rb +2 -14
  9. data/lib/badline/c64/true_drives.rb +13 -1
  10. data/lib/badline/c64.rb +6 -0
  11. data/lib/badline/chips/tia.rb +16 -0
  12. data/lib/badline/chips/vic/register_log.rb +4 -1
  13. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  14. data/lib/badline/drives/engine/core.rb +6 -14
  15. data/lib/badline/drives/engine/idle.rb +42 -4
  16. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  17. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  18. data/lib/badline/drives/engine/sleep.rb +1 -0
  19. data/lib/badline/drives/engine.rb +1 -0
  20. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  21. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  22. data/lib/badline/drives/model1541.rb +1 -0
  23. data/lib/badline/drives/model1551/bus.rb +138 -0
  24. data/lib/badline/drives/model1551/cable.rb +104 -0
  25. data/lib/badline/drives/model1551/cpu.rb +14 -0
  26. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  27. data/lib/badline/drives/model1551/port.rb +74 -0
  28. data/lib/badline/drives/model1551/tia.rb +92 -0
  29. data/lib/badline/drives/model1551.rb +170 -0
  30. data/lib/badline/drives/model1571.rb +1 -0
  31. data/lib/badline/drives/model1581.rb +1 -0
  32. data/lib/badline/frontend/boot.rb +20 -1
  33. data/lib/badline/frontend/menu/menu_dialogs.rb +13 -5
  34. data/lib/badline/frontend/menu/menu_media.rb +92 -14
  35. data/lib/badline/frontend/menu/menu_pages.rb +11 -6
  36. data/lib/badline/frontend/player/player_controller.rb +11 -4
  37. data/lib/badline/machine.rb +9 -5
  38. data/lib/badline/media/true_drive.rb +7 -5
  39. data/lib/badline/options/help.rb +4 -2
  40. data/lib/badline/options/option.rb +2 -1
  41. data/lib/badline/options/table.rb +14 -2
  42. data/lib/badline/options/validation.rb +49 -9
  43. data/lib/badline/options.rb +34 -2
  44. data/lib/badline/plus4/acia/frames.rb +55 -0
  45. data/lib/badline/plus4/acia.rb +276 -16
  46. data/lib/badline/plus4/bus.rb +181 -45
  47. data/lib/badline/plus4/keyed_modules.rb +22 -5
  48. data/lib/badline/plus4/keyed_parts.rb +33 -4
  49. data/lib/badline/plus4/keyed_state.rb +5 -2
  50. data/lib/badline/plus4/ram_expansion.rb +145 -0
  51. data/lib/badline/plus4/sid_card.rb +95 -0
  52. data/lib/badline/plus4/sound_mix.rb +51 -0
  53. data/lib/badline/plus4/tcbm_drive.rb +120 -0
  54. data/lib/badline/plus4/user_port.rb +11 -2
  55. data/lib/badline/plus4.rb +103 -21
  56. data/lib/badline/roms/README +9 -0
  57. data/lib/badline/roms/dos1551.rom +0 -0
  58. data/lib/badline/snapshot/field_doc.rb +7 -5
  59. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  60. data/lib/badline/storage/g64_image.rb +1 -1
  61. data/lib/badline/version.rb +1 -1
  62. 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) = @datasette.sense_low? ? @output & ~TAPE_SENSE : @output
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. Only the TED pulls IRQ, and nothing pulls NMI, which the 7501
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 (C64::TrueDrives), cartridges go
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
- def initialize(model: "plus4", ram: nil, tcbm: false, debug: false)
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 || @model.ram_kb
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
- @acia = plus4_parts ? ACIA.new : nil
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
- @bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb)
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
- @drive1541 = nil
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
- handle_init if @cycles == @init_threshold
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, and through the serial bus's RESET line, the
151
- # drives. The TED has no reset pin.
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!
@@ -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 drive.
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), plus4_with_cartridge,
49
- plus4_with_tape, plus4_with_drive]
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