Hardware Register Mapping with Quantity Points¶
Try it live on Compiler Explorer
Overview¶
This example demonstrates how to use quantity_point and custom origins to model hardware
measurements with both scaling and offset. It implements a realistic embedded scenario where
a 16-bit hardware register maps the voltage range [\(-10\ \mathrm{V}\), \(10\ \mathrm{V}\)]
to raw values [\(-32767\), \(32767\)].
As explained in The Affine Space,
measurements should be modeled as quantity_point rather than plain quantity when they
represent absolute values on a scale (like temperature, voltage level, or position).
Key Features Demonstrated¶
- Creating custom quantity point origins
- Defining offset units with scaling factors
- Handling hardware register mappings in embedded systems
- Converting between hardware units and SI units
Code Walkthrough¶
Defining the Voltage Range¶
We start by defining the actual measurement voltage range:
// real voltage range
inline constexpr int min_voltage = -10;
inline constexpr int max_voltage = 10;
inline constexpr int voltage_range = max_voltage - min_voltage;
Next, we define the hardware representation characteristics:
// hardware encoding of voltage
using voltage_hw_t = std::uint16_t;
inline constexpr voltage_hw_t voltage_hw_error = std::numeric_limits<voltage_hw_t>::max();
inline constexpr voltage_hw_t voltage_hw_min = 0;
inline constexpr voltage_hw_t voltage_hw_max = voltage_hw_error - 1;
inline constexpr voltage_hw_t voltage_hw_range = voltage_hw_max - voltage_hw_min;
inline constexpr voltage_hw_t voltage_hw_zero = voltage_hw_range / 2;
Creating a Custom Origin and Offset Unit¶
The key part is defining a custom origin and an offset unit that handles both scaling and translation:
inline constexpr struct hw_voltage_origin final :
relative_point_origin<point<si::volt>(min_voltage)> {} hw_voltage_origin;
inline constexpr struct hw_voltage_unit final :
named_unit<"hwV", mag_ratio<voltage_range, voltage_hw_range> * si::volt, hw_voltage_origin> {} hw_voltage_unit;
using hw_voltage_quantity_point = quantity_point<hw_voltage_unit, hw_voltage_origin, voltage_hw_t>;
Here's what's happening:
hw_voltage_origin: A custom point origin representing -10 V (the hardware's zero point)hw_voltage_unit: An offset unit that:- Scales by the conversion factor (20 V / 65534 counts)
- Uses
hw_voltage_originas its origin
hw_voltage_quantity_point: Aquantity_pointtype using this offset unit
This setup automatically handles both the scaling (converting counts to volts) and the offset (shifting the zero point from -10 V to 0 V).
Reading Hardware Values¶
Now we can simulate reading from a hardware register:
// mapped HW register
volatile voltage_hw_t hw_voltage_value;
std::optional<hw_voltage_quantity_point> read_hw_voltage()
{
voltage_hw_t local_copy = hw_voltage_value;
if (local_copy == voltage_hw_error) return std::nullopt;
return point<hw_voltage_unit>(local_copy);
}
Working with Volatile Hardware Registers¶
The hardware register is declared as volatile to prevent compiler optimizations that could
eliminate or reorder hardware reads:
Why the local copy?
Direct use of volatile values in expressions can cause multiple hardware reads.
The pattern used here:
- Makes exactly one read from the hardware register into a non-volatile local variable
- Checks for error conditions (invalid hardware state)
- Constructs the quantity point only if the value is valid
This is the standard embedded practice for working with hardware registers:
voltage_hw_t local_copy = hw_voltage_value; // Single volatile read
if (local_copy == voltage_hw_error) return std::nullopt;
return point<hw_voltage_unit>(local_copy); // Use non-volatile copy
Key Benefits:
- Predictable behavior: Exactly one hardware access per call
- Efficiency: No redundant register reads
- Safety: Error checking before constructing typed quantities
- Library compatibility:
quantity_pointconstructors work with non-volatile values
Volatile and mp-units
The mp-units library works seamlessly with values read from volatile memory.
Simply copy the volatile value to a local variable before constructing quantities
or quantity points. This pattern is standard in embedded systems and provides the
best balance of safety and efficiency.
Output¶
The example reads three hardware values and displays them in both hardware units and SI volts:
int main()
{
// simulate reading of 3 values from the hardware
hw_voltage_value = voltage_hw_min;
quantity_point qp1 = read_hw_voltage().value();
hw_voltage_value = voltage_hw_zero;
quantity_point qp2 = read_hw_voltage().value();
hw_voltage_value = voltage_hw_max;
quantity_point qp3 = read_hw_voltage().value();
print(qp1);
print(qp2);
print(qp3);
}
Output:
Notice how:
- Hardware value
0corresponds to-10 V(the minimum of the range) - Hardware value
32767corresponds to0 V(the midpoint) - Hardware value
65534corresponds to10 V(the maximum of the range)
Why Quantity Points Matter¶
This example illustrates why quantity_point is essential for real-world measurements:
- Prevents Errors: You can't accidentally add two absolute voltage measurements
(e.g.,
5 V + 3 Vdoesn't make physical sense for absolute readings) - Handles Offsets: The zero of the hardware scale differs from the zero of the physical scale
- Type Safety: The compiler ensures correct conversions between coordinate systems
- Embedded-Friendly: Minimal runtime overhead while maintaining safety
Practical Applications¶
This pattern is useful for:
- ADC readings with offset calibration
- Temperature sensors (especially with non-zero reference points)
- Position encoders with custom datum points
- Pressure sensors with atmospheric offset
- Any measurement device with non-standard zero reference
Related Concepts¶
- The Affine Space - Detailed explanation of quantity points