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
data/lib/badline/options/help.rb
CHANGED
|
@@ -111,8 +111,10 @@ module Badline
|
|
|
111
111
|
The Plus/4 and the C16 take programs, disks, tapes, and cartridges as
|
|
112
112
|
.crt files or raw .bin ROM images. A .bin goes in cartridge 1, its
|
|
113
113
|
low slot for up to 16K and both slots for 32K, and --c1lo, --c1hi,
|
|
114
|
-
--c2lo and --c2hi put one in the slot they name.
|
|
115
|
-
|
|
114
|
+
--c2lo and --c2hi put one in the slot they name. Their --true-drive
|
|
115
|
+
makes device 8 an emulated 1551, on the parallel TCBM cable, and
|
|
116
|
+
--drive-type 1541 a 1541 on the serial bus instead. There Escape is
|
|
117
|
+
ESC, Page Down RUN/STOP and F4 HELP.
|
|
116
118
|
|
|
117
119
|
--headless plays a .sid tune on the host's audio device without the
|
|
118
120
|
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
@@ -4,7 +4,8 @@ module Badline
|
|
|
4
4
|
class Options
|
|
5
5
|
# One option of the table: its flags and value as the help shows them
|
|
6
6
|
# (`-s, --subtune N`), what it does, the help section it's listed in, the
|
|
7
|
-
# mode it needs (:window, :headless
|
|
7
|
+
# mode it needs (:window, :headless, :either, or :any_window for the
|
|
8
|
+
# emulator's window or the SID player's), and the build that
|
|
8
9
|
# takes it (:native, :ruby or :both).
|
|
9
10
|
class Option
|
|
10
11
|
attr_reader :usage, :name, :short, :value, :text, :section, :needs, :build
|
|
@@ -18,6 +18,9 @@ module Badline
|
|
|
18
18
|
# another name for the C16.
|
|
19
19
|
PLUS4_MODELS = %w[plus4 plus4ntsc c16 c16ntsc c116].freeze
|
|
20
20
|
|
|
21
|
+
# The true drives --drive-type names for the Plus/4.
|
|
22
|
+
PLUS4_DRIVE_TYPES = %w[1541 1551].freeze
|
|
23
|
+
|
|
21
24
|
# The VIC-20 RAM expansions --ram takes, in the order the help lists
|
|
22
25
|
# them.
|
|
23
26
|
RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
|
|
@@ -39,7 +42,10 @@ module Badline
|
|
|
39
42
|
Option.new("--sound", "Play the SID through the host's audio device (F10 mutes)",
|
|
40
43
|
section: "Window options", needs: :window),
|
|
41
44
|
Option.new("--no-sound", "Don't play the SID", section: "Window options", needs: :window, build: :native),
|
|
42
|
-
Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps, a 1571 on the C128"
|
|
45
|
+
Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps, a 1571 on the C128, " \
|
|
46
|
+
"a 1551 on the Plus/4",
|
|
47
|
+
section: "Window options", needs: :window),
|
|
48
|
+
Option.new("--drive-type MODEL", "The Plus/4's true drive: 1541 or 1551 (default: 1551); implies --true-drive",
|
|
43
49
|
section: "Window options", needs: :window),
|
|
44
50
|
Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
|
|
45
51
|
section: "Window options", needs: :window),
|
|
@@ -60,6 +66,12 @@ module Badline
|
|
|
60
66
|
section: "Window options", needs: :window),
|
|
61
67
|
Option.new("--c2hi FILE", "Put a .bin ROM image in cartridge 2's high slot ($C000)",
|
|
62
68
|
section: "Window options", needs: :window),
|
|
69
|
+
Option.new("--sid-card MODEL", "Fit the Plus/4 or C16 with a SID card at $FD40: 6581 or 8580",
|
|
70
|
+
section: "Window options", needs: :window),
|
|
71
|
+
Option.new("--sid-card-address ADDRESS", "Put the SID card at fd40 (the default) or fe80",
|
|
72
|
+
section: "Window options", needs: :window),
|
|
73
|
+
Option.new("--ram-expansion NAME", "Fit the Plus/4 or C16 with a RAM expansion: hannes256, hannes1m or csory256",
|
|
74
|
+
section: "Window options", needs: :window),
|
|
63
75
|
Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
|
|
64
76
|
section: "Window options", needs: :window),
|
|
65
77
|
Option.new("--verbose", "Print the display, sound and gamepad setup and the frame timing",
|
|
@@ -81,7 +93,7 @@ module Badline
|
|
|
81
93
|
section: "Options without the window", needs: :headless),
|
|
82
94
|
Option.new("--no-tui", "Play without the interactive display, even on a terminal",
|
|
83
95
|
section: "Options without the window", needs: :headless),
|
|
84
|
-
Option.new("--frames N", "Quit after N frames", section: "Testing options", needs: :
|
|
96
|
+
Option.new("--frames N", "Quit after N frames", section: "Testing options", needs: :any_window),
|
|
85
97
|
Option.new("--unpaced", "Run as fast as it can, without vsync or pacing",
|
|
86
98
|
section: "Testing options", needs: :window),
|
|
87
99
|
Option.new("--screenshot FILE", "Save the last frame as a .bmp",
|
|
@@ -8,6 +8,18 @@ module Badline
|
|
|
8
8
|
module Validation
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
def sid_card_for(value)
|
|
12
|
+
raise Error, "invalid argument: --sid-card #{value}" unless SID_MODELS.key?(value)
|
|
13
|
+
|
|
14
|
+
SID_MODELS[value]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sid_card_address_for(value)
|
|
18
|
+
raise Error, "invalid argument: --sid-card-address #{value}" unless SID_CARD_ADDRESSES.key?(value.downcase)
|
|
19
|
+
|
|
20
|
+
SID_CARD_ADDRESSES[value.downcase]
|
|
21
|
+
end
|
|
22
|
+
|
|
11
23
|
def sid_model_for(value)
|
|
12
24
|
return if value == "auto"
|
|
13
25
|
raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
|
|
@@ -27,6 +39,18 @@ module Badline
|
|
|
27
39
|
PLUS4_RAM[value]
|
|
28
40
|
end
|
|
29
41
|
|
|
42
|
+
def plus4_ram_expansion(value)
|
|
43
|
+
raise Error, "invalid argument: --ram-expansion #{value}" unless PLUS4_RAM_EXPANSIONS.key?(value)
|
|
44
|
+
|
|
45
|
+
PLUS4_RAM_EXPANSIONS[value]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def drive_type_for(value)
|
|
49
|
+
raise Error, "invalid argument: --drive-type #{value}" unless PLUS4_DRIVE_TYPES.include?(value)
|
|
50
|
+
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
30
54
|
def reu_size(value)
|
|
31
55
|
raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
|
|
32
56
|
|
|
@@ -47,6 +71,8 @@ module Badline
|
|
|
47
71
|
|
|
48
72
|
def validate
|
|
49
73
|
validate_family
|
|
74
|
+
raise Error, "--drive-type needs plus4" unless @drive_type.nil? || @family == :plus4
|
|
75
|
+
|
|
50
76
|
validate_mode
|
|
51
77
|
validate_models
|
|
52
78
|
validate_numbers
|
|
@@ -56,6 +82,7 @@ module Badline
|
|
|
56
82
|
def validate_mode
|
|
57
83
|
if headless?
|
|
58
84
|
raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
|
|
85
|
+
raise Error, "#{@any_window_only.first} needs the window" unless player_window? || @any_window_only.empty?
|
|
59
86
|
raise Error, "no tune given" if tune_paths.empty? && !player_window?
|
|
60
87
|
|
|
61
88
|
tune_paths.each { |path| validate_tune(path) }
|
|
@@ -70,11 +97,7 @@ module Badline
|
|
|
70
97
|
elsif !@ram.nil?
|
|
71
98
|
raise Error, "--ram needs vic20 or plus4"
|
|
72
99
|
end
|
|
73
|
-
|
|
74
|
-
validate_plus4
|
|
75
|
-
elsif !@cartridges.empty?
|
|
76
|
-
raise Error, "--#{@cartridges.keys.first} needs plus4"
|
|
77
|
-
end
|
|
100
|
+
@family == :plus4 ? validate_plus4 : refuse_plus4_options
|
|
78
101
|
if @family == :c128
|
|
79
102
|
validate_c128
|
|
80
103
|
elsif @c64_mode
|
|
@@ -100,19 +123,36 @@ module Badline
|
|
|
100
123
|
raise Error, "the VIC-20 needs the window" if headless?
|
|
101
124
|
end
|
|
102
125
|
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
|
|
126
|
+
# Only the Plus/4 has cartridge slots for raw ROM images and takes a
|
|
127
|
+
# SID card and a RAM expansion.
|
|
128
|
+
def refuse_plus4_options
|
|
129
|
+
raise Error, "--#{@cartridges.keys.first} needs plus4" unless @cartridges.empty?
|
|
130
|
+
raise Error, "--sid-card needs plus4" unless @sid_card.nil?
|
|
131
|
+
raise Error, "--sid-card-address needs plus4" unless @sid_card_address.nil?
|
|
132
|
+
raise Error, "--ram-expansion needs plus4" unless @ram_expansion.nil?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The Plus/4 runs in the window, without the C64's SID and REU but
|
|
136
|
+
# with a SID card if asked, its NTSC models have names of their own,
|
|
137
|
+
# and its cartridge slots take raw ROM images.
|
|
106
138
|
def validate_plus4
|
|
107
139
|
raise Error, "--ntsc needs the C64; the NTSC Plus/4 is --model plus4ntsc" if @models.include?("ntsc")
|
|
108
|
-
raise Error, "--sid needs the C64" unless @sid_model.nil?
|
|
140
|
+
raise Error, "--sid needs the C64; the Plus/4's is --sid-card" unless @sid_model.nil?
|
|
109
141
|
raise Error, "--reu needs the C64" unless @reu.nil?
|
|
110
142
|
raise Error, "the Plus/4 needs the window" if headless?
|
|
111
143
|
|
|
144
|
+
validate_plus4_expansions
|
|
112
145
|
image = @cartridges.values.find { |path| !File.extname(path).casecmp?(".bin") }
|
|
113
146
|
raise Error, "not a .bin ROM image: #{image}" unless image.nil?
|
|
114
147
|
end
|
|
115
148
|
|
|
149
|
+
# A RAM expansion goes over 64K, and a SID card's address needs a
|
|
150
|
+
# card.
|
|
151
|
+
def validate_plus4_expansions
|
|
152
|
+
raise Error, "--ram-expansion needs 64k" unless @ram_expansion.nil? || [nil, 64].include?(@ram_kb)
|
|
153
|
+
raise Error, "--sid-card-address needs --sid-card" unless @sid_card_address.nil? || @sid_card
|
|
154
|
+
end
|
|
155
|
+
|
|
116
156
|
# `sid` takes directories as well as tunes.
|
|
117
157
|
def validate_tune(path)
|
|
118
158
|
return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
|
data/lib/badline/options.rb
CHANGED
|
@@ -51,6 +51,12 @@ module Badline
|
|
|
51
51
|
# The Plus/4's RAM sizes as --ram names them, in K.
|
|
52
52
|
PLUS4_RAM = { "16k" => 16, "32k" => 32, "64k" => 64 }.freeze
|
|
53
53
|
|
|
54
|
+
# Where --sid-card-address puts the Plus/4's SID card.
|
|
55
|
+
SID_CARD_ADDRESSES = { "fd40" => 0xfd40, "fe80" => 0xfe80 }.freeze
|
|
56
|
+
|
|
57
|
+
# The Plus/4's RAM expansions as --ram-expansion names them.
|
|
58
|
+
PLUS4_RAM_EXPANSIONS = { "hannes256" => :hannes256, "hannes1m" => :hannes1m, "csory256" => :csory256 }.freeze
|
|
59
|
+
|
|
54
60
|
# The Plus/4's cartridge slots, by the options that fill them and
|
|
55
61
|
# xplus4's names (Media::Plus4Media::SLOTS).
|
|
56
62
|
CARTRIDGE_SLOTS = { "--c1lo" => "c1lo", "--c1hi" => "c1hi", "--c2lo" => "c2lo", "--c2hi" => "c2hi" }.freeze
|
|
@@ -80,6 +86,18 @@ module Badline
|
|
|
80
86
|
# name, by slot: { "c1lo" => path }.
|
|
81
87
|
attr_reader :cartridges
|
|
82
88
|
|
|
89
|
+
# The SID model of the Plus/4's SID card --sid-card names, :mos6581 or
|
|
90
|
+
# :mos8580, or nil without one.
|
|
91
|
+
attr_reader :sid_card
|
|
92
|
+
|
|
93
|
+
# Where --sid-card-address puts the Plus/4's SID card, $FD40 without
|
|
94
|
+
# it.
|
|
95
|
+
attr_reader :sid_card_address
|
|
96
|
+
|
|
97
|
+
# The Plus/4's RAM expansion --ram-expansion names, a key of
|
|
98
|
+
# Plus4::RAMExpansion::KINDS, or nil without one.
|
|
99
|
+
attr_reader :ram_expansion
|
|
100
|
+
|
|
83
101
|
def self.parse(argv, native: false) = new(native:).parse(argv)
|
|
84
102
|
|
|
85
103
|
# The native build plays the SID in the window unless --no-sound says
|
|
@@ -96,6 +114,9 @@ module Badline
|
|
|
96
114
|
@tune_paths = []
|
|
97
115
|
@subtune = nil
|
|
98
116
|
@sid_model = nil
|
|
117
|
+
@sid_card = nil
|
|
118
|
+
@sid_card_address = nil
|
|
119
|
+
@ram_expansion = nil
|
|
99
120
|
@models = []
|
|
100
121
|
@reu = nil
|
|
101
122
|
@ram = nil
|
|
@@ -109,6 +130,7 @@ module Badline
|
|
|
109
130
|
@vsync = true
|
|
110
131
|
@verbose = false
|
|
111
132
|
@true_drive = false
|
|
133
|
+
@drive_type = nil
|
|
112
134
|
@screenshot = ""
|
|
113
135
|
@save_snapshot = ""
|
|
114
136
|
@vice = false
|
|
@@ -126,6 +148,7 @@ module Badline
|
|
|
126
148
|
@tui = true
|
|
127
149
|
@all_subtunes = false
|
|
128
150
|
@window_only = []
|
|
151
|
+
@any_window_only = []
|
|
129
152
|
@headless_only = []
|
|
130
153
|
end
|
|
131
154
|
|
|
@@ -187,7 +210,10 @@ module Badline
|
|
|
187
210
|
|
|
188
211
|
def verbose? = @verbose
|
|
189
212
|
|
|
190
|
-
def true_drive? = @true_drive
|
|
213
|
+
def true_drive? = @true_drive || !@drive_type.nil?
|
|
214
|
+
|
|
215
|
+
# The Plus/4's true drive --drive-type names, "1551" without it.
|
|
216
|
+
def drive_type = @drive_type || "1551"
|
|
191
217
|
|
|
192
218
|
# Whether --save-snapshot adds VICE's modules, so x64sc opens it.
|
|
193
219
|
def vice? = @vice
|
|
@@ -254,9 +280,11 @@ module Badline
|
|
|
254
280
|
option
|
|
255
281
|
end
|
|
256
282
|
|
|
257
|
-
# Notes an option only the window
|
|
283
|
+
# Notes an option only the window, only a window of either kind, or
|
|
284
|
+
# only the headless player takes.
|
|
258
285
|
def restrict(option, flag)
|
|
259
286
|
@window_only << flag if option.needs == :window
|
|
287
|
+
@any_window_only << flag if option.needs == :any_window
|
|
260
288
|
@headless_only << flag if option.needs == :headless
|
|
261
289
|
end
|
|
262
290
|
|
|
@@ -270,9 +298,13 @@ module Badline
|
|
|
270
298
|
case name
|
|
271
299
|
when "--subtune" then @subtune = number(flag, value)
|
|
272
300
|
when "--sid" then @sid_model = sid_model_for(value)
|
|
301
|
+
when "--sid-card" then @sid_card = sid_card_for(value)
|
|
302
|
+
when "--sid-card-address" then @sid_card_address = sid_card_address_for(value)
|
|
303
|
+
when "--ram-expansion" then @ram_expansion = plus4_ram_expansion(value)
|
|
273
304
|
when "--model" then @models << value
|
|
274
305
|
when "--reu" then @reu = reu_size(value)
|
|
275
306
|
when "--ram" then ram_option(value)
|
|
307
|
+
when "--drive-type" then @drive_type = drive_type_for(value)
|
|
276
308
|
when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
|
|
277
309
|
when "--frames" then @frames = number(flag, value)
|
|
278
310
|
when "--screenshot" then @screenshot = value
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Plus4
|
|
5
|
+
class ACIA
|
|
6
|
+
# A character's frame and how long it takes. A start bit, 5 to 8
|
|
7
|
+
# data bits, a parity bit where the command enables one, and the stop
|
|
8
|
+
# bits: one, or with control bit 7 two, but one and a half for 5 bits
|
|
9
|
+
# without parity and one for 8 bits with it (6551 datasheet). Each bit
|
|
10
|
+
# takes 16 periods of the baud rate generator's 16x clock, the 1.8432
|
|
11
|
+
# MHz crystal divided by the rate's divisor, which the machine's
|
|
12
|
+
# cycles are counted against, carrying what is left of a cycle from
|
|
13
|
+
# one character to the next.
|
|
14
|
+
module Frames
|
|
15
|
+
CRYSTAL_HZ = 1_843_200
|
|
16
|
+
|
|
17
|
+
# The crystal's divisor for each baud rate the control register
|
|
18
|
+
# selects: the external clock, then 50, 75, 109.92, 134.58, 150,
|
|
19
|
+
# 300, 600, 1200, 1800, 2400, 3600, 4800, 7200, 9600 and 19,200.
|
|
20
|
+
DIVISORS = [1, 2304, 1536, 1048, 856, 768, 384, 192, 96, 64, 48, 32, 24, 16, 12, 6].freeze
|
|
21
|
+
|
|
22
|
+
def word_length = 8 - ((@control >> 5) & 0x03)
|
|
23
|
+
|
|
24
|
+
def word_mask = (1 << word_length) - 1
|
|
25
|
+
|
|
26
|
+
# The frame's length in half bits.
|
|
27
|
+
def frame_halves
|
|
28
|
+
length = word_length
|
|
29
|
+
parity = @command.anybits?(0x20)
|
|
30
|
+
stops = if @control < 0x80 || (length == 8 && parity) then 2
|
|
31
|
+
elsif length == 5 && !parity then 3
|
|
32
|
+
else 4
|
|
33
|
+
end
|
|
34
|
+
((1 + length + (parity ? 1 : 0)) * 2) + stops
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# The cycle a character the transmitter, or else the receiver,
|
|
40
|
+
# starts at `time` ends at.
|
|
41
|
+
def after(time, transmitter)
|
|
42
|
+
units = frame_halves * 8 * DIVISORS[@control & 0x0f] * @clock_hz
|
|
43
|
+
if transmitter
|
|
44
|
+
units += @transmit_remainder
|
|
45
|
+
@transmit_remainder = units % CRYSTAL_HZ
|
|
46
|
+
else
|
|
47
|
+
units += @receive_remainder
|
|
48
|
+
@receive_remainder = units % CRYSTAL_HZ
|
|
49
|
+
end
|
|
50
|
+
time + (units / CRYSTAL_HZ)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/badline/plus4/acia.rb
CHANGED
|
@@ -1,44 +1,304 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/plus4/acia/frames"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
class Plus4
|
|
5
|
-
# The Plus/4's 6551 ACIA at $FD00-$FD0F, its four registers repeated
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# The Plus/4's 6551 ACIA at $FD00-$FD0F, its four registers repeated:
|
|
8
|
+
#
|
|
9
|
+
# $FD00 - read the receiver's data, write the transmitter's
|
|
10
|
+
# $FD01 - read the status, write a programmed reset
|
|
11
|
+
# $FD02 - the command register
|
|
12
|
+
# $FD03 - the control register
|
|
13
|
+
#
|
|
14
|
+
# Status: bit 0 a parity error, 1 a framing error, 2 an overrun, 3 the
|
|
15
|
+
# receiver full, 4 the transmitter empty, 5 DCD and 6 DSR as their pins
|
|
16
|
+
# read (1 high, not asserted), 7 an interrupt. Reading it clears bit 7.
|
|
17
|
+
#
|
|
18
|
+
# Command: bit 0 DTR, which also enables the receiver and every
|
|
19
|
+
# interrupt, bit 1 set disables the receiver's interrupt, bits 2-3 the
|
|
20
|
+
# transmitter (00 off with RTS high, 01 on with its interrupt, 10 on
|
|
21
|
+
# without, 11 sending a break), bit 4 echoes what is received, bits 5-7
|
|
22
|
+
# the parity.
|
|
23
|
+
#
|
|
24
|
+
# Control: bits 0-3 the baud rate from the 1.8432 MHz crystal (0, the
|
|
25
|
+
# 16x external clock, runs straight from the crystal, 115,200 baud),
|
|
26
|
+
# bit 4 the receiver's clock (0 the RxC pin, which nothing drives on the
|
|
27
|
+
# Plus/4, so the receiver hears nothing), bits 5-6 the word length (8
|
|
28
|
+
# down to 5 bits) and bit 7 the stop bits (Frames).
|
|
29
|
+
#
|
|
30
|
+
# The transmitter runs in whole characters while it is on: at the end
|
|
31
|
+
# of each it hands the character it shifted out to the line, moves a
|
|
32
|
+
# waiting byte from the data register into the shift register and sets
|
|
33
|
+
# the transmitter empty, and with its interrupt on interrupts while the
|
|
34
|
+
# register is empty, every character until a byte comes. The first
|
|
35
|
+
# character ends a character's time after the transmitter comes on.
|
|
36
|
+
# The receiver takes a character's time for each byte the line sends,
|
|
37
|
+
# and a byte that arrives while the last is unread is lost to an
|
|
38
|
+
# overrun. The receiver full, and DCD or DSR changing, interrupt unless
|
|
39
|
+
# bit 1 disables it.
|
|
40
|
+
#
|
|
41
|
+
# The user port carries no CTS, so the transmitter is always clear to
|
|
42
|
+
# send; the KERNAL takes the user port's 6529 P1 as its handshake.
|
|
43
|
+
#
|
|
44
|
+
# What is on the other end of the user port (#plug): nothing, where
|
|
45
|
+
# DCD and DSR read high and what is sent goes nowhere; a terminal,
|
|
46
|
+
# which asserts DCD and DSR, sends what the host feeds it (#feed) while
|
|
47
|
+
# DTR is asserted and keeps what it receives for the host to drain
|
|
48
|
+
# (#drain); or a loopback plug, which returns TxD to RxD and DTR to DSR
|
|
49
|
+
# and DCD.
|
|
50
|
+
#
|
|
51
|
+
# The machine runs the ACIA at the cycle it next has something to do
|
|
52
|
+
# (#due) through #run!, and learns its IRQ line and that cycle from
|
|
53
|
+
# `clock`'s #acia_changed, whose #cycles is the time.
|
|
11
54
|
class ACIA
|
|
12
|
-
|
|
55
|
+
include Frames
|
|
56
|
+
|
|
57
|
+
PARITY_ERROR = 0x01
|
|
58
|
+
FRAMING_ERROR = 0x02
|
|
59
|
+
OVERRUN = 0x04
|
|
60
|
+
RECEIVER_FULL = 0x08
|
|
61
|
+
TRANSMITTER_EMPTY = 0x10
|
|
62
|
+
DCD = 0x20
|
|
63
|
+
DSR = 0x40
|
|
64
|
+
IRQ = 0x80
|
|
65
|
+
|
|
66
|
+
NOTHING = 0
|
|
67
|
+
TERMINAL = 1
|
|
68
|
+
LOOPBACK = 2
|
|
69
|
+
|
|
70
|
+
# No shift in progress.
|
|
71
|
+
IDLE = -1
|
|
13
72
|
|
|
14
|
-
|
|
73
|
+
# Far enough ahead that it never comes.
|
|
74
|
+
NEVER = 1 << 60
|
|
15
75
|
|
|
16
|
-
|
|
76
|
+
attr_reader :command, :control, :device, :due
|
|
77
|
+
|
|
78
|
+
def initialize(clock, clock_hz)
|
|
79
|
+
@clock = clock
|
|
80
|
+
@clock_hz = clock_hz
|
|
81
|
+
@device = NOTHING
|
|
82
|
+
@incoming = []
|
|
83
|
+
@received = []
|
|
17
84
|
reset!
|
|
18
85
|
end
|
|
19
86
|
|
|
87
|
+
# The RES line: every register cleared, the transmitter empty and
|
|
88
|
+
# both shift registers stopped.
|
|
20
89
|
def reset!
|
|
21
90
|
@command = 0
|
|
22
91
|
@control = 0
|
|
92
|
+
@errors = 0
|
|
93
|
+
@receiver_full = false
|
|
94
|
+
@transmitter_empty = true
|
|
95
|
+
@irq = false
|
|
96
|
+
@receive_data = 0
|
|
97
|
+
@transmit_data = 0
|
|
98
|
+
@shifting = IDLE
|
|
99
|
+
@transmit_at = NEVER
|
|
100
|
+
@transmit_remainder = 0
|
|
101
|
+
@receive_at = NEVER
|
|
102
|
+
@receive_remainder = 0
|
|
103
|
+
@clock_lost = false
|
|
104
|
+
@lines = modem_lines
|
|
105
|
+
changed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def irq? = @irq
|
|
109
|
+
|
|
110
|
+
# DTR and RTS as asserted, low on the pins.
|
|
111
|
+
def dtr? = @command.anybits?(0x01)
|
|
112
|
+
|
|
113
|
+
def rts? = @command.anybits?(0x0c)
|
|
114
|
+
|
|
115
|
+
# Whether the transmitter sends a break, holding TxD low.
|
|
116
|
+
def break? = @command & 0x0c == 0x0c
|
|
117
|
+
|
|
118
|
+
# Plugs NOTHING, a TERMINAL or a LOOPBACK plug into the user port.
|
|
119
|
+
def plug(device)
|
|
120
|
+
@device = device
|
|
121
|
+
@incoming.clear
|
|
122
|
+
lines_changed
|
|
123
|
+
start_receiving
|
|
124
|
+
changed
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Bytes the terminal sends, one a character's time while DTR is
|
|
128
|
+
# asserted.
|
|
129
|
+
def feed(bytes)
|
|
130
|
+
return unless @device == TERMINAL
|
|
131
|
+
|
|
132
|
+
@incoming.concat(bytes)
|
|
133
|
+
start_receiving
|
|
134
|
+
changed
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# What the terminal has received since the last drain.
|
|
138
|
+
def drain
|
|
139
|
+
received = @received
|
|
140
|
+
@received = []
|
|
141
|
+
received
|
|
23
142
|
end
|
|
24
143
|
|
|
25
|
-
|
|
144
|
+
# Runs what falls due up to cycle `now`: the transmitter's character
|
|
145
|
+
# ends, then the receiver's.
|
|
146
|
+
def run!(now)
|
|
147
|
+
end_character while @transmit_at <= now
|
|
148
|
+
receive_character while @receive_at <= now
|
|
149
|
+
changed
|
|
150
|
+
end
|
|
26
151
|
|
|
27
152
|
def peek(addr)
|
|
28
153
|
case addr & 0x03
|
|
29
|
-
when
|
|
154
|
+
when 0
|
|
155
|
+
@receiver_full = false
|
|
156
|
+
@receive_data
|
|
157
|
+
when 1 then read_status
|
|
30
158
|
when 2 then @command
|
|
31
|
-
|
|
32
|
-
else 0
|
|
159
|
+
else @control
|
|
33
160
|
end
|
|
34
161
|
end
|
|
35
162
|
|
|
36
163
|
def poke(addr, value)
|
|
37
164
|
case addr & 0x03
|
|
38
|
-
when
|
|
39
|
-
when
|
|
40
|
-
when
|
|
165
|
+
when 0 then write_data(value)
|
|
166
|
+
when 1 then write_command(@command & 0xe0, clear_overrun: true)
|
|
167
|
+
when 2 then write_command(value)
|
|
168
|
+
else @control = value
|
|
41
169
|
end
|
|
170
|
+
changed
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The status as a read finds it, without clearing the interrupt.
|
|
174
|
+
def status
|
|
175
|
+
status = @errors | (@lines & (DCD | DSR))
|
|
176
|
+
status |= RECEIVER_FULL if @receiver_full
|
|
177
|
+
status |= TRANSMITTER_EMPTY if @transmitter_empty
|
|
178
|
+
@irq ? status | IRQ : status
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def read_status
|
|
184
|
+
value = status
|
|
185
|
+
@irq = false
|
|
186
|
+
changed
|
|
187
|
+
value
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def write_data(value)
|
|
191
|
+
@transmit_data = value
|
|
192
|
+
@transmitter_empty = false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def write_command(value, clear_overrun: false)
|
|
196
|
+
was_on = transmitter_on?
|
|
197
|
+
@command = value
|
|
198
|
+
@errors &= ~OVERRUN if clear_overrun
|
|
199
|
+
start_transmitting if transmitter_on? && !was_on
|
|
200
|
+
stop_transmitting unless transmitter_on? || @shifting != IDLE
|
|
201
|
+
lines_changed
|
|
202
|
+
start_receiving
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def transmitter_on?
|
|
206
|
+
control = @command & 0x0c
|
|
207
|
+
control != 0 && control != 0x0c
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def interrupts? = @command.anybits?(0x01)
|
|
211
|
+
|
|
212
|
+
def receiver_interrupts? = @command & 0x03 == 0x01
|
|
213
|
+
|
|
214
|
+
def transmitter_interrupts? = @command & 0x0d == 0x05
|
|
215
|
+
|
|
216
|
+
def receiving? = dtr? && @control.anybits?(0x10)
|
|
217
|
+
|
|
218
|
+
def start_transmitting
|
|
219
|
+
@transmit_remainder = 0
|
|
220
|
+
@transmit_at = after(@clock.cycles, true)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def stop_transmitting
|
|
224
|
+
@transmit_at = NEVER
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# A character's end: the character shifted out goes to the line, and
|
|
228
|
+
# the transmitter takes the next byte or interrupts while it has none.
|
|
229
|
+
def end_character
|
|
230
|
+
sent = @shifting
|
|
231
|
+
@shifting = IDLE
|
|
232
|
+
deliver(sent) unless sent == IDLE
|
|
233
|
+
on = transmitter_on?
|
|
234
|
+
if on && !@transmitter_empty
|
|
235
|
+
@shifting = @transmit_data & word_mask
|
|
236
|
+
@transmitter_empty = true
|
|
237
|
+
end
|
|
238
|
+
@irq = true if on && @transmitter_empty && transmitter_interrupts?
|
|
239
|
+
if on || @shifting != IDLE
|
|
240
|
+
@transmit_at = after(@transmit_at, true)
|
|
241
|
+
else
|
|
242
|
+
stop_transmitting
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def deliver(byte)
|
|
247
|
+
case @device
|
|
248
|
+
when TERMINAL then @received << byte
|
|
249
|
+
when LOOPBACK then take(byte) if receiving?
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def start_receiving
|
|
254
|
+
return unless @receive_at == NEVER && !@incoming.empty? && receiving?
|
|
255
|
+
|
|
256
|
+
@receive_remainder = 0
|
|
257
|
+
@receive_at = after(@clock.cycles, false)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def receive_character
|
|
261
|
+
byte = @incoming[0]
|
|
262
|
+
@incoming.delete_at(0)
|
|
263
|
+
take(byte)
|
|
264
|
+
@receive_at = !@incoming.empty? && receiving? ? after(@receive_at, false) : NEVER
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A whole character in from RxD: into the data register, or lost to
|
|
268
|
+
# an overrun while the last is unread. In echo mode it goes back out
|
|
269
|
+
# to the terminal.
|
|
270
|
+
def take(byte)
|
|
271
|
+
if @receiver_full
|
|
272
|
+
@errors |= OVERRUN
|
|
273
|
+
else
|
|
274
|
+
@receive_data = byte & word_mask
|
|
275
|
+
@receiver_full = true
|
|
276
|
+
@errors = 0
|
|
277
|
+
end
|
|
278
|
+
@received << byte if @device == TERMINAL && @command & 0x1c == 0x10
|
|
279
|
+
@irq = true if receiver_interrupts?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# DCD and DSR as the device drives them, interrupting on a change.
|
|
283
|
+
def lines_changed
|
|
284
|
+
lines = modem_lines
|
|
285
|
+
return if lines == @lines
|
|
286
|
+
|
|
287
|
+
@lines = lines
|
|
288
|
+
@irq = true if receiver_interrupts?
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def modem_lines
|
|
292
|
+
case @device
|
|
293
|
+
when TERMINAL then 0
|
|
294
|
+
when LOOPBACK then dtr? ? 0 : DCD | DSR
|
|
295
|
+
else DCD | DSR
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def changed
|
|
300
|
+
@due = [@transmit_at, @receive_at].min
|
|
301
|
+
@clock.acia_changed(@irq && interrupts?, @due)
|
|
42
302
|
end
|
|
43
303
|
end
|
|
44
304
|
end
|