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
data/lib/badline/cia.rb
CHANGED
|
@@ -1,68 +1,105 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "badline/cia/interrupt_register"
|
|
4
|
+
require "badline/cia/serial"
|
|
4
5
|
require "badline/cia/timer"
|
|
5
6
|
|
|
6
7
|
module Badline
|
|
7
8
|
# CIA (Complex Interface Adapter) chip
|
|
8
9
|
class CIA
|
|
9
10
|
include Addressable
|
|
10
|
-
extend Forwardable
|
|
11
11
|
|
|
12
|
-
attr_reader :start, :control_a, :control_b, :
|
|
12
|
+
attr_reader :start, :control_a, :control_b, :peripheral, :serial
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
def interrupt_status = @icr.status
|
|
15
|
+
|
|
16
|
+
def interrupt_control = @icr.mask
|
|
17
|
+
|
|
18
|
+
def interrupt!(delay = 1) = @icr.assert!(delay)
|
|
19
|
+
|
|
20
|
+
def timer_a = @ta.counter
|
|
21
|
+
|
|
22
|
+
def timer_a=(value)
|
|
23
|
+
@ta.counter = value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def timer_a_latch = @ta.latch
|
|
27
|
+
|
|
28
|
+
def timer_a_latch=(value)
|
|
29
|
+
@ta.latch = value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def timer_b = @tb.counter
|
|
33
|
+
|
|
34
|
+
def timer_b=(value)
|
|
35
|
+
@tb.counter = value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def timer_b_latch = @tb.latch
|
|
39
|
+
|
|
40
|
+
def timer_b_latch=(value)
|
|
41
|
+
@tb.latch = value
|
|
42
|
+
end
|
|
22
43
|
|
|
23
44
|
def initialize(start: 0, peripheral: nil)
|
|
24
45
|
addressable_at(start, length: 2**8)
|
|
25
46
|
|
|
26
47
|
@peripheral = peripheral
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
@port_b4_handler = nil
|
|
49
|
+
@port_b4_high = true
|
|
50
|
+
reset!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The RES line: ports back to inputs, timers stopped with their latches
|
|
54
|
+
# full, interrupts masked and the TOD clock stopped at 1:00:00.0. The
|
|
55
|
+
# PB4 level carries over and is resampled on the next cycle.
|
|
56
|
+
def reset!
|
|
57
|
+
@data_port_a = 0x00
|
|
58
|
+
@data_port_b = 0x00
|
|
59
|
+
@data_dir_a = 0x00
|
|
60
|
+
@data_dir_b = 0x00
|
|
61
|
+
@port_b4_driven_high = true
|
|
62
|
+
@cnt_high = true
|
|
63
|
+
@cnt_rise = false
|
|
33
64
|
@tod = TimeOfDay.new
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
@
|
|
37
|
-
|
|
38
|
-
@
|
|
39
|
-
|
|
40
|
-
@control_b = Status.new(%i[start output out_mode run_mode load
|
|
41
|
-
in_cnt in_timer_a alarm])
|
|
65
|
+
@icr = InterruptRegister.new
|
|
66
|
+
@icr_status = @icr.status
|
|
67
|
+
@control_a = ControlRegister.new(%i[start output out_mode run_mode load
|
|
68
|
+
in_mode serial_mode clock_frequency])
|
|
69
|
+
@control_b = ControlRegister.new(%i[start output out_mode run_mode load
|
|
70
|
+
in_cnt in_timer_a alarm])
|
|
42
71
|
@ta = Timer.new(@control_a)
|
|
43
72
|
@tb = Timer.new(@control_b)
|
|
73
|
+
@serial = Serial.new(@control_a)
|
|
44
74
|
end
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
# Register a change handler on the PB4 line. On CIA 1, this feeds the
|
|
77
|
+
# light pen input.
|
|
78
|
+
def on_port_b4_change(&handler)
|
|
79
|
+
@port_b4_handler = handler
|
|
48
80
|
end
|
|
49
81
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
# A falling edge on the FLAG pin. On CIA 1 the datasette's tape read
|
|
83
|
+
# line drives it, on CIA 2 the serial bus SRQ.
|
|
84
|
+
def flag! = raise_interrupt(:flag)
|
|
85
|
+
|
|
86
|
+
def interrupted? = @icr_status.value >= 0x80
|
|
53
87
|
|
|
54
88
|
def cycle!
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
update_timers
|
|
89
|
+
@icr.cycle! unless @icr.quiet
|
|
90
|
+
refresh_port_b4 if @port_b4_handler
|
|
91
|
+
level = @serial.cnt
|
|
92
|
+
level == @cnt_high ? @cnt_rise = false : cnt_edge(level)
|
|
93
|
+
underflowed = update_timers
|
|
94
|
+
@serial.cycle!(underflowed) { trigger_serial } if underflowed || !@serial.idle
|
|
60
95
|
@tod.cycle! { trigger_alarm }
|
|
61
96
|
end
|
|
62
97
|
|
|
63
98
|
def read_port_a
|
|
64
|
-
|
|
65
|
-
|
|
99
|
+
lines = driven_lines(@data_port_a, @data_dir_a)
|
|
100
|
+
return lines unless peripheral
|
|
101
|
+
|
|
102
|
+
lines & peripheral.read_a(lines, driven_lines(@data_port_b, @data_dir_b))
|
|
66
103
|
end
|
|
67
104
|
|
|
68
105
|
# Port A as driven by the data/direction registers alone, without
|
|
@@ -72,9 +109,9 @@ module Badline
|
|
|
72
109
|
end
|
|
73
110
|
|
|
74
111
|
def read_port_b
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
apply_timer_output(
|
|
112
|
+
lines = driven_lines(@data_port_b, @data_dir_b)
|
|
113
|
+
lines &= peripheral.read_b(driven_lines(@data_port_a, @data_dir_a), lines) if peripheral
|
|
114
|
+
apply_timer_output(lines)
|
|
78
115
|
end
|
|
79
116
|
|
|
80
117
|
def peek(addr)
|
|
@@ -91,12 +128,8 @@ module Badline
|
|
|
91
128
|
when 0x09 then @tod.seconds
|
|
92
129
|
when 0x0a then @tod.minutes
|
|
93
130
|
when 0x0b then @tod.hours
|
|
94
|
-
when 0x0c then @
|
|
95
|
-
when 0x0d
|
|
96
|
-
value = interrupt_status.value
|
|
97
|
-
interrupt_status.value = 0x0 # Burn after reading
|
|
98
|
-
@irq_pending = 0
|
|
99
|
-
value
|
|
131
|
+
when 0x0c then @serial.data
|
|
132
|
+
when 0x0d then @icr.read
|
|
100
133
|
when 0x0e then control_a.value
|
|
101
134
|
when 0x0f then control_b.value
|
|
102
135
|
end
|
|
@@ -105,9 +138,9 @@ module Badline
|
|
|
105
138
|
def poke(addr, value)
|
|
106
139
|
case index(addr) & 0x0f
|
|
107
140
|
when 0x00 then @data_port_a = value
|
|
108
|
-
when 0x01 then @data_port_b = value
|
|
141
|
+
when 0x01 then update_port_b { @data_port_b = value }
|
|
109
142
|
when 0x02 then @data_dir_a = value
|
|
110
|
-
when 0x03 then @data_dir_b = value
|
|
143
|
+
when 0x03 then update_port_b { @data_dir_b = value }
|
|
111
144
|
when 0x04 then @ta.write_latch_low(value)
|
|
112
145
|
when 0x05 then @ta.write_latch_high(value)
|
|
113
146
|
when 0x06 then @tb.write_latch_low(value)
|
|
@@ -116,16 +149,32 @@ module Badline
|
|
|
116
149
|
when 0x09 then @tod.write(:seconds, value, alarm: control_b.alarm?)
|
|
117
150
|
when 0x0a then @tod.write(:minutes, value, alarm: control_b.alarm?)
|
|
118
151
|
when 0x0b then @tod.write_hours(value, alarm: control_b.alarm?)
|
|
119
|
-
when 0x0c
|
|
120
|
-
|
|
121
|
-
when
|
|
122
|
-
when 0x0e then @ta.write_control(value)
|
|
152
|
+
when 0x0c then @serial.write(value)
|
|
153
|
+
when 0x0d then @icr.write(value)
|
|
154
|
+
when 0x0e then write_control_a(value)
|
|
123
155
|
when 0x0f then @tb.write_control(value)
|
|
124
156
|
end
|
|
125
157
|
end
|
|
126
158
|
|
|
127
159
|
private
|
|
128
160
|
|
|
161
|
+
def update_port_b
|
|
162
|
+
yield
|
|
163
|
+
@port_b4_driven_high = driven_lines(@data_port_b, @data_dir_b).anybits?(0x10)
|
|
164
|
+
refresh_port_b4 if @port_b4_handler
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# PB4 is also control port 1's fire line, and a peripheral pulls it low
|
|
168
|
+
# without any register write, so the level is resampled every cycle rather
|
|
169
|
+
# than only after a poke.
|
|
170
|
+
def refresh_port_b4
|
|
171
|
+
high = @port_b4_driven_high && (peripheral.nil? || peripheral.port_b4_high?)
|
|
172
|
+
return if high == @port_b4_high
|
|
173
|
+
|
|
174
|
+
@port_b4_high = high
|
|
175
|
+
@port_b4_handler.call(high)
|
|
176
|
+
end
|
|
177
|
+
|
|
129
178
|
def driven_lines(register, direction)
|
|
130
179
|
# Output bits are driven from the data register; input bits float high.
|
|
131
180
|
# External peripherals can still pull any line low (wired-AND).
|
|
@@ -133,57 +182,58 @@ module Badline
|
|
|
133
182
|
end
|
|
134
183
|
|
|
135
184
|
def apply_timer_output(value)
|
|
136
|
-
value = with_bit(value, 6,
|
|
137
|
-
value = with_bit(value, 7,
|
|
185
|
+
value = with_bit(value, 6, @ta.output?) if control_a.output?
|
|
186
|
+
value = with_bit(value, 7, @tb.output?) if control_b.output?
|
|
138
187
|
value
|
|
139
188
|
end
|
|
140
189
|
|
|
141
|
-
def timer_a_output?
|
|
142
|
-
control_a.out_mode? ? @ta.toggle? : @ta.underflowed
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def timer_b_output?
|
|
146
|
-
control_b.out_mode? ? @tb.toggle? : @tb.underflowed
|
|
147
|
-
end
|
|
148
|
-
|
|
149
190
|
def with_bit(value, bit, set)
|
|
150
191
|
set ? value | (1 << bit) : value & ~(1 << bit)
|
|
151
192
|
end
|
|
152
193
|
|
|
153
|
-
def trigger_alarm
|
|
154
|
-
|
|
155
|
-
|
|
194
|
+
def trigger_alarm = raise_interrupt(:alarm)
|
|
195
|
+
def trigger_serial = raise_interrupt(:serial)
|
|
196
|
+
|
|
197
|
+
def raise_interrupt(source) = @icr.flag(source)
|
|
198
|
+
|
|
199
|
+
# CNT is sampled once a cycle. When the serial port drives it from this
|
|
200
|
+
# cycle's timer A underflow, the new level is picked up on the next one.
|
|
201
|
+
def cnt_edge(level)
|
|
202
|
+
@cnt_high = level
|
|
203
|
+
@cnt_rise = level
|
|
204
|
+
@serial.rising_edge! { trigger_serial } if level
|
|
156
205
|
end
|
|
157
206
|
|
|
207
|
+
# Returns whether timer A underflowed, which clocks the serial port.
|
|
208
|
+
#
|
|
209
|
+
# CRB bits 6-5 pick timer B's source: ø2, CNT edges, timer A
|
|
210
|
+
# underflows, or timer A underflows gated by the CNT level. Every source
|
|
211
|
+
# drives the same count-enable line, so a cascaded underflow goes through
|
|
212
|
+
# the input pipeline exactly as a CNT edge does.
|
|
158
213
|
def update_timers
|
|
159
|
-
@ta.cycle!(@control_a.value
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
@ta.cycle!(@control_a.value & 0x20 == 0x20 ? @cnt_rise : true)
|
|
215
|
+
underflowed = @ta.underflowed
|
|
216
|
+
@tb.cycle!(
|
|
217
|
+
case @control_b.value & 0x60
|
|
218
|
+
when 0x00 then true
|
|
219
|
+
when 0x20 then @cnt_rise
|
|
220
|
+
when 0x40 then underflowed
|
|
221
|
+
else underflowed && @cnt_high
|
|
222
|
+
end
|
|
223
|
+
)
|
|
224
|
+
if underflowed
|
|
167
225
|
interrupt_status.timer_a = true
|
|
168
226
|
interrupt! if interrupt_control.timer_a?
|
|
169
227
|
end
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
interrupt_status.timer_b = true
|
|
173
|
-
interrupt! if interrupt_control.timer_b?
|
|
228
|
+
@icr.timer_b_underflow! if @tb.underflowed
|
|
229
|
+
underflowed
|
|
174
230
|
end
|
|
175
231
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# Set interrupts based on bits 0-4
|
|
182
|
-
interrupt_control.value |= (value & 0x1f)
|
|
183
|
-
end
|
|
184
|
-
return unless interrupt_control.value.anybits?(interrupt_status.value & 0x1f)
|
|
185
|
-
|
|
186
|
-
interrupt!(2) unless interrupted?
|
|
232
|
+
def write_control_a(value)
|
|
233
|
+
was_output = control_a.serial_mode?
|
|
234
|
+
@ta.write_control(value)
|
|
235
|
+
@serial.reset! { trigger_serial } if control_a.serial_mode? != was_output
|
|
236
|
+
@tod.fifty_hz = control_a.clock_frequency?
|
|
187
237
|
end
|
|
188
238
|
end
|
|
189
239
|
end
|
data/lib/badline/color_memory.rb
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
+
# Colour RAM is four bits wide. A CPU read takes the upper nibble off the
|
|
5
|
+
# open data bus, which still holds the byte the VIC fetched in phi1.
|
|
4
6
|
class ColorMemory < Memory
|
|
7
|
+
def initialize(vic, start: 0xd800, length: 2**10)
|
|
8
|
+
super(start:, length:)
|
|
9
|
+
@vic = vic
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def peek(addr)
|
|
13
|
+
(@vic.phi1_data & 0xf0) | @storage[index(addr)]
|
|
14
|
+
end
|
|
15
|
+
|
|
5
16
|
def poke(addr, value)
|
|
6
|
-
# Only write the lower 4 bits
|
|
7
17
|
super(addr, value & 0x0f)
|
|
8
18
|
end
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
rand(16) << 4
|
|
20
|
+
# The four bits the cell stores, as the VIC's c-access sees them.
|
|
21
|
+
def nibble(addr)
|
|
22
|
+
@storage[index(addr)]
|
|
14
23
|
end
|
|
15
24
|
end
|
|
16
25
|
end
|
data/lib/badline/computer.rb
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Badline
|
|
6
4
|
class Computer
|
|
7
5
|
include IntegerHelper
|
|
8
6
|
include KeyboardBuffer
|
|
9
|
-
extend Forwardable
|
|
10
7
|
|
|
11
8
|
attr_reader :address_bus, :cpu, :cycles
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
def vic = address_bus.vic
|
|
11
|
+
|
|
12
|
+
def cia1 = address_bus.cia1
|
|
13
|
+
|
|
14
|
+
def cia2 = address_bus.cia2
|
|
15
|
+
|
|
16
|
+
def sid = address_bus.sid
|
|
17
|
+
|
|
18
|
+
def ram = address_bus.ram
|
|
19
|
+
|
|
20
|
+
def keyboard = address_bus.keyboard
|
|
21
|
+
|
|
22
|
+
def joystick1 = address_bus.joystick1
|
|
23
|
+
|
|
24
|
+
def joystick2 = address_bus.joystick2
|
|
14
25
|
|
|
15
|
-
def
|
|
16
|
-
|
|
26
|
+
def control_ports = address_bus.control_ports
|
|
27
|
+
|
|
28
|
+
def datasette = address_bus.datasette
|
|
29
|
+
|
|
30
|
+
def install_debug_register(&) = address_bus.install_debug_register(&)
|
|
31
|
+
|
|
32
|
+
def initialize(debug: false, sid_model: :mos6581)
|
|
33
|
+
@address_bus = AddressBus.new(sid_model:)
|
|
17
34
|
@cpu = CPU.new(@address_bus, debug:)
|
|
18
35
|
@vic = @address_bus.vic
|
|
36
|
+
@vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
|
|
19
37
|
@cia1 = @address_bus.cia1
|
|
20
38
|
@cia2 = @address_bus.cia2
|
|
39
|
+
@sid = @address_bus.sid
|
|
40
|
+
@datasette = @address_bus.datasette
|
|
21
41
|
@cycles = 0
|
|
22
42
|
@nmi_asserted = false
|
|
43
|
+
@cartridge_nmi = false
|
|
44
|
+
@freezing = false
|
|
45
|
+
@freeze_writes = 0
|
|
23
46
|
@init_handlers = []
|
|
24
47
|
@pending_keys = nil
|
|
25
48
|
end
|
|
@@ -30,17 +53,19 @@ module Badline
|
|
|
30
53
|
handle_init if @cycles == INIT_THRESHOLD
|
|
31
54
|
feed_keyboard if @pending_keys
|
|
32
55
|
|
|
56
|
+
# The chips clock ahead of the CPU, so a register write lands on the
|
|
57
|
+
# cycle after the one it was issued on.
|
|
33
58
|
@vic.cycle!
|
|
34
59
|
@cia1.cycle!
|
|
35
60
|
@cia2.cycle!
|
|
61
|
+
@sid.cycle!
|
|
62
|
+
@datasette.cycle!
|
|
36
63
|
|
|
37
64
|
@cpu.irq = @cia1.interrupted? || @vic.interrupted?
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@
|
|
42
|
-
|
|
43
|
-
@cpu.cycle! if @cpu.pending_write? || !@vic.ba_low?
|
|
66
|
+
drive_nmi
|
|
67
|
+
watch_freeze if @freezing
|
|
68
|
+
@cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
|
|
44
69
|
|
|
45
70
|
@cycles += 1
|
|
46
71
|
end
|
|
@@ -52,13 +77,54 @@ module Badline
|
|
|
52
77
|
end
|
|
53
78
|
|
|
54
79
|
def attach_cartridge(cartridge)
|
|
80
|
+
cartridge.clock = -> { @cycles }
|
|
81
|
+
cartridge.on_nmi_change { |level| @cartridge_nmi = level }
|
|
55
82
|
address_bus.attach_cartridge(cartridge)
|
|
83
|
+
power_cycle!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A cartridge goes in with the power off, so attaching one switches the
|
|
87
|
+
# machine off and on: the VIC and RAM start from their power-on state,
|
|
88
|
+
# and the RES line resets everything else.
|
|
89
|
+
def power_cycle!
|
|
90
|
+
vic.power_on!
|
|
91
|
+
address_bus.power_on!
|
|
92
|
+
reset!
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The RES line reaches the CPU and its port, both CIAs, the SID and the
|
|
96
|
+
# cartridge port. The VIC has no reset pin.
|
|
97
|
+
def reset!
|
|
98
|
+
address_bus.reset_port!
|
|
99
|
+
@cia1.reset!
|
|
100
|
+
@cia2.reset!
|
|
101
|
+
@sid.reset!
|
|
102
|
+
address_bus.cartridge&.reset
|
|
103
|
+
@freezing = false
|
|
104
|
+
@nmi_asserted = false
|
|
56
105
|
cpu.reset!
|
|
57
106
|
end
|
|
58
107
|
|
|
108
|
+
# Presses the cartridge's freeze button. When the press pulls NMI, the
|
|
109
|
+
# cartridge freezes once the CPU takes the interrupt.
|
|
110
|
+
def press_cartridge_button
|
|
111
|
+
cartridge = address_bus.cartridge
|
|
112
|
+
return unless cartridge
|
|
113
|
+
|
|
114
|
+
cartridge.press_button
|
|
115
|
+
@freezing = cartridge.nmi?
|
|
116
|
+
@freeze_writes = 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def release_cartridge_button
|
|
120
|
+
address_bus.cartridge&.release_button
|
|
121
|
+
end
|
|
122
|
+
|
|
59
123
|
def mount(storage)
|
|
60
|
-
|
|
124
|
+
drive = KernalTrap::Drive.new(storage)
|
|
125
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive:)
|
|
61
126
|
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
127
|
+
KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
|
|
62
128
|
return unless storage.respond_to?(:write_file)
|
|
63
129
|
|
|
64
130
|
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
|
|
@@ -85,6 +151,25 @@ module Badline
|
|
|
85
151
|
|
|
86
152
|
private
|
|
87
153
|
|
|
154
|
+
# The NMI line is wired-OR between CIA 2 and the cartridge, and the CPU
|
|
155
|
+
# takes an interrupt on its falling edge.
|
|
156
|
+
def drive_nmi
|
|
157
|
+
nmi = @cia2.interrupted? || @cartridge_nmi
|
|
158
|
+
@cpu.nmi = true if nmi && !@nmi_asserted
|
|
159
|
+
@nmi_asserted = nmi
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A freezer counts the CPU's write cycles once it pulls NMI and switches
|
|
163
|
+
# to Ultimax on the third in a row, the last push of the interrupt
|
|
164
|
+
# sequence, so the vector fetch that follows reads the cartridge.
|
|
165
|
+
def watch_freeze
|
|
166
|
+
@freeze_writes = @cpu.pending_write? ? @freeze_writes + 1 : 0
|
|
167
|
+
return if @freeze_writes < 3
|
|
168
|
+
|
|
169
|
+
@freezing = false
|
|
170
|
+
address_bus.cartridge.freeze!
|
|
171
|
+
end
|
|
172
|
+
|
|
88
173
|
def booting?
|
|
89
174
|
@cycles < init_threshold
|
|
90
175
|
end
|
|
@@ -1,24 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
-
# CIA 1
|
|
4
|
+
# CIA 1's control ports and the keyboard matrix that spans them.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Port A carries the matrix rows together with joystick 2, port B the matrix
|
|
7
|
+
# columns together with joystick 1. Every switch is active low, so the
|
|
8
|
+
# joysticks are wired-AND onto the lines *before* the matrix settles: a stick
|
|
9
|
+
# held in a direction selects rows and columns of its own, which is where
|
|
10
|
+
# hardware's phantom keypresses come from. Paddles and a 1351 mouse have
|
|
11
|
+
# buttons on those same lines.
|
|
12
|
+
#
|
|
13
|
+
# The ports also carry SID's POTX/POTY lines, which reach them through analog
|
|
14
|
+
# switches closed by CIA 1 PA6 (port 1) and PA7 (port 2). Close both and the
|
|
15
|
+
# pots sit in parallel, so SID charges through the lower of the two
|
|
16
|
+
# resistances; close neither and the lines float high. Reading a pot asks the
|
|
17
|
+
# CIA for the current select lines, since a program sets them and then goes
|
|
18
|
+
# straight to SID without touching the ports again.
|
|
8
19
|
class ControlPorts
|
|
9
|
-
|
|
20
|
+
PORT1_POTS = 0x40
|
|
21
|
+
PORT2_POTS = 0x80
|
|
22
|
+
FIRE = 0x10
|
|
23
|
+
|
|
24
|
+
attr_reader :keyboard, :joystick1, :joystick2
|
|
25
|
+
attr_accessor :device1, :device2, :port_a_source
|
|
10
26
|
|
|
11
|
-
def initialize(keyboard:, joystick2:)
|
|
27
|
+
def initialize(keyboard:, joystick1:, joystick2:)
|
|
12
28
|
@keyboard = keyboard
|
|
29
|
+
@joystick1 = joystick1
|
|
13
30
|
@joystick2 = joystick2
|
|
31
|
+
@device1 = nil
|
|
32
|
+
@device2 = nil
|
|
33
|
+
@port_a_source = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_a(port_a, port_b) = scan(port_a, port_b).first
|
|
37
|
+
|
|
38
|
+
def read_b(port_a, port_b) = scan(port_a, port_b).last
|
|
39
|
+
|
|
40
|
+
# Control port 1 pin 6 is both the port B fire line and the VIC's light
|
|
41
|
+
# pen input, so joystick 1's button and a 1351's left button latch
|
|
42
|
+
# $D013/$D014. The CIA samples this every cycle, so it skips the matrix and
|
|
43
|
+
# asks only what the port's own devices drive.
|
|
44
|
+
def port_b4_high? = port_bits(@joystick1, @device1).anybits?(FIRE)
|
|
45
|
+
|
|
46
|
+
def pot_x = selected_devices.map(&:pot_x).min || 0xff
|
|
47
|
+
|
|
48
|
+
def pot_y = selected_devices.map(&:pot_y).min || 0xff
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def selected_devices
|
|
53
|
+
lines = @port_a_source&.port_a_lines || 0x00
|
|
54
|
+
devices = []
|
|
55
|
+
devices << @device1 if @device1 && lines.anybits?(PORT1_POTS)
|
|
56
|
+
devices << @device2 if @device2 && lines.anybits?(PORT2_POTS)
|
|
57
|
+
devices
|
|
14
58
|
end
|
|
15
59
|
|
|
16
|
-
def
|
|
17
|
-
keyboard.
|
|
60
|
+
def scan(port_a, port_b)
|
|
61
|
+
keyboard.scan(port_a & port_bits(@joystick2, @device2),
|
|
62
|
+
port_b & port_bits(@joystick1, @device1))
|
|
18
63
|
end
|
|
19
64
|
|
|
20
|
-
def
|
|
21
|
-
|
|
65
|
+
def port_bits(joystick, device)
|
|
66
|
+
device ? joystick.port_bits & device.port_bits : joystick.port_bits
|
|
22
67
|
end
|
|
23
68
|
end
|
|
24
69
|
end
|