嵌入式C++ constexpr进阶:把所有常量计算全部转移到编译阶段完成
嵌入式常量别再靠#define硬算。宏没有类型、没有作用域,复杂分频/CRC/查表容易写错;constexpr用普通C++函数让编译器在构建期算出结果,生成后直接进只读段,上电零初始化、不占RAM、不跑除法。
一、时钟分频与寄存器参数
UART、定时器分频若入参都是编译期常量,直接写成constexpr函数:
constexpr uint32_t uart_divisor(uint32_t pclk, uint32_t baud){
return (pclk + baud/2u) / baud; // 四舍五入,避免宏截断
}
constexpr uint32_t tim_presc(uint32_t timclk, uint32_t tick_hz){
uint32_t div = timclk / tick_hz;
return (div == 0u) ? 0u : (div - 1u);
}
constexpr auto USART_DIV = uart_divisor(72'000'000u, 115200u);
constexpr auto TICK_PSC = tim_presc(72'000'000u, 10'000u);
static_assert(USART_DIV >= 1u, "baud too high for current PCLK");
赋值给constexpr变量会强制编译期求值;结果在固件里就是常量,启动代码不再做除法。 多平台用if constexpr按芯片族走不同分频公式,未选中分支不进二进制。
二、CRC/校验表全编译期生成
通信协议最适合。CRC8多项式0x07、MSB优先,256项表编译期展开:
#include <array>
#include <cstdint>
constexpr std::array<uint8_t,256> make_crc8_table(){
std::array<uint8_t,256> t{};
for(uint16_t i=0;i<256;++i){
uint8_t c = static_cast<uint8_t>(i);
for(int k=0;k<8;++k){
c = (c & 0x80) ? static_cast<uint8_t>((c<<1) ^ 0x07)
: static_cast<uint8_t>(c<<1);
}
t[i] = c;
}
return t;
}
constexpr auto CRC8_TAB = make_crc8_table();
static_assert(CRC8_TAB[0]==0x00 && CRC8_TAB[0x07]==0x07, "crc8 table error");
constexpr uint8_t crc8(const uint8_t* d, uint16_t n){
uint8_t r=0xFF;
for(uint16_t i=0;i<n;++i) r = CRC8_TAB[(r^d[i]) & 0xFF];
return r;
}
表进.rodata/Flash,运行时只查不算是常见做法; 固定固件版本号可在编译期直接出CRC,static_assert比对预期值,错一个字节构建就失败。
三、协议偏移、数组尺寸、配置结构体
Modbus、CAN、私有串口帧的字段偏移用constexpr算,避免手填魔术数:
constexpr uint16_t HDR_LEN = 4;
constexpr uint16_t PAYLOAD = 32;
constexpr uint16_t FRAME_LEN = HDR_LEN + PAYLOAD + 2; // +CRC
constexpr uint16_t PAYLOAD_OFF = HDR_LEN;
std::array<uint8_t,FRAME_LEN> tx_buf{};
static_assert(FRAME_LEN % 4u == 0u, "align frame to 4 bytes");
结构体配置也可用constexpr构造,全部字面量成员、无虚函数、无动态分配,结果可直接固化:
struct SensorCfg {
uint8_t id;
uint16_t fs; // 满量程
int16_t offset;
constexpr SensorCfg(uint8_t i,uint16_t f,int16_t o):
id(i),fs(f),offset(o){}
};
constexpr SensorCfg temp_cfg(0x21, 3300, -12);
编译期布局后配合alignas可控制对齐,只读对象不占RAM。
四、C++20 consteval强制不落地运行时
若担心被人传变量退化成运行时,用立即函数:
consteval uint32_t pwm_arr(uint32_t clk, uint32_t freq){
return (clk / freq) - 1u; // 仅编译期,传变量直接报编错
}
constexpr auto ARR_1K = pwm_arr(72'000'000u, 1000u);
consteval比constexpr更硬:不是“可以编译期”,而是“必须编译期”,适合安全参数、校验值、硬件寄存器初值。
五、落地与避坑
constexpr函数被变量/模板/数组尺寸引用才会强制编译期;只普通调用且入参运行时,仍可生成运行代码,别误以为一定零开销。
大表、深循环会拖编译时间,GCC可用-fconstexpr-loop-limit调整;表生成先用运行版单测,再加constexpr和static_assert。
浮点近似(正弦表、PID系数)在M0无FPU要谨慎,编译期算好定点或Q格式再存表,运行期不碰float。
确认存放位置:用size/map看是否进.rodata或内联;某些工具链会把“可能被修改”的全局复制到RAM,关键表可加const、必要时用section属性锁到Flash。
替代宏不是全替代:寄存器和位掩码用constexpr类型安全,条件编译平台差异用if constexpr比#ifdef更利于静态检查。
把分频、CRC、帧格式、标定系数、查找表全部改constexpr/consteval,配合static_assert,MCU固件启动更短、RAM更省、协议参数错在编译阶段而不是现场出怪问题。





