badline 0.10.1 → 0.11.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 +23 -0
- data/CLAUDE.md +25 -6
- data/CONTRIBUTING.md +8 -3
- data/README.md +63 -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/drives.rb +95 -0
- data/lib/badline/c128/fast_serial.rb +62 -0
- data/lib/badline/c128/keys.rb +35 -0
- data/lib/badline/c128/mmu.rb +197 -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 +121 -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 +322 -0
- data/lib/badline/chrout_trap.rb +3 -0
- data/lib/badline/cia/fast_forward.rb +42 -0
- data/lib/badline/cia/serial.rb +12 -0
- data/lib/badline/cia/timer.rb +39 -0
- data/lib/badline/cia.rb +2 -0
- data/lib/badline/computer/attachments.rb +10 -13
- data/lib/badline/computer/kernal_traps.rb +29 -0
- data/lib/badline/computer.rb +5 -0
- data/lib/badline/drive/core.rb +172 -0
- data/lib/badline/{drive1541 → drive}/idle.rb +18 -19
- data/lib/badline/drive/idle_cpu.rb +28 -0
- data/lib/badline/{drive1541 → drive}/orbit.rb +2 -1
- data/lib/badline/drive/serial_lines.rb +73 -0
- data/lib/badline/{drive1541 → drive}/sleep.rb +4 -3
- data/lib/badline/drive/watch.rb +150 -0
- data/lib/badline/drive.rb +9 -0
- data/lib/badline/drive1541/bus.rb +5 -129
- data/lib/badline/drive1541/byte_ready.rb +48 -0
- data/lib/badline/drive1541/cpu.rb +1 -19
- data/lib/badline/drive1541/disk.rb +77 -20
- data/lib/badline/drive1541/disk_state.rb +7 -2
- data/lib/badline/drive1541/disk_via.rb +3 -0
- data/lib/badline/drive1541/mechanism.rb +48 -14
- data/lib/badline/drive1541/mechanism_state.rb +5 -4
- data/lib/badline/drive1541/rotation.rb +5 -5
- data/lib/badline/drive1541/serial_port.rb +4 -60
- data/lib/badline/drive1541.rb +13 -172
- data/lib/badline/drive1571/bus.rb +111 -0
- data/lib/badline/drive1571/cpu.rb +11 -0
- data/lib/badline/drive1571/serial_port.rb +37 -0
- data/lib/badline/drive1571/serial_via.rb +25 -0
- data/lib/badline/drive1571/wd1770.rb +34 -0
- data/lib/badline/drive1571.rb +225 -0
- data/lib/badline/frontend/app.rb +35 -22
- data/lib/badline/frontend/boot.rb +31 -1
- data/lib/badline/frontend/controls.rb +11 -0
- data/lib/badline/frontend/drive_led.rb +1 -1
- data/lib/badline/frontend/keys.rb +28 -1
- data/lib/badline/frontend/machine_window.rb +16 -13
- data/lib/badline/frontend/menu_media.rb +3 -3
- data/lib/badline/frontend/menu_pages.rb +8 -2
- 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/iec_bus.rb +54 -4
- 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 +20 -9
- data/lib/badline/media/true_drive.rb +26 -23
- 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 +7 -2
- data/lib/badline/options/table.rb +8 -2
- data/lib/badline/options/validation.rb +14 -1
- data/lib/badline/options.rb +20 -19
- data/lib/badline/roms/README +36 -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/roms/dos1571.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/storage/d64_image.rb +3 -0
- data/lib/badline/storage/d71_image.rb +9 -4
- data/lib/badline/storage/g64_image.rb +7 -2
- data/lib/badline/time_of_day.rb +9 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/fast_forward.rb +3 -3
- 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/vic20/attachments.rb +6 -0
- data/lib/badline/z80/arithmetic.rb +117 -0
- data/lib/badline/z80/bit_page.rb +55 -0
- data/lib/badline/z80/block_io.rb +58 -0
- data/lib/badline/z80/block_transfers.rb +59 -0
- data/lib/badline/z80/bus_cycles.rb +111 -0
- data/lib/badline/z80/core.rb +111 -0
- data/lib/badline/z80/exchanges.rb +50 -0
- data/lib/badline/z80/extended_page.rb +89 -0
- data/lib/badline/z80/flags.rb +37 -0
- data/lib/badline/z80/interrupt_acceptance.rb +46 -0
- data/lib/badline/z80/jumps.rb +82 -0
- data/lib/badline/z80/loads.rb +98 -0
- data/lib/badline/z80/main_page.rb +134 -0
- data/lib/badline/z80/registers.rb +120 -0
- data/lib/badline/z80/shifts.rb +64 -0
- data/lib/badline/z80.rb +11 -0
- data/lib/badline.rb +1 -0
- metadata +59 -4
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/c128/vdc/memory"
|
|
4
|
+
require "badline/c128/vdc/window"
|
|
5
|
+
require "badline/c128/vdc/painter"
|
|
6
|
+
require "badline/c128/vdc/raster"
|
|
7
|
+
|
|
8
|
+
module Badline
|
|
9
|
+
class C128
|
|
10
|
+
# The 8563 or 8568 VDC: the address register at $D600 and the data
|
|
11
|
+
# register at $D601, mirrored through $D6FF, in front of 37 internal
|
|
12
|
+
# registers (38 on the 8568), the VDC's own RAM (Memory) and its 80
|
|
13
|
+
# column RGBI display (Raster, Painter).
|
|
14
|
+
#
|
|
15
|
+
# A write to $D600 selects a register, and a read there gives the
|
|
16
|
+
# status: bit 7 ready for the next access, bit 5 in the vertical blank
|
|
17
|
+
# (outside the displayed rows), and bits 0-2 the version. $D601 reads
|
|
18
|
+
# and writes the selected register. A register past the last reads $FF.
|
|
19
|
+
# The light pen is left out.
|
|
20
|
+
#
|
|
21
|
+
# R18 and R19 hold the update address, high byte first, and a write to
|
|
22
|
+
# either reads the RAM there into R31. A read of R31 gives that byte,
|
|
23
|
+
# moves the address on and reads the next. A write of R31 stores the
|
|
24
|
+
# byte, moves the address on and reads the next. A write of R30 then
|
|
25
|
+
# repeats the last byte written over R30 more bytes (256 for 0), or with
|
|
26
|
+
# R24 bit 7 copies that many from R32/R33 on, and leaves the update
|
|
27
|
+
# address, and R32/R33 for a copy, after the last byte.
|
|
28
|
+
#
|
|
29
|
+
# The VDC runs on its own 16 MHz dot clock, a character clock every
|
|
30
|
+
# character width of dots (R22, doubled by R25 bit 4). The status reads
|
|
31
|
+
# not ready for ACCESS_CLOCKS character clocks after an access through
|
|
32
|
+
# R31 or a write of R18 or R19, and for one clock per byte a block write
|
|
33
|
+
# fills or two per byte it copies.
|
|
34
|
+
class VDC
|
|
35
|
+
include Addressable
|
|
36
|
+
|
|
37
|
+
STATUS_READY = 0x80
|
|
38
|
+
STATUS_VBLANK = 0x20
|
|
39
|
+
UPDATE_HIGH = 18
|
|
40
|
+
UPDATE_LOW = 19
|
|
41
|
+
WORD_COUNT = 30
|
|
42
|
+
DATA = 31
|
|
43
|
+
SOURCE_HIGH = 32
|
|
44
|
+
SOURCE_LOW = 33
|
|
45
|
+
DOT_CLOCK_HZ = 16_000_000
|
|
46
|
+
|
|
47
|
+
# Character clocks the status reads busy after an access through R31
|
|
48
|
+
# or a write of R18 or R19, fitted to the timings a flat C128 gave
|
|
49
|
+
# VDC/vdctiming (TimingResultsFlatC128.jpg).
|
|
50
|
+
ACCESS_CLOCKS = 30
|
|
51
|
+
|
|
52
|
+
# The version in the status bits: 1 for the 8563 R8 and R9, and 2 for
|
|
53
|
+
# the 8568.
|
|
54
|
+
VERSIONS = { mos8563: 1, mos8568: 2 }.freeze
|
|
55
|
+
|
|
56
|
+
# The 16 RGBI colours, red, green, blue and intensity from bit 3 down,
|
|
57
|
+
# as the Programmer's Reference Guide names them: 1100 is dark yellow.
|
|
58
|
+
PALETTE = Array.new(16) do |rgbi|
|
|
59
|
+
intensity = rgbi.anybits?(1) ? 0x55 : 0
|
|
60
|
+
red = (rgbi.anybits?(8) ? 0xaa : 0) + intensity
|
|
61
|
+
green = (rgbi.anybits?(4) ? 0xaa : 0) + intensity
|
|
62
|
+
blue = (rgbi.anybits?(2) ? 0xaa : 0) + intensity
|
|
63
|
+
(red << 16) | (green << 8) | blue
|
|
64
|
+
end.freeze
|
|
65
|
+
|
|
66
|
+
attr_reader :model, :registers
|
|
67
|
+
|
|
68
|
+
# Whether lines paint into the display as the raster passes them.
|
|
69
|
+
attr_reader :render
|
|
70
|
+
|
|
71
|
+
# +ram_kb+ is 16 or 64, and +clock_hz+ the CPU's clock the VDC is
|
|
72
|
+
# clocked against.
|
|
73
|
+
def initialize(model: :mos8563, ram_kb: 16, clock_hz: Region::PAL.clock_hz)
|
|
74
|
+
addressable_at(0xd600, length: 0x100)
|
|
75
|
+
@model = model
|
|
76
|
+
@version = VERSIONS.fetch(model)
|
|
77
|
+
@clock_hz = clock_hz
|
|
78
|
+
@registers = Array.new(model == :mos8568 ? 38 : 37, 0)
|
|
79
|
+
@memory = Memory.new(ram_kb)
|
|
80
|
+
@painter = Painter.new(@registers, @memory)
|
|
81
|
+
@raster = Raster.new(@registers, @painter)
|
|
82
|
+
@render = false
|
|
83
|
+
power_on!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The dots a character takes, R22 bits 4-7 plus one, or R22 bits 4-7
|
|
87
|
+
# doubled in double-width mode.
|
|
88
|
+
def self.char_dots(registers)
|
|
89
|
+
width = registers[22] >> 4
|
|
90
|
+
registers[25].anybits?(0x10) ? [width, 1].max * 2 : width + 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The registers clear, the RAM clears and the raster starts a frame.
|
|
94
|
+
def power_on!
|
|
95
|
+
@registers.fill(0)
|
|
96
|
+
@memory.clear!
|
|
97
|
+
@memory.mode64 = false
|
|
98
|
+
@selected = 0
|
|
99
|
+
@read_latch = 0
|
|
100
|
+
@write_data = 0
|
|
101
|
+
@busy_until = 0
|
|
102
|
+
@phase = 0
|
|
103
|
+
@raster.reset!
|
|
104
|
+
@line_units = @raster.line_dots * @clock_hz
|
|
105
|
+
@painter.clear!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ram = @memory.ram
|
|
109
|
+
|
|
110
|
+
def render=(on)
|
|
111
|
+
@render = on
|
|
112
|
+
@painter.clear!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Everything the VDC holds but its display, which it paints again
|
|
116
|
+
# while it renders: the registers, the RAM, the data port and the
|
|
117
|
+
# raster. Whether it renders is the host's.
|
|
118
|
+
def save_state(out)
|
|
119
|
+
out.marker("VDC")
|
|
120
|
+
out.ints(@registers).blob(@memory.ram)
|
|
121
|
+
out.int(@selected).int(@read_latch).int(@write_data).int(@busy_until).int(@phase).int(@line_units)
|
|
122
|
+
@raster.save_state(out)
|
|
123
|
+
@painter.save_state(out)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def load_state(input)
|
|
127
|
+
input.marker("VDC")
|
|
128
|
+
input.ints_into(@registers)
|
|
129
|
+
input.blob_into(@memory.ram)
|
|
130
|
+
@memory.mode64 = @registers[28].anybits?(0x10)
|
|
131
|
+
@selected = input.int
|
|
132
|
+
@read_latch = input.int
|
|
133
|
+
@write_data = input.int
|
|
134
|
+
@busy_until = input.int
|
|
135
|
+
@phase = input.int
|
|
136
|
+
@line_units = input.int
|
|
137
|
+
@raster.load_state(input)
|
|
138
|
+
@painter.load_state(input)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The frames the raster has finished, which the blinking counts.
|
|
142
|
+
def frame = @raster.frame
|
|
143
|
+
|
|
144
|
+
def display = @painter.display
|
|
145
|
+
|
|
146
|
+
def width = @painter.width
|
|
147
|
+
|
|
148
|
+
def height = @painter.height
|
|
149
|
+
|
|
150
|
+
def dirty_lines = @painter.dirty_lines
|
|
151
|
+
|
|
152
|
+
def clear_dirty_lines! = @painter.clear_dirty_lines!
|
|
153
|
+
|
|
154
|
+
def palette = PALETTE
|
|
155
|
+
|
|
156
|
+
# The [left, top, width, height] of the display a monitor shows.
|
|
157
|
+
def crop = @painter.crop(@raster.line_dots, @raster.frame_lines, @raster.sync_lines)
|
|
158
|
+
|
|
159
|
+
# The update address R18 and R19 hold.
|
|
160
|
+
def update_address = (@registers[UPDATE_HIGH] << 8) | @registers[UPDATE_LOW]
|
|
161
|
+
|
|
162
|
+
# Whether an access or a block operation is still under way.
|
|
163
|
+
def busy? = @phase < @busy_until
|
|
164
|
+
|
|
165
|
+
# One CPU cycle: the dot clock runs on, and a line ends once it has
|
|
166
|
+
# run a line's dots. The line's phase counts dots times the CPU's
|
|
167
|
+
# clock, DOT_CLOCK_HZ a cycle.
|
|
168
|
+
def cycle!
|
|
169
|
+
@phase += DOT_CLOCK_HZ
|
|
170
|
+
end_line if @phase >= @line_units
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def peek(addr)
|
|
174
|
+
return status if offset_of(addr).even?
|
|
175
|
+
return read_ram if @selected == DATA
|
|
176
|
+
|
|
177
|
+
@selected < @registers.length ? @registers[@selected] : 0xff
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def poke(addr, value)
|
|
181
|
+
return @selected = value & 0x3f if offset_of(addr).even?
|
|
182
|
+
return if @selected >= @registers.length
|
|
183
|
+
|
|
184
|
+
write_register(@selected, value)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def status
|
|
190
|
+
value = @version
|
|
191
|
+
value |= STATUS_READY unless busy?
|
|
192
|
+
value |= STATUS_VBLANK if @raster.vertical_blank?
|
|
193
|
+
value
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def end_line
|
|
197
|
+
@phase -= @line_units
|
|
198
|
+
@busy_until -= @line_units
|
|
199
|
+
@raster.end_line(@render)
|
|
200
|
+
@line_units = @raster.line_dots * @clock_hz
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def write_register(number, value)
|
|
204
|
+
@registers[number] = value
|
|
205
|
+
case number
|
|
206
|
+
when DATA then write_ram(value)
|
|
207
|
+
when WORD_COUNT then block(value.zero? ? 256 : value)
|
|
208
|
+
when UPDATE_HIGH, UPDATE_LOW then read_ahead
|
|
209
|
+
when 28 then @memory.mode64 = value.anybits?(0x10)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def read_ram
|
|
214
|
+
value = @read_latch
|
|
215
|
+
advance(1)
|
|
216
|
+
read_ahead
|
|
217
|
+
value
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def write_ram(value)
|
|
221
|
+
@memory.store(update_address, value)
|
|
222
|
+
@write_data = value
|
|
223
|
+
advance(1)
|
|
224
|
+
read_ahead
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def read_ahead
|
|
228
|
+
@read_latch = @memory.fetch(update_address)
|
|
229
|
+
busy(ACCESS_CLOCKS)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def block(count)
|
|
233
|
+
copy = @registers[24].anybits?(0x80)
|
|
234
|
+
copy ? copy_block(count) : fill_block(count)
|
|
235
|
+
@read_latch = @memory.fetch(update_address)
|
|
236
|
+
busy(copy ? count * 2 : count)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def fill_block(count)
|
|
240
|
+
address = update_address
|
|
241
|
+
count.times { |i| @memory.store(address + i, @write_data) }
|
|
242
|
+
advance(count)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def copy_block(count)
|
|
246
|
+
destination = update_address
|
|
247
|
+
source = (@registers[SOURCE_HIGH] << 8) | @registers[SOURCE_LOW]
|
|
248
|
+
count.times { |i| @memory.store(destination + i, @memory.fetch(source + i)) }
|
|
249
|
+
advance(count)
|
|
250
|
+
source = (source + count) & 0xffff
|
|
251
|
+
@registers[SOURCE_HIGH] = source >> 8
|
|
252
|
+
@registers[SOURCE_LOW] = source & 0xff
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def advance(count)
|
|
256
|
+
address = (update_address + count) & 0xffff
|
|
257
|
+
@registers[UPDATE_HIGH] = address >> 8
|
|
258
|
+
@registers[UPDATE_LOW] = address & 0xff
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Holds the status busy for `clocks` character clocks after whatever
|
|
262
|
+
# is already under way.
|
|
263
|
+
def busy(clocks)
|
|
264
|
+
@busy_until = [@busy_until, @phase].max + (clocks * VDC.char_dots(@registers) * @clock_hz)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
data/lib/badline/c128.rb
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/c128/model"
|
|
4
|
+
require "badline/c128/mmu"
|
|
5
|
+
require "badline/c128/vdc"
|
|
6
|
+
require "badline/c128/mmu_pages"
|
|
7
|
+
require "badline/c128/color_lines"
|
|
8
|
+
require "badline/c128/banks"
|
|
9
|
+
require "badline/c128/bus_state"
|
|
10
|
+
require "badline/c128/bus"
|
|
11
|
+
require "badline/c128/cpu"
|
|
12
|
+
require "badline/c128/saved_state"
|
|
13
|
+
require "badline/c128/keys"
|
|
14
|
+
require "badline/c128/modes"
|
|
15
|
+
require "badline/c128/drives"
|
|
16
|
+
require "badline/c128/fast_serial"
|
|
17
|
+
require "badline/drive1571"
|
|
18
|
+
|
|
19
|
+
module Badline
|
|
20
|
+
# The Commodore 128: the 8502 on the VIC-IIe's clock, two CIAs, the SID,
|
|
21
|
+
# the VDC and its 80 column display, and 128K of RAM, which the 8722 MMU
|
|
22
|
+
# maps (C128::Bus).
|
|
23
|
+
#
|
|
24
|
+
# Built for C128 mode, it runs BASIC 7.0 and the C128 KERNAL from reset.
|
|
25
|
+
# The C128's Z80 runs first and hands the bus to the 8502, but here the
|
|
26
|
+
# 8502 starts at its reset vector, with the MMU as the Z80 leaves it.
|
|
27
|
+
# The KERNAL goes to C64 mode as on the C128, with C= held at reset, a
|
|
28
|
+
# C64 cartridge or GO64.
|
|
29
|
+
#
|
|
30
|
+
# Built for C64 mode, the default, it powers on in C64 mode, the state
|
|
31
|
+
# the C128 KERNAL reaches when C= is held at power-on, and a reset comes
|
|
32
|
+
# back there. It runs the C64's BASIC and KERNAL, so the C64's KERNAL
|
|
33
|
+
# traps, the keyboard buffer and CHROUT capture work unchanged.
|
|
34
|
+
#
|
|
35
|
+
# Where a C64 program sees it differ from a C64C: the 8502's P6 senses CAPS
|
|
36
|
+
# LOCK, $D02F drives the extra keyboard rows, $D030's FAST bit runs the
|
|
37
|
+
# CPU at 2 MHz and its TEST bit races the raster counter, the SID has no
|
|
38
|
+
# mirrors, and $D500-$D7FF holds the hidden MMU, the VDC and open bus.
|
|
39
|
+
class C128
|
|
40
|
+
include IntegerHelper
|
|
41
|
+
include KeyboardBuffer
|
|
42
|
+
include Computer::Attachments
|
|
43
|
+
include Computer::KernalTraps
|
|
44
|
+
include Keys
|
|
45
|
+
include Modes
|
|
46
|
+
include Drives
|
|
47
|
+
include FastSerial
|
|
48
|
+
|
|
49
|
+
# The C64's 8x8 matrix and the three rows K0-K2 select, in port B
|
|
50
|
+
# column order.
|
|
51
|
+
KEYBOARD_MATRIX = (Keyboard::C64_MATRIX + [
|
|
52
|
+
%i[help keypad8 keypad5 tab keypad2 keypad4 keypad7 keypad1],
|
|
53
|
+
%i[esc keypad_plus keypad_minus line_feed keypad_enter keypad6 keypad9 keypad3],
|
|
54
|
+
%i[alt keypad0 keypad_period crsr_up crsr_down crsr_left crsr_right no_scroll]
|
|
55
|
+
]).freeze
|
|
56
|
+
|
|
57
|
+
# The cycle at which #on_init's handlers run, once the KERNAL of the
|
|
58
|
+
# mode the machine is built for has booted: the C64's as on a C64, or
|
|
59
|
+
# BASIC 7.0's, which is READY by then.
|
|
60
|
+
C128_INIT_THRESHOLD = 2_000_000
|
|
61
|
+
|
|
62
|
+
# The C128 KERNAL's keyboard buffer and its count.
|
|
63
|
+
C128_KEYBOARD_BUFFER = 0x034a
|
|
64
|
+
C128_KEYBOARD_COUNT = 0xd0
|
|
65
|
+
|
|
66
|
+
attr_reader :cpu, :cycles, :drive1541, :model, :init_threshold
|
|
67
|
+
|
|
68
|
+
# The path of the disk or directory device 8 serves through the traps
|
|
69
|
+
# (Computer::Attachments), or an empty one.
|
|
70
|
+
def mounted_path = @drive.nil? ? "" : @drive.path
|
|
71
|
+
|
|
72
|
+
def family = :c128
|
|
73
|
+
|
|
74
|
+
def address_bus = @bus
|
|
75
|
+
|
|
76
|
+
def region = @bus.region
|
|
77
|
+
|
|
78
|
+
def vic = @bus.vic
|
|
79
|
+
|
|
80
|
+
def cia1 = @bus.cia1
|
|
81
|
+
|
|
82
|
+
def cia2 = @bus.cia2
|
|
83
|
+
|
|
84
|
+
def sid = @bus.sid
|
|
85
|
+
|
|
86
|
+
def vdc = @bus.vdc
|
|
87
|
+
|
|
88
|
+
def mmu = @bus.mmu
|
|
89
|
+
|
|
90
|
+
def ram = @bus.ram
|
|
91
|
+
|
|
92
|
+
def keyboard = @bus.keyboard
|
|
93
|
+
|
|
94
|
+
def joystick1 = @bus.joystick1
|
|
95
|
+
|
|
96
|
+
def joystick2 = @bus.joystick2
|
|
97
|
+
|
|
98
|
+
def control_ports = @bus.control_ports
|
|
99
|
+
|
|
100
|
+
def datasette = @bus.datasette
|
|
101
|
+
|
|
102
|
+
# :c64 or :c128, the mode the MMU is in.
|
|
103
|
+
def mode = @bus.mmu.mode
|
|
104
|
+
|
|
105
|
+
def install_debug_register(&) = @bus.install_debug_register(&)
|
|
106
|
+
|
|
107
|
+
# +model+ names one of Model::ALL, and +sid_model+, when not nil, the
|
|
108
|
+
# SID in place of the model's. +mode+, :c64 or :c128, is the mode it
|
|
109
|
+
# powers on and resets into.
|
|
110
|
+
def initialize(model: "c128", sid_model: nil, mode: :c64, debug: false)
|
|
111
|
+
raise ArgumentError, "no C128 mode named #{mode}" unless %i[c64 c128].include?(mode)
|
|
112
|
+
|
|
113
|
+
@model = Model.named(model)
|
|
114
|
+
@c64_built = mode == :c64
|
|
115
|
+
@init_threshold = @c64_built ? Computer::INIT_THRESHOLD : C128_INIT_THRESHOLD
|
|
116
|
+
@holding_commodore = false
|
|
117
|
+
@bus = Bus.new(@model, sid_model: sid_model || @model.sid_model, mode:)
|
|
118
|
+
@cpu = CPU.new(@bus, debug:)
|
|
119
|
+
@vic = @bus.vic
|
|
120
|
+
@vic.open_bus = -> { @bus.ram.peek(@cpu.program_counter) }
|
|
121
|
+
@cia1 = @bus.cia1
|
|
122
|
+
@cia2 = @bus.cia2
|
|
123
|
+
@sid = @bus.sid
|
|
124
|
+
@vdc = @bus.vdc
|
|
125
|
+
@datasette = @bus.datasette
|
|
126
|
+
@cycles = 0
|
|
127
|
+
@clock_bits = 0
|
|
128
|
+
@nmi_asserted = false
|
|
129
|
+
@cartridge_nmi = false
|
|
130
|
+
@restore_pulse = false
|
|
131
|
+
@init_handlers = []
|
|
132
|
+
@pending_keys = nil
|
|
133
|
+
@capture_output = nil
|
|
134
|
+
init_drives
|
|
135
|
+
enter_c64_mode if @c64_built
|
|
136
|
+
@bus.on_mode_change { mode_changed }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The chips clock ahead of the CPU, as on the C64. $D030's FAST and
|
|
140
|
+
# TEST bits take hold a cycle after the write that sets them: in FAST
|
|
141
|
+
# mode the CPU runs in both halves of the cycle (#clock_fast), and the
|
|
142
|
+
# TEST bit steps the raster counter in every cycle.
|
|
143
|
+
def cycle!
|
|
144
|
+
handle_init if @cycles == @init_threshold
|
|
145
|
+
feed_keyboard if @pending_keys
|
|
146
|
+
|
|
147
|
+
clock_bits = @clock_bits
|
|
148
|
+
@clock_bits = @vic.clock_bits
|
|
149
|
+
@vic.test_step! if clock_bits >= 0x02
|
|
150
|
+
|
|
151
|
+
@vic.cycle!
|
|
152
|
+
@cia1.cycle!
|
|
153
|
+
@cia2.cycle!
|
|
154
|
+
@sid.cycle!
|
|
155
|
+
@datasette.cycle!
|
|
156
|
+
@vdc.cycle!
|
|
157
|
+
|
|
158
|
+
@cpu.irq = @cia1.interrupted? || @vic.interrupted?
|
|
159
|
+
|
|
160
|
+
drive_nmi
|
|
161
|
+
clock_bits.odd? ? clock_fast : clock_cpu
|
|
162
|
+
clock_serial_bus
|
|
163
|
+
|
|
164
|
+
@cycles += 1
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Runs `count` cycles, one #cycle! after another.
|
|
168
|
+
def run_cycles(count)
|
|
169
|
+
i = 0
|
|
170
|
+
while i < count
|
|
171
|
+
cycle!
|
|
172
|
+
i += 1
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Runs cycles until the block returns true or the cycle count passes
|
|
177
|
+
# `limit`, checking before each cycle.
|
|
178
|
+
def run_until(limit)
|
|
179
|
+
cycle! until yield || @cycles > limit
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def on_init(&block)
|
|
183
|
+
if @cycles < init_threshold
|
|
184
|
+
@init_handlers << block
|
|
185
|
+
else
|
|
186
|
+
block.call
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The clock, the raster and the crop of the VIC-IIe's region.
|
|
191
|
+
def timing
|
|
192
|
+
region = @bus.region
|
|
193
|
+
Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
|
|
194
|
+
lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# The chip whose #display a front end shows: the VIC-IIe, or with
|
|
198
|
+
# :vdc the VDC, whose display paints only while its #render is on.
|
|
199
|
+
def video(chip = :vic) = chip == :vdc ? @vdc : @vic
|
|
200
|
+
|
|
201
|
+
# Renders the VDC's display, and not the VIC-IIe's, while a front end
|
|
202
|
+
# shows it, or with false the other way round.
|
|
203
|
+
def vdc_shown=(shown)
|
|
204
|
+
@vdc.render = shown
|
|
205
|
+
@vic.render = !shown
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The chip a front end records the machine's sound from.
|
|
209
|
+
def sound_source = @sid
|
|
210
|
+
|
|
211
|
+
def load_prg(data)
|
|
212
|
+
uint16(data[0], data[1]).tap do |load_addr|
|
|
213
|
+
ram.write(load_addr, data[2..])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A cartridge goes in with the power off.
|
|
218
|
+
def attach_cartridge(cartridge)
|
|
219
|
+
connect_cartridge(cartridge)
|
|
220
|
+
power_cycle!
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# RAM, the VIC and the VDC start from their power-on state, and the RES
|
|
224
|
+
# line resets everything else.
|
|
225
|
+
def power_cycle!
|
|
226
|
+
@vic.power_on!
|
|
227
|
+
@vdc.power_on!
|
|
228
|
+
@bus.power_on!
|
|
229
|
+
reset!
|
|
230
|
+
enter_c64_mode if @c64_built
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The RES line reaches the CPU and its port, the MMU, both CIAs, the
|
|
234
|
+
# SID, the cartridge port and, through the serial bus, the drives.
|
|
235
|
+
def reset!
|
|
236
|
+
@bus.reset!
|
|
237
|
+
@cia1.reset!
|
|
238
|
+
@cia2.reset!
|
|
239
|
+
@sid.reset!
|
|
240
|
+
@bus.cartridge&.reset
|
|
241
|
+
@drive&.reset!
|
|
242
|
+
@drive1541&.reset!
|
|
243
|
+
@drive1571&.reset!
|
|
244
|
+
@nmi_asserted = false
|
|
245
|
+
@cpu.reset!
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# CHROUT is at $FFD2 in both KERNALs, and the trap follows the mode.
|
|
249
|
+
def capture_output
|
|
250
|
+
@capture_output ||= ChroutTrap.new(cpu:, bus: @bus, layout: trap_layout).tap do |trap|
|
|
251
|
+
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def inspect
|
|
256
|
+
"#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private
|
|
260
|
+
|
|
261
|
+
# The C128 KERNAL leaves $D02F at $FF, every extra keyboard row
|
|
262
|
+
# deselected, before it jumps to the C64's reset.
|
|
263
|
+
def enter_c64_mode
|
|
264
|
+
@vic.poke(0xd02f, 0xff)
|
|
265
|
+
@bus.control_ports.extra_rows = 0xff
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The NMI line is wired-OR between CIA 2, the cartridge and RESTORE,
|
|
269
|
+
# and the CPU takes an interrupt on its falling edge.
|
|
270
|
+
def drive_nmi
|
|
271
|
+
nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
|
|
272
|
+
@restore_pulse = false
|
|
273
|
+
@cpu.nmi = true if nmi && !@nmi_asserted
|
|
274
|
+
@nmi_asserted = nmi
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# BA halts the CPU on a read cycle.
|
|
278
|
+
def clock_cpu
|
|
279
|
+
@cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The 8502 at 2 MHz takes both halves of the cycle, and BA no longer
|
|
283
|
+
# halts it. It leaves phi1 to the VIC in the refresh cycles, and an
|
|
284
|
+
# access to I/O, whose chips run at 1 MHz, waits for phi2. The VIC's
|
|
285
|
+
# fetches in the cycle see the bytes on the bus. In a phi1 the CPU
|
|
286
|
+
# spends waiting that is the byte it writes, or the last byte the bus
|
|
287
|
+
# held.
|
|
288
|
+
def clock_fast
|
|
289
|
+
phi1 = 0xff
|
|
290
|
+
unless @vic.refresh_cycle?
|
|
291
|
+
held = @bus.data
|
|
292
|
+
write = @cpu.pending_write?
|
|
293
|
+
@cpu.cycle!
|
|
294
|
+
if @bus.io_access?
|
|
295
|
+
data = @bus.data
|
|
296
|
+
return @vic.take_cpu_bus(write ? data : held, data, @bus.vic_access?)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
phi1 = @bus.data
|
|
300
|
+
end
|
|
301
|
+
@cpu.cycle!
|
|
302
|
+
@vic.take_cpu_bus(phi1, @bus.data, @bus.vic_access?)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def handle_init
|
|
306
|
+
@init_handlers.each(&:call)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The keyboard buffer of the KERNAL the machine runs: the C64's, or in
|
|
310
|
+
# C128 mode BASIC 7.0's.
|
|
311
|
+
def feed_keyboard
|
|
312
|
+
c64 = mode == :c64
|
|
313
|
+
count = c64 ? KeyboardBuffer::COUNT : C128_KEYBOARD_COUNT
|
|
314
|
+
return unless ram.peek(count).zero?
|
|
315
|
+
|
|
316
|
+
chunk = @pending_keys.shift(KeyboardBuffer::CAPACITY)
|
|
317
|
+
ram.write(c64 ? KeyboardBuffer::ADDRESS : C128_KEYBOARD_BUFFER, chunk)
|
|
318
|
+
ram.poke(count, chunk.length)
|
|
319
|
+
@pending_keys = nil if @pending_keys.empty?
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
data/lib/badline/chrout_trap.rb
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CIA
|
|
5
|
+
# Running a CIA a stretch of cycles at once, for Drive::Idle.
|
|
6
|
+
module FastForward
|
|
7
|
+
# Whether fast_forward can run the CIA: timer B stopped with nothing
|
|
8
|
+
# in its pipeline, no interrupt or acknowledge on its way, CNT where it
|
|
9
|
+
# was, the TOD clock stopped, and timer A either stopped, or free
|
|
10
|
+
# running (Timer#free_running?) with its interrupt masked and the
|
|
11
|
+
# serial port an idle input. Drive::Idle sleeps the 1571's CIA through
|
|
12
|
+
# such cycles, which its DOS leaves timer A running in.
|
|
13
|
+
def quiet?
|
|
14
|
+
return false unless @tb.idle? && @icr.quiet && @tod.quiet? && cnt_steady?
|
|
15
|
+
|
|
16
|
+
@ta.idle? ? @serial.idle : timer_a_free_running?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Runs +cycles+ cycles at once while quiet?, as cycle! would: timer A's
|
|
20
|
+
# underflows flag in the ICR, and reach the serial port's underflow
|
|
21
|
+
# line.
|
|
22
|
+
def fast_forward(cycles)
|
|
23
|
+
@tod.fast_forward(cycles)
|
|
24
|
+
return if @ta.idle?
|
|
25
|
+
|
|
26
|
+
@icr.flag(:timer_a) if @ta.fast_forward(cycles).positive?
|
|
27
|
+
@serial.follow_underflow(@ta.underflowed)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Timer A free running with its interrupt masked, and the serial port
|
|
33
|
+
# an idle input it only clocks.
|
|
34
|
+
def timer_a_free_running?
|
|
35
|
+
@ta.free_running? && !@control_a.serial_mode? && @icr.mask.value.nobits?(0x01) && @serial.drained?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether CNT holds the level the last cycle sampled.
|
|
39
|
+
def cnt_steady? = @serial.cnt == @cnt_high && !@cnt_rise
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/badline/cia/serial.rb
CHANGED
|
@@ -79,6 +79,18 @@ module Badline
|
|
|
79
79
|
@control.serial_mode?
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Whether nothing is counting down or in the delay lines, so only
|
|
83
|
+
# timer A's underflows reach the register.
|
|
84
|
+
def drained? = idle?
|
|
85
|
+
|
|
86
|
+
# Takes timer A's underflow line as cycle! would have left it, with
|
|
87
|
+
# +underflowed+ its level on the last cycle, for a CIA run without
|
|
88
|
+
# the register in use (CIA#fast_forward).
|
|
89
|
+
def follow_underflow(underflowed)
|
|
90
|
+
@underflow_high = underflowed
|
|
91
|
+
@idle = !underflowed
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
def cnt_in=(level)
|
|
83
95
|
@cnt_in = level
|
|
84
96
|
@cnt = level unless output?
|