Fixed-Point Models

Fixed and UFixed are the PyRogue Model types for fixed-point values. They are one of the most common sources of confusion because they are easy to read as “floating-point values stored in hardware.” That is not what they are doing.

The right mental model is:

  • Hardware stores an integer bit pattern.

  • binPoint says how many fractional bits that integer represents.

  • PyRogue converts between the stored integer and the Python float you see in the Variable API.

So a fixed-point value is really a scaled integer.

What bitSize And binPoint Mean

For pr.Fixed(bitSize, binPoint) and pr.UFixed(bitSize, binPoint):

  • bitSize is the total stored width.

  • binPoint is the number of fractional bits.

  • The value resolution is 2^-binPoint.

If binPoint is 8, then one integer count in hardware corresponds to 1 / 256 in the Python-facing value.

Examples:

  • pr.Fixed(16, 8) means a signed 16-bit integer interpreted with 8 fractional bits.

  • pr.UFixed(12, 4) means an unsigned 12-bit integer interpreted with 4 fractional bits.

In both cases, the binary point is not an extra stored bit. It is just the scaling rule used for conversion.

How Conversion Works

In the current implementation, fixed-point conversion is handled by the lower level Rogue memory path using modelId = rim.Fixed together with the Model’s bitSize, binPoint, and signedness. The Python Fixed and UFixed classes mainly supply that metadata.

The conversion rule is:

  • On write: store round(value * 2^binPoint) as the underlying integer.

  • On read: return stored_integer / 2^binPoint.

For signed Fixed, the stored integer uses two’s-complement interpretation. For UFixed, it is treated as unsigned.

This leads to two practical consequences:

  • Values are quantized to steps of 2^-binPoint.

  • A value that is not exactly representable is rounded to the nearest representable fixed-point step on write.

Numeric Range

For a signed Fixed(bitSize, binPoint) value:

  • Minimum representable value: -2^(bitSize - 1) / 2^binPoint

  • Maximum representable value: (2^(bitSize - 1) - 1) / 2^binPoint

For an unsigned UFixed(bitSize, binPoint) value:

  • Minimum representable value: 0

  • Maximum representable value: (2^bitSize - 1) / 2^binPoint

Examples:

  • pr.Fixed(16, 8) has resolution 1/256 and range -128.0 to 127.99609375.

  • pr.UFixed(12, 4) has resolution 1/16 and range 0.0 to 255.9375.

One detail that often surprises users is that the positive maximum is usually slightly smaller in magnitude than the negative minimum for signed fixed-point types. That is normal two’s-complement behavior.

Worked Example

import pyrogue as pr

class DspRegs(pr.Device):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.add(pr.RemoteVariable(
            name='Gain',
            description='Signed gain coefficient in Q1.15 format',
            offset=0x00,
            bitSize=16,
            bitOffset=0,
            mode='RW',
            base=pr.Fixed(16, 15),
            disp='{:.6f}',
        ))

Here Gain is a signed 16-bit fixed-point value with 15 fractional bits. That format is often called Q1.15.

Interpretation examples:

  • Python value 0.5 writes the integer 16384 because 0.5 * 2^15 = 16384.

  • Python value -0.25 writes the two’s-complement integer representing -8192.

  • A raw stored integer value of 24576 reads back as 0.75 because 24576 / 2^15 = 0.75.

For coefficient-style registers, this is usually the easiest way to reason about the model: first think about the scaled integer in hardware, then divide or multiply by 2^binPoint.

Common Mistakes

The mistakes users make most often are:

  • Treating bitSize as “integer bits” instead of total bits.

  • Treating binPoint as an extra stored field rather than a scaling rule.

  • Expecting arbitrary decimal values to round-trip exactly.

  • Forgetting that signed Fixed uses two’s-complement range and therefore has an asymmetric positive and negative limit.

  • Forgetting to set an explicit disp when operator-facing decimal formatting matters.

In practice, if you know the hardware format name, such as Q1.15 or Q5.11, convert it like this:

  • Total width is bitSize.

  • Fractional width is binPoint.

  • Use Fixed for signed formats and UFixed for unsigned formats.

If the register documentation instead gives a scale factor directly, choose binPoint so that the scale factor is 1 / 2^binPoint.

What To Explore Next

API Reference

  • Python Model base/reference: Model

  • C++ memory Model reference: Model