Understanding how Verilog's arithmetic operators translate into actual hardware is crucial for writing efficient and synthesizable code. Verilog provides a set of arithmetic operators similar to those found in C programming, categorized as binary (operating on two operands) and unary (operating on a single operand).
What You'll Learn
- Which binary and unary arithmetic operators Verilog provides and the hardware they infer
- How signed and unsigned operands change the result
- How expression bit length decides the width of the result
- Common arithmetic operator mistakes, and how to avoid them
Binary Arithmetic Operators
The core principle for synthesizability is that the operation must map directly to physical hardware components that operate on discrete binary values (0s and 1s). In Verilog, this generally means operations on reg and integer data types are synthesizable.
When you use these operators with reg or integer types, synthesis tools infer standard digital circuits.
| Operator | Description | Synthesis |
|---|---|---|
| + | Performs addition | Adder circuits (e.g., ripple-carry adders, carry-lookahead adders) |
| - | Performs subtraction | Subtractor circuits, often implemented using adders and two's complement for the subtrahend |
| * | Performs multiplication | Depending on the size of the operands, this can be a simple array multiplier or a more complex Wallace tree multiplier |
| / | Performs division | Divider circuits and may use iterative algorithms. Integer division truncates any fractional part towards zero. |
| % | Modulus - returns the remainder of a division | Synthesizes alongside a divider or as part of a specialized remainder circuit. The sign of the result matches the sign of the first operand |
| ** | Raises the first operand to the power of the second | Generally not synthesizable. |
If the second operand (divisor) is zero, the result is x. Hardware implementation must handle this (e.g., a division-by-zero flag).
Important Note on X and Z Values: For any arithmetic operator, if any bit of an operand holds an unknown (x) or high-impedance (z) value, the entire result will be x. While this is a Verilog simulation behavior, in hardware, 'x' and 'z' values often represent undefined states, and their propagation can lead to unexpected synthesis results or warnings. Good design practice involves avoiding 'x' and 'z' states in critical logic.
module arithmetic(input [7:0] data1, data2, output reg [7:0] sum ,diff, product, div, modulus);
always @ (data1, data2) begin
sum = data1 + data2;
diff = data1 - data2;
product = data1 * data2;
div = data1 / data2;
modulus = data1 % data2;
end
endmodule
Unary Arithmetic Operators
| Operator | Description | Synthesis |
|---|---|---|
| +m | Essentially m, it has no effect on the operand's value | This is trivial and synthesizes directly as the operand itself |
| -m | Performs negation on the operand | For signed integer or signed reg types, this implies a two's complement negation |
The primary distinction for non-synthesizable arithmetic operators lies in the data types they operate on. Verilog includes real and realtime data types, which represent floating-point numbers. Standard hardware synthesis tools generally cannot infer complex floating-point units from basic arithmetic operators when applied to real or realtime operands. Such operations are primarily for simulation and verification purposes, where the simulator can perform the floating-point calculations.
So, all binary and unary arithmetic operators (+, -, *, /, +m, -m) become non-synthesizable if any operand is a real or realtime type.
Important Points to Note
When working with Verilog arithmetic operators, especially for hardware implementation, several nuances must be understood:
Data Type Interpretation (Signed vs. Unsigned)
When values are assigned between different types, the bit representation usually stays the same, but the interpretation changes. This can lead to unexpected results if not handled carefully. For example, assigning a negative integer to an unsigned reg will interpret the two's complement bit pattern as a large positive number.
To explicitly handle signedness, use $signed() and $unsigned() system functions.
- reg and net: These are unsigned by default unless explicitly declared with the signed keyword. For example, reg [7:0] my_reg; is unsigned. reg signed [7:0] my_signed_reg; is signed.
- integer: Always treated as signed (two's complement representation)
- real and realtime: floating-point numbers (IEEE 754 double-precision) that can already represent negative values directly -- unlike reg/integer types, they have no separate
signedkeyword variant
Expression Bit Lengths and Sizing
Verilog determines the bit length of an expression based on the operands involved and the context. For binary arithmetic operations (+, -, *, /, %), the result's bit length is typically the maximum bit length of the operands.
Carry/Overflow Truncation: This rule can cause issues. For instance, adding two 8-bit numbers might produce a 9-bit result if there's a carry. If the result is assigned to an 8-bit reg, the carry bit will be lost (truncated)
reg [15:0] a, b, answer; // Declare 16-bit registers
initial begin
a = 16'hFFFF; // Assign 'a' the maximum 16-bit unsigned value
b = 16'h0001; // Assign 'b' the value 1
// The intent is to add 'a' and 'b', which can result in an overflow,
// and then shift the result right by 1 bit to get the carry
answer = (a + b) >> 1;
$display("Result with truncation: answer = %h", answer);
end
Forcing Larger Evaluation: To prevent intermediate expression truncation, you can introduce a wider operand, like adding 0 with the width of an integer (which is typically 32 bits, a common default size for unsized constants).
reg [15:0] a, b, answer; // 16-bit registers
initial begin
a = 16'hFFFF; // max 16-bit value
b = 16'h0001; // value 1
// Problematic evaluation: (a + b) produces a 16-bit interim result,
// losing the carry bit before the shift.
// FFFF + 0001 = 1_0000 (17 bits), but it gets truncated to 0000 (16 bits).
// Then 0000 >> 1 is still 0000.
answer = (a + b) >> 1;
$display("Problematic answer = %h", answer); // Expected: 0000 (due to truncation)
// Forcing Larger Evaluation: Adding '0' makes the expression evaluate using a larger width (e.g., 32 bits, the size of an integer).
// This preserves the carry bit.
// (FFFF + 0001 + 0) becomes (32'h0001_0000)
// (32'h0001_0000) >> 1 = 32'h0000_8000
// When assigned to a 16-bit 'answer', it becomes 16'h8000
answer = (a + b + 0) >> 1;
$display("Corrected answer = %h", answer); // Expected: 8000
end
Division by Zero: As noted, division or modulus by zero results in x. Hardware designers should implement safeguards (e.g., conditional logic) to prevent this from occurring in synthesizable code, as an 'x' in hardware can be difficult to manage.
module division_by_zero_example;
reg [7:0] dividend;
reg [7:0] divisor;
reg [7:0] result_division;
reg [7:0] result_modulus;
initial begin
dividend = 8'd10; // Decimal 10
divisor = 8'd0; // Decimal 0
// Division by zero
result_division = dividend / divisor;
$display("At time %0t: Result of %d / %d = %h", $time, dividend, divisor, result_division);
// Modulus by zero
result_modulus = dividend % divisor;
$display("At time %0t: Result of %d %% %d = %h", $time, dividend, divisor, result_modulus);
divisor = 8'd5; // Change divisor to a non-zero value
result_division = dividend / divisor;
$display("At time %0t: Result of %d / %d = %h", $time, dividend, divisor, result_division);
end
endmodule
xcelium> run At time 0: Result of 10 / 0 = xx At time 0: Result of 10 % 0 = xx At time 0: Result of 10 / 5 = 02 xmsim: *W,RNQUIE: Simulation is complete.
Parameter Arithmetic: Parameters are constants defined at elaboration time. Arithmetic operations involving only parameters and constant numbers are evaluated at compilation/elaboration time, not runtime. This means complex arithmetic can be performed on parameters, and the result will be a constant, which is inherently synthesizable.
module ParameterArithmeticExample;
// Simple parameter declarations
parameter DATA_WIDTH = 8; // Data bus width
parameter NUM_REGS = 16; // Number of registers in an array
parameter CLOCK_FREQ_MHZ = 100; // Clock frequency in MHz
// Parameters defined using arithmetic operations on other parameters and constants
// Example 1: Subtracting a constant from a parameter
parameter ADDR_WIDTH = $clog2(NUM_REGS); // Address width needed for NUM_REGS
// $clog2 is a system function that returns
// the ceiling of log base 2
// Example 2: Arithmetic involving multiple parameters and a constant
parameter TOTAL_BUS_SIZE = DATA_WIDTH * 2; // Total size of two data buses
// Example 3: Calculation of a related timing parameter
parameter CLOCK_PERIOD_NS = 1000 / CLOCK_FREQ_MHZ; // Clock period in nanoseconds
// Example 4: A mask derived from a width parameter
parameter BYTE_SIZE = 8;
parameter BYTE_MASK = BYTE_SIZE - 1; // Example directly from sources
// Example 5: Average calculation using other parameters
parameter REAL_VAL_R = 5.7; // Declares 'r' as a real parameter
parameter INT_VAL_F = 9; // Declares 'f' as an integer parameter
parameter AVERAGE_DELAY = (REAL_VAL_R + INT_VAL_F) / 2; // Uses addition and division
initial begin
$display("--- Parameter Arithmetic Example ---");
$display("DATA_WIDTH = %0d", DATA_WIDTH); // Expected: 8
$display("NUM_REGS = %0d", NUM_REGS); // Expected: 16
$display("CLOCK_FREQ_MHZ = %0d", CLOCK_FREQ_MHZ); // Expected: 100
$display("ADDR_WIDTH = %0d", ADDR_WIDTH); // NUM_REGS=16, log2(16)=4. Expected: 4
$display("TOTAL_BUS_SIZE = %0d", TOTAL_BUS_SIZE); // DATA_WIDTH*2 = 8*2 = 16. Expected: 16
$display("CLOCK_PERIOD_NS = %0d", CLOCK_PERIOD_NS); // 1000/100 = 10. Expected: 10
$display("BYTE_MASK = %0d", BYTE_MASK); // BYTE_SIZE-1 = 8-1 = 7. Expected: 7
$display("AVERAGE_DELAY = %0f", AVERAGE_DELAY); // (5.7 + 9) / 2 = 14.7 / 2 = 7.35. Expected: 7.35
end
endmodule
xcelium> run --- Parameter Arithmetic Example --- DATA_WIDTH = 8 NUM_REGS = 16 CLOCK_FREQ_MHZ = 100 ADDR_WIDTH = 4 TOTAL_BUS_SIZE = 16 CLOCK_PERIOD_NS = 10 BYTE_MASK = 7 AVERAGE_DELAY = 7.350000 xmsim: *W,RNQUIE: Simulation is complete.
Always remember that Verilog is for describing hardware, and the code should reflect the physical gates and wires that will be created.
Common Beginner Mistakes
Mistake #1: Bit-Width Mismatch and Overflow
When adding two $N$-bit numbers, the result can require 'N+1' bits. Beginners often assign the result back to an 'N'-bit wire or reg, causing the most significant bit (MSB) to be silently dropped. Always ensure the destination net has sufficient width to handle the carry bit.
Mistake #2: Unintended Signed Arithmetic
Verilog treats nets as unsigned by default. If you perform arithmetic on standard wire or reg types, the simulator will not perform sign extension, even if you intended the values to be negative. To perform signed operations, both operands must be explicitly declared with the signed keyword.
Mistake #3: Mixing Signed and Unsigned Operands
If a single operand in an expression is unsigned, Verilog will treat the entire expression as unsigned. This often leads to "vanishing" negative signs and logic errors that are difficult to debug. Ensure all signals in an arithmetic expression are consistently typed as signed.
Mistake #4: Using Modulus (%) with Negative Numbers
The modulus operator in Verilog follows the sign of the first operand. Beginners often expect it to behave like a mathematical "modulo" (which is always positive), but in Verilog, -7 % 3 results in -1. This can break indexing or counter logic.
Best Practice: Use the concatenation operator { } to manually handle carries or sign extensions if you want to be explicit. For complex math, consider using the signed attribute and the arithmetic shift operator >>> to maintain the sign bit during scaling.
Practice Exercise
Challenge: Watch the Carry and the Wrap
With a = 200 and b = 100 (both 8 bits), compute the full sum including the carry, and then display a - b and b - a. Explain the last value.
Hint
Write the sum as {cout, sum} = a + b; with a 1-bit cout and an 8-bit sum.
Solution
module tb;
reg [7:0] a, b, sum;
reg cout;
initial begin
a = 200; b = 100;
{cout, sum} = a + b;
$display("{cout,sum} = %0d (cout=%0b sum=%0d)", {cout, sum}, cout, sum);
$display("a-b = %0d, b-a = %0d", a - b, b - a);
end
endmodule
The output is {cout,sum} = 300 (cout=1 sum=44) and a-b = 100, b-a = 156. The 9-bit left-hand side keeps the carry, while sum alone wraps around to 44. The value b - a is -100, which wraps to 156 in 8 unsigned bits.
Summary
In this tutorial, you learned:
+,-,*,/and%are the binary arithmetic operators, and+and-also work as unary operators- Division and modulus by zero give x, and the result of
%takes the sign of the first operand - If any bit of an operand is x or z, the whole result is x
- The width of an expression is set by its largest operand and the left-hand side, so a carry can be lost when the result is too narrow
- Arithmetic is signed only if all operands are signed; mixing in one unsigned operand makes the whole expression unsigned
- The power operator and
realoperands are not synthesizable, while arithmetic on parameters is evaluated at compile time