Declare numeric ports and signals signed with their true width.
Widen the result: adding two N-bit numbers needs N+1 bits; multiplying needs 2N.
Operate in the wide type so nothing is lost.
Narrow deliberately: truncate, saturate or raise an overflow flag. Never let it happen by accident.
Operation | Result width needed | Worst case (8-bit inputs) |
|---|---|---|
a + b | N + 1 | 127 + 127 = 254 needs 9 bits |
a − b | N + 1 | −128 − 127 = −255 needs 9 bits |
a × b | 2N | (−128)(−128) = 16384 needs 16 bits |
Σ of M values | N + ⌈log₂ M⌉ | 256 values of 8 bits need 16 bits |
In the direct assignment wide = a + b, the signed N+1-bit destination supplies the arithmetic width: operands are sign-extended before addition. This context does not recover bits already lost in a narrow intermediate. Parentheses preserve the direct context; a concatenation operand is self-determined and can stop it.
Four-bit signed a=7, b=1 | Operation / stored result | Five-bit result |
|---|---|---|
| Add in 5 bits from the destination context | 01000 = +8 |
| Make both signed operands 5 bits first | 01000 = +8 |
| 4-bit 1000 already wrapped to -8; widening sign-extends it | 11000 = -8 |
Predict which row a refactor uses before changing temporaries. Use the width-discipline lesson for the self-determined boundary examples; the adder below deliberately uses direct wide context.
module sat_add #(parameter int N = 8) (
input logic signed [N-1:0] a,
input logic signed [N-1:0] b,
output logic signed [N-1:0] y,
output logic saturated
);
localparam logic signed [N-1:0] MAX = {1'b0, {(N-1){1'b1}}}; // 127 for N=8
localparam logic signed [N-1:0] MIN = {1'b1, {(N-1){1'b0}}}; // -128 for N=8
logic signed [N:0] wide; // one extra bit
always_comb begin
wide = a + b; // both signed, N+1 bits: no loss
saturated = 1'b0;
y = wide[N-1:0];
if (wide > $signed({1'b0, MAX})) begin
y = MAX; saturated = 1'b1;
end else if (wide < $signed({MIN[N-1], MIN})) begin
y = MIN; saturated = 1'b1;
end
end
endmodule
module tb_sat_add;
logic signed [7:0] a, b, y;
logic sat;
sat_add #(8) dut (.a, .b, .y, .saturated(sat));
int errors = 0;
int exp;
logic exp_sat;
initial begin
for (int i = -128; i < 128; i++) begin
for (int j = -128; j < 128; j++) begin
a = i[7:0]; b = j[7:0]; #1;
exp = i + j;
exp_sat = 1'b0;
if (exp > 127) begin exp = 127; exp_sat = 1'b1; end
if (exp < -128) begin exp = -128; exp_sat = 1'b1; end
if (y !== exp[7:0]) begin errors++; $display("FAIL a=%0d b=%0d y=%0d exp=%0d", i, j, y, exp); end
if (sat !== exp_sat) begin errors++; $display("FAIL a=%0d b=%0d sat=%0d exp_sat=%0d", i, j, sat, exp_sat); end
end
end
if (errors == 0) $display("PASS: saturating adder matches reference");
$finish;
end
endmoduleThe adder stays correct for all 65,536 input pairs. The sum is formed in N+1 bits first, so comparing the wide value against the limits makes overflow visible.
Declare the product wide: logic signed [2*N-1:0] p; driven by assign p = a * b; with both operands signed. A declaration initialiser is evaluated once at time 0, so the initialiser form would never track later changes to a and b.
Right-shift a signed value with >>> to divide by 2ᵏ while keeping the sign. It rounds toward −∞ (−5 >>> 1 = −3).
Use signed'(x) casts sparingly; prefer declaring the signal signed once.
32 bits for each product plus ⌈log₂1024⌉ = 10 bits of growth: 42 bits. Truncating earlier loses precision or overflows; saturating at the end is a design choice, not an accident.