Every expression gets its signedness before any value is computed. The context is signed only when every operand is signed.
A single unsigned operand makes the whole context unsigned. The signed operands then zero-extend, so negative values read as large positives.
Operand A | Operand B | Context result |
|---|---|---|
signed | signed | SIGNED (both sign-extend) |
signed | unsigned | UNSIGNED (both zero-extend) |
unsigned | unsigned | UNSIGNED |
Example: byte s = -1 (8'hFF) against logic [15:0] u = 16'h10. s < u goes unsigned, s zero-extends to 255, and the result is FALSE.
A signed temperature against an unsigned threshold is the classic case. The mixed comparison goes unsigned, so -5 reads as 251 and the branch never fires.
The fix keeps every operand signed. Cast the unsigned side through one extra zero bit, or declare it signed at the source.
// snippet
logic signed [15:0] a16, b16;
logic ok1 = (a16 > -16'sd4); // sized signed literal: signed compare
logic ok2 = (a16 > -4); // unsized decimal is signed: signed compare
logic bad2 = (a16 > 16'hFFFC); // sized hex defaults unsigned: unsigned compareOnly sized based literals default to unsigned. Unsized decimals are signed 32-bit. The unbased fills '0 and '1 adapt to their context width, but they are unsigned, so s < '0 is never true: compare against 0 for sign tests.
s8 is a signed byte holding -1. Is s8[7:0] < 0 true?No. A part-select is unsigned even when it covers the whole signed vector, so it reads 255. The second bench checks exactly this.
$signed(b) and $signed({1'b0, b}) for an 8-bit unsigned b?$signed only reinterprets bits. On values above 127 it turns the top bit into a sign bit, while the zero-padded form keeps the magnitude.
temp is -5 and bound is 10. What do lt_mixed and lt_fixed read?lt_mixed is 0 and lt_fixed is 1. The mixed compare goes unsigned and sees 251 < 10; the fixed one stays signed and sees -5 < 10.
One block checks the mixed compare and the cast fix; a second checks that selects and concatenations are unsigned.
module signed_cmp (
input logic signed [7:0] temp,
input logic [7:0] bound,
output logic lt_mixed,
output logic lt_fixed
);
always_comb begin
lt_mixed = temp < bound;
lt_fixed = temp < $signed({1'b0, bound});
end
endmodule
module tb_signed_cmp;
timeunit 1ns; timeprecision 1ps;
logic signed [7:0] temp;
logic [7:0] bound;
logic lt_mixed, lt_fixed;
int errors = 0;
signed_cmp dut (.temp(temp), .bound(bound), .lt_mixed(lt_mixed), .lt_fixed(lt_fixed));
initial begin
temp = -8'sd5; bound = 8'd10; #1;
if (lt_mixed !== 1'b0) errors++;
if (lt_fixed !== 1'b1) errors++;
temp = 8'sd7; bound = 8'd10; #1;
if (lt_mixed !== 1'b1) errors++;
if (lt_fixed !== 1'b1) errors++;
temp = -8'sd5; bound = 8'd251; #1;
if (lt_fixed !== 1'b1) errors++;
if (errors != 0) $fatal(1, "FAIL: signed compare checks disagree");
$display("PASS: mixed compare goes unsigned while the zero-extended cast holds");
$finish;
end
endmodulePASS: mixed compare goes unsigned while the zero-extended cast holdsmodule tb_select_unsigned;
timeunit 1ns; timeprecision 1ps;
logic signed [7:0] s8;
logic r1, r2, r3;
int errors = 0;
initial begin
s8 = -8'sd1;
r1 = (s8[7:0] < 0);
r2 = ({s8} < 0);
r3 = (s8 < 0);
#1;
if (r1 !== 1'b0) errors++;
if (r2 !== 1'b0) errors++;
if (r3 !== 1'b1) errors++;
if (errors != 0) $fatal(1, "FAIL: select signedness checks disagree");
$display("PASS: part-select and concat stay unsigned while the plain signal stays signed");
$finish;
end
endmodulePASS: part-select and concat stay unsigned while the plain signal stays signedRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 (-g2012) runs them too, with the same output.