Skip to article

Modules, Signals, and Numbers

Numbers in Hardware

How Verilog vector widths and number literals work — sized and unsized, binary/octal/decimal/hex — and why a fixed-width addition can overflow and truncate silently.

5 min read · Updated


In Verilog, a signal can be a single bit or a vector of bits. [3:0] declares four bits, numbered 3 down to 0, with bit 3 as the most significant bit (MSB) and bit 0 as the least significant. Hex is just a compact way to write a vector: 4'hD means a 4-bit value equal to 4'b1101, which is 13 in decimal.

The important habit for hardware is that width is fixed. A wire that is four bits wide can only hold four bits — there is no automatic growth. When you add two 4-bit numbers, the result can need five bits (the extra bit is the carry-out). If the output you assign the sum to is only four bits wide, the top bit is dropped and the value truncates. Nothing warns you at run time; the simulation simply shows the low bits.

For example, 4'hF + 4'h2 is 15 + 2 = 17. Seventeen needs five bits (5'b10001). Stored in a 4-bit output, only 0001 (decimal 1) remains. To keep the full result, widen the output — a [4:0] output holds all five bits, carry included. Choosing the right width for every signal is one of the first real hardware-design decisions you make.

Literal syntax

A sized literal has the form <width>'<base><digits> — the width in bits, an apostrophe, a base letter (b binary, o octal, d decimal, h hex), then the digits in that base:

Literal Width Base Value
8'b0000_1010 8 bits (declared) binary 10
8'hA 8 bits (declared) hex 10
8'o12 8 bits (declared) octal 10
10 unsized — see below decimal 10

The underscore in 8'b0000_1010 is a pure readability separator; it carries no value and may be placed between any two digits.

A literal with no size and no base, like the plain 10 above, is an unsized decimal literal. It is not sizeless: the language requires it to have a default width of its own — implementation-defined, but at least 32 bits — the same as any other operand. That default width then participates in ordinary Verilog width rules: when the literal is used inside a context that combines it with a narrower or wider operand (an 8-bit assignment target, for example), it is extended or truncated exactly the way any other differently-sized operand would be. It does not somehow start out at 8 bits just because you happened to assign it to an 8-bit register.

A literal can also contain the unknown digit x or the high-impedance digit z directly, most often seen in a supplied checker or a "don't care" pattern: 4'b10xz is a 4-bit literal whose top two bits are known and whose bottom two are unknown/high-Z. Four-State Logic covers what x and z mean during simulation.

Predict before running

Before running it, predict what the unsized literal 10 displays as once it is assigned into an 8-bit register — and separately, how Icarus prints the bare literal with no narrowing target at all.

module tb;
  reg [7:0] a, b, c, d, e;

  initial begin
    a = 8'b0000_1010;
    b = 8'hA;
    c = 8'o12;
    d = 10;
    e = 4'bxz01;

    $display("a (bin)   = %b = %0d", a, a);
    $display("b (hex)   = %b = %0d", b, b);
    $display("c (oct)   = %b = %0d", c, c);
    $display("d (unsized dec, assigned into 8 bits) = %b = %0d", d, d);
    $display("e (literal with x/z) = %b", e);
    $display("10 printed with no narrowing context: %b", 10);

    $display("PASS");
    $finish;
  end
endmodule
Expected output — reveal after you predict
a (bin)   = 00001010 = 10
b (hex)   = 00001010 = 10
c (oct)   = 00001010 = 10
d (unsized dec, assigned into 8 bits) = 00001010 = 10
e (literal with x/z) = 0000xz01
10 printed with no narrowing context: 00000000000000000000000000001010
PASS

All three sized forms of "ten" produce the identical 8-bit pattern once declared at 8 bits — the base only changes how you write the literal, not what value results. d also displays as 8 bits, but that is the assignment context narrowing it, not evidence that 10 was 8 bits to begin with. Icarus prints the bare literal as 32 bits here; the Verilog standard permits an implementation to choose a width of 32 bits or more, so portable RTL should use an explicitly sized literal rather than depend on an implementation's chosen width. e's 4-bit literal zero-extends into the 8-bit register, keeping its x/z digits exactly where they were.

Common mistakes

  • Assuming an unsized literal takes its width entirely from wherever it happens to be used, with no size of its own. It doesn't — it defaults to an implementation-defined width of at least 32 bits (32 on this tutorial's simulator), and that value is then extended or truncated by the surrounding context like any other operand. The practical guidance either way is the same: prefer explicitly sized literals in RTL so you never depend on this rule. The full expression-width rules for mixed-width and mixed-signedness operands are covered in a dedicated article, not here.
  • Forgetting the base letter is case-insensitive but the digits still have to match the base. 8'hA and 8'ha are identical; 8'b2 is a compile error because 2 is not a binary digit.
  • Losing the carry bit on a fixed-width sum, as in the truncation example above — always check whether the destination width covers the arithmetic result.

Sources and verification

Example provenance: SkillLift Labs authored the example for this tutorial. tests/scripts/tutorial-verified-examples.test.ts extracts the complete marked example from this Markdown file, compiles it in Verilog-2005 mode with Icarus Verilog, runs it, and requires the simulator output to match the Expected output block exactly. The same test also carries an independent, code-owned semantic oracle, and exercises the article-to-runner source-splitting path. Automation verifies those stated properties; human technical review for indexing remains a separate gate.