badline 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +40 -0
- data/exe/badline +55 -0
- data/lib/badline/address_bus.rb +206 -0
- data/lib/badline/addressable.rb +61 -0
- data/lib/badline/cartridge/magic_desk.rb +23 -0
- data/lib/badline/cartridge/ocean.rb +33 -0
- data/lib/badline/cartridge/standard.rb +31 -0
- data/lib/badline/cartridge.rb +75 -0
- data/lib/badline/chrout_trap.rb +39 -0
- data/lib/badline/cia/timer.rb +122 -0
- data/lib/badline/cia.rb +189 -0
- data/lib/badline/color_memory.rb +16 -0
- data/lib/badline/computer.rb +100 -0
- data/lib/badline/control_ports.rb +24 -0
- data/lib/badline/cpu.rb +239 -0
- data/lib/badline/cycleable.rb +35 -0
- data/lib/badline/gui/application.rb +94 -0
- data/lib/badline/gui/joy_map.rb +19 -0
- data/lib/badline/gui/key_map.rb +34 -0
- data/lib/badline/gui/palette.rb +35 -0
- data/lib/badline/gui/pane.rb +35 -0
- data/lib/badline/gui/screen_pane.rb +50 -0
- data/lib/badline/gui/window.rb +46 -0
- data/lib/badline/gui.rb +11 -0
- data/lib/badline/instruction.rb +334 -0
- data/lib/badline/instruction_set/arithmetic.rb +119 -0
- data/lib/badline/instruction_set/bitwise.rb +131 -0
- data/lib/badline/instruction_set/branch.rb +78 -0
- data/lib/badline/instruction_set/flag.rb +63 -0
- data/lib/badline/instruction_set/illegal.rb +278 -0
- data/lib/badline/instruction_set/inc_dec.rb +71 -0
- data/lib/badline/instruction_set/stack.rb +104 -0
- data/lib/badline/instruction_set/transfer.rb +137 -0
- data/lib/badline/instruction_set.rb +77 -0
- data/lib/badline/integer_helper.rb +39 -0
- data/lib/badline/joystick.rb +25 -0
- data/lib/badline/kernal_trap/file.rb +54 -0
- data/lib/badline/kernal_trap/load.rb +63 -0
- data/lib/badline/kernal_trap/save.rb +42 -0
- data/lib/badline/kernal_trap.rb +5 -0
- data/lib/badline/keyboard.rb +58 -0
- data/lib/badline/keyboard_buffer.rb +33 -0
- data/lib/badline/media.rb +59 -0
- data/lib/badline/memory.rb +43 -0
- data/lib/badline/rom.rb +23 -0
- data/lib/badline/roms/README +18 -0
- data/lib/badline/roms/basic.rom +0 -0
- data/lib/badline/roms/character.rom +0 -0
- data/lib/badline/roms/kernal.rom +0 -0
- data/lib/badline/sid.rb +25 -0
- data/lib/badline/status.rb +56 -0
- data/lib/badline/storage/crt_file.rb +53 -0
- data/lib/badline/storage/d64_image.rb +21 -0
- data/lib/badline/storage/d71_image.rb +13 -0
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image.rb +71 -0
- data/lib/badline/storage/host_directory.rb +49 -0
- data/lib/badline/storage/p00.rb +24 -0
- data/lib/badline/storage.rb +28 -0
- data/lib/badline/time_of_day.rb +101 -0
- data/lib/badline/traps.rb +15 -0
- data/lib/badline/version.rb +5 -0
- data/lib/badline/vic/bank.rb +65 -0
- data/lib/badline/vic/display_state.rb +78 -0
- data/lib/badline/vic/graphics_mode.rb +139 -0
- data/lib/badline/vic/registers.rb +170 -0
- data/lib/badline/vic/sequencer.rb +237 -0
- data/lib/badline/vic/sprite.rb +121 -0
- data/lib/badline/vic/sprites.rb +112 -0
- data/lib/badline/vic.rb +192 -0
- data/lib/badline.rb +29 -0
- metadata +131 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CIA
|
|
5
|
+
class Timer
|
|
6
|
+
attr_accessor :counter, :latch
|
|
7
|
+
attr_reader :control, :underflowed
|
|
8
|
+
|
|
9
|
+
def initialize(control)
|
|
10
|
+
@control = control
|
|
11
|
+
@counter = @latch = 0x0
|
|
12
|
+
@pipe = 0
|
|
13
|
+
@load_delay = 0
|
|
14
|
+
@reload = false
|
|
15
|
+
@underflowed = false
|
|
16
|
+
@oneshot_linger = 0
|
|
17
|
+
@toggle = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def toggle?
|
|
21
|
+
@toggle
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cycle!(feed, pulse)
|
|
25
|
+
return @counter -= 1 if feed && pulse && steady?
|
|
26
|
+
|
|
27
|
+
if (@pipe | @load_delay).zero? && !@reload
|
|
28
|
+
@pipe = 0b10 if feed && started?
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
run_tick(feed, pulse)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run_tick(feed, pulse)
|
|
35
|
+
@underflowed = false
|
|
36
|
+
@oneshot_linger -= 1 if @oneshot_linger.positive?
|
|
37
|
+
tick(feed && started?, pulse)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def write_control(value)
|
|
41
|
+
@toggle = true if value.anybits?(0x01) && !started?
|
|
42
|
+
@oneshot_linger = 2 if control.run_mode? && value.nobits?(0x08)
|
|
43
|
+
control.value = value & ~0x10
|
|
44
|
+
@load_delay = 3 if value.anybits?(0x10)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def write_latch_low(value)
|
|
48
|
+
@latch = (@latch & 0xff00) | value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def write_latch_high(value)
|
|
52
|
+
@latch = (value << 8) | (@latch & 0xff)
|
|
53
|
+
@counter = @latch unless started?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def started?
|
|
59
|
+
control.value.anybits?(0x01)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def steady?
|
|
63
|
+
@counter > 1 && @pipe == 0b11 && !@reload &&
|
|
64
|
+
(@load_delay | @oneshot_linger).zero? && started?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def tick(feed, pulse)
|
|
68
|
+
counting = @pipe.anybits?(0b01) && pulse
|
|
69
|
+
@pipe = (@pipe >> 1) | (feed ? 0b10 : 0)
|
|
70
|
+
|
|
71
|
+
if premature_underflow?(pulse)
|
|
72
|
+
underflow
|
|
73
|
+
elsif apply_reload?
|
|
74
|
+
return
|
|
75
|
+
elsif counting && @load_delay != 1
|
|
76
|
+
count
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
forced_load
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def forced_load
|
|
83
|
+
return unless @load_delay.positive?
|
|
84
|
+
|
|
85
|
+
@load_delay -= 1
|
|
86
|
+
@counter = @latch if @load_delay == 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# the final pipeline stage, before any pending load lands
|
|
90
|
+
def premature_underflow?(pulse)
|
|
91
|
+
@counter.zero? && !@reload && pulse && @pipe.anybits?(0b01) &&
|
|
92
|
+
started?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# An underflow reload consumes the tick after the flag
|
|
96
|
+
def apply_reload?
|
|
97
|
+
return false unless @reload
|
|
98
|
+
|
|
99
|
+
@reload = false
|
|
100
|
+
@counter = @latch
|
|
101
|
+
# A zero latch underflows again on the reload tick while running
|
|
102
|
+
underflow if @counter.zero? && started?
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def count
|
|
107
|
+
@counter -= 1 if @counter.positive?
|
|
108
|
+
underflow if @counter.zero? && @pipe.anybits?(0b01)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def underflow
|
|
112
|
+
@underflowed = true
|
|
113
|
+
@reload = true
|
|
114
|
+
@toggle = !@toggle
|
|
115
|
+
return unless control.run_mode? || @oneshot_linger.positive?
|
|
116
|
+
|
|
117
|
+
control.start = false
|
|
118
|
+
@pipe = 0
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/badline/cia.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require "badline/cia/timer"
|
|
5
|
+
|
|
6
|
+
module Badline
|
|
7
|
+
# CIA (Complex Interface Adapter) chip
|
|
8
|
+
class CIA
|
|
9
|
+
include Addressable
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
attr_reader :start, :control_a, :control_b, :interrupt_status, :interrupt_control, :peripheral
|
|
13
|
+
|
|
14
|
+
def_delegator :@ta, :counter, :timer_a
|
|
15
|
+
def_delegator :@ta, :counter=, :timer_a=
|
|
16
|
+
def_delegator :@ta, :latch, :timer_a_latch
|
|
17
|
+
def_delegator :@ta, :latch=, :timer_a_latch=
|
|
18
|
+
def_delegator :@tb, :counter, :timer_b
|
|
19
|
+
def_delegator :@tb, :counter=, :timer_b=
|
|
20
|
+
def_delegator :@tb, :latch, :timer_b_latch
|
|
21
|
+
def_delegator :@tb, :latch=, :timer_b_latch=
|
|
22
|
+
|
|
23
|
+
def initialize(start: 0, peripheral: nil)
|
|
24
|
+
addressable_at(start, length: 2**8)
|
|
25
|
+
|
|
26
|
+
@peripheral = peripheral
|
|
27
|
+
@data_port_a = 0xff
|
|
28
|
+
@data_port_b = 0xff
|
|
29
|
+
@data_dir_a = 0xff
|
|
30
|
+
@data_dir_b = 0x0
|
|
31
|
+
@irq_pending = 0
|
|
32
|
+
@serial_data = 0x0
|
|
33
|
+
@tod = TimeOfDay.new
|
|
34
|
+
@interrupt_control = Status.new([:timer_a, :timer_b, :alarm, :serial,
|
|
35
|
+
:flag, 0, 0, 0])
|
|
36
|
+
@interrupt_status = Status.new([:timer_a, :timer_b, :alarm, :serial,
|
|
37
|
+
:flag, 0, 0, :interrupt])
|
|
38
|
+
@control_a = Status.new(%i[start output out_mode run_mode load
|
|
39
|
+
in_mode serial_mode clock_frequency])
|
|
40
|
+
@control_b = Status.new(%i[start output out_mode run_mode load
|
|
41
|
+
in_cnt in_timer_a alarm])
|
|
42
|
+
@ta = Timer.new(@control_a)
|
|
43
|
+
@tb = Timer.new(@control_b)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def interrupt!(delay = 1)
|
|
47
|
+
@irq_pending = @irq_pending.positive? ? [@irq_pending, delay].min : delay
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def interrupted?
|
|
51
|
+
interrupt_status.value.anybits?(0x80)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cycle!
|
|
55
|
+
if @irq_pending.positive?
|
|
56
|
+
@irq_pending -= 1
|
|
57
|
+
interrupt_status.interrupt = true if @irq_pending.zero?
|
|
58
|
+
end
|
|
59
|
+
update_timers
|
|
60
|
+
@tod.cycle! { trigger_alarm }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_port_a
|
|
64
|
+
pulldown = peripheral ? peripheral.read_a(@data_port_a, @data_port_b) : 0xff
|
|
65
|
+
driven_lines(@data_port_a, @data_dir_a) & pulldown
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Port A as driven by the data/direction registers alone, without
|
|
69
|
+
# peripheral pulldown. Cheap path for the VIC bank lookup.
|
|
70
|
+
def port_a_lines
|
|
71
|
+
driven_lines(@data_port_a, @data_dir_a)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read_port_b
|
|
75
|
+
pulldown = peripheral ? peripheral.read_b(@data_port_a, @data_port_b) : 0xff
|
|
76
|
+
value = driven_lines(@data_port_b, @data_dir_b) & pulldown
|
|
77
|
+
apply_timer_output(value)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def peek(addr)
|
|
81
|
+
case index(addr) & 0x0f
|
|
82
|
+
when 0x00 then read_port_a
|
|
83
|
+
when 0x01 then read_port_b
|
|
84
|
+
when 0x02 then @data_dir_a
|
|
85
|
+
when 0x03 then @data_dir_b
|
|
86
|
+
when 0x04 then low_byte(@ta.counter)
|
|
87
|
+
when 0x05 then high_byte(@ta.counter)
|
|
88
|
+
when 0x06 then low_byte(@tb.counter)
|
|
89
|
+
when 0x07 then high_byte(@tb.counter)
|
|
90
|
+
when 0x08 then @tod.tenths
|
|
91
|
+
when 0x09 then @tod.seconds
|
|
92
|
+
when 0x0a then @tod.minutes
|
|
93
|
+
when 0x0b then @tod.hours
|
|
94
|
+
when 0x0c then @serial_data
|
|
95
|
+
when 0x0d
|
|
96
|
+
value = interrupt_status.value
|
|
97
|
+
interrupt_status.value = 0x0 # Burn after reading
|
|
98
|
+
@irq_pending = 0
|
|
99
|
+
value
|
|
100
|
+
when 0x0e then control_a.value
|
|
101
|
+
when 0x0f then control_b.value
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def poke(addr, value)
|
|
106
|
+
case index(addr) & 0x0f
|
|
107
|
+
when 0x00 then @data_port_a = value
|
|
108
|
+
when 0x01 then @data_port_b = value
|
|
109
|
+
when 0x02 then @data_dir_a = value
|
|
110
|
+
when 0x03 then @data_dir_b = value
|
|
111
|
+
when 0x04 then @ta.write_latch_low(value)
|
|
112
|
+
when 0x05 then @ta.write_latch_high(value)
|
|
113
|
+
when 0x06 then @tb.write_latch_low(value)
|
|
114
|
+
when 0x07 then @tb.write_latch_high(value)
|
|
115
|
+
when 0x08 then @tod.write(:tenths, value, alarm: control_b.alarm?)
|
|
116
|
+
when 0x09 then @tod.write(:seconds, value, alarm: control_b.alarm?)
|
|
117
|
+
when 0x0a then @tod.write(:minutes, value, alarm: control_b.alarm?)
|
|
118
|
+
when 0x0b then @tod.write_hours(value, alarm: control_b.alarm?)
|
|
119
|
+
when 0x0c
|
|
120
|
+
# TODO: Serial
|
|
121
|
+
when 0x0d then write_interrupt_control(value)
|
|
122
|
+
when 0x0e then @ta.write_control(value)
|
|
123
|
+
when 0x0f then @tb.write_control(value)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def driven_lines(register, direction)
|
|
130
|
+
# Output bits are driven from the data register; input bits float high.
|
|
131
|
+
# External peripherals can still pull any line low (wired-AND).
|
|
132
|
+
(register & direction) | (~direction & 0xff)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def apply_timer_output(value)
|
|
136
|
+
value = with_bit(value, 6, timer_a_output?) if control_a.output?
|
|
137
|
+
value = with_bit(value, 7, timer_b_output?) if control_b.output?
|
|
138
|
+
value
|
|
139
|
+
end
|
|
140
|
+
|
|
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
|
+
def with_bit(value, bit, set)
|
|
150
|
+
set ? value | (1 << bit) : value & ~(1 << bit)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def trigger_alarm
|
|
154
|
+
interrupt_status.alarm = true
|
|
155
|
+
interrupt! if interrupt_control.alarm?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def update_timers
|
|
159
|
+
@ta.cycle!(@control_a.value.nobits?(0x20), true)
|
|
160
|
+
crb = @control_b.value
|
|
161
|
+
if crb.anybits?(0x40)
|
|
162
|
+
@tb.cycle!(true, @ta.underflowed)
|
|
163
|
+
else
|
|
164
|
+
@tb.cycle!(crb.nobits?(0x20), true)
|
|
165
|
+
end
|
|
166
|
+
if @ta.underflowed
|
|
167
|
+
interrupt_status.timer_a = true
|
|
168
|
+
interrupt! if interrupt_control.timer_a?
|
|
169
|
+
end
|
|
170
|
+
return unless @tb.underflowed
|
|
171
|
+
|
|
172
|
+
interrupt_status.timer_b = true
|
|
173
|
+
interrupt! if interrupt_control.timer_b?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def write_interrupt_control(value)
|
|
177
|
+
if value.nobits?(0x80)
|
|
178
|
+
# Clear interrupts based on bits 0-4
|
|
179
|
+
interrupt_control.value &= ~(value & 0x1f)
|
|
180
|
+
else
|
|
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?
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class Computer
|
|
7
|
+
include IntegerHelper
|
|
8
|
+
include KeyboardBuffer
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
attr_reader :address_bus, :cpu, :cycles
|
|
12
|
+
|
|
13
|
+
def_delegators :address_bus, :vic, :cia1, :cia2, :sid, :ram, :keyboard, :joystick2
|
|
14
|
+
|
|
15
|
+
def initialize(debug: false)
|
|
16
|
+
@address_bus = AddressBus.new
|
|
17
|
+
@cpu = CPU.new(@address_bus, debug:)
|
|
18
|
+
@vic = @address_bus.vic
|
|
19
|
+
@cia1 = @address_bus.cia1
|
|
20
|
+
@cia2 = @address_bus.cia2
|
|
21
|
+
@cycles = 0
|
|
22
|
+
@nmi_asserted = false
|
|
23
|
+
@init_handlers = []
|
|
24
|
+
@pending_keys = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
INIT_THRESHOLD = 2_500_000
|
|
28
|
+
|
|
29
|
+
def cycle!
|
|
30
|
+
handle_init if @cycles == INIT_THRESHOLD
|
|
31
|
+
feed_keyboard if @pending_keys
|
|
32
|
+
|
|
33
|
+
@vic.cycle!
|
|
34
|
+
@cia1.cycle!
|
|
35
|
+
@cia2.cycle!
|
|
36
|
+
|
|
37
|
+
@cpu.irq = @cia1.interrupted? || @vic.interrupted?
|
|
38
|
+
|
|
39
|
+
nmi = @cia2.interrupted?
|
|
40
|
+
@cpu.nmi = true if nmi && !@nmi_asserted
|
|
41
|
+
@nmi_asserted = nmi
|
|
42
|
+
|
|
43
|
+
@cpu.cycle! if @cpu.pending_write? || !@vic.ba_low?
|
|
44
|
+
|
|
45
|
+
@cycles += 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def load_prg(data)
|
|
49
|
+
uint16(data[0], data[1]).tap do |load_addr|
|
|
50
|
+
ram.write(load_addr, data[2..])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def attach_cartridge(cartridge)
|
|
55
|
+
address_bus.attach_cartridge(cartridge)
|
|
56
|
+
cpu.reset!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mount(storage)
|
|
60
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, storage:)
|
|
61
|
+
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
62
|
+
return unless storage.respond_to?(:write_file)
|
|
63
|
+
|
|
64
|
+
save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
|
|
65
|
+
cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def capture_output
|
|
69
|
+
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
|
|
70
|
+
cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def inspect
|
|
75
|
+
"#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_init(&block)
|
|
79
|
+
if booting?
|
|
80
|
+
@init_handlers << block
|
|
81
|
+
else
|
|
82
|
+
block.call
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def booting?
|
|
89
|
+
@cycles < init_threshold
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_init
|
|
93
|
+
@init_handlers.each(&:call)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def init_threshold
|
|
97
|
+
INIT_THRESHOLD
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# CIA 1 peripheral combining the keyboard matrix with the control ports.
|
|
5
|
+
#
|
|
6
|
+
# Joystick switches share the same lines as the keyboard matrix and are
|
|
7
|
+
# wired-AND with it (active low). Port 2 sits on Port A.
|
|
8
|
+
class ControlPorts
|
|
9
|
+
attr_reader :keyboard, :joystick2
|
|
10
|
+
|
|
11
|
+
def initialize(keyboard:, joystick2:)
|
|
12
|
+
@keyboard = keyboard
|
|
13
|
+
@joystick2 = joystick2
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def read_a(port_a, port_b)
|
|
17
|
+
keyboard.read_a(port_a, port_b) & joystick2.port_bits
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def read_b(port_a, port_b)
|
|
21
|
+
keyboard.read_b(port_a, port_b)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/badline/cpu.rb
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class CPU < Cycleable
|
|
5
|
+
STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1,
|
|
6
|
+
:overflow, :negative].freeze
|
|
7
|
+
|
|
8
|
+
class InvalidOpcodeError < StandardError; end
|
|
9
|
+
include IntegerHelper
|
|
10
|
+
include InstructionSet
|
|
11
|
+
include Traps
|
|
12
|
+
|
|
13
|
+
attr_reader :memory, :instructions, :boundary_crossed
|
|
14
|
+
attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y,
|
|
15
|
+
:nmi, :irq
|
|
16
|
+
|
|
17
|
+
def initialize(memory = nil, debug: false)
|
|
18
|
+
@debug = debug
|
|
19
|
+
@memory = memory || Memory.new
|
|
20
|
+
@status = Status.new(STATUS_FLAGS, value: 0b00100000)
|
|
21
|
+
reset_registers
|
|
22
|
+
|
|
23
|
+
@nmi = @irq = false
|
|
24
|
+
|
|
25
|
+
@instructions = 0
|
|
26
|
+
@traps = nil
|
|
27
|
+
super()
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset!
|
|
31
|
+
status.interrupt = true
|
|
32
|
+
reset_registers
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def p
|
|
36
|
+
status.value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def p=(new_value)
|
|
40
|
+
status.value = new_value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def step!
|
|
44
|
+
@loop.resume unless @instruction || @interrupt
|
|
45
|
+
cycle! while @instruction || @interrupt
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inspect
|
|
49
|
+
"Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
|
|
50
|
+
"SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
|
|
51
|
+
"Y: #{format8(y)}, P: #{format8(p)}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def extra_cycle(instruction)
|
|
57
|
+
return cycle unless instruction.boundary_cycle?
|
|
58
|
+
|
|
59
|
+
boundary_crossed && cycle
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_interrupt(vector, brk: false, pre_cycles: 2)
|
|
63
|
+
pre_cycles.times { cycle }
|
|
64
|
+
|
|
65
|
+
pc = program_counter
|
|
66
|
+
pc = (pc + 1) & 0xffff if brk
|
|
67
|
+
|
|
68
|
+
write_byte(stack_address, high_byte(pc))
|
|
69
|
+
@stack_pointer = (@stack_pointer - 1) & 0xff
|
|
70
|
+
write_byte(stack_address, low_byte(pc))
|
|
71
|
+
@stack_pointer = (@stack_pointer - 1) & 0xff
|
|
72
|
+
write_byte(stack_address,
|
|
73
|
+
status.clone.tap { |s| s.break = brk }.value)
|
|
74
|
+
@stack_pointer = (@stack_pointer - 1) & 0xff
|
|
75
|
+
status.interrupt = true
|
|
76
|
+
@program_counter = read_word(vector)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_interrupts
|
|
80
|
+
@interrupt = if nmi
|
|
81
|
+
0xfffa
|
|
82
|
+
elsif irq && !status.interrupt?
|
|
83
|
+
0xfffe
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
handle_interrupt(@interrupt) if @interrupt
|
|
87
|
+
@interrupt = nil
|
|
88
|
+
@nmi = @irq = false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read_byte(addr)
|
|
92
|
+
cycle { @memory.peek(addr) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def read_word(addr)
|
|
96
|
+
uint16(read_byte(addr),
|
|
97
|
+
read_byte((addr + 1) & 0xffff))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read_zeropage_word(addr)
|
|
101
|
+
uint16(read_byte(addr & 0xff),
|
|
102
|
+
read_byte((addr + 1) & 0xff))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def read_instruction
|
|
106
|
+
Instruction.find(@memory.peek(@program_counter))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def read_operand(instruction)
|
|
110
|
+
return nil unless instruction.operand?
|
|
111
|
+
# JSR fetches its operand high byte late (see Stack#jsr).
|
|
112
|
+
return read_byte(program_counter) if instruction.name == :jsr
|
|
113
|
+
return read_word(program_counter) if instruction.operand_length == 2
|
|
114
|
+
|
|
115
|
+
read_byte(program_counter)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def reset_registers
|
|
119
|
+
# The program counter is initialized from the reset vector
|
|
120
|
+
@program_counter = @memory.peek16(0xfffc)
|
|
121
|
+
# Stack pointer starts at 0x01ff and grows down
|
|
122
|
+
@stack_pointer = 0xff
|
|
123
|
+
@a = @x = @y = 0x0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def read_address(instruction, operand)
|
|
127
|
+
case instruction.addressing_mode
|
|
128
|
+
when :implied, :immediate
|
|
129
|
+
nil
|
|
130
|
+
when :accumulator
|
|
131
|
+
:accumulator
|
|
132
|
+
when :relative
|
|
133
|
+
(@program_counter + signed_int8(operand) + 1) & 0xffff
|
|
134
|
+
when :zeropage, :absolute
|
|
135
|
+
operand
|
|
136
|
+
when :zeropage_x
|
|
137
|
+
cycle { (operand + @x) & 0xff }
|
|
138
|
+
when :zeropage_y
|
|
139
|
+
cycle { (operand + @y) & 0xff }
|
|
140
|
+
when :absolute_x
|
|
141
|
+
# Do an extra cycle if page boundary is crossed
|
|
142
|
+
@boundary_crossed = high_byte(operand + @x) != high_byte(operand)
|
|
143
|
+
extra_cycle(instruction)
|
|
144
|
+
(operand + @x) & 0xffff
|
|
145
|
+
when :absolute_y
|
|
146
|
+
@boundary_crossed = high_byte(operand + @y) != high_byte(operand)
|
|
147
|
+
# Do an extra cycle if page boundary is crossed
|
|
148
|
+
extra_cycle(instruction)
|
|
149
|
+
(operand + @y) & 0xffff
|
|
150
|
+
when :indirect
|
|
151
|
+
# This is only used for JMP. There's no carry associated, so an
|
|
152
|
+
# indirect jump to $30FF will wrap around on the same page and read
|
|
153
|
+
# from [0x30ff, 0x3000].
|
|
154
|
+
uint16(
|
|
155
|
+
read_byte(operand),
|
|
156
|
+
read_byte(uint16(
|
|
157
|
+
(low_byte(operand) + 1) & 0xff, # Wrap around low byte
|
|
158
|
+
high_byte(operand)
|
|
159
|
+
))
|
|
160
|
+
)
|
|
161
|
+
when :indirect_x
|
|
162
|
+
cycle
|
|
163
|
+
read_zeropage_word(operand + @x)
|
|
164
|
+
when :indirect_y
|
|
165
|
+
value = read_zeropage_word(operand)
|
|
166
|
+
@boundary_crossed = ((value & 0xff) + y) > 0xff
|
|
167
|
+
# Do an extra cycle if page boundary is crossed
|
|
168
|
+
extra_cycle(instruction)
|
|
169
|
+
(value + y) & 0xffff
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def realize_value(instruction, operand, address)
|
|
174
|
+
case instruction.addressing_mode
|
|
175
|
+
when :implied
|
|
176
|
+
raise "Implied value can't be realized"
|
|
177
|
+
when :accumulator
|
|
178
|
+
@a
|
|
179
|
+
when :immediate
|
|
180
|
+
operand
|
|
181
|
+
else
|
|
182
|
+
read_byte(address)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def stack_address(offset = 0)
|
|
187
|
+
uint16((stack_pointer + offset) & 0xff, 0x01)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def log(instruction, operand, address)
|
|
191
|
+
return unless @debug
|
|
192
|
+
|
|
193
|
+
pc = (@program_counter - 1) - instruction.operand_length
|
|
194
|
+
puts(
|
|
195
|
+
"#{@cycles}: " \
|
|
196
|
+
"PC: #{pc.to_s(16)} - " \
|
|
197
|
+
"#{@instruction.name.upcase} #{@instruction.addressing_mode} " \
|
|
198
|
+
"Operand: #{operand.inspect} Address: #{address.inspect}"
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def main_loop
|
|
203
|
+
if nmi || (irq && !status.interrupt?)
|
|
204
|
+
handle_interrupts
|
|
205
|
+
else
|
|
206
|
+
run_traps
|
|
207
|
+
@boundary_crossed = false
|
|
208
|
+
@instruction = read_instruction
|
|
209
|
+
raise InvalidOpcodeError unless @instruction
|
|
210
|
+
|
|
211
|
+
@program_counter = (@program_counter + 1) & 0xffff
|
|
212
|
+
@cycles += 1
|
|
213
|
+
|
|
214
|
+
@operand = operand = read_operand(@instruction)
|
|
215
|
+
@address = address = read_address(@instruction, operand)
|
|
216
|
+
|
|
217
|
+
@program_counter = (@program_counter + @instruction.operand_length) &
|
|
218
|
+
0xffff
|
|
219
|
+
|
|
220
|
+
log(@instruction, operand, address)
|
|
221
|
+
|
|
222
|
+
# Run the instruction; :lazy realizes the value through #resolve
|
|
223
|
+
send(@instruction.name, address, :lazy)
|
|
224
|
+
|
|
225
|
+
@instructions += 1
|
|
226
|
+
@instruction = nil
|
|
227
|
+
end
|
|
228
|
+
Fiber.yield
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def write_byte(addr, value)
|
|
232
|
+
if addr == :accumulator
|
|
233
|
+
@a = value
|
|
234
|
+
else
|
|
235
|
+
cycle(write: true) { @memory.poke(addr, value) }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|