badline 0.1.0 → 0.2.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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +88 -23
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +131 -15
  51. data/lib/badline/cia/interrupt_register.rb +88 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +109 -74
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +68 -8
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +171 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +108 -167
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +33 -0
  85. data/lib/badline/kernal_trap/drive.rb +239 -0
  86. data/lib/badline/kernal_trap/file.rb +4 -24
  87. data/lib/badline/kernal_trap/load.rb +2 -2
  88. data/lib/badline/kernal_trap/routine.rb +34 -0
  89. data/lib/badline/kernal_trap/save.rb +1 -1
  90. data/lib/badline/kernal_trap/serial.rb +139 -0
  91. data/lib/badline/kernal_trap.rb +4 -0
  92. data/lib/badline/keyboard.rb +40 -16
  93. data/lib/badline/media.rb +48 -10
  94. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  95. data/lib/badline/roms/eapi/README +41 -0
  96. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  97. data/lib/badline/sid/decimator.rb +46 -0
  98. data/lib/badline/sid/envelope.rb +232 -0
  99. data/lib/badline/sid/filter.rb +213 -0
  100. data/lib/badline/sid/voice.rb +61 -0
  101. data/lib/badline/sid/waveform/combined.rb +126 -0
  102. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  103. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  104. data/lib/badline/sid/waveform.rb +289 -0
  105. data/lib/badline/sid.rb +274 -7
  106. data/lib/badline/status.rb +32 -27
  107. data/lib/badline/storage/crt_file.rb +18 -2
  108. data/lib/badline/storage/disk_image.rb +52 -5
  109. data/lib/badline/storage/host_directory.rb +18 -4
  110. data/lib/badline/storage/sid_file.rb +274 -0
  111. data/lib/badline/storage/song_lengths.rb +65 -0
  112. data/lib/badline/storage/t64.rb +67 -0
  113. data/lib/badline/storage/tap.rb +68 -0
  114. data/lib/badline/storage.rb +19 -0
  115. data/lib/badline/time_of_day.rb +105 -52
  116. data/lib/badline/version.rb +1 -1
  117. data/lib/badline/vic/bank.rb +14 -6
  118. data/lib/badline/vic/border_mask.rb +76 -0
  119. data/lib/badline/vic/collisions.rb +104 -0
  120. data/lib/badline/vic/color_patches.rb +33 -0
  121. data/lib/badline/vic/display_state.rb +92 -29
  122. data/lib/badline/vic/graphics_mode.rb +78 -53
  123. data/lib/badline/vic/graphics_shifter.rb +140 -0
  124. data/lib/badline/vic/register_log.rb +63 -0
  125. data/lib/badline/vic/registers.rb +3 -0
  126. data/lib/badline/vic/sequencer.rb +163 -137
  127. data/lib/badline/vic/sequencer_output.rb +116 -0
  128. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  129. data/lib/badline/vic/sprite/shifter.rb +129 -0
  130. data/lib/badline/vic/sprite.rb +275 -65
  131. data/lib/badline/vic/sprites.rb +220 -58
  132. data/lib/badline/vic.rb +373 -49
  133. data/lib/badline.rb +5 -0
  134. data/vendor/.gitkeep +0 -0
  135. metadata +103 -4
data/lib/badline/cia.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
+ require "badline/cia/interrupt_register"
5
+ require "badline/cia/serial"
4
6
  require "badline/cia/timer"
5
7
 
6
8
  module Badline
@@ -9,7 +11,11 @@ module Badline
9
11
  include Addressable
10
12
  extend Forwardable
11
13
 
12
- attr_reader :start, :control_a, :control_b, :interrupt_status, :interrupt_control, :peripheral
14
+ attr_reader :start, :control_a, :control_b, :peripheral, :serial
15
+
16
+ def_delegator :@icr, :status, :interrupt_status
17
+ def_delegator :@icr, :mask, :interrupt_control
18
+ def_delegator :@icr, :assert!, :interrupt!
13
19
 
14
20
  def_delegator :@ta, :counter, :timer_a
15
21
  def_delegator :@ta, :counter=, :timer_a=
@@ -24,45 +30,61 @@ module Badline
24
30
  addressable_at(start, length: 2**8)
25
31
 
26
32
  @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
+ @port_b4_handler = nil
34
+ @port_b4_high = true
35
+ reset!
36
+ end
37
+
38
+ # The RES line: ports back to inputs, timers stopped with their latches
39
+ # full, interrupts masked and the TOD clock stopped at 1:00:00.0. The
40
+ # PB4 level carries over and is resampled on the next cycle.
41
+ def reset!
42
+ @data_port_a = 0x00
43
+ @data_port_b = 0x00
44
+ @data_dir_a = 0x00
45
+ @data_dir_b = 0x00
46
+ @port_b4_driven_high = true
47
+ @cnt_high = true
48
+ @cnt_rise = false
33
49
  @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])
50
+ @icr = InterruptRegister.new
51
+ @icr_status = @icr.status
38
52
  @control_a = Status.new(%i[start output out_mode run_mode load
39
53
  in_mode serial_mode clock_frequency])
40
54
  @control_b = Status.new(%i[start output out_mode run_mode load
41
55
  in_cnt in_timer_a alarm])
42
56
  @ta = Timer.new(@control_a)
43
57
  @tb = Timer.new(@control_b)
58
+ @serial = Serial.new(@control_a)
44
59
  end
45
60
 
46
- def interrupt!(delay = 1)
47
- @irq_pending = @irq_pending.positive? ? [@irq_pending, delay].min : delay
61
+ # Register a change handler on the PB4 line. On CIA 1, this feeds the
62
+ # light pen input.
63
+ def on_port_b4_change(&handler)
64
+ @port_b4_handler = handler
48
65
  end
49
66
 
50
- def interrupted?
51
- interrupt_status.value.anybits?(0x80)
52
- end
67
+ # A falling edge on the FLAG pin. On CIA 1 the datasette's tape read
68
+ # line drives it, on CIA 2 the serial bus SRQ.
69
+ def flag! = raise_interrupt(:flag)
70
+
71
+ def interrupted? = @icr_status.value >= 0x80
53
72
 
54
73
  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
74
+ @icr.cycle! unless @icr.quiet
75
+ refresh_port_b4 if @port_b4_handler
76
+ level = @serial.cnt
77
+ level == @cnt_high ? @cnt_rise = false : cnt_edge(level)
78
+ underflowed = update_timers
79
+ @serial.cycle!(underflowed) { trigger_serial } if underflowed || !@serial.idle
60
80
  @tod.cycle! { trigger_alarm }
61
81
  end
62
82
 
63
83
  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
84
+ lines = driven_lines(@data_port_a, @data_dir_a)
85
+ return lines unless peripheral
86
+
87
+ lines & peripheral.read_a(lines, driven_lines(@data_port_b, @data_dir_b))
66
88
  end
67
89
 
68
90
  # Port A as driven by the data/direction registers alone, without
@@ -72,9 +94,9 @@ module Badline
72
94
  end
73
95
 
74
96
  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)
97
+ lines = driven_lines(@data_port_b, @data_dir_b)
98
+ lines &= peripheral.read_b(driven_lines(@data_port_a, @data_dir_a), lines) if peripheral
99
+ apply_timer_output(lines)
78
100
  end
79
101
 
80
102
  def peek(addr)
@@ -91,12 +113,8 @@ module Badline
91
113
  when 0x09 then @tod.seconds
92
114
  when 0x0a then @tod.minutes
93
115
  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
116
+ when 0x0c then @serial.data
117
+ when 0x0d then @icr.read
100
118
  when 0x0e then control_a.value
101
119
  when 0x0f then control_b.value
102
120
  end
@@ -105,9 +123,9 @@ module Badline
105
123
  def poke(addr, value)
106
124
  case index(addr) & 0x0f
107
125
  when 0x00 then @data_port_a = value
108
- when 0x01 then @data_port_b = value
126
+ when 0x01 then update_port_b { @data_port_b = value }
109
127
  when 0x02 then @data_dir_a = value
110
- when 0x03 then @data_dir_b = value
128
+ when 0x03 then update_port_b { @data_dir_b = value }
111
129
  when 0x04 then @ta.write_latch_low(value)
112
130
  when 0x05 then @ta.write_latch_high(value)
113
131
  when 0x06 then @tb.write_latch_low(value)
@@ -116,16 +134,32 @@ module Badline
116
134
  when 0x09 then @tod.write(:seconds, value, alarm: control_b.alarm?)
117
135
  when 0x0a then @tod.write(:minutes, value, alarm: control_b.alarm?)
118
136
  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)
137
+ when 0x0c then @serial.write(value)
138
+ when 0x0d then @icr.write(value)
139
+ when 0x0e then write_control_a(value)
123
140
  when 0x0f then @tb.write_control(value)
124
141
  end
125
142
  end
126
143
 
127
144
  private
128
145
 
146
+ def update_port_b
147
+ yield
148
+ @port_b4_driven_high = driven_lines(@data_port_b, @data_dir_b).anybits?(0x10)
149
+ refresh_port_b4 if @port_b4_handler
150
+ end
151
+
152
+ # PB4 is also control port 1's fire line, and a peripheral pulls it low
153
+ # without any register write, so the level is resampled every cycle rather
154
+ # than only after a poke.
155
+ def refresh_port_b4
156
+ high = @port_b4_driven_high && (peripheral.nil? || peripheral.port_b4_high?)
157
+ return if high == @port_b4_high
158
+
159
+ @port_b4_high = high
160
+ @port_b4_handler.call(high)
161
+ end
162
+
129
163
  def driven_lines(register, direction)
130
164
  # Output bits are driven from the data register; input bits float high.
131
165
  # External peripherals can still pull any line low (wired-AND).
@@ -133,57 +167,58 @@ module Badline
133
167
  end
134
168
 
135
169
  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?
170
+ value = with_bit(value, 6, @ta.output?) if control_a.output?
171
+ value = with_bit(value, 7, @tb.output?) if control_b.output?
138
172
  value
139
173
  end
140
174
 
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
175
  def with_bit(value, bit, set)
150
176
  set ? value | (1 << bit) : value & ~(1 << bit)
151
177
  end
152
178
 
153
- def trigger_alarm
154
- interrupt_status.alarm = true
155
- interrupt! if interrupt_control.alarm?
179
+ def trigger_alarm = raise_interrupt(:alarm)
180
+ def trigger_serial = raise_interrupt(:serial)
181
+
182
+ def raise_interrupt(source) = @icr.flag(source)
183
+
184
+ # CNT is sampled once a cycle. When the serial port drives it from this
185
+ # cycle's timer A underflow, the new level is picked up on the next one.
186
+ def cnt_edge(level)
187
+ @cnt_high = level
188
+ @cnt_rise = level
189
+ @serial.rising_edge! { trigger_serial } if level
156
190
  end
157
191
 
192
+ # Returns whether timer A underflowed, which clocks the serial port.
193
+ #
194
+ # CRB bits 6-5 pick timer B's source: ø2, CNT edges, timer A
195
+ # underflows, or timer A underflows gated by the CNT level. Every source
196
+ # drives the same count-enable line, so a cascaded underflow goes through
197
+ # the input pipeline exactly as a CNT edge does.
158
198
  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
199
+ @ta.cycle!(@control_a.value & 0x20 == 0x20 ? @cnt_rise : true)
200
+ underflowed = @ta.underflowed
201
+ @tb.cycle!(
202
+ case @control_b.value & 0x60
203
+ when 0x00 then true
204
+ when 0x20 then @cnt_rise
205
+ when 0x40 then underflowed
206
+ else underflowed && @cnt_high
207
+ end
208
+ )
209
+ if underflowed
167
210
  interrupt_status.timer_a = true
168
211
  interrupt! if interrupt_control.timer_a?
169
212
  end
170
- return unless @tb.underflowed
171
-
172
- interrupt_status.timer_b = true
173
- interrupt! if interrupt_control.timer_b?
213
+ @icr.timer_b_underflow! if @tb.underflowed
214
+ underflowed
174
215
  end
175
216
 
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?
217
+ def write_control_a(value)
218
+ was_output = control_a.serial_mode?
219
+ @ta.write_control(value)
220
+ @serial.reset! { trigger_serial } if control_a.serial_mode? != was_output
221
+ @tod.fifty_hz = control_a.clock_frequency?
187
222
  end
188
223
  end
189
224
  end
@@ -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
- private
11
-
12
- def blank_value
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
@@ -10,16 +10,23 @@ module Badline
10
10
 
11
11
  attr_reader :address_bus, :cpu, :cycles
12
12
 
13
- def_delegators :address_bus, :vic, :cia1, :cia2, :sid, :ram, :keyboard, :joystick2
13
+ def_delegators :address_bus, :vic, :cia1, :cia2, :sid, :ram, :keyboard, :joystick1, :joystick2,
14
+ :control_ports, :datasette, :install_debug_register
14
15
 
15
- def initialize(debug: false)
16
- @address_bus = AddressBus.new
16
+ def initialize(debug: false, sid_model: :mos6581)
17
+ @address_bus = AddressBus.new(sid_model:)
17
18
  @cpu = CPU.new(@address_bus, debug:)
18
19
  @vic = @address_bus.vic
20
+ @vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
19
21
  @cia1 = @address_bus.cia1
20
22
  @cia2 = @address_bus.cia2
23
+ @sid = @address_bus.sid
24
+ @datasette = @address_bus.datasette
21
25
  @cycles = 0
22
26
  @nmi_asserted = false
27
+ @cartridge_nmi = false
28
+ @freezing = false
29
+ @freeze_writes = 0
23
30
  @init_handlers = []
24
31
  @pending_keys = nil
25
32
  end
@@ -30,17 +37,19 @@ module Badline
30
37
  handle_init if @cycles == INIT_THRESHOLD
31
38
  feed_keyboard if @pending_keys
32
39
 
40
+ # The chips clock ahead of the CPU, so a register write lands on the
41
+ # cycle after the one it was issued on.
33
42
  @vic.cycle!
34
43
  @cia1.cycle!
35
44
  @cia2.cycle!
45
+ @sid.cycle!
46
+ @datasette.cycle!
36
47
 
37
48
  @cpu.irq = @cia1.interrupted? || @vic.interrupted?
38
49
 
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?
50
+ drive_nmi
51
+ watch_freeze if @freezing
52
+ @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
44
53
 
45
54
  @cycles += 1
46
55
  end
@@ -52,13 +61,45 @@ module Badline
52
61
  end
53
62
 
54
63
  def attach_cartridge(cartridge)
64
+ cartridge.clock = -> { @cycles }
65
+ cartridge.on_nmi_change { |level| @cartridge_nmi = level }
55
66
  address_bus.attach_cartridge(cartridge)
67
+ reset!
68
+ end
69
+
70
+ # The RES line reaches the CPU and its port, both CIAs, the SID and the
71
+ # cartridge port. The VIC has no reset pin.
72
+ def reset!
73
+ address_bus.reset_port!
74
+ @cia1.reset!
75
+ @cia2.reset!
76
+ @sid.reset!
77
+ address_bus.cartridge&.reset
78
+ @freezing = false
79
+ @nmi_asserted = false
56
80
  cpu.reset!
57
81
  end
58
82
 
83
+ # Presses the cartridge's freeze button. When the press pulls NMI, the
84
+ # cartridge freezes once the CPU takes the interrupt.
85
+ def press_cartridge_button
86
+ cartridge = address_bus.cartridge
87
+ return unless cartridge
88
+
89
+ cartridge.press_button
90
+ @freezing = cartridge.nmi?
91
+ @freeze_writes = 0
92
+ end
93
+
94
+ def release_cartridge_button
95
+ address_bus.cartridge&.release_button
96
+ end
97
+
59
98
  def mount(storage)
60
99
  load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, storage:)
61
100
  cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
101
+ drive = KernalTrap::Drive.new(storage)
102
+ KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
62
103
  return unless storage.respond_to?(:write_file)
63
104
 
64
105
  save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, storage:)
@@ -85,6 +126,25 @@ module Badline
85
126
 
86
127
  private
87
128
 
129
+ # The NMI line is wired-OR between CIA 2 and the cartridge, and the CPU
130
+ # takes an interrupt on its falling edge.
131
+ def drive_nmi
132
+ nmi = @cia2.interrupted? || @cartridge_nmi
133
+ @cpu.nmi = true if nmi && !@nmi_asserted
134
+ @nmi_asserted = nmi
135
+ end
136
+
137
+ # A freezer counts the CPU's write cycles once it pulls NMI and switches
138
+ # to Ultimax on the third in a row, the last push of the interrupt
139
+ # sequence, so the vector fetch that follows reads the cartridge.
140
+ def watch_freeze
141
+ @freeze_writes = @cpu.pending_write? ? @freeze_writes + 1 : 0
142
+ return if @freeze_writes < 3
143
+
144
+ @freezing = false
145
+ address_bus.cartridge.freeze!
146
+ end
147
+
88
148
  def booting?
89
149
  @cycles < init_threshold
90
150
  end
@@ -1,24 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- # CIA 1 peripheral combining the keyboard matrix with the control ports.
4
+ # CIA 1's control ports and the keyboard matrix that spans them.
5
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.
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
- attr_reader :keyboard, :joystick2
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 read_a(port_a, port_b)
17
- keyboard.read_a(port_a, port_b) & joystick2.port_bits
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 read_b(port_a, port_b)
21
- keyboard.read_b(port_a, port_b)
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
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CPU
5
+ # Steps that read an instruction's operand bytes and work out the
6
+ # address it operates on, which they leave in @address. Also the
7
+ # single-step implied, accumulator and immediate instructions, and JMP.
8
+ module Addressing
9
+ private
10
+
11
+ def op_implied_dummy
12
+ @memory.peek(@program_counter)
13
+ end
14
+
15
+ def op_implied_exec
16
+ @memory.peek(@program_counter)
17
+ send(@operation, nil, nil)
18
+ end_instruction
19
+ end
20
+
21
+ def op_accumulator_exec
22
+ @memory.peek(@program_counter)
23
+ send(@operation, :accumulator, @a)
24
+ end_instruction
25
+ end
26
+
27
+ def op_immediate_read
28
+ value = @memory.peek(@program_counter)
29
+ @program_counter = (@program_counter + 1) & 0xffff
30
+ send(@operation, nil, value)
31
+ end_instruction
32
+ end
33
+
34
+ def op_zp
35
+ @address = @memory.peek(@program_counter)
36
+ @program_counter = (@program_counter + 1) & 0xffff
37
+ end
38
+
39
+ def op_zp_x
40
+ @memory.peek(@address)
41
+ @address = (@address + @x) & 0xff
42
+ end
43
+
44
+ def op_zp_y
45
+ @memory.peek(@address)
46
+ @address = (@address + @y) & 0xff
47
+ end
48
+
49
+ def op_abs_low
50
+ @address = @memory.peek(@program_counter)
51
+ @program_counter = (@program_counter + 1) & 0xffff
52
+ end
53
+
54
+ def op_abs_high
55
+ @address |= @memory.peek(@program_counter) << 8
56
+ @program_counter = (@program_counter + 1) & 0xffff
57
+ end
58
+
59
+ def op_abs_high_x
60
+ high = @memory.peek(@program_counter)
61
+ @program_counter = (@program_counter + 1) & 0xffff
62
+ index_address(high, @x)
63
+ end
64
+
65
+ def op_abs_high_y
66
+ high = @memory.peek(@program_counter)
67
+ @program_counter = (@program_counter + 1) & 0xffff
68
+ index_address(high, @y)
69
+ end
70
+
71
+ def op_idx_dummy
72
+ @memory.peek(@dummy_address)
73
+ end
74
+
75
+ def op_zp_pointer
76
+ @pointer = @memory.peek(@program_counter)
77
+ @program_counter = (@program_counter + 1) & 0xffff
78
+ end
79
+
80
+ def op_pointer_x
81
+ @memory.peek(@pointer)
82
+ @pointer = (@pointer + @x) & 0xff
83
+ end
84
+
85
+ def op_pointer_low
86
+ @address = @memory.peek(@pointer)
87
+ end
88
+
89
+ def op_pointer_high
90
+ @address |= @memory.peek((@pointer + 1) & 0xff) << 8
91
+ end
92
+
93
+ def op_pointer_high_y
94
+ index_address(@memory.peek((@pointer + 1) & 0xff), @y)
95
+ end
96
+
97
+ def op_jmp_high
98
+ @program_counter = @address | (@memory.peek(@program_counter) << 8)
99
+ end_instruction
100
+ end
101
+
102
+ def op_indirect_low
103
+ @pointer = @address
104
+ @address = @memory.peek(@pointer)
105
+ end
106
+
107
+ # The pointer's high byte never increments, so JMP ($30FF) reads its
108
+ # target's high byte from $3000, not $3100.
109
+ def op_indirect_high
110
+ high = @memory.peek((@pointer & 0xff00) | ((@pointer + 1) & 0xff))
111
+ @program_counter = @address | (high << 8)
112
+ end_instruction
113
+ end
114
+
115
+ # Adds an index register to a base address. The 6502 adds to the low
116
+ # byte first and spends the next cycle reading from that address,
117
+ # before the carry reaches the high byte. Read instructions skip that
118
+ # cycle when there is no carry.
119
+ def index_address(high, index)
120
+ low = @address
121
+ @address = (((high << 8) | low) + index) & 0xffff
122
+ @boundary_crossed = (low + index) > 0xff
123
+ if @optional_dummy && !@boundary_crossed
124
+ @index += 1
125
+ else
126
+ @dummy_address = (high << 8) | ((low + index) & 0xff)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end