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.
y (old) is the Verilog-2001 block with always @(a or b); y (new) is its translation with always_comb.- Cycle 0
Edge 2:
aandbchanged, so the old block ran. Both versions show 11. - Cycle 1
Edge 3: only
selchanged. The old block never woke, soystill showsa; the new one shows 22. - Cycle 3
Edge 5:
achanged, the old block woke at last, and now showsb, two edges late. - Cycle 4
Edge 6:
selalone again, and the oldyis 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-2001 | SystemVerilog | Kind | What changes |
|---|---|---|---|
reg [7:0] y; and wire | logic [7:0] y; | spelling | nothing for a signal with one driver; reg never meant a register |
module m (a, y); input [7:0] a; | ports declared in the header | spelling | nothing |
always @(posedge clk or negedge rst_n) | always_ff @(...) | spelling | the same flops; always_ff adds compile checks |
always @(a or b) | always_comb | behaviour | the old block wakes only on the signals listed |
always @* | always_comb | behaviour at time 0 | always @* waits for a first change; always_comb runs once at time 0 |
| a misspelled name | the directive default_nettype none first | behaviour | the typo becomes a 1-bit wire, or an error |
| text macros for constants | localparam | scope | a 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.
always @(a or b).- Simulation
Both simulators obey the list, so
yis stale wheneverselchanges alone. - Lint
Lint reads the block and prints nothing at all about the list.
- Synthesis
The synthesizer warns
non-edge sensitivity on a signal will be synthesized as @* sensitivityand 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
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| a, b | input | [7:0] | the two choices |
| sel | input | 1 | picks b when 1, else a |
| y | output | [7:0] | y is b when sel is 1, else a |
| y_q | output | [7:0] | y_q registers y |
- Clock
- rising edge
- Reset
rst_nasynchronous, active-low- Corner cases
selchanging whileaandbstay still. Done when: Run showsyfollowingselat edges 3, 4 and 6, and Synthesize still reports 16 cells
Run as shipped
Edges 3, 4 and 6 show the stale
y, andy_qstores it. Lint is silent. Synthesize reports 16 cells and warns about the list.Fix the list the old way
Write
always @(a or b or sel). Run:yfollowssel. Synthesize still warns, because it warns about every hand-written list, complete or not.Translate
Replace the file with the translation below and Run. The lines match the fixed list; the Gates tab shows the same 16 cells.
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
// 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
What is the difference between
regandlogic?For a signal with one driver, none in the hardware.
regwas Verilog's variable type and never meant a register;logicsays the same thing without the misleading name.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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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=33Original 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
Remember
reg,wire, old port lists andalways @(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 noneat the top of each file andwireagain at its end. - Translation is a review: a result that changes is a finding about the old simulation, so check it against the spec.