Three weeks before tape-out, the gate-level regression goes red. The RTL regression has been green for a month, and equivalence checking says the netlist matches the RTL. The first failure is not even a mismatch: the bench does not compile, because a backdoor probe points at a register that synthesis merged away. You fix that, and the next run has xxxxxxxx on the read-data bus for the whole simulation. A manager asks the obvious question: if equivalence checking passed, what is this simulation for, and how long will it take to make it green?
Gate-level simulation (GLS) runs your testbench against the netlist: the design after synthesis (and later after DFT insertion, place-and-route and ECOs), built from library cells instead of RTL statements. Logical equivalence checking (LEC) proves mathematically that the netlist computes the same next state as the RTL for every input, and static timing analysis (STA) proves every path meets timing. Between them they cover most of what GLS used to be for, which is why modern projects run far fewer gate-level tests than they did 20 years ago.
They do not cover everything. Think of LEC as checking a translated book against the original, sentence by sentence, assuming both start on page one. GLS is what opens the book in the middle, with some pages still blank:
Initialisation and X. LEC compares two designs from matching states; it does not prove that reset brings every flop out of X. A flop without reset, or a reset tree that misses a branch, is a GLS failure first.
Simulation-synthesis mismatch. full_case/parallel_case pragmas, incomplete sensitivity lists, initial blocks and code under translate_off can make RTL simulation behave differently from the logic synthesis built. LEC compares against the synthesis view of the RTL, so the gap can survive it.
Timing with real delays. An SDF-annotated simulation at slow and fast corners checks that asynchronous paths, reset release, clock-gating cells and paths that STA treated as exceptions (false paths, multicycle paths) behave as the exceptions claimed. A wrong exception is a silicon bug that STA will never flag.
DFT and ECO netlists. Scan insertion adds test-mode muxes and ECOs edit the netlist by hand. A short simulation of functional mode on the final netlist checks that test logic is really inactive in mission mode and that the ECO did what its ticket said.
The netlist also changes in steps through the project, and each step gets a different flavour of GLS:
You can make a real netlist of the course DMA with Yosys, the open-source synthesis tool. A generic synth maps the design to Yosys's own internal cell library instead of a foundry library, which is enough to see what changes for the bench:
yosys -q -p "read_verilog -sv dma_dut.sv; synth -top dma_dut -flatten; opt_clean -purge; \
stat; write_verilog -noattr -noexpr dma_dut_gates.v"=== dma_dut === (stat excerpt)
384 cells
23 $_ANDNOT_
99 $_AND_
99 $_DFFE_PN0P_
4 $_DFF_PN0_
77 $_NAND_
48 $_XOR_
...Every flop became a cell instance ($_DFFE_PN0P_ is a flop with enable and an active-low asynchronous reset to 0). Look at two of them in the netlist, because they show the first thing that breaks a bench:
// snippet: from dma_dut_gates.v (Yosys output, ports joined on one line)
\$_DFF_PN0_ pready_reg /* _514_ */ (
.C(clk), .D(_004_), .Q(pready), .R(rst_n)
);
\$_DFFE_PN0P_ \len_reg[0] /* _516_ */ (
.C(clk), .D(pwdata[0]), .E(_009_), .Q(len[0]), .R(rst_n)
);The RTL had a flop called waited (the APB wait-state flag) and drove pready from it through a continuous assignment. Synthesis merged them: there is now a pready_reg and no waited at all. Any testbench reference to dut.waited (a probe, a force, an SVA bind port) stops compiling. Here is exactly that, on Icarus:
probe.sv:6: error: Unable to bind wire/reg/memory `dut.waited' in `probe_tb'
1 error(s) during elaboration.This netlist happens to keep key, len and count as net names, so the course's register backdoor (dma_bd_if, bound into dma_dut with ports on those three nets) and the fault campaign's force dut.key[0] would still find them. A commercial flow is less kind: registers are commonly renamed after their instance, hierarchy may be flattened or uniquified, and optimised-away nets do not exist. What survives depends on the tool's naming rules and your scripts, so check the real netlist.
Bench part | RTL run | Gate-level run |
|---|---|---|
Internal probes, | RTL signal names | Netlist names, or disabled. Keep a path map per netlist, or limit GLS tests to front-door access. |
SVA | Binds to RTL modules and signals | Bind only to ports of preserved hierarchy (or to the top's ports); internal binds go behind |
Monitors | May sample internal state (FSM, counters) | Must sample ports only. A monitor that peeks at |
Driver timing | Drive at the clock edge with NBA | Drive with an output skew after the edge. With SDF, flop clocks arrive after the clock-tree delay, so an input that changes exactly at the testbench edge looks like a hold violation. |
Delays | Zero delay | Zero or unit delay for early netlists; SDF min/max per corner for the routed netlist. |
Memories and macros | RTL memory models | Library or memory-compiler simulation models, with their own timing checks and X behaviour. |
Test list | Thousands of seeds | A reduced list: reset and boot, one test per mode, register access, every clock and reset domain crossing, low-power entry and exit. |
Make GLS a configuration, not a fork: one define removes internal binds and probes and adds the drive skew; sequences, scoreboard and coverage stay unchanged.
// snippet: interface timing for GLS; the RTL build uses a zero skew
interface axis_if (input logic clk);
logic [31:0] tdata;
logic tvalid, tlast, tready;
`ifdef GLS
localparam time DRIVE_SKEW = 1ns; // after the edge: clear of the flops' hold window
`else
localparam time DRIVE_SKEW = 0;
`endif
clocking drv_cb @(posedge clk);
default input #1step output #(DRIVE_SKEW);
output tdata, tvalid, tlast;
input tready;
endclocking
endinterfaceBefore adding timing, run the bench unchanged against the netlist. This scratch run compiled dma_dut_gates.v and Yosys's cell models in place of dma_dut.sv, with the stock dma_tb.sv, on Verilator:
+UVM_TESTNAME=dma_smoke_test (scratch run: gate netlist, Verilator)
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0
+UVM_TESTNAME=dma_rand_test
UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
UVM_ERROR : 0
+UVM_TESTNAME=dma_truncate_test
UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_ERROR : 0Identical results to RTL, and that is the point to notice: Verilator is two-state and zero-delay, so this run proves no more than LEC already did. GLS earns its cost only with four-state simulation (X) and, later, timing. Run the netlist on a four-state simulator (Icarus) with a small directed bench, and one innocent testbench habit changes the picture: the APB driver parks the bus with X on paddr and pwdata between transfers, which is legal (the protocol ignores them while PSEL is low).
// snippet: the directed bench's APB write task (tb_gls.v); PARK_X parks the bus at X
psel <= 0; penable <= 0;
`ifdef PARK_X
paddr <= 8'hxx; pwrite <= 1'bx; pwdata <= 32'hxxxxxxxx;
`endif== RTL, PARK_X (Icarus 13.0)
175000 out a5a51f0f last=0
195000 out a5a51f0e last=0
215000 out a5a51f0d last=1
255000 irq=1
== gates, PARK_X
75000 X on an output: prdata=0000000X pready=0 pslverr=0 s_tready=0 m_tvalid=0 irq=0 m_tlast=0
115000 X on an output: prdata=XXXX0x0x pready=0 pslverr=0 s_tready=0 m_tvalid=0 irq=0 m_tlast=0
...
175000 out a5a51f0f last=0
175000 X on an output: prdata=XXXX0x0x pready=0 pslverr=0 s_tready=1 m_tvalid=1 irq=0 m_tlast=0The data is still right in both (the same three words and the interrupt), but in the netlist prdata turns unknown whenever paddr is unknown. In the RTL, case (paddr) with an X selector matches no item and takes default: prdata = 0, so RTL showed a clean zero. In the netlist the address decoder is AND/OR gates, and X on an input propagates to the output. Neither is a DUT bug: PRDATA only matters on the cycle the access completes. But a bench check that says "no X on any output, ever" passed for months on RTL and fires on every cycle in GLS. The fix belongs in the check, not the waveform: qualify each no-X check with its valid signal (prdata only when PSEL, PENABLE and PREADY are all 1).
The classic GLS failure is a flop left without reset to save area: "software always loads it before use". Here is a minimal version. mode_q has no reset and selects between two inputs for a flop that does reset:
// not-run: needs a four-state simulator (run with Icarus)
// A config flop with no reset, used as a mux select by a flop that is reset.
module mode_mux (
input logic clk, rst_n,
input logic load, mode_in,
input logic [7:0] a, b,
output logic [7:0] y
);
logic mode_q; // no reset: "software always loads it first"
always_ff @(posedge clk)
if (load) mode_q <= mode_in;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) y <= 8'h00;
else if (mode_q) y <= a; // RTL: if (X) takes the else branch
else y <= b;
endmoduleSynthesised with Yosys (8 flops with reset, 1 enable flop without, 8 muxes) and simulated on Icarus with the same bench, which releases reset and loads the mode three clocks later (a=AA, b=55):
== RTL == gates
20 ns y=00 20 ns y=00
30 ns y=55 30 ns y=xx <-- X on the output
40 ns y=55 40 ns y=xx <-- X on the output
50 ns y=55 50 ns y=xx <-- X on the output
60 ns y=55 60 ns y=xx <-- X on the output
70 ns y=55 70 ns y=55The RTL never shows a problem: if (mode_q) with mode_q = X is treated as false, so y takes b. The netlist mux has an unknown select and different inputs, so y is X until software loads the mode. If anything samples y in those cycles (a status bit, an interrupt, a register read at boot), the chip can behave differently on each power-up. The debug recipe for any GLS X is the same: find the first time an X appears on a port, trace it back in the waveform through the fan-in cone to a flop with X on Q, and ask why that flop was never initialised. Then decide with the designer: add the reset, prove the load always happens first (and constrain the bench to match), or, as a documented last resort, initialise the flop in simulation only and record the waiver.
In an SDF simulation, each library flop model carries timing checks in its specify block, such as $setuphold(posedge CK, D, tsu, th, notifier) and $width(posedge CK, tmin). SDF annotation replaces the placeholder limits with the values for the chosen corner. When data changes inside the setup/hold window, the simulator prints a violation with the instance and the time, and toggles the notifier register, which the flop's model uses to drive Q to X. One violation therefore becomes a spreading X, which is why the first violation in the log matters far more than the thousandth.
Icarus parses specify blocks but does not run timing checks, so this course cannot show a live violation. On a project the flow is:
The implementation team delivers the netlist, cell and memory models, and one SDF file per corner. The testbench annotates with the standard task $sdf_annotate("dma_max.sdf", top.dut, , "sdf.log", "MAXIMUM"), run once at time 0. The annotation log must show zero unannotated or mismatched cells.
You run the reduced test list at least twice: maximum delays at the slow corner (setup-type failures) and minimum delays at the fast corner (hold-type failures). Projects add corners for specific concerns.
First stage flops of synchronisers will violate timing in simulation, because their inputs are asynchronous by design. Every commercial simulator can disable timing checks, or the notifier's X, for a listed set of instances. That list is reviewed: each entry must be a real synchroniser flop that CDC sign-off also recognises.
A violation on a path STA declared false or multicycle, in a mode the exception says cannot happen, means the exception is wrong.
Every test on the agreed GLS list passes on the final netlist, zero-delay and SDF at each required corner, with the same scoreboard and checks as RTL.
No X on any qualified output after reset completes; every initialisation workaround is listed and approved.
No timing violations except instances on the reviewed exclusion list; SDF annotation complete.
Each ECO netlist reruns the subset the ECO could affect, and the results are attached to the ECO ticket.
prdata full of X between APB transfers, but every register read in the scoreboard matches. Is this a bug, and what do you change?Most likely not a DUT bug: the address decoder propagates the X the bench parks on paddr, and PRDATA is only meaningful on the completing cycle. RTL hid it because case with an X selector takes the default. Change the no-X check to sample prdata only when PSEL, PENABLE and PREADY are all 1. Parking the bus at a known value would also silence it, but then the bench stops exercising the "ignored while idle" rule.
The testbench, not the design. The driver changes inputs exactly at the testbench clock edge, while the flops' clocks arrive later by the clock-tree insertion delay, so the input change lands inside their hold window. STA assumes inputs arrive per the input-delay constraints, not at time zero. Drive with an output skew after the edge (a clocking block output # value that matches the input-delay constraint) and rerun.
initial mode_q = 0; in the RTL so GLS stops showing X. What is wrong with that?An initial block is ignored by synthesis (or is only honoured on FPGAs), so the netlist flop still powers up unknown and silicon still has the problem. It only hides the X in simulation. Either add a real reset, or prove the flop is always loaded before anything uses it and document the waiver.
Next: most GLS X failures start long before GLS. Reset and X-Propagation Verification shows how to find them in RTL simulation, weeks earlier and at a fraction of the cost.