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
@@ -1,33 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/cartridge/bank"
4
+ require "badline/cartridge/flash"
3
5
  require "badline/cartridge/standard"
6
+ require "badline/cartridge/simons_basic"
4
7
  require "badline/cartridge/ocean"
8
+ require "badline/cartridge/fun_play"
9
+ require "badline/cartridge/super_games"
10
+ require "badline/cartridge/epyx_fastload"
11
+ require "badline/cartridge/westermann"
12
+ require "badline/cartridge/rex_utility"
13
+ require "badline/cartridge/game_system"
14
+ require "badline/cartridge/dinamic"
15
+ require "badline/cartridge/zaxxon"
5
16
  require "badline/cartridge/magic_desk"
17
+ require "badline/cartridge/comal80"
18
+ require "badline/cartridge/easy_flash"
19
+ require "badline/cartridge/mach5"
20
+ require "badline/cartridge/pagefox"
21
+ require "badline/cartridge/rgcd"
22
+ require "badline/cartridge/g_mod2"
23
+ require "badline/cartridge/freezer"
24
+ require "badline/cartridge/action_replay"
25
+ require "badline/cartridge/atomic_power"
26
+ require "badline/cartridge/final_cartridge3"
27
+ require "badline/cartridge/retro_replay"
28
+ require "badline/cartridge/kcs_power"
6
29
 
7
30
  module Badline
8
31
  class Cartridge
9
32
  class UnsupportedTypeError < StandardError; end
10
33
 
11
- ROML_START = 0x8000
12
34
  ROMH_START = 0xa000
13
- ULTIMAX_ROMH_START = 0xe000
14
35
  BANK_SIZE = 0x2000
15
36
 
16
- HARDWARE_TYPES = { 0 => :Standard, 5 => :Ocean, 19 => :MagicDesk }.freeze
37
+ HARDWARE_TYPES = {
38
+ 0 => :Standard, 1 => :ActionReplay, 2 => :KCSPower, 3 => :FinalCartridge3,
39
+ 4 => :SimonsBasic, 5 => :Ocean, 7 => :FunPlay, 8 => :SuperGames,
40
+ 9 => :AtomicPower, 10 => :EpyxFastload, 11 => :Westermann,
41
+ 12 => :RexUtility, 15 => :GameSystem, 17 => :Dinamic, 18 => :Zaxxon,
42
+ 19 => :MagicDesk, 21 => :Comal80, 32 => :EasyFlash, 36 => :RetroReplay,
43
+ 51 => :Mach5, 53 => :Pagefox, 57 => :RGCD, 60 => :GMod2
44
+ }.freeze
45
+
46
+ # The EXROM and GAME line levels of each memory configuration. The lines
47
+ # are active low.
48
+ MODES = {
49
+ rom8k: [0, 1].freeze, rom16k: [0, 0].freeze,
50
+ ultimax: [1, 0].freeze, off: [1, 1].freeze
51
+ }.freeze
17
52
 
18
53
  attr_reader :name, :exrom, :game, :roml, :romh
19
54
 
55
+ # A callable returning the current cycle, for mappers with timed state.
56
+ attr_writer :clock
57
+
20
58
  class << self
21
- def from_file(path)
22
- from_crt(Storage::CRTFile.new(path))
59
+ # Options set the cartridge's jumpers, for the mappers that have them.
60
+ def from_file(path, **)
61
+ from_crt(Storage::CRTFile.new(path), **)
23
62
  end
24
63
 
25
- def from_crt(crt)
64
+ def from_crt(crt, **)
26
65
  type = HARDWARE_TYPES.fetch(crt.hardware_type) do
27
66
  raise UnsupportedTypeError,
28
67
  "Unsupported cartridge hardware type #{crt.hardware_type}"
29
68
  end
30
- const_get(type).new(crt)
69
+ const_get(type).new(crt, **)
31
70
  end
32
71
  end
33
72
 
@@ -37,6 +76,10 @@ module Badline
37
76
  @game = crt.game
38
77
  @roml = @romh = nil
39
78
  @on_change = nil
79
+ @on_nmi_change = nil
80
+ @nmi = false
81
+ @open_bus = nil
82
+ @clock = nil
40
83
  install_chips(crt.chips)
41
84
  end
42
85
 
@@ -44,32 +87,105 @@ module Badline
44
87
  @on_change = block
45
88
  end
46
89
 
90
+ # Called with the level of the cartridge's pull on the NMI line when it
91
+ # changes. The line is wired-OR with CIA 2's.
92
+ def on_nmi_change(&block)
93
+ @on_nmi_change = block
94
+ end
95
+
96
+ def nmi?
97
+ @nmi
98
+ end
99
+
100
+ # The address bus hands over the C64's RAM, for mappers that write
101
+ # through to it, and the open bus, for I/O reads the cartridge doesn't
102
+ # drive.
103
+ def connect(ram:, open_bus:)
104
+ @ram = ram
105
+ @open_bus = open_bus
106
+ end
107
+
47
108
  def ultimax?
48
109
  @game.zero? && @exrom == 1
49
110
  end
50
111
 
51
- # IO1/IO2 reads are open bus unless a mapper says otherwise.
52
- def peek(_addr)
53
- 0xff
112
+ # Whether the first half of the cycle, where the VIC does most of its
113
+ # fetching, sees Ultimax mode. Some cartridges drive GAME and EXROM
114
+ # differently in the two halves; #ultimax? is the second, the CPU's.
115
+ def phi1_ultimax?
116
+ ultimax?
117
+ end
118
+
119
+ # The I/O pages ($de, $df) whose reads the mapper drives. Reads of the
120
+ # others are open bus.
121
+ def readable_io_pages
122
+ []
54
123
  end
55
124
 
56
125
  def poke(_addr, _value); end
57
126
 
127
+ # A window taking writes to $E000-$FFFF while reads there see the C64's
128
+ # memory, for a cartridge that asserts Ultimax on write cycles only.
129
+ def romh_writes; end
130
+
131
+ # A window at $A000-$BFFF in Ultimax mode, where the C64 maps nothing.
132
+ def ultimax_a000; end
133
+
134
+ # The RES line on the expansion port.
135
+ def reset; end
136
+
137
+ # The freeze button, which cartridges without one ignore.
138
+ def press_button; end
139
+
140
+ def release_button; end
141
+
58
142
  private
59
143
 
144
+ def nmi=(level)
145
+ return if @nmi == level
146
+
147
+ @nmi = level
148
+ @on_nmi_change&.call(level)
149
+ end
150
+
60
151
  def changed!
61
152
  @on_change&.call
62
153
  end
63
154
 
155
+ def mode=(mode)
156
+ @exrom, @game = MODES.fetch(mode)
157
+ end
158
+
159
+ def open_bus(addr)
160
+ @open_bus ? @open_bus.peek(addr) : 0xff
161
+ end
162
+
64
163
  def install_chips(_chips)
65
164
  raise NotImplementedError
66
165
  end
67
166
 
68
- # Chips smaller than 8K (e.g. 4K Ultimax ROMs) have unconnected upper
69
- # address lines and mirror across the full bank.
70
- def rom_bank(data, start)
71
- data *= BANK_SIZE / data.length if data.length < BANK_SIZE
72
- ROM.new(data, length: data.length, start:)
167
+ def rom_bank(data)
168
+ Bank.new(data)
169
+ end
170
+
171
+ def bank(banks, number)
172
+ banks[number] || EMPTY_BANK
173
+ end
174
+
175
+ # Sorts the CHIP packets into ROML and ROMH banks by load address. A 16K
176
+ # chip at $8000 spans both.
177
+ def banks_from(chips)
178
+ roml = []
179
+ romh = []
180
+ chips.each do |chip|
181
+ if chip.address >= ROMH_START
182
+ romh[chip.bank] = rom_bank(chip.data)
183
+ else
184
+ roml[chip.bank] = rom_bank(chip.data[0, BANK_SIZE])
185
+ romh[chip.bank] = rom_bank(chip.data[BANK_SIZE, BANK_SIZE]) if chip.data.length > BANK_SIZE
186
+ end
187
+ end
188
+ [roml, romh]
73
189
  end
74
190
  end
75
191
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CIA
5
+ # The interrupt control register: the source flags, their mask and the
6
+ # IR bit that drives the interrupt line, with the 6526's acknowledge
7
+ # timing.
8
+ class InterruptRegister
9
+ attr_reader :status, :mask, :quiet
10
+
11
+ def initialize
12
+ @mask = Status.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
13
+ @status = Status.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
14
+ @pending = 0
15
+ @read = @read_last_cycle = @read_two_cycles_ago = nil
16
+ @timer_b_bug = false
17
+ @quiet = true
18
+ end
19
+
20
+ # The CIA skips this while quiet: no read is recent enough to matter
21
+ # and no assert is due.
22
+ def cycle!
23
+ @read_two_cycles_ago = @read_last_cycle
24
+ @read_last_cycle = @read
25
+ @read = nil
26
+ if @pending.positive?
27
+ @pending -= 1
28
+ status.interrupt = true if @pending.zero?
29
+ end
30
+ @quiet = @pending.zero? && @read_last_cycle.nil? && @read_two_cycles_ago.nil?
31
+ end
32
+
33
+ def interrupted? = status.value >= 0x80
34
+
35
+ # Schedules IR to rise after delay cycles, keeping an earlier one.
36
+ def assert!(delay = 1)
37
+ @pending = @pending.positive? ? [@pending, delay].min : delay
38
+ @quiet = false
39
+ end
40
+
41
+ # Latch a source, pulling the interrupt line if it is armed.
42
+ def flag(source)
43
+ status.public_send(:"#{source}=", true)
44
+ assert! if mask.public_send(:"#{source}?")
45
+ end
46
+
47
+ # The 6526 timer B bug: an underflow on the cycle after a read still
48
+ # raises the flag, but the next read drops it unseen.
49
+ def timer_b_underflow!
50
+ @timer_b_bug = !@read_last_cycle.nil?
51
+ flag(:timer_b)
52
+ end
53
+
54
+ # A read releases the interrupt line at once, and cancels an assert
55
+ # due on the next cycle, but on the 6526 its IR acknowledge lands a
56
+ # cycle late: the next cycle still reads IR set if it was set, or
57
+ # about to be.
58
+ def read
59
+ status.timer_b = false if @timer_b_bug
60
+ @timer_b_bug = false
61
+ value = status.value
62
+ @read = @pending == 1 ? value | 0x80 : value
63
+ @quiet = false
64
+ value |= 0x80 if @read_last_cycle&.anybits?(0x80)
65
+ status.value = 0x0
66
+ @pending = 0
67
+ value
68
+ end
69
+
70
+ # Bit 7 picks between setting and clearing the mask bits given.
71
+ def write(value)
72
+ if value.nobits?(0x80)
73
+ mask.value &= ~(value & 0x1f)
74
+ else
75
+ mask.value |= (value & 0x1f)
76
+ end
77
+ if mask.value.anybits?(status.value & 0x1f)
78
+ assert!(2) unless interrupted?
79
+ elsif @pending == 1 && @read_two_cycles_ago
80
+ # On the 6526, masking the pending source cancels the assert due
81
+ # on the next cycle only while a read two cycles back is
82
+ # acknowledging.
83
+ @pending = 0
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class CIA
5
+ # The serial shift register and the CNT line that clocks it.
6
+ #
7
+ # With CRA bit 6 clear the port is an input: SP is sampled on every
8
+ # rising CNT edge, and the byte reaches the data register after eight of
9
+ # them. With the bit set the CIA drives CNT itself, toggling it on every
10
+ # timer A underflow, so a byte takes sixteen underflows to go out. CNT falls
11
+ # as each bit is put on SP, and rises again to clock it in.
12
+ #
13
+ # The register counts itself empty as soon as the eighth bit is on SP,
14
+ # fifteen underflows in: the serial flag follows a few cycles later
15
+ # rather than waiting for the sixteenth underflow, and a byte written
16
+ # from that point on goes out on the next one.
17
+ class Serial
18
+ BITS = 8
19
+
20
+ # A byte written to the data register reaches the shift register one
21
+ # cycle later. The empty countdown starts on the underflow that puts
22
+ # the eighth bit on SP and is decremented on that same cycle, so the
23
+ # flag lands four cycles after it.
24
+ LOAD_DELAY = 1
25
+ EMPTY_DELAY = 5
26
+ BUSY_DELAY = 5
27
+
28
+ # The logic watching the shift register runs a few cycles behind it,
29
+ # so the level saying a bit is still on its way out is scheduled
30
+ # through a delay line: a bit set at position 7 - n takes effect n
31
+ # cycles later. Putting a bit on SP raises the level after one cycle
32
+ # and drops it again after two before raising it for good after
33
+ # three; the eighth bit skips straight to the last of those. CNT
34
+ # rising over a bit lowers the level four cycles later.
35
+ IN_FLIGHT = 0x50
36
+ LAST_IN_FLIGHT = 0x10
37
+ BOUNCE = 0x20
38
+ SETTLED = 0x08
39
+
40
+ attr_reader :data, :sp_out, :cnt, :cnt_in, :idle
41
+ attr_accessor :sp_in
42
+
43
+ def initialize(control)
44
+ @control = control
45
+ @data = 0x0
46
+ @shift = 0x0
47
+ @steps = 0
48
+ @empty_in = nil
49
+ @busy_in = nil
50
+ @busy = false
51
+ @abandoned = false
52
+ @pending = nil
53
+ @underflow_high = false
54
+ @in_flight = false
55
+ @flight_up = 0x0
56
+ @flight_down = 0x0
57
+ @idle = true
58
+ # Nothing drives the user port, so both lines float high.
59
+ @cnt = true
60
+ @cnt_in = true
61
+ @sp_in = true
62
+ @sp_out = true
63
+ end
64
+
65
+ def output?
66
+ @control.serial_mode?
67
+ end
68
+
69
+ def cnt_in=(level)
70
+ @cnt_in = level
71
+ @cnt = level unless output?
72
+ end
73
+
74
+ def write(value)
75
+ @data = value
76
+ @pending = LOAD_DELAY
77
+ @idle = false
78
+ end
79
+
80
+ # Clocked every cycle with timer A's underflow line. Yields once the
81
+ # shift register reports itself empty, which it does without waiting
82
+ # for the underflow that raises CNT over the eighth bit.
83
+ def cycle!(underflowed)
84
+ return if @idle && !underflowed
85
+
86
+ shift(underflowed)
87
+ @pending -= 1 if @pending&.positive?
88
+ drain_flight
89
+ count_busy
90
+ @idle = !underflowed && idle?
91
+ return if @empty_in.nil?
92
+
93
+ @empty_in -= 1
94
+ return unless @empty_in.zero?
95
+
96
+ @empty_in = nil
97
+ @busy = false
98
+ yield
99
+ end
100
+
101
+ # Clocked by rising CNT edges while receiving. Yields once a whole
102
+ # byte has arrived.
103
+ def rising_edge!
104
+ return if output?
105
+
106
+ @shift = ((@shift << 1) | (@sp_in ? 1 : 0)) & 0xff
107
+ @steps += 1
108
+ return unless @steps == BITS
109
+
110
+ @steps = 0
111
+ @data = @shift
112
+ yield if block_given?
113
+ end
114
+
115
+ # A mode change drops the byte in flight and hands CNT back to
116
+ # whichever side drives it now, and yields: that byte counts as gone.
117
+ # Handing the port over as an input tears a transmission down, so a
118
+ # busy register reports it; the same change latches how far the bit
119
+ # in flight had got, and taking the port back reports that.
120
+ def reset!
121
+ abandoned = output? ? @abandoned : @busy
122
+ @abandoned = output? ? false : @in_flight
123
+ @in_flight = false
124
+ @steps = 0
125
+ @empty_in = nil
126
+ @busy_in = nil
127
+ @busy = false
128
+ @pending = nil
129
+ @cnt = output? || @cnt_in
130
+ yield if abandoned
131
+ end
132
+
133
+ private
134
+
135
+ # Without an underflow, a register with nothing counting down and
136
+ # nothing in the delay lines has nothing to do on a cycle.
137
+ def idle?
138
+ !@pending&.positive? && (@flight_up | @flight_down).zero? &&
139
+ @busy_in.nil? && @empty_in.nil?
140
+ end
141
+
142
+ # The shift register picks up a waiting byte whenever the underflow
143
+ # line is asserted, but every half-step after that needs a fresh edge
144
+ # on it. A zero latch holds the line down for good rather than
145
+ # pulsing it, so such a byte stops after its first bit and never
146
+ # reaches the data register.
147
+ def shift(underflowed)
148
+ edge = underflowed && !@underflow_high
149
+ @underflow_high = underflowed
150
+ clock(edge) if underflowed && output?
151
+ end
152
+
153
+ def clock(edge)
154
+ start if @steps <= 1
155
+ if @steps.zero?
156
+ @flight_down |= SETTLED
157
+ elsif edge || @steps == BITS * 2
158
+ half_step
159
+ end
160
+ end
161
+
162
+ def half_step
163
+ @steps -= 1
164
+ @cnt = @steps.even?
165
+ if @cnt
166
+ @flight_down |= SETTLED
167
+ else
168
+ shift_out
169
+ end
170
+ end
171
+
172
+ # The register reads as busy from a few cycles after the first bit
173
+ # reaches SP until it reports itself empty over the eighth.
174
+ def mark_busy
175
+ case @steps
176
+ when (BITS * 2) - 1 then @busy_in = BUSY_DELAY
177
+ when 1
178
+ @empty_in = EMPTY_DELAY
179
+ @busy_in = nil
180
+ end
181
+ end
182
+
183
+ def count_busy
184
+ return if @busy_in.nil?
185
+
186
+ @busy_in -= 1
187
+ return unless @busy_in.zero?
188
+
189
+ @busy_in = nil
190
+ @busy = true
191
+ end
192
+
193
+ def drain_flight
194
+ @in_flight = false if @flight_down.anybits?(0x80)
195
+ @flight_down = (@flight_down << 1) & 0xff
196
+ @in_flight = true if @flight_up.anybits?(0x80)
197
+ @flight_up = (@flight_up << 1) & 0xff
198
+ end
199
+
200
+ # A byte only moves out of the data register once the one before it
201
+ # has put its last bit on SP.
202
+ def start
203
+ return unless @pending&.zero?
204
+
205
+ @pending = nil
206
+ @shift = @data
207
+ @steps = BITS * 2
208
+ end
209
+
210
+ def shift_out
211
+ @sp_out = @shift.anybits?(0x80)
212
+ @shift = (@shift << 1) & 0xff
213
+ mark_busy
214
+ @flight_up |= @steps == 1 ? LAST_IN_FLIGHT : IN_FLIGHT
215
+ @flight_down |= BOUNCE
216
+ end
217
+ end
218
+ end
219
+ end
@@ -8,33 +8,38 @@ module Badline
8
8
 
9
9
  def initialize(control)
10
10
  @control = control
11
- @counter = @latch = 0x0
11
+ @counter = @latch = 0xffff
12
12
  @pipe = 0
13
13
  @load_delay = 0
14
14
  @reload = false
15
15
  @underflowed = false
16
16
  @oneshot_linger = 0
17
17
  @toggle = true
18
+ settle
18
19
  end
19
20
 
20
- def toggle?
21
- @toggle
21
+ # The level this timer drives on its port B pin: a square wave that
22
+ # flips on each underflow, or a single high tick when one happens.
23
+ def output?
24
+ control.out_mode? ? @toggle : @underflowed
22
25
  end
23
26
 
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?
27
+ def cycle!(feed)
28
+ feed &&= @control.value & 0x01 != 0
29
+ if @settled
30
+ return @counter -= 1 if feed && @counter > 1
31
+ elsif @empty
32
+ enter if feed
29
33
  return
30
34
  end
31
- run_tick(feed, pulse)
35
+ run_tick(feed)
32
36
  end
33
37
 
34
- def run_tick(feed, pulse)
38
+ def run_tick(feed)
35
39
  @underflowed = false
36
40
  @oneshot_linger -= 1 if @oneshot_linger.positive?
37
- tick(feed && started?, pulse)
41
+ tick(feed)
42
+ settle
38
43
  end
39
44
 
40
45
  def write_control(value)
@@ -42,6 +47,7 @@ module Badline
42
47
  @oneshot_linger = 2 if control.run_mode? && value.nobits?(0x08)
43
48
  control.value = value & ~0x10
44
49
  @load_delay = 3 if value.anybits?(0x10)
50
+ settle
45
51
  end
46
52
 
47
53
  def write_latch_low(value)
@@ -56,23 +62,33 @@ module Badline
56
62
  private
57
63
 
58
64
  def started?
59
- control.value.anybits?(0x01)
65
+ @control.value & 0x01 != 0
66
+ end
67
+
68
+ def enter
69
+ @pipe = 0b10
70
+ @empty = false
60
71
  end
61
72
 
62
- def steady?
63
- @counter > 1 && @pipe == 0b11 && !@reload &&
64
- (@load_delay | @oneshot_linger).zero? && started?
73
+ # With no load, reload or one-shot linger pending, a full pipe only
74
+ # counts and an empty one only waits to be fed.
75
+ def settle
76
+ quiet = (@load_delay | @oneshot_linger).zero? && !@reload
77
+ @settled = quiet && @pipe == 0b11
78
+ @empty = quiet && @pipe.zero?
65
79
  end
66
80
 
67
- def tick(feed, pulse)
68
- counting = @pipe.anybits?(0b01) && pulse
81
+ def tick(feed)
82
+ counting = @pipe.anybits?(0b01)
69
83
  @pipe = (@pipe >> 1) | (feed ? 0b10 : 0)
84
+ loading = @reload || @load_delay == 1
85
+ @reload = false
70
86
 
71
- if premature_underflow?(pulse)
87
+ if loading
88
+ reload
89
+ elsif premature_underflow?
72
90
  underflow
73
- elsif apply_reload?
74
- return
75
- elsif counting && @load_delay != 1
91
+ elsif counting
76
92
  count
77
93
  end
78
94
 
@@ -87,20 +103,15 @@ module Badline
87
103
  end
88
104
 
89
105
  # the final pipeline stage, before any pending load lands
90
- def premature_underflow?(pulse)
91
- @counter.zero? && !@reload && pulse && @pipe.anybits?(0b01) &&
92
- started?
106
+ def premature_underflow?
107
+ @counter.zero? && @pipe.anybits?(0b01) && started?
93
108
  end
94
109
 
95
- # An underflow reload consumes the tick after the flag
96
- def apply_reload?
97
- return false unless @reload
98
-
99
- @reload = false
110
+ # A load consumes its tick, so the counter never decrements on it
111
+ def reload
100
112
  @counter = @latch
101
113
  # A zero latch underflows again on the reload tick while running
102
- underflow if @counter.zero? && started?
103
- true
114
+ underflow if @counter.zero? && started? && @pipe.anybits?(0b01)
104
115
  end
105
116
 
106
117
  def count
@@ -111,6 +122,7 @@ module Badline
111
122
  def underflow
112
123
  @underflowed = true
113
124
  @reload = true
125
+ @counter = @latch
114
126
  @toggle = !@toggle
115
127
  return unless control.run_mode? || @oneshot_linger.positive?
116
128