FPGA

Sign Extension in Verilog

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: Verilog extends a value to the target width using sign extension (replicating the MSB) only when every operand in the expression is signed; the instant one operand is unsigned, the whole expression turns unsigned and zero-extension kicks in, silently turning a negative number into a huge positive one.

    Width mismatch and sign handling are the two bugs that bite every RTL designer, and they usually hide together. A subtraction that worked in your head produces garbage in simulation, lint flags a dozen "truncation" warnings, and the synthesized netlist behaves differently from the testbench. Nearly all of it traces back to how Verilog decides whether to sign-extend or zero-extend an operand when it widens it to fit an expression. This article walks through the rules exactly as the IEEE 1364/1800 standard defines them, with concrete bit patterns, and shows where the classic traps are.

    What sign extension actually means

    When a narrow value is used in a wider context, the extra high-order bits have to come from somewhere. There are two choices:

    • Zero extension: fill the new upper bits with 0. Correct for unsigned numbers.
    • Sign extension: replicate the most significant bit (the sign bit) into the new upper bits. This preserves the two's-complement value of a signed number.

    Take the 4-bit value 4'sb1101, which is −3 in two's complement. Widen it to 8 bits. Sign-extended it becomes 8'b1111_1101 = −3, value preserved. Zero-extended it becomes 8'b0000_1101 = +13, value destroyed. That single wrong choice is the entire bug class.

    Sign extension: 4 bits to 8 bitsSource: 4'sb1101 = −31101MSB = sign bitReplicate the MSB into the 4 new upper bits11111101New bits (teal) copy the sign bit (orange)Sign-extend: 8'b1111_1101 = −3 (value preserved)Zero-extend would give 8'b0000_1101 = +13 (wrong)CourseTron · coursetron.com
    Sign-extending −3 from 4 to 8 bits preserves the value; zero-extending corrupts it.

    The role of the signed keyword

    Verilog nets and regs default to unsigned. A reg [7:0] a; is treated as an unsigned 8-bit number no matter what bit pattern it holds. To tell the tool a vector carries a two's-complement value, you declare it signed: reg signed [7:0] a; or wire signed [15:0] bus;. Only then will the simulator and synthesis tool (Synopsys VCS, Cadence Xcelium, Siemens Questa all agree here, because it is standardized) choose sign extension when widening a.

    Integer literals follow the same logic through the 's notation: 8'd200 is unsigned, 8'sd200 is signed (and actually represents −56, because 200 does not fit in a signed 8-bit range). The plain decimal -5 is a signed 32-bit value; 4'b1101 is unsigned even though it looks like it "should" be negative.

    Key fact: Signedness is a property of the type, not the bit pattern. 8'b11111011 and 8'sb11111011 hold identical bits, but the first prints as 251 and the second as −5.

    The contagion rule: one unsigned operand poisons the whole expression

    This is the single most important rule and the source of most real bugs. Verilog computes the sign of a binary expression by this rule: if either operand is unsigned, the result is unsigned. Signedness only survives when every operand is signed. The determination happens before any extension, and it decides whether the common width will be reached by sign or zero extension.

    reg signed [7:0] a;      // −5  = 8'sb1111_1011
    reg        [7:0] b;      // unsigned
    reg signed [8:0] r;
    
    a = -5;
    b = 2;
    r = a + b;   // b is unsigned -> expression unsigned
                 // a is zero-extended as if +251, r = 253, NOT −3

    Because b is unsigned, the + is an unsigned operation. a is reinterpreted as 251, the sum is 253, and even though r is declared signed, it receives the unsigned result bits. The fix is to make b signed, or to cast at the point of use with $signed(b).

    A signed bit enters an unsigned expressionsigned regunsigned expr1sign lostInside the unsigned expression the bit is just a value bit, never a sign.CourseTron · coursetron.com
    Animated: a sign bit entering an unsigned expression no longer means "negative" — this is the contagion rule in motion.

    How width and sign interact in a self-determined expression

    Verilog first finds the self-determined or context-determined bit width of each sub-expression, then extends every operand to the maximum width before evaluating. The extension method (sign vs zero) comes from the sign rule above. So two independent decisions stack: how wide, then how to fill. The table summarizes the common cases.

    ExpressionOperand typesResult signFill used
    a + bboth signedsignedsign extend
    a + bone unsignedunsignedzero extend
    a >> 2a signedsignedlogical shift (>> never sign-fills)
    a >>> 2a signedsignedarithmetic: sign-fills on left
    a >>> 2a unsignedunsignedzero-fills (no sign to copy)
    {a}anyunsignedconcatenation is always unsigned

    Key fact: The arithmetic shift >>> only sign-extends when its left operand is signed. On an unsigned operand it behaves exactly like the logical >> — a frequent surprise when designers expect "arithmetic" to mean "keep the sign."

    a = 8'sb1111_0100 (−12), shifted right by 2a = −1211110100a >> 2 = +6100111101a >>> 2 = −311111101Logical: zeros shifted inArithmetic: sign bit copied inOrange cells are the 2 bits shifted in on the left.>>> sign-fills only when the operand is signed.CourseTron · coursetron.com
    Worked example: a logical shift of −12 gives +61; an arithmetic shift gives −3.

    The concatenation and part-select traps

    Two constructs strip signedness regardless of their inputs: concatenation {...} and any part-select or bit-select. Writing {a} around a signed value a forces it unsigned, so the common idiom r = {a}; quietly disables sign extension. Likewise a[7:0] is unsigned even if a is declared signed. If you need the slice to stay signed, re-cast it with $signed(a[7:0]).

    Comparisons are equally dangerous: if (a < 0) where a is an unsigned reg is always false, because an unsigned value can never be below zero. Lint tools from Siemens (Questa AutoCheck) and Synopsys (SpyGlass) flag this as an unsigned-comparison-with-zero warning — do not suppress it blindly.

    Width mismatch: truncation and the hidden extend

    Assignment is context-determined, so the right-hand side is widened or truncated to the left-hand side width. A reg [3:0] small = big; silently drops the upper bits of big, while reg [15:0] wide = small; extends — sign-extending only if both small and the assignment context are signed. The dangerous version is a mixed-width arithmetic such as sum = a * b; where a and b are 16-bit but sum is only 16 bits: the 32-bit product is computed, then truncated, losing the high half. Always size accumulators and products deliberately. For a deeper treatment of how the operators themselves behave, see our companion piece on signed vs unsigned arithmetic in Verilog, and the related note on bit-width and truncation rules.

    If you are building intuition through hands-on labs, the structured RTL track on our courses page runs these exact cases on real simulators. Teams that need a production design partner rather than training can reach the silicon engineering group at Avecas design services.

    Ready to stop fighting sign bugs? CourseTron's FPGA & RTL Design course drills sign/width semantics with graded simulator labs on industry tools. Enrol and turn these traps into muscle memory.

    Frequently Asked Questions

    Why does my signed subtraction give a huge positive number?

    Almost certainly one operand is unsigned. Under Verilog's sign rule a single unsigned operand makes the whole expression unsigned, so the negative result is reinterpreted as a large positive value. Declare both operands signed or wrap the unsigned one in $signed().

    Does declaring the destination reg as signed fix the problem?

    No. The sign of the expression is fixed by its operands before the result is assigned. A signed destination receiving an unsigned result just stores the unsigned bits. You must fix the operands, not the target.

    What is the difference between >> and >>>?

    >> is a logical shift and always fills with zeros. >>> is an arithmetic shift that replicates the sign bit — but only when its left operand is declared signed; on an unsigned operand it fills with zeros like >>.

    Why is concatenation always unsigned?

    The IEEE standard defines the result of a concatenation (and of part-selects) as unsigned by rule, because the construct is about assembling bit fields, not arithmetic values. Re-apply $signed() if you need the assembled result treated as two's complement.

    Advertisement