VLSI

Verilog Sensitivity List

By CourseTron Team ·  3 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: A Verilog always-block's sensitivity list names the signals whose change re-triggers the block; using always @(*) for combinational logic lets the simulator derive that list automatically, eliminating the classic "simulation says one thing, silicon does another" bug caused by an incomplete hand-written list.

    If you have ever chased a bug where your RTL simulated perfectly but the synthesized netlist behaved differently, the culprit is very often a mangled sensitivity list. It is one of the most common things interviewers at companies using Synopsys VCS, Cadence Xcelium, or Siemens Questa probe, because it reveals whether a candidate understands the gap between event-driven simulation semantics and what hardware actually does. This article walks through exactly how the sensitivity list drives an always block, the crucial difference between level and edge sensitivity, and why always @(*) (added in Verilog-2001) is the safe default for combinational logic.

    What the sensitivity list actually controls

    An always block in Verilog is a process that executes once, then suspends at its timing control until a triggering event occurs. The sensitivity list, written in the @(...) after always, is that list of triggers. The simulator maintains an event queue; whenever a signal in the list changes value during the current simulation time step, the process is woken, runs its procedural statements top-to-bottom, and suspends again. Nothing in the list means the block never re-evaluates after its initial run.

    This is pure simulation bookkeeping. Real combinational gates have no "sensitivity list" — they continuously propagate. The list exists only so the event-driven simulator knows when recomputation is worthwhile. The mismatch between those two mental models is where bugs breed.

    A change in the list wakes the block Signals a, b, sel change events always @(*) wakes, runs the statements, suspends out recomputed The list is simulator bookkeeping; real gates have no such list. CourseTron · coursetron.com
    Any change on a signal in the list wakes the always block, which recomputes the output.

    Level sensitivity vs edge sensitivity

    There are two fundamentally different ways to list triggers, and they model two different kinds of hardware.

    Level sensitivity fires on any transition — rising or falling — of the listed signal. A block like always @(a or b or sel) is level-sensitive: it should re-run whenever a, b, or sel changes. This models combinational logic (muxes, adders, decoders) and transparent latches.

    Edge sensitivity uses the posedge or negedge keywords to fire only on a specific transition. always @(posedge clk) wakes only on the 0→1 clock transition, which is exactly how an edge-triggered flip-flop behaves. Add or negedge rst_n and you get an asynchronous reset.

    // Edge-sensitive: a D flip-flop with async active-low reset
    always @(posedge clk or negedge rst_n)
      if (!rst_n) q <= 1'b0;
      else        q <= d;
    
    // Level-sensitive: a 2:1 mux (combinational)
    always @(*)
      out = sel ? b : a;

    Rule of thumb: edge lists (posedge/negedge) build sequential cells; bare level lists build combinational logic or latches. Mixing an edge and a level signal in the same list (e.g. @(posedge clk or a)) is illegal for synthesis and a frequent interview trap.

    Note the assignment-operator pairing that goes with each style: non-blocking <= inside edge-triggered blocks, blocking = inside combinational blocks. That choice is a whole topic on its own — see our guide to blocking vs non-blocking assignments for why it matters for race-free sequential design.

    Two list styles, two kinds of hardware Edge list: sequential @(posedge clk or negedge rst_n) D flip-flop d q clk edge and async reset only Level list: combinational @(*) 2:1 mux a, b, sel out any input change re-evaluates CourseTron · coursetron.com
    A posedge/negedge list builds a flip-flop; a level list such as @(*) builds combinational logic.

    The incomplete-sensitivity-list bug

    Before Verilog-2001, you had to enumerate every right-hand-side signal by hand. Miss one, and the simulator simply does not re-evaluate the block when that signal changes — so the output goes stale. Consider this (pre-2001) mux where sel was forgotten:

    always @(a or b)          // BUG: sel missing
      out = sel ? b : a;

    In simulation, flipping sel alone does not wake the block, so out keeps its old value until a or b happens to change. The RTL now behaves like a sticky latch. But synthesis tools (Design Compiler, Genus) ignore the sensitivity list and infer pure combinational logic from the equation, so the gate-level netlist does switch on sel. Result: a functional mismatch between RTL simulation and the netlist that can survive all the way to a failed gate-level simulation or, worse, a taped-out bug.

    Complete list vs incomplete list when sel toggles sel out (@*) out (stale) Blue/cyan follow sel at once; orange lags behind (missing sel in the list). CourseTron · coursetron.com
    With a complete list the output tracks sel immediately (cyan); an incomplete list leaves it lagging and stale (orange).

    How always @(*) fixes it

    Verilog-2001 introduced @(*) (equivalently @*), the implicit or automatic sensitivity list. The simulator inspects every signal read on the right-hand side of assignments and in conditionals inside the block, and builds the complete level-sensitive list for you. You cannot forget a signal because you never type one.

    SystemVerilog went further with always_comb, which not only infers the list but also lets the tool check that the block really is combinational (no latches, single driver). Lint tools in VCS, Xcelium, and Questa flag violations at elaboration — a strong reason to prefer always_comb in modern designs.

    ConstructListIntentLatch risk
    always @(a or b)manualcombinationalHigh — easy to miss a signal
    always @(*)autocombinationalLow — list always complete
    always_combauto + checkedcombinationalLowest — tool enforces it
    always @(posedge clk)edgesequential (flop)n/a

    One subtlety worth knowing for interviews: @(*) only adds signals that are read inside the block. It does not descend into the contents of a function called elsewhere, and it does not include signals read only to compute an index that the tool cannot resolve statically. These corner cases are rare, but they are exactly why always_comb — with its stricter semantics — is the recommended construct in current coding standards used across the industry, including the flows we teach alongside ChipXpert's RTL and verification training.

    Latch inference: the other side of the same coin

    A complete sensitivity list is necessary but not sufficient for clean combinational logic. If a combinational block does not assign the output on every path — a missing else, or an incomplete case without a default — the synthesizer must remember the old value, so it infers a transparent latch. The simulator with @(*) will faithfully model that latch, so sim and synthesis agree, but you still almost never wanted a latch. The discipline is: assign a default at the top of the block, or cover every branch. We drill this flow end-to-end in our combinational logic design guide.

    Putting it together

    The sensitivity list is the contract between your RTL and the event-driven simulator. Get it wrong for combinational logic and you create a phantom latch that only exists in simulation; get it wrong by mixing edges and levels and the code will not synthesize. The modern answer is simple: use always @(*) (or better, always_comb) for everything combinational, use explicit posedge/negedge lists for sequential cells, and never hand-enumerate a combinational list again.

    Want to master RTL coding that passes gate-level sim the first time? Explore CourseTron's hands-on VLSI RTL & Verification track on our courses page — real Synopsys/Cadence/Siemens flows, not just slides.

    Frequently asked questions

    Is always @(*) the same as always @*?

    Yes. @(*) and @* are syntactically interchangeable in Verilog-2001 and later; both build the same automatic level-sensitive list. The parenthesized form is slightly more common in style guides.

    Can I use always @(*) for sequential logic?

    No. An automatic list captures level changes on data inputs, so it would re-trigger on data as well as clock and cannot model edge-triggered storage. Flip-flops require an explicit @(posedge clk) (optionally with an async reset edge).

    Why does synthesis ignore the sensitivity list?

    Synthesis infers hardware from the logic equation, not from simulation triggers. Gates propagate continuously, so the tool has no use for a trigger list — which is precisely why an incomplete list causes sim-versus-netlist mismatches rather than synthesis errors.

    Should I still write manual lists today?

    For combinational logic, no — prefer always_comb or @(*). The only place you type signals by hand is the edge list of a sequential block, where enumeration is intentional and small.

    Advertisement