CMP-450 · Lesson 2 of 12 · 24 min

Verilog/SystemVerilog for synthesis I: combinational logic, always blocks, blocking vs non-blocking

A hardware description language looks like a programming language and is read by two very different programs. A simulator executes it, event by event, to predict what the circuit will do. A synthesis tool reads the same text and builds a circuit from it. Most of the bugs that new FPGA designers write come from code that means one thing to the simulator and another to synthesis, or that has a meaning to the simulator and none to synthesis. This lesson and the next teach the synthesizable subset of Verilog and SystemVerilog as a set of templates whose hardware meaning is unambiguous, together with the simulation semantics that explain why the rules exist.

Jonah's first block for the NS-5 front end is combinational: a saturating adder that removes the ADC's DC offset from each sample, and a small priority encoder that reports which channel tripped an overload detector. Both are a few lines of code, and both contain traps.

Languages and the synthesizable subset

Verilog was created in the 1980s as a simulation language and standardized as IEEE 1364, last revised in 2005. SystemVerilog, IEEE 1800, absorbed Verilog and added a richer type system, interfaces, assertions and a large verification language; current tools accept SystemVerilog source for synthesis, and this course uses its design features freely. VHDL, IEEE 1076, is the other major language, more strongly typed and more verbose, and widely used in aerospace, defence and European industry. The concepts in this course apply to all three.

Only part of each language is synthesizable. Delays such as #5, file input and output, the initial block (except for memory and register initialization, which FPGA tools accept), dynamic objects, and most of the verification features describe simulation behaviour that has no circuit equivalent. Synthesis tools ignore or reject them. Testbenches, the subject of lesson 4, use the whole language; designs use the subset.

Modules, ports and types

A design is a hierarchy of modules. Each module has a list of ports with directions and types, and its body contains declarations, continuous assignments, procedural blocks and instances of other modules. SystemVerilog's logic type replaces Verilog's distinction between wire, which must be driven by a continuous assignment or a port, and reg, which must be assigned in a procedural block. The name reg was always misleading, since a reg can describe a combinational signal; logic simply means a four-state variable (0, 1, X or Z) that may have a single driver.

Vectors are declared with a range, logic [11:0] sample, and are unsigned by default. The signed keyword makes arithmetic and comparisons two's complement. Literals carry an explicit size and base, such as 12'h800 or 4'b1010, and an unsized literal like 5 is a 32-bit signed integer. These rules matter because Verilog silently extends and truncates operands to make widths match, and because an expression is signed only if every operand in it is signed.

A saturating offset-removal block. The 13-bit difference cannot overflow; the always_comb block clamps it back to 12 bits.
module offset_sat #(
    parameter int W = 12
) (
    input  logic signed [W-1:0] x,        // raw ADC sample
    input  logic signed [W-1:0] offset,   // DC offset from calibration
    output logic signed [W-1:0] y,        // x - offset, saturated
    output logic                clipped
);
    localparam logic signed [W-1:0] MAXV = {1'b0, {(W-1){1'b1}}};
    localparam logic signed [W-1:0] MINV = {1'b1, {(W-1){1'b0}}};

    logic signed [W:0] diff;              // one extra bit: no overflow
    assign diff = x - offset;             // both operands signed, sign-extended

    always_comb begin
        clipped = 1'b0;                   // default assignments first
        y       = diff[W-1:0];
        if (diff > MAXV) begin
            y = MAXV;  clipped = 1'b1;
        end else if (diff < MINV) begin
            y = MINV;  clipped = 1'b1;
        end
    end
endmodule

The listing shows the main combinational templates. The continuous assignment, assign diff = x − offset, describes a wire permanently driven by an expression; in hardware it is a subtractor on the carry chain. The always_comb block describes combinational logic procedurally: the simulator re-runs it whenever any signal it reads changes, and synthesis builds the equivalent logic. The replication operator {(W−1){1'b1}} and concatenation build the constants, and the parameter W makes the block reusable for other sample widths.

Width and sign rules

Verilog evaluates an expression at the width of its widest operand or of the target it is assigned to, whichever is larger, extending narrower operands first. Unsigned operands are zero-extended and signed operands are sign-extended, but only if the whole expression is signed. A single unsigned operand, including an unsized-looking constant written as 4'd3, makes the entire expression unsigned, and then a negative signed operand is zero-extended into a large positive number. Comparisons have the same rule, which is how x < 0 can be false for every x.

Worked problem

Widths for the offset subtraction

x and offset are signed 12-bit values from −2,048 to 2,047. What width does x − offset need to avoid overflow, and what goes wrong if offset is declared unsigned?

Show solution
  1. The extreme differences are 2,047 − (−2,048) and −2,048 − 2,047.
  2. \[ x - offset \in [-4095,\ 4095]\]
  3. \[ -2^{12} \le -4095 \quad\text{and}\quad 4095 \le 2^{12} - 1 \;\to\; 13 \text{ bits signed}\]
  4. If offset is unsigned, the subtraction is unsigned. A negative x such as −5, bit pattern 4,091, is zero-extended to 13 bits as 4,091 rather than 8,187, so diff is far too large and the clamp saturates a small negative sample to the positive maximum.

Answer: 13 bits, signed; mixing in an unsigned operand turns the whole subtraction unsigned and corrupts negative samples.

The code below emulates these rules on bit patterns, so you can see the effect without a simulator. It evaluates the subtraction once with both operands signed and once with an unsigned offset.

Emulating Verilog sign extension for the 13-bit offset subtraction.
def to_signed(v, w):
    v &= (1 << w) - 1
    return v - (1 << w) if v >> (w - 1) else v

def extend(v, w_from, w_to, signed):
    v &= (1 << w_from) - 1
    if signed and v >> (w_from - 1):
        v |= ((1 << w_to) - 1) ^ ((1 << w_from) - 1)   # copy the sign bit upward
    return v

W, WD = 12, 13
for x, off in [(-5, 3), (2047, -2048), (-2048, 2047)]:
    xb, ob = x & 0xFFF, off & 0xFFF
    s = (extend(xb, W, WD, True) - extend(ob, W, WD, True)) & 0x1FFF
    u = (extend(xb, W, WD, False) - extend(ob, W, WD, False)) & 0x1FFF
    print(f"x = {x:6d}  offset = {off:6d}   signed diff = {to_signed(s, WD):6d}"
          f"   unsigned diff = {u:5d} (read as signed: {to_signed(u, WD):6d})")
Output
x =     -5  offset =      3   signed diff =     -8   unsigned diff =  4088 (read as signed:   4088)
x =   2047  offset =  -2048   signed diff =   4095   unsigned diff =  8191 (read as signed:     -1)
x =  -2048  offset =   2047   signed diff =  -4095   unsigned diff =     1 (read as signed:      1)

With both operands signed, every difference is correct. When the operands are treated as unsigned, −5 − 3 becomes 4,091 − 3 = 4,088 instead of −8, which the clamp in the listing would then treat as a large positive overflow. The fix is to declare every arithmetic signal with the sign it really has and, where an unsigned value must join signed arithmetic, to convert it explicitly with $signed({1'b0, u}), which adds a zero sign bit first.

Combinational always blocks and latches

A combinational block must assign every output on every path through it. If some path leaves an output unassigned, the language says the output keeps its previous value, and the only hardware that keeps a value without a clock is a latch. Synthesis therefore infers a level-sensitive latch, reports something like warning: latch inferred for signal y, and the design now contains a storage element that static timing analysis handles poorly and that usually was not intended. FPGAs implement latches in the flip-flop sites, but they are almost always a bug in FPGA designs.

A priority encoder for the overload flags, first with an unintended latch, then fixed with a default assignment.
// latch inferred: when none of the bits is set, ch is not assigned
always_comb begin
    if      (ovl[0]) ch = 2'd0;
    else if (ovl[1]) ch = 2'd1;
    else if (ovl[2]) ch = 2'd2;
    else if (ovl[3]) ch = 2'd3;
end

// fixed: default assignment at the top, so every path assigns ch and any
always_comb begin
    ch  = 2'd0;
    any = |ovl;
    unique casez (ovl)               // priority encoder, lowest channel first
        4'b???1: ch = 2'd0;
        4'b??10: ch = 2'd1;
        4'b?100: ch = 2'd2;
        4'b1000: ch = 2'd3;
        default: ch = 2'd0;          // ovl == 0
    endcase
end

The cure is the default-assignment idiom: assign every output a value at the top of the block, then override it where needed. A complete else or default branch also works, but defaults are more robust when the block grows. SystemVerilog's always_comb helps too, because tools check that a block declared combinational really is combinational and report a latch as a problem rather than a curiosity. For the same reason, always_comb should be preferred over Verilog's always @*, which also lacks always_comb's guarantee that the block is evaluated once at time zero.

A case statement compares an expression against a list of items in order and takes the first match, so it describes a priority structure unless the items are mutually exclusive. The unique keyword tells the tool that exactly one item will match, which allows a parallel multiplexer instead of a priority chain, and makes the simulator report a violation if the assumption fails. The priority keyword says at least one will match. Both replace the old full_case and parallel_case synthesis comments, which changed the synthesized hardware without changing the simulation and produced exactly the kind of mismatch this lesson is about.

How the simulator schedules events

To understand the most important rule in RTL coding, you need a model of simulation. A Verilog simulator advances time in steps. At each time step it maintains an ordered set of event regions. The active region holds processes ready to run: always blocks woken by a change, continuous assignments to re-evaluate. When the active region empties, the simulator moves the contents of the non-blocking assignment (NBA) region into the active region, and so on until nothing is left at that time step. The order in which processes within one region run is not defined by the standard.

The two assignment operators interact with these regions differently. A blocking assignment, a = b, evaluates the right-hand side and updates a immediately, before the next statement in the block runs, just like assignment in software. A non-blocking assignment, a <= b, evaluates the right-hand side immediately but schedules the update of a for the NBA region, after every process triggered by the current event has run. Every non-blocking assignment triggered by a clock edge therefore reads the values that existed just before the edge, which is exactly how a set of flip-flops sharing a clock behaves.

The simulation below models both operators for two classic cases: a three-stage shift register written as three assignments in one clocked block, and a swap of two registers written in two separate clocked blocks whose execution order the simulator may choose.

A model of blocking and non-blocking assignment at a clock edge.
def clock_edge(stmts, state, nonblocking):
    """run assignment statements (target, source) at one clock edge"""
    s = dict(state)
    pending = []
    for tgt, src in stmts:
        val = s[src]                    # right-hand side is read now
        if nonblocking:
            pending.append((tgt, val))  # update deferred to the NBA region
        else:
            s[tgt] = val                # update visible to later statements
    for tgt, val in pending:
        s[tgt] = val
    return s

shift = [("q1", "d"), ("q2", "q1"), ("q3", "q2")]
for nb in (False, True):
    st = {"d": 0, "q1": 0, "q2": 0, "q3": 0}
    trace = []
    for d in [1, 0, 1, 1, 0]:
        st["d"] = d
        st = clock_edge(shift, st, nb)
        trace.append(f"{st['q1']}{st['q2']}{st['q3']}")
    kind = "non-blocking" if nb else "blocking    "
    print(f"shift register, {kind}: q1q2q3 after each edge: {' '.join(trace)}")

# swap written as two always blocks; the simulator may run either one first
for nb in (False, True):
    results = set()
    for order in ([("a", "b"), ("b", "a")], [("b", "a"), ("a", "b")]):
        st = clock_edge(order, {"a": 1, "b": 0}, nb)
        results.add((st["a"], st["b"]))
    kind = "non-blocking" if nb else "blocking    "
    print(f"swap a=1, b=0, {kind}: possible results (a, b) = {sorted(results)}")
Output
shift register, blocking    : q1q2q3 after each edge: 111 000 111 111 000
shift register, non-blocking: q1q2q3 after each edge: 100 010 101 110 011
swap a=1, b=0, blocking    : possible results (a, b) = [(0, 0), (1, 1)]
swap a=1, b=0, non-blocking: possible results (a, b) = [(0, 1)]

With blocking assignments, q1 takes d, then q2 takes the new q1, then q3 takes the new q2, so after every edge all three outputs equal the input: the shift register has collapsed into a single flip-flop driving three outputs. Synthesis builds what the code says, a single register, so in this case the simulation and the hardware agree with each other and both disagree with the designer. With non-blocking assignments, each stage takes the value its predecessor had before the edge and the pattern moves one stage per clock, as intended.

The swap is worse. With blocking assignments, the result depends on which always block the simulator happens to run first: both registers end up 0, or both end up 1. This is a race condition, and different simulators, or the same simulator after an unrelated change, can give different answers. The hardware, two flip-flops each loaded from the other, swaps correctly. Non-blocking assignments give the swap in every order, matching the hardware.

Two schematics. Left, labelled blocking: input d feeds one flip-flop whose output drives q1, q2 and q3 together. Right, labelled non-blocking: d feeds a chain of three flip-flops, q1 to q2 to q3, all sharing one clock.
What synthesis builds from the three-assignment shift register written with blocking (left) and non-blocking (right) assignments.

The coding rules

The guidelines that follow from this model were set out by Clifford Cummings in a widely cited conference paper in 2000, and nearly every design team uses them. They are rules about simulation semantics, chosen so that the simulator and the synthesized circuit always agree.

RTL coding rules for consistent simulation and synthesis.
RuleReason
Use non-blocking assignments (<=) in clocked always_ff blocks.Every register samples values from before the edge, as flip-flops do.
Use blocking assignments (=) in combinational always_comb blocks.Statements evaluate in order like a chain of gates; intermediate values are visible to later statements.
Do not mix blocking and non-blocking assignments in one block.Mixed blocks are hard to reason about and easy to break in edits.
Assign each variable from only one always block.Multiple drivers create races in simulation and are illegal or ambiguous in hardware.
Give every combinational output a default value.Prevents unintended latches.
Do not use delays (#) in synthesizable code.Synthesis ignores them, so simulation would differ from hardware.

Blocking assignments are correct, and convenient, inside combinational blocks, because there the statements describe a chain of logic: an intermediate variable computed on one line is meant to feed the next line within the same evaluation. Written with non-blocking assignments, the same block would read stale intermediate values and need to be re-evaluated, and in some simulators it would simulate with an extra delta-cycle delay that confuses waveform debugging.

Operators and the hardware they imply

Every operator has a hardware cost, and a designer should be able to estimate it. Bitwise operators (&, |, ^, ~) and reductions (&x, |x, ^x) cost one LUT per six inputs. Addition, subtraction and magnitude comparison use the carry chain at about one LUT per bit. Equality comparison costs about one LUT per three bits plus a small reduction tree. Shifts by a constant are free, because they are just wiring; a shift by a variable amount is a barrel shifter, with log2 of the width stages of multiplexers. Multiplication maps to DSP slices. Division and modulo by anything other than a power of two are expensive: synthesis either refuses or builds a large slow array, so designers use a sequential divider, as lesson 5 does for the square root, or avoid division altogether, for example by multiplying by a precomputed reciprocal.

The conditional operator, sel ? a : b, and if-else chains both become multiplexers. A long if-else-if chain describes a priority multiplexer whose last condition passes through every earlier one, so its delay grows with the length of the chain; when the conditions are mutually exclusive, a unique case gives a balanced tree instead. These differences rarely matter for small blocks but often decide whether a design meets timing, which lesson 9 returns to.

Simulation and synthesis mismatches

The final category of bug is code that simulates differently from the hardware it produces. Incomplete sensitivity lists in Verilog's always @(a or b) style are the classic case: if the block also reads c but c is missing from the list, the simulator will not re-run the block when c changes, while synthesis builds logic that responds to c immediately. always_comb and always @* compute the list automatically and remove the problem. Other mismatches come from X values: in simulation, if (x) treats an unknown condition as false and takes the else branch, which can hide a reset bug that the hardware will expose. Lesson 4 shows how assertions catch unknowns before they hide anything.

Jonah's offset block went through review with one change. Maren Holt pointed out that the calibration offset was coming from a register written by firmware as an unsigned value, so the port had to be declared signed and the register map documented as two's complement. It is a small change, but it is exactly the class of bug that the width and sign rules produce: no tool reports it, the simulation of a positive offset passes, and the failure appears only in the field when a board with a negative offset is calibrated.

Common mistakes

  • MistakeUsing blocking assignments in clocked blocks.

    InsteadUse non-blocking assignments in always_ff; reserve blocking assignments for always_comb.

  • MistakeLeaving an output unassigned on some path of a combinational block.

    InsteadAssign defaults at the top of every always_comb block to avoid latches.

  • MistakeMixing signed and unsigned operands in one expression.

    InsteadDeclare signals with their true sign and convert explicitly with $signed when combining them.

  • MistakeWriting an incomplete sensitivity list in always @(...).

    InsteadUse always_comb, which infers the complete list and checks for combinational behaviour.

  • MistakeUsing delays or file input and output in design code.

    InsteadKeep simulation-only constructs in testbenches; synthesis ignores or rejects them.

Summary and key terms

  • The same HDL text is executed by simulators and turned into circuits by synthesis tools; the synthesizable subset is what both interpret consistently.
  • logic replaces wire and reg; vectors are unsigned unless declared signed, and any unsigned operand makes an expression unsigned.
  • Continuous assignments and always_comb blocks describe combinational logic; incomplete assignment infers a latch.
  • Blocking assignments update immediately; non-blocking assignments update in the NBA region after all triggered processes run.
  • Use non-blocking in clocked blocks and blocking in combinational blocks, never mixed, with one driver per variable.
  • Operators have predictable costs: carry chains for arithmetic, barrel shifters for variable shifts and DSP slices for multiplication.
Synthesizable subset
The constructs of an HDL that synthesis tools can turn into hardware with the same meaning as in simulation.
Continuous assignment
An assign statement that permanently drives a net from an expression.
Latch inference
Creation of a level-sensitive storage element when a combinational block leaves an output unassigned on some path.
Blocking assignment
An assignment (=) whose update takes effect before the next statement executes.
Non-blocking assignment
An assignment (<=) whose update is deferred until all processes triggered at the current time have run.
NBA region
The simulator event region in which non-blocking updates are applied.
Race condition
A simulation result that depends on the arbitrary order in which concurrent processes run.
Sign extension
Widening a two's complement value by copying its sign bit into the new upper bits.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. What width does the sum of two signed 16-bit values need to avoid overflow?

    Show answer

    17 bits.

  2. A block always @(posedge clk) begin b = a; c = b; end is synthesized. What hardware results?

    Show answer

    One flip-flop loaded from a, driving both b and c; c does not get an extra stage of delay.

  3. Why does if (sel) y = a; with no else in always_comb produce a latch?

    Show answer

    When sel is 0, y is not assigned, so it must hold its old value, which requires storage.

  4. What does a variable shift x >> n cost for a 32-bit x?

    Show answer

    A barrel shifter of five multiplexer stages, roughly 32 × 5 two-input multiplexers before LUT packing.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz