Procedural Block Enhancements
Combinational vs. Sequential always Blocks laid out the rule synthesis tools use to turn a plain always block into either combinational logic, a latch, or a flip-flop — and how easy that rule is to violate by accident: forget a sensitivity-list signal, mix blocking and nonblocking in the same block, or miss a case branch, and the inferred hardware silently diverges from the intended hardware. SystemVerilog's always_comb, always_ff, and always_latch don't add new capability — they add a compiler that enforces the intent you're declaring, catching exactly the mistakes that page had to explain as things to watch for by hand.
always_comb: declares "this is combinational," and the tool checks it
always_comb begin
sum = a ^ b ^ cin;
cout = (a & b) | (b & cin) | (a & cin);
end
always_comb needs no sensitivity list at all — the tool automatically infers it from every signal read inside the block, so there's no way to forget one (the single most common plain-Verilog mistake this replaces: an always @(a, b) that forgot cin, silently producing a block that doesn't update when cin changes). It also statically checks that every assigned signal is covered on every path — the missing-default/else latch trap from Combinational vs. Sequential always becomes a compile-time error instead of a silent inferred latch:
always_comb begin
if (enable)
y = a;
// no else — always_comb flags this as an error, not a silent latch
end
always_ff: declares "this is a flip-flop"
always_ff @(posedge clk or posedge rst) begin
if (rst) state <= S0;
else state <= next_state;
end
always_ff still needs its clock-edge sensitivity list (unlike always_comb) — the tool's job here isn't to infer that, but to enforce that only nonblocking (<=) assignment is used inside, and that the block only responds to edges, not levels. Using a blocking (=) assignment inside always_ff, or a level-sensitive condition, is flagged rather than silently accepted the way plain always would accept it. Two more restrictions follow the same "enforce, don't just permit" pattern: always_ff allows exactly one event control, and only at the very start of the block (@(posedge clk or posedge rst) — never a second @(...) partway through the body), and it disallows any blocking delay control (#delay) inside the block entirely, since a real flip-flop's output can't legally lag its clock edge by an arbitrary simulation delay.
always_latch: declares "this is intentionally a latch"
always_latch begin
if (enable)
y = a;
// else intentionally omitted — this really is meant to be a latch
end
The rare case where a latch genuinely is the intended hardware (not a mistake) gets its own explicit keyword — always_latch accepts exactly the code pattern always_comb would reject, but only when the reader (and the tool) can see from the keyword itself that it's deliberate. Reaching for always_latch on purpose, instead of accidentally tripping the same shape inside a plain always, is exactly the difference between a documented design decision and a bug that happens to simulate. always_latch shares always_comb's single-writer-per-variable enforcement too — y above still cannot legally be assigned by any other procedural block anywhere else in the design, even though the whole point of this block is to hold y's previous value when enable is low rather than update it every cycle.
always_comb vs. plain always @(*): three differences beyond sensitivity
always_comb isn't just always @(*) under a new name — even though both auto-derive their sensitivity list, three real behavioral differences separate them:
- Runs once automatically at time 0.
always_combis guaranteed to execute once at the very start of simulation (after allinitial/alwaysblocks have started), so its outputs are already consistent with its inputs before anything else can read them. Plainalways @(*)has no such guarantee — it only runs the first time one of its sensitivity-list signals actually changes, which can leave its outputs at their uninitialized default (x) if nothing changes them in that first delta cycle. - Sensitive to a called function's body, not just its arguments. If the block calls a function,
always_combre-triggers whenever anything the function reads internally changes — not only when the arguments passed to it change.always @(*)'s inferred sensitivity list only covers the function's arguments, so a change to something the function reads internally (without it being passed in as an argument) can silently fail to re-trigger analways @(*)block, whilealways_combcatches it correctly. - Enforces a single writer per variable. A variable assigned inside an
always_combblock cannot legally be assigned by any other process anywhere else in the design — the tool flags that as an error. Plainalways @(*)has no such restriction; two differentalways @(*)blocks (or analways @(*)and aninitialblock) can both drive the same variable, the exact multiple-driver ambiguityalways_combexists to rule out at compile time.
Why this triplet exists: the summary rule
| Old plain-Verilog risk | New keyword | What it enforces |
|---|---|---|
| Incomplete/wrong sensitivity list | always_comb | Sensitivity list auto-derived — impossible to forget a signal |
| Missing branch → accidental latch | always_comb | Compile error if any assigned signal isn't covered on every path |
| Blocking assignment inside a clocked block | always_ff | Only nonblocking assignment accepted |
| Level-sensitive logic mistaken for a flip-flop | always_ff | Only edge-sensitive (posedge/negedge) triggering accepted |
| An intentional latch looking identical to a bug | always_latch | Same latch-inferring shape, but self-documenting and distinct from always_comb's error case |
None of this changes what hardware gets built — a correct plain-Verilog always @(*) combinational block and its always_comb equivalent synthesize to the identical gates. The value is entirely in moving Combinational vs. Sequential always's manual rules into the compiler, so violations are caught immediately rather than during a waveform-debugging session later. Every RTL example from here on in this curriculum uses always_comb/always_ff in place of plain always, exactly as real modern SystemVerilog RTL does.
What's next
The next page covers SystemVerilog's smaller but still useful control-flow additions — foreach, do-while, break/continue — rounding out the procedural vocabulary before moving on to interface and package.