Welcome to my ongoing series covering mathematics and algorithms with FPGAs. This series begins with the basics of Verilog numbers, then considers fixedpoint, 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.
This post was completely revised in November 2022.
Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)
Series Outline
 Numbers in Verilog (this post)  introduction to numbers in Verilog
 Verilog Vectors and Arrays (coming soon)
 FixedPoint Numbers in Verilog  precision without complexity
 Multiplication with FPGA DSPs  efficient multiplication with DSPs
 More maths in 2023
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 familiar with different representations of numbers we hardly give them a second thought.
Some representations of fortytwo:
101010
(binary)52
(octal)42
(decimal)0x2A
(hexadecimal)4.2x10
^{1} (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 fixedpoint 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: 101010_{2}
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
Fortytwo 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). Packed^{1} BCD uses a nibble (4bit) value to represent each decimal digit.
To get the packed BCD representation, convert each decimal digit into a 4bit 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 binarycoded 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; // 6bit wire
reg [7:0] b; // 8bit reg
logic [11:0] c; // 12bit logic
a
, b
, and c
are vectors:
 Wire a handles 063 inclusive (2^{6} is 64).
 Reg b handles 0255 inclusive (2^{8} is 256).
 Logic c handles 04095 inclusive (2^{12} 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 48bit internal vectors when using 16bit 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
, whereX
is unknown, andZ
is high impedance. For this introduction to numbers, we’re going to ignoreX
andZ
.
We cover vectors in detail in the next part of the series: Verilog Vectors and Arrays (coming soon).
Literals
Verilog has several options for interpreting literal numbers.
A lone unadorned number, such as 42
is interpreted as a signed 32bit decimal:
// signed 32bit 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
// 32bit 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 af
and AF
as well as the usual digits:
// 32bit 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:
// 32bit 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 32bit 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:
// 12bit 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 daytoday.
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 [CORDW1:0] x = {CORDW{1'b1}}; // x = 1111_1111_1111
reg [CORDW1:0] y = { {1'b1}, {CORDW1{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 Nbit number add up to 2^{N}.
For example, with fourbit values: 7 is 0111
and 7 is 1001
because 0111 + 1001 = 10000
(2^{4}).
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^{(n1)} to +2^{(n1)}1
For example, an 8bit signed vector has the range:
2^{7} to +2^{7}1 = 128 to +127
You can’t represent +128 with an 8bit 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 zero 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
:
m
is 4 bits wide, butu
andy
are 8 bitsm
must be widened to 8 bits to match the widest operandsu
is unsigned, som
is also considered unsigned Being unsigned,
m
is widened to 8 bits with zeros:00001100
(12 in decimal) 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 lefthand side of an assignment is smaller than the righthand 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 32bit 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 righthand 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, subtraction, multiplication, and division.
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 1011_{2} in binary.
z
has the value 1011_{2}: z = 11
as expected.
If we set x = 11
and y = 7
, you might expect the result to be (x + y) = 18
or 10010_{2} 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 0010_{2}, 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 10010_{2}. The value of 1 gets assigned to c
while 0010_{2} 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 lowpower 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. In a later post, we’ll look into division in detail, 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 Verilog Vectors and Arrays (due late November 2022). Until then, check out our other maths posts: division, square root, and sine & cosine.
Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)

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