30 September 2021

Numbers in Verilog

Welcome to my new series covering mathematics and algorithms with FPGAs. Whatever hardware you’re designing, you’re likely to be working with numbers. This series begins with the basics of Verilog numbers, covers simple mathematics, including division and CORDIC, before looking at more complex algorithms, such as data compression. This post was last updated in February 2022.

In this first post, we examine how integers (whole numbers) are represented and dig into the challenges of signed numbers in Verilog.

Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)

Sponsor My Work
If you like what I do, consider sponsoring me on GitHub.
I love FPGAs and want to help more people discover and use them in their projects.
My hardware designs are open source, and my blog is advert free.

Representing Numbers

We’re so familair with different representations of numbers we hardly give them a second thought. For example, the following are all forty-two: XLII, 1010102, 42, 0x2A, 528, 4.2x101, zweiundvierzig.

Different representations express (almost) the same thing but work better (or worse) in different circumstances: hexadecimal is suitable for a memory address, while scientific notation compactly expresses vast and tiny numbers alike.

Hardware designers face similar choices and trade-offs. Do I need signed numbers? Will BCD make my design simpler? Is fixed-point accurate enough, or must I use floating point?

Cistercian Numerals
For something a bit less ordinary, try Cistercian numerals (Wikipedia).


Computers famously “think” in binary, and the same is true for most electronics. Off and on, high and low. Simple, right?

For positive integers, things are pretty straightforward.

Let’s take a look at 42 in binary: 1010102

32   16    8    4    2    1
 ^    ^    ^    ^    ^    ^
 1    0    1    0    1    0

Each binary digit is twice the previous one: 1, 2, 4, 8, 16, 32…

32 + 8 + 2 = 42

Forty-two requires at least six binary digits to represent in this way.

Binary Coded Decimal

But this is not the only possible representation: some systems use binary coded decimal (BCD). Packed1 BCD uses a nibble (4-bit) value to represent each decimal digit.

To get the packed BCD representation, convert each decimal digit into a 4-bit binary value:

Decimal          4    2
BCD           0100 0010

Decimal     1    0    1
BCD      0001 0000 0001

Decimal     9    8    7
BCD      1001 1000 0111

The BCD representation requires eight bits to represent 42, two more than the plain old binary version. However, there are advantages, including the ease of display (each nibble is one character) and the ability to accurately represent decimal numbers (such as 10.1).

On typical binary computers, BCD adds overhead for arithmetic operations. However, when designing your own hardware, you can support BCD directly in logic, making it an attractive option for straightforward numerical designs.

Wikipedia’s binary-coded decimal article covers different BCD variants and sign encoding.

Interesting as BCD is, we’ll be sticking with plain old binary for the rest of this document. Many of the concepts we’ll cover also apply to BCD.

Binary in Verilog

By default, a Verilog register or wire is 1 bit wide. This is a scalar:

wire  x;  // 1 bit wire
reg   y;  // also 1 bit
logic z;  // me too!

A scalar can only hold 0 or 1 (but see Four State Data Types, below).

We need a vector to hold values other than 0 and 1.

A vector is declared like this: type [upper:lower] name;

wire   [5:0] a;  // 6-bit wire
reg    [7:0] b;  // 8-bit reg
logic [11:0] c;  // 12-bit logic

a, b, and c are vectors:

  • Wire a handles 0-63 inclusive (26 is 64).
  • Register b handles 0-255 inclusive (28 is 256).
  • Logic c handles 0-4095 inclusive (212 is 4096).

You need to ensure your vector is large enough to handle the full range of values your design requires. Synthesis tools are good at discarding unused bits, so it’s better to err on the side of too large rather than too small.

Deciding on the appropriate vector width for an algorithm requires an understanding of that algorithm. For example, I’ve worked with an ellipse drawing algorithm that required 48-bit internal vectors when using 16-bits coordinates.

It’s easy to miss the width off a signal and create a scalar by mistake:

wire [11:0] x;  // 12 bit (vector)
wire x1;        // 1 bit (scalar)

always_comb x1 = x;  // discards 11 bits!

Alas, many tools provide no warning on width mismatches. To catch issues with bit widths, I strongly recommend you lint your designs with Verilator.

Four State Data Types
The logic, reg, and wire data types can take one of four values: 0, 1, X, Z, where X is unknown, and Z is high impedance. For the purposes of this introduction to numbers, we’re going to ignore X and Z.

Slicing Vectors

You can select an individual vector bit with its index; for example:

wire [3:0] n;  // 4-bit wire vector
wire p, q;     // wire scalars

always_comb begin
    p = n[0];
    q = n[3];

You select a subset by specifying the start and end bits:

wire [11:0] a;       // 12-bit wire vector
wire [3:0] x, y, z;  // 4-bit wire vectors

always_comb begin
    x = a[11:8];
    y = a[7:4];
    z = a[3:0];

You can also use the concat operator {} to select bits from vectors:

always_comb begin
    {x,y,z} = a;

Rather than specify an end bit, you can specify a width.

These three assignments all select the same four bits:

always_comb begin
    x = a[11:8];   // 11:8
    y = a[11-:4];  // also 11:8
    z = a[8+:4];   // also 11:8

ProTip: The start bit can be a variable, but not the width.

Configurable Widths

Avoid hard-coding bit widths; it limits your design flexibility.

Parameters provide a simple way to configure bit widths:

parameter ADDRW=16;  // address width: 16 bits for 2^16 memory locations

logic [ADDRW-1:0] addr_read;
logic [ADDRW-1:0] addr_write;

The width of a vector often depends on another parameter, so calculating it yourself isn’t ideal.

Imagine you’re creating a game engine where the number of sprites is configurable:

parameter SPR_CNT=10;   // maximum number of sprites on screen
logic [3:0] sprite_id;  // 4 bits is correct for a count of 10, but if SPR_CNT changes?

If we hardcode the width, changing the sprite count will break the design.

Verilog 2005 introduced $clog2 to handle this.

Calculating Widths

The $clog2 function returns the ceiling of the logarithm to base 2.

For example, $clog2(10) = 4 because 23 < 10 ≤ 24.

If you need to handle N things (such as sprites or memory locations), then $clog2(N) will tell you how wide your vector needs to be:

parameter SPR_CNT=10;  // maximum number of sprites on screen
parameter SPR_BITW=$clog2(SPR_CNT);  // sprite ID bit width

logic [SPR_BITW-1:0] sprite_id;  // sprite identifier

$clog2 is handy, but you need to be careful.

If you’re specifying a maximum value (rather than a count), it doesn’t do what you want:

parameter MAX_VOLTAGE=256;  // maximum voltage
parameter VOLTW=$clog2(MAX_VOLTAGE);  // voltage bit width (INCORRECT!)

logic [VOLTW-1:0] volatage;  // we can't handle 256!

$clog2 returns ‘8’, giving a voltage range of 0-255 inclusive. 256 is out of range.

If you’re specifying a maximum value, you need to add one to the value passed to $clog2:

parameter MAX_VOLTAGE=256;  // maximum voltage
parameter VOLTW=$clog2(MAX_VOLTAGE+1);  // voltage bit width (add one for max)

logic [VOLTW-1:0] volatage;  // we can now handle 256 volts :)

This problem is often hidden because it doesn’t occur if your parameter isn’t a power of 2. For example, if you specify ‘240’ as your MAX_VOLTAGE, you won’t see any issues. Later, you increase MAX_VOLTAGE to ‘256’, and the design has a subtle bug.


Verilog gives you several options for representing literal numbers. You can use binary, decimal, octal, or hexadecimal literals and specify their width in bits.

I think this is easiest to understand with some examples:

// unsigned 4-bit wide, decimal value 9
4'b1001             // binary literal
4'd9                // decimal literal
4'h9                // hexadecimal literal
4'o11               // octal literal

// unsigned 12-bit wide, decimal value 1024
12'b0100_0000_0000  // binary literal (underscores for readability)
12'd1024            // decimal literal
12'h400             // hex literal
12'o2000            // octal literal

It’s good practice to include the width and base for any literal, but they are optional.

  • If the width is omitted, it defaults to 32-bits
  • If the base is omitted, it defaults to decimal (base 10)

Here are some examples:

// 32-bit wide
'b1001  // unsigned binary literal
'd9     // unsigned decimal literal
9       // signed decimal literal
1024    // signed decimal literal

NB. If you omit the base, the literal is signed! This isn’t a problem most of the time, but it can lead to subtle bugs. We’ll cover signed numbers in the next section.

Literal Tricks

In SystemVerilog, you can set all the bits of a vector to ‘1’:

// x and y have the same value:
reg [11:0] x = '1;
reg [11:0] y = 12'b1111_1111_1111;

You can also use the concat operator {} to set specific patterns:

localparam COORDW = 12;  // coordinate width in bits

reg [COORDW-1:0] x = {COORDW{1'b1}};  // x = 1111_1111_1111

reg [COORDW-1:0] y = { {1'b1}, {COORDW-1{1'b0}} };  // y = 1000_0000_0000

Concat allows us to set an appropriate value, however wide the vectors are.

Signed Numbers

If your application requires negative values, you need to handle signed numbers. The standard approach is two’s complement, as with most software development.

The Two’s Complement

With two’s complement, addition, subtraction, and multiplication all work as they do with positive binary numbers. But what is the two’s complement? The positive and negative two’s complement representations of an N-bit number add up to 2N.

For example, with four-bit values: 7 is 0111 and -7 is 1001, because 0111 + 1001 = 10000 (24).

However, the usual way to switch the sign of a number is to invert the bits and add one:

Start:  0111    (decimal +7)
Invert: 1000
Add 1:  0001
Result: 1001    (decimal -7)

Start:  1001    (decimal -7)
Invert: 0110
Add 1:  0001
Result: 0111    (decimal +7)

You rarely need to determine the two’s complement yourself; Verilog can handle it for you.

Let’s look at a few simple additions to confirm things work as expected:

  0110      +6
+ 1101      -3
= 0011      +3

  1001      -7
+ 0011      +3
= 1100      -4

  1001      -7
+ 0111      +7
= 0000       0

The most significant bit of a signed vector is always 1 for a negative number, so it’s easy to determine if a value is negative: we get the main benefit of a sign bit without the downsides.

To learn more, check out the Wikipedia article on two’s complement.

Signing Your Signals

Telling Verilog that your vector is signed is easy:

reg        [7:0] u;  // unsigned (0..255)
reg signed [7:0] s;  // signed   (-128..127)

For literals, you can add the s prefix to the base:

reg signed [7:0] x = -8'sd55;  // x position: -55 (signed)
reg signed [7:0] y =  8'sd32;  // y position: +32 (signed)

Beware Signed Logic!

Verilog has a nasty habit of treating things as unsigned unless all variables in an expression are signed. But you can use the $signed operator to force a variable to be signed if required.

So, if you take one thing away from this post:

Never mix and match signed and unsigned variables!

The following testbench shows what can happen when you mix signed and unsigned variables:

module signed_tb ();
    logic [7:0] x, y;
    logic signed [7:0] x1, y1;
    logic signed [3:0] offset;

    always_comb begin
        x1 = x + offset;  // AVOID! This probably won't work as expected!
        y1 = $signed(y) + offset;  // ensure y is treated as signed

    initial begin
        $display("Coordinates (7,7):");
        x = 8'd7;
        y = 8'd7;
        $display("x : %b  %d", x, x);
        $display("y : %b  %d", y, y);

        $display("With offset +4:");
        offset = 4'sd4;
        $display("x1: %b  %d", x1, x1);
        $display("y1: %b  %d", y1, y1);

        $display("With offset -4:");
        offset = -4'sd4;
        $display("x1: %b  %d  *SURPRISE*", x1, x1);
        $display("y1: %b  %d", y1, y1);

Running this test bench gives the following output in Vivado:

Coordinates (7,7):
x : 00000111    7
y : 00000111    7
With offset +4:
x1: 00001011    11
y1: 00001011    11
With offset -4:
x1: 00010011    19  *SURPRISE*
y1: 00000011     3

Big-Endian, Little-Endian

Earlier, we said a vector was declared like this: type [upper:lower] name;

A more general definition is: type [msb_index:lsb_index] name;
Where msb_index is the most significant bit index and lsb_index is the least.

The usual way of declaring vectors has the least significant bit at the lowest index. These are little-endian (LE) vectors:

wire   [5:0] a;  // 6-bit wire (little-endian)
reg   [11:0] b;  // 12-bit reg (little-endian)

Alternatively, we can declare vectors like this:

wire   [0:5] a;  // 6-bit wire (big-endian)
reg   [0:11] b;  // 12-bit reg (big-endian)

These vectors have the most significant bit at the lowest index; they’re big-endian (BE) vectors.

Either will work, but the convention is to use little-endian unless you need to interface with big-endian hardware. For example, I2C uses big-endian bit order.

Switching Ends

Say you’ve got a bit-endian byte from I2C and want to convert it to little-endian. Alas, you can’t mix big and little-endian vectors, so the following won’t work:

wire [0:7] i2c_byte;  // 8-bit wire (big-endian)
reg  [7:0] le_byte;   // 8-bit reg (little-endian)

always_ff @(posedge clk) le_byte <= i2c_byte;  // Won't work :(

Instead you need to reverse the bits explicitly. All bits are swapped in parallel:

always_ff @(posedge clk) begin
    le_byte[0] <= i2c_byte[7];
    le_byte[1] <= i2c_byte[6];
    le_byte[2] <= i2c_byte[5];
    le_byte[3] <= i2c_byte[4];
    le_byte[4] <= i2c_byte[3];
    le_byte[5] <= i2c_byte[2];
    le_byte[6] <= i2c_byte[1];
    le_byte[7] <= i2c_byte[0];

Updating each individual bit is tedious, but a for loop can handle this for us:

always_ff @(posedge clk) begin
    for (i=0; i<8; i=i+1) le_byte[i] <= i2c_byte[7-i];

Verilog for is NOT like a software loop: this for loop is unrolled into parallel bit swaps.

Byte Endian

So far, we’ve been talking about endianness at the bit level, but it most commonly occurs in the context of bytes. If you have a 32-bit word, do you store the least significant byte at the lowest address (little-endian) or the most significant byte at the lowest address (big-endian)?

RISC-V, x86, and ARM are little-endian, while Internet protocols (TCP/IP) and Motorola 68K are big-endian. This isn’t particularly relevant to what we’re looking at, so I won’t go into this further. If you’re interested in learning more, check out Endianness on Wikipedia.

Reckoning with Arithmetic

We’ve talked a lot about the representation of numbers, but we’ve not done any maths.

Modern FPGAs include logic to handle addition and subtraction: there’s no need to roll your own. If you want to create an adder from scratch, there are plenty of university slide decks online. Unless you’re working with massive vectors, you’re unlikely to encounter problems with addition or subtraction. The usual advice to register your values between calculation steps applies, especially to subtraction on simpler FPGAs.

Multiplication is more complex, but FPGAs can handle it with dedicated DSP blocks. We’ll be covering multiplication in the next part of this series: Multiplication with FPGA DSPs.

What about division? I’ve got some bad news for you: Verilog won’t do this for you. We’ll look into division in detail in a later post, but for now, you can use the division design from the Project F cookbook.

Next Time

In the second part of this series, we look at Multiplication with FPGA DSPs.

You can also check out our other maths posts: division, square root, and sine & cosine.

Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)

  1. Unpacked BCD uses a whole byte (8-bits) for each decimal digit. ↩︎

©2022 Will Green, Project F