Project F

Numbers in Verilog

Published · Updated

Welcome to my ongoing series covering mathematics and algorithms with FPGAs. This series begins with the basics of Verilog numbers, then considers fixed-point, division, square roots and CORDIC before covering more complex algorithms, such as data compression.

In this first post, we consider integers, dig into the challenges of signed numbers and expressions, and then finish with a bit of arithmetic.

Share your thoughts with @WillFlux on Mastodon or Twitter. If you like what I do, sponsor me. πŸ™

Series Outline

Representing Numbers

We’re so familiar with different representations of numbers we hardly give them a second thought.

Some representations of forty-two:

  • 101010 (binary)
  • 52 (octal)
  • 42 (decimal)
  • 0x2A (hexadecimal)
  • 4.2x101 (scientific notation)
  • XLII (Roman numerals)
  • ε››δΊŒ (Japanese numerals)
  • zweiundvierzig (German)

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.

As a hardware designer, you need to consider how you represent numbers. How many bits do I need? Do I need signed numbers? Will BCD make my design simpler? Is fixed-point accurate enough? What happens when I mix different widths in one expression?

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 there are other possible representations: 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 reg 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).
  • Reg 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-bit coordinates.

It’s easy to miss the width from a signal declaration 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 this introduction to numbers, we’re going to ignore X and Z.

We cover vectors in detail in the next part of the series: Vectors and Arrays.

Literals

Verilog has several options for interpreting literal numbers.

A lone unadorned number, such as 42 is interpreted as a signed 32-bit decimal:

// signed 32-bit decimal literals
42
123
7
65536

The default interpretation seems reasonable, but sometimes it’s easier to work in another base, and Verilog gives you the option of binary, octal and hexadecimal as well as decimal.

You specify the base (radix) using a single quote followed by a letter:

  • b - binary
  • o - octal
  • d - decimal
  • h - hexadecimal
// 32-bit wide literals with decimal value 9
'b1001  // unsigned binary
'd9     // unsigned decimal
'h9     // unsigned hexadecimal
'o11    // unsigned octal

With hexadecimal, you can use the letters a-f and A-F as well as the usual digits:

// 32-bit wide hexadecimal literals
'hA       // unsigned 10 in decimal
'h3C      // unsigned 60 in decimal
'hfa      // unsigned 250 in decimal
'hFFCC00  // bright yellow hexadecimal colour

Unlike the default decimal interpretation, these literals are unsigned, even with a decimal base.

If you want a signed literal with a base, you need to add an s before the base:

// 32-bit wide with decimal value 9
'sb1001  // signed binary
'sd9     // signed decimal
'sh9     // signed hexadecimal
'so11    // signed octal

You can use a negative sign to create a negative literal. Stick with decimal for literals with minus signs: it quickly gets confusing in other bases.

// signed 32-bit wide decimal literals
-42
-1
-65536
-25_000_000  // negative 25 million (underscores for readability)

We’ll be covering signed numbers in detail in the next section.

You specify a literal width in bits by putting it before the single quote:

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

ProTip: You can include underscores in your literals to improve readability.

Zero Fill

What happens if you only specify some of the bits in a literal?

8'b00001111;  // unsigned 00001111 (decimal 15)
8'b1111;      // unsigned 00001111 (decimal 15)

Verilog filled the remaining bits of 8'b1111 with zero, which is what you might expect and is how numbers usually work day-to-day.

It doesn’t matter if the literal is signed or another base; the value is always filled with zeros:

8'hA;         // unsigned 00001010 (decimal 10)
8'sb1010;     // signed   00001010 (decimal 10)

In case you’re wondering about sign extension, fear not we come to that later in this post.

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 in SystemVerilog and Verilog:

localparam CORDW = 12;  // coordinate width in bits

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

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

You can nest concat operators, as in the final example above.

Concat allows us to set an appropriate value regardless of the vector width.

Signed Numbers

If your design requires negative values, you need to handle signed numbers. The standard approach is two’s complement, as with almost all CPUs and software.

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

Discarding the extra bit, the result of adding a number and its two’s complement is always zero.

You can switch the sign of a two’s complement number by inverting the bits and adding 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; Verilog can handle it for you.

Let’s look at a few 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

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

Range

The range of a two’s complement vector of width n is:

-2(n-1) to +2(n-1)-1

For example, an 8-bit signed vector has the range:

-27 to +27-1 = -128 to +127

You can’t represent +128 with an 8-bit signed vector.

Try to take the two’s complement of -128:

Start:      1000_0000   (decimal -128)
Invert:     0111_1111
Add 1:      0000_0001
Result:     1000_0000   (decimal -128)

You get the original -128 back.

Signing Your Signals

Declaring a vector as signed is easy:

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

For signed literals, as we discussed above, you 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)

Test Negative

It’s straightforward to check the sign of a number with two’s complement:

  • For positive numbers (and zero), the most significant bit is 0
  • For negative numbers, the most significant bit is 1

You can check the most significant bit directly:

reg signed [7:0] s;  // 8 bit signed vector

always_ff @(posedge clk) begin
    if (s[7]) begin  // ?! intent unclear
        // s is negative
    end else begin
        // s is positive or zero
    end
end

However, comparison operators are usually clearer:

reg signed [7:0] s;  // 8 bit signed vector

always_ff @(posedge clk) begin
    if (s < 0) begin
        // s is negative
    end else begin
        // s is positive or zero
    end
end

Signed Expressions

Now we know how to handle signed vectors and literals, it’s time to wrestle with expressions. I’ve done my best to accurately distil a rather dry and complex subject into something palatable, so I hope I don’t offend the language lawyers and bore everyone else.

An expression consists of operands, such as variables and literals, and operators, such as addition and assignment.

Verilog uses the width of the widest operand when evaluating an expression.

It doesn’t matter what the operators are; all Verilog cares about is the width of the operands.

Narrower operands are widened until they’re the same width as the widest. For unsigned operands, Verilog simply fills the new bits with zero, but with signed operands, it uses sign extension.

Sign extension copies the most significant bit (MSB) to fill the width. Remember, for a signed number, the MSB is 1 for negative numbers and 0 otherwise.

This doesn’t sound too bad until you learn that in Verilog:

If all the operands are signed, the result is signed. Otherwise, it’s unsigned.

Verilog doesn’t consider it an error to mix signed and unsigned operands; it treats them all as unsigned. This leads to painful surprises that can be hard to debug.

Take a look at this example:

module wider_tb ();
    logic signed [7:0] x, y;  // signed 8 bits wide
    logic signed [7:0] s;  // signed 8 bits wide
    logic        [7:0] u;  // unsigned 8 bits wide
    logic signed [3:0] m;  // signed 4 bits wide

    always_comb begin
        x = s + m;  // signed + signed
        y = u + m;  // unsigned + signed
    end
    initial begin
        #10
        s = 8'sb0000_0111;  // decimal 7
        u = 8'b0000_0111;   // decimal 7
        #10
        $display(" s: %b  %d", s, s);
        $display(" u: %b  %d", u, u);


        #10
        m = 4;  // decimal 4
        $display("When 'm' is +4:");
        $display("m:     %b  %d", m, m);
        $display("x: %b  %d", x, x);
        $display("y: %b  %d", y, y);

        #10
        m = -4;  // decimal -4
        $display("When 'm' is -4:");
        $display("m:     %b  %d", m, m);
        $display("x: %b  %d", x, x);
        $display("y: %b  %d  **SURPRISE**", y, y);
    end
endmodule

Running the wider test bench:

s: 00000111     7
u: 00000111     7

When 'm' is +4:
m:     0100     4
x: 00001011    11 
y: 00001011    11

When 'm' is -4:
m:     1100    -4
x: 00000011     3
y: 00010011    19  **SURPRISE**

Looking at the binary, we can understand where 19 comes from.

When we set m = -4 it has binary value 1100.

When Verilog evaluates the expression y = u + m:

  1. m is 4 bits wide, but u and y are 8 bits
  2. m must be widened to 8 bits to match the widest operands
  3. u is unsigned, so m is also considered unsigned
  4. Treated as unsigned, m is widened to 8 bits with zeros: 00001100 (12 in decimal)
  5. 00001100 + 00000111 = 00010011 (12+7=19 in decimal)

If you take one thing away from this post:

Never mix signed and unsigned variables in one expression!

Truncated

If the left-hand side of an assignment is smaller than the right-hand side, then the value is truncated. The following examples use literals, but this also applies to expressions and variables.

wire [3:0] a = 15;           // a gets the value 1111
wire [3:0] b = 8'b10001010;  // b gets the value 1010
wire [3:0] c = 12'hF0F;      // c gets the value 1111

Because 15 has no base, Verilog treats it as a signed 32-bit decimal:

0000_0000_0000_0000_0000_0000_0000_1111

For a, the truncated bits are all zero, so don’t change the value.

If the right-hand side is signed, truncating it may change its value and sign:

wire signed [3:0] d = 8'b11111100;  // d gets the value 1100 (-4)
wire signed [3:0] e = 8'b11110011;  // e gets the value 0011 (-13 becomes +3)

We assigned -13 to e, but after truncation, we get +3.

Your synthesis tool or simulator should warn you about truncated values.

Reckoning with Arithmetic

We’ve talked a lot about the representation of numbers, but we’ve yet to do much maths. Let’s finish this post by having a quick look at the elementary arithmetic operations.

Addition

Modern FPGAs include dedicated logic, such as carry chains, to handle addition efficiently: there’s no need to roll your own design. However, there are plenty of university slide decks online if you want to create an adder from scratch.

Before we skip over addition entirely, it’s worth having a quick look at overflow.

Consider the following:

wire [3:0] x, y, z;

always_comb z = x + y;

The signals x y z are unsigned and 4 bits wide.

If we set x = 2 and y = 9 then (x + y) = 11 or 10112 in binary.

z has the value 10112: z = 11 as expected.

If we set x = 11 and y = 7, you might expect the result to be (x + y) = 18 or 100102 in binary, which is then truncated to fit in z.

However, Verilog uses the width of the widest operand to evaluate expressions. The operands in our case are the three variables x, y, and z, all of which are 4 bits wide. Thus the result of the expression is 00102, and it’s assigned to z.

Modular arithmatic, where the result wraps around, is the norm in hardware and software.

Catching the Overflow

However, there are times when you want to know if overflow has occurred:

wire [3:0] x, y, z;
wire c;  // carry bit

always_comb {c, z} = x + y;

The widest operand in our new expression is {c, z}, which is 5 bits wide, so when we evaluate x + y, we get 100102. The value of 1 gets assigned to c while 00102 is assigned to z as before.

You could use the carry bit to set an overflow flag when designing a CPU.

Another potential use is with saturation arithmetic: we keep z at its maximum value when overflow occurs:

wire [3:0] x, y, z;
wire c;  // carry bit

always_comb begin
    {c, z} = x + y;
    if (c) z = 4'b1111;  // 15: the maximum value z can hold
end

With this saturation design, if x = 11 and y = 7, then z = 15.

Learn more from saturation arithmetic on Wikipedia.

Subtraction

Subtraction is almost the same as addition: subtracting y is equivalent to adding -y. It’s easy to find -y, as we saw earlier when discussing two’s complement.

Consider the following:

wire [3:0] x, y, z;

always_comb z = x - y;

If we set x = 11 and y = 7:

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

Add x and -y:
      1011    +11  (x)
    + 1001    - 7  (y)
    = 0100    + 4  (z)

Your synthesis tool will handle this, but remember that subtraction is a little more complex than addition. Prefer incrementing over decrementing and register the results of subtractions before using them in subsequent calculations, especially on low-power FPGAs such as the iCE40UP.

Multiplication

Multiplication is more complex than addition or subtraction, but modern FPGAs can handle it with dedicated DSP blocks. Small vectors don’t require too much thought, but the resulting output has potentially twice as many bits as the inputs:

wire [3:0] x;
wire [3:0] y;
wire [7:0] z;  // product (twice as wide)

always_comb z = x * y;

If we set x = 11 and y = 7:

Multiply x and y:
          1011  +11  (x)
    x     0111  + 7  (y)
    = 01001101  +77  (z)

I’ve written a dedicated post on Multiplication with FPGA DSPs that looks at the use of reg and pipelining to improve multiplication performance and minimise logic use.

Division

What about division? I’ve bad news: Verilog won’t synthesise this for you. The good news is that it’s not hard to implement yourself: I have a dedicated post on Division in Verilog that covers integers, fixed-point, and signed numbers.

What’s Next?

In the second part, we look at Vectors and Arrays or jump ahead to Fixed-Point Numbers.


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