Skip to main content

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_comb is guaranteed to execute once at the very start of simulation (after all initial/always blocks have started), so its outputs are already consistent with its inputs before anything else can read them. Plain always @(*) 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_comb re-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 an always @(*) block, while always_comb catches it correctly.
  • Enforces a single writer per variable. A variable assigned inside an always_comb block cannot legally be assigned by any other process anywhere else in the design — the tool flags that as an error. Plain always @(*) has no such restriction; two different always @(*) blocks (or an always @(*) and an initial block) can both drive the same variable, the exact multiple-driver ambiguity always_comb exists to rule out at compile time.

Why this triplet exists: the summary rule​

Old plain-Verilog riskNew keywordWhat it enforces
Incomplete/wrong sensitivity listalways_combSensitivity list auto-derived — impossible to forget a signal
Missing branch → accidental latchalways_combCompile error if any assigned signal isn't covered on every path
Blocking assignment inside a clocked blockalways_ffOnly nonblocking assignment accepted
Level-sensitive logic mistaken for a flip-flopalways_ffOnly edge-sensitive (posedge/negedge) triggering accepted
An intentional latch looking identical to a bugalways_latchSame 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.