badline 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/library.md +15 -1
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +17 -3
- data/lib/badline/address_bus/fittings.rb +38 -0
- data/lib/badline/address_bus/pla.rb +160 -0
- data/lib/badline/address_bus/roms.rb +12 -5
- data/lib/badline/address_bus.rb +15 -90
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +47 -7
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +52 -37
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +3 -0
- data/lib/badline/datasette.rb +39 -1
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +62 -64
- data/lib/badline/frontend/boot.rb +33 -8
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_media.rb +49 -24
- data/lib/badline/frontend/menu_pages.rb +23 -40
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +30 -33
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +1 -1
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/sound.rb +11 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +2 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +16 -27
- data/lib/badline/kernal_trap/routine.rb +11 -27
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +21 -19
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +28 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +32 -18
- data/lib/badline/model.rb +29 -12
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +12 -2
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +51 -0
- data/lib/badline/roms/c64gs.rom +0 -0
- data/lib/badline/roms/kernal-pet64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +15 -10
- data/lib/badline/snapshot/state.rb +5 -3
- data/lib/badline/snapshot/vice.rb +2 -1
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +3 -0
- data/lib/badline/storage/tap.rb +38 -4
- data/lib/badline/storage/unavailable.rb +2 -0
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +55 -17
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +48 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- metadata +31 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
data/lib/badline/iec_bus.rb
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
# The serial bus between the C64 and its drives: ATN, CLK and DATA, each
|
|
5
5
|
# an open-collector line with a pull-up. A line is low while any device
|
|
6
|
-
# pulls it (wired-AND), and high otherwise.
|
|
7
|
-
#
|
|
6
|
+
# pulls it (wired-AND), and high otherwise. Every read works the levels
|
|
7
|
+
# out from what each side drives now.
|
|
8
8
|
#
|
|
9
9
|
# The C64 drives the lines from CIA 2's port A through 7406 inverters, so
|
|
10
10
|
# an output bit of 1 pulls its line low: PA3 ATN, PA4 CLK and PA5 DATA.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# The machine pushes those bits into the bus (host_lines=), which holds
|
|
12
|
+
# them. PA6 and PA7 read CLK and DATA directly, 1 for a released line.
|
|
13
|
+
# The bus stands in as CIA 2's peripheral for those two inputs.
|
|
13
14
|
#
|
|
14
15
|
# A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
|
|
15
16
|
# also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
|
|
@@ -33,30 +34,21 @@ module Badline
|
|
|
33
34
|
CLK = 0x02
|
|
34
35
|
DATA = 0x04
|
|
35
36
|
|
|
36
|
-
attr_reader :
|
|
37
|
+
attr_reader :drives, :host_lines
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
def initialize(host: nil)
|
|
40
|
-
@host = host
|
|
39
|
+
def initialize
|
|
41
40
|
@drives = []
|
|
42
|
-
@
|
|
41
|
+
@host_lines = 0
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@
|
|
50
|
-
@
|
|
44
|
+
# The machine pushes the lines it pulls, as port A bits, whenever they
|
|
45
|
+
# may have moved: its output bits, with input bits floating high as the
|
|
46
|
+
# 7406 inputs see them. A bus with only drives on it keeps 0.
|
|
47
|
+
def host_lines=(lines)
|
|
48
|
+
@host_lines = lines
|
|
49
|
+
@drives.each(&:host_written!)
|
|
51
50
|
end
|
|
52
51
|
|
|
53
|
-
# Whether the host says when it may have moved a line (see plug_in!).
|
|
54
|
-
# Without, a drive asleep checks ATN every cycle.
|
|
55
|
-
def notifies? = @notifies
|
|
56
|
-
|
|
57
|
-
# CIA 2 wrote its port A, which may have moved a line.
|
|
58
|
-
def host_written = @drives.each(&:host_written!)
|
|
59
|
-
|
|
60
52
|
def attach(drive)
|
|
61
53
|
@drives << drive unless @drives.include?(drive)
|
|
62
54
|
end
|
|
@@ -68,7 +60,7 @@ module Badline
|
|
|
68
60
|
# One pass over everything on the bus, for a reader that wants more
|
|
69
61
|
# than one line. +host+ is the C64's port A lines as the reader sees
|
|
70
62
|
# them, which a drive takes a cycle late (see Drive1541::SerialPort).
|
|
71
|
-
def low_lines(host = host_lines)
|
|
63
|
+
def low_lines(host = @host_lines)
|
|
72
64
|
atn = host.anybits?(HOST_ATN_OUT)
|
|
73
65
|
low = atn ? ATN : 0
|
|
74
66
|
low |= CLK if host.anybits?(HOST_CLK_OUT)
|
|
@@ -83,7 +75,7 @@ module Badline
|
|
|
83
75
|
low
|
|
84
76
|
end
|
|
85
77
|
|
|
86
|
-
def atn_low?(host = host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
78
|
+
def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
|
|
87
79
|
|
|
88
80
|
def clk_low? = low_lines.anybits?(CLK)
|
|
89
81
|
|
|
@@ -101,9 +93,5 @@ module Badline
|
|
|
101
93
|
|
|
102
94
|
# Port B goes to the user port, not the serial bus.
|
|
103
95
|
def read_b(_port_a, _port_b) = 0xff
|
|
104
|
-
|
|
105
|
-
# What the C64 pulls, as port A bits: its output bits, with input bits
|
|
106
|
-
# floating high as the 7406 inputs see them.
|
|
107
|
-
def host_lines = @host ? @host.port_a_lines : 0
|
|
108
96
|
end
|
|
109
97
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# Where a machine's KERNAL keeps the routines the traps stand in for,
|
|
6
|
+
# and the ROM code they hand over to, with the steps that depend on the
|
|
7
|
+
# machine around it: whether the KERNAL is mapped in, the timer that
|
|
8
|
+
# times each serial byte and the release of the serial lines.
|
|
9
|
+
#
|
|
10
|
+
# +load+ and +save+ are the default ILOAD and ISAVE vector targets.
|
|
11
|
+
# LOAD hands over to the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
12
|
+
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
13
|
+
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the FILE NOT FOUND
|
|
14
|
+
# and MISSING FILE NAME error exits. SAVE hands over to the SAVING
|
|
15
|
+
# message, the two line releases that end UNLISTEN (clock, then data,
|
|
16
|
+
# leaving A as read from the port), the successful return (CLC, RTS)
|
|
17
|
+
# and the MISSING FILE NAME exit. The rest are the serial bus
|
|
18
|
+
# primitives' entry points.
|
|
19
|
+
#
|
|
20
|
+
# +banked+ says whether the 6510 port can bank the KERNAL out, and so
|
|
21
|
+
# whether RAM lies under every ROM and the I/O. Each
|
|
22
|
+
# serial byte starts a one-shot timer by writing the high byte of its
|
|
23
|
+
# count to +serial_timer+, and the control value TIMER_ONE_SHOT_START
|
|
24
|
+
# to +serial_timer_start+ where the timer has a control register to
|
|
25
|
+
# start it (a CIA's), or nil where the write to the high byte starts it
|
|
26
|
+
# (a VIA's timer 2). The ROM lets go of each serial line by reading a
|
|
27
|
+
# port, masking the line's bit off and writing it back: ATN at
|
|
28
|
+
# +atn_port+ with +atn_mask+, then the clock and the data line.
|
|
29
|
+
Layout = Data.define(:load, :save, :searching_message, :loading_message, :load_byte_loop, :load_done,
|
|
30
|
+
:file_not_found_exit, :missing_file_name_exit, :saving_message, :clock_release,
|
|
31
|
+
:data_release, :save_done, :talk, :listen, :second, :tksa, :ciout, :untalk,
|
|
32
|
+
:unlisten, :acptr, :banked, :serial_timer, :serial_timer_start, :atn_port, :atn_mask,
|
|
33
|
+
:clock_port, :clock_mask, :data_port, :data_mask) do
|
|
34
|
+
# Whether the CPU reads the KERNAL ROM at the trapped addresses: on a
|
|
35
|
+
# banked machine, the 6510 port's banking bits select it.
|
|
36
|
+
def kernal_mapped?(bus)
|
|
37
|
+
!banked || bus.io_port.kernal?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Stores a loaded file's bytes from +address+ on: on a banked machine
|
|
41
|
+
# into the RAM under whatever is banked in, and otherwise where the
|
|
42
|
+
# ROM's byte loop would store them, through the bus.
|
|
43
|
+
def store_file(bus, address, bytes)
|
|
44
|
+
return bus.ram.write(address, bytes) if banked
|
|
45
|
+
|
|
46
|
+
bytes.each_with_index { |byte, i| bus.poke(address + i, byte) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Starts the timer the ROM times a serial byte with, its count's high
|
|
50
|
+
# byte +timer_high+.
|
|
51
|
+
def time_serial_byte(bus, timer_high)
|
|
52
|
+
bus.poke(serial_timer, timer_high)
|
|
53
|
+
start = serial_timer_start
|
|
54
|
+
bus.poke(start, TIMER_ONE_SHOT_START) if start
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The ROM's release of the serial bus: ATN, then the clock, then the
|
|
58
|
+
# data line, each a read of its port, a mask and a write back, so the
|
|
59
|
+
# input bits written back follow the lines as each one is let go.
|
|
60
|
+
# Returns the last value written, which the ROM leaves in A.
|
|
61
|
+
def release_serial_lines(bus)
|
|
62
|
+
release_line(bus, atn_port, atn_mask)
|
|
63
|
+
release_line(bus, clock_port, clock_mask)
|
|
64
|
+
release_line(bus, data_port, data_mask)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def release_line(bus, port, mask)
|
|
70
|
+
value = bus.peek(port) & mask
|
|
71
|
+
bus.poke(port, value)
|
|
72
|
+
value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A CIA's control value that starts its timer: force load, one-shot,
|
|
77
|
+
# start.
|
|
78
|
+
TIMER_ONE_SHOT_START = 0x19
|
|
79
|
+
|
|
80
|
+
# The C64 KERNAL, revision 3. The SX-64's, 251104-04, differs from it
|
|
81
|
+
# only in bytes none of these addresses reach. It times serial bytes
|
|
82
|
+
# with CIA 1's timer B, and drives the lines from CIA 2's port A
|
|
83
|
+
# ($EE03: ATN $EDBE, clock $EE85, data $EE97).
|
|
84
|
+
C64_LAYOUT = Layout.new(
|
|
85
|
+
load: 0xf4a5, save: 0xf5ed,
|
|
86
|
+
searching_message: 0xf5af, loading_message: 0xf5d2, load_byte_loop: 0xf4f3, load_done: 0xf5a9,
|
|
87
|
+
file_not_found_exit: 0xf704, missing_file_name_exit: 0xf710,
|
|
88
|
+
saving_message: 0xf68f, clock_release: 0xee85, data_release: 0xee97, save_done: 0xf657,
|
|
89
|
+
talk: 0xed09, listen: 0xed0c, second: 0xedb9, tksa: 0xedc7,
|
|
90
|
+
ciout: 0xeddd, untalk: 0xedef, unlisten: 0xedfe, acptr: 0xee13,
|
|
91
|
+
banked: true, serial_timer: 0xdc07, serial_timer_start: 0xdc0f,
|
|
92
|
+
atn_port: 0xdd00, atn_mask: 0xf7, clock_port: 0xdd00, clock_mask: 0xef, data_port: 0xdd00, data_mask: 0xdf
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# The VIC-20 KERNAL, 901486-07 (PAL). The NTSC one, 901486-06, differs
|
|
96
|
+
# from it only at $E475, $EDE4-$EDE5, $FE3F, $FE44 and $FF5C-$FF70,
|
|
97
|
+
# none of them here. Nothing banks it out. It times serial bytes with
|
|
98
|
+
# VIA 2's timer 2 ($9129), drives ATN from VIA 1's PA7 ($911F) and the
|
|
99
|
+
# clock and data lines from VIA 2's CA2 and CB2 ($912C), and UNLISTEN
|
|
100
|
+
# lets them go at $EF09: ATN $EEC5, clock $EF84, data $E4A0.
|
|
101
|
+
VIC20_LAYOUT = Layout.new(
|
|
102
|
+
load: 0xf549, save: 0xf685,
|
|
103
|
+
searching_message: 0xf647, loading_message: 0xf66a, load_byte_loop: 0xf58a, load_done: 0xf641,
|
|
104
|
+
file_not_found_exit: 0xf787, missing_file_name_exit: 0xf793,
|
|
105
|
+
saving_message: 0xf728, clock_release: 0xef84, data_release: 0xe4a0, save_done: 0xf6ef,
|
|
106
|
+
talk: 0xee14, listen: 0xee17, second: 0xeec0, tksa: 0xeece,
|
|
107
|
+
ciout: 0xeee4, untalk: 0xeef6, unlisten: 0xef04, acptr: 0xef19,
|
|
108
|
+
banked: false, serial_timer: 0x9129, serial_timer_start: nil,
|
|
109
|
+
atn_port: 0x911f, atn_mask: 0x7f, clock_port: 0x912c, clock_mask: 0xfd, data_port: 0x912c, data_mask: 0xdf
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -2,29 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
|
-
# PC trap on the KERNAL serial LOAD routine (
|
|
6
|
-
#
|
|
7
|
-
# channel 0, then hands over to the
|
|
8
|
-
# SEARCHING FOR and LOADING (or VERIFYING)
|
|
9
|
-
# errors and returns with the routine's
|
|
10
|
-
# other devices fall through to the ROM.
|
|
5
|
+
# PC trap on the KERNAL serial LOAD routine (the default ILOAD vector
|
|
6
|
+
# target, $F4A5 on the C64, $F549 on the VIC-20). Reads device 8
|
|
7
|
+
# requests from the virtual drive's channel 0, then hands over to the
|
|
8
|
+
# ROM's own tail so it prints SEARCHING FOR and LOADING (or VERIFYING)
|
|
9
|
+
# in direct mode, reports errors and returns with the routine's
|
|
10
|
+
# register/zeropage contract; other devices fall through to the ROM.
|
|
11
|
+
# So does a load that reaches
|
|
11
12
|
# below $0334, into the zero page, the stack or the KERNAL vectors: the
|
|
12
13
|
# ROM's byte loop then loads it through the serial traps, and a loader
|
|
13
14
|
# that overwrites ISTOP takes over mid-load as it does on a real drive.
|
|
14
15
|
class Load < File
|
|
15
|
-
ADDRESS = 0xf4a5
|
|
16
|
-
|
|
17
|
-
# ROM entry points: the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
18
|
-
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
19
|
-
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the
|
|
20
|
-
# FILE NOT FOUND and MISSING FILE NAME error exits
|
|
21
|
-
SEARCHING_MESSAGE = 0xf5af
|
|
22
|
-
LOADING_MESSAGE = 0xf5d2
|
|
23
|
-
BYTE_LOOP = 0xf4f3
|
|
24
|
-
LOAD_DONE = 0xf5a9
|
|
25
|
-
FILE_NOT_FOUND_EXIT = 0xf704
|
|
26
|
-
MISSING_FILE_NAME_EXIT = 0xf710
|
|
27
|
-
|
|
28
16
|
# ST bits at $90
|
|
29
17
|
EOI = 0x40
|
|
30
18
|
VERIFY_MISMATCH = 0x10
|
|
@@ -35,8 +23,8 @@ module Badline
|
|
|
35
23
|
# address even when the first one relocated.
|
|
36
24
|
LOAD_SECONDARY = 0x60
|
|
37
25
|
|
|
38
|
-
def initialize(cpu:, bus:, drive:)
|
|
39
|
-
super(cpu:, bus:)
|
|
26
|
+
def initialize(cpu:, bus:, layout:, drive:)
|
|
27
|
+
super(cpu:, bus:, layout:)
|
|
40
28
|
@drive = drive
|
|
41
29
|
end
|
|
42
30
|
|
|
@@ -46,7 +34,7 @@ module Badline
|
|
|
46
34
|
@bus.poke(0x93, @cpu.a)
|
|
47
35
|
@bus.poke(0x90, 0x00)
|
|
48
36
|
name = filename
|
|
49
|
-
return @cpu.program_counter =
|
|
37
|
+
return @cpu.program_counter = @layout.missing_file_name_exit if Storage.parse_name(name).first.empty?
|
|
50
38
|
|
|
51
39
|
data, complete = receive(name)
|
|
52
40
|
if low_memory?(data)
|
|
@@ -80,22 +68,23 @@ module Badline
|
|
|
80
68
|
if data.empty?
|
|
81
69
|
@bus.poke(0x90, EOI | READ_TIMEOUT)
|
|
82
70
|
leave_bus(ACPTR_TIMEOUT)
|
|
83
|
-
continue_with(
|
|
71
|
+
continue_with(@layout.searching_message, @layout.file_not_found_exit)
|
|
84
72
|
else
|
|
85
73
|
deliver(data)
|
|
86
74
|
@bus.poke(0x90, @bus.peek(0x90) & ~EOI) unless complete
|
|
87
75
|
leave_bus(ISOUR_TIMEOUT)
|
|
88
|
-
continue_with(
|
|
76
|
+
continue_with(@layout.searching_message, @layout.loading_message,
|
|
77
|
+
complete ? @layout.load_done : @layout.load_byte_loop)
|
|
89
78
|
end
|
|
90
79
|
@bus.poke(0xb9, LOAD_SECONDARY)
|
|
91
80
|
end
|
|
92
81
|
|
|
93
82
|
# A load ends with the UNLISTEN that closes the file, a byte sent,
|
|
94
83
|
# and a missing file with the ACPTR that timed out. Either way it
|
|
95
|
-
# releases ATN, the clock and the data line
|
|
84
|
+
# releases ATN, the clock and the data line.
|
|
96
85
|
def leave_bus(timer_high)
|
|
97
86
|
time_serial_byte(timer_high)
|
|
98
|
-
release_serial_lines
|
|
87
|
+
@layout.release_serial_lines(@bus)
|
|
99
88
|
end
|
|
100
89
|
|
|
101
90
|
def low_memory?(data)
|
|
@@ -112,7 +101,7 @@ module Badline
|
|
|
112
101
|
payload = data[2..] || []
|
|
113
102
|
payload = payload[0, 0x10000 - addr] if addr + payload.length > 0x10000
|
|
114
103
|
if load?
|
|
115
|
-
@
|
|
104
|
+
@layout.store_file(@bus, addr, payload)
|
|
116
105
|
@bus.poke(0x90, EOI)
|
|
117
106
|
else
|
|
118
107
|
verify(addr, payload)
|
|
@@ -3,50 +3,34 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
5
|
# Base class for the PC traps on KERNAL routines. Holds the device
|
|
6
|
-
# number they answer for
|
|
7
|
-
# routine's RTS.
|
|
6
|
+
# number they answer for, the layout of the KERNAL they trap, and the
|
|
7
|
+
# return that stands in for the trapped routine's RTS.
|
|
8
8
|
class Routine
|
|
9
9
|
include IntegerHelper
|
|
10
10
|
|
|
11
11
|
DEVICE = 8
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
# control value both start it with: force load, one-shot, start.
|
|
13
|
+
# The serial timer's high byte as ISOUR and ACPTR set it.
|
|
15
14
|
ISOUR_TIMEOUT = 0x04
|
|
16
15
|
ACPTR_TIMEOUT = 0x01
|
|
17
|
-
TIMER_ONE_SHOT_START = 0x19
|
|
18
16
|
|
|
19
|
-
def initialize(cpu:, bus:)
|
|
17
|
+
def initialize(cpu:, bus:, layout:)
|
|
20
18
|
@cpu = cpu
|
|
21
19
|
@bus = bus
|
|
20
|
+
@layout = layout
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
private
|
|
25
24
|
|
|
26
|
-
def
|
|
27
|
-
@
|
|
25
|
+
def kernal_mapped?
|
|
26
|
+
@layout.kernal_mapped?(@bus)
|
|
28
27
|
end
|
|
29
28
|
|
|
30
|
-
# The ROM times each byte on the serial bus with
|
|
31
|
-
#
|
|
32
|
-
#
|
|
29
|
+
# The ROM times each byte on the serial bus with a one-shot timer
|
|
30
|
+
# (Layout#time_serial_byte): ISOUR_TIMEOUT for a byte sent,
|
|
31
|
+
# ACPTR_TIMEOUT for one received.
|
|
33
32
|
def time_serial_byte(timer_high)
|
|
34
|
-
@
|
|
35
|
-
@bus.poke(0xdc0f, TIMER_ONE_SHOT_START)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# The ROM's release of the serial bus ($EE03): ATN ($EDBE), then the
|
|
39
|
-
# clock ($EE85), then the data line ($EE97), each a read of CIA 2's
|
|
40
|
-
# port A, a mask and a write back, so the input bits written back
|
|
41
|
-
# follow the lines as each one is let go. Returns the last value
|
|
42
|
-
# written, which the ROM leaves in A.
|
|
43
|
-
def release_serial_lines
|
|
44
|
-
value = 0
|
|
45
|
-
[0xf7, 0xef, 0xdf].each do |mask|
|
|
46
|
-
value = @bus.peek(0xdd00) & mask
|
|
47
|
-
@bus.poke(0xdd00, value)
|
|
48
|
-
end
|
|
49
|
-
value
|
|
33
|
+
@layout.time_serial_byte(@bus, timer_high)
|
|
50
34
|
end
|
|
51
35
|
|
|
52
36
|
def return_to_caller
|
|
@@ -2,37 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
|
-
# PC trap on the KERNAL serial SAVE routine (
|
|
6
|
-
#
|
|
7
|
-
# (load address followed by the memory
|
|
8
|
-
# through to the ROM, and so do saves to a
|
|
9
|
-
# whole. The ROM prints SAVING in direct
|
|
10
|
-
# into the trap, which then writes the file and leaves
|
|
11
|
-
# ROM's own tail with the registers its UNLISTEN and return
|
|
5
|
+
# PC trap on the KERNAL serial SAVE routine (the default ISAVE vector
|
|
6
|
+
# target, $F5ED on the C64, $F685 on the VIC-20). Hands device 8 saves
|
|
7
|
+
# to the virtual drive as a PRG (load address followed by the memory
|
|
8
|
+
# range); other devices fall through to the ROM, and so do saves to a
|
|
9
|
+
# disk that doesn't take them whole. The ROM prints SAVING in direct
|
|
10
|
+
# mode and returns into the trap, which then writes the file and leaves
|
|
11
|
+
# through the ROM's own tail with the registers its UNLISTEN and return
|
|
12
|
+
# leave. A
|
|
12
13
|
# write that fails, to a full disk, a name already on it or a host file
|
|
13
14
|
# that can't be written, ends the way a 1541 ends a SAVE it can't
|
|
14
15
|
# write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
|
|
15
16
|
# and the ROM returns without an error. A "@" before the drive prefix
|
|
16
17
|
# writes over a file of the same name.
|
|
17
18
|
class Save < File
|
|
18
|
-
ADDRESS = 0xf5ed
|
|
19
|
-
|
|
20
|
-
# ROM entry points: the SAVING message, the two line releases that
|
|
21
|
-
# end UNLISTEN (clock, then data, leaving A as read from $DD00), the
|
|
22
|
-
# successful return (CLC, RTS) and the MISSING FILE NAME error exit
|
|
23
|
-
SAVING_MESSAGE = 0xf68f
|
|
24
|
-
CLOCK_RELEASE = 0xee85
|
|
25
|
-
DATA_RELEASE = 0xee97
|
|
26
|
-
SAVE_DONE = 0xf657
|
|
27
|
-
MISSING_FILE_NAME_EXIT = 0xf710
|
|
28
|
-
|
|
29
19
|
SECONDARY = 0x61
|
|
30
20
|
|
|
31
21
|
# ST bit at $90
|
|
32
22
|
DEVICE_NOT_PRESENT = 0x80
|
|
33
23
|
|
|
34
|
-
def initialize(cpu:, bus:, drive:)
|
|
35
|
-
super(cpu:, bus:)
|
|
24
|
+
def initialize(cpu:, bus:, layout:, drive:)
|
|
25
|
+
super(cpu:, bus:, layout:)
|
|
36
26
|
@drive = drive
|
|
37
27
|
@saving = false
|
|
38
28
|
end
|
|
@@ -51,11 +41,11 @@ module Badline
|
|
|
51
41
|
return unless @drive.saves?
|
|
52
42
|
|
|
53
43
|
@bus.poke(0xb9, SECONDARY)
|
|
54
|
-
return @cpu.program_counter =
|
|
44
|
+
return @cpu.program_counter = @layout.missing_file_name_exit if name.empty?
|
|
55
45
|
|
|
56
46
|
@bus.poke(0x90, 0x00)
|
|
57
47
|
@saving = true
|
|
58
|
-
continue_with(
|
|
48
|
+
continue_with(@layout.saving_message, @layout.save)
|
|
59
49
|
end
|
|
60
50
|
|
|
61
51
|
private
|
|
@@ -72,7 +62,7 @@ module Badline
|
|
|
72
62
|
@bus.poke(0xad, @bus.peek(0xaf))
|
|
73
63
|
@cpu.y = 0
|
|
74
64
|
@cpu.status.overflow = true
|
|
75
|
-
continue_with(
|
|
65
|
+
continue_with(@layout.clock_release, @layout.data_release, @layout.save_done)
|
|
76
66
|
end
|
|
77
67
|
|
|
78
68
|
# Start address at $C1/$C2 (STAL), end address (exclusive) at
|
|
@@ -12,17 +12,6 @@ module Badline
|
|
|
12
12
|
class Serial < Routine
|
|
13
13
|
attr_accessor :device
|
|
14
14
|
|
|
15
|
-
ROUTINES = {
|
|
16
|
-
0xed09 => :talk,
|
|
17
|
-
0xed0c => :listen,
|
|
18
|
-
0xedb9 => :second,
|
|
19
|
-
0xedc7 => :tksa,
|
|
20
|
-
0xeddd => :ciout,
|
|
21
|
-
0xedef => :untalk,
|
|
22
|
-
0xedfe => :unlisten,
|
|
23
|
-
0xee13 => :acptr
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
15
|
# Frame type in the high nibble of the secondary address
|
|
27
16
|
OPEN = 0xf0
|
|
28
17
|
CLOSE = 0xe0
|
|
@@ -34,8 +23,8 @@ module Badline
|
|
|
34
23
|
|
|
35
24
|
NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
|
|
36
25
|
|
|
37
|
-
def initialize(cpu:, bus:, drive:, device: DEVICE)
|
|
38
|
-
super(cpu:, bus:)
|
|
26
|
+
def initialize(cpu:, bus:, layout:, drive:, device: DEVICE)
|
|
27
|
+
super(cpu:, bus:, layout:)
|
|
39
28
|
@drive = drive
|
|
40
29
|
@device = device
|
|
41
30
|
@listening = false
|
|
@@ -63,14 +52,27 @@ module Badline
|
|
|
63
52
|
end
|
|
64
53
|
|
|
65
54
|
def install
|
|
66
|
-
|
|
67
|
-
@cpu.install_trap(address) { call_routine(routine) if
|
|
55
|
+
routines.each do |address, routine|
|
|
56
|
+
@cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
|
|
68
57
|
end
|
|
69
58
|
self
|
|
70
59
|
end
|
|
71
60
|
|
|
72
61
|
private
|
|
73
62
|
|
|
63
|
+
def routines
|
|
64
|
+
{
|
|
65
|
+
@layout.talk => :talk,
|
|
66
|
+
@layout.listen => :listen,
|
|
67
|
+
@layout.second => :second,
|
|
68
|
+
@layout.tksa => :tksa,
|
|
69
|
+
@layout.ciout => :ciout,
|
|
70
|
+
@layout.untalk => :untalk,
|
|
71
|
+
@layout.unlisten => :unlisten,
|
|
72
|
+
@layout.acptr => :acptr
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
74
76
|
def call_routine(routine)
|
|
75
77
|
case routine
|
|
76
78
|
when :talk then talk
|
|
@@ -86,7 +88,7 @@ module Badline
|
|
|
86
88
|
|
|
87
89
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
88
90
|
# in the secondary address that follows. Each byte sent under ATN,
|
|
89
|
-
# and each byte of a frame, times its handshake with
|
|
91
|
+
# and each byte of a frame, times its handshake with the serial timer
|
|
90
92
|
# as ISOUR does.
|
|
91
93
|
def talk
|
|
92
94
|
@talk_channel = nil
|
|
@@ -157,10 +159,10 @@ module Badline
|
|
|
157
159
|
release_bus
|
|
158
160
|
end
|
|
159
161
|
|
|
160
|
-
# UNLSN and UNTLK end by releasing ATN, the clock and the data line
|
|
161
|
-
#
|
|
162
|
+
# UNLSN and UNTLK end by releasing ATN, the clock and the data line,
|
|
163
|
+
# and leave the port's last read in the accumulator.
|
|
162
164
|
def release_bus
|
|
163
|
-
@cpu.a = release_serial_lines
|
|
165
|
+
@cpu.a = @layout.release_serial_lines(@bus)
|
|
164
166
|
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
165
167
|
@cpu.status.zero = @cpu.a.zero?
|
|
166
168
|
return_to_caller
|
data/lib/badline/kernal_trap.rb
CHANGED
data/lib/badline/keyboard.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
-
# The
|
|
5
|
-
# port B.
|
|
4
|
+
# The key matrix spanning CIA 1's two ports: rows on port A, columns on
|
|
5
|
+
# port B. Row lines beyond the eighth sit above port A's bits in +rows+.
|
|
6
6
|
#
|
|
7
7
|
# A pressed key shorts its row line to its column line, so whichever side is
|
|
8
8
|
# pulled low drags the other down with it. Scanning works in either
|
|
@@ -19,26 +19,40 @@ module Badline
|
|
|
19
19
|
cursor_left: %i[lshift cursor_h]
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
|
+
# The C64's wiring: one row of keys per port A line, in port B column
|
|
23
|
+
# order.
|
|
24
|
+
C64_MATRIX = [
|
|
25
|
+
%i[delete return cursor_h f7 f1 f3 f5 cursor_v],
|
|
26
|
+
%i[3 w a 4 z s e lshift],
|
|
27
|
+
%i[5 r d 6 c f t x],
|
|
28
|
+
%i[7 y g 8 b h u v],
|
|
29
|
+
%i[9 i j 0 m k o n],
|
|
30
|
+
%i[+ p l - . : @ ,],
|
|
31
|
+
%i[£ * ; clr_home rshift = up /],
|
|
32
|
+
%i[1 left control 2 space cbm q run_stop]
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
22
35
|
attr_reader :keys, :matrix
|
|
23
36
|
|
|
24
|
-
def initialize
|
|
37
|
+
def initialize(matrix: C64_MATRIX)
|
|
25
38
|
@keys = []
|
|
26
39
|
@row_masks = nil
|
|
40
|
+
@matrix = matrix
|
|
41
|
+
@connected = true
|
|
42
|
+
end
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
%i[1 left control 2 space cbm q run_stop]
|
|
37
|
-
]
|
|
44
|
+
# Whether the machine has a keyboard. The C64GS has none: no key goes
|
|
45
|
+
# down on it.
|
|
46
|
+
def connected? = @connected
|
|
47
|
+
|
|
48
|
+
def connected=(connected)
|
|
49
|
+
@connected = connected
|
|
50
|
+
@keys.clear unless connected
|
|
51
|
+
@row_masks = nil
|
|
38
52
|
end
|
|
39
53
|
|
|
40
54
|
def press(key)
|
|
41
|
-
return unless valid_key?(key)
|
|
55
|
+
return unless @connected && valid_key?(key)
|
|
42
56
|
|
|
43
57
|
@keys << key
|
|
44
58
|
@row_masks = nil
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# = Machine
|
|
5
|
+
#
|
|
6
|
+
# The machines badline builds by family, as the command line's
|
|
7
|
+
# subcommand names them: the C64, Computer, and the VIC-20, Vic20.
|
|
8
|
+
#
|
|
9
|
+
# A machine answers what the front end, Media, the runners and the
|
|
10
|
+
# snapshots call on it:
|
|
11
|
+
#
|
|
12
|
+
# - Identity: +family+, the family it was built as.
|
|
13
|
+
# - Clocking: +run_cycles(n)+, +run_until(limit) { ... }+, +cycles+,
|
|
14
|
+
# +on_init+ and +init_threshold+, the cycle its handlers run at.
|
|
15
|
+
# - Lifecycle: +reset!+ and +power_cycle!+.
|
|
16
|
+
# - Video: +video+, the chip the screen reads (+display+, +width+,
|
|
17
|
+
# +dirty_lines+, +clear_dirty_lines!+ and +palette+), and +timing+, a
|
|
18
|
+
# Timing.
|
|
19
|
+
# - Sound: +sound_source+, which answers +record(rate:)+,
|
|
20
|
+
# +drain_samples+ and +model+.
|
|
21
|
+
# - Input: +keyboard+, +joystick1+, +joystick2+, +control_ports+,
|
|
22
|
+
# +press_restore+ and +release_restore+.
|
|
23
|
+
# - Media: +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
|
|
24
|
+
# +mounted?+, +datasette+, +attach_cartridge+,
|
|
25
|
+
# +press_cartridge_button+, +release_cartridge_button+,
|
|
26
|
+
# +attach_drive1541+, +drive1541+, and +address_bus+ for its
|
|
27
|
+
# +cartridge+ and +detach_cartridge+.
|
|
28
|
+
# - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
|
|
29
|
+
# +install_debug_register+ and +capture_output+.
|
|
30
|
+
#
|
|
31
|
+
# The VIC-20 answers the identity, clocking, lifecycle, video, sound and
|
|
32
|
+
# input parts, with its one joystick as both +joystick1+ and +joystick2+
|
|
33
|
+
# and nil for +control_ports+, and of the rest +ram+, +load_prg+,
|
|
34
|
+
# +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
|
|
35
|
+
# +attach_drive1541+, +drive1541+, +install_debug_register+ and
|
|
36
|
+
# +capture_output+.
|
|
37
|
+
module Machine
|
|
38
|
+
# A new machine of `family`, :c64 or :vic20, the model of it `model`
|
|
39
|
+
# names. A C64's model is one of Model::ALL, with its SID `sid_model`
|
|
40
|
+
# when not nil and an REU of `reu` K when not nil. A VIC-20's is "pal",
|
|
41
|
+
# with the RAM expansion `ram` names (Vic20::Bus::RAM_CONFIGURATIONS),
|
|
42
|
+
# unexpanded when nil.
|
|
43
|
+
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
44
|
+
return vic20(model, ram) if family == :vic20
|
|
45
|
+
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
46
|
+
|
|
47
|
+
profile = Model.named(model)
|
|
48
|
+
Computer.new(vic_model: profile.vic_model, cia_model: profile.cia_model,
|
|
49
|
+
sid_model: sid_model || profile.sid_model, region: profile.region, reu:,
|
|
50
|
+
kernal: profile.kernal, datasette: profile.datasette, board: profile.board)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.vic20(model, ram)
|
|
54
|
+
raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
|
|
55
|
+
|
|
56
|
+
Vic20.new(ram: ram || :unexpanded)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :vic20
|
|
59
|
+
end
|
|
60
|
+
end
|