Verilog bug catalog
Output Never Assigned: A Common RTL Bug and How to Catch It
You declare an output port on a module, the synthesis tool happily builds the design, simulation runs, and the consumer of that signal sees x or z at runtime. Or worse, sees a stable 0 and looks correct in some test cases. The output was never driven. This page covers the typical causes, the patterns that mask the bug, and how to catch it across a whole repo before it bites you.
What it looks like
The textbook case is a module that declares an output and then forgets to write a continuous or procedural assignment for it. The synthesizer in many tool flows will leave the net floating, which in a verilog simulator yields z on a wire and x on a logic variable. Downstream combinational logic propagates the x; downstream flip-flops latch a random value once they see a real edge.
// BUG: 'ready' is declared but never driven anywhere in the module.
module fifo_iface (
input logic clk,
input logic rst_n,
input logic valid_in,
output logic [7:0] data_out,
output logic ready // <-- never assigned
);
always_ff @(posedge clk) begin
if (!rst_n) data_out <= '0;
else if (valid_in) data_out <= data_out + 1;
end
endmoduleThe fix is trivial once you see it: drive the port. The interesting question is why this kind of defect ships in the first place.
// FIX: drive the port. If it should be a constant, drive a constant.
module fifo_iface (
input logic clk,
input logic rst_n,
input logic valid_in,
output logic [7:0] data_out,
output logic ready
);
assign ready = 1'b1; // always ready in this stub
always_ff @(posedge clk) begin
if (!rst_n) data_out <= '0;
else if (valid_in) data_out <= data_out + 1;
end
endmoduleWhy it happens
- Incomplete refactor. An engineer renames an internal signal but misses the assign that wired it to a port. Verilog-2001 and earlier silently auto-create wires for unknown identifiers, so the file still compiles.
- Conditional branches that miss cases. The output is driven inside an if/else chain that does not cover every scenario. On some control paths the assignment is skipped and the port is left at its default.
- Stub modules that grew up. Early in a project the module is a placeholder with empty ports. As real logic gets added, two or three of the original outputs never become real.
- Macro-driven generation. A generate or `ifdef guard removes the assignment in some configurations, and nobody notices until that configuration is built.
The renamed-signal trap
The most insidious version of the bug is the rename. You change count_q to count_r inside the always block but forget the assign at the bottom of the file. With `default_nettype none the parser will reject the file. Without it, you get an implicit 1-bit wire named count_q, which is silently zero-extended into the 8-bit output port and reads as 8'h00.
// BUG: 'count_q' was renamed to 'count_r' inside the always block,
// but the assign that drives the output port still names the old wire.
module counter (
input logic clk,
input logic rst_n,
output logic [7:0] count_o
);
logic [7:0] count_r;
always_ff @(posedge clk) begin
if (!rst_n) count_r <= '0;
else count_r <= count_r + 1;
end
// count_q does not exist anymore; this fails to drive count_o.
// Some tools accept the implicit-wire and silently leave count_o at 'z.
assign count_o = count_q;
endmoduleTwo preventative measures: put `default_nettype none at the top of every file, and run a connectivity pass that diffs the set of declared output ports against the set of signals that actually have an assignment.
Conditional-branch variant
A different shape of the same bug: the output is driven, but only on some paths. This often produces an inferred latch (see inferred latches), but in registered code the signal simply holds whatever value it got at reset and stays there forever.
// BUG: 'busy' is only ever cleared, never set. After reset it stays at 0.
always_ff @(posedge clk) begin
if (!rst_n) busy <= 1'b0;
else if (job_done) busy <= 1'b0;
// missing: 'else if (job_start) busy <= 1'b1;'
endStatic analysis catches this because the set of values written to busy is a one-element set ({1'b0}), which is suspicious for a non-constant signal.
The generate-guard variant
A third shape: the output is driven inside a generate if or inside an `ifdef guard. Some configurations build the assignment, others do not. Tools usually catch the active configuration but say nothing about configurations they did not elaborate.
// BUG: when DEBUG is undefined, dbg_state is left undriven.
output logic [3:0] dbg_state;
`ifdef DEBUG
assign dbg_state = state_q;
`endifThe fix is to provide an `else branch with a tied-off value, or to remove the port in the configuration where it is not used. Better yet, make the port optional via parameter so the parent does not have to know about the macro at all.
Why simulation often misses it
A 4-state simulator will show x on the undriven port. That is the good case. The bad cases:
- The testbench never reads the port directly. The bug only matters when the integrating module exists, and at that point the integration test is a different file with a different schedule.
- The downstream consumer is a flop with an asynchronous reset that initialises the state to a known value, and the test never lets the reset deassert long enough to expose the x.
- The implicit-wire trap above produces a clean 0, which behaves correctly for any test that does not depend on the count being non-zero.
- Verilator with --x-assign 0 silently zeros undriven nets, hiding the issue entirely.
How to detect it at scale
There are three layers of defense:
- Lint. Verilator's UNDRIVEN warning and Verible's undriven-output rule both fire on the basic case. Both miss the renamed-wire trap unless `default_nettype none is set.
- Synthesis elaboration. Yosys reports undriven wires as "Wire ... has no driver" during the check pass. Useful but slow to run on a per-PR basis.
- Static AST analysis. Walk the AST, build the set of all declared output ports, build the set of all left-hand sides of any assignment (continuous, procedural, port connection inside generate), and diff. This is the fastest pass and catches the cases lint misses.
Tools like ChipVerify AI's TinyTapeout scanner flag this rule as output_never_assigned automatically. Each finding includes the module, the port, the declaration line, and a confidence note distinguishing truly-undriven outputs from outputs that depend on a `ifdef or unbound parameter.
Pre-commit checklist
- Every file starts with `default_nettype none.
- Every output port appears as the LHS of at least one assignment (continuous, procedural, or instance port).
- Every output port has its width matched on every assignment.
- Macro-guarded assignments either tie off the alternative or remove the port entirely in the alternative.
- CI runs a connectivity check on every PR, not only at synthesis time.
Related rules
- Inferred latches — the case where the output is sometimes driven, sometimes not.
- Width mismatches — the implicit 1-bit wire created by a typo silently truncates into a wider port.
- Blocking vs non-blocking — can mask undriven-output bugs in mixed-style code.
What integrators see
The downstream effect of an undriven output depends on how the parent module wires the signal. A few common outcomes:
- If the parent treats the signal as combinational, the x propagates through every gate it touches and you eventually see corruption on the destination flop.
- If the parent registers the signal first, the x is captured on the first clock edge and held. Subsequent stages see a stable but arbitrary value.
- If the parent connects the port to a constant or to a different child's output, Verilog's multiple-driver rules kick in and the simulator may resolve to x on every conflicting bit.
The point: a missing assignment in one module produces unpredictable behaviour in modules that did nothing wrong. Catch it at the source.
FAQ
Should every output have a default in the declaration?
SystemVerilog allows an initial value on logic declarations, but this only sets the simulation initial value, not the synthesised reset value. Do not rely on it to mask undriven-output bugs.
What about tied-off debug ports?
Tie them off explicitly. A bare port that is intentionally unused should still have assign port_name = '0; in the module body, with a comment. That is the difference between "I considered it" and "I forgot it".
Does this apply to inout ports?
Same rule, plus a tristate driver. An inout declared without any assign behaves identically to an undriven output.
Scan your repo
Scan your repo for undriven outputs: paste a GitHub URL at chipverify.ai/tinytapeout and within seconds you'll see every output port that has no driver, plus the renamed-wire variant that lint typically misses. The scan runs without a synthesis license and without you setting up a filelist.
Catch undriven outputs before tapeout
Sign in and point ChipVerify AI at your RTL. Its analyzer names every output port with no driver, plus the renamed-wire variant lint usually misses — a pre-signoff structural check that returns file-and-line evidence, not a foundry signoff.