badline 0.15.1 → 0.16.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 +55 -0
- data/CLAUDE.md +8 -7
- data/README.md +128 -9
- data/doc/media.md +9 -0
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +215 -18
- data/doc/snapshots.md +55 -0
- data/lib/badline/c64/common.rb +1 -1
- 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/vic/register_log.rb +1 -4
- data/lib/badline/core.rb +3 -0
- data/lib/badline/drives/ieee_bus.rb +74 -0
- data/lib/badline/drives/model1551/cable.rb +5 -2
- data/lib/badline/drives/model1551/tia.rb +13 -34
- data/lib/badline/drives/model1551.rb +26 -4
- data/lib/badline/drives/model2031/ieee_port.rb +133 -0
- data/lib/badline/drives/model2031/keyed_state.rb +66 -0
- data/lib/badline/drives/model2031/parts.rb +25 -0
- data/lib/badline/drives/model2031.rb +92 -0
- data/lib/badline/drives.rb +1 -0
- data/lib/badline/frontend/app.rb +1 -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 +2 -1
- data/lib/badline/frontend/menu/menu_media.rb +27 -28
- data/lib/badline/frontend/menu/menu_pages.rb +24 -2
- 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/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 +151 -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 +29 -6
- 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 +19 -6
- data/lib/badline/media/vic20_media.rb +2 -2
- data/lib/badline/media.rb +41 -17
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +23 -3
- data/lib/badline/options/table.rb +22 -4
- data/lib/badline/options/validation.rb +84 -5
- data/lib/badline/options.rb +47 -16
- data/lib/badline/pet/bus/expansion.rb +200 -0
- data/lib/badline/pet/bus.rb +222 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +32 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +155 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_drive.rb +76 -0
- data/lib/badline/pet/ieee_ports.rb +77 -0
- data/lib/badline/pet/ieee_traps.rb +80 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +88 -0
- data/lib/badline/pet/keyed_parts.rb +127 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +163 -0
- data/lib/badline/pet/rom_sets.rb +89 -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 +244 -0
- data/lib/badline/plus4/keyed_state.rb +5 -3
- data/lib/badline/plus4/tcbm_drive.rb +14 -0
- data/lib/badline/plus4.rb +1 -1
- data/lib/badline/roms/README +169 -0
- data/lib/badline/roms/dos2031.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/characters-din.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-8296.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 +6 -1
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +17 -3
- data/lib/badline/snapshot/image.rb +28 -10
- data/lib/badline/snapshot/pet_setup.rb +88 -0
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- data/lib/badline/snapshot/vice/pet.rb +117 -0
- data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
- data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
- data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
- data/lib/badline/snapshot/vice/pet_via.rb +164 -0
- data/lib/badline/snapshot/vice.rb +1 -0
- 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/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 +70 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# The traps on a PET's IEEE-488 primitives (IEEELayout), which serve
|
|
6
|
+
# device 8 from the virtual drive as Serial does on the serial bus
|
|
7
|
+
# (Frames). Other devices fall through to the ROM, which finds nobody on
|
|
8
|
+
# the bus. It is no Routine, so that Routine's layout stays a Layout.
|
|
9
|
+
#
|
|
10
|
+
# The PET's primitives differ from the serial KERNALs': TALK and LISTEN
|
|
11
|
+
# address the device in FA, not in A, and BASIC 2's LOAD sends its
|
|
12
|
+
# channel after TALK with SECOND, not TKSA, so SECOND takes a talker's
|
|
13
|
+
# channel too. BASIC 1's UNTALK and UNLISTEN store their command in FA,
|
|
14
|
+
# and its LOAD and SAVE address the device in FA after an UNLISTEN, so
|
|
15
|
+
# they find no device, as on a real PET with those ROMs.
|
|
16
|
+
# Each trap leaves A and the lines the ROM drives as the routine does.
|
|
17
|
+
class IEEE
|
|
18
|
+
include IntegerHelper
|
|
19
|
+
include Steps
|
|
20
|
+
include Frames
|
|
21
|
+
|
|
22
|
+
# The command BASIC 1's UNTALK and UNLISTEN leave in FA.
|
|
23
|
+
UNTALK_COMMAND = 0x5f
|
|
24
|
+
UNLISTEN_COMMAND = 0x3f
|
|
25
|
+
|
|
26
|
+
# The device the traps serve, or nil to serve none.
|
|
27
|
+
attr_accessor :device
|
|
28
|
+
|
|
29
|
+
def initialize(cpu:, bus:, layout:, dos:)
|
|
30
|
+
@cpu = cpu
|
|
31
|
+
@bus = bus
|
|
32
|
+
@layout = layout
|
|
33
|
+
@dos = dos
|
|
34
|
+
@device = Routine::DEVICE
|
|
35
|
+
@listening = false
|
|
36
|
+
@talking = false
|
|
37
|
+
@listen_channel = nil
|
|
38
|
+
@talk_channel = nil
|
|
39
|
+
@frame = OPEN
|
|
40
|
+
@buffer = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def talk
|
|
46
|
+
@talk_channel = nil
|
|
47
|
+
@talking = addressed?
|
|
48
|
+
address_device if @talking
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def listen
|
|
52
|
+
@listen_channel = nil
|
|
53
|
+
@listening = addressed?
|
|
54
|
+
address_device if @listening
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def addressed? = @bus.peek(zero_page.device) == @device
|
|
58
|
+
|
|
59
|
+
# TALK and LISTEN end with the command sent under ATN, which stays
|
|
60
|
+
# low, and A holding $FF.
|
|
61
|
+
def address_device
|
|
62
|
+
@layout.attention(@bus)
|
|
63
|
+
send_byte
|
|
64
|
+
@cpu.a = 0xff
|
|
65
|
+
set_flags
|
|
66
|
+
return_to_caller
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A close needs no data, and the drive closes the channel as the
|
|
70
|
+
# secondary address comes: BASIC 1's CLOSE sends no UNLISTEN after it.
|
|
71
|
+
def second
|
|
72
|
+
if @listening
|
|
73
|
+
@frame = @cpu.a & 0xf0
|
|
74
|
+
@listen_channel = @cpu.a & 0x0f
|
|
75
|
+
@buffer = []
|
|
76
|
+
close_channel if @frame == CLOSE
|
|
77
|
+
elsif @talking
|
|
78
|
+
@talk_channel = @cpu.a & 0x0f
|
|
79
|
+
else
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
send_byte
|
|
83
|
+
release_attention
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def tksa
|
|
87
|
+
return unless @talking
|
|
88
|
+
|
|
89
|
+
@talk_channel = @cpu.a & 0x0f
|
|
90
|
+
send_byte
|
|
91
|
+
@layout.listen_to_talker(@bus)
|
|
92
|
+
release_attention
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def untalk
|
|
96
|
+
return unless @talking
|
|
97
|
+
|
|
98
|
+
@talking = false
|
|
99
|
+
@bus.poke(zero_page.device, UNTALK_COMMAND) if @layout.overwrites_device
|
|
100
|
+
time_serial_byte(Routine::ISOUR_TIMEOUT)
|
|
101
|
+
release_bus
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def unlisten
|
|
105
|
+
return unless @listening
|
|
106
|
+
|
|
107
|
+
@listening = false
|
|
108
|
+
@bus.poke(zero_page.device, UNLISTEN_COMMAND) if @layout.overwrites_device
|
|
109
|
+
deliver_frame
|
|
110
|
+
time_serial_byte(Routine::ISOUR_TIMEOUT)
|
|
111
|
+
release_bus
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ACPTR leaves the byte in A, with the flags PLA gives it, and the PET
|
|
115
|
+
# listening, NRFD and NDAC pulled low.
|
|
116
|
+
def acptr
|
|
117
|
+
return unless @talking
|
|
118
|
+
|
|
119
|
+
@layout.listen_to_talker(@bus)
|
|
120
|
+
byte, status = received
|
|
121
|
+
time_serial_byte(Routine::ACPTR_TIMEOUT)
|
|
122
|
+
@cpu.a = byte
|
|
123
|
+
set_flags
|
|
124
|
+
update_status(status)
|
|
125
|
+
@cpu.status.carry = false
|
|
126
|
+
return_to_caller
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def close_channel
|
|
130
|
+
@dos.close(@listen_channel)
|
|
131
|
+
@listen_channel = nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def send_byte
|
|
135
|
+
time_serial_byte(Routine::ISOUR_TIMEOUT)
|
|
136
|
+
@layout.byte_sent(@bus)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def release_attention
|
|
140
|
+
@cpu.a = @layout.release_attention(@bus)
|
|
141
|
+
set_flags
|
|
142
|
+
return_to_caller
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def set_flags
|
|
146
|
+
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
147
|
+
@cpu.status.zero = @cpu.a.zero?
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# Where a PET KERNAL keeps the eight IEEE-488 bus primitives, which are
|
|
6
|
+
# internal routines, not jump table entries, and the ZeroPage of its
|
|
7
|
+
# file variables. Each ROM set has its own.
|
|
8
|
+
#
|
|
9
|
+
# TALK and LISTEN take no device number in A: they send $40 or $20
|
|
10
|
+
# ORed with FA. UNTALK and UNLISTEN send $5F and $3F through the same
|
|
11
|
+
# code, and SECOND, TKSA, UNTALK and UNLISTEN end by letting go of ATN
|
|
12
|
+
# (VIA PB2) themselves. Each handshake times out on VIA timer 1, which
|
|
13
|
+
# the ROM starts with a write of $FF to $E845. LOAD and SAVE run in the
|
|
14
|
+
# ROM, through the primitives. +overwrites_device+ says UNTALK and
|
|
15
|
+
# UNLISTEN store their command in FA before they send it.
|
|
16
|
+
#
|
|
17
|
+
# The ROM drives the bus's control lines from three chips: ATN from VIA
|
|
18
|
+
# PB2 and NRFD from PB1 at $E840, NDAC from PIA 2's CA2 and DAV from its
|
|
19
|
+
# CB2, through the control registers at $E821 and $E823, which the ROM
|
|
20
|
+
# writes $34 to pull a line low and $3C to let it go, and the data lines
|
|
21
|
+
# from PIA 2's port B at $E822. The steps below leave the lines as each
|
|
22
|
+
# primitive leaves them.
|
|
23
|
+
IEEELayout = Data.define(:talk, :listen, :second, :tksa, :ciout, :untalk, :unlisten, :acptr, :overwrites_device,
|
|
24
|
+
:zero_page) do
|
|
25
|
+
# The KERNAL is mapped unless an 8096's board lies over it.
|
|
26
|
+
def kernal_mapped?(bus) = bus.kernal_mapped?
|
|
27
|
+
|
|
28
|
+
# Starts VIA timer 1, which times out the handshake of each byte.
|
|
29
|
+
def time_serial_byte(bus, _timer_high)
|
|
30
|
+
bus.poke(0xe845, 0xff)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# TALK and LISTEN: NRFD and NDAC let go, then ATN pulled low.
|
|
34
|
+
def attention(bus)
|
|
35
|
+
bus.poke(0xe840, bus.peek(0xe840) | 0x02)
|
|
36
|
+
bus.poke(0xe821, 0x3c)
|
|
37
|
+
bus.poke(0xe840, bus.peek(0xe840) & 0xfb)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The end of a byte sent: DAV and the data lines let go.
|
|
41
|
+
def byte_sent(bus)
|
|
42
|
+
bus.poke(0xe823, 0x3c)
|
|
43
|
+
bus.poke(0xe822, 0xff)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The end of SECOND, TKSA, UNTALK and UNLISTEN: ATN let go. Returns
|
|
47
|
+
# the value written, which the ROM leaves in A.
|
|
48
|
+
def release_attention(bus)
|
|
49
|
+
value = bus.peek(0xe840) | 0x04
|
|
50
|
+
bus.poke(0xe840, value)
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# TKSA and ACPTR end listening: NRFD and NDAC pulled low.
|
|
55
|
+
def listen_to_talker(bus)
|
|
56
|
+
bus.poke(0xe840, bus.peek(0xe840) & 0xfd)
|
|
57
|
+
bus.poke(0xe821, 0x34)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The end of UNTALK and UNLISTEN, which address the bus as TALK and
|
|
61
|
+
# LISTEN do, send their command and let ATN go.
|
|
62
|
+
def release_serial_lines(bus)
|
|
63
|
+
attention(bus)
|
|
64
|
+
byte_sent(bus)
|
|
65
|
+
release_attention(bus)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The file variables of the BASIC 2 and BASIC 4 KERNALs, which keep
|
|
70
|
+
# them at the same addresses. SAVE copies STAL at $FB to SAL at $C7 and
|
|
71
|
+
# walks SAL up to EAL at $C9. LOAD keeps no MEMUSS: it stores each byte
|
|
72
|
+
# at STAL, which starts at the file's own address, and ends with EAL
|
|
73
|
+
# set from it, so +load_address+ names STAL.
|
|
74
|
+
PET_ZERO_PAGE = ZeroPage.new(
|
|
75
|
+
status: 0x96, verify: 0x9d, start_address: 0xc7, end_address: 0xc9, filename_length: 0xd1,
|
|
76
|
+
secondary_address: 0xd3, device: 0xd4, filename_address: 0xda, save_start: 0xfb, load_address: 0xfb
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# BASIC 1's, some of them outside the zero page: ST at $020C and VERCK
|
|
80
|
+
# at $020B. SAVE copies STAL at $F7 to SAL at $E3 and walks SAL up to
|
|
81
|
+
# EAL at $E5. LOAD from the IEEE bus stores each byte at STAL, which it
|
|
82
|
+
# sets to $0400 itself.
|
|
83
|
+
PET_BASIC1_ZERO_PAGE = ZeroPage.new(
|
|
84
|
+
status: 0x020c, verify: 0x020b, start_address: 0xe3, end_address: 0xe5, filename_length: 0xee,
|
|
85
|
+
secondary_address: 0xf0, device: 0xf1, filename_address: 0xf9, save_start: 0xf7, load_address: 0xf7
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# BASIC 1, 901447-06 and 901447-07. Its UNTALK and UNLISTEN store $5F
|
|
89
|
+
# and $3F in FA at $F1 before they send it. LOAD and SAVE send TALK and
|
|
90
|
+
# LISTEN without a secondary address.
|
|
91
|
+
PET_BASIC1_LAYOUT = IEEELayout.new(
|
|
92
|
+
talk: 0xf0b6, listen: 0xf0ba, second: 0xf12c, tksa: 0xf15b,
|
|
93
|
+
ciout: 0xf167, untalk: 0xf17a, unlisten: 0xf17e, acptr: 0xf187, overwrites_device: true,
|
|
94
|
+
zero_page: PET_BASIC1_ZERO_PAGE
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# BASIC 2, 901465-03. Its LOAD sends the secondary address after TALK
|
|
98
|
+
# with SECOND, not TKSA.
|
|
99
|
+
PET_BASIC2_LAYOUT = IEEELayout.new(
|
|
100
|
+
talk: 0xf0b6, listen: 0xf0ba, second: 0xf128, tksa: 0xf164,
|
|
101
|
+
ciout: 0xf16f, untalk: 0xf17f, unlisten: 0xf183, acptr: 0xf18c, overwrites_device: false,
|
|
102
|
+
zero_page: PET_ZERO_PAGE
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# BASIC 4, 901465-22. LISTEN at $F0D5 is the operand of the BIT
|
|
106
|
+
# instruction that skips it on the way from TALK.
|
|
107
|
+
PET_BASIC4_LAYOUT = IEEELayout.new(
|
|
108
|
+
talk: 0xf0d2, listen: 0xf0d5, second: 0xf143, tksa: 0xf193,
|
|
109
|
+
ciout: 0xf19e, untalk: 0xf1ae, unlisten: 0xf1b9, acptr: 0xf1c0, overwrites_device: false,
|
|
110
|
+
zero_page: PET_ZERO_PAGE
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -117,10 +117,10 @@ module Badline
|
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
# The serial trap's fields under `prefix`: whether device
|
|
122
|
-
# or talks, on which channel, the frame it is in and the
|
|
123
|
-
# gathered.
|
|
120
|
+
module Frames
|
|
121
|
+
# The serial or IEEE-488 trap's fields under `prefix`: whether device
|
|
122
|
+
# 8 listens or talks, on which channel, the frame it is in and the
|
|
123
|
+
# bytes it has gathered.
|
|
124
124
|
def save_fields(out, prefix)
|
|
125
125
|
out.bool("#{prefix}listening", @listening).bool("#{prefix}talking", @talking)
|
|
126
126
|
out.optional_int("#{prefix}listen_channel", @listen_channel)
|
|
@@ -3,10 +3,11 @@
|
|
|
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
|
-
#
|
|
6
|
+
# number they answer for and the layout of the KERNAL they trap, and
|
|
7
|
+
# takes its Steps around them.
|
|
8
8
|
class Routine
|
|
9
9
|
include IntegerHelper
|
|
10
|
+
include Steps
|
|
10
11
|
|
|
11
12
|
DEVICE = 8
|
|
12
13
|
|
|
@@ -19,57 +20,6 @@ module Badline
|
|
|
19
20
|
@bus = bus
|
|
20
21
|
@layout = layout
|
|
21
22
|
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def kernal_mapped?
|
|
26
|
-
@layout.kernal_mapped?(@bus)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def zero_page = @layout.zero_page
|
|
30
|
-
|
|
31
|
-
# The two-byte value whose low byte is at +address+.
|
|
32
|
-
def peek_word(address)
|
|
33
|
-
uint16(@bus.peek(address), @bus.peek(address + 1))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def poke_word(address, value)
|
|
37
|
-
@bus.poke(address, low_byte(value))
|
|
38
|
-
@bus.poke(address + 1, high_byte(value))
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# The ROM times each byte on the serial bus with a one-shot timer
|
|
42
|
-
# (Layout#time_serial_byte): ISOUR_TIMEOUT for a byte sent,
|
|
43
|
-
# ACPTR_TIMEOUT for one received.
|
|
44
|
-
def time_serial_byte(timer_high)
|
|
45
|
-
@layout.time_serial_byte(@bus, timer_high)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def return_to_caller
|
|
49
|
-
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Runs the ROM routines in order, each returning into the next, and
|
|
53
|
-
# the last one to the trapped routine's caller
|
|
54
|
-
def continue_with(first, *rest)
|
|
55
|
-
rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
|
|
56
|
-
@cpu.program_counter = first
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def push_address(address)
|
|
60
|
-
push_byte(high_byte(address))
|
|
61
|
-
push_byte(low_byte(address))
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def push_byte(value)
|
|
65
|
-
@bus.poke(0x0100 + @cpu.stack_pointer, value)
|
|
66
|
-
@cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def pull_byte
|
|
70
|
-
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
71
|
-
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
72
|
-
end
|
|
73
23
|
end
|
|
74
24
|
end
|
|
75
25
|
end
|
|
@@ -5,23 +5,15 @@ module Badline
|
|
|
5
5
|
# PC traps on the KERNAL's serial bus primitives. Frames addressed to
|
|
6
6
|
# device 8 go to a virtual drive instead of out over the IEC lines, so
|
|
7
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
|
|
8
|
+
# loaders make through them (Frames). Other devices fall through to the
|
|
9
|
+
# ROM.
|
|
9
10
|
#
|
|
10
11
|
# With +device+ nil the traps answer no device, and every frame goes
|
|
11
12
|
# out over the bus, to a true drive if one is plugged in.
|
|
12
13
|
class Serial < Routine
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Frame type in the high nibble of the secondary address
|
|
16
|
-
OPEN = 0xf0
|
|
17
|
-
CLOSE = 0xe0
|
|
18
|
-
|
|
19
|
-
# ST bits at $90
|
|
20
|
-
DEVICE_NOT_PRESENT = 0x80
|
|
21
|
-
EOI = 0x40
|
|
22
|
-
READ_TIMEOUT = 0x02
|
|
14
|
+
include Frames
|
|
23
15
|
|
|
24
|
-
|
|
16
|
+
attr_accessor :device
|
|
25
17
|
|
|
26
18
|
def initialize(cpu:, bus:, layout:, dos:, device: DEVICE)
|
|
27
19
|
super(cpu:, bus:, layout:)
|
|
@@ -35,46 +27,8 @@ module Badline
|
|
|
35
27
|
@buffer = []
|
|
36
28
|
end
|
|
37
29
|
|
|
38
|
-
def install
|
|
39
|
-
routines.each do |address, routine|
|
|
40
|
-
@cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
|
|
41
|
-
end
|
|
42
|
-
self
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Takes the traps out again, so the KERNAL's own routines run.
|
|
46
|
-
def uninstall
|
|
47
|
-
routines.each_key { |address| @cpu.remove_trap(address) }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
30
|
private
|
|
51
31
|
|
|
52
|
-
def routines
|
|
53
|
-
{
|
|
54
|
-
@layout.talk => :talk,
|
|
55
|
-
@layout.listen => :listen,
|
|
56
|
-
@layout.second => :second,
|
|
57
|
-
@layout.tksa => :tksa,
|
|
58
|
-
@layout.ciout => :ciout,
|
|
59
|
-
@layout.untalk => :untalk,
|
|
60
|
-
@layout.unlisten => :unlisten,
|
|
61
|
-
@layout.acptr => :acptr
|
|
62
|
-
}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def call_routine(routine)
|
|
66
|
-
case routine
|
|
67
|
-
when :talk then talk
|
|
68
|
-
when :listen then listen
|
|
69
|
-
when :second then second
|
|
70
|
-
when :tksa then tksa
|
|
71
|
-
when :ciout then ciout
|
|
72
|
-
when :untalk then untalk
|
|
73
|
-
when :unlisten then unlisten
|
|
74
|
-
when :acptr then acptr
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
32
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
79
33
|
# in the secondary address that follows. Each byte sent under ATN,
|
|
80
34
|
# and each byte of a frame, times its handshake with the serial timer
|
|
@@ -112,25 +66,6 @@ module Badline
|
|
|
112
66
|
sent_under_atn
|
|
113
67
|
end
|
|
114
68
|
|
|
115
|
-
# CIOUT holds each byte back until the next one, or the UNLISTEN,
|
|
116
|
-
# comes, so the first byte of a frame sends nothing.
|
|
117
|
-
def ciout
|
|
118
|
-
return unless @listening
|
|
119
|
-
|
|
120
|
-
time_serial_byte(ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
|
|
121
|
-
@buffer << @cpu.a
|
|
122
|
-
update_status(DEVICE_NOT_PRESENT) unless accepted?
|
|
123
|
-
@cpu.status.carry = false
|
|
124
|
-
return_to_caller
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# The drive acknowledges every byte of an open or close frame, but
|
|
128
|
-
# takes data only on a channel it has open. Otherwise nothing holds
|
|
129
|
-
# the data line when a byte starts, so the KERNAL finds no device.
|
|
130
|
-
def accepted?
|
|
131
|
-
[OPEN, CLOSE].include?(@frame) || @dos.listening?(@listen_channel)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
69
|
def unlisten
|
|
135
70
|
return unless @listening
|
|
136
71
|
|
|
@@ -148,15 +83,6 @@ module Badline
|
|
|
148
83
|
release_bus
|
|
149
84
|
end
|
|
150
85
|
|
|
151
|
-
# UNLSN and UNTLK end by releasing ATN, the clock and the data line,
|
|
152
|
-
# and leave the port's last read in the accumulator.
|
|
153
|
-
def release_bus
|
|
154
|
-
@cpu.a = @layout.release_serial_lines(@bus)
|
|
155
|
-
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
156
|
-
@cpu.status.zero = @cpu.a.zero?
|
|
157
|
-
return_to_caller
|
|
158
|
-
end
|
|
159
|
-
|
|
160
86
|
def acptr
|
|
161
87
|
return unless @talking
|
|
162
88
|
|
|
@@ -167,30 +93,6 @@ module Badline
|
|
|
167
93
|
@cpu.status.carry = false
|
|
168
94
|
return_to_caller
|
|
169
95
|
end
|
|
170
|
-
|
|
171
|
-
def deliver_frame
|
|
172
|
-
return unless @listen_channel
|
|
173
|
-
|
|
174
|
-
case @frame
|
|
175
|
-
when OPEN then @dos.open(@listen_channel, @buffer.pack("C*"))
|
|
176
|
-
when CLOSE then @dos.close(@listen_channel)
|
|
177
|
-
else @dos.write(@listen_channel, @buffer)
|
|
178
|
-
end
|
|
179
|
-
@buffer = []
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def received
|
|
183
|
-
byte, eoi = @dos.read(@talk_channel)
|
|
184
|
-
return NO_DATA unless byte
|
|
185
|
-
|
|
186
|
-
[byte, eoi ? EOI : 0]
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def update_status(bits)
|
|
190
|
-
return if bits.zero?
|
|
191
|
-
|
|
192
|
-
@bus.poke(zero_page.status, @bus.peek(zero_page.status) | bits)
|
|
193
|
-
end
|
|
194
96
|
end
|
|
195
97
|
end
|
|
196
98
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# The steps the PC traps on KERNAL routines share, on the @cpu, @bus
|
|
6
|
+
# and @layout of the trap that includes them, which includes
|
|
7
|
+
# IntegerHelper too: the layout's checks and timer, the zero page's
|
|
8
|
+
# words, and the return that stands in for the trapped routine's RTS.
|
|
9
|
+
# Routine includes them, and so does each trap that isn't a Routine,
|
|
10
|
+
# so every trap class keeps its own layout's type.
|
|
11
|
+
module Steps
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def kernal_mapped?
|
|
15
|
+
@layout.kernal_mapped?(@bus)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def zero_page = @layout.zero_page
|
|
19
|
+
|
|
20
|
+
# The two-byte value whose low byte is at +address+.
|
|
21
|
+
def peek_word(address)
|
|
22
|
+
uint16(@bus.peek(address), @bus.peek(address + 1))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def poke_word(address, value)
|
|
26
|
+
@bus.poke(address, low_byte(value))
|
|
27
|
+
@bus.poke(address + 1, high_byte(value))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The ROM times each byte on the serial bus with a one-shot timer
|
|
31
|
+
# (Layout#time_serial_byte): Routine::ISOUR_TIMEOUT for a byte sent,
|
|
32
|
+
# Routine::ACPTR_TIMEOUT for one received.
|
|
33
|
+
def time_serial_byte(timer_high)
|
|
34
|
+
@layout.time_serial_byte(@bus, timer_high)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def return_to_caller
|
|
38
|
+
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Runs the ROM routines in order, each returning into the next, and
|
|
42
|
+
# the last one to the trapped routine's caller
|
|
43
|
+
def continue_with(first, *rest)
|
|
44
|
+
rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
|
|
45
|
+
@cpu.program_counter = first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def push_address(address)
|
|
49
|
+
push_byte(high_byte(address))
|
|
50
|
+
push_byte(low_byte(address))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def push_byte(value)
|
|
54
|
+
@bus.poke(0x0100 + @cpu.stack_pointer, value)
|
|
55
|
+
@cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pull_byte
|
|
59
|
+
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
60
|
+
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/badline/kernal_trap.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/kernal_trap/layout"
|
|
4
|
+
require "badline/kernal_trap/ieee_layout"
|
|
5
|
+
require "badline/kernal_trap/steps"
|
|
4
6
|
require "badline/kernal_trap/routine"
|
|
5
7
|
require "badline/kernal_trap/dos"
|
|
6
8
|
require "badline/kernal_trap/file_routine"
|
|
7
9
|
require "badline/kernal_trap/load"
|
|
8
10
|
require "badline/kernal_trap/save"
|
|
11
|
+
require "badline/kernal_trap/frames"
|
|
9
12
|
require "badline/kernal_trap/serial"
|
|
13
|
+
require "badline/kernal_trap/ieee"
|
|
10
14
|
require "badline/kernal_trap/chrout"
|
|
11
15
|
require "badline/kernal_trap/keyed_state"
|
|
@@ -20,6 +20,14 @@ module Badline
|
|
|
20
20
|
# ($D86E).
|
|
21
21
|
PLUS4_LAYOUT = Layout.new(address: 0x0527, count: 0xef, capacity: 10)
|
|
22
22
|
|
|
23
|
+
# The PET's, under BASIC 2 and BASIC 4. Every editor's GETIN takes
|
|
24
|
+
# ten keys from it, though the CRTC editors' keyboard scans stop at
|
|
25
|
+
# nine (XMAX).
|
|
26
|
+
PET_LAYOUT = Layout.new(address: 0x026f, count: 0x9e, capacity: 10)
|
|
27
|
+
|
|
28
|
+
# The PET's under BASIC 1, outside the zero page.
|
|
29
|
+
PET_BASIC1_LAYOUT = Layout.new(address: 0x020f, count: 0x020d, capacity: 10)
|
|
30
|
+
|
|
23
31
|
def type_text(text)
|
|
24
32
|
codes = text.bytes.map { |b| ascii_to_petscii(b) }
|
|
25
33
|
on_init { @pending_keys = codes }
|
|
@@ -4,7 +4,7 @@ module Badline
|
|
|
4
4
|
module Machine
|
|
5
5
|
# Brings the machine to the point a snapshot file is saved at: the main
|
|
6
6
|
# CPU between instructions, or jammed, with no REU transfer holding the
|
|
7
|
-
# bus. C64, Vic20, C128 and
|
|
7
|
+
# bus. C64, Vic20, C128, Plus4 and PET each include it, and answer
|
|
8
8
|
# #settled_for_snapshot? and #settle_chips_for_snapshot, which brings
|
|
9
9
|
# what each chip defers up to the cycle without running one.
|
|
10
10
|
module Settling
|
data/lib/badline/machine.rb
CHANGED
|
@@ -8,8 +8,9 @@ module Badline
|
|
|
8
8
|
# = Machine
|
|
9
9
|
#
|
|
10
10
|
# The machines badline builds by family, as the command line's
|
|
11
|
-
# subcommand names them: the C64, Badline::C64, the VIC-20, Vic20,
|
|
12
|
-
# C128 in C64 mode, C128,
|
|
11
|
+
# subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, the
|
|
12
|
+
# C128 in C64 mode, C128, the 264 series, Plus4, and the PET and CBM,
|
|
13
|
+
# PET.
|
|
13
14
|
#
|
|
14
15
|
# A machine answers what the front end, Media, the runners and the
|
|
15
16
|
# snapshots call on it:
|
|
@@ -57,23 +58,31 @@ module Badline
|
|
|
57
58
|
# and +capture_output+, and besides +model_label+, +cartridge_slots+ and
|
|
58
59
|
# +detach_cartridges+ for its two cartridges.
|
|
59
60
|
#
|
|
61
|
+
# The PET answers the identity, clocking, lifecycle, video, sound and
|
|
62
|
+
# keyboard parts, with no joysticks, RESTORE or control ports, and of
|
|
63
|
+
# the rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
|
|
64
|
+
# +mounted?+, +datasette+, +true_drive+, nil, +install_debug_register+
|
|
65
|
+
# and +capture_output+, and besides +model_label+ and +ram_kb+.
|
|
66
|
+
#
|
|
60
67
|
# Each machine includes Machine::Clocking for the clocking part.
|
|
61
68
|
module Machine
|
|
62
69
|
# Media the machine named doesn't take.
|
|
63
70
|
class Unsupported < ArgumentError; end
|
|
64
71
|
|
|
65
|
-
# A new machine of `family`, :c64, :vic20, :c128 or :
|
|
66
|
-
# of it `model` names. A C64's model is one of Model::ALL, with its SID
|
|
72
|
+
# A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
|
|
73
|
+
# model of it `model` names. A C64's model is one of Model::ALL, with its SID
|
|
67
74
|
# `sid_model` when not nil and an REU of `reu` K when not nil. A
|
|
68
75
|
# VIC-20's is "pal", with the RAM expansion `ram` names
|
|
69
76
|
# (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
|
|
70
77
|
# one of C128::Model::ALL, with its SID `sid_model` when not nil, built
|
|
71
78
|
# for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
|
|
72
|
-
# the model's own RAM.
|
|
79
|
+
# the model's own RAM. A PET's is one of PET::Model::ALL, with its own
|
|
80
|
+
# RAM and ROMs, its CRTC's editor the 50 Hz one.
|
|
73
81
|
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
74
82
|
return vic20(model, ram) if family == :vic20
|
|
75
83
|
return c128(model, sid_model, :c64) if family == :c128
|
|
76
84
|
return Plus4.new(model:) if family == :plus4
|
|
85
|
+
return PET.new(model:) if family == :pet
|
|
77
86
|
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
78
87
|
|
|
79
88
|
profile = Model.named(model)
|
|
@@ -89,11 +98,12 @@ module Badline
|
|
|
89
98
|
# only C64 mode runs: a .sid tune or a program that loads at the C64's
|
|
90
99
|
# BASIC start. A VIC-20 without RAM named gets the expansion the media
|
|
91
100
|
# needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
|
|
92
|
-
# Unsupported for media a VIC-20
|
|
101
|
+
# Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
|
|
93
102
|
def self.for_options(options, media, &)
|
|
94
103
|
family = options.family
|
|
95
104
|
return vic20_for(media, options.model, options.ram, &) if family == :vic20
|
|
96
105
|
return plus4_for(media, options) if family == :plus4
|
|
106
|
+
return pet_for(media, options) if family == :pet
|
|
97
107
|
|
|
98
108
|
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
99
109
|
return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
|
|
@@ -141,6 +151,19 @@ module Badline
|
|
|
141
151
|
end
|
|
142
152
|
private_class_method :plus4_for
|
|
143
153
|
|
|
154
|
+
# A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
|
|
155
|
+
# model's own without it, the editor at the frame rate --60hz or
|
|
156
|
+
# --editor asks for, the ROMs --basic, --editor and --characters fit it
|
|
157
|
+
# with, and the 8296 board's jumpers --jumpers names.
|
|
158
|
+
def self.pet_for(media, options)
|
|
159
|
+
unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
160
|
+
raise Unsupported, "doesn't go in a PET" if unsupported
|
|
161
|
+
|
|
162
|
+
PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
|
|
163
|
+
editor: options.editor, characters: options.characters, jumpers: options.jumpers)
|
|
164
|
+
end
|
|
165
|
+
private_class_method :pet_for
|
|
166
|
+
|
|
144
167
|
def self.c64_media?(media)
|
|
145
168
|
return false if media.nil? || !File.file?(media)
|
|
146
169
|
|