Skip to content
This repository has been archived by the owner on Jan 7, 2023. It is now read-only.

Q Format

Dan McLeran edited this page Jul 9, 2020 · 26 revisions

Introduction

Q-Format is a binary, fixed-point number format. Tinymind contains a template library which allows us to specify the number of integer as well as the number of fractional bits as template parameters. Q-Format can be used in lieu of floating point numbers when fixed-point precision is adequate (or when we don't have an FPU at all!). See Q-Format for a deep dive.

Tinymind QValue

Tinymind contains a C++ template library for defining and using Q format values. The template class is called QValue. The template declaration is here:

template<
        unsigned NumFixedBits,
        unsigned NumFractionalBits,
        bool QValueIsSigned,
        template<typename, unsigned> class QValueRoundingPolicy = TruncatePolicy
        >
struct QValue
{
...

NumFixedBits - Number of bits for the integer portion of the fixed-point value.

NumFractionalBits - Number of bits for the fractional portion of the fixed-point value.

QValueIsSigned - true - 1 bit reserved in the integer portion for the sign bit, false - no sign bit

QValueRoundingPolicy - A template template parameter which specifies a policy class to handle rounding. Tinymind provides 2 options but you can define your own as well. One option which tinymind provides is TruncatePolicy. This rounds the Q format value by dropping the lower bits (e.g. integer division does this). The other option which tinymind provides is RoundUpPolicy. RoundUpPolicy rounds up to the nearest fractional value.

Example Use

Using the template, we can declare types for virtually any kind of Q-Format quantity. Some examples from the unit test (qformat_unit_test.cpp):

typedef tinymind::QValue<24, 8, true> SignedQ24_8Type; // 24 bits of integer, 8 bits of fractional, signed
typedef tinymind::QValue<16, 16, true> SignedQ16_16Type; // 16 bits of integer, 16 bits of fractional, signed
typedef tinymind::QValue<24, 8, false> UnSignedQ24_8Type; // 24 bits of integer, 8 bits of fractional, unsigned
typedef tinymind::QValue<8, 24, true> SignedQ8_24Type; // 8 bits of integer, 24 bits of fractional, signed

Instance of this type can be declared and initialized like any other class:

SignedQ24_8Type Q8(-1, 0);

Here we declare and initialize a signed Q24.8 value to its representation of negative one. We can use this variable as if it were a plain old integer:

SignedQ24_8Type Q6(-1, 0);
SignedQ24_8Type Q7(1, 0);
SignedQ24_8Type Q8;
...
Q8 = Q6 + Q7;
BOOST_TEST(static_cast<SignedQ8_24Type::FullWidthValueType>(0x0) == Q8.getValue());

Tinymind QValue Class Diagram

QValue uses several classes within qformat.hpp to accomplish its goals. The relationship between the classes is presented here.

Compile-Time Type Selection

QValue uses compile-time type selection to choose the minimally-sized types for: Fixed part, fractional part, as well as the whole value representation (fixed + fractional).

template<
        unsigned NumFixedBits,
        unsigned NumFractionalBits,
        bool QValueIsSigned,
        template<typename, unsigned> class QValueRoundingPolicy = TruncatePolicy
        >
struct QValue
{
...
    typedef typename QTypeChooser<NumberOfFixedBits, NumberOfFractionalBits, IsSigned>::FullWidthValueType                     FullWidthValueType;
    typedef typename QTypeChooser<NumberOfFixedBits, NumberOfFractionalBits, IsSigned>::FixedPartFieldType                     FixedPartFieldType;
    typedef typename QTypeChooser<NumberOfFixedBits, NumberOfFractionalBits, IsSigned>::FractionalPartFieldType                FractionalPartFieldType;
    typedef typename QTypeChooser<NumberOfFixedBits, NumberOfFractionalBits, IsSigned>::FullWidthFieldType                     FullWidthFieldType;
    typedef typename QTypeChooser<NumberOfFixedBits, NumberOfFractionalBits, IsSigned>::MultiplicationResultFullWidthFieldType MultiplicationResultFullWidthFieldType;
    typedef typename QTypeChooser<NumberOfFixedBits, NumberOfFractionalBits, IsSigned>::DivisionResultFullWidthFieldType       DivisionResultFullWidthFieldType;
    typedef QValueRoundingPolicy<MultiplicationResultFullWidthFieldType, NumberOfFractionalBits>                               RoundingPolicy;

As you can see from the code snippet above, it does this via a template class whose purpose is to choose the optimal types, QTypeChooser.

template<unsigned NumFixedBits, unsigned NumFractionalBits, bool IsSigned>
struct QTypeChooser
{
    typedef typename FullWidthFieldTypeChooser<NumFixedBits + NumFractionalBits, IsSigned>::FixedPartFieldType                                         FixedPartFieldType;
    typedef typename FullWidthFieldTypeChooser<NumFixedBits + NumFractionalBits, IsSigned>::FractionalPartFieldType                                    FractionalPartFieldType;
    typedef typename FullWidthFieldTypeChooser<NumFixedBits + NumFractionalBits, IsSigned>::FullWidthFieldType                                         FullWidthFieldType;
    typedef typename FullWidthFieldTypeChooser<NumFixedBits + NumFractionalBits, IsSigned>::FullWidthValueType                                         FullWidthValueType;
    typedef typename MultiplicationResultFullWidthFieldTypeChooser<NumFixedBits + NumFractionalBits, IsSigned>::MultiplicationResultFullWidthFieldType MultiplicationResultFullWidthFieldType;
    typedef typename DivisionResultFullWidthFieldTypeChooser<NumFixedBits + NumFractionalBits, IsSigned>::DivisionResultFullWidthFieldType             DivisionResultFullWidthFieldType;
    };

QTypeChooser adds the number of fixed bits and number of fractional bits and passes that value, along with the signed flag, to FullWidthFieldTypeChooser. At bottom, everyone uses the class FullWidthFieldTypeChooser for selecting types. FullWidthFieldTypeChooser analyzes the template parameters for number of fixed bits + number of fractional bits as well as the sign bit at compile time and make the type selection.

template<unsigned NumBits, bool IsSigned>
struct FullWidthType
{};

template<>
struct FullWidthType<8, false>
{
    typedef uint8_t FractionalPartFieldType;
    typedef uint8_t FixedPartFieldType;
    typedef uint8_t FullWidthFieldType;
    typedef uint8_t FullWidthValueType;
};

template<>
struct FullWidthType<8, true>
{
    typedef uint8_t FractionalPartFieldType;
    typedef int8_t  FixedPartFieldType;
    typedef uint8_t FullWidthFieldType;
    typedef int8_t  FullWidthValueType;
};

template<>
struct FullWidthType<16, false>
{
    typedef uint16_t FractionalPartFieldType;
    typedef uint16_t FixedPartFieldType;
    typedef uint16_t FullWidthFieldType;
    typedef uint16_t FullWidthValueType;
};

template<>
struct FullWidthType<16, true>
{
    typedef uint16_t FractionalPartFieldType;
    typedef int16_t  FixedPartFieldType;
    typedef uint16_t FullWidthFieldType;
    typedef int16_t  FullWidthValueType;
};

template<>
struct FullWidthType<32, false>
{
    typedef uint32_t FractionalPartFieldType;
    typedef uint32_t FixedPartFieldType;
    typedef uint32_t FullWidthFieldType;
    typedef uint32_t FullWidthValueType;
};

template<>
struct FullWidthType<32, true>
{
    typedef uint32_t FractionalPartFieldType;
    typedef int32_t  FixedPartFieldType;
    typedef uint32_t FullWidthFieldType;
    typedef int32_t  FullWidthValueType;
};

template<>
struct FullWidthType<64, false>
{
    typedef uint64_t FractionalPartFieldType;
    typedef uint64_t FixedPartFieldType;
    typedef uint64_t FullWidthFieldType;
    typedef uint64_t FullWidthValueType;
};

template<>
struct FullWidthType<64, true>
{
    typedef uint64_t FractionalPartFieldType;
    typedef int64_t  FixedPartFieldType;
    typedef uint64_t FullWidthFieldType;
    typedef int64_t  FullWidthValueType;
};

template<unsigned NumBits, bool IsSigned>
struct FullWidthFieldTypeChooser
{
    static_assert(NumBits <= 64, "NumBits must be <= 64.");
    static const unsigned Result = (NumBits <= 8) ? 8 : (NumBits <= 16) ? 16 : (NumBits <= 32) ? 32 : 64;
    typedef typename FullWidthType<Result, IsSigned>::FractionalPartFieldType FractionalPartFieldType;
    typedef typename FullWidthType<Result, IsSigned>::FixedPartFieldType      FixedPartFieldType;
    typedef typename FullWidthType<Result, IsSigned>::FullWidthFieldType      FullWidthFieldType;
    typedef typename FullWidthType<Result, IsSigned>::FullWidthValueType      FullWidthValueType;
};

The representation of the Q-Format value is stored within QValue as a union.

union
{
    struct
    {
        FractionalPartFieldType mFractionalPart : NumberOfFractionalBits;
        FixedPartFieldType mFixedPart : NumberOfFixedBits;
    };
    FullWidthFieldType mValue;
};

This allows us to operate upon Q-Format values as if they were regular integers. Most of the code within qformat.hpp is providing the operator overloads necessary to treat QValues as if they were regular integers.

Let's look at an example. From the unit test (qformat_unit_test.cpp):

typedef tinymind::QValue<8, 8, false, tinymind::RoundUpPolicy> UnsignedQ8_8Type;

This defines a Q-Format type which uses 8 bits to represent the fixed portion as well as 8 bits to represent the fractional portion of the Q-Format value. We have chosen unsigned via the template parameter false. The rounding policy is overridden from the default of TruncatePolicy to RoundUpPolicy. By inspecting the QTypeChooser code you can see that we will choose a uint16_t for both fixed part as well as fractional part of the Q-Format value. Because we are using 8 bits for the fractional part, there are 256 unique values between 0 and 1. Our resolution is calculated to be 2^-8 or 0.00390625 (in floating point). To represent the value 1.5 in this format, we would write it as 0x180. This is the value 1 left-shifted by the number of fractional bits + 1/2 of the range provided by the fractional bits (i.e. 2^8 >> 1 or 2^7, which is 0x80).

Using QValues

From the unit test (qformat_unit_test.cpp) we can study how QValues are used. Since we should be supporting all common mathematical operators, we can treat them as if they are normal PODs (plain-old data).

Addition

We can add both other QValues of the same type as well as integers which have the same underlying representation of the full value. See test_case_addition in qformat_unit_test.cpp.

BOOST_AUTO_TEST_CASE(test_case_addition)
{
    UnsignedQ8_8Type uQ0(0, 0);
    UnsignedQ8_8Type uQ1(0, 1);
    UnsignedQ8_8Type uQ2(1, 1);
    UnsignedQ8_8Type uQ3;
...
    uQ0 += 0;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0) == uQ0.getValue());

    uQ0 += uQ0;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0) == uQ0.getValue());

    uQ0 += uQ1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(1) == uQ0.getValue());

    uQ0 += 1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(2) == uQ0.getValue());

    uQ0 += uQ2;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x103) == uQ0.getValue());

    uQ3 = uQ1 + uQ2;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x102) == uQ3.getValue());

QValues can be added to each other as well as to integer constants.

Subtraction

Subtraction is a similar story. We can subtract QValues form other QValues of the same type as well as subtract integers from QValues. See test_case_subtraction in qformat_unit_test.cpp.

BOOST_AUTO_TEST_CASE(test_case_subtraction)
{
    UnsignedQ8_8Type uQ0(1, 1);
    UnsignedQ8_8Type uQ1(0, 1);
    UnsignedQ8_8Type uQ2(0, 0xFF);
    UnsignedQ8_8Type uQ3;
...
    uQ3 = uQ0 - uQ1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x100) == uQ3.getValue());

    uQ3 = uQ3 - uQ2;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x1) == uQ3.getValue());

    uQ0 -= 0;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x101) == uQ0.getValue());

    uQ0 -= uQ1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x100) == uQ0.getValue());

    uQ0 -= 1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0xFF) == uQ0.getValue());

    uQ0 -= uQ2;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0) == uQ0.getValue());
...

Pre/Post Increment and Decrement

Both pre and post increment and decrement are supported for both signed as well as unsigned Q-Format types. See test_case_increment_decrement in qformat_unit_test.cpp.

BOOST_AUTO_TEST_CASE(test_case_increment_decrement)
{
    UnsignedQ8_8Type uQ0(0, 0);
    UnsignedQ8_8Type uQ2(-1, 0);
...
    --uQ0;
    BOOST_TEST(uQ2.getValue() == uQ0.getValue());

    ++uQ0;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0) == uQ0.getValue());

    uQ0--;
    BOOST_TEST(uQ2.getValue() == uQ0.getValue());

    uQ0++;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0) == uQ0.getValue());

Multiplication

QValues can be multiplied by each other as well as by integer values, just like normal PODs. See test_case_multiplication in qformat_unit_test.cpp.

BOOST_AUTO_TEST_CASE(test_case_multiplication)
{
    UnsignedQ8_8Type uQ0(1, 1);
    UnsignedQ8_8Type uQ1(1, 0);
    UnsignedQ8_8Type uQ2;
...
    uQ2 = uQ0 * uQ1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x101) == uQ2.getValue());

    uQ2 = uQ1 * 2;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x200) == uQ2.getValue());

    uQ2 = uQ0 * 1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x101) == uQ2.getValue());

Multiplication by other QValues of the same type, as well as by integer constants, is supported.

Division

QValues can be divided by each other as well as by integers just like normal PODs. See test_case_division in qformat_unit_test.cpp.

BOOST_AUTO_TEST_CASE(test_case_division)
{
    UnsignedQ8_8Type uQ0(1, 1);
    UnsignedQ8_8Type uQ1(1, 0);
    UnsignedQ8_8Type uQ2;
    UnsignedQ8_8Type uQ3(2, 0);
    UnsignedQ8_8Type uQ4(0, 0x80);
...
    uQ2 = uQ0 / uQ1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x101) == uQ2.getValue());

    uQ2 = uQ0 / 1;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x101) == uQ2.getValue());

    uQ2 = uQ1 / 2;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x80) == uQ2.getValue());

    uQ2 = uQ1 / uQ3;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x80) == uQ2.getValue());

    uQ2 = uQ1 / uQ4;
    BOOST_TEST(static_cast<UnsignedQ8_8Type::FullWidthValueType>(0x200) == uQ2.getValue());

Comparators

QValues can be compared against other QValues as well as against integers. See test_case_comparators in qformat_unit_test.cpp.

BOOST_AUTO_TEST_CASE(test_case_comparators)
{
    UnsignedQ8_8Type uQ0(1, 1);
    UnsignedQ8_8Type uQ1(1, 0);
    UnsignedQ8_8Type uQ2(1, 0);
...
    int one = 1;
    int negativeOne = -1;
    int bigValue = 100;
    int smallValue = -100;
...
    BOOST_TEST(uQ0 > uQ1);
    BOOST_TEST(uQ1 < uQ0);
    BOOST_TEST(uQ2 <= uQ1);
    BOOST_TEST(uQ2 >= uQ1);
    BOOST_TEST((uQ2 <= one));
    BOOST_TEST((uQ2 <= bigValue));
    BOOST_TEST((uQ2 < bigValue));

Comparison against other QValues as well as integer constants is supported.

Conversion

QValues can be assigned values after conversion from another QValue type. See test_case_conversion in qformat_unit_test.cpp. To support conversion, QValue defines a template method

template<typename OtherQValueType>
void convertFromOtherQValueType(const OtherQValueType& other)
{
    typedef typename ShiftPolicy<
                                typename OtherQValueType::FractionalPartFieldType,
                                FractionalPartFieldType,
                                OtherQValueType::NumberOfFractionalBits,
                                NumberOfFractionalBits>::ShiftPolicyType ShiftPolicyType;
    mFixedPart = static_cast<FixedPartFieldType>(other.getFixedPart());
    mFractionalPart = ShiftPolicyType::shift(other.getFractionalPart());
}

This method makes use of compile-time type selection to choose the appropriate shift policy to apply when converting from some other QValue type to the QValue type being assigned.

BOOST_AUTO_TEST_CASE(test_case_conversion)
{
    SignedQ8_8Type Q0;
    SignedQ8_24Type Q1(1, 0);
    SignedQ8_24Type Q2(1, (1 << 23));
    SignedQ16_16Type Q3(1, (1 << 15));
    SignedQ16_16Type Q4(-1, (1 << 15));
    SignedQ24_8Type Q5(1, (1 << 7));
    SignedQ24_8Type Q6(-1, (1 << 7));
    UnSignedQ24_8Type UQ0(1, (1 << 7));

    BOOST_TEST(Q0.getValue() == 0x0);
    Q0.convertFromOtherQValueType(Q1);
    BOOST_TEST(Q0.getValue() == 0x100);

    Q0.convertFromOtherQValueType(Q2);
    BOOST_TEST(Q0.getValue() == 0x180);

    Q0.convertFromOtherQValueType(Q3);
    BOOST_TEST(Q0.getValue() == 0x180);

    Q0.convertFromOtherQValueType(Q4);
    BOOST_TEST(Q0.getValue() == static_cast<typename SignedQ8_8Type::FullWidthValueType>(0xFF80));

Conclusion

QValues represent Q-Format numbers for systems which either do not have floating point or for scenarios where a fixed point resolution is sufficient. QValues can be treated as normal PODs. Addition, subtraction, division, and multiplication can all be performed upon QValues as if they were plain old integers.