AY-3-8910 / YM2149 Noise LFSR Polynomial Bug
Summary
The noise generator in rtl/ym2149.sv used a 17-bit LFSR with feedback taps
bit0 XOR bit2. The real AY-3-8910 — confirmed by die decap analysis
(MAME ay8910.cpp, followed by the unreal-ng emulator) — uses feedback
bit0 XOR bit3:
"The Random Number Generator of the 8910 is a 17-bit shift register.
The input to the shift register is bit0 XOR bit3. Bit0 is the output."
Why it matters
The tap choice is not cosmetic — it changes the algebra of the sequence:
| Feedback taps |
Polynomial |
Cycle structure (of 2^17−1 = 131071 states) |
| bit0 ^ bit3 (real chip) |
x^17 + x^3 + 1 (primitive) |
one maximal cycle of 131071 |
| bit0 ^ bit2 (old RTL) |
x^17 + x^2 + 1 (non-primitive) |
114681 + 16383 + 7 |
Verified by brute-force cycle enumeration of both polynomials.
Consequences of the non-primitive 0^2 polynomial:
- Wrong noise sequence — the pseudo-random pattern never matches real
hardware or reference emulators, so recordings can never null against a
software render.
- Shorter period — the register starts in a 114681-state cycle
(from the zero-lockup escape), ~12.5% shorter than the genuine sequence,
with different spectral fine structure.
- 7-state trap — the state space contains a 7-state cycle. Any entry
into it (e.g. via an unlucky power-up state on real silicon synthesis)
turns "noise" into a strongly tonal buzz at 1/7 of the noise clock.
The fix
One line in rtl/ym2149.sv, noise generator process (p_noise_gen):
if(CE) begin
if (ena_div_noise) begin
if (!ymreg[6][4:0] || (noise_gen_cnt >= ymreg[6][4:0] - 1'd1)) begin
noise_gen_cnt <= 0;
- poly17 <= {(poly17[0] ^ poly17[2] ^ !poly17), poly17[16:1]};
+ // 17-bit LFSR, feedback bit0 XOR bit3 (die-verified AY-3-8910,
+ // x^17+x^3+1, maximal 131071-state sequence). The previous
+ // bit0^bit2 polynomial is non-primitive (cycles of 114681/
+ // 16383/7 states). !poly17 escapes the all-zero lockup state.
+ poly17 <= {(poly17[0] ^ poly17[3] ^ !poly17), poly17[16:1]};
end else begin
noise_gen_cnt <= noise_gen_cnt + 1'd1;
end
noise_gen_op <= {3{poly17[0]}};
end
end
The !poly17 term is kept: it injects a 1 when the register is all-zero
(the XOR-LFSR lockup state), which both seeds the register out of reset and
guards the pathological state. Once non-zero, it never fires again, so the
generated sequence is exactly the real chip's maximal-length sequence.
Everything else in the noise path was already correct and is unchanged:
the 17-bit width, bit0 as output, the half-rate update (ena_div_noise =
prescaler/2, equivalent to the software's period << 1 counter), and the
period-0 → period-1 handling.
Verification
sim/audio_tb/test_ym2149_cosim.cpp — Verilator co-simulation of
ym2149.sv against an exact port of the unreal-ng generators, comparing the
5-bit channel level streams tick-by-tick at 218.75 kHz. Noise-only
(periods 0/1/5/15/31), tone+noise, noise+envelope, and
tone+noise+envelope configurations: bit-exact after the fix (with the
golden model using the same taps and the RTL's zero-escape seeding).
Cycle-structure proof (Python):
def cycle_len(taps, seed=1):
s, n = seed, 0
while True:
fb = 0
for t in taps:
fb ^= (s >> t) & 1
s = (fb << 16) | (s >> 1)
n += 1
if s == seed:
return n
cycle_len([0, 3]) # 131071 (maximal - real chip)
cycle_len([0, 2]) # 114681 (+ separate cycles of 16383 and 7)
Scope
This LFSR feeds both audio paths (legacy mixer output and the HQ
pipeline), so the fix changes the noise character of the core everywhere —
intentionally, to match real hardware and reference emulators.
AY-3-8910 / YM2149 Noise LFSR Polynomial Bug
Summary
The noise generator in
rtl/ym2149.svused a 17-bit LFSR with feedback tapsbit0 XOR bit2. The real AY-3-8910 — confirmed by die decap analysis
(MAME
ay8910.cpp, followed by the unreal-ng emulator) — uses feedbackbit0 XOR bit3:
Why it matters
The tap choice is not cosmetic — it changes the algebra of the sequence:
Verified by brute-force cycle enumeration of both polynomials.
Consequences of the non-primitive 0^2 polynomial:
hardware or reference emulators, so recordings can never null against a
software render.
(from the zero-lockup escape), ~12.5% shorter than the genuine sequence,
with different spectral fine structure.
into it (e.g. via an unlucky power-up state on real silicon synthesis)
turns "noise" into a strongly tonal buzz at 1/7 of the noise clock.
The fix
One line in
rtl/ym2149.sv, noise generator process (p_noise_gen):The
!poly17term is kept: it injects a 1 when the register is all-zero(the XOR-LFSR lockup state), which both seeds the register out of reset and
guards the pathological state. Once non-zero, it never fires again, so the
generated sequence is exactly the real chip's maximal-length sequence.
Everything else in the noise path was already correct and is unchanged:
the 17-bit width, bit0 as output, the half-rate update (
ena_div_noise=prescaler/2, equivalent to the software's
period << 1counter), and theperiod-0 → period-1 handling.
Verification
sim/audio_tb/test_ym2149_cosim.cpp— Verilator co-simulation ofym2149.svagainst an exact port of the unreal-ng generators, comparing the5-bit channel level streams tick-by-tick at 218.75 kHz. Noise-only
(periods 0/1/5/15/31), tone+noise, noise+envelope, and
tone+noise+envelope configurations: bit-exact after the fix (with the
golden model using the same taps and the RTL's zero-escape seeding).
Cycle-structure proof (Python):
Scope
This LFSR feeds both audio paths (legacy mixer output and the HQ
pipeline), so the fix changes the noise character of the core everywhere —
intentionally, to match real hardware and reference emulators.