badline 0.16.1 → 0.17.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/acia"
4
+ require "badline/chips/tpi"
5
+ require "badline/cbm2/model"
6
+ require "badline/cbm2/p500/model"
7
+ require "badline/cbm2/bus"
8
+ require "badline/cbm2/cpu"
9
+ require "badline/cbm2/acia"
10
+ require "badline/cbm2/keyboard_ports"
11
+ require "badline/cbm2/crtc_video"
12
+ require "badline/cbm2/sid_page"
13
+ require "badline/cbm2/ieee_layout"
14
+ require "badline/cbm2/ieee_traps"
15
+ require "badline/cbm2/ieee_ports"
16
+ require "badline/cbm2/ieee_drive"
17
+ require "badline/snapshot/cbm2_setup"
18
+ require "badline/cbm2/keyed_state"
19
+ require "badline/cbm2/keyed_modules"
20
+ require "badline/cbm2/p500"
21
+
22
+ module Badline
23
+ # The Commodore CBM-II's B series, the 610, 620, 710 and 720: a 6509 at
24
+ # 2 MHz on a bus of sixteen 64K segments (Bus, CPU), a 6845 CRTC at
25
+ # $D800, a 6581 SID at $DA00, a 6526 CIA at $DC00, a 6551 ACIA at $DD00
26
+ # and two 6525 TPIs at $DE00 and $DF00. Nothing halts the CPU: the video
27
+ # reads its own screen RAM between the CPU's accesses.
28
+ #
29
+ # TPI1 is the interrupt controller, and its IRQ the CPU's only one. Its
30
+ # inputs are the mains on I0, the IEEE-488 bus's SRQ on I1, the CIA on
31
+ # I2, the coprocessor board on I3 and the ACIA on I4, each pulled low to
32
+ # interrupt. The mains, 50 or 60 Hz, is the system tick, and feeds the
33
+ # CIA's TOD pin too. TPI2 scans the key matrix and reads the model's
34
+ # jumpers (KeyboardPorts), and type_text types through the editor's
35
+ # keyboard buffer in segment 15.
36
+ #
37
+ # The DRAM's refresh takes the CPU's clock away for a cycle (Refresh),
38
+ # every 20 cycles or so: about 4.5% of its time.
39
+ #
40
+ # The CRTC paints its picture from the screen RAM and the character ROM
41
+ # (CRTCVideo). Device 8 answers through traps on the KERNAL's IEEE-488
42
+ # primitives (IEEETraps), or a true 2031, 8050 or 8250 on the bus
43
+ # (IEEEPorts, IEEEDrive) answers. It runs headless.
44
+ class CBM2
45
+ include Machine::KeyboardBuffer
46
+ include Machine::Clocking
47
+ include Machine::Settling
48
+ include IEEETraps
49
+ include IEEEDrive
50
+
51
+ CLOCK_HZ = 2_000_000
52
+
53
+ # The DRAM refresh (sheet 3 of schematic 8256043, U89 and U55): a
54
+ # counter of the CPU's cycles asks for a refresh once it reaches 20,
55
+ # stops there, and the refresh comes on the next opcode fetch (SYNC),
56
+ # which it holds off a cycle with RDY while it stops the CPU's clock,
57
+ # and clears the counter.
58
+ REFRESH_COUNT = 20
59
+
60
+ # The segment BASIC keeps its program text in.
61
+ TEXT_SEGMENT = 1
62
+
63
+ # The editor's keyboard buffer in segment 15, KEYD at $03AB and NDX at
64
+ # $D1, the same in 901244-03b and -04a.
65
+ KEYBOARD_LAYOUT = Machine::KeyboardBuffer::Layout.new(address: 0x03ab, count: 0xd1, capacity: 10)
66
+
67
+ attr_reader :model, :bus, :cpu, :crtc, :sid, :cia, :acia, :tpi1, :tpi2, :keyboard, :ieee, :mains_hz, :kernal,
68
+ :cycles, :init_threshold
69
+
70
+ def ram = @bus.ram
71
+
72
+ # Whether the DRAM refresh takes its cycles from the CPU.
73
+ def refresh? = @refreshing
74
+
75
+ def family = :cbm2
76
+
77
+ # Reads and writes memory as the CPU sees it, in its execution segment.
78
+ def peek(addr) = @bus.peek(addr)
79
+
80
+ def poke(addr, value) = @bus.poke(addr, value)
81
+
82
+ def install_debug_register(&) = @bus.install_debug_register(&)
83
+
84
+ def keyboard_buffer = KEYBOARD_LAYOUT
85
+
86
+ def sound_source = @sid
87
+
88
+ # The Canvas a front end shows (CRTCVideo).
89
+ def video = @video.canvas
90
+
91
+ # What paints the picture.
92
+ def crtc_video = @video
93
+
94
+ # +model+ names one of Model::ALL, +kernal+ one of Model::KERNALS, and
95
+ # +mains_hz+ the mains, 50 or 60, which also sets a 600's jumper for
96
+ # the editor's 60 Hz CRTC table. +refresh+ false leaves out the DRAM
97
+ # refresh's stolen cycles.
98
+ def initialize(model: Model::DEFAULT, kernal: "04a", mains_hz: 50, refresh: true, debug: false)
99
+ @model = Model.named(model)
100
+ raise ArgumentError, "no CBM-II KERNAL named #{kernal}" unless Model::KERNALS.key?(kernal)
101
+ raise ArgumentError, "the mains is 50 or 60 Hz, not #{mains_hz}" unless [50, 60].include?(mains_hz)
102
+
103
+ @kernal = kernal
104
+ @mains_hz = mains_hz
105
+ @bus = Bus.new(basic: Model.basic(@model, kernal), kernal: Model::KERNALS.fetch(kernal),
106
+ ram_segments: @model.ram_segments)
107
+ @ieee = IEEEPorts.new(Drives::IEEEBus.new)
108
+ @tpi1 = Chips::TPI.new(peripheral: @ieee)
109
+ registers = Array.new(Chips::CRTC::MASKS.length, 0)
110
+ @video = CRTCVideo.new(ROM.read(@model.characters), @bus.screen, @tpi1, registers,
111
+ high_profile: @model.high_profile)
112
+ @crtc = Chips::CRTC.new(@video, registers:)
113
+ @sid = Chips::SID.new(model: :mos6581, at: 0xda00)
114
+ @cia = Chips::CIA.new(start: 0xdc00, peripheral: @ieee.data, region: Region::PAL.with(clock_hz: CLOCK_HZ),
115
+ tod_hz: mains_hz)
116
+ @ieee.connect(@cia, @tpi1)
117
+ @acia = ACIA.new(self, CLOCK_HZ)
118
+ @keyboard = Input::Keyboard.new(matrix: KeyboardPorts::MATRIX)
119
+ keyboard_ports = KeyboardPorts.new(@keyboard, jumpers)
120
+ @tpi2 = Chips::TPI.new(peripheral: keyboard_ports)
121
+ keyboard_ports.connect(@tpi2)
122
+ map_io
123
+ @cpu = CPU.new(@bus, debug:)
124
+ @cycles = 0
125
+ @mains_half_period = CLOCK_HZ / (2 * mains_hz)
126
+ @mains_countdown = @mains_half_period
127
+ @mains_high = true
128
+ @refresh = 0
129
+ @refreshing = refresh
130
+ @acia_irq = false
131
+ @acia_due = Chips::ACIA::NEVER
132
+ @init_handlers = []
133
+ @pending_keys = nil
134
+ @dos = nil
135
+ @ieee_trap = nil
136
+ @capture_output = nil
137
+ init_ieee_drives
138
+ @init_threshold = boot_cycles
139
+ end
140
+
141
+ # The model, its RAM and its mains as the pause menu names them, such
142
+ # as "610, 128K RAM, 50 HZ".
143
+ def model_label = "#{@model.name}, #{@model.ram_kb}K RAM, #{@mains_hz} HZ"
144
+
145
+ # The 2 MHz clock and the frame of the editor's CRTC table, nominal,
146
+ # since a program can program the CRTC otherwise, and all of the
147
+ # Canvas, two dots to a window pixel.
148
+ def timing
149
+ cycles_per_line, lines_per_frame = crtc_frame
150
+ canvas = @video.canvas
151
+ Timing.new(clock_hz: CLOCK_HZ, cycles_per_line:, lines_per_frame:, crop: [0, 0, canvas.width, canvas.height],
152
+ pixel_width: 1, dots: 2)
153
+ end
154
+
155
+ # The CRTC and the mains ahead of the chips, then TPI1's inputs from
156
+ # the CIA and the ACIA, and the CPU last.
157
+ def cycle!
158
+ handle_init if @cycles == @init_threshold
159
+ feed_keyboard if @pending_keys
160
+
161
+ @crtc.cycle!
162
+ tick_mains
163
+ @cia.cycle!
164
+ @sid.cycle!
165
+ @acia.run!(@cycles) if @cycles >= @acia_due
166
+ tpi1 = @tpi1
167
+ tpi1.interrupt_line(2, !@cia.interrupted?)
168
+ tpi1.interrupt_line(4, !@acia_irq)
169
+ @cpu.irq = tpi1.irq?
170
+ if @refreshing && @refresh >= REFRESH_COUNT && @cpu.boundary?
171
+ @refresh = 0
172
+ @cpu.stall!
173
+ else
174
+ @refresh += 1 if @refresh < REFRESH_COUNT
175
+ @cpu.cycle!
176
+ end
177
+ clock_ieee_drives
178
+
179
+ @cycles += 1
180
+ end
181
+
182
+ # The ACIA's IRQ line, and the cycle it next has something to do at.
183
+ def acia_changed(irq, due)
184
+ @acia_irq = irq
185
+ @acia_due = due
186
+ end
187
+
188
+ # The RES line reaches the CPU and its segment registers, the CRTC, the
189
+ # SID, the CIA, the ACIA and both TPIs.
190
+ def reset!
191
+ @crtc.reset!
192
+ @sid.reset!
193
+ @cia.reset!
194
+ @acia.reset!
195
+ @tpi1.reset!
196
+ @tpi2.reset!
197
+ @bus.reset!
198
+ @cpu.reset!
199
+ reset_ieee_drives
200
+ end
201
+
202
+ # Puts a program file's bytes where its first two say in BASIC's text
203
+ # segment, segment 1, and returns that address.
204
+ def load_prg(data)
205
+ load_addr = data[0] | (data[1] << 8)
206
+ bytes = data[2..] || []
207
+ bytes = bytes[0, 0x10000 - load_addr] if load_addr + bytes.length > 0x10000
208
+ bytes.each_with_index { |byte, i| @bus.poke_bank(TEXT_SEGMENT, load_addr + i, byte) }
209
+ load_addr
210
+ end
211
+
212
+ # Puts a cartridge's ROM chips in (Bus#attach_cartridge) and switches
213
+ # the machine off and on, for the KERNAL to find it as it starts.
214
+ def attach_cartridge(chips)
215
+ @bus.attach_cartridge(chips)
216
+ power_cycle!
217
+ end
218
+
219
+ # Switches the machine off and on: RAM starts cleared, and the RES
220
+ # line resets everything else.
221
+ def power_cycle!
222
+ @bus.power_on!
223
+ reset!
224
+ end
225
+
226
+ private
227
+
228
+ # PC6 and PC7 as the board's jumpers drive them.
229
+ def jumpers
230
+ return 0x80 if @model.high_profile
231
+
232
+ @mains_hz == 60 ? 0x40 : 0x00
233
+ end
234
+
235
+ def map_io
236
+ @bus.map_io(0xd8, @crtc)
237
+ @bus.map_io(0xda, SIDPage.new(@sid))
238
+ @bus.map_io(0xdc, @cia)
239
+ @bus.map_io(0xdd, @acia)
240
+ @bus.map_io(0xde, @tpi1)
241
+ @bus.map_io(0xdf, @tpi2)
242
+ end
243
+
244
+ # The mains, low for half its period and high for the other half, on
245
+ # TPI1's I0, whose falling edge latches the tick.
246
+ def tick_mains
247
+ @mains_countdown -= 1
248
+ return unless @mains_countdown.zero?
249
+
250
+ @mains_countdown = @mains_half_period
251
+ @mains_high = !@mains_high
252
+ @tpi1.interrupt_line(0, @mains_high)
253
+ end
254
+
255
+ # The characters a line and the lines a frame of the editor's table:
256
+ # the 700's, or the 600's 50 or 60 Hz one.
257
+ def crtc_frame
258
+ return [108, 367] if @model.high_profile
259
+
260
+ @mains_hz == 60 ? [127, 262] : [128, 313]
261
+ end
262
+
263
+ # The cycle by which the KERNAL has tested the RAM board, about 3.7
264
+ # million cycles a segment with the refresh, and BASIC printed READY, with a margin.
265
+ def boot_cycles = 1_000_000 + (3_800_000 * @model.ram_segments.length)
266
+ end
267
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- class Plus4
4
+ module Chips
5
5
  class ACIA
6
6
  # A character's frame and how long it takes. A start bit, 5 to 8
7
7
  # data bits, a parity bit where the command enables one, and the stop
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class ACIA
6
+ # The registers, the status, the shift register and when the
7
+ # transmitter's character ends, in a keyed snapshot module (BL-ACIA).
8
+ # What the device on the line has still to send stays with the
9
+ # host, and the receiver starts on it again once loaded (#restored!).
10
+ # A snapshot from before the transmitter's timing was saved starts
11
+ # its character clock on load.
12
+ module KeyedState
13
+ def save_fields(out)
14
+ out.int("command", @command).int("control", @control).int("errors", @errors)
15
+ out.bool("receiver_full", @receiver_full).bool("transmitter_empty", @transmitter_empty).bool("irq", @irq)
16
+ out.int("receive_data", @receive_data).int("transmit_data", @transmit_data).int("shifting", @shifting)
17
+ out.int("transmit_at", @transmit_at).int("transmit_remainder", @transmit_remainder)
18
+ end
19
+
20
+ def load_fields(input)
21
+ @command = input.byte("command", @command)
22
+ @control = input.byte("control", @control)
23
+ @errors = input.masked("errors", 0, PARITY_ERROR | FRAMING_ERROR | OVERRUN)
24
+ @receiver_full = input.bool("receiver_full", false)
25
+ @transmitter_empty = input.bool("transmitter_empty", true)
26
+ @irq = input.bool("irq", false)
27
+ @receive_data = input.byte("receive_data", 0)
28
+ @transmit_data = input.byte("transmit_data", 0)
29
+ @shifting = input.int("shifting", IDLE)
30
+ @transmit_remainder = input.int("transmit_remainder", 0)
31
+ transmit_at = input.optional_int("transmit_at")
32
+ @transmit_at = transmit_at || NEVER
33
+ @clock_lost = transmit_at.nil?
34
+ end
35
+
36
+ def restored!
37
+ @receive_at = NEVER
38
+ @lines = modem_lines
39
+ start_transmitting if @clock_lost && transmitter_on?
40
+ @clock_lost = false
41
+ start_receiving
42
+ changed
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/acia/frames"
4
+ require "badline/chips/acia/keyed_state"
5
+
6
+ module Badline
7
+ module Chips
8
+ # The 6551 ACIA, its four registers repeated through the page it is
9
+ # decoded in:
10
+ #
11
+ # 0 - read the receiver's data, write the transmitter's
12
+ # 1 - read the status, write a programmed reset
13
+ # 2 - the command register
14
+ # 3 - the control register
15
+ #
16
+ # Status: bit 0 a parity error, 1 a framing error, 2 an overrun, 3 the
17
+ # receiver full, 4 the transmitter empty, 5 DCD and 6 DSR as their pins
18
+ # read (1 high, not asserted), 7 an interrupt. Reading it clears bit 7.
19
+ #
20
+ # Command: bit 0 DTR, which also enables the receiver and every
21
+ # interrupt, bit 1 set disables the receiver's interrupt, bits 2-3 the
22
+ # transmitter (00 off with RTS high, 01 on with its interrupt, 10 on
23
+ # without, 11 sending a break), bit 4 echoes what is received, bits 5-7
24
+ # the parity.
25
+ #
26
+ # Control: bits 0-3 the baud rate from the 1.8432 MHz crystal (0, the
27
+ # 16x external clock, runs straight from the crystal, 115,200 baud),
28
+ # bit 4 the receiver's clock (0 the RxC pin, which nothing drives, so
29
+ # the receiver hears nothing), bits 5-6 the word length (8 down to 5
30
+ # bits) and bit 7 the stop bits (Frames).
31
+ #
32
+ # The transmitter runs in whole characters while it is on: at the end
33
+ # of each it hands the character it shifted out to the line, moves a
34
+ # waiting byte from the data register into the shift register and sets
35
+ # the transmitter empty, and with its interrupt on interrupts while the
36
+ # register is empty, every character until a byte comes. The first
37
+ # character ends a character's time after the transmitter comes on.
38
+ # The receiver takes a character's time for each byte the line sends,
39
+ # and a byte that arrives while the last is unread is lost to an
40
+ # overrun. The receiver full, and DCD or DSR changing, interrupt unless
41
+ # bit 1 disables it. CTS is held asserted, so the transmitter is always
42
+ # clear to send.
43
+ #
44
+ # What is on the other end of the line (#plug): nothing, where DCD and
45
+ # DSR read high and what is sent goes nowhere; a terminal, which
46
+ # asserts DCD and DSR, sends what the host feeds it (#feed) while DTR is
47
+ # asserted and keeps what it receives for the host to drain (#drain);
48
+ # or a loopback plug, which returns TxD to RxD and DTR to DSR and DCD.
49
+ #
50
+ # The machine runs the ACIA at the cycle it next has something to do
51
+ # (#due) through #run!, and learns its IRQ line and that cycle from
52
+ # `clock`'s #acia_changed, whose #cycles is the time, counted at
53
+ # `clock_hz`. Each machine has a subclass of its own, so `clock` keeps
54
+ # one type in each.
55
+ class ACIA
56
+ include Frames
57
+ include KeyedState
58
+
59
+ PARITY_ERROR = 0x01
60
+ FRAMING_ERROR = 0x02
61
+ OVERRUN = 0x04
62
+ RECEIVER_FULL = 0x08
63
+ TRANSMITTER_EMPTY = 0x10
64
+ DCD = 0x20
65
+ DSR = 0x40
66
+ IRQ = 0x80
67
+
68
+ NOTHING = 0
69
+ TERMINAL = 1
70
+ LOOPBACK = 2
71
+
72
+ # No shift in progress.
73
+ IDLE = -1
74
+
75
+ # Far enough ahead that it never comes.
76
+ NEVER = 1 << 60
77
+
78
+ attr_reader :command, :control, :device, :due
79
+
80
+ def initialize(clock, clock_hz)
81
+ @clock = clock
82
+ @clock_hz = clock_hz
83
+ @device = NOTHING
84
+ @incoming = []
85
+ @received = []
86
+ reset!
87
+ end
88
+
89
+ # The RES line: every register cleared, the transmitter empty and
90
+ # both shift registers stopped.
91
+ def reset!
92
+ @command = 0
93
+ @control = 0
94
+ @errors = 0
95
+ @receiver_full = false
96
+ @transmitter_empty = true
97
+ @irq = false
98
+ @receive_data = 0
99
+ @transmit_data = 0
100
+ @shifting = IDLE
101
+ @transmit_at = NEVER
102
+ @transmit_remainder = 0
103
+ @receive_at = NEVER
104
+ @receive_remainder = 0
105
+ @clock_lost = false
106
+ @lines = modem_lines
107
+ changed
108
+ end
109
+
110
+ def irq? = @irq
111
+
112
+ # DTR and RTS as asserted, low on the pins.
113
+ def dtr? = @command.anybits?(0x01)
114
+
115
+ def rts? = @command.anybits?(0x0c)
116
+
117
+ # Whether the transmitter sends a break, holding TxD low.
118
+ def break? = @command & 0x0c == 0x0c
119
+
120
+ # Plugs NOTHING, a TERMINAL or a LOOPBACK plug into the user port.
121
+ def plug(device)
122
+ @device = device
123
+ @incoming.clear
124
+ lines_changed
125
+ start_receiving
126
+ changed
127
+ end
128
+
129
+ # Bytes the terminal sends, one a character's time while DTR is
130
+ # asserted.
131
+ def feed(bytes)
132
+ return unless @device == TERMINAL
133
+
134
+ @incoming.concat(bytes)
135
+ start_receiving
136
+ changed
137
+ end
138
+
139
+ # What the terminal has received since the last drain.
140
+ def drain
141
+ received = @received
142
+ @received = []
143
+ received
144
+ end
145
+
146
+ # Runs what falls due up to cycle `now`: the transmitter's character
147
+ # ends, then the receiver's.
148
+ def run!(now)
149
+ end_character while @transmit_at <= now
150
+ receive_character while @receive_at <= now
151
+ changed
152
+ end
153
+
154
+ def peek(addr)
155
+ case addr & 0x03
156
+ when 0
157
+ @receiver_full = false
158
+ @receive_data
159
+ when 1 then read_status
160
+ when 2 then @command
161
+ else @control
162
+ end
163
+ end
164
+
165
+ def poke(addr, value)
166
+ case addr & 0x03
167
+ when 0 then write_data(value)
168
+ when 1 then write_command(@command & 0xe0, clear_overrun: true)
169
+ when 2 then write_command(value)
170
+ else @control = value
171
+ end
172
+ changed
173
+ end
174
+
175
+ # The status as a read finds it, without clearing the interrupt.
176
+ def status
177
+ status = @errors | (@lines & (DCD | DSR))
178
+ status |= RECEIVER_FULL if @receiver_full
179
+ status |= TRANSMITTER_EMPTY if @transmitter_empty
180
+ @irq ? status | IRQ : status
181
+ end
182
+
183
+ private
184
+
185
+ def read_status
186
+ value = status
187
+ @irq = false
188
+ changed
189
+ value
190
+ end
191
+
192
+ def write_data(value)
193
+ @transmit_data = value
194
+ @transmitter_empty = false
195
+ end
196
+
197
+ def write_command(value, clear_overrun: false)
198
+ was_on = transmitter_on?
199
+ @command = value
200
+ @errors &= ~OVERRUN if clear_overrun
201
+ start_transmitting if transmitter_on? && !was_on
202
+ stop_transmitting unless transmitter_on? || @shifting != IDLE
203
+ lines_changed
204
+ start_receiving
205
+ end
206
+
207
+ def transmitter_on?
208
+ control = @command & 0x0c
209
+ control != 0 && control != 0x0c
210
+ end
211
+
212
+ def interrupts? = @command.anybits?(0x01)
213
+
214
+ def receiver_interrupts? = @command & 0x03 == 0x01
215
+
216
+ def transmitter_interrupts? = @command & 0x0d == 0x05
217
+
218
+ def receiving? = dtr? && @control.anybits?(0x10)
219
+
220
+ def start_transmitting
221
+ @transmit_remainder = 0
222
+ @transmit_at = after(@clock.cycles, true)
223
+ end
224
+
225
+ def stop_transmitting
226
+ @transmit_at = NEVER
227
+ end
228
+
229
+ # A character's end: the character shifted out goes to the line, and
230
+ # the transmitter takes the next byte or interrupts while it has none.
231
+ def end_character
232
+ sent = @shifting
233
+ @shifting = IDLE
234
+ deliver(sent) unless sent == IDLE
235
+ on = transmitter_on?
236
+ if on && !@transmitter_empty
237
+ @shifting = @transmit_data & word_mask
238
+ @transmitter_empty = true
239
+ end
240
+ @irq = true if on && @transmitter_empty && transmitter_interrupts?
241
+ if on || @shifting != IDLE
242
+ @transmit_at = after(@transmit_at, true)
243
+ else
244
+ stop_transmitting
245
+ end
246
+ end
247
+
248
+ def deliver(byte)
249
+ case @device
250
+ when TERMINAL then @received << byte
251
+ when LOOPBACK then take(byte) if receiving?
252
+ end
253
+ end
254
+
255
+ def start_receiving
256
+ return unless @receive_at == NEVER && !@incoming.empty? && receiving?
257
+
258
+ @receive_remainder = 0
259
+ @receive_at = after(@clock.cycles, false)
260
+ end
261
+
262
+ def receive_character
263
+ byte = @incoming[0]
264
+ @incoming.delete_at(0)
265
+ take(byte)
266
+ @receive_at = !@incoming.empty? && receiving? ? after(@receive_at, false) : NEVER
267
+ end
268
+
269
+ # A whole character in from RxD: into the data register, or lost to
270
+ # an overrun while the last is unread. In echo mode it goes back out
271
+ # to the terminal.
272
+ def take(byte)
273
+ if @receiver_full
274
+ @errors |= OVERRUN
275
+ else
276
+ @receive_data = byte & word_mask
277
+ @receiver_full = true
278
+ @errors = 0
279
+ end
280
+ @received << byte if @device == TERMINAL && @command & 0x1c == 0x10
281
+ @irq = true if receiver_interrupts?
282
+ end
283
+
284
+ # DCD and DSR as the device drives them, interrupting on a change.
285
+ def lines_changed
286
+ lines = modem_lines
287
+ return if lines == @lines
288
+
289
+ @lines = lines
290
+ @irq = true if receiver_interrupts?
291
+ end
292
+
293
+ def modem_lines
294
+ case @device
295
+ when TERMINAL then 0
296
+ when LOOPBACK then dtr? ? 0 : DCD | DSR
297
+ else DCD | DSR
298
+ end
299
+ end
300
+
301
+ def changed
302
+ @due = [@transmit_at, @receive_at].min
303
+ @clock.acia_changed(@irq && interrupts?, @due)
304
+ end
305
+ end
306
+ end
307
+ end
@@ -23,7 +23,8 @@ module Badline
23
23
  # row ends after the line where it equals R9; past R9 it runs on to
24
24
  # 31 and wraps.
25
25
  # - The row counter (seven bits) counts rows to R4. R5 more lines of
26
- # vertical adjust follow, then the frame starts again. The display
26
+ # vertical adjust follow, then the frame starts again. The line
27
+ # counter counts them, and they end no row, past R9 or not. The display
27
28
  # stops at the start of the row the counter equals R6, and comes back
28
29
  # at the next frame; vertical sync starts with row R7 and lasts
29
30
  # VSYNC_LINES lines.
@@ -91,7 +92,7 @@ module Badline
91
92
  column = @column
92
93
  if column == regs[1]
93
94
  @column_display = false
94
- @row_start = @address if @line == regs[9]
95
+ @row_start = @address if @line == regs[9] && !@adjusting
95
96
  end
96
97
  @output.character(@address, @line, @since_hsync) if @column_display && @row_display
97
98
  if column == regs[2]