Module 7: Advanced RTL Design Techniques ยท 7.2 Arithmetic Circuits in RTL

Saturating Arithmetic

The previous article was about interpreting bits correctly. This one is about what happens when a result does not fit in the bits at all.

Turn the volume up on a digital audio path far enough and the sample values reach the top of their range. Two’s complement addition has an answer for what happens next, and for audio it is the wrong one: the sum wraps. The largest positive value plus one becomes the largest negative value. The loudest positive sample flips instantly to the loudest negative sample, which comes out of the speaker as a click that has nothing to do with the sound. Saturating arithmetic exists to prevent this. Instead of wrapping past the boundary, the result sticks at the maximum or minimum representable value.

Detecting signed overflow correctly

Before you can saturate you have to detect overflow, and for signed addition the obvious check, the carry-out of the top bit, is wrong. Two equivalent rules are correct. The formal one is that signed overflow occurs when the carry into the sign bit differs from the carry out of it. The one that is easier to hold in your head is that signed overflow can only happen when both operands have the same sign and the result comes out with the opposite sign. Adding a positive and a negative number can never overflow; only positive plus positive landing negative, or negative plus negative landing positive, signals overflow.

wire signed [7:0] a, b;
wire signed [7:0] sum = a + b;
wire overflow = (a[7] == b[7]) && (sum[7] != a[7]);

This is the form you will see most often in real RTL: compare the sign of both inputs against the sign of the possibly wrapped result. No extra sign-extended bit is needed.

Building the saturating adder

Once you can detect overflow, saturation is a mux: on overflow, replace the wrapped result with the right boundary value.

`timescale 1ns / 1ps
`default_nettype none

module sat_adder_signed #(parameter WIDTH = 8) (
    input  wire signed [WIDTH-1:0] a, b,
    output reg  signed [WIDTH-1:0] sum
);
    wire signed [WIDTH-1:0] raw_sum = a + b;
    wire overflow = (a[WIDTH-1] == b[WIDTH-1]) && (raw_sum[WIDTH-1] != a[WIDTH-1]);

    localparam signed [WIDTH-1:0] MAX_VAL = {1'b0, {(WIDTH-1){1'b1}}};   // 0111...1
    localparam signed [WIDTH-1:0] MIN_VAL = {1'b1, {(WIDTH-1){1'b0}}};   // 1000...0

    always @(*) begin
        if (overflow)
            sum = a[WIDTH-1] ? MIN_VAL : MAX_VAL;  // both negative -> clamp low, both positive -> clamp high
        else
            sum = raw_sum;
    end
endmodule

Note which boundary is selected. If both operands were negative and the sum wrapped, the true result was more negative than the format can hold, so clamp to MIN_VAL. If both were positive, clamp to MAX_VAL. Either operand’s sign tells you the direction, since they are guaranteed equal when overflow has happened.

The unsigned case is simpler

Unsigned saturating addition has only one boundary, the ceiling:

module sat_adder_unsigned #(parameter WIDTH = 8) (
    input  wire [WIDTH-1:0] a, b,
    output reg  [WIDTH-1:0] sum
);
    wire [WIDTH:0] raw_sum = a + b;   // one extra bit catches the carry-out directly

    always @(*) begin
        if (raw_sum[WIDTH])          // carry out of the top bit means we overflowed
            sum = {WIDTH{1'b1}};      // clamp to all-ones, the max representable value
        else
            sum = raw_sum[WIDTH-1:0];
    end
endmodule

For unsigned addition the plain carry-out check is correct. It is specifically signed addition where it gives the wrong answer, which is why the previous article’s warnings matter here too.

Where this shows up

Saturation is the default assumption in audio and video DSP. A mixer summing several channels saturates rather than wraps, because a wrapped sample is a loud click while a saturated one sounds like ordinary analog clipping, unpleasant but not disruptive. Fixed-point accumulators in audio codecs, pixel blending in video, and the output stage of a PID controller all saturate by default for the same reason: a wrapped value is wrong in the worst possible direction. ARM (QADD, QSUB) and the RISC-V P extension have dedicated saturating instructions because DSP code needs this so often that checking in software one overflow at a time would cost too much. A saturating adder in RTL is the same primitive those instructions expose.

Practice Exercise

Build a saturating subtractor for signed 8-bit operands (diff = a - b, saturating instead of wrapping). Reuse the overflow detection pattern from the adder, adjusted for subtraction. Remember that a - b is a + (-b), so the overflow condition changes.

Solution
`timescale 1ns / 1ps
`default_nettype none

module sat_subtractor_signed #(parameter WIDTH = 8) (
    input  wire signed [WIDTH-1:0] a, b,
    output reg  signed [WIDTH-1:0] diff
);
    wire signed [WIDTH-1:0] raw_diff = a - b;
    // For subtraction, overflow occurs when a and b have DIFFERENT signs
    // and the result's sign does not match a's sign.
    wire overflow = (a[WIDTH-1] != b[WIDTH-1]) && (raw_diff[WIDTH-1] != a[WIDTH-1]);

    localparam signed [WIDTH-1:0] MAX_VAL = {1'b0, {(WIDTH-1){1'b1}}};
    localparam signed [WIDTH-1:0] MIN_VAL = {1'b1, {(WIDTH-1){1'b0}}};

    always @(*) begin
        if (overflow)
            diff = a[WIDTH-1] ? MIN_VAL : MAX_VAL;
        else
            diff = raw_diff;
    end
endmodule

The condition changes from “same sign in, opposite sign out” for addition to “different signs in, result sign not matching a” for subtraction, because subtracting b is adding its negation. A positive a minus a negative b can overflow high; a negative a minus a positive b can overflow low.

Addition and subtraction wrap silently past the representable range, which is fine for a counter and a problem for any signal that reaches a speaker, a display or an actuator. Signed overflow needs the same-sign-in, opposite-sign-out rule; once detected, saturation is a mux to the right boundary. In the next article we take the adder we have now made correct at the edges and make it fast enough to meet a clock target, by pipelining it.

โ† Signed Arithmetic Pitfalls Pipelining a Wide Adder โ†’