Each process type states what the block implements. The tool then checks that the code keeps the promise.
Block | Promise you make | What the tool checks |
|---|---|---|
| This is edge-triggered storage. | Only edge events in the list; assignments should be non-blocking; one block per variable. |
| This is combinational logic. | Sensitivity is automatic; the variable gets a value on every path; no latch; no second writer. |
| This is a level-sensitive latch (on purpose). | A latch really is intended and is inferred. |
module seq_comb_demo (
input logic clk, rst_n,
input logic [1:0] sel,
input logic [7:0] a, b, c,
output logic [7:0] y,
output logic [7:0] y_q
);
// Pure logic: every path assigns y, so no latch is possible
always_comb begin
y = '0; // default
unique case (sel)
2'd0: y = a;
2'd1: y = b;
2'd2: y = c;
default: y = '0;
endcase
end
// Registered copy of the result
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) y_q <= '0;
else y_q <= y;
endmodule// bad-example: y is not assigned when en is 0 -> a latch is inferred
module latch_bug (input logic en, a, output logic y);
always_comb begin
if (en) y = a; // no else branch
end
endmoduleMany tools can warn that y has no value on every path, which implies a latch. Enable the checks and review each report. With a plain always @(*) the language accepts the latch, and lint and synthesis only warn, because nothing said the block was meant to be combinational. With always_comb the same latch contradicts declared intent, so tools can treat it as an error.
The fix is a default assignment or an else branch:
module latch_fixed (input logic en, a, output logic y);
always_comb begin
y = 1'b0; // default makes every path assign y
if (en) y = a;
end
endmodulealways_latch the right choice?Only when you actually want a transparent latch, for example in a hand-designed clock-gating cell or time-borrowing structure. In ordinary synchronous RTL a latch is almost always a bug, so always_latch should be rare and commented.
These modifiers state how case branches relate. They replace the old full_case parallel_case pragmas. Synthesis tools honoured those pragmas but simulators ignored them, so simulation and hardware could disagree, and a wrong pragma went unnoticed.
unique case promises exactly one matching branch. Simulation can warn on overlap or no match, so enable the checks and review each report. A default branch suppresses the no-match report, while a pre-case default assignment still leaves it reported. Both can give a deterministic value.
priority case and priority if describe ordered branches, of which at least one matches. Synthesis can use that assumption to simplify the logic, so keep the promise exact.