Two lines from the run further down this page: a signed -8 compared with an unsigned 200 is reported as not smaller, and 200 + 100 stored in eight bits is 44. Neither is a tool bug. The language did exactly what the code said, and the code said something the writer did not mean.
This page is a reference you can keep open while you write RTL. Every row gives the construct, what it means, the trap, and the lesson that teaches it properly. Start with the five constructs below: used together they turn the most common RTL mistakes into compile errors, lint reports or visible x values.
Construct | What it prevents | Lesson |
|---|---|---|
| A block that was meant to be a flop but is not, and a second block writing the same register. | |
| Accidental latches and stale sensitivity lists. | |
| Mixed continuous and procedural drivers; an unreset flop shows | |
| Raw numbers reaching a state register; states show by name in the waveform. | |
| Unstated assumptions about overlapping or missing branches, and latches from a missing value. |
Here are all five in one small block. Use it as the starting shape for a control block: storage in always_ff, logic in always_comb with defaults at the top, typed states, and a default branch for the encoding nobody uses.
// yosys: ok
`default_nettype none
module five_habits (
input logic clk, rst_n,
input logic start, done_in,
input logic [7:0] din,
output logic busy,
output logic [7:0] dout
);
typedef enum logic [1:0] {IDLE, RUN, HOLD} state_e; // 2'b11 unused
state_e state, next;
always_ff @(posedge clk or negedge rst_n) // storage: <= only
if (!rst_n) state <= IDLE;
else state <= next;
always_ff @(posedge clk) // datapath: no reset needed
if (state == RUN) dout <= din;
always_comb begin // logic: defaults first
next = state;
busy = 1'b0;
unique case (state)
IDLE: if (start) next = RUN;
RUN: begin busy = 1'b1; if (done_in) next = HOLD; end
HOLD: next = IDLE;
default: next = IDLE; // 2'b11: recover
endcase
end
endmodule
`default_nettype wireRead it top to bottom. The default_nettype none line makes a misspelt signal name a compile error instead of a new one-bit wire. The enum gives the register a type, so state <= 2 will not compile. The first always_ff is the only writer of state. The second has no reset, because dout is written before anyone reads it; What a reset costs measures what that saves. In always_comb, next = state and busy = 1'b0 come before the case, so every path assigns both and no latch can appear.
Construct | Meaning | Trap | Lesson |
|---|---|---|---|
| A 4-state vector with one writer. |
| |
| Packed: four bytes forming one 32-bit vector; |
| |
| Names a type once. | Two modules that each declare the same typedef get two types; share it from a package. | |
| Named, typed values. | Without the base type the enum is a 2-state 32-bit | |
| Named fields over one vector. | The first field listed is the most significant. | |
| Set per instance / derived inside. | A derived width declared as | |
| Bits needed to index N items. |
| |
| Fill every bit of the target. | A fixed |
Construct | Meaning | Trap | Lesson |
|---|---|---|---|
| Edge-triggered storage. | Put the reset in the event list only for an asynchronous reset. | |
| Combinational logic; sensitivity is automatic. | A path that does not assign an output still means storage. Defaults first. | |
| A latch on purpose. | Almost never wanted in synchronous RTL; comment why it is there. | |
| Flops update together; logic computes in order. |
| |
| A continuous driver. | A second driver of the same |
The blocking rule fits on one line: flops get <=, logic gets =, and one block owns each variable. Most race and latch bugs break one of those three.
Four operator rules cause most arithmetic surprises: result width, signedness, shift direction and the unknown value. The bench below prints one result for each. It is a single module with no design under test, so you can paste it into any simulator.
module tb_ops_bite;
logic [7:0] u8 = 8'd200;
logic signed [7:0] s8 = -8'sd8; // bit pattern 1111_1000
logic [7:0] a = 8'd200, b = 8'd100;
logic [7:0] sum8;
logic [8:0] sum9;
logic x1 = 1'bx;
int errors = 0;
initial begin
sum8 = a + b; // 8-bit target keeps 8 bits
sum9 = a + b; // 9-bit target keeps the carry
$display("1 s8 < u8 -> %b", s8 < u8); // mixed: compare is unsigned
$display("2 s8 >> 1 -> %0d", s8 >> 1); // logical: fills with 0
$display("3 s8 >>> 1 -> %0d", s8 >>> 1); // arithmetic: copies the sign
$display("4 sum8 = a + b -> %0d", sum8);
$display("5 sum9 = a + b -> %0d", sum9);
$display("6 x1 == 1'b1 -> %b", x1 == 1'b1);
$display("7 x1 === 1'bx -> %b", x1 === 1'bx);
if (x1 == 1'b1) $display("8 if (x1 == 1) -> then branch");
else $display("8 if (x1 == 1) -> else branch");
if ((s8 < u8) !== 1'b0) errors++;
if ((s8 >> 1) !== 8'sd124) errors++;
if ((s8 >>> 1) !== -8'sd4) errors++;
if (sum8 !== 8'd44) errors++;
if (sum9 !== 9'd300) errors++;
if ((x1 == 1'b1) !== 1'bx) errors++;
if ((x1 === 1'bx) !== 1'b1) errors++;
if (errors == 0) $display("PASS: all eight results as described");
else $fatal(1, "FAIL: %0d unexpected results", errors);
$finish;
end
endmodule1 s8 < u8 -> 0
2 s8 >> 1 -> 124
3 s8 >>> 1 -> -4
4 sum8 = a + b -> 44
5 sum9 = a + b -> 300
6 x1 == 1'b1 -> x
7 x1 === 1'bx -> 1
8 if (x1 == 1) -> else branch
PASS: all eight results as describedRead the output one line at a time.
Line 1. An expression is signed only when every operand is signed. u8 is unsigned, so s8 is read as the unsigned pattern 248, and 248 < 200 is false.
Lines 2 and 3. >> always fills with zeros, so the pattern 1111_1000 becomes 0111_1100, which is 124. >>> copies the sign bit, giving -4, but only because s8 is declared signed; on an unsigned operand >>> also fills with zeros.
Lines 4 and 5. The addition is as wide as its widest operand or target. An 8-bit target drops the carry: 300 - 256 = 44. A 9-bit target keeps it.
Lines 6 to 8. == with an x operand gives x. An if treats x as false and takes the else branch, so simulation shows a clean value that silicon need not produce. === compares x literally; it belongs in testbenches, not RTL.
Operator | Meaning | Trap | Lesson |
|---|---|---|---|
| Width of the widest operand or target. | A narrow target drops the carry (line 4). | |
| How the bits are read. | One unsigned operand makes the whole expression unsigned (line 1). | |
| Logical / arithmetic right shift. |
| |
| Reduce a vector to one bit. |
| |
| Concatenate, replicate. | The result is unsigned and self-determined: | |
| Logical equality / 4-state identity. |
| |
Sign extension | Widening a signed value. | Concatenating a sign bit gives an unsigned result; cast it back. |
Construct | Meaning | Trap | Lesson |
|---|---|---|---|
| First matching item wins. | No | |
| Exactly one item matches. | The check runs only in a simulator that implements it; Icarus 13 says it ignores the keyword. | |
| First match wins, and at least one matches. | Synthesis may remove the logic for the no-match case; keep the promise exact. | |
| Every value not listed. | In a | |
| Items may be ranges | Only the item's | |
|
| A |
The Icarus message is worth knowing. Compiling the five_habits block above prints vvp.tgt sorry: Case unique/unique0 qualities are ignored. The design still simulates, but no overlap or no-match check runs. If your flow depends on those checks, run the block in a simulator or lint tool that implements them.
Construct | Meaning | Trap | Lesson |
|---|---|---|---|
| Builds N copies at elaboration. | Name every block. Unnamed ones get tool-made names such as | |
| Builds one option or the other. | The condition must be a constant, normally a parameter. | |
| Shared types, parameters and functions. | The package must compile before every file that imports it. | |
| A bundle of signals with directions per side. | Support differs across synthesis, lint and CDC tools; try the whole flow first. |
Each of these simulates in one way and builds in another, or hides a bug the tools would otherwise report.
Construct | Why not | Use instead | Lesson |
|---|---|---|---|
| An ASIC has no power-up values; most ASIC flows ignore or reject it. | A reset. The exception is FPGA register or ROM contents, which the bitstream loads. | |
| Synthesis ignores them, so simulation timing differs from the hardware. | Clocked logic; delays only in testbenches. | |
| An |
| |
Implicit nets | A misspelt name becomes a new one-bit wire. |
| |
| Synthesis obeys them, simulation ignores them. |
| |
| Hardware has no |
|
logic signed [7:0] s = -8'sd8; What do s >> 2 and s >>> 2 give, read as signed decimals?s >> 2 fills with zeros: 1111_1000 becomes 0011_1110, which is 62. s >>> 2 copies the sign bit: 1111_1110, which is -2. The second answer holds only because s is declared signed.
casex (opcode) and if (mode === 2'b01) in a synthesisable block. What do you change, and why?Replace casex with case (opcode) inside, so an unknown opcode does not match a wildcard item. Replace === with ==: hardware has no x to compare, and synthesis may reject === or treat it as ==, which hides the difference from simulation.
Tool: Icarus Verilog 13.0, run on 2026-10-17. Command: iverilog -g2012 -o ops_bite.vvp ops_bite.sv && vvp -n ops_bite.vvp, where ops_bite.sv is the tb_ops_bite module on the page. Result: the eight result lines and the PASS line shown on the page, copied unedited.
The five_habits block was compiled with iverilog -g2012 (which prints: vvp.tgt sorry: Case unique/unique0 qualities are ignored.), synthesised with Yosys 0.69+post (synth -top five_habits) with no warnings and no latch, and linted with Verilator 5.048 --lint-only -Wall (only a file-name warning). The shift answer in the first check-yourself (62 and -2) was confirmed with a one-line Icarus run.
Scope and limits: the table rows summarise the linked lessons and IEEE 1800 rules; they were not each run here. Synthesis support for interfaces, case inside and === differs between tools.