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.

The accumulator and the compare, before and after the fixOne line per rising edge after it settles. 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.
012345clkd02001000sum020044sum fixed0200300x05-11-128lowlow fixed
  1. Cycle 2

    Reset ends after cycle 1. The first sample, 200, lands in sum. Both designs agree.

  2. Cycle 3

    200 + 100 is 300, which needs 9 bits: 1_0010_1100. The 8-bit sum keeps the low eight, 0010_1100, which is 44. Bit 8 had no flop to land in.

  3. Cycle 3

    In the same cycle x is -1 and the threshold is 1. The compare is unsigned, so -1 is read as 255, and 255 is not below 1: low stays 0.

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

Where the carry of 200 + 100 goesThe adder takes the sample and the old total. Its result needs nine bits, but the register that holds the total has eight flops, so bit 8 falls off.
Where the carry of 200 + 100 goesThe adder takes the sample and the old total. Its result needs nine bits, but the register that holds the total has eight flops, so bit 8 falls off. manager d[7:0] sample: 100 bus sum + d sized to 8 bits by thestatement bridge sum[7:0] 8 flops; held 200 queue bit 8 no flop to hold it operandold totalbits 7..0bit 8
  1. Operands in

    At cycle 3 the adder sees the old total, 200, and the sample, 100.

    Follow the calls, values and waits
    1. data 100: d[7:0] → sum + d
    2. data 200: sum[7:0] → sum + d
  2. 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
    1. data 44: sum + d → sum[7:0]
  3. 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
    1. data 1 (worth 256): sum + d → bit 8
  4. 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 usedResult
s8 = a + b; (s8 is 8 bits)8: every name is 8 bits44
s9 = a + b; (s9 is 9 bits)9: the target is wider300
s9 = {a + b};8: inside braces, an operand keeps its own width44
s9 = a << 1;9: a shifted value follows the context400
s9 = {a << 1};8: braces again144
s9 = sel ? a + b : 0;32: the unsized 0 is a 32-bit number300
a + b > 8'd2558: the wider side of the compare0
a + b > 25532: the unsized 255 widens the compare1

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])KindResult
x < 1signed: the plain 1 is a signed number1
x < 8'd1unsigned: a based literal without s0
x < 8'sd1signed1
x < u (u is logic [7:0], holding 1)unsigned0
x < $signed(u)signed: the cast changes how u is read1
x[7:0] < 8'sd1unsigned: a part-select is unsigned0

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.

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1asynchronous active-low reset; clears sum
dinput[7:0]sample to add, 0 to 255
sumoutput[8:0]sum adds d at every rising edge; totals up to 510 (two full-scale samples between resets), so sum needs 9 bits
xinputsigned [7:0]reading, from -128 to 127
thrinputsigned [7:0]threshold for x, from -128 to 127
lowoutput11 when x < thr as signed numbers
Clock
rising edge of clk
Reset
asynchronous active-low rst_n clears sum
Latency
sum is a register; low is 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
  1. What is the width of a + b in 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.

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

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

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: 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 wrong

Original 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

Predict the outputBoth operands and the target are 4 bits wide. What does the program print?
module tb;
  logic [3:0] a = 4'd12, b = 4'd7;
  logic [3:0] s;
  initial begin
    s = a + b;
    $display("s=%0d", s);
    $finish(0);
  end
endmodule
Spot the bugx is logic signed [7:0] holding -1; ts is signed and tu unsigned, both holding 1. Each option sets low. The check expects low to be 1, since -1 is below 1. Which option fails?
ConceptIn c = a + b;, a and b are 8 bits wide and c is 16 bits wide. At what width is the addition done?
Conceptx is logic signed [7:0] holding -1. Which statement about x < 8'd1 is true?
Read the waveformThe accumulator adds d at every rising edge. In which cycle does sum first disagree with the true total?
The 8-bit accumulator
012345rst_nd02001000sum020044

One value per cycle, from cycle 0, measured on the lesson's bench.

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.