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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -0
- data/CLAUDE.md +15 -7
- data/README.md +142 -16
- data/doc/media.md +19 -0
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +274 -31
- data/doc/snapshots.md +14 -0
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/machine_player.rb +2 -14
- data/lib/badline/c64/common.rb +1 -1
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/crtc/keyed_state.rb +46 -0
- data/lib/badline/chips/crtc.rb +163 -0
- data/lib/badline/chips/pia/control_lines.rb +138 -0
- data/lib/badline/chips/pia/keyed_parts.rb +56 -0
- data/lib/badline/chips/pia.rb +138 -0
- data/lib/badline/chips/tia.rb +16 -0
- data/lib/badline/core.rb +3 -0
- 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/ieee_bus.rb +28 -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 +107 -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 +71 -0
- data/lib/badline/drives/model1551.rb +192 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/drives.rb +1 -0
- data/lib/badline/frontend/app.rb +1 -1
- data/lib/badline/frontend/boot.rb +20 -1
- data/lib/badline/frontend/controls.rb +23 -3
- data/lib/badline/frontend/gamepads.rb +10 -3
- data/lib/badline/frontend/keys.rb +88 -0
- data/lib/badline/frontend/media_slots.rb +1 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
- data/lib/badline/frontend/menu/menu_media.rb +96 -19
- data/lib/badline/frontend/menu/menu_pages.rb +25 -7
- data/lib/badline/frontend/menu/menu_ports.rb +22 -1
- data/lib/badline/frontend/menu/pause_menu.rb +1 -0
- data/lib/badline/frontend/pet_keyboard.rb +124 -0
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/timeline.rb +16 -9
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/dos/writes.rb +3 -1
- data/lib/badline/kernal_trap/dos.rb +6 -1
- data/lib/badline/kernal_trap/frames.rb +117 -0
- data/lib/badline/kernal_trap/ieee.rb +148 -0
- data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
- data/lib/badline/kernal_trap/keyed_state.rb +4 -4
- data/lib/badline/kernal_trap/routine.rb +3 -53
- data/lib/badline/kernal_trap/serial.rb +4 -102
- data/lib/badline/kernal_trap/steps.rb +64 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/machine/keyboard_buffer.rb +8 -0
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +37 -11
- data/lib/badline/media/extensions.rb +5 -2
- data/lib/badline/media/pet_media.rb +40 -0
- data/lib/badline/media/program.rb +14 -2
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/media/vic20_media.rb +2 -2
- data/lib/badline/media.rb +36 -16
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +23 -5
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +28 -4
- data/lib/badline/options/validation.rb +117 -14
- data/lib/badline/options.rb +65 -21
- data/lib/badline/pet/bus/expansion.rb +106 -0
- data/lib/badline/pet/bus.rb +204 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +26 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +142 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_ports.rb +66 -0
- data/lib/badline/pet/ieee_traps.rb +75 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +85 -0
- data/lib/badline/pet/keyed_parts.rb +123 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +113 -0
- data/lib/badline/pet/rom_sets.rb +65 -0
- data/lib/badline/pet/sound.rb +128 -0
- data/lib/badline/pet/via.rb +10 -0
- data/lib/badline/pet/via_ports.rb +19 -0
- data/lib/badline/pet.rb +226 -0
- 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 +10 -5
- 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 +134 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +104 -22
- data/lib/badline/roms/README +146 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/roms/pet/basic1.rom +0 -0
- data/lib/badline/roms/pet/basic2.rom +0 -0
- data/lib/badline/roms/pet/basic4.rom +0 -0
- data/lib/badline/roms/pet/characters1.rom +0 -0
- data/lib/badline/roms/pet/characters2.rom +0 -0
- data/lib/badline/roms/pet/editor1.rom +0 -0
- data/lib/badline/roms/pet/editor2-b.rom +0 -0
- data/lib/badline/roms/pet/editor2-n.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-b.rom +0 -0
- data/lib/badline/roms/pet/editor4-n.rom +0 -0
- data/lib/badline/roms/pet/kernal1.rom +0 -0
- data/lib/badline/roms/pet/kernal2.rom +0 -0
- data/lib/badline/roms/pet/kernal4.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +21 -6
- data/lib/badline/snapshot/image.rb +14 -7
- data/lib/badline/snapshot/pet_setup.rb +71 -0
- data/lib/badline/snapshot/plus4_setup.rb +52 -10
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- data/lib/badline/snapshot.rb +9 -5
- data/lib/badline/storage/d80_image.rb +48 -0
- data/lib/badline/storage/d82_image.rb +21 -0
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/storage/tap.rb +16 -3
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/support/pet_checkpoint.rb +38 -0
- data/lib/badline/timing.rb +5 -4
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/attachments.rb +7 -2
- data/lib/badline/vic20/bus/keyed_state.rb +5 -4
- data/lib/badline/vic20/bus.rb +5 -0
- data/lib/badline/vic20.rb +1 -1
- metadata +69 -1
data/lib/badline/pet.rb
ADDED
|
@@ -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
|
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
|