The Verilog-2001 you will meet

Read the Verilog-2001 in older blocks, tell the spellings that only look different from the ones that behave differently, and translate it to SystemVerilog as a review.

Lesson
R7.4
Time
20 min
Runs on
Your browser
Check
5 questions

A new engineer translated an inherited block from reg and always @(a or b) into logic and always_comb. The old regression failed at once. Its expected values had come from a simulation that never woke when sel changed, while the gates had always been right.

Two spellings, two simulations, one netlistThe lab bench, 1 ns after edges 2 to 7. y (old) is the Verilog-2001 block with always @(a or b); y (new) is its translation with always_comb.
012345clksela1133b2244y (old)112233y (new)112233stalestalestale
  1. Cycle 0

    Edge 2: a and b changed, so the old block ran. Both versions show 11.

  2. Cycle 1

    Edge 3: only sel changed. The old block never woke, so y still shows a; the new one shows 22.

  3. Cycle 3

    Edge 5: a changed, the old block woke at last, and now shows b, two edges late.

  4. Cycle 4

    Edge 6: sel alone again, and the old y is stale again.

Icarus 12 and Verilator 5.052 print the same old lines. Synthesize builds 16 cells for both versions.

Spellings that look different, and ones that behave differently

Most RTL in existing chips is Verilog-2001, the language before SystemVerilog extended it. You will read it every week. Most of its spellings map one to one onto SystemVerilog; a few change behaviour when you translate them.

Verilog-2001SystemVerilogKindWhat changes
reg [7:0] y; and wirelogic [7:0] y;spellingnothing for a signal with one driver; reg never meant a register
module m (a, y); input [7:0] a;ports declared in the headerspellingnothing
always @(posedge clk or negedge rst_n)always_ff @(...)spellingthe same flops; always_ff adds compile checks
always @(a or b)always_combbehaviourthe old block wakes only on the signals listed
always @*always_combbehaviour at time 0always @* waits for a first change; always_comb runs once at time 0
a misspelled namethe directive default_nettype none firstbehaviourthe typo becomes a 1-bit wire, or an error
text macros for constantslocalparamscopea macro is global to all later files (R7.3)

The time-0 row was measured on Icarus: with inputs set by declaration, always @* left y at x and always_comb gave the right value. Verilator gave the right value for both.

The list a reader trusts

A sensitivity list tells the simulator when to run a block: @(a or b) means "run when a or b changes". The block also reads sel, which the list leaves out. So when sel changes alone, the simulator does nothing, and y keeps its old value.

Three tools read the same listArrows show what each tool does with always @(a or b).
Three tools read the same listArrows show what each tool does with `always @(a or b)`. component always @(a or b) reads a, b and sel component Simulators Icarus 12, Verilator5.052 component Lint Verilator -Wall component Synthesizer Yosys 0.70, slang component Stale y edges 3, 4 and 6 component 8 MUX + 8 flops the full choice obey the listno messagetreats it as @*runnetlist
  1. Simulation

    Both simulators obey the list, so y is stale whenever sel changes alone.

  2. Lint

    Lint reads the block and prints nothing at all about the list.

  3. Synthesis

    The synthesizer warns non-edge sensitivity on a signal will be synthesized as @* sensitivity and builds the full mux. It gives that warning for a complete list too.

Read the diagram as text
  • always @(a or b): reads a, b and sel
  • Simulators: Icarus 12, Verilator 5.052
  • Lint: Verilator -Wall
  • Synthesizer: Yosys 0.70, slang
  • Stale y: edges 3, 4 and 6
  • 8 MUX + 8 flops: the full choice
  • always @(a or b) → Simulators: obey the list
  • always @(a or b) → Lint: no message
  • always @(a or b) → Synthesizer: treats it as @*
  • Simulators → Stale y: run
  • Synthesizer → 8 MUX + 8 flops: netlist

The gates were right all along. The simulation the old tests were written against was wrong.

That is the oldest mismatch between simulation and gates. Verilog-2001 added always @* to infer the list; SystemVerilog added always_comb, which also runs once at time 0. Synthesize both versions of the lab: 16 cells each, 8 $_MUX_ and 8 $_DFF_PN0_, 1 level of logic on this flow.

Typos that become wires

Verilog lets you use a name on the left of an assign without declaring it. The tool creates an implicit net: a 1-bit wire. A typo therefore compiles.

A typo, implicit nets on

module pick (a, b, sel, y);
  input  [7:0] a, b;
  input        sel;
  output [7:0] y;
  wire   [7:0] chosen;
  assign chosn = sel ? b : a;   // typo
  assign y = chosen;
endmodule

Icarus prints y=zz, Verilator y=00. Lint warns IMPLICIT, WIDTHTRUNC, UNDRIVEN and UNUSEDSIGNAL. Yosys 0.70 builds 0 cells: Wire pick.\chosen [7] is used but has no driver.

The same typo after default_nettype none

`default_nettype none
module pick (a, b, sel, y);
  // ... the same lines ...
  assign chosn = sel ? b : a;   // typo
  assign y = chosen;
endmodule
`default_nettype wire

Icarus: Net chosn is not defined in this context. Yosys 0.70: use of undeclared identifier 'chosn'; did you mean 'chosen'? Lint stops too.

A directive like this one affects every file compiled after it. So set it to none at the top of your file and back to wire at its end, as the translation below does.

The lab: translate the block as a review

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
a, binput[7:0]the two choices
selinput1picks b when 1, else a
youtput[7:0]y is b when sel is 1, else a
y_qoutput[7:0]y_q registers y
Clock
rising edge
Reset
rst_n asynchronous, active-low
Corner cases
sel changing while a and b stay still. Done when: Run shows y following sel at edges 3, 4 and 6, and Synthesize still reports 16 cells
  1. Run as shipped

    Edges 3, 4 and 6 show the stale y, and y_q stores it. Lint is silent. Synthesize reports 16 cells and warns about the list.

  2. Fix the list the old way

    Write always @(a or b or sel). Run: y follows sel. Synthesize still warns, because it warns about every hand-written list, complete or not.

  3. Translate

    Replace the file with the translation below and Run. The lines match the fixed list; the Gates tab shows the same 16 cells.

  4. Treat a changed line as a finding

    When a translation changes a result, check the old expected value against the spec before you trust either.

The translation to SystemVerilog
Lint is silent on this file; Synthesize reports 16 cells, 8 flops, 1 level of logic.
// pick_q in SystemVerilog: the same ports, the same behaviour, and a list the tool writes.
`default_nettype none
module pick_q (
  input  logic       clk,
  input  logic       rst_n,        // asynchronous, active-low
  input  logic [7:0] a, b,
  input  logic       sel,
  output logic [7:0] y,
  output logic [7:0] y_q
);
  always_comb
    if (sel) y = b;
    else     y = a;

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) y_q <= 8'h00;
    else        y_q <= y;
endmodule
`default_nettype wire
Asked in interviews
  1. What is the difference between reg and logic?

    For a signal with one driver, none in the hardware. reg was Verilog's variable type and never meant a register; logic says the same thing without the misleading name.

  2. What can go wrong with always @(a or b)?

    If the block reads a signal the list leaves out, simulation does not rerun when it changes and shows a stale output. Synthesis builds the full logic, so the gates and the simulation disagree.

  3. Why turn off implicit nets with the default_nettype directive?

    It turns off implicit nets, so a misspelled name is a compile error and not a silent 1-bit wire. Set it back to wire at the end of the file, because it affects later files too.

Try it

The bench changes its inputs 1 ns after an edge and prints 1 ns after the next one, so y_q shows the y that the flop loaded. At edges 3, 4 and 6 both show a stale value.

Synthesize reports 16 cells (8 $_MUX_, 8 $_DFF_PN0_), 0 latches and 1 level of logic, with the warning about the list. The translation builds the same 16 cells with no warning.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e2 sel=0 a=11 b=22 y=11 y_q=11 e3 sel=1 a=11 b=22 y=11 y_q=11 e4 sel=1 a=11 b=22 y=11 y_q=11 e5 sel=1 a=33 b=22 y=22 y_q=22 e6 sel=0 a=33 b=22 y=22 y_q=22 e7 sel=0 a=33 b=44 y=33 y_q=33

Original design · 16 cells · 8 flip-flops · 0 latches · 1 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic [7:0] a = 0, b = 0;
  logic       sel = 0;
  logic [7:0] y, y_q;
  pick_q dut (.*);
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (rst_n) $display("e%0d sel=%b a=%h b=%h y=%h y_q=%h", edge_n, sel, a, b, y, y_q);
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    a = 8'h11; b = 8'h22; tick();   // a and b change: the block runs
    sel = 1;              tick();   // sel changes alone
                          tick();
    a = 8'h33;            tick();   // a changes: the block runs again
    sel = 0;              tick();   // sel changes alone
    b = 8'h44;            tick();
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe block's list leaves out sel. The bench sets a to 3 and b to 9, then sets sel to 1 alone. What does it print?
module pick (a, b, sel, y);
  input  [3:0] a, b;
  input        sel;
  output [3:0] y;
  reg    [3:0] y;
  always @(a or b)
    if (sel) y = b;
    else     y = a;
endmodule
module tb;
  reg  [3:0] a = 0, b = 0;
  reg        sel = 0;
  wire [3:0] y;
  pick dut (.a(a), .b(b), .sel(sel), .y(y));
  initial begin
    #1 a = 4'd3; b = 4'd9;
    #1 sel = 1;
    #1 $display("sel=%b y=%0d", sel, y);
    $finish(0);
  end
endmodule
Spot the bugEach option is the event control of a Verilog-2001 combinational block. The bench changes a, b and sel one at a time and checks y after each change. Which option fails?
ConceptWhat does Yosys 0.70 on this flow build from always @(a or b) when the block also reads sel?
ConceptWhat does the directive default_nettype none change?
ConceptYou translate an old block line by line. Which change can alter what the simulation prints?

Remember

  • reg, wire, old port lists and always @(posedge clk) are spellings; translate them freely.
  • A hand-written list that misses a signal leaves the simulation stale while the synthesizer builds the full logic; Lint said nothing here.
  • Implicit nets turn typos into 1-bit wires; set default_nettype none at the top of each file and wire again at its end.
  • Translation is a review: a result that changes is a finding about the old simulation, so check it against the spec.