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.

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

Updated 2021-10-26. Share your thoughts with @WillFlux or find me on 1BitSquared Discord.

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).

Binary

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.

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
logic [11:0] b;  // 12-bit logic

Both a and b are vectors:

  • Wire a handles 0-63 inclusive (26 is 64).
  • Logic b 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 the appropriate size for an algorithm requires an understanding of that algorithm. For example, I’ve worked with an ellipse drawing algorithm that required much wider internal vectors: if you had 16 bits coordinates, you needed 48 bits for internal error calculations.

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

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

always_comb x1 = x;  // discards 11 bits!

Alas, many tools provide no warning on bit 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.

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, then 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 (NOT RIGHT)

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.

Literals

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 literals, 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! Most of the time, this isn’t a problem, 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. If you’re coming at this fresh, your first instinct might be to use a sign bit. A sign bit can work but requires custom logic for arithmetic operations and leads to the thorny issue of negative zero. For regular binary numbers, the standard approach is two’s complement.

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
    end

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

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

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

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;.

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

These vectors are little-endian because the lower bit is the smallest.

Alternatively, we could have declared the vectors like this:

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

These vectors are big-endian because the lower bit is the largest.

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 @(posedge clk) le_byte <= i2c_byte;  // Won't work :(

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

always @(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];
end

Handling the individual bits is tedious, but a for loop can handle it:

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

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

Byte Endian

So far, we’ve been talking about endianness at the bit level, but it also occurs with 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. To learn more, hop over to Wikipedia’s entry on Endianness.

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 when using subtraction on simpler FPGAs.

Multiplication is more complex, but FPGAs handle it with dedicated DSP blocks. Check your datasheet for the number and capabilities of your FPGA DSPs. Small FPGAs have only a few DSPs (for example, iCE40UP5K has eight 16x16-bit DSPs); you can easily use them all with a few multiplications. However, careful design can often avoid the need for multiplication altogether.

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

Stay tuned for the next part of Maths and Algorithms with FPGAs, where we’ll look at real numbers including fixed point. Until then, why not check out our existing maths posts: division, square root, and sine & cosine.

Constructive feedback is always welcome. Get in touch with @WillFlux or open an issue on GitHub.

Sponsor Project F
If you like what I do, consider sponsoring me on GitHub. I use contributions to spend more time creating open-source FPGA designs and tutorials.


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

©2021 Will Green, Project F