Widths, truncation and sign
Predict the width and the sign SystemVerilog gives an expression, keep a carry on purpose, and write a signed compare that stays signed.
- Lesson
- R1.4
- Time
- 25 min
- Runs on
- Your browser
- Check
- 5 questions
An 8-bit accumulator added 200, then 100, and read 44 instead of 300. In the same block, a signed reading of -1 was compared with a threshold of 1, and the "below" flag stayed 0. Both lines simulated exactly as written, and synthesis built exactly what the simulation showed. The code said 8 bits and unsigned; the brief said 300 and -1.
sum and low come from the design as written; sum fixed and low fixed from the same bench with a 9-bit sum and a signed threshold. The threshold is 1 from cycle 2.- Cycle 2
Reset ends after cycle 1. The first sample, 200, lands in
sum. Both designs agree. - Cycle 3
200 + 100 is 300, which needs 9 bits: 1_0010_1100. The 8-bit
sumkeeps the low eight, 0010_1100, which is 44. Bit 8 had no flop to land in. - Cycle 3
In the same cycle
xis -1 and the threshold is 1. The compare is unsigned, so -1 is read as 255, and 255 is not below 1:lowstays 0. - Cycle 5
-128 is read as 128, still above 1. Two flags missed. With the 9-bit sum and a signed threshold, both columns are right.
Measured on the lesson's bench (Icarus Verilog 12.0); Verilator 5.052 prints the same lines.
How SystemVerilog sizes an expression
Before it adds, SystemVerilog picks one width for the whole expression. For +, -, * and the bitwise operators, that width is the widest operand or the target of the assignment, whichever is larger. The operands are extended to that width, the operation runs, and the result is cut to fit the target. Dropping the upper bits is called truncation.
In sum <= sum + d every name is 8 bits wide, so the addition runs at 8 bits. Its carry, bit 8, is computed by nobody and stored nowhere. Give sum nine bits and the same line adds at 9 bits: the target is now the widest thing in the statement.
- Operands in
At cycle 3 the adder sees the old total, 200, and the sample, 100.
Follow the calls, values and waits
- data 100: d[7:0] → sum + d
- data 200: sum[7:0] → sum + d
- Low eight bits stored
300 is 1_0010_1100. Bits 7 to 0 are 44, and that is what the eight flops store.
Follow the calls, values and waits
- data 44: sum + d → sum[7:0]
- The carry is dropped
Bit 8 is worth 256. The statement is 8 bits wide, so synthesis builds no logic for it and no flop to hold it.
Follow the calls, values and waits
- data 1 (worth 256): sum + d → bit 8
- With sum[8:0]
Declare the total with nine bits. The statement becomes 9 bits wide, the adder grows by one bit, and a ninth flop keeps the carry: 300.
Read the diagram as text
- d[7:0]: sample: 100
- sum + d: sized to 8 bits by the statement
- sum[7:0]: 8 flops; held 200
- bit 8: no flop to hold it
- d[7:0] → sum + d: operand
- sum[7:0] → sum + d: old total
- sum + d → sum[7:0]: bits 7..0
- sum + d → bit 8: bit 8
Synthesis builds what the sized statement says. Nothing in the netlist disagrees with the simulation.
Not every operator follows the context. Some size each operand alone, by its own width: these operands are self-determined. The braces of a concatenation are the trap here. A comparison is a small context of its own: its two sides are sized to the wider side, and the result is a single bit.
| Statement (a = 200, b = 100, both 8 bits) | Width used | Result |
|---|---|---|
s8 = a + b; (s8 is 8 bits) | 8: every name is 8 bits | 44 |
s9 = a + b; (s9 is 9 bits) | 9: the target is wider | 300 |
s9 = {a + b}; | 8: inside braces, an operand keeps its own width | 44 |
s9 = a << 1; | 9: a shifted value follows the context | 400 |
s9 = {a << 1}; | 8: braces again | 144 |
s9 = sel ? a + b : 0; | 32: the unsized 0 is a 32-bit number | 300 |
a + b > 8'd255 | 8: the wider side of the compare | 0 |
a + b > 255 | 32: the unsized 255 widens the compare | 1 |
Measured: Icarus Verilog 12.0 and Verilator 5.052 print the same results. A 1-bit compare result never widens its operands.
When an expression is signed
Signedness follows a stricter rule than width. An expression is signed only if every operand in it is signed. One unsigned operand makes the whole expression unsigned, and a signed -1 is then read as its bit pattern, 1111_1111, which is 255. The target of the assignment does not count.
Unsigned operands hide in ordinary code. A port declared without signed is unsigned. A literal with a base, such as 8'd1, is unsigned unless it carries an s, as in 8'sd1. A part-select such as x[7:0] and a concatenation are unsigned even when every bit comes from a signed variable. A plain decimal number such as 1 is signed and 32 bits wide.
Threshold port without signed
input logic signed [7:0] x,
input logic [7:0] thr,
...
assign low = x < thr; // unsigned
One operand is unsigned, so the compare is unsigned. -1 reads as 255 and low stays 0.
Both operands signed
input logic signed [7:0] x,
input logic signed [7:0] thr,
...
assign low = x < thr; // signed
Both operands are signed, so -1 is below 1 and low is 1.
| Compare, with x = -1 (logic signed [7:0]) | Kind | Result |
|---|---|---|
x < 1 | signed: the plain 1 is a signed number | 1 |
x < 8'd1 | unsigned: a based literal without s | 0 |
x < 8'sd1 | signed | 1 |
x < u (u is logic [7:0], holding 1) | unsigned | 0 |
x < $signed(u) | signed: the cast changes how u is read | 1 |
x[7:0] < 8'sd1 | unsigned: a part-select is unsigned | 0 |
Measured on Icarus Verilog 12.0 and Verilator 5.052, which agree on every row.
Signedness changes which function the gates compute, and not how many there are. Measured on this flow, an 8-bit compare x < thr is 29 cells either way: 9 levels of logic unsigned, 10 signed. In the lab design, making the threshold signed leaves the count at 71 cells.
What Lint reports, and the lab
Lint flags a statement whose two sides have different widths. Write s8 = {1'b0, a} + b; and Verilator 5.052 reports %Warning-WIDTHTRUNC: Operator ASSIGNW expects 8 bits on the Assign RHS, but Assign RHS's ADD generates 9 bits. Write s9 = {a + b}; and it reports WIDTHEXPAND.
The lab design draws no width or sign warning at all. Every name in sum <= sum + d is 8 bits, so the sides match, and a signed-against-unsigned compare is legal code. Lint compares the code with itself. Only the brief says the total must reach 300 and the reading can be negative.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | asynchronous active-low reset; clears sum |
| d | input | [7:0] | sample to add, 0 to 255 |
| sum | output | [8:0] | sum adds d at every rising edge; totals up to 510 (two full-scale samples between resets), so sum needs 9 bits |
| x | input | signed [7:0] | reading, from -128 to 127 |
| thr | input | signed [7:0] | threshold for x, from -128 to 127 |
| low | output | 1 | 1 when x < thr as signed numbers |
- Clock
- rising edge of
clk - Reset
- asynchronous active-low
rst_nclearssum - Latency
sumis a register;lowis combinational- Corner cases
- a carry out of bit 7; a negative reading against a positive threshold
Run the lab: the total reads 44 and the bench marks two wrong flags. Press Synthesize: 71 cells, 8 flops, 14 levels of logic on this flow. Now give sum nine bits, with its reset value, and declare thr as signed. Run again: the total is 300 and no flag is wrong. Synthesize: 77 cells, 9 flops, 16 levels. The carry cost one flop, five gates and two levels; the sign cost nothing.
Asked in interviews
What is the width of
a + bin a comparison?The two sides of a compare are sized to the wider side, and the 1-bit result does not widen them. With two 8-bit operands, a + b > 8'd255 adds at 8 bits and can never be true. An unsized 255 is 32 bits, so a + b > 255 keeps the carry.
When is an expression signed?
Only when every operand is signed. One unsigned port, a based literal without s, a part-select or a concatenation makes it unsigned, and negative values are read as large positive ones. The left-hand side does not count.
How do you keep a carry?
Make the statement wide enough: a target one bit wider than the operands, or an operand extended with {1'b0, a}. Then size the register that stores the result from the brief, so the netlist has a flop for the carry.
Try it
Edges 0 and 1 are reset. At edge 2 the total takes 200. At edge 3 it should be 300 and reads 44: the carry fell off bit 7. In the same line, -1 against a threshold of 1 gives low = 0, because the unsigned thr makes the compare unsigned. -128 at edge 5 is missed the same way.
Change the sum port to [8:0] and its reset value to 9'd0, and declare thr as logic signed [7:0]. The bench then prints a total of 300 and 0 wrong flags. Synthesize before and after: 71 cells, 8 flops and 14 levels become 77 cells, 9 flops and 16 levels.
Synthesize the design to measure its cells and inspect the mapped gates.
e0 d=0 sum=0 x=0 thr=0 low=0 e1 d=0 sum=0 x=0 thr=0 low=0 e2 d=200 sum=200 x=5 thr=1 low=0 e3 d=100 sum=44 x=-1 thr=1 low=0 wrong e4 d=0 sum=44 x=1 thr=1 low=0 e5 d=0 sum=44 x=-128 thr=1 low=0 wrong total 44, want 300; 2 low flags wrongOriginal design · 71 cells · 8 flip-flops · 0 latches · 14 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic rst_n = 1; // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic [7:0] d = 0;
logic [8:0] sum; // 9 bits: wide enough for the fixed design too
logic signed [7:0] x = 0, thr = 0;
logic low;
int wrong_low = 0;
acc dut (.clk(clk), .rst_n(rst_n), .d(d), .sum(sum), .x(x), .thr(thr), .low(low));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
if (low !== (x < thr)) wrong_low++; // the bench compares as signed numbers
$display("e%0d d=%0d sum=%0d x=%0d thr=%0d low=%b%s", edge_n, d, sum, x, thr, low, low === (x < thr) ? "" : " wrong");
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
d = 200; x = 5; thr = 1; tick();
d = 100; x = -1; tick();
d = 0; x = 1; tick();
x = -128; tick();
$display("total %0d, want 300; %0d low flags wrong", sum, wrong_low);
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- An arithmetic expression is computed at the width of its widest operand or its target, whichever is larger; the result is then truncated to the target.
- To keep a carry, make the target wider than the operands and give the register a flop for it; the 9-bit accumulator is 77 cells against 71 on this flow.
- An expression is signed only if every operand is signed: an unsigned port, a based literal without s, a part-select or a concatenation turns -1 into 255.
- Lint flags sides of different widths. A statement whose widths match but whose brief needs more bits passes Lint, simulation and synthesis alike.