The most reliable fix is to assign every output before any conditional logic. Later statements only override the default for specific cases.
No path can leave the output unassigned, because the default has already assigned it.
// yosys: ok
module fixed_if (input logic en, a, output logic y);
always_comb begin
y = 1'b0; // default
if (en) y = a; // override
end
endmodule// yosys: ok
module fixed_case (input logic [1:0] sel, input logic a, b, c, output logic y);
always_comb begin
y = 1'b0; // default covers sel == 3 and anything unexpected
case (sel)
2'd0: y = a;
2'd1: y = b;
2'd2: y = c;
default: ;
endcase
end
endmodule// yosys: ok
module fixed_partial (input logic sel, a, b, output logic x, y);
always_comb begin
x = a;
y = 1'b0; // every output has a default
if (sel) y = b;
end
endmoduleYosys accepts all three without a latch; the broken versions in the previous lesson are rejected. That is the same check your synthesis tool performs on your design.
Use blocking = in always_comb and reserve nonblocking <= for always_ff. Do not mix them.
The defaults-then-override pattern needs order: each statement must read the values written above it. Blocking = gives that order, and mixing the two is how simulation and synthesis start to disagree.
<= instead of =?The default y = 0 is blocking, so it takes effect at once, but the override y <= a is scheduled for later. A read such as z = y; after the if in the same activation sees the default 0, not a, and y takes a only afterwards. That stale read is why always_comb uses = throughout and always_ff uses <=. An all-nonblocking block is a separate case, where every read of the block's own outputs is stale; see the scheduling lesson.
if branches. What is the cheapest change that guarantees no latch?Assign all 8 outputs a default value at the very top of the always_comb, then let the branches override. You do not need to touch the branches at all.
case lists every select value you planned for, but has no default. An unplanned input combination arrives. What happens?The unplanned value has no branch, so the output holds and a latch returns. A default assignment before the case would have covered it.
Pattern | Example | Use when |
|---|---|---|
Defaults first |
| Always the safest choice |
Full |
| A single decision with two outcomes |
|
| Multi-way selects |
Temporary variable | Compute | Complex logic with many outputs |
Choose the default so it is a safe, benign value (0, an idle code, 'no request'), not x. In x defaults, simulation and silicon can disagree.
For next-state logic, next = state; is the natural default: stay where you are unless told otherwise.
A default: branch that is truly unreachable should still assign a safe value, and an assertion can prove it is never taken.