badline 0.10.1 → 0.11.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 +15 -0
- data/CLAUDE.md +5 -3
- data/README.md +59 -4
- data/doc/pinned-behaviour.md +75 -0
- data/doc/snapshots.md +15 -0
- data/lib/badline/address_bus/pla.rb +3 -16
- data/lib/badline/address_bus.rb +14 -0
- data/lib/badline/c128/banks.rb +35 -0
- data/lib/badline/c128/bus.rb +347 -0
- data/lib/badline/c128/bus_state.rb +53 -0
- data/lib/badline/c128/color_lines.rb +39 -0
- data/lib/badline/c128/cpu.rb +10 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +182 -0
- data/lib/badline/c128/mmu_pages.rb +105 -0
- data/lib/badline/c128/model.rb +39 -0
- data/lib/badline/c128/modes.rb +48 -0
- data/lib/badline/c128/saved_state.rb +120 -0
- data/lib/badline/c128/vdc/memory.rb +54 -0
- data/lib/badline/c128/vdc/painter.rb +143 -0
- data/lib/badline/c128/vdc/raster.rb +97 -0
- data/lib/badline/c128/vdc/window.rb +215 -0
- data/lib/badline/c128/vdc.rb +268 -0
- data/lib/badline/c128.rb +320 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/computer/attachments.rb +4 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +5 -0
- data/lib/badline/frontend/app.rb +34 -21
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_pages.rb +6 -1
- data/lib/badline/frontend/menu_ports.rb +1 -1
- data/lib/badline/frontend/pause_menu.rb +1 -0
- data/lib/badline/frontend/screen.rb +33 -8
- data/lib/badline/frontend/screens.rb +90 -0
- data/lib/badline/frontend/timeline.rb +15 -4
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +58 -8
- data/lib/badline/kernal_trap/load.rb +1 -1
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +5 -0
- data/lib/badline/machine.rb +15 -6
- data/lib/badline/media.rb +8 -6
- data/lib/badline/options/event.rb +5 -1
- data/lib/badline/options/family.rb +52 -0
- data/lib/badline/options/help.rb +5 -1
- data/lib/badline/options/table.rb +7 -1
- data/lib/badline/options/validation.rb +14 -1
- data/lib/badline/options.rb +20 -19
- data/lib/badline/roms/README +27 -0
- data/lib/badline/roms/c128/basic.rom +0 -0
- data/lib/badline/roms/c128/character.rom +0 -0
- data/lib/badline/roms/c128/kernal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +14 -0
- data/lib/badline/snapshot.rb +9 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/display_state.rb +12 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic.rb +108 -7
- data/lib/badline.rb +1 -0
- metadata +24 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The 8502's bus over the 128K of RAM, as the MMU maps it.
|
|
6
|
+
#
|
|
7
|
+
# In C128 mode CR picks what the CPU sees: BASIC at $4000-$BFFF, the
|
|
8
|
+
# screen editor, the character ROM and the KERNAL at $C000-$FFFF, an
|
|
9
|
+
# empty function ROM socket, or RAM, and I/O at $D000-$DFFF. The MMU's
|
|
10
|
+
# registers sit at $D500 and its configuration registers at
|
|
11
|
+
# $FF00-$FF04.
|
|
12
|
+
#
|
|
13
|
+
# In C64 mode the 8721 PLA decodes $01's LORAM, HIRAM and CHAREN and the
|
|
14
|
+
# cartridge's EXROM and GAME as a C64's does (AddressBus::PLA), and the
|
|
15
|
+
# MMU answers nowhere.
|
|
16
|
+
#
|
|
17
|
+
# In both modes the RAM is the bank CR picks, with common RAM in bank 0,
|
|
18
|
+
# and a page that page 0 or page 1 moves to reaches page 0 or page 1 of
|
|
19
|
+
# that bank. C128 mode also moves page 0 and page 1 there. The I/O area:
|
|
20
|
+
#
|
|
21
|
+
# $D000-$D3FF - the VIC-IIe
|
|
22
|
+
# $D400-$D4FF - the SID, with no mirrors above it
|
|
23
|
+
# $D500-$D5FF - the MMU in C128 mode, nothing in C64 mode
|
|
24
|
+
# $D600-$D6FF - the VDC
|
|
25
|
+
# $D700-$D7FF - nothing, but for VICE's debug register at $D7FF
|
|
26
|
+
# $D800-$DBFF - colour RAM
|
|
27
|
+
# $DC00-$DCFF - CIA 1
|
|
28
|
+
# $DD00-$DDFF - CIA 2
|
|
29
|
+
# $DE00-$DFFF - I/O 1 and 2, for cartridges
|
|
30
|
+
#
|
|
31
|
+
# An empty page reads the byte the VIC fetched in the preceding phi1
|
|
32
|
+
# half-cycle, as I/O 1 and 2 do on the C64.
|
|
33
|
+
#
|
|
34
|
+
# Colour RAM has two 1K banks (ColorLines).
|
|
35
|
+
class Bus
|
|
36
|
+
include Addressable
|
|
37
|
+
include AddressBus::PLA
|
|
38
|
+
include AddressBus::ROMs
|
|
39
|
+
include MMUPages
|
|
40
|
+
include SavedState
|
|
41
|
+
|
|
42
|
+
# The 8502's port has seven pins. P0-P5 are the 6510's, and P6 senses
|
|
43
|
+
# the CAPS LOCK key, high while it is up. Bit 7 has no pin, and holds
|
|
44
|
+
# the charge last driven onto it, as the 6510's floating bits do.
|
|
45
|
+
PORT_PULLUPS = 0b0001_0111
|
|
46
|
+
PORT_FLOATING = 0b1000_1000
|
|
47
|
+
CAPS_LOCK = 0b0100_0000
|
|
48
|
+
TAPE_SENSE = 0b0001_0000
|
|
49
|
+
|
|
50
|
+
# The C64's power-on pattern (AddressBus::RAM_POWER_ON) in both 64K
|
|
51
|
+
# banks. The C128's own DRAMs' pattern is unmeasured.
|
|
52
|
+
RAM_POWER_ON = Array.new(2**17) { |addr| AddressBus::RAM_POWER_ON[addr & 0xffff] }.freeze
|
|
53
|
+
|
|
54
|
+
# The VIC's write side: a write to $D02F also drives the extra
|
|
55
|
+
# keyboard lines K0-K2 onto the keyboard's rows 8-10.
|
|
56
|
+
class VICWrites
|
|
57
|
+
def initialize(vic, control_ports)
|
|
58
|
+
@vic = vic
|
|
59
|
+
@control_ports = control_ports
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def peek(addr) = @vic.peek(addr)
|
|
63
|
+
|
|
64
|
+
def poke(addr, value)
|
|
65
|
+
@vic.poke(addr, value)
|
|
66
|
+
@control_ports.extra_rows = 0xf8 | @vic.extra_keyboard_lines if (addr & 0x3f) == 0x2f
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The $D7xx page with VICE's debug register at $D7FF: a write there
|
|
71
|
+
# hands the handler its byte, and the rest of the page is empty.
|
|
72
|
+
class DebugPage
|
|
73
|
+
ADDRESS = 0xd7ff
|
|
74
|
+
|
|
75
|
+
def initialize(open_bus, &handler)
|
|
76
|
+
@open_bus = open_bus
|
|
77
|
+
@handler = handler
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def peek(addr) = @open_bus.peek(addr)
|
|
81
|
+
|
|
82
|
+
def poke(addr, value)
|
|
83
|
+
@handler.call(value) if addr == ADDRESS
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# RAM seen somewhere else: the address's bits in +mask+ from +base+
|
|
88
|
+
# on. Bank 1 is the whole 64K from $10000, and a relocated page the
|
|
89
|
+
# low byte from the page it moved to.
|
|
90
|
+
class RAMWindow
|
|
91
|
+
attr_accessor :base
|
|
92
|
+
|
|
93
|
+
def initialize(ram, base, mask)
|
|
94
|
+
@ram = ram
|
|
95
|
+
@base = base
|
|
96
|
+
@mask = mask
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def peek(addr) = @ram.peek(@base | (addr & @mask))
|
|
100
|
+
def poke(addr, value) = @ram.poke(@base | (addr & @mask), value)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The page at $FF00 in C128 mode: CR and the LCRs at $FF00-$FF04, and
|
|
104
|
+
# whatever CR maps there above them.
|
|
105
|
+
class ConfigurationPage
|
|
106
|
+
attr_accessor :below
|
|
107
|
+
|
|
108
|
+
def initialize(mmu, below)
|
|
109
|
+
@mmu = mmu
|
|
110
|
+
@below = below
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def peek(addr) = addr < 0xff05 ? @mmu.peek_configuration(addr) : @below.peek(addr)
|
|
114
|
+
|
|
115
|
+
def poke(addr, value)
|
|
116
|
+
addr < 0xff05 ? @mmu.poke_configuration(addr, value) : @below.poke(addr, value)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
attr_reader :io_port, :ram, :mmu, :vic, :sid, :vdc, :color_ram, :cia1, :cia2, :keyboard, :joystick1,
|
|
121
|
+
:joystick2, :control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette, :region
|
|
122
|
+
|
|
123
|
+
# The C128 mode ROMs: BASIC's two halves, the screen editor, the
|
|
124
|
+
# KERNAL and the character ROM's C128 set.
|
|
125
|
+
attr_reader :basic_low_rom, :basic_high_rom, :editor_rom, :c128_kernal_rom, :c128_character_rom
|
|
126
|
+
|
|
127
|
+
# Whether CAPS LOCK is down, holding P6 low.
|
|
128
|
+
attr_reader :caps_lock
|
|
129
|
+
|
|
130
|
+
# The chips +model+, a Model::Profile, names, with the SID
|
|
131
|
+
# +sid_model+. +mode+ is the mode the MMU resets into.
|
|
132
|
+
def initialize(model, sid_model: model.sid_model, mode: :c64)
|
|
133
|
+
@region = model.region
|
|
134
|
+
@ram = Memory.new(RAM_POWER_ON, length: 2**17, start: 0)
|
|
135
|
+
@bank1 = RAMWindow.new(@ram, 0x10000, 0xffff)
|
|
136
|
+
@relocated = Array.new(4) { RAMWindow.new(@ram, 0, 0xff) }
|
|
137
|
+
@mmu = MMU.new(mode)
|
|
138
|
+
@cartridge = nil
|
|
139
|
+
@debug_page = nil
|
|
140
|
+
@caps_lock = false
|
|
141
|
+
|
|
142
|
+
load_roms
|
|
143
|
+
load_c128_roms
|
|
144
|
+
@banks = Banks.new(@ram, @mmu, @c128_kernal_rom)
|
|
145
|
+
@c64_mode = @mmu.c64_mode?
|
|
146
|
+
@on_mode_change = nil
|
|
147
|
+
plug_chips(model, sid_model)
|
|
148
|
+
|
|
149
|
+
@datasette = Datasette.new
|
|
150
|
+
@datasette.on_flag { @cia1.flag! }
|
|
151
|
+
@datasette.on_sense_change { @io_port.value = port_value }
|
|
152
|
+
|
|
153
|
+
@color_lines = ColorLines.new(@vic, @cia2, character_rom, @c128_character_rom)
|
|
154
|
+
@color_ram = @color_lines.color_ram(1)
|
|
155
|
+
@vic_writes = VICWrites.new(@vic, @control_ports)
|
|
156
|
+
@open_bus = AddressBus::OpenBus.new(@vic)
|
|
157
|
+
@configuration_reads = ConfigurationPage.new(@mmu, @open_bus)
|
|
158
|
+
@configuration_writes = ConfigurationPage.new(@mmu, @open_bus)
|
|
159
|
+
|
|
160
|
+
@port_ddr = 0x00
|
|
161
|
+
@port_out = 0x00
|
|
162
|
+
@port_floating = 0x00
|
|
163
|
+
@io_port = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
|
|
164
|
+
|
|
165
|
+
@read_pages = Array.new(256)
|
|
166
|
+
@write_pages = Array.new(256)
|
|
167
|
+
@address = 0
|
|
168
|
+
@data = 0
|
|
169
|
+
@io_mapped = false
|
|
170
|
+
@mmu.on_change { update_overlays! }
|
|
171
|
+
update_overlays!
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def attach_cartridge(cartridge)
|
|
175
|
+
@cartridge = cartridge
|
|
176
|
+
cartridge.connect(ram: @ram, open_bus: @open_bus)
|
|
177
|
+
cartridge.on_change { update_overlays! }
|
|
178
|
+
update_overlays!
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Takes the cartridge out of the expansion port.
|
|
182
|
+
def detach_cartridge
|
|
183
|
+
@cartridge = nil
|
|
184
|
+
update_overlays!
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def power_on!
|
|
188
|
+
@ram.clear!(RAM_POWER_ON)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The RES line clears the port's direction and output registers and
|
|
192
|
+
# resets the MMU. The port's floating bit keeps its charge.
|
|
193
|
+
def reset!
|
|
194
|
+
@port_ddr = 0x00
|
|
195
|
+
@port_out = 0x00
|
|
196
|
+
@mmu.reset!
|
|
197
|
+
update_port!
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Holds CAPS LOCK down, or lets it up.
|
|
201
|
+
def caps_lock=(down)
|
|
202
|
+
@caps_lock = down
|
|
203
|
+
@io_port.value = port_value
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Calls the block whenever MCR bit 6 takes the machine into C64 mode or
|
|
207
|
+
# a reset brings it back to C128 mode.
|
|
208
|
+
def on_mode_change(&block)
|
|
209
|
+
@on_mode_change = block
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Whether the CPU sees the C128 KERNAL, in C128 mode with CR's system
|
|
213
|
+
# ROM at $C000-$FFFF.
|
|
214
|
+
def system_rom_mapped? = !@mmu.c64_mode? && @mmu.cr.nobits?(0x30)
|
|
215
|
+
|
|
216
|
+
# The RAM the C128 KERNAL's bank number +bank+ reaches (Banks).
|
|
217
|
+
def peek_bank(bank, addr) = @banks.peek(bank, addr)
|
|
218
|
+
|
|
219
|
+
def write_bank(bank, addr, bytes) = @banks.write(bank, addr, bytes)
|
|
220
|
+
|
|
221
|
+
def install_debug_register(&)
|
|
222
|
+
@debug_page = DebugPage.new(@open_bus, &)
|
|
223
|
+
update_overlays!
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# The address and the byte of the CPU's last access, which a VIC-IIe
|
|
227
|
+
# in FAST mode fetches in place of its own, and which tells the
|
|
228
|
+
# machine when an access reached I/O.
|
|
229
|
+
attr_reader :address, :data
|
|
230
|
+
|
|
231
|
+
def peek(addr)
|
|
232
|
+
@address = addr
|
|
233
|
+
@data = if addr > 0x01
|
|
234
|
+
@read_pages[addr >> 8].peek(addr)
|
|
235
|
+
else
|
|
236
|
+
addr.zero? ? @port_ddr : @io_port.value
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Whether the last access went to a chip at $D000-$DFFF, which runs
|
|
241
|
+
# at 1 MHz.
|
|
242
|
+
def io_access? = @io_mapped && (@address & 0xf000) == 0xd000
|
|
243
|
+
|
|
244
|
+
# Whether the last access went to the VIC's registers.
|
|
245
|
+
def vic_access? = @io_mapped && (@address & 0xfc00) == 0xd000
|
|
246
|
+
|
|
247
|
+
# A write to $00 or $01 goes to the port, and the RAM below takes the
|
|
248
|
+
# byte the VIC fetched in the phi1 half of the cycle.
|
|
249
|
+
def poke(addr, value)
|
|
250
|
+
@address = addr
|
|
251
|
+
@data = value
|
|
252
|
+
if addr < 0x02
|
|
253
|
+
@write_pages[0].poke(addr, @vic.phi1_data)
|
|
254
|
+
addr.zero? ? @port_ddr = value : @port_out = value
|
|
255
|
+
update_port!
|
|
256
|
+
else
|
|
257
|
+
@write_pages[addr >> 8].poke(addr, value)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def inspect
|
|
262
|
+
"#<#{self.class.name} mode=#{@mmu.mode} port=#{format('0x%02x', @io_port.value)} " \
|
|
263
|
+
"cartridge=#{@cartridge ? @cartridge.class.name : 'none'} " \
|
|
264
|
+
"ultimax=#{@ultimax}>"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
private
|
|
268
|
+
|
|
269
|
+
def load_c128_roms
|
|
270
|
+
basic = ROM.read("c128/basic.rom")
|
|
271
|
+
kernal = ROM.read("c128/kernal.rom")
|
|
272
|
+
@basic_low_rom = ROM.new(basic[0, 0x4000], length: 0x4000, start: 0x4000)
|
|
273
|
+
@basic_high_rom = ROM.new(basic[0x4000, 0x4000], length: 0x4000, start: 0x8000)
|
|
274
|
+
@editor_rom = ROM.new(kernal[0, 0x1000], length: 0x1000, start: 0xc000)
|
|
275
|
+
@c128_kernal_rom = ROM.new(kernal[0x2000, 0x2000], length: 0x2000, start: 0xe000)
|
|
276
|
+
characters = ROM.read("c128/character.rom")
|
|
277
|
+
@c128_character_rom = ROM.new(characters[0x1000, 0x1000], length: 0x1000, start: 0xd000)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def plug_chips(model, sid_model)
|
|
281
|
+
@keyboard = Keyboard.new(matrix: KEYBOARD_MATRIX)
|
|
282
|
+
@joystick1 = Joystick.new
|
|
283
|
+
@joystick2 = Joystick.new
|
|
284
|
+
@control_ports = ControlPorts.new(keyboard: @keyboard, joystick1: @joystick1, joystick2: @joystick2)
|
|
285
|
+
@vic = VIC.new(model: model.vic_model, region: @region)
|
|
286
|
+
@cia1 = CIA.new(start: 0xdc00, peripheral: @control_ports, model: model.cia_model, region: @region)
|
|
287
|
+
@cia2 = CIA.new(start: 0xdd00, model: model.cia_model, region: @region)
|
|
288
|
+
@control_ports.port_a_source = @cia1
|
|
289
|
+
@cia1.on_port_b4_change { |high| @vic.lightpen_level(high) }
|
|
290
|
+
@sid = SID.new(model: sid_model, pots: @control_ports)
|
|
291
|
+
@vdc = VDC.new(model: model.vdc_model, ram_kb: model.vdc_ram_kb, clock_hz: @region.clock_hz)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def update_port!
|
|
295
|
+
driven = @port_ddr & PORT_FLOATING
|
|
296
|
+
@port_floating = (@port_floating & ~driven) | (@port_out & driven)
|
|
297
|
+
@io_port.value = port_value
|
|
298
|
+
@datasette.motor = !@io_port.tape_motor?
|
|
299
|
+
update_overlays!
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def port_value
|
|
303
|
+
input = PORT_PULLUPS | (@port_floating & PORT_FLOATING)
|
|
304
|
+
input |= CAPS_LOCK unless @caps_lock
|
|
305
|
+
input &= ~TAPE_SENSE if @datasette.sense_low?
|
|
306
|
+
(@port_out & @port_ddr) | (input & ~@port_ddr & 0xff)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def update_overlays!
|
|
310
|
+
@ultimax = @cartridge ? @cartridge.ultimax? : false
|
|
311
|
+
@phi1_ultimax = @cartridge ? @cartridge.phi1_ultimax? : false
|
|
312
|
+
@mmu.game = @cartridge ? @cartridge.game : 1
|
|
313
|
+
@mmu.exrom = @cartridge ? @cartridge.exrom : 1
|
|
314
|
+
c64_mode = @mmu.c64_mode?
|
|
315
|
+
port = @io_port.value
|
|
316
|
+
@color_lines.push(c64_mode, port)
|
|
317
|
+
@vic.vic_bank.map(@ram, base: @mmu.vic_bank << 16, phi1_ultimax: @phi1_ultimax, ultimax: @ultimax,
|
|
318
|
+
romh: @cartridge&.romh)
|
|
319
|
+
@color_ram = @color_lines.cpu_color_ram(c64_mode, port)
|
|
320
|
+
c64_mode ? map_pla_pages : map_c128_pages
|
|
321
|
+
mode_changed if c64_mode != @c64_mode
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def mode_changed
|
|
325
|
+
@c64_mode = !@c64_mode
|
|
326
|
+
@on_mode_change&.call
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def map_io_pages
|
|
330
|
+
@io_mapped = true
|
|
331
|
+
@read_pages.fill(@vic, 0xd0, 4)
|
|
332
|
+
@write_pages.fill(@vic_writes, 0xd0, 4)
|
|
333
|
+
@read_pages[0xd4] = @write_pages[0xd4] = @sid
|
|
334
|
+
@read_pages[0xd5] = @write_pages[0xd5] = @mmu.c64_mode? ? @open_bus : @mmu
|
|
335
|
+
@read_pages[0xd6] = @write_pages[0xd6] = @vdc
|
|
336
|
+
@read_pages[0xd7] = @write_pages[0xd7] = @debug_page || @open_bus
|
|
337
|
+
@read_pages.fill(@color_ram, 0xd8, 4)
|
|
338
|
+
@write_pages.fill(@color_ram, 0xd8, 4)
|
|
339
|
+
@read_pages[0xdc] = @write_pages[0xdc] = @cia1
|
|
340
|
+
@read_pages[0xdd] = @write_pages[0xdd] = @cia2
|
|
341
|
+
@read_pages.fill(@open_bus, 0xde, 2)
|
|
342
|
+
@write_pages.fill(@open_bus, 0xde, 2)
|
|
343
|
+
map_cartridge_io if @cartridge
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
class Bus
|
|
6
|
+
# The bus's state for a snapshot, as AddressBus::SavedState is the
|
|
7
|
+
# C64's.
|
|
8
|
+
module SavedState
|
|
9
|
+
# The machine's memory, the last access and everything on the bus but
|
|
10
|
+
# the keyboard, the joysticks, the pot devices and CAPS LOCK, which the
|
|
11
|
+
# host holds. A cartridge is already in the port, built from its setup.
|
|
12
|
+
def save_state(out)
|
|
13
|
+
out.marker("C128 BUS")
|
|
14
|
+
out.int(@port_ddr).int(@port_out).int(@port_floating).int(@address).int(@data)
|
|
15
|
+
@mmu.save_state(out)
|
|
16
|
+
@ram.save_state(out)
|
|
17
|
+
@color_lines.color_ram(0).save_state(out)
|
|
18
|
+
@color_lines.color_ram(1).save_state(out)
|
|
19
|
+
@cartridge&.save_state(out)
|
|
20
|
+
@datasette.save_state(out)
|
|
21
|
+
@vic.save_state(out)
|
|
22
|
+
@cia1.save_state(out)
|
|
23
|
+
@cia2.save_state(out)
|
|
24
|
+
@sid.save_state(out)
|
|
25
|
+
@vdc.save_state(out)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def load_state(input)
|
|
29
|
+
input.marker("C128 BUS")
|
|
30
|
+
@port_ddr = input.int
|
|
31
|
+
@port_out = input.int
|
|
32
|
+
@port_floating = input.int
|
|
33
|
+
@address = input.int
|
|
34
|
+
@data = input.int
|
|
35
|
+
@mmu.load_state(input)
|
|
36
|
+
@ram.load_state(input)
|
|
37
|
+
@color_lines.color_ram(0).load_state(input)
|
|
38
|
+
@color_lines.color_ram(1).load_state(input)
|
|
39
|
+
@cartridge&.load_state(input)
|
|
40
|
+
@datasette.load_state(input)
|
|
41
|
+
@io_port.value = port_value
|
|
42
|
+
update_overlays!
|
|
43
|
+
@vic.load_state(input)
|
|
44
|
+
@control_ports.extra_rows = 0xf8 | @vic.extra_keyboard_lines
|
|
45
|
+
@cia1.load_state(input)
|
|
46
|
+
@cia2.load_state(input)
|
|
47
|
+
@sid.load_state(input)
|
|
48
|
+
@vdc.load_state(input)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The C128's two 1K colour RAM banks, and the colour RAM and character
|
|
6
|
+
# ROM it pushes into the VIC's bank. In C128 mode the 8502's port picks
|
|
7
|
+
# them: P0 the colour RAM bank the CPU sees, P1 the one the VIC sees,
|
|
8
|
+
# and P2 low shows the VIC the character ROM's C128 set. In C64 mode
|
|
9
|
+
# both see colour RAM bank 1, and the VIC the C64 set.
|
|
10
|
+
class ColorLines
|
|
11
|
+
def initialize(vic, cia2, c64_characters, c128_characters)
|
|
12
|
+
@vic_bank = vic.vic_bank
|
|
13
|
+
@cia2 = cia2
|
|
14
|
+
@c64_characters = c64_characters
|
|
15
|
+
@c128_characters = c128_characters
|
|
16
|
+
@bank0 = ColorMemory.new(vic)
|
|
17
|
+
@bank1 = ColorMemory.new(vic)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Colour RAM bank +number+, 0 or 1.
|
|
21
|
+
def color_ram(number) = number.zero? ? @bank0 : @bank1
|
|
22
|
+
|
|
23
|
+
# The colour RAM bank the CPU sees.
|
|
24
|
+
def cpu_color_ram(c64_mode, port) = c64_mode ? @bank1 : color_ram(port & 0x01)
|
|
25
|
+
|
|
26
|
+
# Pushes the colour RAM and the character ROM the VIC sees into its
|
|
27
|
+
# bank.
|
|
28
|
+
def push(c64_mode, port)
|
|
29
|
+
if c64_mode
|
|
30
|
+
@vic_bank.connect(cia2: @cia2, color_ram: @bank1)
|
|
31
|
+
@vic_bank.map_character_rom(@c64_characters)
|
|
32
|
+
else
|
|
33
|
+
@vic_bank.connect(cia2: @cia2, color_ram: color_ram((port >> 1) & 0x01))
|
|
34
|
+
@vic_bank.map_character_rom(@c128_characters, shadow: port.nobits?(0x04))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The three keys outside the keyboard matrix: RESTORE, CAPS LOCK and
|
|
6
|
+
# 40/80 DISPLAY.
|
|
7
|
+
module Keys
|
|
8
|
+
# RESTORE pulses NMI for a cycle, as on the C64.
|
|
9
|
+
def press_restore
|
|
10
|
+
@restore_pulse = true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def release_restore; end
|
|
14
|
+
|
|
15
|
+
# CAPS LOCK locks down and up, and holds the 8502's P6 low while down.
|
|
16
|
+
def press_caps_lock
|
|
17
|
+
@bus.caps_lock = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def release_caps_lock
|
|
21
|
+
@bus.caps_lock = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The 40/80 DISPLAY key locks down and up. The KERNAL reads it at reset
|
|
25
|
+
# and starts the screen editor on the VDC's 80 columns while it is down.
|
|
26
|
+
def press_display_key
|
|
27
|
+
@bus.mmu.display_key = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def release_display_key
|
|
31
|
+
@bus.mmu.display_key = false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class C128
|
|
5
|
+
# The 8722 MMU's registers, $D500-$D50B in C128 mode:
|
|
6
|
+
#
|
|
7
|
+
# $D500 CR: I/O, the ROMs and the RAM bank the CPU sees
|
|
8
|
+
# $D501-$D504 PCRA-PCRD: preset configurations
|
|
9
|
+
# $D505 MCR: the CPU (bit 0, 1 for the 8502), FSDIR, GAME and EXROM
|
|
10
|
+
# in, C64 mode (bit 6) and the 40/80 key
|
|
11
|
+
# $D506 RCR: common RAM and the VIC's RAM bank
|
|
12
|
+
# $D507-$D50A P0L, P0H, P1L, P1H: page 0 and page 1 relocation
|
|
13
|
+
# $D50B VR: the version, 2 banks of version 0
|
|
14
|
+
#
|
|
15
|
+
# The rest of the page reads $FF. $FF00 is CR again, and a write to
|
|
16
|
+
# $FF01-$FF04, the LCRs, copies PCRA-PCRD into CR. Those five answer in
|
|
17
|
+
# every configuration, whatever CR maps there.
|
|
18
|
+
#
|
|
19
|
+
# A write to P0H or P1H waits until the matching P0L or P1L write
|
|
20
|
+
# takes it.
|
|
21
|
+
#
|
|
22
|
+
# On the C128 the Z80 runs first and hands the bus to the 8502. Here the
|
|
23
|
+
# 8502 runs from reset, with MCR as the Z80 leaves it. A machine built
|
|
24
|
+
# for C64 mode resets into C64 mode, the state the C128 KERNAL leaves
|
|
25
|
+
# when C= is held at power-on. In C64 mode the MMU answers nowhere, but
|
|
26
|
+
# the CPU's RAM bank, common RAM and the VIC's bank it holds stay in
|
|
27
|
+
# force.
|
|
28
|
+
class MMU
|
|
29
|
+
CR = 0
|
|
30
|
+
MCR = 5
|
|
31
|
+
RCR = 6
|
|
32
|
+
P0L = 7
|
|
33
|
+
P0H = 8
|
|
34
|
+
P1L = 9
|
|
35
|
+
P1H = 10
|
|
36
|
+
VR = 11
|
|
37
|
+
|
|
38
|
+
MCR_8502 = 0x01
|
|
39
|
+
MCR_C64_MODE = 0x40
|
|
40
|
+
VERSION = 0x20
|
|
41
|
+
|
|
42
|
+
# MCR's bits that read back what was written: the CPU, FSDIR and C64
|
|
43
|
+
# mode. Bits 1 and 2 have no pins and read 1.
|
|
44
|
+
MCR_WRITABLE = 0x49
|
|
45
|
+
MCR_UNUSED = 0x06
|
|
46
|
+
MCR_GAME = 0x10
|
|
47
|
+
MCR_EXROM = 0x20
|
|
48
|
+
MCR_DISPLAY_KEY = 0x80
|
|
49
|
+
|
|
50
|
+
# The common RAM sizes RCR bits 0-1 pick, in pages.
|
|
51
|
+
COMMON_PAGES = [4, 16, 32, 64].freeze
|
|
52
|
+
|
|
53
|
+
# The registers, CR first.
|
|
54
|
+
attr_reader :registers
|
|
55
|
+
|
|
56
|
+
# Whether the 40/80 DISPLAY key is locked down, which MCR bit 7 reads
|
|
57
|
+
# as 0 in C128 mode.
|
|
58
|
+
attr_accessor :display_key
|
|
59
|
+
|
|
60
|
+
# The cartridge's GAME and EXROM lines, which MCR bits 4 and 5 read.
|
|
61
|
+
attr_writer :game, :exrom
|
|
62
|
+
|
|
63
|
+
# +mode+ is the mode a reset leaves, :c128 or :c64.
|
|
64
|
+
def initialize(mode = :c64)
|
|
65
|
+
@reset_mode = mode
|
|
66
|
+
@registers = Array.new(12, 0)
|
|
67
|
+
@display_key = false
|
|
68
|
+
@game = 1
|
|
69
|
+
@exrom = 1
|
|
70
|
+
@on_change = nil
|
|
71
|
+
reset!
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Calls the block whenever the mapping the registers ask for changes.
|
|
75
|
+
def on_change(&block)
|
|
76
|
+
@on_change = block
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reset!
|
|
80
|
+
@registers.fill(0)
|
|
81
|
+
@registers[MCR] = @reset_mode == :c64 ? MCR_C64_MODE | MCR_8502 : MCR_8502
|
|
82
|
+
@registers[P1L] = 0x01
|
|
83
|
+
@registers[VR] = VERSION
|
|
84
|
+
@p0h_latch = 0
|
|
85
|
+
@p1h_latch = 0
|
|
86
|
+
@on_change&.call
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def c64_mode? = @registers[MCR].anybits?(MCR_C64_MODE)
|
|
90
|
+
|
|
91
|
+
# :c64 or :c128, the mode MCR bit 6 selects.
|
|
92
|
+
def mode = c64_mode? ? :c64 : :c128
|
|
93
|
+
|
|
94
|
+
# The 64K bank the VIC sees, RCR bit 6. Bit 7 picks banks a 256K
|
|
95
|
+
# machine has.
|
|
96
|
+
def vic_bank = (@registers[RCR] >> 6) & 0x01
|
|
97
|
+
|
|
98
|
+
# The RAM bank the CPU sees, CR bit 6.
|
|
99
|
+
def cpu_bank = (@registers[CR] >> 6) & 0x01
|
|
100
|
+
|
|
101
|
+
def cr = @registers[CR]
|
|
102
|
+
|
|
103
|
+
# The pages from $0000 up that common RAM keeps in bank 0, or 0.
|
|
104
|
+
def common_low_pages = @registers[RCR].anybits?(0x04) ? common_pages : 0
|
|
105
|
+
|
|
106
|
+
# The first page of the common RAM below $FFFF, or 256 for none.
|
|
107
|
+
def common_high_start = @registers[RCR].anybits?(0x08) ? 256 - common_pages : 256
|
|
108
|
+
|
|
109
|
+
# The page and the bank page 0 moves to.
|
|
110
|
+
def p0_page = @registers[P0L]
|
|
111
|
+
def p0_bank = @registers[P0H] & 0x01
|
|
112
|
+
|
|
113
|
+
# The page and the bank page 1 moves to.
|
|
114
|
+
def p1_page = @registers[P1L]
|
|
115
|
+
def p1_bank = @registers[P1H] & 0x01
|
|
116
|
+
|
|
117
|
+
# The registers at $D500-$D5FF.
|
|
118
|
+
def peek(addr)
|
|
119
|
+
offset = addr & 0xff
|
|
120
|
+
case offset
|
|
121
|
+
when MCR then mcr
|
|
122
|
+
when P0H, P1H then 0xf0 | @registers[offset]
|
|
123
|
+
when 0..VR then @registers[offset]
|
|
124
|
+
else 0xff
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def poke(addr, value)
|
|
129
|
+
offset = addr & 0xff
|
|
130
|
+
case offset
|
|
131
|
+
when P0H then @p0h_latch = value & 0x0f
|
|
132
|
+
when P1H then @p1h_latch = value & 0x0f
|
|
133
|
+
when P0L then relocate(P0L, P0H, @p0h_latch, value)
|
|
134
|
+
when P1L then relocate(P1L, P1H, @p1h_latch, value)
|
|
135
|
+
when CR..RCR then write(offset, value)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The registers and the P0H and P1H writes waiting for P0L and P1L.
|
|
140
|
+
# The bus maps the restored registers once its own state is back.
|
|
141
|
+
def save_state(out)
|
|
142
|
+
out.ints(@registers).int(@p0h_latch).int(@p1h_latch)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def load_state(input)
|
|
146
|
+
input.ints_into(@registers)
|
|
147
|
+
@p0h_latch = input.int
|
|
148
|
+
@p1h_latch = input.int
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# CR and the LCRs at $FF00-$FF04: an LCR reads its PCR.
|
|
152
|
+
def peek_configuration(addr) = @registers[addr & 0x07]
|
|
153
|
+
|
|
154
|
+
def poke_configuration(addr, value)
|
|
155
|
+
offset = addr & 0x07
|
|
156
|
+
write(CR, offset.zero? ? value : @registers[offset])
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def common_pages = COMMON_PAGES[@registers[RCR] & 0x03]
|
|
162
|
+
|
|
163
|
+
def mcr
|
|
164
|
+
value = (@registers[MCR] & MCR_WRITABLE) | MCR_UNUSED
|
|
165
|
+
value |= MCR_GAME if @game == 1
|
|
166
|
+
value |= MCR_EXROM if @exrom == 1
|
|
167
|
+
value |= MCR_DISPLAY_KEY unless @display_key
|
|
168
|
+
value
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def relocate(low, high, latch, value)
|
|
172
|
+
@registers[high] = latch
|
|
173
|
+
write(low, value)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def write(offset, value)
|
|
177
|
+
@registers[offset] = value
|
|
178
|
+
@on_change&.call
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|