Verilog · lesson 14

From Verilog to SystemVerilog

logic, always_comb/always_ff catching a real latch bug as a build error, enum and .* port connections, and what to learn next.

11 min read · Free, no login

Not a different language

It is worth dispelling a specific, common misconception before anything else: SystemVerilog is not a separate, verification-only language sitting beside Verilog. The IEEE merged the two standards in 2009 โ€” since then, "Verilog" and "SystemVerilog" describe one language, and every plain-Verilog construct this entire course has used is still exactly as valid as it was in lesson 1. What SystemVerilog adds, for the RTL-design purposes this lesson covers, is a cleaner type system and a handful of constructs that remove entire categories of the bugs earlier lessons spent real effort teaching how to avoid by discipline. This lesson covers exactly the additions relevant to everything already built in this course โ€” not SystemVerilog's much larger verification-only feature set, which is a separate topic outside this course's scope.

logic: one keyword instead of two

Lesson 3 spent real effort on the rule that a wire is driven continuously while a reg is assigned inside a procedural block, and that getting this wrong is a compile error. logic removes the need to choose: it can be used anywhere either wire or reg was required in plain Verilog, and the tool infers which one is meant from how the signal is actually driven. It carries the same four-value (0/1/x/z) set lesson 3 covered โ€” logic is not a new value system, only a merged keyword โ€” with one restriction worth knowing: a logic signal can still only be driven from one place (one assign, or one procedural block), exactly like a reg could only be assigned in one block. Every module in this course from this point forward could be rewritten with every wire and reg declaration replaced by logic, with no change in behavior โ€” this lesson's own examples do exactly that.

Three procedural blocks that state their own intent

Lessons 5 through 9 built a specific discipline by hand: use always @* for combinational logic, a clocked always @(posedge clk ...) for sequential logic, and never confuse the two. SystemVerilog adds three procedural block keywords that make this discipline a checkable fact the tool can verify, rather than a rule a designer has to remember and follow correctly every time: always_comb, always_ff, and always_latch.

The same mux and register from earlier lessons, in SystemVerilog
module sv_basics (
    input  logic       clk,
    input  logic       rst_n,
    input  logic [3:0] a, b,
    input  logic        sel,
    output logic [3:0] y_comb,
    output logic [3:0] y_reg
);
    always_comb begin
        y_comb = sel ? a : b;
    end

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) y_reg <= 4'd0;
        else        y_reg <= a + b;
    end
endmodule
y_comb (sel=1, should be a=3) =  3
y_comb (sel=0, should be b=5) =  5
y_reg after one clock (should be a+b=8) =  8

always_comb replaces always @* (lesson 5) and always_ff replaces a clocked always block (lesson 7) โ€” the sensitivity list for always_ff still has to be written explicitly, exactly as before, because nothing about the keyword tells a tool which edge is the clock. What is genuinely new is that these keywords are not just documentation: a tool actively checks that the block's body matches what the keyword claims.

Proof: always_comb catching a real latch bug

Lesson 6 spent an entire lesson on how an incomplete if silently infers a latch inside an ordinary always @* block, with a warning that has to be read and acted on. The same bug, written inside always_comb, is caught much harder.

module bad_always_comb (input logic a, input logic b, output logic c);
    always_comb begin
        if (a) c = b;   // no else -- would infer a latch
    end
endmodule
ERROR: Latch inferred for signal `\bad_always_comb.\c' from
always_comb process `\bad_always_comb.$proc$bad_always_comb.sv:2$1'.

Running this exact module through Yosys does not just produce a warning the way lesson 6's plain always @* version did โ€” it stops the build outright with an error, because always_comb is a direct, checkable claim ("this block is purely combinational") that the tool can verify and refuse to pass silently. always_latch works the same way in the opposite direction, flagging an error if a block declared to intentionally model a latch turns out not to actually need one. This is the real, practical payoff of the three specialized keywords: the discipline lesson 6 taught by explanation becomes something a tool enforces by construction.

enum: named states that cannot silently take on an illegal value

Lesson 9's three-process FSM template used localparam to name each state, which improves readability but adds no real protection โ€” nothing stops a bug from assigning a state register some value that was never one of the named states. A SystemVerilog enum is a genuinely distinct type with a restricted, enforced set of legal values.

The three-process FSM template, in SystemVerilog
typedef enum logic [1:0] {IDLE, RUN, DONE} state_t;
state_t state, next_state;

always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) state <= IDLE;
    else        state <= next_state;

always_comb begin
    unique case (state)
        IDLE: if (go) next_state = RUN; else next_state = IDLE;
        RUN:  next_state = DONE;
        DONE: next_state = IDLE;
    endcase
end

assign busy = (state == RUN);
state=IDLE busy=0
state=RUN  busy=1
state=DONE busy=0
state=IDLE busy=0

Beyond readability โ€” a waveform viewer or a $display of an enum signal shows the name (IDLE, RUN, DONE) rather than a bare number, visible directly in the simulated output above โ€” an enum can only ever be assigned one of its own named values or another enum value of the identical type, which is a real, tool-enforced guarantee lesson 9's plain localparam approach could not offer. The unique keyword on the case statement adds one more checkable claim: it asserts that exactly one branch matches for every reachable value, and tells a tool it is free to flag a violation rather than silently falling through to default priority-encoded behavior.

.*: implicit port connection by name

Lesson 2 established named port connection (.port(signal)) as the only style this course uses, specifically because it survives a module's port order changing later. .* takes that one step further for the common case where a signal in the instantiating module already happens to share its name with the port it connects to: it connects every such pair automatically, with no need to list them.

sv_basics dut (.*);   // connects clk, rst_n, a, b, sel, y_comb, y_reg automatically,
                       // since every one of those names already exists in this scope

This is not a loosening of lesson 2's rule โ€” it still binds strictly by name, the same safety property named connection always had, just without having to write out every pairing by hand when the names already match. A port whose connecting signal is named differently, or that needs explicit handling, is still written out individually alongside the .*, which is exactly what this lesson's own testbenches do throughout.

Where this course ends, and where SystemVerilog goes next

Everything covered across these fourteen lessons โ€” modules, the four-value system, combinational and sequential logic, FSMs, testbenches, parameterization, and now SystemVerilog's cleaner surface on top of all of it โ€” is the RTL design core that every other Verilog topic builds on. SystemVerilog's much larger verification feature set โ€” classes, constrained-random generation, functional coverage, assertions beyond the simple kind used informally in this course's testbenches โ€” is a genuinely separate discipline, usually called UVM-based verification, built by a different group of engineers than the RTL designers this course has been written for. It is worth knowing that feature set exists and is deliberately out of scope here, rather than discovering it by accident and assuming it belongs to the same skill this course just finished teaching.

Interview questions

What is the practical advantage of always_comb over always @*? Both infer a combinational sensitivity list automatically. always_comb additionally states the designer's intent explicitly, which lets a synthesis tool verify that intent and flag an error โ€” not just a warning โ€” if the block's actual logic would require a latch, catching exactly lesson 6's incomplete-branch bug earlier and harder than a plain always @* block would.

Why does always_ff still require an explicit sensitivity list, unlike always_comb? A tool can infer a combinational block's full sensitivity list by reading every signal the block uses. It cannot infer which signal is the clock, which edge is active, or whether a reset is meant to be synchronous or asynchronous โ€” that information only exists in the sensitivity list the designer writes, exactly as it did in plain Verilog's clocked always blocks since lesson 7.

What real protection does an enum type add over a plain localparam-named state register? A localparam is just a readable name for an ordinary number โ€” nothing stops a bug from assigning the register a value that was never meant to be a legal state. An enum is a distinct, tool-enforced type that can only hold one of its own named values (or another value of the identical enum type), which is a real guarantee, not only a readability improvement.

Key takeaways

  • SystemVerilog is not a separate language โ€” since the 2009 merger, every plain-Verilog construct from lessons 1 through 13 is still exactly as valid as it was there.
  • logic replaces both wire and reg, inferring which one applies from how the signal is actually driven, with the same four-value set and the same one-driver rule as before.
  • always_comb/always_ff/always_latch turn lessons 5 through 7's hand-followed discipline into a tool-checked claim โ€” confirmed here by always_comb turning lesson 6's missing-else bug into a hard build error instead of a warning.
  • An enum state register, unlike a plain localparam-named one, is a distinct type a tool actually enforces, and prints by name in simulation output rather than as a bare number.
  • .* connects same-named ports automatically while still binding strictly by name โ€” it extends lesson 2's named-connection safety rather than relaxing it.