Module math

This document contains technical documentation for the math module. To browse the source code, visit the repository on GitHub.

This module contains a contains entities and packages for common math operations in VHDL.

math_pkg.vhd

View source code on GitHub.

Package with some common mathematical functions.

saturate_signed.vhd

View source code on GitHub.

component saturate_signed is
  generic (
    input_width : positive;
    result_width : positive range 1 to input_width;
    enable_output_register : boolean
  );
  port (
    clk : in std_ulogic;
    --# {{}}
    input_valid : in std_ulogic;
    input_value : in u_signed;
    --# {{}}
    result_valid : out std_ulogic;
    result_value : out u_signed;
    result_is_saturated : out std_ulogic
  );
end component;

Saturate a signed number by removing MSBs. Typically used in fixed-point applications after performing arithmetic operations, such as addition or multiplication. Or to clamp a value from a third source to a certain range.

In digital arithmetic scenarios, the concept of guard bits is commonly used, and the input value will be of the form:

input_value = S G G G N N N N N N N N

Where S is the sign bit, and G are a guard bits. Note that the number of guard bits, three in the example above, is the difference between input_width and result_width.

Pseudo code

This entity performs the following operation, which is equivalent to a range clamping with power-of-two limits:

min_value = - 2 ** (result_width - 1)
max_value = 2 ** (result_width - 1) - 1
if input_value < min_value:
  return min_value
if input_value > max_value:
  return max_value
return input_value

Fixed-point implementation

The pseudo code above is efficiently implemented in digital logic by looking at the guard bits and the sign bit. If any of them have different value, the input value is outside the result range. If the sign bit is ‘1’, the input is negative, and the result should be greatest negative value possible. If the sign bit is ‘0’, the input is positive, and the result should be the greatest positive value possible.

Note that you have to choose your number of guard bits carefully in any upstream arithmetic operation. If you have too few guard bits, the value might already have wrapped around, and the saturation will not work as expected. This is all dependent on the details of your application.

truncate_round_signed.vhd

View source code on GitHub.

component truncate_round_signed is
  generic (
    input_width : positive;
    result_width : positive range 1 to input_width;
    convergent_rounding : boolean;
    enable_addition_register : boolean;
    enable_saturation : boolean;
    enable_saturation_register : boolean
  );
  port (
    clk : in std_ulogic;
    --# {{}}
    input_valid : in std_ulogic;
    input_value : in u_signed;
    --# {{}}
    result_valid : out std_ulogic;
    result_value : out u_signed;
    result_overflow : out std_ulogic
  );
end component;

Truncate a numeric value by removing LSBs and rounding the result.

Rounding details

The truncation can be seen as removing a number of fractional bits from a fixed-point number. The result value will be the closest integer value to the fractional input value. If the value is exactly in the middle between two integers, the result will be

  1. The integer value of the two that is even, if convergent_rounding is true.

  2. The integer value of the two that is closest to positive infinity, if convergent_rounding is false.

Convergent rounding is the default rounding mode used in IEEE 754 and comes with some advantages, most notably that it yields no bias. Convergent mode does consume one more LUT and results in a longer critical path.

Overflow and saturation

If the input value is already at the maximum value, and the fractional value is such that a rounding upwards should happen, the addition will overflow and the result_overflow signal will read as 1. If the enable_saturation generic is set to true, the result will instead be saturated to the maximum value.

Alternative approach

One could sign-extend the input value with one guard bit, add then instantiate saturate_signed.vhd on the result. The netlist build showed, however, that this alternative approach results in more LUTs and longer critical path, for both wide and narrow words.

Resource utilization

This entity has netlist builds set up with automatic size checkers in module_math.py. The following table lists the resource utilization for the entity, depending on generic configuration.

Resource utilization for truncate_round_signed netlist builds.

Generics

Total LUTs

FFs

Maximum logic level

convergent_rounding = False

enable_addition_register = True

enable_saturation = True

enable_saturation_register = True

input_width = 32

result_width = 24

6

52

6

convergent_rounding = True

enable_addition_register = True

enable_saturation = True

enable_saturation_register = True

input_width = 32

result_width = 24

7

52

8

unsigned_divider.vhd

View source code on GitHub.

component unsigned_divider is
  generic (
    dividend_width : positive;
    divisor_width : positive
  );
  port (
    clk : in std_ulogic;
    --# {{}}
    input_ready : out std_ulogic;
    input_valid : in std_ulogic;
    dividend : in u_unsigned;
    divisor : in u_unsigned;
    --# {{}}
    result_ready : in std_ulogic;
    result_valid : out std_ulogic;
    quotient : out u_unsigned;
    remainder : out u_unsigned
  );
end component;

Calculates

dividend / divisor = quotient + remainder / divisor

This is a bit serial divider. Algorithm is the same as long division from elementary school, but with number base two. Latency scales linearly with dividend_width.