cpp-embedded

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Embedded 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

分配决策表

NeedSolutionNotes
Short-lived local dataStackKeep < 256 bytes per frame; profile with
-fstack-usage
Fixed singleton objects
static
at file or function scope
Zero-initialized before
main()
Fixed array of objectsObject pool (
ObjectPool<T, N>
)
O(1) alloc/free, no fragmentation
Temporary scratch spaceArena / bump allocatorReset whole arena at end of operation
Variable-size messagesRing buffer of fixed-size slotsSimplest ISR-safe comms pattern
Custom lifetime controlPlacement new + static storageFull control, no heap involvement
Never in ISRAny of the above except stackAllocator calls are not ISR-safe
Avoid entirely
malloc
/
new
/
std::vector
Non-deterministic; fragmentation risk

需求解决方案说明
短生命周期本地数据每帧栈占用保持在256字节以内;使用
-fstack-usage
参数分析栈使用情况
固定单例对象文件或函数作用域的
static
变量
main()
执行前完成零初始化
固定对象数组对象池(
ObjectPool<T, N>
O(1)时间复杂度分配/释放,无碎片化问题
临时暂存空间竞技场/碰撞分配器操作完成后整体重置竞技场
可变大小消息固定大小槽位的环形缓冲区最简单的ISR安全通信模式
自定义生命周期控制placement new + 静态存储完全可控,不涉及堆
ISR中禁止使用除栈以外的所有分配方式分配器调用不具备ISR安全性
完全避免使用
malloc
/
new
/
std::vector
非确定性;存在碎片化风险

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
    references/memory-patterns.md
    §3 for the full implementation. Always use
    _Static_assert
    or
    static_assert
    to validate the capacity is a power of 2.
  • 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的幂,使用位掩码索引)——最适合可变速率数据流。完整实现可参考
    references/memory-patterns.md
    第3节。始终使用
    _Static_assert
    static_assert
    验证容量是2的幂。
  • 乒乓(双)缓冲——最适合固定突发的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
references/memory-patterns.md
§1 for the full arena allocator and §2 for CRTP patterns.
cpp
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),不涉及堆。完整的竞技场分配器实现可参考
references/memory-patterns.md
第1节,CRTP模式参考第2节。
cpp
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硬件寄存器访问

volatile
tells the compiler the value can change outside its knowledge (hardware can write it). Without
volatile
, the compiler may cache the read in a register and never see the hardware update.
cpp
// 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);
}
volatile
关键字告知编译器该值可能在编译器感知范围外发生变化(硬件可以修改它)。如果不使用
volatile
,编译器可能会将读取结果缓存到寄存器中,永远感知不到硬件更新。
cpp
// 定义与硬件手册匹配的寄存器布局
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
std::atomic
. Use atomics when the type fits in a single load/store (usually ≤ pointer size). Use critical sections for larger structures.
cpp
#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::atomic
。当类型大小支持单次加载/存储(通常≤指针大小)时使用原子变量,更大的结构使用临界区。
cpp
#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 typeUse in embedded?Guidance
Raw pointer (observing)YesFor non-owning references; make ownership explicit in naming
Raw pointer (owning)CarefullyOnly into static/pool storage where lifetime is obvious
std::unique_ptr
Yes, with careZero overhead; use with custom deleters for pool objects
std::unique_ptr
+ custom deleter
YesReturns pool objects to their pool on destruction
std::shared_ptr
AvoidReference counting uses heap and is non-deterministic
std::weak_ptr
AvoidTied to
shared_ptr
; same concerns
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);
}

指针类型嵌入式中是否可用指导说明
原始指针(观测用)用于非持有引用;通过命名明确所有权
原始指针(持有用)谨慎使用仅指向生命周期明确的静态/池存储
std::unique_ptr
是,需谨慎零开销;搭配自定义删除器用于池对象
std::unique_ptr
+ 自定义删除器
析构时自动将池对象归还到对应对象池
std::shared_ptr
避免使用引用计数需要堆,且非确定性
std::weak_ptr
避免使用
shared_ptr
绑定;存在相同问题
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 example
C++ features to avoid in embedded:
AvoidReasonAlternative
-fexceptions
Code size (10-30% increase), non-deterministic stack unwind
std::optional
,
std::expected
(C++23/polyfill), error codes
-frtti
/
typeid
/
dynamic_cast
Runtime type tables increase ROMCRTP, explicit type tags
std::vector
,
std::string
,
std::map
Heap allocation
std::array
, ETL containers
std::thread
Requires OS primitivesRTOS tasks
std::function
Heap allocation for capturesFunction pointers, templates
Virtual destructors in deep hierarchiesvtable size, indirect dispatch, blocks inliningCRTP or flat hierarchies (≤2 levels); virtual OK for non-critical paths
Compile with
-fno-exceptions -fno-rtti
for ARM targets. Use the Embedded Template Library (ETL) for fixed-size
etl::vector
,
etl::map
,
etl::string
alternatives.

优先使用编译时计算和校验,而非运行时检查:
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++特性:
避免使用原因替代方案
-fexceptions
代码体积增加10-30%,栈展开非确定性
std::optional
std::expected
(C++23/填充实现)、错误码
-frtti
/
typeid
/
dynamic_cast
运行时类型表增加ROM占用CRTP、显式类型标签
std::vector
std::string
std::map
堆分配
std::array
、ETL容器
std::thread
需要操作系统原语RTOS任务
std::function
捕获内容需要堆分配函数指针、模板
深层继承体系中的虚析构函数虚表占用ROM、间接分发、阻碍内联CRTP或扁平继承体系(≤2层);非关键路径可以使用虚函数
ARM目标平台编译时添加
-fno-exceptions -fno-rtti
参数。使用Embedded Template Library (ETL)作为固定大小
etl::vector
etl::map
etl::string
的替代方案。

Common Anti-Patterns

常见反模式

These patterns cause real bugs in production firmware. Knowing them saves hours of debugging.
Anti-PatternProblemFix
std::function
with captures
Heap-allocates when captures exceed small-buffer threshold (~16-32 bytes, implementation-dependent)Function pointer +
void* context
, template callable, or lambda passed directly to template parameter
Arduino
String
in loops
Every concatenation/conversion heap-allocates; 10Hz × 4 sensors = 3.4M alloc/day → fragmentationFixed
char[]
with
snprintf
; for complex strings use
etl::string<N>
new
/
delete
in periodic tasks
Same fragmentation; violates MISRA C++ Rule 21.6.1Object pool, static array, or ETL fixed-capacity container
std::shared_ptr
anywhere
Atomic ref-counting overhead, control block on heap
std::unique_ptr
with pool deleter, or raw pointer with clear ownership
Deep virtual hierarchiesvtable per class (ROM), indirect dispatch, blocks inliningCRTP or flat hierarchy (≤2 levels)
Hidden
std::string
/
std::vector
Dynamic allocation on every operation — often hidden in library APIsETL containers (
etl::string<N>
,
etl::vector<T,N>
)
volatile
for thread sync
volatile
only prevents the compiler from caching or eliminating reads/writes — it does NOT prevent reordering between threads or provide atomicity. Data races on
volatile
variables are still undefined behavior
std::atomic
with explicit memory ordering (
memory_order_relaxed
suffices for ISR-to-task on single-core Cortex-M)
ISR handler name mismatchMisspelled ISR name silently falls through to
Default_Handler
— system hangs or resets
Verify names against startup
.s
vector table; use
-Wl,--undefined=USART1_IRQHandler
to catch missing symbols at link time
printf
/
sprintf
in ISR
Heap allocation, locking, non-reentrant — crashes or deadlocks
snprintf
to pre-allocated buffer outside ISR, or ITM/SWO trace output
Unbounded recursionStack overflow on MCU with 1–8KB stack; MISRA C Rule 17.2 bans recursionConvert to iterative with explicit stack
Hidden allocation checklist: Before using any STL type, check whether it allocates. Common surprises:
std::string::operator+=
,
std::vector::push_back
(reallocation),
std::function
(type-erased captures),
std::any
,
std::regex
.

这些模式会在生产固件中引发实际故障,了解它们可以节省数小时的调试时间。
反模式问题修复方案
带捕获的
std::function
当捕获内容超出小缓冲区阈值(约16-32字节,依实现而定)时会进行堆分配函数指针 +
void* context
、模板可调用对象,或直接将lambda传递给模板参数
循环中使用Arduino
String
每次拼接/转换都会进行堆分配;10Hz × 4个传感器 = 每天340万次分配 → 碎片化固定
char[]
搭配
snprintf
;复杂字符串使用
etl::string<N>
周期任务中使用
new
/
delete
同样存在碎片化问题;违反MISRA C++规则21.6.1对象池、静态数组或ETL固定容量容器
任何场景下使用
std::shared_ptr
原子引用计数开销、控制块存储在堆上带池删除器的
std::unique_ptr
,或所有权明确的原始指针
深层虚继承体系每个类对应虚表(占用ROM)、间接分发、阻碍内联CRTP或扁平继承体系(≤2层)
隐式使用
std::string
/
std::vector
每次操作都会动态分配——通常隐藏在库API中ETL容器(
etl::string<N>
etl::vector<T,N>
使用
volatile
做线程同步
volatile
仅防止编译器缓存或消除读写操作——它不会阻止线程间的指令重排,也不提供原子性。
volatile
变量上的数据竞争仍然属于未定义行为
使用带显式内存序的
std::atomic
(单核Cortex-M平台上ISR到任务通信使用
memory_order_relaxed
即可)
ISR处理函数名称不匹配拼写错误的ISR名称会静默落入
Default_Handler
——系统挂起或复位
对照启动
.s
文件的向量表验证名称;使用
-Wl,--undefined=USART1_IRQHandler
参数在链接时捕获缺失的符号
ISR中使用
printf
/
sprintf
堆分配、锁、不可重入——会引发崩溃或死锁在ISR外使用
snprintf
写入预分配缓冲区,或使用ITM/SWO trace输出
无界递归栈大小仅1–8KB的MCU会发生栈溢出;MISRA C规则17.2禁止使用递归转换为带显式栈的迭代实现
隐式分配检查清单: 使用任何STL类型前,检查它是否会进行分配。常见的意外情况:
std::string::operator+=
std::vector::push_back
(重分配)、
std::function
(类型擦除捕获)、
std::any
std::regex

Common Memory Bug Diagnosis

常见内存bug诊断

SymptomLikely causeFirst action
Crash after N hours of uptimeHeap fragmentationSwitch to pools/ETL containers; cite MISRA C Rule 21.3; see
references/memory-patterns.md
§9 for ESP32-specific guidance
HardFault with BFAR/MMFAR validNull/wild pointer dereference or bus faultRead CFSR sub-registers; check MMARVALID/BFARVALID before using address registers; see
references/debugging.md
§4
Stack pointer in wrong regionStack overflowCheck
.su
files; add MPU guard region; see
references/debugging.md
§1
ISR data looks staleMissing
volatile
Add
volatile
to shared variables; audit ISR data paths
Random corruption near ISRData raceApply atomics or critical section; see
references/debugging.md
§3
Use-after-freeObject returned to pool while still referencedVerify no aliasing; use unique_ptr with pool deleter
MPU fault in taskTask overflowed its stack into neighboring regionIncrease stack size or reduce frame depth
Uninitialized readLocal variable used before assignmentEnable
-Wuninitialized
; initialize all locals
HardFault on first
float
operation
FPU not enabled in CPACR`SCB->CPACR
ISR does nothing / default handler runsISR function name misspelledVerify name against startup
.s
vector table

症状可能原因首要处理措施
运行N小时后崩溃堆碎片化切换到对象池/ETL容器;引用MISRA C规则21.3;ESP32专属指导参考
references/memory-patterns.md
第9节
带有效BFAR/MMFAR的HardFault空/野指针解引用或总线错误读取CFSR子寄存器;使用地址寄存器前检查MMARVALID/BFARVALID;参考
references/debugging.md
第4节
栈指针处于错误区域栈溢出检查
.su
文件;添加MPU保护区域;参考
references/debugging.md
第1节
ISR数据看起来过时缺失
volatile
修饰
为共享变量添加
volatile
;审计ISR数据路径
ISR附近出现随机损坏数据竞争应用原子变量或临界区;参考
references/debugging.md
第3节
释放后使用对象被归还到池后仍被引用检查无别名;使用带池删除器的unique_ptr
任务中触发MPU故障任务栈溢出侵入相邻区域增加栈大小或减少帧深度
未初始化读取局部变量赋值前被使用开启
-Wuninitialized
;初始化所有局部变量
首次
float
运算时触发HardFault
CPACR中未启用FPU执行任何浮点代码前执行`SCB->CPACR
ISR无响应/运行默认处理函数ISR函数名称拼写错误对照启动
.s
向量表验证名称

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 §6
Static analysis: run
clang-tidy
with
clang-analyzer-*
and
cppcoreguidelines-*
checks. Run
cppcheck --enable=all
for C code. Both catch many issues before target hardware.

bug是否可以在主机(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节
静态分析:使用
clang-tidy
运行
clang-analyzer-*
cppcoreguidelines-*
检查。C代码运行
cppcheck --enable=all
。两者都可以在烧录到目标硬件前发现很多问题。

Error Handling Philosophy

错误处理理念

Four layers, each for a distinct failure category:
Recoverable errors (with context) — use
std::expected
(C++23, or
tl::expected
/
etl::expected
as header-only polyfills for C++17) when you need to communicate why something failed. Zero heap allocation, zero exceptions, type-safe error propagation:
cpp
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
std::optional
when the only failure is "no value":
cpp
[[nodiscard]] std::optional<SensorReading> read_sensor() {
    if (!sensor_ready()) return std::nullopt;
    return SensorReading{.temp = read_temp(), .humidity = read_humidity()};
}
[[nodiscard]]
enforcement:
Mark every function returning an error code, status, or allocated pointer with
[[nodiscard]]
. The compiler then warns if the caller silently ignores the return value — this catches the #1 error handling bug in firmware (discarded error codes). In C, use
[[nodiscard]]
(C23) or
__attribute__((warn_unused_result))
.
Programming errors — use
assert
or a trap that halts with debug info:
cpp
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
NVIC_SystemReset()
or spin and let the watchdog fire.

四层架构,分别对应不同的故障类别:
可恢复错误(带上下文) ——当你需要传递故障原因时,使用
std::expected
(C++23特性,C++17可以使用
tl::expected
/
etl::expected
作为头文件-only的填充实现)。零堆分配、零异常、类型安全的错误传递:
cpp
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::optional
cpp
[[nodiscard]] std::optional<SensorReading> read_sensor() {
    if (!sensor_ready()) return std::nullopt;
    return SensorReading{.temp = read_temp(), .humidity = read_humidity()};
}
[[nodiscard]]
强制检查:
为所有返回错误码、状态或分配指针的函数添加
[[nodiscard]]
修饰。如果调用方静默忽略返回值,编译器会发出警告——这可以解决固件中最常见的错误处理bug(丢弃错误码)。C语言中使用
[[nodiscard]]
(C23)或
__attribute__((warn_unused_result))
编程错误 ——使用
assert
或陷阱指令,停止执行并输出调试信息:
cpp
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:
ArchitectureComplexityBest for
Superloop (bare-metal polling)Lowest< 5 tasks, loose timing, fully deterministic
Cooperative scheduler (time-triggered)LowHard real-time, safety-critical (IEC 61508 SIL 1–2), analyzable
RTOS preemptive (FreeRTOS/Zephyr)MediumComplex multi-task, priority-based scheduling
Active Object (QP framework)HighestEvent-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.md
.
For 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.md
STM32 CubeMX常见陷阱、HAL与LL驱动选择、难调试的嵌入式问题、代码评审检查清单 → 参考
references/stm32-pitfalls.md

ESP32 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
    heap_caps_malloc(size, MALLOC_CAP_SPIRAM)
    or
    ps_malloc()
    . Never use PSRAM in ISRs or tight control loops — access latency is ~100ns vs ~10ns for DRAM.
  • 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
std::vector
,
std::string
, or
std::map
on ESP32, always recommend the Embedded Template Library (ETL) by name with specific types:
etl::vector<T, N>
,
etl::string<N>
,
etl::map<K, V, N>
,
etl::queue_spsc_atomic<T, N>
. ETL works on ESP32 with both Arduino (
lib_deps = ETLCPP/Embedded Template Library
) 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.
Deep sleep and fast wake: For battery-powered ESP32 sensor nodes, see
references/architecture.md
for RTC memory WiFi caching, static IP, and sensor forced mode patterns.
Task stack sizing on ESP32: WiFi tasks need 4096-8192 bytes, BLE 4096-8192, TLS/SSL 8192-16384. Monitor with
uxTaskGetStackHighWaterMark()
. See
references/memory-patterns.md
§9 for details.
For full ESP32 heap fragmentation diagnosis, monitoring, and ETL integration → see
references/memory-patterns.md
§9.

回复任何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)
    ps_malloc()
    分配。永远不要在ISR或紧控循环中使用PSRAM——访问延迟约100ns,而DRAM仅约10ns。
  • 如果ESP32型号搭载PSRAM,建议将大型格式化/写入缓冲区移到PSRAM,为实时用途释放DRAM。如果没有PSRAM,说明DRAM压力和大小预算。始终同时提及两个区域,让用户了解权衡。
ESP32上的ETL: 在ESP32上替换
std::vector
std::string
std::map
时,始终明确推荐Embedded Template Library (ETL)及对应类型:
etl::vector<T, N>
etl::string<N>
etl::map<K, V, N>
etl::queue_spsc_atomic<T, N>
。ETL在ESP32上同时支持Arduino(
lib_deps = ETLCPP/Embedded Template Library
)和ESP-IDF(作为组件添加)。即使提供了自定义实现(比如环形缓冲区),也要提及ETL作为用户可以考虑的生产级替代方案。
深度休眠和快速唤醒: 电池供电的ESP32传感器节点相关内容,参考
references/architecture.md
中的RTC内存WiFi缓存、静态IP和传感器强制模式说明。
ESP32上的任务栈大小设置: WiFi任务需要4096-8192字节,BLE需要4096-8192字节,TLS/SSL需要8192-16384字节。使用
uxTaskGetStackHighWaterMark()
监控使用情况。详细说明参考
references/memory-patterns.md
第9节。
完整的ESP32堆碎片化诊断、监控和ETL集成说明 → 参考
references/memory-patterns.md
第9节。

Coding Conventions Summary

编码规范摘要

See
references/coding-style.md
for the full guide. Key rules:
  • Variables and functions:
    snake_case
  • Classes and structs:
    PascalCase
  • Constants:
    kConstantName
    (Google style) or
    ALL_CAPS
    for macros
  • Member variables:
    trailing_underscore_
  • Include guards:
    #pragma once
    (prefer) or
    #ifndef HEADER_H_
    guard
  • const correctness: const every non-mutating method, const every parameter that isn't modified
  • [[nodiscard]]
    : on any function whose return value must not be silently dropped (error codes, pool allocate)

完整指南参考
references/coding-style.md
。核心规则:
  • 变量和函数
    snake_case
    (蛇形命名)
  • 类和结构体
    PascalCase
    (大驼峰命名)
  • 常量
    kConstantName
    (Google风格)或宏使用
    ALL_CAPS
    (全大写)
  • 成员变量
    trailing_underscore_
    (尾部下划线)
  • 包含守卫:优先使用
    #pragma once
    ,或使用
    #ifndef HEADER_H_
    守卫
  • const正确性:所有非修改方法、所有不会被修改的参数都添加const修饰
  • [[nodiscard]]
    :所有返回值不能被静默丢弃的函数都添加该修饰(错误码、对象池分配函数等)

Reference File Index

参考文件索引

FileRead when
references/memory-patterns.md
Implementing arena, ring buffer, DMA buffers, lock-free SPSC, singletons, linker sections, ESP32 heap fragmentation, ETL containers
references/c-patterns.md
Writing C99/C11 firmware, C memory pools, C error handling, C/C++ interop, MISRA C rules, UART read-to-clear mechanism,
_Static_assert
for buffer validation
references/debugging.md
Diagnosing stack overflow, heap corruption, HardFault, data races, NVIC priority issues, or running ASan/GDB
references/coding-style.md
Naming conventions, feature usage table, struct packing, attributes, include guards
references/architecture.md
Choosing firmware architecture, FreeRTOS IPC patterns, low-power modes, ESP32 deep sleep + fast wake, CI/CD pipeline setup, binary size budgets
references/stm32-pitfalls.md
CubeMX code generation issues, HAL vs LL selection, hard-to-debug issues (cache, priority inversion, flash stall), code review checklist
references/design-patterns.md
HAL design patterns (CRTP/template/virtual/opaque), dependency injection strategies, SOLID for embedded, callback + trampoline patterns
references/safety-hardware.md
MPU protection patterns, watchdog hierarchy, IEC 61508 fault recovery, peripheral protocol selection (UART/I2C/SPI/CAN), linker script memory placement
文件适用场景
references/memory-patterns.md
实现竞技场、环形缓冲区、DMA缓冲区、无锁SPSC、单例、链接节、ESP32堆碎片化、ETL容器
references/c-patterns.md
编写C99/C11固件、C内存池、C错误处理、C/C++互操作、MISRA C规则、UART读清除机制、
_Static_assert
缓冲区验证
references/debugging.md
诊断栈溢出、堆损坏、HardFault、数据竞争、NVIC优先级问题,或运行ASan/GDB
references/coding-style.md
命名规范、特性使用表、结构体打包、属性、包含守卫
references/architecture.md
选择固件架构、FreeRTOS IPC模式、低功耗模式、ESP32深度休眠+快速唤醒、CI/CD流水线设置、二进制大小预算
references/stm32-pitfalls.md
CubeMX代码生成问题、HAL与LL选择、难调试问题(缓存、优先级反转、flash停顿)、代码评审检查清单
references/design-patterns.md
HAL设计模式(CRTP/模板/虚函数/不透明)、依赖注入策略、嵌入式SOLID原则、回调+跳板模式
references/safety-hardware.md
MPU保护模式、看门狗层级、IEC 61508故障恢复、外设协议选择(UART/I2C/SPI/CAN)、链接脚本内存布局