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, soconstexpr 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++).
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=releaseThe 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.
| 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).
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 |
-
==/!=tolerate float rounding. Two values are equal if they differ by at mostUtils::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 examplemeasured.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 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 compileTemperature::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.
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.500Pitfalls:
- 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 usestd::chrono::duration<float>(a) / b.- Avoid fractional literals such as
1.5s: they arelong doubledurations and do not convert tomillisecondsimplicitly. Write1500ms. - Scaling by a float (
timeout * 0.9f) gives a float duration; convert back withstd::chrono::round<std::chrono::milliseconds>(...).
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.
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.