跳转至

嵌入式现代 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_m10.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)); }
auto d = 100.0_m;
auto t = 9.58_s;
auto speed = d / t; // type: UnitDiv<MeterTag, SecondTag>

如果你尝试把 d + t,编译器会报错 —— 这是我们要的:在编译期抓住单位混用。