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 except
<cstdint>inTime.hpp(a freestanding header, available with-ffreestanding).
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 |
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).
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.
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 compileDuration 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/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.