Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
    • Digital Logic for RTL Engineers
    • Number Systems for RTL Engineers
      • Representation Drives Behavior
      • Binary and Hexadecimal Conversion Habits
      • Two's Complement Intuition
      • Sign Extension, Zero Extension and Casting
      • SystemVerilog Signed Arithmetic Pattern
      • Overflow Detection Patterns
    • Verilog Fundamentals
    • Verilog Operators and Types
    • SystemVerilog for RTL
    • Simulation and Testbenches
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

←Number Systems for RTL Engineers
  1. Topics
  2. RTL Design
  3. RTL Foundations
  4. Number Systems for RTL Engineers
  5. SystemVerilog Signed Arithmetic Pattern
On this page

    Part 1 of 9, lesson 11 of 36Part 1 of 9/Foundations/Lesson 11 of 36Beginner

    SystemVerilog Signed Arithmetic Pattern

    Declare signed, widen before operating, and saturate or flag the narrowed result.

    A narrow intermediate loses the carry before anyone can look at it. Declare signed, widen the result, compute wide, then narrow on purpose: this keeps a saturating adder right for all 65,536 input pairs.

    After this lesson you can: write signed add and multiply with the right result width and narrow the result by truncating, saturating or flagging overflow.

    The pattern in four steps

    1. Declare numeric ports and signals signed with their true width.

    2. Widen the result: adding two N-bit numbers needs N+1 bits; multiplying needs 2N.

    3. Operate in the wide type so nothing is lost.

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

    A saturating adder

    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

    direct = a + b

    Add in 5 bits from the destination context

    01000 = +8

    explicit_sum = $signed({a[3],a}) + $signed({b[3],b})

    Make both signed operands 5 bits first

    01000 = +8

    narrow = a + b; late = narrow

    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.

    sat_add.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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
    endmodule

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

    Multiplication and shifting

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

    Check yourself Two signed 16-bit samples are multiplied and the full 32-bit product is summed over 1024 samples. How wide must the accumulator be to never overflow?

    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.

    • An add of two N-bit numbers needs N+1 bits and a multiply needs 2N.

    • In a direct assignment the wide destination sets the arithmetic width, but it cannot recover bits lost inside a narrow intermediate.

    • >>> divides a signed value by 2^k and rounds toward minus infinity: -5 >>> 1 is -3.

    Rule of thumb: narrow only at the end, and only on purpose.

    • Code lab: Build saturation logic

    • Interview: run a mock interview on this topic

    Related topics

    • Overflow detection patterns

    • Combinational arithmetic blocks

    Previous

    11 / 350

    Next: Overflow Detection Patterns

    Up next

    Up nextOverflow Detection Patterns

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Representation Drives Behavior
    • Binary and Hexadecimal Conversion Habits
    • Two's Complement Intuition