badline 0.1.0 → 0.2.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 +185 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +96 -25
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +132 -16
- data/lib/badline/cia/interrupt_register.rb +102 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +138 -88
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +97 -12
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +169 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +109 -168
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +51 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +277 -0
- data/lib/badline/kernal_trap/file.rb +5 -25
- data/lib/badline/kernal_trap/load.rb +86 -24
- data/lib/badline/kernal_trap/routine.rb +51 -0
- data/lib/badline/kernal_trap/save.rb +46 -14
- data/lib/badline/kernal_trap/serial.rb +171 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/memory.rb +5 -0
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +251 -23
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +99 -13
- data/lib/badline/storage/host_directory.rb +30 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +389 -55
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +104 -4
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# An AMD 29F0x0 flash chip, driven through the JEDEC command set in the
|
|
6
|
+
# AMD datasheets. Each command opens with two unlock cycles, $AA to the
|
|
7
|
+
# first unlock address and $55 to the second, and a third write picks
|
|
8
|
+
# the command:
|
|
9
|
+
#
|
|
10
|
+
# $90 autoselect: offset 0 reads the manufacturer, 1 the device and 2
|
|
11
|
+
# the sector protection, until a reset
|
|
12
|
+
# $F0 reset to reading the array, which a lone $F0 also does
|
|
13
|
+
# $A0 byte program: the next write clears bits of the array, which
|
|
14
|
+
# only an erase sets again
|
|
15
|
+
# $80 erase, after two more unlock cycles: $10 to the first unlock
|
|
16
|
+
# address erases the chip, and $30 to a sector erases the sector.
|
|
17
|
+
# Further sectors can follow within the 50 us window.
|
|
18
|
+
#
|
|
19
|
+
# While a program or erase runs, a read returns status: DQ7 the
|
|
20
|
+
# complement of the programmed bit (0 while erasing), DQ6 toggling on
|
|
21
|
+
# every read, DQ5 set when a program failed and DQ3 set once an erase
|
|
22
|
+
# has left its window. $B0 suspends a sector erase and $30 resumes it.
|
|
23
|
+
#
|
|
24
|
+
# The cartridge reads the array through windows, and the chip swaps
|
|
25
|
+
# them for status windows while it is busy or in autoselect.
|
|
26
|
+
class Flash
|
|
27
|
+
Model = Data.define(:size, :sector_size, :unlock1, :unlock2, :unlock_mask, :manufacturer, :device)
|
|
28
|
+
|
|
29
|
+
AM29F040 = Model.new(size: 0x80000, sector_size: 0x10000, unlock1: 0x5555, unlock2: 0x2aaa,
|
|
30
|
+
unlock_mask: 0x7fff, manufacturer: 0x01, device: 0xa4)
|
|
31
|
+
AM29F040B = Model.new(size: 0x80000, sector_size: 0x10000, unlock1: 0x555, unlock2: 0x2aa,
|
|
32
|
+
unlock_mask: 0x7ff, manufacturer: 0x01, device: 0xa4)
|
|
33
|
+
AM29F010 = Model.new(size: 0x20000, sector_size: 0x4000, unlock1: 0x5555, unlock2: 0x2aaa,
|
|
34
|
+
unlock_mask: 0x7fff, manufacturer: 0x01, device: 0x20)
|
|
35
|
+
|
|
36
|
+
# The datasheet's typical times, in cycles at 1 MHz.
|
|
37
|
+
PROGRAM_CYCLES = 7
|
|
38
|
+
ERASE_WINDOW_CYCLES = 50
|
|
39
|
+
SECTOR_ERASE_CYCLES = 1_000_000
|
|
40
|
+
CHIP_ERASE_CYCLES = 8_000_000
|
|
41
|
+
|
|
42
|
+
# The array, read directly while the chip reads it.
|
|
43
|
+
class Window
|
|
44
|
+
def initialize(flash, base)
|
|
45
|
+
@flash = flash
|
|
46
|
+
@data = flash.data
|
|
47
|
+
@base = base
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def peek(addr)
|
|
51
|
+
@data[@base | (addr & 0x1fff)]
|
|
52
|
+
end
|
|
53
|
+
alias [] peek
|
|
54
|
+
|
|
55
|
+
def poke(addr, value)
|
|
56
|
+
@flash.write(@base | (addr & 0x1fff), value)
|
|
57
|
+
end
|
|
58
|
+
alias []= poke
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reads through the chip, for its status and autoselect codes.
|
|
62
|
+
class StatusWindow < Window
|
|
63
|
+
def peek(addr)
|
|
64
|
+
@flash.read(@base | (addr & 0x1fff))
|
|
65
|
+
end
|
|
66
|
+
alias [] peek
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
attr_reader :data, :state
|
|
70
|
+
attr_writer :clock
|
|
71
|
+
|
|
72
|
+
def initialize(data, model: AM29F040B, clock: -> { 0 })
|
|
73
|
+
@model = model
|
|
74
|
+
@data = data + ([0xff] * (model.size - data.length))
|
|
75
|
+
@clock = clock
|
|
76
|
+
@windows = {}
|
|
77
|
+
@status_windows = {}
|
|
78
|
+
@on_change = nil
|
|
79
|
+
@state = :read
|
|
80
|
+
@base_state = :read
|
|
81
|
+
@cycle = 0
|
|
82
|
+
@toggle = 0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Called when the chip starts or stops answering reads from the array.
|
|
86
|
+
def on_change(&block)
|
|
87
|
+
@on_change = block
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def window(base)
|
|
91
|
+
if array_mode?
|
|
92
|
+
@windows[base] ||= Window.new(self, base)
|
|
93
|
+
else
|
|
94
|
+
@status_windows[base] ||= StatusWindow.new(self, base)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def array_mode?
|
|
99
|
+
@state == :read || @state == :program_setup
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def read(offset)
|
|
103
|
+
settle
|
|
104
|
+
case @state
|
|
105
|
+
when :read then @data[offset]
|
|
106
|
+
when :autoselect then autoselect_code(offset)
|
|
107
|
+
when :suspended then suspended_read(offset)
|
|
108
|
+
else status
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def write(offset, value)
|
|
113
|
+
settle
|
|
114
|
+
case @state
|
|
115
|
+
when :program_setup then program(offset, value)
|
|
116
|
+
when :erase_window then extend_erase(offset, value)
|
|
117
|
+
when :erasing then suspend if value == 0xb0
|
|
118
|
+
when :suspended then resume if value == 0x30
|
|
119
|
+
when :program_error then enter(:read, base: :read) if value == 0xf0
|
|
120
|
+
when :programming, :chip_erase then nil
|
|
121
|
+
else command(offset, value)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def command(offset, value)
|
|
128
|
+
if value == 0xf0
|
|
129
|
+
@cycle = 0
|
|
130
|
+
return enter(:read, base: :read)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
address = offset & @model.unlock_mask
|
|
134
|
+
@cycle = case @cycle
|
|
135
|
+
when 0, 3 then address == @model.unlock1 && value == 0xaa ? @cycle + 1 : 0
|
|
136
|
+
when 1, 4 then address == @model.unlock2 && value == 0x55 ? @cycle + 1 : 0
|
|
137
|
+
when 2 then command_cycle(address, value)
|
|
138
|
+
when 5 then erase_command(offset, address, value)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def command_cycle(address, value)
|
|
143
|
+
return 0 unless address == @model.unlock1
|
|
144
|
+
|
|
145
|
+
case value
|
|
146
|
+
when 0x90 then enter(:autoselect, base: :autoselect)
|
|
147
|
+
when 0xa0 then @state = :program_setup
|
|
148
|
+
when 0x80 then return 3
|
|
149
|
+
end
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def erase_command(offset, address, value)
|
|
154
|
+
if value == 0x10 && address == @model.unlock1
|
|
155
|
+
start_busy(:chip_erase, CHIP_ERASE_CYCLES)
|
|
156
|
+
elsif value == 0x30
|
|
157
|
+
@erasing_sectors = [sector(offset)]
|
|
158
|
+
start_busy(:erase_window, ERASE_WINDOW_CYCLES)
|
|
159
|
+
end
|
|
160
|
+
0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def program(offset, value)
|
|
164
|
+
@programmed = value
|
|
165
|
+
@data[offset] &= value
|
|
166
|
+
if @data[offset] == value
|
|
167
|
+
start_busy(:programming, PROGRAM_CYCLES)
|
|
168
|
+
else
|
|
169
|
+
enter(:program_error)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def extend_erase(offset, value)
|
|
174
|
+
case value
|
|
175
|
+
when 0x30
|
|
176
|
+
@erasing_sectors |= [sector(offset)]
|
|
177
|
+
@done_at = now + ERASE_WINDOW_CYCLES
|
|
178
|
+
when 0xb0
|
|
179
|
+
@remaining = @erasing_sectors.length * SECTOR_ERASE_CYCLES
|
|
180
|
+
enter(:suspended)
|
|
181
|
+
else
|
|
182
|
+
@erasing_sectors = nil
|
|
183
|
+
enter(@base_state)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def suspend
|
|
188
|
+
@remaining = @done_at - now
|
|
189
|
+
enter(:suspended)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def resume
|
|
193
|
+
start_busy(:erasing, @remaining)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Moves a timed operation on once the clock has passed its end.
|
|
197
|
+
def settle
|
|
198
|
+
return unless @done_at && now >= @done_at
|
|
199
|
+
|
|
200
|
+
case @state
|
|
201
|
+
when :erase_window
|
|
202
|
+
start_busy(:erasing, @done_at - now + (@erasing_sectors.length * SECTOR_ERASE_CYCLES))
|
|
203
|
+
settle
|
|
204
|
+
when :erasing
|
|
205
|
+
@erasing_sectors.each { |s| @data.fill(0xff, s * @model.sector_size, @model.sector_size) }
|
|
206
|
+
@erasing_sectors = nil
|
|
207
|
+
finish
|
|
208
|
+
when :chip_erase
|
|
209
|
+
@data.fill(0xff)
|
|
210
|
+
finish
|
|
211
|
+
else
|
|
212
|
+
finish
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def finish
|
|
217
|
+
@done_at = nil
|
|
218
|
+
enter(@base_state)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def start_busy(state, cycles)
|
|
222
|
+
@done_at = now + cycles
|
|
223
|
+
@programmed = 0xff unless state == :programming
|
|
224
|
+
enter(state)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def enter(state, base: @base_state)
|
|
228
|
+
was_array = array_mode?
|
|
229
|
+
@state = state
|
|
230
|
+
@base_state = base
|
|
231
|
+
@done_at = nil unless %i[programming erase_window erasing chip_erase].include?(state)
|
|
232
|
+
@on_change&.call if was_array != array_mode?
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def status
|
|
236
|
+
@toggle ^= 0x40
|
|
237
|
+
value = (~@programmed & 0x80) | @toggle
|
|
238
|
+
value |= 0x20 if @state == :program_error
|
|
239
|
+
value |= 0x08 if @state == :erasing || @state == :chip_erase
|
|
240
|
+
value
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def suspended_read(offset)
|
|
244
|
+
@erasing_sectors.include?(sector(offset)) ? 0x80 : @data[offset]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def autoselect_code(offset)
|
|
248
|
+
case offset & 0x43
|
|
249
|
+
when 0 then @model.manufacturer
|
|
250
|
+
when 1 then @model.device
|
|
251
|
+
when 2 then 0x00
|
|
252
|
+
else @data[offset]
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def sector(offset)
|
|
257
|
+
offset / @model.sector_size
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def now
|
|
261
|
+
@clock.call
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# The freeze button of a freezer cartridge. A press pulls NMI, and the
|
|
6
|
+
# computer calls #freeze! once the CPU has pushed the return address and
|
|
7
|
+
# status of the interrupt, so the vector fetch that follows reads the
|
|
8
|
+
# cartridge ROM in Ultimax mode. The cartridge lets go of NMI when its
|
|
9
|
+
# software acknowledges the freeze.
|
|
10
|
+
module Freezer
|
|
11
|
+
def press_button
|
|
12
|
+
@button = true
|
|
13
|
+
self.nmi = true if freeze_allowed?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def release_button
|
|
17
|
+
@button = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def freeze_allowed?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def button_pressed?
|
|
27
|
+
@button == true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# Fun Play / Power Play: up to 16 8K banks. A write to I/O 1 selects the
|
|
6
|
+
# bank from bits 3-5 and 0, and either 8K mode ($00 in bits 7, 6, 2 and 1)
|
|
7
|
+
# or no ROM ($86 in those bits).
|
|
8
|
+
class FunPlay < Cartridge
|
|
9
|
+
def poke(addr, value)
|
|
10
|
+
return if addr > 0xdeff
|
|
11
|
+
|
|
12
|
+
@roml = bank(@banks, self.class.bank_number(value))
|
|
13
|
+
case value & 0xc6
|
|
14
|
+
when 0x00 then self.mode = :rom8k
|
|
15
|
+
when 0x86 then self.mode = :off
|
|
16
|
+
end
|
|
17
|
+
changed!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The CRT file numbers each bank by the register value that selects it.
|
|
21
|
+
def reset
|
|
22
|
+
poke(0xde00, 0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.bank_number(value)
|
|
26
|
+
((value >> 3) & 0x07) | ((value & 0x01) << 3)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def install_chips(chips)
|
|
32
|
+
@banks = []
|
|
33
|
+
chips.each { |chip| @banks[self.class.bank_number(chip.bank)] = rom_bank(chip.data) }
|
|
34
|
+
reset
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# GMod2: an Am29F040 flash chip of 64 8K banks at ROML. A write to I/O 1
|
|
6
|
+
# selects the bank from bits 0-5. Bit 6 selects the EEPROM, which
|
|
7
|
+
# switches the ROM out, and bits 6 and 7 together enable flash writes:
|
|
8
|
+
# the cartridge then asserts Ultimax on write cycles only, so reads see
|
|
9
|
+
# the C64's memory and writes to $E000-$FFFF reach the flash. The EEPROM
|
|
10
|
+
# isn't emulated.
|
|
11
|
+
class GMod2 < Cartridge
|
|
12
|
+
BANKS = 64
|
|
13
|
+
|
|
14
|
+
attr_reader :flash
|
|
15
|
+
|
|
16
|
+
def clock=(clock)
|
|
17
|
+
super
|
|
18
|
+
@flash.clock = clock
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def poke(addr, value)
|
|
22
|
+
return if addr > 0xdeff
|
|
23
|
+
|
|
24
|
+
@bank = value & 0x3f
|
|
25
|
+
@flash_writes = value.allbits?(0xc0)
|
|
26
|
+
self.mode = value.anybits?(0x40) ? :off : :rom8k
|
|
27
|
+
select_bank
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def romh_writes
|
|
31
|
+
@roml if @flash_writes
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reset selects the first bank in 8K mode, and leaves the flash as it
|
|
35
|
+
# is.
|
|
36
|
+
def reset
|
|
37
|
+
@bank = 0
|
|
38
|
+
@flash_writes = false
|
|
39
|
+
self.mode = :rom8k
|
|
40
|
+
select_bank
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def select_bank
|
|
46
|
+
@roml = @flash.window(@bank * BANK_SIZE)
|
|
47
|
+
changed!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def install_chips(chips)
|
|
51
|
+
data = Array.new(BANKS * BANK_SIZE, 0xff)
|
|
52
|
+
chips.each do |chip|
|
|
53
|
+
bytes = chip.data.first(BANK_SIZE)
|
|
54
|
+
data[(chip.bank & 0x3f) * BANK_SIZE, bytes.length] = bytes
|
|
55
|
+
end
|
|
56
|
+
@flash = Flash.new(data, model: Flash::AM29F040)
|
|
57
|
+
@flash.on_change { select_bank }
|
|
58
|
+
reset
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# C64 Game System / System 3: up to 64 8K banks. Any access to
|
|
6
|
+
# $DE00-$DEFF selects the bank from the low six address bits; the data
|
|
7
|
+
# is ignored.
|
|
8
|
+
class GameSystem < Cartridge
|
|
9
|
+
def readable_io_pages
|
|
10
|
+
[0xde]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def peek(addr)
|
|
14
|
+
select(addr) if addr < 0xdf00
|
|
15
|
+
open_bus(addr)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poke(addr, _value)
|
|
19
|
+
select(addr) if addr < 0xdf00
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset
|
|
23
|
+
select(0)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def select(addr)
|
|
29
|
+
@roml = bank(@banks, addr & 0x3f)
|
|
30
|
+
changed!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def install_chips(chips)
|
|
34
|
+
@banks = banks_from(chips).first
|
|
35
|
+
self.mode = :rom8k
|
|
36
|
+
reset
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# The KCS Power Cartridge: 16K of ROM, 128 bytes of RAM and a freeze
|
|
6
|
+
# button. It boots in 16K mode, and any access to I/O 1 sets the lines
|
|
7
|
+
# from address bit 1 and the direction:
|
|
8
|
+
#
|
|
9
|
+
# read, bit 1 clear 8K
|
|
10
|
+
# read, bit 1 set off
|
|
11
|
+
# write, bit 1 clear 16K
|
|
12
|
+
# write, bit 1 set Ultimax
|
|
13
|
+
#
|
|
14
|
+
# A read of I/O 1 returns the second last page of ROML. I/O 2 holds the
|
|
15
|
+
# RAM in its lower half, and the upper half reads the EXROM line in bit 7
|
|
16
|
+
# and GAME in bit 6, which the freezer checks to find the mode it
|
|
17
|
+
# interrupted. A freeze switches to Ultimax and lets go of NMI.
|
|
18
|
+
class KCSPower < Cartridge
|
|
19
|
+
include Freezer
|
|
20
|
+
|
|
21
|
+
IO1_ROM_PAGE = 0x1e00
|
|
22
|
+
|
|
23
|
+
def readable_io_pages
|
|
24
|
+
[0xde, 0xdf]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def peek(addr)
|
|
28
|
+
if addr < 0xdf00
|
|
29
|
+
switch(addr.anybits?(0x02) ? :off : :rom8k)
|
|
30
|
+
@roml.peek(IO1_ROM_PAGE | (addr & 0xff))
|
|
31
|
+
elsif addr.anybits?(0x80)
|
|
32
|
+
(@exrom << 7) | (@game << 6) | (open_bus(addr) & 0x3f)
|
|
33
|
+
else
|
|
34
|
+
@ram[addr & 0x7f]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def poke(addr, value)
|
|
39
|
+
if addr < 0xdf00
|
|
40
|
+
switch(addr.anybits?(0x02) ? :ultimax : :rom16k)
|
|
41
|
+
elsif addr.nobits?(0x80)
|
|
42
|
+
@ram[addr & 0x7f] = value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset
|
|
47
|
+
self.nmi = false
|
|
48
|
+
switch(:rom16k)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def freeze!
|
|
52
|
+
self.nmi = false
|
|
53
|
+
switch(:ultimax)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def switch(mode)
|
|
59
|
+
self.mode = mode
|
|
60
|
+
changed!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def install_chips(chips)
|
|
64
|
+
roml, romh = banks_from(chips)
|
|
65
|
+
@roml = roml.first || EMPTY_BANK
|
|
66
|
+
@romh = romh.first || EMPTY_BANK
|
|
67
|
+
@ram = Array.new(0x80, 0xff)
|
|
68
|
+
reset
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# 8K. Writing I/O 1 switches the ROM in, writing I/O 2 switches it out.
|
|
6
|
+
# Both I/O pages read the last 512 bytes of the ROM.
|
|
7
|
+
class Mach5 < Cartridge
|
|
8
|
+
def readable_io_pages
|
|
9
|
+
[0xde, 0xdf]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def peek(addr)
|
|
13
|
+
@roml.peek(addr)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def poke(addr, _value)
|
|
17
|
+
self.mode = addr < 0xdf00 ? :rom8k : :off
|
|
18
|
+
changed!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reset
|
|
22
|
+
self.mode = :rom8k
|
|
23
|
+
changed!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def install_chips(chips)
|
|
29
|
+
@roml = banks_from(chips).first.compact.first
|
|
30
|
+
reset
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -11,12 +11,15 @@ module Badline
|
|
|
11
11
|
changed!
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def reset
|
|
15
|
+
poke(0xde00, 0)
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
private
|
|
15
19
|
|
|
16
20
|
def install_chips(chips)
|
|
17
|
-
@banks =
|
|
18
|
-
|
|
19
|
-
@roml = @banks.first
|
|
21
|
+
@banks = banks_from(chips).first
|
|
22
|
+
reset
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
@@ -6,27 +6,26 @@ module Badline
|
|
|
6
6
|
def poke(addr, value)
|
|
7
7
|
return if addr > 0xdeff
|
|
8
8
|
|
|
9
|
-
select_bank((value & 0x3f) % @
|
|
9
|
+
select_bank((value & 0x3f) % @banks.length)
|
|
10
|
+
changed!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset
|
|
14
|
+
select_bank(0)
|
|
10
15
|
changed!
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
private
|
|
14
19
|
|
|
20
|
+
# In 16K mode the selected bank shows through ROMH as well.
|
|
15
21
|
def select_bank(number)
|
|
16
|
-
@roml = @
|
|
17
|
-
@romh = @
|
|
22
|
+
@roml = @banks[number]
|
|
23
|
+
@romh = @roml if game.zero?
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def install_chips(chips)
|
|
21
|
-
@
|
|
22
|
-
|
|
23
|
-
chips.each { |chip| install_chip(chip) }
|
|
24
|
-
select_bank(0)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def install_chip(chip)
|
|
28
|
-
@roml_banks[chip.bank] = rom_bank(chip.data, ROML_START)
|
|
29
|
-
@romh_banks[chip.bank] = rom_bank(chip.data, ROMH_START) if @romh_banks
|
|
27
|
+
@banks = banks_from(chips).first
|
|
28
|
+
reset
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Cartridge
|
|
5
|
+
# Pagefox: two 32K EPROMs and 32K of RAM, seen 16K at a time. A write to
|
|
6
|
+
# $DE80-$DEFF selects the half with bit 1 and the chip with bits 2-3
|
|
7
|
+
# (EPROM, EPROM, RAM, nothing), and switches the cartridge out with
|
|
8
|
+
# bit 4. Writes to the RAM land in the C64's RAM as well.
|
|
9
|
+
class Pagefox < Cartridge
|
|
10
|
+
RAM_CHIP = 2
|
|
11
|
+
EMPTY_CHIP = 3
|
|
12
|
+
|
|
13
|
+
def connect(ram:, open_bus:)
|
|
14
|
+
super
|
|
15
|
+
@ram_banks.each { |b| b.backing = ram }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poke(addr, value)
|
|
19
|
+
return if addr < 0xde80 || addr > 0xdeff
|
|
20
|
+
|
|
21
|
+
select(value)
|
|
22
|
+
changed!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset
|
|
26
|
+
select(0)
|
|
27
|
+
changed!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def select(value)
|
|
33
|
+
half = (value >> 1) & 0x01
|
|
34
|
+
chip = (value >> 2) & 0x03
|
|
35
|
+
@roml, @romh = window(chip, half)
|
|
36
|
+
self.mode = value.anybits?(0x10) ? :off : :rom16k
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def window(chip, half)
|
|
40
|
+
case chip
|
|
41
|
+
when RAM_CHIP then @ram_banks[half * 2, 2]
|
|
42
|
+
when EMPTY_CHIP then [@open_bus || EMPTY_BANK] * 2
|
|
43
|
+
else
|
|
44
|
+
number = (chip << 1) | half
|
|
45
|
+
[bank(@roml_banks, number), bank(@romh_banks, number)]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def install_chips(chips)
|
|
50
|
+
@roml_banks, @romh_banks = banks_from(chips)
|
|
51
|
+
@ram_banks = Array.new(4) { RAMBank.new }
|
|
52
|
+
reset
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|