badline 0.1.0 → 0.2.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 +167 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +88 -23
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +131 -15
- data/lib/badline/cia/interrupt_register.rb +88 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +109 -74
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +68 -8
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +171 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +108 -167
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +33 -0
- data/lib/badline/kernal_trap/drive.rb +239 -0
- data/lib/badline/kernal_trap/file.rb +4 -24
- data/lib/badline/kernal_trap/load.rb +2 -2
- data/lib/badline/kernal_trap/routine.rb +34 -0
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +139 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +32 -27
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/disk_image.rb +52 -5
- data/lib/badline/storage/host_directory.rb +18 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +373 -49
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +103 -4
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# One open channel on the virtual drive, holding a file, a block buffer
|
|
6
|
+
# or the command channel's status message. It gives out one byte at a
|
|
7
|
+
# time and flags the last one, so the bus can raise EOI with it.
|
|
8
|
+
class Channel
|
|
9
|
+
attr_accessor :pointer
|
|
10
|
+
|
|
11
|
+
def initialize(bytes = [])
|
|
12
|
+
replace(bytes)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def replace(bytes)
|
|
16
|
+
@bytes = bytes
|
|
17
|
+
@pointer = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exhausted?
|
|
21
|
+
@pointer >= @bytes.length
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read
|
|
25
|
+
return if exhausted?
|
|
26
|
+
|
|
27
|
+
byte = @bytes[@pointer]
|
|
28
|
+
@pointer += 1
|
|
29
|
+
[byte, exhausted?]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# The CBM DOS side of the serial bus. Keeps the open channels, the
|
|
6
|
+
# command channel and its status message. `U1` reads a raw block into a
|
|
7
|
+
# buffer channel, which is how block-access loaders bypass the KERNAL's
|
|
8
|
+
# LOAD.
|
|
9
|
+
class Drive
|
|
10
|
+
COMMAND_CHANNEL = 15
|
|
11
|
+
BLOCK_SIZE = 256
|
|
12
|
+
|
|
13
|
+
OK = 0
|
|
14
|
+
WRITE_PROTECT_ON = 26
|
|
15
|
+
SYNTAX_ERROR = 30
|
|
16
|
+
FILE_NOT_FOUND = 62
|
|
17
|
+
ILLEGAL_TRACK_OR_SECTOR = 66
|
|
18
|
+
NO_CHANNEL = 70
|
|
19
|
+
DRIVE_NOT_READY = 74
|
|
20
|
+
|
|
21
|
+
MESSAGES = {
|
|
22
|
+
OK => " OK",
|
|
23
|
+
20 => "READ ERROR",
|
|
24
|
+
21 => "READ ERROR",
|
|
25
|
+
22 => "READ ERROR",
|
|
26
|
+
23 => "READ ERROR",
|
|
27
|
+
24 => "READ ERROR",
|
|
28
|
+
25 => "WRITE ERROR",
|
|
29
|
+
WRITE_PROTECT_ON => "WRITE PROTECT ON",
|
|
30
|
+
27 => "READ ERROR",
|
|
31
|
+
28 => "WRITE ERROR",
|
|
32
|
+
29 => "DISK ID MISMATCH",
|
|
33
|
+
SYNTAX_ERROR => "SYNTAX ERROR",
|
|
34
|
+
FILE_NOT_FOUND => "FILE NOT FOUND",
|
|
35
|
+
ILLEGAL_TRACK_OR_SECTOR => "ILLEGAL TRACK OR SECTOR",
|
|
36
|
+
NO_CHANNEL => "NO CHANNEL",
|
|
37
|
+
DRIVE_NOT_READY => "DRIVE NOT READY"
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
MEMORY_COMMANDS = { "M-W" => :memory_write, "M-R" => :memory_read }.freeze
|
|
41
|
+
|
|
42
|
+
RAM_SIZE = 0x800
|
|
43
|
+
|
|
44
|
+
# The job queue: a job code for each of five buffers at $00, their
|
|
45
|
+
# track and sector pairs from $06, and the buffers from $0300. A job
|
|
46
|
+
# code is replaced by its result, 1 for success or an error table
|
|
47
|
+
# code: the DOS error less 18, or 15 for DRIVE NOT READY.
|
|
48
|
+
JOBS = 5
|
|
49
|
+
READ_JOB = 0x80
|
|
50
|
+
JOB_OK = 1
|
|
51
|
+
HEADER_NOT_FOUND = 20
|
|
52
|
+
|
|
53
|
+
COMMANDS = {
|
|
54
|
+
/\AU[1A]\s*:?\s*(.*)/i => :block_read,
|
|
55
|
+
/\AB-R\s*:?\s*(.*)/i => :counted_block_read,
|
|
56
|
+
/\AB-P\s*:?\s*(.*)/i => :buffer_pointer,
|
|
57
|
+
/\A[IV]/i => :initialized,
|
|
58
|
+
/\A(?:U[2B]|B-[WAF])/i => :write_protected
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
def initialize(storage)
|
|
62
|
+
@storage = storage
|
|
63
|
+
@channels = {}
|
|
64
|
+
@status = Channel.new
|
|
65
|
+
@ram = Array.new(RAM_SIZE, 0)
|
|
66
|
+
report(OK)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A name on the command channel is a command, "#" opens a block
|
|
70
|
+
# buffer, anything else opens a file for reading. The name comes as
|
|
71
|
+
# the bytes sent, because a memory command's arguments are binary.
|
|
72
|
+
def open(secondary, name)
|
|
73
|
+
if secondary == COMMAND_CHANNEL
|
|
74
|
+
command(name.bytes) unless name.empty?
|
|
75
|
+
elsif name.start_with?("#")
|
|
76
|
+
@channels[secondary] = Channel.new(Array.new(BLOCK_SIZE, 0))
|
|
77
|
+
report(OK)
|
|
78
|
+
else
|
|
79
|
+
open_file(secondary, Storage.ascii(name.bytes))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Closing the command channel closes every other channel with it.
|
|
84
|
+
def close(secondary)
|
|
85
|
+
secondary == COMMAND_CHANNEL ? @channels.clear : @channels.delete(secondary)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def write(secondary, bytes)
|
|
89
|
+
return report(WRITE_PROTECT_ON) unless secondary == COMMAND_CHANNEL
|
|
90
|
+
|
|
91
|
+
command(bytes)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the next byte and whether it is the channel's last, or nil
|
|
95
|
+
# when there is nothing left to send.
|
|
96
|
+
def read(secondary)
|
|
97
|
+
secondary == COMMAND_CHANNEL ? read_status : @channels[secondary]&.read
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def read_status
|
|
103
|
+
result = @status.read
|
|
104
|
+
report(OK) if result&.last
|
|
105
|
+
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Secondary addresses 0 and 1 are LOAD and SAVE, which look for a PRG
|
|
110
|
+
# file unless the name asks for another type. Other channels take any
|
|
111
|
+
# type the name doesn't pin down.
|
|
112
|
+
def open_file(secondary, name)
|
|
113
|
+
file, type = Storage.parse_name(name)
|
|
114
|
+
type ||= :prg if secondary < 2
|
|
115
|
+
data = @storage.read_file(file, type:)
|
|
116
|
+
@channels[secondary] = Channel.new(data || [])
|
|
117
|
+
report(data ? OK : FILE_NOT_FOUND)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def execute(text)
|
|
121
|
+
command = text.strip
|
|
122
|
+
pattern, action = COMMANDS.find { |candidate, _| candidate.match?(command) }
|
|
123
|
+
return report(SYNTAX_ERROR) unless action
|
|
124
|
+
|
|
125
|
+
send(action, arguments(pattern.match(command)[1]))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def arguments(text)
|
|
129
|
+
text.to_s.scan(/\d+/).map(&:to_i)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def command(bytes)
|
|
133
|
+
action = MEMORY_COMMANDS[bytes[0, 3].pack("C*")]
|
|
134
|
+
action ? send(action, bytes[3..]) : execute(Storage.ascii(bytes))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def block_read(arguments, counted: false)
|
|
138
|
+
channel, _drive, track, sector = arguments
|
|
139
|
+
buffer = @channels[channel]
|
|
140
|
+
return report(NO_CHANNEL) unless buffer
|
|
141
|
+
return report(DRIVE_NOT_READY) unless @storage.respond_to?(:read_block)
|
|
142
|
+
|
|
143
|
+
data = sector && @storage.read_block(track, sector)
|
|
144
|
+
return report(ILLEGAL_TRACK_OR_SECTOR, track, sector) unless data
|
|
145
|
+
|
|
146
|
+
buffer.replace(counted ? data[0, data[0] + 1] : data)
|
|
147
|
+
buffer.pointer = 1 if counted
|
|
148
|
+
report_block_error(track, sector)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A block the image's error table marks bad still fills the buffer,
|
|
152
|
+
# but the read reports its error, which copy protection checks for.
|
|
153
|
+
def report_block_error(track, sector)
|
|
154
|
+
error = @storage.block_error(track, sector)
|
|
155
|
+
error ? report(error, track, sector) : report(OK)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# B-R takes the block's first byte as the index of its last one, and
|
|
159
|
+
# hands out the bytes from the second up to there.
|
|
160
|
+
def counted_block_read(arguments)
|
|
161
|
+
block_read(arguments, counted: true)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def buffer_pointer(arguments)
|
|
165
|
+
channel, position = arguments
|
|
166
|
+
buffer = @channels[channel]
|
|
167
|
+
return report(NO_CHANNEL) unless buffer
|
|
168
|
+
|
|
169
|
+
buffer.pointer = position.to_i
|
|
170
|
+
report(OK)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# M-W takes an address, a count and that many bytes. Writes past the
|
|
174
|
+
# RAM are dropped, since the rest of the address space is I/O and ROM.
|
|
175
|
+
def memory_write(arguments)
|
|
176
|
+
address = word(arguments)
|
|
177
|
+
arguments[3, arguments[2].to_i].to_a.each.with_index(address) do |byte, target|
|
|
178
|
+
@ram[target] = byte if target < RAM_SIZE
|
|
179
|
+
end
|
|
180
|
+
run_jobs
|
|
181
|
+
report(OK)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# M-R takes an address and a count, and answers on the command
|
|
185
|
+
# channel. Without a count, or with only the carriage return PRINT#
|
|
186
|
+
# ends a line with, it reads one byte. Only the RAM reads back.
|
|
187
|
+
def memory_read(arguments)
|
|
188
|
+
address = word(arguments)
|
|
189
|
+
count = arguments[2..] == ["\r".ord] ? 1 : arguments.fetch(2, 1)
|
|
190
|
+
count = BLOCK_SIZE if count.zero?
|
|
191
|
+
@status.replace(Array.new(count) { |i| @ram.fetch(address + i, 0) })
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def word(arguments)
|
|
196
|
+
arguments[0].to_i | (arguments[1].to_i << 8)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def run_jobs
|
|
200
|
+
JOBS.times do |job|
|
|
201
|
+
next unless @ram[job] == READ_JOB
|
|
202
|
+
|
|
203
|
+
track, sector = @ram[6 + (job * 2), 2]
|
|
204
|
+
@ram[job] = read_job(0x300 + (job * BLOCK_SIZE), track, sector)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def read_job(buffer, track, sector)
|
|
209
|
+
data = @storage.respond_to?(:read_block) && @storage.read_block(track, sector)
|
|
210
|
+
return job_result(HEADER_NOT_FOUND) unless data
|
|
211
|
+
|
|
212
|
+
@ram[buffer, BLOCK_SIZE] = data
|
|
213
|
+
job_result(@storage.block_error(track, sector))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def job_result(error)
|
|
217
|
+
return JOB_OK unless error
|
|
218
|
+
|
|
219
|
+
error == DRIVE_NOT_READY ? 15 : error - 18
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def initialized(_arguments)
|
|
223
|
+
report(OK)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def write_protected(_arguments)
|
|
227
|
+
report(WRITE_PROTECT_ON)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def report(code, track = 0, sector = 0)
|
|
231
|
+
message = format("%<code>02d,%<message>s,%<track>02d,%<sector>02d\r",
|
|
232
|
+
code:, message: MESSAGES.fetch(code),
|
|
233
|
+
track: track.to_i, sector: sector.to_i)
|
|
234
|
+
@status.replace(message.bytes)
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -2,23 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
|
-
class File
|
|
6
|
-
include IntegerHelper
|
|
7
|
-
|
|
8
|
-
DEVICE = 8
|
|
9
|
-
|
|
5
|
+
class File < Routine
|
|
10
6
|
MISSING_FILENAME = 0x08
|
|
11
7
|
|
|
12
8
|
def initialize(cpu:, bus:, storage:)
|
|
13
|
-
|
|
14
|
-
@bus = bus
|
|
9
|
+
super(cpu:, bus:)
|
|
15
10
|
@storage = storage
|
|
16
11
|
end
|
|
17
12
|
|
|
18
13
|
private
|
|
19
14
|
|
|
20
15
|
def active?
|
|
21
|
-
|
|
16
|
+
kernal? && @bus.peek(0xba) == DEVICE
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
# Filename pointer at $BB/$BC, length at $B7
|
|
@@ -27,28 +22,13 @@ module Badline
|
|
|
27
22
|
bytes = Array.new(@bus.peek(0xb7)) do |i|
|
|
28
23
|
@bus.peek((pointer + i) & 0xffff)
|
|
29
24
|
end
|
|
30
|
-
strip_drive_prefix(Storage.ascii(bytes))
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# CBM DOS drive prefix — "0:NAME" selects a drive, "@0:NAME" is
|
|
34
|
-
# save-with-replace
|
|
35
|
-
def strip_drive_prefix(name)
|
|
36
|
-
name.sub(/\A@?\d*:/, "")
|
|
25
|
+
Storage.strip_drive_prefix(Storage.ascii(bytes))
|
|
37
26
|
end
|
|
38
27
|
|
|
39
28
|
def error(code)
|
|
40
29
|
@cpu.a = code
|
|
41
30
|
@cpu.status.carry = true
|
|
42
31
|
end
|
|
43
|
-
|
|
44
|
-
def return_to_caller
|
|
45
|
-
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def pull_byte
|
|
49
|
-
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
50
|
-
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
51
|
-
end
|
|
52
32
|
end
|
|
53
33
|
end
|
|
54
34
|
end
|
|
@@ -14,10 +14,10 @@ module Badline
|
|
|
14
14
|
def call
|
|
15
15
|
return unless active?
|
|
16
16
|
|
|
17
|
-
name = filename
|
|
17
|
+
name, type = Storage.parse_name(filename)
|
|
18
18
|
if name.empty?
|
|
19
19
|
error(MISSING_FILENAME)
|
|
20
|
-
elsif (data = @storage.read_file(name))
|
|
20
|
+
elsif (data = @storage.read_file(name, type: type || :prg))
|
|
21
21
|
deliver(data)
|
|
22
22
|
else
|
|
23
23
|
error(FILE_NOT_FOUND)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# Base class for the PC traps on KERNAL routines. Holds the device
|
|
6
|
+
# number they answer for and the return that stands in for the trapped
|
|
7
|
+
# routine's RTS.
|
|
8
|
+
class Routine
|
|
9
|
+
include IntegerHelper
|
|
10
|
+
|
|
11
|
+
DEVICE = 8
|
|
12
|
+
|
|
13
|
+
def initialize(cpu:, bus:)
|
|
14
|
+
@cpu = cpu
|
|
15
|
+
@bus = bus
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def kernal?
|
|
21
|
+
@bus.io_port.kernal?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def return_to_caller
|
|
25
|
+
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pull_byte
|
|
29
|
+
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
30
|
+
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# PC traps on the KERNAL's serial bus primitives. Frames addressed to
|
|
6
|
+
# device 8 go to a virtual drive instead of out over the IEC lines, so
|
|
7
|
+
# OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
|
|
8
|
+
# loaders make through them. Other devices fall through to the ROM.
|
|
9
|
+
class Serial < Routine
|
|
10
|
+
ROUTINES = {
|
|
11
|
+
0xed09 => :talk,
|
|
12
|
+
0xed0c => :listen,
|
|
13
|
+
0xedb9 => :second,
|
|
14
|
+
0xedc7 => :tksa,
|
|
15
|
+
0xeddd => :ciout,
|
|
16
|
+
0xedef => :untalk,
|
|
17
|
+
0xedfe => :unlisten,
|
|
18
|
+
0xee13 => :acptr
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Frame type in the high nibble of the secondary address
|
|
22
|
+
OPEN = 0xf0
|
|
23
|
+
CLOSE = 0xe0
|
|
24
|
+
|
|
25
|
+
# ST bits at $90
|
|
26
|
+
EOI = 0x40
|
|
27
|
+
READ_TIMEOUT = 0x02
|
|
28
|
+
|
|
29
|
+
NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
|
|
30
|
+
|
|
31
|
+
def initialize(cpu:, bus:, drive:)
|
|
32
|
+
super(cpu:, bus:)
|
|
33
|
+
@drive = drive
|
|
34
|
+
@listening = false
|
|
35
|
+
@talking = false
|
|
36
|
+
@listen_channel = nil
|
|
37
|
+
@talk_channel = nil
|
|
38
|
+
@frame = OPEN
|
|
39
|
+
@buffer = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def install
|
|
43
|
+
ROUTINES.each do |address, routine|
|
|
44
|
+
@cpu.install_trap(address) { send(routine) if kernal? }
|
|
45
|
+
end
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Addressing the bus starts a new frame. The channel number arrives
|
|
52
|
+
# in the secondary address that follows.
|
|
53
|
+
def talk
|
|
54
|
+
@talk_channel = nil
|
|
55
|
+
@talking = @cpu.a == DEVICE
|
|
56
|
+
return_to_caller if @talking
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def listen
|
|
60
|
+
@listen_channel = nil
|
|
61
|
+
@listening = @cpu.a == DEVICE
|
|
62
|
+
return_to_caller if @listening
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def second
|
|
66
|
+
return unless @listening
|
|
67
|
+
|
|
68
|
+
@frame = @cpu.a & 0xf0
|
|
69
|
+
@listen_channel = @cpu.a & 0x0f
|
|
70
|
+
@buffer = []
|
|
71
|
+
return_to_caller
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tksa
|
|
75
|
+
return unless @talking
|
|
76
|
+
|
|
77
|
+
@talk_channel = @cpu.a & 0x0f
|
|
78
|
+
return_to_caller
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ciout
|
|
82
|
+
return unless @listening
|
|
83
|
+
|
|
84
|
+
@buffer << @cpu.a
|
|
85
|
+
@cpu.status.carry = false
|
|
86
|
+
return_to_caller
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def unlisten
|
|
90
|
+
return unless @listening
|
|
91
|
+
|
|
92
|
+
@listening = false
|
|
93
|
+
deliver_frame
|
|
94
|
+
return_to_caller
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def untalk
|
|
98
|
+
return unless @talking
|
|
99
|
+
|
|
100
|
+
@talking = false
|
|
101
|
+
return_to_caller
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def acptr
|
|
105
|
+
return unless @talking
|
|
106
|
+
|
|
107
|
+
byte, status = received
|
|
108
|
+
@cpu.a = byte
|
|
109
|
+
update_status(status)
|
|
110
|
+
@cpu.status.carry = false
|
|
111
|
+
return_to_caller
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def deliver_frame
|
|
115
|
+
return unless @listen_channel
|
|
116
|
+
|
|
117
|
+
case @frame
|
|
118
|
+
when OPEN then @drive.open(@listen_channel, @buffer.pack("C*"))
|
|
119
|
+
when CLOSE then @drive.close(@listen_channel)
|
|
120
|
+
else @drive.write(@listen_channel, @buffer)
|
|
121
|
+
end
|
|
122
|
+
@buffer = []
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def received
|
|
126
|
+
byte, eoi = @drive.read(@talk_channel)
|
|
127
|
+
return NO_DATA unless byte
|
|
128
|
+
|
|
129
|
+
[byte, eoi ? EOI : 0]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def update_status(bits)
|
|
133
|
+
return if bits.zero?
|
|
134
|
+
|
|
135
|
+
@bus.poke(0x90, @bus.peek(0x90) | bits)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/badline/kernal_trap.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/kernal_trap/routine"
|
|
4
|
+
require "badline/kernal_trap/channel"
|
|
5
|
+
require "badline/kernal_trap/drive"
|
|
3
6
|
require "badline/kernal_trap/file"
|
|
4
7
|
require "badline/kernal_trap/load"
|
|
5
8
|
require "badline/kernal_trap/save"
|
|
9
|
+
require "badline/kernal_trap/serial"
|
data/lib/badline/keyboard.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
+
# The 8x8 key matrix spanning CIA 1's two ports: rows on port A, columns on
|
|
5
|
+
# port B.
|
|
6
|
+
#
|
|
7
|
+
# A pressed key shorts its row line to its column line, so whichever side is
|
|
8
|
+
# pulled low drags the other down with it. Scanning works in either
|
|
9
|
+
# direction, and chains of keys sharing a line propagate the pulldown -- the
|
|
10
|
+
# ghost keys real hardware reports.
|
|
4
11
|
class Keyboard
|
|
5
12
|
include IntegerHelper
|
|
6
13
|
|
|
@@ -8,9 +15,10 @@ module Badline
|
|
|
8
15
|
|
|
9
16
|
def initialize
|
|
10
17
|
@keys = []
|
|
18
|
+
@row_masks = nil
|
|
11
19
|
|
|
12
20
|
@matrix = [
|
|
13
|
-
%i[delete return cursor_h f7 f1 f3 cursor_v],
|
|
21
|
+
%i[delete return cursor_h f7 f1 f3 f5 cursor_v],
|
|
14
22
|
%i[3 w a 4 z s e lshift],
|
|
15
23
|
%i[5 r d 6 c f t x],
|
|
16
24
|
%i[7 y g 8 b h u v],
|
|
@@ -22,35 +30,51 @@ module Badline
|
|
|
22
30
|
end
|
|
23
31
|
|
|
24
32
|
def press(key)
|
|
25
|
-
|
|
33
|
+
return unless valid_key?(key)
|
|
34
|
+
|
|
35
|
+
@keys << key
|
|
36
|
+
@row_masks = nil
|
|
26
37
|
end
|
|
27
38
|
|
|
28
|
-
def
|
|
29
|
-
|
|
39
|
+
def release(key)
|
|
40
|
+
@keys.reject! { |k| k == key }
|
|
41
|
+
@row_masks = nil
|
|
30
42
|
end
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
# Settles the row (port A) and column (port B) lines against each other
|
|
45
|
+
# until no key contact pulls a line low any more, and returns both.
|
|
46
|
+
def scan(rows, cols)
|
|
47
|
+
return [rows, cols] if keys.empty?
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
loop do
|
|
50
|
+
settled_rows = rows
|
|
51
|
+
settled_cols = cols
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
row_masks.each_with_index do |mask, row|
|
|
54
|
+
bit = 1 << row
|
|
55
|
+
next if mask.zero? || (rows.allbits?(bit) && cols.allbits?(mask))
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
rows &= ~bit
|
|
58
|
+
cols &= ~mask
|
|
42
59
|
end
|
|
43
|
-
end
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
return [rows, cols] if rows == settled_rows && cols == settled_cols
|
|
62
|
+
end
|
|
46
63
|
end
|
|
47
64
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
def read_a(port_a, port_b) = scan(port_a, port_b).first
|
|
66
|
+
|
|
67
|
+
def read_b(port_a, port_b) = scan(port_a, port_b).last
|
|
51
68
|
|
|
52
69
|
private
|
|
53
70
|
|
|
71
|
+
# Columns held down per row, as a bit mask of port B lines.
|
|
72
|
+
def row_masks
|
|
73
|
+
@row_masks ||= matrix.map do |row|
|
|
74
|
+
row.each_with_index.sum { |key, column| keys.include?(key) ? 1 << column : 0 }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
54
78
|
def valid_key?(key)
|
|
55
79
|
matrix.flatten.include?(key)
|
|
56
80
|
end
|