Skip to main content

Library for working with fixed-point numbers in SystemVerilog

Project description

svreal

Travis Status BuildKite Status Code Coverage License: MIT

svreal is a single-file SystemVerilog library that makes it easy to perform fixed-point operations in a synthesizable fashion in SystemVerilog. The exponent and alignment details are handled automatically, so the user is free to customize the format of each fixed-point signal in the design without inconvenience. For debugging range/resolution issues, the user can switch all signal types to a floating-point representation using a single define command-line option. Supported fixed-point operations include addition, subtraction, negation, multiplication, comparison, and conditional assignment.

Installation

  1. Clone the repository:
> git clone https://github.com/sgherbst/svreal.git
  1. Install the corresponding Python3 package:
> cd svreal
> pip install -e .

If you get a permissions error when running the pip command, you can try adding the --user flag. This will cause pip to install packages in your user directory rather than to a system-wide location.

Introduction

Simple example

Here's a simple svreal example to get started. Note that we only have to include a single file, "svreal.sv":

`include "svreal.sv"
`MAKE_REAL(a, 5.0);
`MAKE_GENERIC_REAL(b, 10.0, 42);
`ADD_REAL(a, b, c);
initial begin
    `FORCE_REAL(1.23, a);
    `FORCE_REAL(4.56, b);
    #(1ns);
    `PRINT_REAL(c);
end

This creates fixed-point signals a, b, and c and instantiates an adder that sums a and b into c. In the initial block, the values of a and b are set to "1.23" and "4.56" and then the value of "c" is printed. Hence, we'd expect that the value of c is around "5.79".

Fixed-point formatting

In svreal, fixed-point formats are generally determined automatically from the range of the signals they represent. In this case, the range of a is set to +/- 5.0, and the range of b is set to +/- 10.0. The width of a is not specified, so it defaults to `LONG_WIDTH_REAL (which is 25 unless overridden by the user). For b, the user has explicitly specified a width of 42 bits. In both cases, the exponent used in the representation is automatically determined from the the width and range, using the formula:

exponent = int(ceil(log2(range/(2^(width-1)-1))))

This method of selecting the exponent guarantees that the user-specified range can be represented given the width of the fixed-point value.

Since the user has not provided any formatting information for c, its range is automatically determined from the ranges of a and b. Since a is +/- 5.0 and b is +/- 10.0, the range of c is +/- 15.0. The width of c defaults to `LONG_WIDTH_REAL, and its exponent is calculated using the formula above.

Debugging

Suppose that the range of a is not sufficient to contain the value being assigned to it. Then a may contain an entirely wrong value due to overflow. In order to debug this problem, define the FLOAT_REAL flag (for example, using +define+FLOAT_REAL). This switches the real number representation from fixed-point to float-point, which helps the user to identify whether the fixed-point representation is the cause of the problem, or whether it is something else. In addition, this flag adds assertions to check if any real-number signal exceeds its specified range.

It is also possible to debug underflow issues using svreal. First set the FLOAT_REAL flag to switch to a floating-point representation. If the problem goes away, but there are no assertion errors indicating overflows, then the problem is likely due to inadequate resolution in one or more svreal signals. This theory can be validated by increasing `LONG_WIDTH_REAL and/or `SHORT_WIDTH_REAL. If increasing `SHORT_WIDTH_REAL helps, then one or more multiplication constants need to have higher resolution. Otherwise, one or more fixed-point signals or additive constants need more resolution.

Operations available

Here is a partial list of operations that can be performed with svreal:

Assignment, negation, and absolute value

`ASSIGN_REAL(in, out);
`NEGATE_REAL(in, out);
`ABS_REAL(in, out)

These operations take one input (first argument) and produce one output (second argument). Note that `ASSIGN_REAL should always be used in place of a raw assign statement. This is because `ASSIGN_REAL performs alignment as necessary.

Arithmetic operations

`MIN_REAL(a, b, out);
`MAX_REAL(a, b, out);
`ADD_REAL(a, b, out);
`SUB_REAL(a, b, out);
`MUL_REAL(a, b, out);

These operations take two inputs (first and second arguments) and produce one output (third argument). Note the ordering of the subtraction operation: `SUB_REAL(a, b, out) means "out := a - b".

Mux operations

`ITE_REAL(cond, val_if_true, val_if_false, out);

This is a handy operation when constructing conditional operations: if cond is "1", then val_if_true is muxed to out, otherwise if cond is "0", then val_if_false is muxed to out. Note that this is not implemented as a literal mux, but instead performs alignment as necessary. Hence, the formats of all three fixed-point numbers can be different.

Real <-> integer conversion

`REAL_TO_INT(in, $size(out), out);
`INT_TO_REAL(in, $size(in), out);

Sometimes it is necessary to convert a signed integer into an svreal type or vice versa.

The macro `REAL_TO_INT takes as its first argument an svreal type and as its third argument a logic signed type. The second argument is the width of the logic signed type (it's left up to the user whether this comes from $size, $bits, or from a parameter due to simulator quirks).

The macro `INT_TO_REAL takes as its first argument a logic signed type and as its third argument an svreal type. The third argument is the width of the logic signed type (it's left up to the user whether this comes from $size, $bits, or from a parameter due to simulator quirks).

Comparisons

`LT_REAL(lhs, rhs, out);
`LE_REAL(lhs, rhs, out);
`GT_REAL(lhs, rhs, out);
`GE_REAL(lhs, rhs, out);
`EQ_REAL(lhs, rhs, out);
`NE_REAL(lhs, rhs, out);

Comparisons always take two fixed-point numbers as the first two arguments, ordered as the left-hand side followed by the right-hand side. The third macro argument is the output, which is a single bit (type logic) with value "1" if the comparison is true and "0" if it is false.

Working with constants

`MAKE_CONST_REAL(const, name);
`ASSIGN_CONST_REAL(const, name);
`ADD_CONST_REAL(const, in, out);
`MUL_CONST_REAL(const, in, out);

Several functions are available to work with numeric constants. `MAKE_CONST_REAL creates a new svreal type and assigns the given real-number constant to it. Its width defaults to `LONG_WIDTH_REAL and the exponent is selected automatically based on the constant value. `ASSIGN_CONST_REAL is similar but does not declare a new svreal type; it simply assigns the constant to an existing fixed-point signal (performing the floating-to-fixed conversion at compile time).

`ADD_CONST_REAL and `MUL_CONST_REAL allow the user to add a constant to a number or multiply a constant by a number, respectively. There is one special caveat for `MUL_CONST_REAL, which is that it represents the constant with a fixed-point number of width `SHORT_WIDTH_REAL (18 unless overridden by the user). As a result, the user can cause `MUL_CONST_REAL to consume exactly one DSP block by picking `LONG_WIDTH_REAL and `SHORT_WIDTH_REAL to be the operand widths of the target FPGA's DSP multipliers.

Memory

`DFF_REAL(d, q, rst, clk, cke, init);

Fixed-point memory is implemented as a generic D-type flip-flop (DFF). The input to this flip-flop is d, and the output is q. Both are fixed-point types, but it's fine if they can have different formats.

The rst signals is a single active-high bit (type logic). It's a synchronous reset, and when active it causes q to take the value of init. init is simply a real-number value like "1.23".

Finally, clk and cke are single bit signals (type logic). clk is the clock input of the DFF (active on the rising edge), and cke is the clock enable signal (active high).

Assigning results to existing signals

Most operations have an alternate form that allows the user to assign the result of an operation to an existing fixed-point signal. This is indicated by the word INTO in the macro name. For example, suppose that we have defined three signals, a, b, and c, and want to assign the sum of a and b into c:

`MAKE_REAL(a, 10.0); // i.e., +/- 10
`MAKE_REAL(b, 21.0); // i.e., +/- 21
`MAKE_REAL(c, 32.0); // i.e., +/- 32
`ADD_INTO_REAL(a, b, c);

This special form of the "add" operation will not declare a new signal c, but instead will assign the result of the addition to the existing signal called c (performing alignment shifts as necessary, of course).

In this case, there is a risk that c may have been declared with insufficient range to hold the result. As mentioned before, this can be debugged using the FLOAT_REAL flag, which adds range assertions. So why would a user ever want to use the INTO form of the svreal macros? The most common case is that they are assigning to a signal that appears on the I/O list of the module. Alternatively, they may want to manually specify the range or resolution of the output signal. However, a better way to accomplish that is to use the GENERIC form of operations, as described in the next section.

Specifying output resolution

Most operations have an alternate form ending with GENERIC that allows the user to specify the width of the result. Since the range of the operation is determined automatically, this effectively controls the resolution of the operation. As an example, suppose we want to multiply two signals, but represent the output with more precision than the default. In that case we could write

`MAKE_REAL(a, 10.0); // i.e., +/- 10
`MAKE_REAL(b, 21.0); // i.e., +/- 21
`MUL_REAL_GENERIC(a, b, c, 40);

This means: multiply a and b and store the result in c with the appropriate alignment. The width of c is given the custom value "40", and the range of c is still determined automatically. As a result, the user can control the precision of intermediate results, and this in turn is useful when debugging underflow issues.

Passing fixed-point signals

Since compile-time parameters are used to store some of the fixed-point formatting information, some care must be taken when passing svreal signals through a hierarchy to ensure that information is not lost. Consider this example, in which an outer block instantiates a module that multiplies together two signals to produce an output:

`include "svreal.sv"
module inner #(
    `DECL_REAL(in0),
    `DECL_REAL(in1),
    `DECL_REAL(out)
) (
    `INPUT_REAL(in0),
    `INPUT_REAL(in1),
    `OUTPUT_REAL(out)
);
    `MUL_INTO_REAL(in0, in1, out);
endmodule
module outer;
    `MAKE_REAL(a, 10.0); // i.e., +/- 10
    `MAKE_REAL(b, 21.0); // i.e., +/- 21
    `MAKE_REAL(c, 32.0); // i.e., +/- 32

    inner #(
        `PASS_REAL(in0, a),
        `PASS_REAL(in1, b),
        `PASS_REAL(out, c)
    ) inner_i (
        .in0(a),
        .in1(b),
        .out(c)
    );
    ...
endmodule

There are a few things to observe here. First, the parameters and I/O list of the inner module, which has fixed-point I/O, has to be declared in a certain way. Every fixed-point number in the I/O list (regardless of whether it is an input or an output) needs to have a corresponding `DECL_REAL statement in the parameter list for the module. This declares all of the parameters needed for that fixed-point signal. Then, in the I/O list for the module, fixed-point inputs and outputs should be declared using `INPUT_REAL and `OUTPUT_REAL, respectively.

Going up one level to the outer block, observe that a special macro `PASS_REAL is needed to pass parameter information for the fixed-point signals a, b, and c into the inner module. The syntax of `PASS_REAL is meant to mimick using dot-notation to connect signals to a module instance; that is, the name of the port on the inner module comes first, followed by the name of the local signal. Finally, note that fixed-point signals are wired up in the I/O list using standard dot notation.

Using interfaces

Suppose you want to bundle svreal signals into an interface. This might make it easier to pass around groups of fixed-point numbers, or allow you to pass digital control signals along with the fixed point numbers. This task can be achieved using a set of special svreal macros.

Here's the simplest such interface, containing a single svreal signal and nothing else:

`include "svreal.sv"
interface svreal #(
    `INTF_DECL_REAL(value)
);
    `INTF_MAKE_REAL(value);
    modport in(`MODPORT_IN_REAL(value));
    modport out(`MODPORT_OUT_REAL(value));
endinterface

It looks similar to a module declaration that includes svreal signals, but there are a few key differences:

  1. INTF_DECL_REAL is used instead of DECL_REAL.
  2. Each svreal signal that has been declared in the parameter list needs a corresponding INTF_MAKE_REAL statement in the body of the interface.
  3. When declaring modports for the interface, the `MODPORT_IN_REAL and `MODPORT_OUT_REAL macros must be used to specify that a given svreal signal should be treated as an input or an output.

To work with an svreal signal contained in an interface, there are two options: alias the signal to a local name, or pass the signal into a submodule. Both methods are illustrated below.

Aliasing an svreal signal contained in an interface to a local name

This is likely the simpler method to use for handwritten code. As shown in the code sample below, the I/O list for mymod directly uses the modports of the svreal interface described in the previous section; the macros DECL_REAL, INPUT_REAL, and OUTPUT_REAL are not used. Inside the module body, however, the "value" signals are aliased to local names using the macros INTF_INPUT_TO_REAL and INTF_OUTPUT_TO_REAL, at which point all normal svreal macros can be used on the local names.

Crucially, the body of mymod needs to be wrapped in a generate block. This is required to avoid bugs in some simulator and synthesis tools related to reading properties out of interfaces.

module mymod (
    svreal.in a,
    svreal.in b,
    svreal.out c
); 
    generate
        `INTF_INPUT_TO_REAL(a.value, a_value);
        `INTF_INPUT_TO_REAL(b.value, b_value);
        `INTF_OUTPUT_TO_REAL(c.value, c_value);
        `MUL_INTO_REAL(a_value, b_value, c_value); 
    endgenerate
endmodule

Passing an svreal signal contained in an interface to a submodule

This method is more useful when automatically generating models that use svreal. In the code example below, observe that the outer module is just a wrapper for the inner module; it breaks out the svreal signals contained in interfaces and passes them through to the inner module directly. Hence, there is nothing special about the inner module; it contains no references to interfaces or interface macros.

In the outer module, however, there are two special requirements. First, as with the previous method, the body of the module must be wrapped in a generate block to ensure proper tool behavior. Second, when passing signals contained in interfaces, the INTF_PASS_REAL macro must be used instead of PASS_REAL.

Even though this method is more verbose, it can be handy for generated code, because it decouples implementation of the real-number module from the details of how the incoming signals are bundled. This allows for two simpler generators (model generator + wrapper generator) rather than one complicated generator.

module inner #(
    `DECL_REAL(a),
    `DECL_REAL(b),
    `DECL_REAL(c)
) (
    `INPUT_REAL(a),
    `INPUT_REAL(b),
    `OUTPUT_REAL(c)
);
    `MUL_INTO_REAL(a, b, c); 
endmodule
module outer (
    svreal.in a,
    svreal.in b,
    svreal.out c
);
    generate
        inner #(
            `INTF_PASS_REAL(a, a.value),
            `INTF_PASS_REAL(b, b.value),
            `INTF_PASS_REAL(c, c.value)
        ) inner_i (
            .a(a.value),
            .b(b.value),
            .c(c.value)
        );
    endgenerate
endmodule

Running the Tests

To test svreal, please make sure that at least one of the following simulators is in the system path:

  1. vivado
  2. ncsim
  3. vcs
  4. iverilog

Then make sure that pytest is installed. If it's not, run the following command:

> pip install pytest

Finally, run pytest on the tests directory (the "-xs" flag means "stop if there are any failures, and print output from tests as it is available."):

> pytest -xs tests/

This will run as many tests as possible given the tools installed on your system. For example, if vivado is installed, synthesis tests will be run, but if not, only the simulation-based tests will be run.

You can run a specific test of interest like this:

> pytest -xs tests/test_ops.py

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

svreal-0.1.3.tar.gz (10.1 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page