badline 0.15.0 → 0.16.0

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/pet/rom_sets"
4
+ require "badline/pet/model"
5
+ require "badline/pet/cpu"
6
+ require "badline/pet/via"
7
+ require "badline/pet/via_ports"
8
+ require "badline/pet/keyboard_ports"
9
+ require "badline/pet/ieee_ports"
10
+ require "badline/pet/ieee_traps"
11
+ require "badline/pet/cassettes"
12
+ require "badline/pet/characters"
13
+ require "badline/pet/canvas"
14
+ require "badline/pet/discrete_video"
15
+ require "badline/pet/crtc_video"
16
+ require "badline/pet/bus"
17
+ require "badline/pet/sound"
18
+ require "badline/snapshot/pet_setup"
19
+ require "badline/pet/keyed_parts"
20
+ require "badline/pet/keyed_state"
21
+ require "badline/pet/keyed_modules"
22
+
23
+ module Badline
24
+ # The Commodore PET and CBM: a 6502 at 1 MHz, two 6520 PIAs at $E810 and
25
+ # $E820, a 6522 VIA at $E840 and, from the 4032 on, a 6545 CRTC at
26
+ # $E880. Every chip's IRQ is wired to the CPU's, and nothing pulls NMI.
27
+ # Nothing halts the CPU either: the video reads the screen RAM between
28
+ # its accesses.
29
+ #
30
+ # The video's vertical drive line, low in the retrace, reaches PIA 1's
31
+ # CB1, whose falling edge is the editor's 50 or 60 Hz interrupt, and the
32
+ # VIA's PB5. The discrete boards count it out themselves
33
+ # (DiscreteVideo), and the CRTC boards take it from the CRTC's vertical
34
+ # sync, inverted.
35
+ #
36
+ # The key matrix hangs off PIA 1 (KeyboardPorts), the graphics one or
37
+ # the business one as the model has, and type_text types through the
38
+ # editor's keyboard buffer.
39
+ #
40
+ # The discrete boards paint their picture (DiscreteVideo) on a Canvas.
41
+ # The CRTC boards paint theirs from the CRTC's outputs (CRTCVideo).
42
+ # The VIA's CB2 is the sound (Sound).
43
+ #
44
+ # Device 8 answers through traps on the KERNAL's IEEE-488 primitives
45
+ # (IEEETraps), on a bus nothing else is plugged into (IEEEPorts), and
46
+ # each cassette port has a datasette (Cassettes). It runs headless.
47
+ class PET
48
+ include Machine::KeyboardBuffer
49
+ include Machine::Clocking
50
+ include Machine::Settling
51
+ include IEEETraps
52
+
53
+ attr_reader :model, :bus, :cpu, :pia1, :pia2, :via, :crtc, :keyboard, :ieee, :cassettes, :cycles, :init_threshold
54
+
55
+ # The editor's frame rate: 50 or 60 on a CRTC board, 60 on the others.
56
+ attr_reader :refresh_hz
57
+
58
+ # The RAM below $8000, in K.
59
+ attr_reader :ram_kb
60
+
61
+ def ram = @bus.ram
62
+
63
+ # Reads and writes memory as the CPU sees it, through the bus.
64
+ def peek(addr) = @bus.peek(addr)
65
+
66
+ def poke(addr, value) = @bus.poke(addr, value)
67
+
68
+ def install_debug_register(&) = @bus.install_debug_register(&)
69
+
70
+ def family = :pet
71
+
72
+ # The datasette on cassette port #1, where a tape goes in (Cassettes).
73
+ def datasette = @cassettes.datasette1
74
+
75
+ # +model+ names one of Model::ALL, with +ram_kb+ of RAM below $8000
76
+ # (Model::RAM_SIZES) in place of the model's own when not nil. A CRTC
77
+ # model runs its 50 Hz editor, or its 60 Hz one for +refresh_hz+ 60.
78
+ # +basic+ and +editor+ name ROMs the board takes in place of its own
79
+ # (Model.upgraded).
80
+ def initialize(model: Model::DEFAULT, ram_kb: nil, refresh_hz: 50, debug: false, basic: nil, editor: nil)
81
+ @model = Model.upgraded(Model.named(model), basic, editor)
82
+ ram_kb ||= @model.ram_kb
83
+ raise ArgumentError, "a PET has no #{ram_kb}K RAM board" unless Model::RAM_SIZES.include?(ram_kb)
84
+
85
+ @ram_kb = ram_kb
86
+
87
+ @refresh_hz = @model.crtc? ? refresh_hz : 60
88
+ @crtc_video = @model.crtc?
89
+ @vertical_drive = true
90
+ matrix = @model.keyboard == :business ? KeyboardPorts::BUSINESS_MATRIX : KeyboardPorts::GRAPHICS_MATRIX
91
+ @keyboard = Input::Keyboard.new(matrix:)
92
+ keyboard_ports = KeyboardPorts.new(@keyboard)
93
+ registers = Array.new(Chips::CRTC::MASKS.length, 0)
94
+ @pia1 = Chips::PIA.new(peripheral: keyboard_ports)
95
+ @ieee = IEEEPorts.new(Drives::IEEEBus.new)
96
+ @pia2 = Chips::PIA.new(peripheral: @ieee)
97
+ @via = VIA.new(start: 0xe800, peripheral: VIAPorts.new(self))
98
+ @sound = Sound.new(@via)
99
+ @cassettes = Cassettes.new(@pia1, @via)
100
+ keyboard_ports.connect(@pia1, @ieee, @cassettes)
101
+ characters = Characters.new(@model.characters)
102
+ @crtc_output = CRTCVideo.new(characters, @via, registers, columns80: @model.video == :crtc80)
103
+ @crtc = Chips::CRTC.new(@crtc_output, registers:)
104
+ @ieee.connect(@pia1, @pia2, @via)
105
+ @bus = Bus.new(model: @model, ram_kb:, editor: @model.editor(@refresh_hz), pia1: @pia1, pia2: @pia2,
106
+ via: @via, crtc: @crtc)
107
+ @crtc_output.cells = @bus.screen.cells
108
+ @discrete = DiscreteVideo.new(@bus.screen.cells, characters, @via)
109
+ @canvas = @crtc_video ? @crtc_output.canvas : @discrete.canvas
110
+ cassettes = @cassettes
111
+ @bus.on_io_write { cassettes.lines_written }
112
+ @cpu = CPU.new(@bus, debug:)
113
+ @cycles = 0
114
+ @init_handlers = []
115
+ @pending_keys = nil
116
+ @dos = nil
117
+ @ieee_trap = nil
118
+ @capture_output = nil
119
+ @init_threshold = boot_cycles(ram_kb)
120
+ end
121
+
122
+ # The vertical drive line, high but in the retrace.
123
+ def vertical_drive? = @vertical_drive
124
+
125
+ # The model, its RAM and its editor's frame rate as the pause menu
126
+ # names them, such as "8032, 32K RAM, 50 HZ".
127
+ def model_label = "#{@model.name.upcase}, #{@ram_kb}K RAM, #{@refresh_hz} HZ"
128
+
129
+ # The Canvas a front end shows.
130
+ def video = @canvas
131
+
132
+ # The 1 MHz clock and the frame the editor sets up, and all of the
133
+ # Canvas. The CRTC editors' lines and frames are their ROM's
134
+ # (ROMSets.crtc_frame), which a program can change. 80 columns show
135
+ # two dots to a window pixel.
136
+ def timing
137
+ crop = [0, 0, @canvas.width, @canvas.height]
138
+ if @crtc_video
139
+ cycles_per_line, lines_per_frame = ROMSets.crtc_frame(@model.editor(@refresh_hz), @refresh_hz)
140
+ Timing.new(clock_hz: 1_000_000, cycles_per_line:, lines_per_frame:,
141
+ crop:, pixel_width: 1, dots: @model.video == :crtc80 ? 2 : 1)
142
+ else
143
+ Timing.new(clock_hz: 1_000_000, cycles_per_line: DiscreteVideo::CYCLES_PER_LINE,
144
+ lines_per_frame: DiscreteVideo::LINES_PER_FRAME, crop:, pixel_width: 1, dots: 1)
145
+ end
146
+ end
147
+
148
+ # The video counts and moves the vertical drive line, then the PIAs
149
+ # and the VIA clock, ahead of the CPU.
150
+ def cycle!
151
+ handle_init if @cycles == @init_threshold
152
+ feed_keyboard if @pending_keys
153
+
154
+ if @crtc_video
155
+ @crtc.cycle!
156
+ drive_retrace(!@crtc.vsync?)
157
+ else
158
+ @discrete.cycle!
159
+ drive_retrace(@discrete.vertical_drive?)
160
+ end
161
+ @pia1.cycle!
162
+ @pia2.cycle!
163
+ @via.cycle!
164
+ @sound.cycle! if @sound.recording?
165
+ @cassettes.cycle!
166
+ @cpu.irq = @pia1.irq? || @pia2.irq? || @via.irq?
167
+ @cpu.cycle!
168
+
169
+ @cycles += 1
170
+ end
171
+
172
+ # What a front end plays: the VIA's CB2 (Sound), which answers #record
173
+ # and #drain_samples.
174
+ def sound_source = @sound
175
+
176
+ # The RES line reaches the CPU, the PIAs, the VIA, the CRTC and the
177
+ # 8096's control register.
178
+ def reset!
179
+ @pia1.reset!
180
+ @pia2.reset!
181
+ @via.reset!
182
+ @crtc.reset!
183
+ @bus.reset!
184
+ @cpu.reset!
185
+ end
186
+
187
+ # Switches the machine off and on: RAM starts cleared, and the RES
188
+ # line resets everything else.
189
+ def power_cycle!
190
+ @bus.power_on!
191
+ reset!
192
+ end
193
+
194
+ # Puts a program file's bytes where its first two say, through the bus,
195
+ # and returns that address.
196
+ def load_prg(data)
197
+ load_addr = data[0] | (data[1] << 8)
198
+ bytes = data[2..] || []
199
+ bytes = bytes[0, 0x10000 - load_addr] if load_addr + bytes.length > 0x10000
200
+ bytes.each_with_index { |byte, i| @bus.poke(load_addr + i, byte) }
201
+ load_addr
202
+ end
203
+
204
+ # Where the editor keeps its keyboard buffer: in page 2 under BASIC 1,
205
+ # in the zero page and page 2 under BASIC 2 and 4.
206
+ def keyboard_buffer
207
+ @model.basic1? ? Machine::KeyboardBuffer::PET_BASIC1_LAYOUT : Machine::KeyboardBuffer::PET_LAYOUT
208
+ end
209
+
210
+ private
211
+
212
+ # The cycle by which the KERNAL has booted and BASIC printed READY,
213
+ # with a margin. The RAM test takes most of it, about 41,000 to
214
+ # 44,000 cycles a K, and the CRTC editors take another 600,000 or so
215
+ # before it: READY is up by about 440,000 on a 2001 with 8K, 1,450,000
216
+ # on a 3032 and 1,980,000 on an 8032.
217
+ def boot_cycles(ram_kb) = (@model.basic == "pet/basic4.rom" ? 700_000 : 150_000) + (46_000 * ram_kb)
218
+
219
+ def drive_retrace(high)
220
+ return if high == @vertical_drive
221
+
222
+ @vertical_drive = high
223
+ @pia1.cb1 = high
224
+ end
225
+ end
226
+ end
@@ -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