cpp-embedded
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEmbedded C and C++ Skill
嵌入式C和C++技能
Quick navigation
- Memory patterns, ESP32 heap fragmentation, ETL containers →
references/memory-patterns.md- C99/C11 patterns, UART read-to-clear, _Static_assert →
references/c-patterns.md- Debugging workflows (stack overflow, heap corruption, HardFault) →
references/debugging.md- Coding style and conventions →
references/coding-style.md- Firmware architecture, RTOS IPC, low-power, ESP32 deep sleep, CI/CD →
references/architecture.md- STM32 pitfalls, CubeMX, hard-to-debug issues, code review →
references/stm32-pitfalls.md- Design patterns, SOLID for embedded, HAL design, C/C++ callbacks →
references/design-patterns.md- MPU protection, watchdog hierarchy, safety-critical, protocols, linker scripts →
references/safety-hardware.md
快速导航
- 内存模式、ESP32堆碎片化、ETL容器 →
references/memory-patterns.md- C99/C11模式、UART读清除、_Static_assert →
references/c-patterns.md- 调试工作流(栈溢出、堆损坏、HardFault) →
references/debugging.md- 编码风格与规范 →
references/coding-style.md- 固件架构、RTOS进程间通信、低功耗、ESP32深度休眠、CI/CD →
references/architecture.md- STM32常见陷阱、CubeMX、难调试问题、代码评审 →
references/stm32-pitfalls.md- 设计模式、嵌入式SOLID原则、HAL设计、C/C++回调 →
references/design-patterns.md- MPU保护、看门狗层级、安全关键设计、协议、链接脚本 →
references/safety-hardware.md
Embedded Memory Mindset
嵌入式内存设计思路
Embedded systems have no OS safety net. A bad pointer dereference doesn't produce a polite segfault — it
silently corrupts memory, triggers a HardFault hours later, or hangs in an ISR. The stakes of every
allocation decision are higher than in hosted environments.
Three principles govern embedded memory:
Determinism over convenience. Dynamic allocation (malloc/new) is non-deterministic in both time and
failure mode. MISRA C Rule 21.3 and MISRA C++ Rule 21.6.1 ban dynamic memory after initialization.
Even outside MISRA, avoid heap allocation in production paths.
Size is known at compile time. Embedded software has a fixed maximum number of each object type.
Design around this. If you need 8 UART message buffers, declare 8 at compile time. Don't discover the
maximum at runtime.
ISRs are sacred ground. Never allocate, never block, never call non-reentrant functions from an ISR.
Keep ISRs minimal — set a flag or write to a ring buffer, then do the real work in a task.
嵌入式系统没有操作系统安全兜底。错误的指针解引用不会返回友好的段错误提示——它会静默损坏内存、数小时后触发HardFault,或者在ISR中挂起。每一个内存分配决策的风险都远高于托管环境。
嵌入式内存遵循三大原则:
确定性优先于便捷性。 动态分配(malloc/new)在时间消耗和故障模式上都不具备确定性。MISRA C规则21.3和MISRA C++规则21.6.1禁止初始化完成后使用动态内存。即使不遵循MISRA规范,也应避免在生产路径中使用堆分配。
大小在编译时确定。 嵌入式软件的各类对象最大数量是固定的,设计时应围绕这一点展开。如果你需要8个UART消息缓冲区,就在编译时声明8个,不要在运行时才确定最大值。
ISR是不可侵犯的区域。 永远不要在ISR中分配内存、阻塞、调用不可重入函数。保持ISR尽可能精简——仅设置标志位或写入环形缓冲区,后续实际处理逻辑放在任务中执行。
Allocation Decision Table
分配决策表
| Need | Solution | Notes |
|---|---|---|
| Short-lived local data | Stack | Keep < 256 bytes per frame; profile with |
| Fixed singleton objects | | Zero-initialized before |
| Fixed array of objects | Object pool ( | O(1) alloc/free, no fragmentation |
| Temporary scratch space | Arena / bump allocator | Reset whole arena at end of operation |
| Variable-size messages | Ring buffer of fixed-size slots | Simplest ISR-safe comms pattern |
| Custom lifetime control | Placement new + static storage | Full control, no heap involvement |
| Never in ISR | Any of the above except stack | Allocator calls are not ISR-safe |
| Avoid entirely | | Non-deterministic; fragmentation risk |
| 需求 | 解决方案 | 说明 |
|---|---|---|
| 短生命周期本地数据 | 栈 | 每帧栈占用保持在256字节以内;使用 |
| 固定单例对象 | 文件或函数作用域的 | |
| 固定对象数组 | 对象池( | O(1)时间复杂度分配/释放,无碎片化问题 |
| 临时暂存空间 | 竞技场/碰撞分配器 | 操作完成后整体重置竞技场 |
| 可变大小消息 | 固定大小槽位的环形缓冲区 | 最简单的ISR安全通信模式 |
| 自定义生命周期控制 | placement new + 静态存储 | 完全可控,不涉及堆 |
| ISR中禁止使用 | 除栈以外的所有分配方式 | 分配器调用不具备ISR安全性 |
| 完全避免使用 | | 非确定性;存在碎片化风险 |
Critical Patterns
关键模式
RAII Resource Guard
RAII资源守卫
Acquire on construction, release on destruction. Guarantees release even through early returns or exceptions
(if using exceptions — rare in embedded, but possible in C++ environments that allow them).
cpp
class SpiGuard {
public:
explicit SpiGuard(SPI_HandleTypeDef* spi) : spi_(spi) {
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
}
~SpiGuard() {
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
}
// Non-copyable, non-movable — guard is tied to this scope
SpiGuard(const SpiGuard&) = delete;
SpiGuard& operator=(const SpiGuard&) = delete;
private:
SPI_HandleTypeDef* spi_;
};
// Usage: CS deasserts automatically at end of scope
void read_sensor() {
SpiGuard guard(&hspi1);
// ... transfer bytes ...
} // CS deasserts here构造时获取资源,析构时释放资源。即使提前返回或触发异常也能保证资源释放(嵌入式场景很少用异常,但在允许异常的C++环境下也有效)。
cpp
class SpiGuard {
public:
explicit SpiGuard(SPI_HandleTypeDef* spi) : spi_(spi) {
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
}
~SpiGuard() {
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
}
// 不可复制、不可移动——守卫与当前作用域绑定
SpiGuard(const SpiGuard&) = delete;
SpiGuard& operator=(const SpiGuard&) = delete;
private:
SPI_HandleTypeDef* spi_;
};
// 使用示例:作用域结束时CS引脚自动取消断言
void read_sensor() {
SpiGuard guard(&hspi1);
// ... 传输字节 ...
} // 此处CS引脚自动取消断言ISR-to-Task Communication: Ring Buffer vs Ping-Pong DMA
ISR到任务通信:环形缓冲区 vs 乒乓DMA
For passing data from an ISR/DMA to a task, two main patterns exist:
- SPSC ring buffer (power-of-2 capacity with bitmask indexing) — best for variable-rate streams. See §3 for the full implementation. Always use
references/memory-patterns.mdor_Static_assertto validate the capacity is a power of 2.static_assert - Ping-pong (double) buffering — best for fixed-burst DMA transfers. ISR fills one buffer while task processes the other.
When asked for ISR-to-task communication, include a ring buffer with power-of-2 bitmask indexing as the primary pattern, even if ping-pong is also mentioned as an alternative for specific DMA scenarios.
将数据从ISR/DMA传递到任务有两种主流模式:
- SPSC环形缓冲区(容量为2的幂,使用位掩码索引)——最适合可变速率数据流。完整实现可参考第3节。始终使用
references/memory-patterns.md或_Static_assert验证容量是2的幂。static_assert - 乒乓(双)缓冲——最适合固定突发的DMA传输。ISR填充一个缓冲区的同时任务处理另一个缓冲区。
当用户询问ISR到任务通信方案时,优先推荐带2的幂位掩码索引的环形缓冲区作为主要方案,也可以提及乒乓缓冲作为特定DMA场景的替代方案。
Static Object Pool
静态对象池
Pre-allocates N objects of type T with O(1) alloc/free and no heap involvement.
Read §1 for the full arena allocator and §2 for CRTP patterns.
references/memory-patterns.mdcpp
template<typename T, size_t N>
class ObjectPool {
public:
template<typename... Args>
T* allocate(Args&&... args) {
for (auto& slot : slots_) {
if (!slot.used) {
slot.used = true;
return new (&slot.storage) T(std::forward<Args>(args)...);
}
}
return nullptr; // Pool exhausted — handle at call site
}
void free(T* obj) {
obj->~T();
for (auto& slot : slots_) {
if (reinterpret_cast<T*>(&slot.storage) == obj) {
slot.used = false;
return;
}
}
}
private:
struct Slot {
alignas(T) std::byte storage[sizeof(T)];
bool used = false;
};
Slot slots_[N]{};
};预分配N个T类型对象,分配/释放时间复杂度O(1),不涉及堆。完整的竞技场分配器实现可参考第1节,CRTP模式参考第2节。
references/memory-patterns.mdcpp
template<typename T, size_t N>
class ObjectPool {
public:
template<typename... Args>
T* allocate(Args&&... args) {
for (auto& slot : slots_) {
if (!slot.used) {
slot.used = true;
return new (&slot.storage) T(std::forward<Args>(args)...);
}
}
return nullptr; // 池耗尽——调用方自行处理
}
void free(T* obj) {
obj->~T();
for (auto& slot : slots_) {
if (reinterpret_cast<T*>(&slot.storage) == obj) {
slot.used = false;
return;
}
}
}
private:
struct Slot {
alignas(T) std::byte storage[sizeof(T)];
bool used = false;
};
Slot slots_[N]{};
};Volatile Hardware Register Access
Volatile硬件寄存器访问
volatilevolatilecpp
// Define register layout matching the hardware manual
struct UartRegisters {
volatile uint32_t SR; // Status register
volatile uint32_t DR; // Data register
volatile uint32_t BRR; // Baud rate register
volatile uint32_t CR1; // Control register 1
};
// Map to the hardware base address
auto* uart = reinterpret_cast<UartRegisters*>(0x40011000U);
// Read status — volatile ensures each read hits the hardware
if (uart->SR & (1U << 5)) { // RXNE bit
uint8_t byte = static_cast<uint8_t>(uart->DR);
}volatilevolatilecpp
// 定义与硬件手册匹配的寄存器布局
struct UartRegisters {
volatile uint32_t SR; // 状态寄存器
volatile uint32_t DR; // 数据寄存器
volatile uint32_t BRR; // 波特率寄存器
volatile uint32_t CR1; // 控制寄存器1
};
// 映射到硬件基地址
auto* uart = reinterpret_cast<UartRegisters*>(0x40011000U);
// 读取状态——volatile确保每次读取都直接访问硬件
if (uart->SR & (1U << 5)) { // RXNE位
uint8_t byte = static_cast<uint8_t>(uart->DR);
}Interrupt-Safe Access
中断安全访问
Sharing data between an ISR and a task requires either a critical section or .
Use atomics when the type fits in a single load/store (usually ≤ pointer size). Use critical sections
for larger structures.
std::atomiccpp
#include <atomic>
// Atomic: ISR and task can access without disabling interrupts
std::atomic<uint32_t> adc_value{0};
// In ISR:
void ADC_IRQHandler() {
adc_value.store(ADC1->DR, std::memory_order_relaxed);
}
// In task:
uint32_t val = adc_value.load(std::memory_order_relaxed);
// Critical section for larger structures (ARM Cortex-M):
struct SensorFrame { uint32_t timestamp; int16_t x, y, z; };
volatile SensorFrame latest_frame{};
void update_frame_from_isr(const SensorFrame& f) {
__disable_irq();
latest_frame = f;
__enable_irq();
}在ISR和任务之间共享数据需要使用临界区或者。当类型大小支持单次加载/存储(通常≤指针大小)时使用原子变量,更大的结构使用临界区。
std::atomiccpp
#include <atomic>
// 原子变量:ISR和任务无需禁用中断即可访问
std::atomic<uint32_t> adc_value{0};
// ISR中代码:
void ADC_IRQHandler() {
adc_value.store(ADC1->DR, std::memory_order_relaxed);
}
// 任务中代码:
uint32_t val = adc_value.load(std::memory_order_relaxed);
// 更大结构的临界区实现(ARM Cortex-M平台):
struct SensorFrame { uint32_t timestamp; int16_t x, y, z; };
volatile SensorFrame latest_frame{};
void update_frame_from_isr(const SensorFrame& f) {
__disable_irq();
latest_frame = f;
__enable_irq();
}Smart Pointer Policy
智能指针使用策略
| Pointer type | Use in embedded? | Guidance |
|---|---|---|
| Raw pointer (observing) | Yes | For non-owning references; make ownership explicit in naming |
| Raw pointer (owning) | Carefully | Only into static/pool storage where lifetime is obvious |
| Yes, with care | Zero overhead; use with custom deleters for pool objects |
| Yes | Returns pool objects to their pool on destruction |
| Avoid | Reference counting uses heap and is non-deterministic |
| Avoid | Tied to |
cpp
// unique_ptr with pool deleter — zero heap, automatic return to pool
ObjectPool<SensorData, 8> sensor_pool;
auto deleter = [](SensorData* p) { sensor_pool.free(p); };
using PooledSensor = std::unique_ptr<SensorData, decltype(deleter)>;
PooledSensor acquire_sensor() {
return PooledSensor(sensor_pool.allocate(), deleter);
}| 指针类型 | 嵌入式中是否可用 | 指导说明 |
|---|---|---|
| 原始指针(观测用) | 是 | 用于非持有引用;通过命名明确所有权 |
| 原始指针(持有用) | 谨慎使用 | 仅指向生命周期明确的静态/池存储 |
| 是,需谨慎 | 零开销;搭配自定义删除器用于池对象 |
| 是 | 析构时自动将池对象归还到对应对象池 |
| 避免使用 | 引用计数需要堆,且非确定性 |
| 避免使用 | 与 |
cpp
// 带池删除器的unique_ptr——零堆占用,自动归还到对象池
ObjectPool<SensorData, 8> sensor_pool;
auto deleter = [](SensorData* p) { sensor_pool.free(p); };
using PooledSensor = std::unique_ptr<SensorData, decltype(deleter)>;
PooledSensor acquire_sensor() {
return PooledSensor(sensor_pool.allocate(), deleter);
}Compile-Time Preferences
编译时优先原则
Prefer compile-time computation and verification over runtime checks:
cpp
// constexpr: computed at compile time, no runtime cost
constexpr uint32_t BAUD_DIVISOR = PCLK_FREQ / (16U * TARGET_BAUD);
static_assert(BAUD_DIVISOR > 0 && BAUD_DIVISOR < 65536, "Baud divisor out of range");
// std::array: bounds info preserved, unlike raw arrays
std::array<uint8_t, 64> tx_buffer{};
// std::span: non-owning view, no allocation, C++20 but often available via ETL
// std::string_view: for string literals and buffers, no heap
// CRTP replaces virtual dispatch — zero runtime overhead
// See references/memory-patterns.md §2 for full exampleC++ features to avoid in embedded:
| Avoid | Reason | Alternative |
|---|---|---|
| Code size (10-30% increase), non-deterministic stack unwind | |
| Runtime type tables increase ROM | CRTP, explicit type tags |
| Heap allocation | |
| Requires OS primitives | RTOS tasks |
| Heap allocation for captures | Function pointers, templates |
| Virtual destructors in deep hierarchies | vtable size, indirect dispatch, blocks inlining | CRTP or flat hierarchies (≤2 levels); virtual OK for non-critical paths |
Compile with for ARM targets. Use the Embedded Template Library (ETL)
for fixed-size , , alternatives.
-fno-exceptions -fno-rttietl::vectoretl::mapetl::string优先使用编译时计算和校验,而非运行时检查:
cpp
// constexpr:编译时计算,无运行时开销
constexpr uint32_t BAUD_DIVISOR = PCLK_FREQ / (16U * TARGET_BAUD);
static_assert(BAUD_DIVISOR > 0 && BAUD_DIVISOR < 65536, "波特率除数超出范围");
// std::array:保留边界信息,不同于原始数组
std::array<uint8_t, 64> tx_buffer{};
// std::span:非持有视图,无分配,C++20特性,通常也可通过ETL获取
// std::string_view:用于字符串字面量和缓冲区,无堆占用
// CRTP替代虚函数分发——零运行时开销
// 完整示例参考references/memory-patterns.md第2节嵌入式中需要避免的C++特性:
| 避免使用 | 原因 | 替代方案 |
|---|---|---|
| 代码体积增加10-30%,栈展开非确定性 | |
| 运行时类型表增加ROM占用 | CRTP、显式类型标签 |
| 堆分配 | |
| 需要操作系统原语 | RTOS任务 |
| 捕获内容需要堆分配 | 函数指针、模板 |
| 深层继承体系中的虚析构函数 | 虚表占用ROM、间接分发、阻碍内联 | CRTP或扁平继承体系(≤2层);非关键路径可以使用虚函数 |
ARM目标平台编译时添加参数。使用Embedded Template Library (ETL)作为固定大小、、的替代方案。
-fno-exceptions -fno-rttietl::vectoretl::mapetl::stringCommon Anti-Patterns
常见反模式
These patterns cause real bugs in production firmware. Knowing them saves hours of debugging.
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Heap-allocates when captures exceed small-buffer threshold (~16-32 bytes, implementation-dependent) | Function pointer + |
Arduino | Every concatenation/conversion heap-allocates; 10Hz × 4 sensors = 3.4M alloc/day → fragmentation | Fixed |
| Same fragmentation; violates MISRA C++ Rule 21.6.1 | Object pool, static array, or ETL fixed-capacity container |
| Atomic ref-counting overhead, control block on heap | |
| Deep virtual hierarchies | vtable per class (ROM), indirect dispatch, blocks inlining | CRTP or flat hierarchy (≤2 levels) |
Hidden | Dynamic allocation on every operation — often hidden in library APIs | ETL containers ( |
| | |
| ISR handler name mismatch | Misspelled ISR name silently falls through to | Verify names against startup |
| Heap allocation, locking, non-reentrant — crashes or deadlocks | |
| Unbounded recursion | Stack overflow on MCU with 1–8KB stack; MISRA C Rule 17.2 bans recursion | Convert to iterative with explicit stack |
Hidden allocation checklist: Before using any STL type, check whether it allocates. Common
surprises: , (reallocation),
(type-erased captures), , .
std::string::operator+=std::vector::push_backstd::functionstd::anystd::regex这些模式会在生产固件中引发实际故障,了解它们可以节省数小时的调试时间。
| 反模式 | 问题 | 修复方案 |
|---|---|---|
带捕获的 | 当捕获内容超出小缓冲区阈值(约16-32字节,依实现而定)时会进行堆分配 | 函数指针 + |
循环中使用Arduino | 每次拼接/转换都会进行堆分配;10Hz × 4个传感器 = 每天340万次分配 → 碎片化 | 固定 |
周期任务中使用 | 同样存在碎片化问题;违反MISRA C++规则21.6.1 | 对象池、静态数组或ETL固定容量容器 |
任何场景下使用 | 原子引用计数开销、控制块存储在堆上 | 带池删除器的 |
| 深层虚继承体系 | 每个类对应虚表(占用ROM)、间接分发、阻碍内联 | CRTP或扁平继承体系(≤2层) |
隐式使用 | 每次操作都会动态分配——通常隐藏在库API中 | ETL容器( |
使用 | | 使用带显式内存序的 |
| ISR处理函数名称不匹配 | 拼写错误的ISR名称会静默落入 | 对照启动 |
ISR中使用 | 堆分配、锁、不可重入——会引发崩溃或死锁 | 在ISR外使用 |
| 无界递归 | 栈大小仅1–8KB的MCU会发生栈溢出;MISRA C规则17.2禁止使用递归 | 转换为带显式栈的迭代实现 |
隐式分配检查清单: 使用任何STL类型前,检查它是否会进行分配。常见的意外情况:、(重分配)、(类型擦除捕获)、、。
std::string::operator+=std::vector::push_backstd::functionstd::anystd::regexCommon Memory Bug Diagnosis
常见内存bug诊断
| Symptom | Likely cause | First action |
|---|---|---|
| Crash after N hours of uptime | Heap fragmentation | Switch to pools/ETL containers; cite MISRA C Rule 21.3; see |
| HardFault with BFAR/MMFAR valid | Null/wild pointer dereference or bus fault | Read CFSR sub-registers; check MMARVALID/BFARVALID before using address registers; see |
| Stack pointer in wrong region | Stack overflow | Check |
| ISR data looks stale | Missing | Add |
| Random corruption near ISR | Data race | Apply atomics or critical section; see |
| Use-after-free | Object returned to pool while still referenced | Verify no aliasing; use unique_ptr with pool deleter |
| MPU fault in task | Task overflowed its stack into neighboring region | Increase stack size or reduce frame depth |
| Uninitialized read | Local variable used before assignment | Enable |
HardFault on first | FPU not enabled in CPACR | `SCB->CPACR |
| ISR does nothing / default handler runs | ISR function name misspelled | Verify name against startup |
| 症状 | 可能原因 | 首要处理措施 |
|---|---|---|
| 运行N小时后崩溃 | 堆碎片化 | 切换到对象池/ETL容器;引用MISRA C规则21.3;ESP32专属指导参考 |
| 带有效BFAR/MMFAR的HardFault | 空/野指针解引用或总线错误 | 读取CFSR子寄存器;使用地址寄存器前检查MMARVALID/BFARVALID;参考 |
| 栈指针处于错误区域 | 栈溢出 | 检查 |
| ISR数据看起来过时 | 缺失 | 为共享变量添加 |
| ISR附近出现随机损坏 | 数据竞争 | 应用原子变量或临界区;参考 |
| 释放后使用 | 对象被归还到池后仍被引用 | 检查无别名;使用带池删除器的unique_ptr |
| 任务中触发MPU故障 | 任务栈溢出侵入相邻区域 | 增加栈大小或减少帧深度 |
| 未初始化读取 | 局部变量赋值前被使用 | 开启 |
首次 | CPACR中未启用FPU | 执行任何浮点代码前执行`SCB->CPACR |
| ISR无响应/运行默认处理函数 | ISR函数名称拼写错误 | 对照启动 |
Debugging Tools Decision Tree
调试工具决策树
Is the bug reproducible on a host (PC)?
├── YES → Use AddressSanitizer (ASan) + Valgrind
│ Compile embedded logic for PC with -fsanitize=address
│ See references/debugging.md §5
└── NO → Is it a memory layout/access issue?
├── YES → Enable MPU; add stack canaries; read CFSR on fault
│ See references/debugging.md §1, §4
└── NO → Is it a data-race between ISR and task?
├── YES → Audit shared state; apply atomics/critical section
│ See references/debugging.md §3
└── NO → Use GDB watchpoint on the corrupted address
See references/debugging.md §6Static analysis: run with and checks.
Run for C code. Both catch many issues before target hardware.
clang-tidyclang-analyzer-*cppcoreguidelines-*cppcheck --enable=allbug是否可以在主机(PC)上复现?
├── 是 → 使用AddressSanitizer (ASan) + Valgrind
│ 为PC编译嵌入式逻辑时添加-fsanitize=address参数
│ 参考references/debugging.md第5节
└── 否 → 是否是内存布局/访问问题?
├── 是 → 启用MPU;添加栈金丝雀;故障时读取CFSR
│ 参考references/debugging.md第1、4节
└── 否 → 是否是ISR和任务之间的数据竞争?
├── 是 → 审计共享状态;应用原子变量/临界区
│ 参考references/debugging.md第3节
└── 否 → 对损坏地址使用GDB观察点
参考references/debugging.md第6节静态分析:使用运行和检查。C代码运行。两者都可以在烧录到目标硬件前发现很多问题。
clang-tidyclang-analyzer-*cppcoreguidelines-*cppcheck --enable=allError Handling Philosophy
错误处理理念
Four layers, each for a distinct failure category:
Recoverable errors (with context) — use (C++23, or /
as header-only polyfills for C++17) when you need to communicate why something failed. Zero heap
allocation, zero exceptions, type-safe error propagation:
std::expectedtl::expectedetl::expectedcpp
enum class SensorError : uint8_t { not_ready, crc_fail, timeout };
[[nodiscard]] std::expected<SensorReading, SensorError> read_sensor() {
if (!sensor_ready()) return std::unexpected(SensorError::not_ready);
auto raw = read_raw();
if (!verify_crc(raw)) return std::unexpected(SensorError::crc_fail);
return SensorReading{.temp = convert(raw)};
}Recoverable errors (simple) — use when the only failure is "no value":
std::optionalcpp
[[nodiscard]] std::optional<SensorReading> read_sensor() {
if (!sensor_ready()) return std::nullopt;
return SensorReading{.temp = read_temp(), .humidity = read_humidity()};
}[[nodiscard]][[nodiscard]][[nodiscard]]__attribute__((warn_unused_result))Programming errors — use or a trap that halts with debug info:
assertcpp
void write_to_pool(uint8_t* buf, size_t len) {
assert(buf != nullptr);
assert(len <= MAX_PACKET_SIZE); // Trips in debug, removed in release with NDEBUG
// ...
}Unrecoverable runtime errors — log fault reason + registers to non-volatile memory
(flash/EEPROM), then let the watchdog reset the system. Without pre-reset logging, field
failures leave no post-mortem data. Write CFSR + stacked PC + LR to a dedicated flash sector,
then call or spin and let the watchdog fire.
NVIC_SystemReset()四层架构,分别对应不同的故障类别:
可恢复错误(带上下文) ——当你需要传递故障原因时,使用(C++23特性,C++17可以使用/作为头文件-only的填充实现)。零堆分配、零异常、类型安全的错误传递:
std::expectedtl::expectedetl::expectedcpp
enum class SensorError : uint8_t { not_ready, crc_fail, timeout };
[[nodiscard]] std::expected<SensorReading, SensorError> read_sensor() {
if (!sensor_ready()) return std::unexpected(SensorError::not_ready);
auto raw = read_raw();
if (!verify_crc(raw)) return std::unexpected(SensorError::crc_fail);
return SensorReading{.temp = convert(raw)};
}可恢复错误(简单) ——当唯一的故障是“无值”时,使用:
std::optionalcpp
[[nodiscard]] std::optional<SensorReading> read_sensor() {
if (!sensor_ready()) return std::nullopt;
return SensorReading{.temp = read_temp(), .humidity = read_humidity()};
}[[nodiscard]][[nodiscard]][[nodiscard]]__attribute__((warn_unused_result))编程错误 ——使用或陷阱指令,停止执行并输出调试信息:
assertcpp
void write_to_pool(uint8_t* buf, size_t len) {
assert(buf != nullptr);
assert(len <= MAX_PACKET_SIZE); // 调试模式下触发,发布模式下通过NDEBUG移除
// ...
}不可恢复的运行时错误 ——将故障原因+寄存器写入非易失性内存(flash/EEPROM),然后让看门狗复位系统。如果没有复位前日志,现场故障不会留下任何事后分析数据。将CFSR + 栈中PC + LR写入专用flash扇区,然后调用或自旋等待看门狗触发。
NVIC_SystemReset()Testability Architecture
可测试性架构
Write firmware that can be tested on a PC without target hardware. The key: separate business
logic from hardware I/O at a clear HAL boundary.
Compile-time dependency injection — the hardware is known at compile time, so use templates
instead of virtual dispatch. Zero runtime overhead, full testability:
cpp
// HAL interface as a concept (or just template parameter)
template<typename Hal>
class SensorController {
public:
explicit SensorController(Hal& hal) : hal_(hal) {}
std::optional<float> read_temperature() {
auto raw = hal_.i2c_read(SENSOR_ADDR, TEMP_REG, 2);
if (!raw) return std::nullopt;
return convert_raw_to_celsius(*raw);
}
private:
Hal& hal_;
};
// Production: uses real hardware
SensorController<StmHal> controller(real_hal);
// Test: uses mock — same code, no vtable, no overhead in production
SensorController<MockHal> test_controller(mock_hal);Testing pyramid for firmware:
- Unit tests (host PC): Business logic with mock HAL — runs with ASan/UBSan, fast CI
- Integration tests (QEMU): Full firmware on emulated Cortex-M — catches linker/startup issues
- Hardware-in-the-loop (HIL): On real target — catches timing, peripheral, and electrical issues
编写可以在PC上测试、无需目标硬件的固件。核心要点:在清晰的HAL边界处将业务逻辑与硬件I/O分离。
编译时依赖注入 ——硬件在编译时已确定,因此使用模板而非虚函数分发。零运行时开销,完全可测试:
cpp
// HAL接口作为concept(或直接作为模板参数)
template<typename Hal>
class SensorController {
public:
explicit SensorController(Hal& hal) : hal_(hal) {}
std::optional<float> read_temperature() {
auto raw = hal_.i2c_read(SENSOR_ADDR, TEMP_REG, 2);
if (!raw) return std::nullopt;
return convert_raw_to_celsius(*raw);
}
private:
Hal& hal_;
};
// 生产环境:使用真实硬件
SensorController<StmHal> controller(real_hal);
// 测试环境:使用mock——相同代码,无虚表,生产环境无开销
SensorController<MockHal> test_controller(mock_hal);固件测试金字塔:
- 单元测试(主机PC): 使用mock HAL测试业务逻辑——配合ASan/UBSan运行,CI执行速度快
- 集成测试(QEMU): 在模拟的Cortex-M上运行完整固件——捕获链接/启动问题
- 硬件在环(HIL): 在真实目标上运行——捕获时序、外设和电气问题
Firmware Architecture Selection
固件架构选择
Choose the simplest architecture that meets your requirements:
| Architecture | Complexity | Best for |
|---|---|---|
| Superloop (bare-metal polling) | Lowest | < 5 tasks, loose timing, fully deterministic |
| Cooperative scheduler (time-triggered) | Low | Hard real-time, safety-critical (IEC 61508 SIL 1–2), analyzable |
| RTOS preemptive (FreeRTOS/Zephyr) | Medium | Complex multi-task, priority-based scheduling |
| Active Object (QP framework) | Highest | Event-heavy, hierarchical state machines, protocol handling |
For FreeRTOS IPC selection (task notifications vs queues vs stream buffers), low-power
patterns, CI/CD pipeline setup, and binary size budgeting → see .
references/architecture.mdFor STM32 CubeMX pitfalls, HAL vs LL driver selection, hard-to-debug embedded issues,
and code review checklists → see .
references/stm32-pitfalls.md选择满足需求的最简单架构:
| 架构 | 复杂度 | 适用场景 |
|---|---|---|
| 超级循环(裸机轮询) | 最低 | <5个任务、时序要求宽松、完全确定性的场景 |
| 协作式调度器(时间触发) | 低 | 硬实时、安全关键(IEC 61508 SIL 1–2)、可分析的场景 |
| RTOS抢占式(FreeRTOS/Zephyr) | 中等 | 复杂多任务、基于优先级调度的场景 |
| 活动对象(QP框架) | 最高 | 事件密集、分层状态机、协议处理的场景 |
FreeRTOS进程间通信选择(任务通知 vs 队列 vs 流缓冲区)、低功耗模式、CI/CD流水线设置、二进制大小预算 → 参考。
references/architecture.mdSTM32 CubeMX常见陷阱、HAL与LL驱动选择、难调试的嵌入式问题、代码评审检查清单 → 参考。
references/stm32-pitfalls.mdESP32 Platform Guidance
ESP32平台指导
When responding to any ESP32/ESP32-S2/S3/C3 question, always consider these platform-specific concerns:
Memory architecture — always address in ESP32 responses: ESP32 has multiple non-contiguous
memory regions. In every ESP32 response, explicitly discuss where buffers should be placed:
- DRAM (~320KB, fast): Ring buffers, DMA buffers, ISR data, FreeRTOS stacks. All real-time and latency-sensitive data goes here.
- PSRAM (4-8MB, ~10× slower, optional on -WROVER/S3): Large non-realtime data like SD card
write buffers, CSV formatting buffers, web server buffers, display framebuffers. Allocate with
or
heap_caps_malloc(size, MALLOC_CAP_SPIRAM). Never use PSRAM in ISRs or tight control loops — access latency is ~100ns vs ~10ns for DRAM.ps_malloc() - If the ESP32 variant has PSRAM, recommend moving large format/write buffers there to free DRAM for real-time use. If PSRAM is not available, note the DRAM pressure and size budgets. Always mention both regions so the user understands the tradeoff.
ETL on ESP32: When replacing , , or on ESP32, always
recommend the Embedded Template Library (ETL) by name with specific
types: , , , .
ETL works on ESP32 with both Arduino () and ESP-IDF
(add as component). Even when providing a custom implementation (like a ring buffer), mention ETL
as a production-ready alternative the user should consider.
std::vectorstd::stringstd::mapetl::vector<T, N>etl::string<N>etl::map<K, V, N>etl::queue_spsc_atomic<T, N>lib_deps = ETLCPP/Embedded Template LibraryDeep sleep and fast wake: For battery-powered ESP32 sensor nodes, see
for RTC memory WiFi caching, static IP, and sensor forced mode patterns.
references/architecture.mdTask stack sizing on ESP32: WiFi tasks need 4096-8192 bytes, BLE 4096-8192, TLS/SSL 8192-16384.
Monitor with . See §9 for details.
uxTaskGetStackHighWaterMark()references/memory-patterns.mdFor full ESP32 heap fragmentation diagnosis, monitoring, and ETL integration →
see §9.
references/memory-patterns.md回复任何ESP32/ESP32-S2/S3/C3相关问题时,始终考虑以下平台专属注意事项:
内存架构——所有ESP32回复中都必须提及: ESP32有多个非连续内存区域。所有ESP32回复中都要明确说明缓冲区应该放置的位置:
- DRAM(约320KB,速度快):环形缓冲区、DMA缓冲区、ISR数据、FreeRTOS栈。所有实时和对延迟敏感的数据都放在这里。
- PSRAM(4-8MB,速度慢约10倍,-WROVER/S3型号可选):大型非实时数据,比如SD卡写入缓冲区、CSV格式化缓冲区、web服务器缓冲区、显示帧缓冲区。使用或
heap_caps_malloc(size, MALLOC_CAP_SPIRAM)分配。永远不要在ISR或紧控循环中使用PSRAM——访问延迟约100ns,而DRAM仅约10ns。ps_malloc() - 如果ESP32型号搭载PSRAM,建议将大型格式化/写入缓冲区移到PSRAM,为实时用途释放DRAM。如果没有PSRAM,说明DRAM压力和大小预算。始终同时提及两个区域,让用户了解权衡。
ESP32上的ETL: 在ESP32上替换、或时,始终明确推荐Embedded Template Library (ETL)及对应类型:、、、。ETL在ESP32上同时支持Arduino()和ESP-IDF(作为组件添加)。即使提供了自定义实现(比如环形缓冲区),也要提及ETL作为用户可以考虑的生产级替代方案。
std::vectorstd::stringstd::mapetl::vector<T, N>etl::string<N>etl::map<K, V, N>etl::queue_spsc_atomic<T, N>lib_deps = ETLCPP/Embedded Template Library深度休眠和快速唤醒: 电池供电的ESP32传感器节点相关内容,参考中的RTC内存WiFi缓存、静态IP和传感器强制模式说明。
references/architecture.mdESP32上的任务栈大小设置: WiFi任务需要4096-8192字节,BLE需要4096-8192字节,TLS/SSL需要8192-16384字节。使用监控使用情况。详细说明参考第9节。
uxTaskGetStackHighWaterMark()references/memory-patterns.md完整的ESP32堆碎片化诊断、监控和ETL集成说明 → 参考第9节。
references/memory-patterns.mdCoding Conventions Summary
编码规范摘要
See for the full guide. Key rules:
references/coding-style.md- Variables and functions:
snake_case - Classes and structs:
PascalCase - Constants: (Google style) or
kConstantNamefor macrosALL_CAPS - Member variables:
trailing_underscore_ - Include guards: (prefer) or
#pragma onceguard#ifndef HEADER_H_ - const correctness: const every non-mutating method, const every parameter that isn't modified
- : on any function whose return value must not be silently dropped (error codes, pool allocate)
[[nodiscard]]
完整指南参考。核心规则:
references/coding-style.md- 变量和函数:(蛇形命名)
snake_case - 类和结构体:(大驼峰命名)
PascalCase - 常量:(Google风格)或宏使用
kConstantName(全大写)ALL_CAPS - 成员变量:(尾部下划线)
trailing_underscore_ - 包含守卫:优先使用,或使用
#pragma once守卫#ifndef HEADER_H_ - const正确性:所有非修改方法、所有不会被修改的参数都添加const修饰
- :所有返回值不能被静默丢弃的函数都添加该修饰(错误码、对象池分配函数等)
[[nodiscard]]
Reference File Index
参考文件索引
| File | Read when |
|---|---|
| Implementing arena, ring buffer, DMA buffers, lock-free SPSC, singletons, linker sections, ESP32 heap fragmentation, ETL containers |
| Writing C99/C11 firmware, C memory pools, C error handling, C/C++ interop, MISRA C rules, UART read-to-clear mechanism, |
| Diagnosing stack overflow, heap corruption, HardFault, data races, NVIC priority issues, or running ASan/GDB |
| Naming conventions, feature usage table, struct packing, attributes, include guards |
| Choosing firmware architecture, FreeRTOS IPC patterns, low-power modes, ESP32 deep sleep + fast wake, CI/CD pipeline setup, binary size budgets |
| CubeMX code generation issues, HAL vs LL selection, hard-to-debug issues (cache, priority inversion, flash stall), code review checklist |
| HAL design patterns (CRTP/template/virtual/opaque), dependency injection strategies, SOLID for embedded, callback + trampoline patterns |
| MPU protection patterns, watchdog hierarchy, IEC 61508 fault recovery, peripheral protocol selection (UART/I2C/SPI/CAN), linker script memory placement |
| 文件 | 适用场景 |
|---|---|
| 实现竞技场、环形缓冲区、DMA缓冲区、无锁SPSC、单例、链接节、ESP32堆碎片化、ETL容器 |
| 编写C99/C11固件、C内存池、C错误处理、C/C++互操作、MISRA C规则、UART读清除机制、 |
| 诊断栈溢出、堆损坏、HardFault、数据竞争、NVIC优先级问题,或运行ASan/GDB |
| 命名规范、特性使用表、结构体打包、属性、包含守卫 |
| 选择固件架构、FreeRTOS IPC模式、低功耗模式、ESP32深度休眠+快速唤醒、CI/CD流水线设置、二进制大小预算 |
| CubeMX代码生成问题、HAL与LL选择、难调试问题(缓存、优先级反转、flash停顿)、代码评审检查清单 |
| HAL设计模式(CRTP/模板/虚函数/不透明)、依赖注入策略、嵌入式SOLID原则、回调+跳板模式 |
| MPU保护模式、看门狗层级、IEC 61508故障恢复、外设协议选择(UART/I2C/SPI/CAN)、链接脚本内存布局 |