r/chipdesign • u/UpbeatBarnacle9064 • 10h ago
Gate-level sim hung on every multiply in my RISC-V chip. The cause was X-pessimism in PicoRV32’s multiplier counter
Some context: Argo is my PicoRV32 + 8x8 systolic array SoC on IHP SG13CMOS5L (posted here a few weeks ago https://www.reddit.com/r/chipdesign/comments/1wzfdef/i_designed_a_riscv_chip_with_a_matrix_accelerator/ ). Since then it’s been signed off for tapeout at IHP. The RTL regression was clean. On the routed netlist, a hello-world passed, but the array test and the power-on self-test never printed anything. They just hit the timeout.
Narrowing it down
I wrote a tiny test that prints a token after each bus access, all on one UART line, so a timeout shows exactly how far it got:
FAIL: timeout, partial line "S0 DW DR+ IW IR+ AW AR "
DSRAM, ISRAM and the array registers all worked. The next step was a mul, and hello-world turned out to be the only program with no multiply in it.
Probing the multiplier (image 1)
A probe on PicoRV32’s PCPI multiplier showed the handshake starting fine, then mul_counter going bad one bit per cycle: 001111x, 00111xx, ... xxxxxxx. Bit 0 was X from the start, and each decrement dragged it up a bit until it reached bit 6, the “done” bit. Then mul_waiting went X, the X spread back into the CPU’s wait signal, and the core sat there forever.
Why bit 0 never cleaned up (image 2)
mul_counter has no reset in the RTL, which is fine because it reloads on every idle cycle. The flop in the netlist is a reset-capable cell, but its RESET_B is tied off. This is what reloads bit 0:
sg13cmos5l_nor2_1 _099351_ (.A(mul_counter[0]), .B(net1070), .Y(_047204_));
sg13cmos5l_a21oi_1 _099352_ (.A1(mul_counter[0]), .A2(net2985), .B1(_047204_), .Y(_005099_));
net2985 is a buffered constant 1, and net1070 is low while the multiplier is idle. So:
D = NOT( (Q AND 1) OR NOT(Q OR 0) )
= NOT( Q OR NOT Q )
In silicon, Q OR NOT Q is always 1, so D is always 0. The flop powers up as 0 or 1, and the first idle cycle loads 0. In a 4-state simulator, X OR NOT X is X, so the flop feeds its own X back forever. It’s plain X-pessimism from how synthesis restructured the logic. My v1 netlist happened to come out of ABC with a different structure, which is why I never saw it before.
Proving it (image 3)
I forced that one D net to 0 for the whole SPI boot, while the CPU is still held in reset, then released it. The counter loaded 0011110, counted down cleanly and the multiply returned the right answer. My first attempt only forced it for 200 ns, and the flop picked up X again as soon as I released it, because the other gate input was still X that early in the boot.
The fix (image 4)
The fix is in the testbench, not the chip. A small script finds every CPU flop in the netlist and holds its D input at 0 during the SPI boot, then releases it before RUN, which models one legal power-up state. With that, the power-on self-test passes on the routed netlist in both zero-delay and typical SDF simulation.
What I took from it:
- A hello-world passing at gate level tells you very little. Make sure your GLS programs actually use every functional unit.
- A sim-only X and a real missing reset look identical in a waveform. I didn’t trust any other GLS result until I’d proved which one this was.
- The cheapest real fix is adding resets to those few control flops in the RTL. That’s on the list for v3.
How do you handle non-reset flops in GLS? Testbench init like this, random init across seeds, a simulator option, or resets everywhere?