Skip to content

AY-3-8910/YM2149 noise LFSR uses wrong feedback taps (bit0^bit2 instead of die-verified bit0^bit3) #62

Description

@alfishe

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:

  1. Wrong noise sequence — the pseudo-random pattern never matches real
    hardware or reference emulators, so recordings can never null against a
    software render.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions