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
@@ -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. There Escape is ESC,
115
- Page Down RUN/STOP and F4 HELP.
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 or :either), and the build that
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: :window),
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
- if @family == :plus4
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
- # The Plus/4 runs in the window, without the C64's SID and REU, its
104
- # NTSC models have names of their own, and its cartridge slots take
105
- # raw ROM images.
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")
@@ -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 or only the headless player takes.
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
@@ -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
- # as a stub with nothing on the line: it never receives, its
7
- # transmitter is always empty, and it never interrupts. The command and
8
- # control registers keep what is written to them, and a write to the
9
- # status register, a programmed reset, clears the command register's
10
- # low five bits.
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
- STATUS = 0x10
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
- attr_reader :command, :control
73
+ # Far enough ahead that it never comes.
74
+ NEVER = 1 << 60
15
75
 
16
- def initialize
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
- def irq? = false
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 1 then STATUS
154
+ when 0
155
+ @receiver_full = false
156
+ @receive_data
157
+ when 1 then read_status
30
158
  when 2 then @command
31
- when 3 then @control
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 1 then @command &= 0xe0
39
- when 2 then @command = value
40
- when 3 then @control = value
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