Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 at all (they compile with -ffreestanding -nostdinc -nostdinc++).

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

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

Durations use plain std::chrono:

using namespace std::chrono_literals;

std::chrono::milliseconds timeout = 1500ms;
std::println("{:%H:%M:%S}", 1h + 30min + 90500ms);   // 01:31:30.500

Pitfalls:

  • A 32-bit millisecond tick (like HAL_GetTick()) wraps every ~49.7 days: compare elapsed time (now - start >= timeout), never time points or stored deadlines.
  • 3s / 2s == 1: dividing two durations truncates. For a ratio use std::chrono::duration<float>(a) / b.
  • Avoid fractional literals such as 1.5s: they are long double durations and do not convert to milliseconds implicitly. Write 1500ms.
  • Scaling by a float (timeout * 0.9f) gives a float duration; convert back with std::chrono::round<std::chrono::milliseconds>(...).

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