Skip to content
opokatechPublic

About

Simple type-safe physical units

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

Repository files navigation

Physical Units

A small header-only C++23 library of type-safe physical units, meant for embedded code.

Voltage v   = 3.3_V;
Current i   = (v - 2_V) / 330_Ohm;   // Ohm's law -> Current
Power   p   = v * i;                 // P = U * I  -> Power
auto    bad = v + i;                 // does not compile: different units
  • Type-safe: each quantity is its own type, so units cannot be mixed by accident.
  • Embedded-friendly: values are float (fast on MCUs with a single-precision FPU), no heap, no exceptions.
  • Compile-time: everything is constexpr, so constexpr Voltage maxSupply = 3.6_V; costs nothing at runtime.
  • No standard library needed: the headers include no system headers except <cstdint> in Time.hpp (a freestanding header, available with -ffreestanding).

Build

Requires CMake 3.21+ and a C++23 compiler with <print> for the example (GCC 14+, Clang 19+).

make          # list targets
make build    # configure and build the example and tests (PRESET=debug by default)
make run      # build and run the example
make test     # build the compile-time tests
make clean    # remove all build output
make run PRESET=release

The library itself is just the headers in src/: add that directory to your include path, or link the units CMake target.

In VS Code, open the folder, install the recommended extensions (clangd, CMake Tools, C/C++ for debugging), pick the Debug preset and press F5 to debug the example.

Units

Header Type Stored as Create Read Literals
Voltage.hpp Voltage V makeV, makemV getV, getmV _V, _mV
Current.hpp Current A makeA, makemA getA, getmA _A, _mA
Power.hpp Power W makeW, makekW getW, getkW _W, _kW
Resistance.hpp Resistance Ω makeOhm, makekOhm getOhm, getkOhm _Ohm, _kOhm
Acceleration.hpp Acceleration m/s² makeMps2, makeG getMps2, getG _mps2, _g
Percentage.hpp Percentage % makePercent getPercent, getNormalized (0..1) _pct
Temperature.hpp Temperature °C makeCelsius, makeKelvin, makeFahrenheit getCelsius, getKelvin, getFahrenheit _degC, _K, _F
TemperatureDelta.hpp TemperatureDelta °C makeCelsius getCelsius _dltC
Time.hpp Duration µs (integer) us, ms, s toUs, toMs _us, _ms, _s

Literals accept whole and fractional numbers (5_V, 3.3_V).

Standard gravity is Acceleration::G (1 g = 9.80665 m/s²). A raw accelerometer reading converts with the datasheet sensitivity, for example Acceleration::makeG(raw * 0.000061f) for 0.061 mg/LSB.

A value can only be created with a make… function or a literal; Voltage v = 3.3f; does not compile. A default-constructed value is zero (0 °C for Temperature).

Arithmetic

All units except Temperature support the same operations:

Voltage a = 1_V + 500_mV;        // + and - with the same unit
a *= 2.0f;                       // * and / by a number, in both orders
Voltage b = -a;
float ratio = 3_V / 2_V;         // same unit / same unit -> plain number (1.5)

Relations between units:

Expression Result
Voltage * Current Power
Power / Voltage Current
Power / Current Voltage
Resistance * Current Voltage
Voltage / Resistance Current
Voltage / Current Resistance
Percentage * X X, for example 75_pct * 2_kW == 1.5_kW

Comparing values

  • == / != tolerate float rounding. Two values are equal if they differ by at most Utils::RELATIVE_PRECISION (1e-5, about 5 significant digits) of the larger one. The tolerance scales with the value, so it works the same for µA and kW:

    1.1_V + 2.2_V == 3.3_V   // true, although the floats differ (3.30000019 vs 3.29999995)
    3.2995_V == 3.3_V        // false
    0_A == 1e-9 A            // false: nothing non-zero equals zero
  • isNear(other, tolerance) compares with an explicit tolerance, for example measured.isNear(3.3_V, 1_mV). Use it for "close enough" checks and for comparisons with zero.

  • <, <=, >, >= compare exactly, so two values that differ only by rounding can be both == and <.

Temperature

Temperature is an absolute temperature, TemperatureDelta is a difference between two temperatures. Adding or scaling absolute temperatures has no physical meaning, so only these operations exist:

Temperature      t    = 20_degC;
t += 5.5_dltC;                          // Temperature +/- TemperatureDelta -> Temperature
TemperatureDelta rise = 80_degC - t;    // Temperature - Temperature        -> TemperatureDelta
rise = rise / 2.0f;                     // a difference can be scaled

// 20_degC + 5_degC, 20_degC * 2.0f and -20_degC do not compile

Temperature::isNear takes a TemperatureDelta as tolerance: t.isNear(25_degC, 0.5_dltC). For == the relative tolerance is applied on the Kelvin scale (about ±3 mK at room temperature), so it also works near 0 °C.

The Celsius literal is _degC, not _C, because newlib's <ctype.h> defines _C as a macro.

Time

Time.hpp has Duration and TimePoint, both whole microseconds in a signed 64-bit integer. A 64-bit µs counter wraps after ~292,000 years, so comparing time points directly is safe.

Duration  timeout = 1500_ms;                  // also 1_s + 500_ms, Duration::ms(1500)
TimePoint start   = getCurrentTime();
if (elapsed(start, timeout)) { /* ... */ }    // getCurrentTime() - start >= timeout
delay(10_ms);

(2_s - 500_ms).toMs();                        // 1500
1999_us .toMs();                              // 1: toMs truncates

// TimePoint + TimePoint and Duration + 5 do not compile

Duration and TimePoint can be template parameters (template <Duration Timeout>); for that their value is a public member impl_detail_us_, which is not meant to be used directly.

getCurrentTime() and delay() are only declared: the application defines them, for example over a 64-bit tick counter and an RTOS sleep. Literals take whole numbers only (_us, _ms, _s).

Tests

tests/static_tests.cpp checks the library with static_assert, including that invalid operations such as Temperature + Temperature or Voltage + Current do not compile. If make test builds, all tests pass.

Adding a unit

A float unit derives from Quantity (Quantity.hpp), which provides all comparisons and arithmetic. The unit itself only adds its conversions and literals:

#pragma once

#include "Quantity.hpp"

// Stored in hertz.
class Frequency : public Quantity<Frequency>
{
public:
    constexpr Frequency() = default;

    static constexpr Frequency makeHz(float arg) { return Frequency(arg); }
    static constexpr Frequency makekHz(float arg) { return Frequency(arg * 1000); }

    constexpr float getHz() const { return value(); }
    constexpr float getkHz() const { return value() / 1000; }

private:
    constexpr explicit Frequency(float hertz) : Quantity(hertz) {}
};

constexpr auto operator""_Hz(long double v) { return Frequency::makeHz(static_cast<float>(v)); }
constexpr auto operator""_Hz(unsigned long long v) { return Frequency::makeHz(static_cast<float>(v)); }
constexpr auto operator""_kHz(long double v) { return Frequency::makekHz(static_cast<float>(v)); }
constexpr auto operator""_kHz(unsigned long long v) { return Frequency::makekHz(static_cast<float>(v)); }

Code style is defined in .clang-format.

About

Simple type-safe physical units

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages