嵌入式现代 C++教程——编译期计算的妙用:查找表生成、状态机设计、单位与物理量计算¶
TL;DR¶
- 查找表(LUT):把复杂但可预测的映射在编译期计算好,运行时只做索引;用
constexpr+std::array实现,保持零运行时成本。 - 状态机(FSM):用
enum class+ 编译期表/constexpr校验过的转移表,动作用小而明确的函数指针或内联 lambda(避免虚表)。 - 单位/物理量:用模板标签 + 运算符重载建“静态类型的单位”,用
std::ratio做缩放,提供字面量,所有错误在编译期报错。
1. 编译期生成查找表(LUT)¶
咱们举一个例子:ADC 输出 0..4095,需要转换成温度(例如通过多项式拟合),或者把传感器非线性关系用插值表逼近。直接运行浮点计算会慢、占用 Flash/CPU。我们把多项式在编译期求值,生成 std::array<float, N>。
为了实现这个目的,我们可以——
- 使用
constexpr的 Horner 法评估多项式; - 返回
std::array<T, N>,索引对应输入值(或分段索引); - 通过
static_assert检验边界/预期值。
代码(可放 luts.hpp)¶
#pragma once
#include <array>
#include <cstddef>
// Horner 在 constexpr 中评估多项式
template<typename T, size_t M>
constexpr T horner(const T (&coeffs)[M], T x) {
T res = coeffs[0];
for (size_t i = 1; i < M; ++i) {
res = res * x + coeffs[i];
}
return res;
}
// 生成 LUT:输入从 0 .. (N-1) 映射到 f(x)(x 可以是归一化的输入)
template<typename T, size_t N, size_t M>
constexpr std::array<T, N> make_lut_from_poly(const T (&coeffs)[M], T x0, T x1) {
std::array<T, N> a{};
for (size_t i = 0; i < N; ++i) {
// 线性映射索引->x
T t = (N == 1) ? x0 : (x0 + (x1 - x0) * static_cast<T>(i) / static_cast<T>(N - 1));
a[i] = horner<T, M>(coeffs, t);
}
return a;
}
使用示例¶
假设我们有一个三次拟合:T(x) = a*x^3 + b*x^2 + c*x + d,要把输入电压 0..3.3V 的映射做成 256 点 LUT:
#include "luts.hpp"
#include <iostream>
constexpr size_t LUT_N = 256;
constexpr double coeffs[4] = { 0.0023, -0.01, 0.5, 10.0 }; // a,b,c,d
constexpr auto temp_lut = make_lut_from_poly<double, LUT_N>(coeffs, 0.0, 3.3);
// compile-time sanity check
static_assert(temp_lut.size() == LUT_N);
static_assert(temp_lut[0] == coeffs[3]); // x==0 -> d
int main() {
// 运行时只用索引即可
constexpr size_t adc_max = 4095;
size_t adc = 2048;
// 映射 ADC -> 索引(示例)
size_t idx = (adc * (LUT_N - 1)) / adc_max;
double temp = temp_lut[idx];
std::cout << "temp = " << temp << "\n";
}
make_lut_from_poly 在编译期计算每个点,生成静态常量数组(如果放在 .cpp 里,编译器会把它放到只读数据区)。优点是运行时零 CPU 开销,只要 LUT 足够描述函数(空间换时间)。
轻量可验证的状态机设计(Embedded-friendly FSM)¶
设计原则¶
- 状态与事件用
enum class,以便静态类型检查; - 转移表
constexpr定义,这样可以用static_assert校验覆盖性 / 无不合法转移; - 动作(action)要小、确定,可以用内联函数指针(或 function pointer),避免虚函数开销;
- 状态机运行时逻辑尽量是一个小
switch或直接查表。
例子:按键去抖状态机¶
状态:Idle, Bounce, Pressed
事件:EdgeDown, EdgeUp, Timeout
动作:启动计时器/报告按下/报告释放
代码实现(fsm.hpp)¶
#pragma once
#include <array>
#include <cstdint>
#include <utility>
// 状态与事件
enum class DebState : uint8_t { Idle=0, Bounce=1, Pressed=2, Count=3 };
enum class DebEvent : uint8_t { EdgeDown=0, EdgeUp=1, Timeout=2, Count=3 };
// 动作类型
using ActionFn = void(*)(void* ctx);
// 转移表条目
struct Transition {
DebState from;
DebEvent event;
DebState to;
ActionFn action; // 可为 nullptr
};
// 示例动作
inline void start_timer(void* ctx) { /* start timer with ctx */ }
inline void report_press(void* ctx) { /* notify */ }
inline void report_release(void* ctx) { /* notify */ }
// constexpr 转移表
constexpr Transition debounce_table[] = {
{DebState::Idle, DebEvent::EdgeDown, DebState::Bounce, start_timer},
{DebState::Bounce, DebEvent::Timeout, DebState::Pressed, report_press},
{DebState::Pressed,DebEvent::EdgeUp, DebState::Idle, report_release},
{DebState::Bounce, DebEvent::EdgeUp, DebState::Idle, nullptr},
};
constexpr size_t TRANS_COUNT = sizeof(debounce_table) / sizeof(debounce_table[0]);
// 小型状态机实现(表驱动)
struct Debouncer {
DebState state{DebState::Idle};
void* ctx{nullptr};
void handle(DebEvent ev) {
for (size_t i = 0; i < TRANS_COUNT; ++i) {
auto &t = debounce_table[i];
if (t.from == state && t.event == ev) {
state = t.to;
if (t.action) t.action(ctx);
return;
}
}
// 未找到转移:忽略或断言(取决于策略)
}
};
上面这个是一个玩具实现,如果,咱们需要在编译期检查每个 (state,event) 是否都有 handler,可以把表转成 constexpr 二维数组(std::array<std::array<TransitionOrEmpty, EventCount>, StateCount>)并 static_assert 无缺失。
单位与物理量的类型安全系统¶
在嵌入式里,单位搞错代价很高。C++ 的范型让我们能在编译期强制单位一致:Quantity<UnitTag, T>。不追逐完整的 units 库,而做简洁、可审计、易嵌入的实现。
- 每种基单位用空 struct 作为标签(
struct Meter);复合单位通过模板元编程得到(例如Meter / Second); - 运算符重载:加减仅同单位;乘除产生新单位类型;
- 提供常用字面量:
100.0_m、10.0_s; - 利用
std::ratio支持前缀(milli,kilo)在类型层面表达。
#pragma once
#include <ratio>
#include <type_traits>
// 基本单位标签
struct MeterTag {};
struct SecondTag {};
struct KilogramTag {};
// Quantity 模板:Unit 是一个类型表达单位,Rep 是数值类型
template<typename Unit, typename Rep = double>
struct Quantity {
Rep v;
constexpr explicit Quantity(Rep val) : v(val) {}
};
// helper 类型:乘除产生新 Unit 类型
template<typename U1, typename U2> struct UnitMul {};
template<typename U1, typename U2> struct UnitDiv {};
// 运算:同单位加减
template<typename U, typename R>
constexpr Quantity<U, R> operator+(Quantity<U, R> a, Quantity<U, R> b) {
return Quantity<U, R>(a.v + b.v);
}
template<typename U, typename R>
constexpr Quantity<U, R> operator-(Quantity<U, R> a, Quantity<U, R> b) {
return Quantity<U, R>(a.v - b.v);
}
// 乘法 -> 产生乘积单位
template<typename U1, typename U2, typename R>
constexpr Quantity<UnitMul<U1,U2>, R> operator*(Quantity<U1,R> a, Quantity<U2,R> b) {
return Quantity<UnitMul<U1,U2>, R>(a.v * b.v);
}
// 除法 -> 产生商单位
template<typename U1, typename U2, typename R>
constexpr Quantity<UnitDiv<U1,U2>, R> operator/(Quantity<U1,R> a, Quantity<U2,R> b) {
return Quantity<UnitDiv<U1,U2>, R>(a.v / b.v);
}
// 允许与标量相乘/除
template<typename U, typename R>
constexpr Quantity<U, R> operator*(Quantity<U,R> a, R s) { return Quantity<U,R>(a.v * s); }
template<typename U, typename R>
constexpr Quantity<U, R> operator*(R s, Quantity<U,R> a) { return Quantity<U,R>(a.v * s); }
template<typename U, typename R>
constexpr Quantity<U, R> operator/(Quantity<U,R> a, R s) { return Quantity<U,R>(a.v / s); }
// 便捷别名
using Meter = Quantity<MeterTag, double>;
using Second = Quantity<SecondTag, double>;
using Kilogram = Quantity<KilogramTag, double>;
// 字面量(constexpr)
constexpr Meter operator"" _m(long double v) { return Meter(static_cast<double>(v)); }
constexpr Second operator"" _s(long double v) { return Second(static_cast<double>(v)); }
constexpr Kilogram operator"" _kg(long double v) { return Kilogram(static_cast<double>(v)); }
如果你尝试把 d + t,编译器会报错 —— 这是我们要的:在编译期抓住单位混用。