Module 4: Introduction to Verilog HDL ยท 4.3 Operators

Precedence and Width

Verilog will compile a & b == 8'hFF without a murmur, build it, and give you hardware that does something other than what you meant. No error, no warning. This last article of the section is about the two ways an expression can quietly mean something else: operator precedence and expression width.

The table

Higher rows bind tighter. Operators on one row associate left to right.

Level Operators
1 + - ! ~ & ~& | ~| ^ ~^ as unary unary and reduction
2 ** power
3 * / %
4 + -
5 << >> <<< >>> shifts
6 < <= > >= relational
7 == != === !== equality
8 & bitwise AND
9 ^ ~^ bitwise XOR
10 | bitwise OR
11 &&
12 ||
13 ?: conditional

Two rows surprise people coming from C. Equality binds tighter than bitwise AND, and shifts bind tighter than addition.

The traps

Bitwise operators among themselves. a & b | c is (a & b) | c; AND binds tighter than XOR, which binds tighter than OR. If you meant a & (b | c) you have to say so, and even when you meant the default, the reader should not have to know the table.

Shift before add. a + b << 2 is a + (b << 2): b times four plus a, which is almost never what was wanted. (a + b) << 2 is what was wanted.

The conditional at the bottom. ?: binds loosest of all, so sel ? a : b + 1 is sel ? a : (b + 1), and x = a & b ? c : d has the whole a & b as its condition. Both happen to be what was probably meant; neither is obvious to the next reader. Inside a concatenation, {sel ? a : b, c}, put the conditional in parentheses.

Equality before AND, the worst one:

wire [7:0] a, b;

wire [7:0] wrong = a & b == 8'hFF;     // a & (b == 8'hFF): a 1-bit compare, zero-extended, ANDed with a
wire       right = (a & b) == 8'hFF;   // what was meant

The comparison gives a single bit; that bit is ANDed with a; the result is either 0 or the bottom bit of a, and the design goes out with a masked equality test that is not one. This one has hidden in shipped designs for months.

The habit

Parenthesise everything that combines two kinds of operator. Parentheses cost nothing, the netlist is identical, and they are documentation:

wire result = ((a & mask) == expected) && (|status_bits) && ~fault;

Experienced RTL engineers do not know the table better than beginners. They do not use it.

Width

Precedence is one way an expression can mean something else. Width is the other, and it is subtler because the rule is right most of the time.

In a plain assignment every operand and the target are all extended to the widest of them before the expression is evaluated. So the target widens the arithmetic, and this is fine:

wire [7:0]  a, b;
wire [8:0]  sum     = a + b;   // computed at 9 bits; the carry is bit 8
wire [15:0] product = a * b;   // computed at 16 bits; the full product

And this truncates:

wire [3:0] p = 4'hF;
wire [7:0] q = 8'h01;
wire [3:0] r = p + q;    // computed at 8 bits, 8'h10, then the top four bits dropped: 4'h0

The rule stops helping where an expression is self-determined, its width fixed by its own operands and nothing outside:

  • the operands of a comparison, which are sized against each other, not against the 1-bit result;
  • anything inside a concatenation;
  • the shift amount in a shift;
  • the condition of a conditional;
  • arguments to functions and system tasks.

The commonest casualty is the overflow check:

wire [7:0] a, b;
wire ovf_never = (a + b) > 8'hFF;                           // the add is 8 bits wide; never true
wire ovf_right = ({1'b0, a} + {1'b0, b}) > 9'd255;          // 9 bits; true when it should be

And the intermediate wire that is narrower than the result it is on the way to:

wire [7:0]  a, b;
wire [7:0]  t = a * b;     // the product is computed at 8 bits and truncated here
wire [15:0] p = t;         // widening afterwards does not bring the bits back

Unsized literals add a third way to be surprised. A bare 0 or 255 is at least 32 bits, and takes the width of its context. Size every literal.

The defence is the same as for carries in the arithmetic article: extend the operands yourself, with a concatenated zero or a {N{sign}}, wherever the width of a result matters. The reader sees it, the tool cannot get it wrong, and the hardware is the same adder either way.

Lint

Verilator with -Wall, and the commercial linters, flag width mismatches, truncation, mixed signed and unsigned operands, and mixed operators without parentheses. Turn those on and a class of silent bugs becomes a class of compile errors. On real projects a clean lint run is a tape-out requirement.

Practice Exercise

Each expression has a bug. Say what Verilog computes and fix it.

wire [7:0] a = 8'h0F;
wire [7:0] b = 8'hF0;
wire [7:0] c = 8'hAA;

wire       check1 = a | b == 8'hFF;      // meant: is (a OR b) equal to 0xFF
wire [7:0] check2 = a ^ b << 2;          // meant: (a XOR b) shifted left by 2
wire       check3 = a + b > 8'hFF;       // meant: does a + b overflow 8 bits
Solution

check1 is a | (b == 8'hFF). The comparison is 0, so the result is a | 0, which is 8'h0F, truncated to the 1-bit target: 1. It looks true and is not the test that was meant. Fix: (a | b) == 8'hFF, which is 1 here for the right reason.

check2 is a ^ (b << 2). b << 2 is 8'hC0, and 8'h0F ^ 8'hC0 is 8'hCF. Fix: (a ^ b) << 2, which is 8'hFF << 2, 8'hFC.

check3 parses as intended, (a + b) > 8'hFF, but the add is computed at 8 bits because the operands of a comparison are self-determined, so it can never exceed 8'hFF and the check is always 0. Fix: ({1'b0, a} + {1'b0, b}) > 9'd255.

That is the whole operator set and the two rules for combining them. The next section puts modules together the way a schematic does, by instantiating parts and wiring them: structural modelling.

โ† The Conditional Operator Gate Primitives โ†’