Widening an N-bit value means choosing the new upper bits. The choice must preserve the value.
Zero extension fills with 0. Correct for unsigned values.
Sign extension copies the top bit into every new bit. Correct for two's complement values (−5 stays −5).
module extend_demo (
input wire [3:0] a,
output wire [7:0] zx,
output wire [7:0] sx
);
assign zx = {4'b0000, a}; // zero extension
assign sx = {{4{a[3]}}, a}; // sign extension by replication
endmodulea (4-bit) | zero-extended (8-bit) | sign-extended (8-bit) | Signed meaning |
|---|---|---|---|
0101 | 0000_0101 | 0000_0101 | +5 (both agree) |
1011 | 0000_1011 = 11 | 1111_1011 | −5 only the sign-extended one is still −5 |
Replication builds the right bits but a concatenation is unsigned. The sx port above therefore holds unsigned 251 for 8'hFB; it means -5 only when interpreted as signed. A signed declaration or cast makes that interpretation explicit before a comparison or further widening.
module tb_extension_meaning;
logic [3:0] a = 4'b1011;
logic [7:0] raw;
logic signed [7:0] signed_copy;
logic signed [8:0] widened;
initial begin
raw = {{4{a[3]}}, a};
signed_copy = $signed(raw);
widened = signed_copy; // sign-extends the signed source
if (raw !== 8'd251 || signed_copy !== -8'sd5 || widened !== -9'sd5)
$fatal(1, "FAIL: sign-extended bits and signed interpretation differ");
if (raw < 8'd0 || !(signed_copy < 8'sd0))
$fatal(1, "FAIL: signed comparison did not use the declared interpretation");
$display("PASS: FB is unsigned 251 or signed -5; signed widening keeps -5");
$finish;
end
endmoduleVerilog types an expression from all its operands together. If any operand is unsigned, the result is unsigned.
Add the 4-bit signed value −8 to the unsigned 8-bit value 1. The experiment below shows the rule in action.
module tb_typing;
logic [3:0] a = 4'b1000; // -8 if read as signed
logic [7:0] b = 8'd1; // unsigned
logic signed [7:0] r_bad, r_good, r_explicit;
int errors = 0;
initial begin
r_bad = $signed(a) + b; // b is unsigned -> whole expression unsigned
r_good = $signed(a) + $signed(b); // fine only because b = 1 is below 128 (see the next example)
r_explicit = {{4{a[3]}}, a} + b; // explicit sign extension, then add
$display("bad=%0d good=%0d explicit=%0d", r_bad, r_good, r_explicit);
if (r_bad !== 8'sd9) errors++; // -8 treated as 8: 8 + 1 = 9 (the trap)
if (r_good !== -8'sd7) errors++; // -8 + 1 = -7
if (r_explicit !== -8'sd7) errors++;
if (errors == 0) $display("PASS: confirmed the unsigned-contamination rule");
$finish;
end
endmoduleFour habits avoid the trap:
Declare numeric signals signed where they are declared, not at the point of use.
Cast both operands with $signed(a) + $signed(b) only when both really are signed values. A cast changes interpretation, not the bits, so an unsigned 8-bit 200 becomes −56.
To mix a signed operand with an unsigned one, zero-extend the unsigned one into a wider signed type first, for example $signed({1'b0, b}) or a wider signed copy. Once the whole expression is signed, the signed operand is sign-extended automatically.
Or extend explicitly with replication, so the code shows exactly what happens to the upper bits.
The cast-both habit goes wrong for an upper-half unsigned value. Add the signed 4-bit −8 to the unsigned 8-bit value 200: the correct sum is 192, but $signed(a) + $signed(b) reads b as −56 and gives −64. Zero-extending b first gives 192.
module tb_cast_unsigned;
logic signed [3:0] a = -4'sd8;
logic [7:0] b = 8'd200; // upper-half unsigned value
logic signed [9:0] r_wrong, r_right;
int errors = 0;
initial begin
r_wrong = $signed(a) + $signed(b); // b is reinterpreted: 200 -> -56
r_right = $signed(a) + $signed({1'b0, b}); // zero-extend first: stays +200
$display("wrong=%0d right=%0d", r_wrong, r_right);
if (r_wrong !== -10'sd64) errors++; // -8 + (-56) = -64
if (r_right !== 10'sd192) errors++; // -8 + 200 = 192
if (errors == 0) $display("PASS: zero-extend the unsigned operand before mixing");
else $display("FAIL: wrong=%0d right=%0d", r_wrong, r_right);
$finish;
end
endmoduleIn SystemVerilog, a size cast such as 8'(x) changes width while preserving the operand's signedness. Cast order therefore depends on the intended interpretation: to widen a raw two's-complement pattern, interpret it as signed first, then widen. For unsigned logic [3:0] x = 4'h8, signed'(8'(x)) zero-extends first and gives +8, while 8'($signed(x)) sign-extends and gives -8. If the pattern really represents unsigned 8, widening first preserves that meaning.
module tb_cast_order;
logic [3:0] x;
logic signed [7:0] widen_first, sign_first;
int expected_unsigned, expected_signed, errors = 0;
initial begin
x = 4'h8;
widen_first = $signed(8'(x));
sign_first = 8'($signed(x));
if (widen_first !== 8'sd8 || sign_first !== -8'sd8) errors++;
for (int i = 0; i < 16; i++) begin
x = i[3:0];
widen_first = $signed(8'(x));
sign_first = 8'($signed(x));
expected_unsigned = i;
expected_signed = (i < 8) ? i : i - 16;
if (widen_first !== expected_unsigned[7:0]) errors++;
if (sign_first !== expected_signed[7:0]) errors++;
end
if (errors == 0) $display("PASS: cast order preserves the chosen interpretation for all 16 patterns");
else $display("FAIL: cast-order errors=%0d", errors);
$finish;
end
endmodule{{4{sample[11]}}, sample} replicates the sign bit four times to reach 16 bits. If both are declared signed, plain acc + sample does the same extension automatically.