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 fortytwo: XLII, 101010_{2}, 42, 0x2A, 52_{8}, 4.2x10^{1}, 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 tradeoffs. Do I need signed numbers? Will BCD make my design simpler? Is fixedpoint 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: 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 this is not the only possible representation: 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 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; // 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).
 Register 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 16bits 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
, whereX
is unknown, andZ
is high impedance. For the purposes of this introduction to numbers, we’re going to ignoreX
andZ
.
Slicing Vectors
You can select an individual vector bit with its index; for example:
wire [3:0] n; // 4bit wire vector
wire p, q; // wire scalars
always_comb begin
p = n[0];
q = n[3];
end
You select a subset by specifying the start and end bits:
wire [11:0] a; // 12bit wire vector
wire [3:0] x, y, z; // 4bit wire vectors
always_comb begin
x = a[11:8];
y = a[7:4];
z = a[3:0];
end
You can also use the concat operator {}
to select bits from vectors:
always_comb begin
{x,y,z} = a;
end
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
end
ProTip: The start bit can be a variable, but not the width.
Configurable Widths
Avoid hardcoding 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 [ADDRW1:0] addr_read;
logic [ADDRW1: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 2^{3} < 10 ≤ 2^{4}.
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_BITW1: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 [VOLTW1:0] volatage; // we can't handle 256!
$clog2
returns ‘8’, giving a voltage range of 0255 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 [VOLTW1: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 4bit wide, decimal value 9
4'b1001 // binary literal
4'd9 // decimal literal
4'h9 // hexadecimal literal
4'o11 // octal literal
// unsigned 12bit 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 32bits
 If the base is omitted, it defaults to decimal (base 10)
Here are some examples:
// 32bit 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 [COORDW1:0] x = {COORDW{1'b1}}; // x = 1111_1111_1111
reg [COORDW1:0] y = { {1'b1}, {COORDW1{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 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}).
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
BigEndian, LittleEndian
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 littleendian (LE) vectors:
wire [5:0] a; // 6bit wire (littleendian)
reg [11:0] b; // 12bit reg (littleendian)
Alternatively, we can declare vectors like this:
wire [0:5] a; // 6bit wire (bigendian)
reg [0:11] b; // 12bit reg (bigendian)
These vectors have the most significant bit at the lowest index; they’re bigendian (BE) vectors.
Either will work, but the convention is to use littleendian unless you need to interface with bigendian hardware. For example, I^{2}C uses bigendian bit order.
Switching Ends
Say you’ve got a bitendian byte from I^{2}C and want to convert it to littleendian. Alas, you can’t mix big and littleendian vectors, so the following won’t work:
wire [0:7] i2c_byte; // 8bit wire (bigendian)
reg [7:0] le_byte; // 8bit reg (littleendian)
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];
end
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[7i];
end
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 32bit word, do you store the least significant byte at the lowest address (littleendian) or the most significant byte at the lowest address (bigendian)?
RISCV, x86, and ARM are littleendian, while Internet protocols (TCP/IP) and Motorola 68K are bigendian. 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)

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