Module 5: Behavioral Verilog for Synthesis · 5.2 Sensitivity Lists and Combinational always Blocks

always @(*)

Every signal a combinational block reads has to be in its list. That is easy for two inputs and hopeless for a two-hundred-line decoder with a dozen inputs, some of them buried in nested conditions. Verilog-2001 removed the problem with @(*).

The problem with manual lists

`timescale 1ns / 1ps
`default_nettype none

module mux4 (
    input  wire [1:0] sel,
    input  wire       a, b, c, d,
    output reg        y
);

    always @(sel or a or b or c or d) begin
        case (sel)
            2'b00: y = a;
            2'b01: y = b;
            2'b10: y = c;
            2'b11: y = d;
        endcase
    end

endmodule

Correct as written. Now someone adds a fifth input and a new arm, and forgets the list. It compiles, it simulates, and it is wrong for one sequence of inputs that the tests may or may not try. The list lives at the top of the block and the logic lives in the body, and the two drift apart.

The automatic list

`timescale 1ns / 1ps
`default_nettype none

module mux4_star (
    input  wire [1:0] sel,
    input  wire       a, b, c, d,
    output reg        y
);

    always @(*) begin
        case (sel)
            2'b00: y = a;
            2'b01: y = b;
            2'b10: y = c;
            2'b11: y = d;
        endcase
    end

endmodule

@(*), or @* without parentheses, means “sensitive to everything this block reads”. The two modules are identical to the simulator and the synthesiser, and the second cannot go stale, because there is nothing to update. Use it for every combinational block.

What it includes

Everything read in the block: every signal on the right of an assignment, every signal in an if, case or ?: condition, every index into an array or part-select, and signals read inside functions the block calls. Signals that appear in branches which may not execute are included too; if a changes while mode is 0 in

always @(*) begin
    y = 1'b0;
    if (mode)
        y = a & b;
    else
        y = c | d;
end

the block runs, recomputes c | d, and nothing changes, which costs a little simulation time and no correctness.

What it leaves out: signals only ever written, and edge events. @(*) cannot express posedge; a flip-flop has to be written as @(posedge clk), which is a useful restriction rather than a limitation.

The same hardware

// manual list
always @(en or req) begin
    valid = 1'b0;
    grant = 2'b00;
    if (en) begin
        if      (req[3]) begin grant = 2'b11; valid = 1'b1; end
        else if (req[2]) begin grant = 2'b10; valid = 1'b1; end
        else if (req[1]) begin grant = 2'b01; valid = 1'b1; end
        else if (req[0]) begin grant = 2'b00; valid = 1'b1; end
    end
end

// automatic list, same netlist
always @(*) begin
    valid = 1'b0;
    grant = 2'b00;
    if (en) begin
        if      (req[3]) begin grant = 2'b11; valid = 1'b1; end
        else if (req[2]) begin grant = 2'b10; valid = 1'b1; end
        else if (req[1]) begin grant = 2'b01; valid = 1'b1; end
        else if (req[0]) begin grant = 2'b00; valid = 1'b1; end
    end
end

Same waveforms, same gates. The default assignments at the top are the latch-prevention pattern of the fourth article.

Converting

The conversion is mechanical: delete the list, write *.

always @(a or b or sel) begin                       →   always @(*) begin
    y = sel ? a : b;                                        y = sel ? a : b;
end                                                     end

always @(state or din or valid or ready or count)   →   always @(*) begin

Some teams keep manual lists as documentation of what a block depends on. The intent is fair and the mechanism is wrong, because a stale list documents nothing; a comment does the job without the risk.

always_comb

SystemVerilog goes one step further:

always_comb begin
    y = a & b & c;
end

It runs once at time zero, where @(*) waits for the first change, so initial values are right before the first edge; and a synthesiser that finds a latch in an always_comb must report an error rather than a warning. Module 9 moves to it. Until then, @(*).

Practice Exercise

Convert each block and list what @(*) infers.

always @(x or y or z) begin
    out = (x & y) | (~x & z);
end

always @(addr or wen or wdata or mem_out) begin
    rdata = wen ? wdata : mem_out;
    ack   = (addr < 8'hFF);
end

always @(state or req or ack or timeout) begin
    case (state)
        2'b00:   next_state = req     ? 2'b01 : 2'b00;
        2'b01:   next_state = ack     ? 2'b10 : 2'b01;
        2'b10:   next_state = timeout ? 2'b00 : 2'b10;
        default: next_state = 2'b00;
    endcase
end
Solution

In each case the header becomes always @(*) and the body is unchanged. The inferred lists are x, y, z; addr, wen, wdata, mem_out, with rdata and ack excluded as outputs; and state, req, ack, timeout, with next_state excluded.

The next article traces exactly what goes wrong when a signal is missing, cycle by cycle, and why the synthesiser does not see the problem the simulator has.

← The Sensitivity List The Missing Signal →