RP2040慎用CriticalSection
之前在做中断调用的过程的时候,在RP2040的SDK里面找到两个敏感数据保护的类,critical section 和 mutex,当时因为发现 mutex 并不像 linux 的 mutex 那样支持临界区重入,于是选择了critical section 作为敏感数据的保护类。
到最近为止,这个选择一直没有出问题,因为近期我给恒流板加了CAN控制器,用于和主控板通讯使用,因此,又增加了很多代码,其中就包括CAN的中断和读写的临界区。于是,在一天我编译代码上传RP2040之后,突然发现RP2040没有反应了。具体表现就是板子插到电脑之后没有任何反应,没有识别到com端口。我开始还以为是usb线有问题,换了线,还是依旧,换了板子上传代码,还是依旧,更新usb大容量设备驱动也没用,我逐渐意识到可能是RP2040被代码锁死了。但是这种情况下,Arduino完全没办法上传代码到主板,我只能想了一个取巧的办法,先找到RP2040 的例子,比如Blink,用 Arduino 编译这个例子,生成UF2文件,找到这个文件,路径如下:
C:\Users\xxxx\AppData\Local\arduino\sketches
这个目录下有不少目录,一个一个的进去看,找到Blink的UF2文件。
找到这个文件之后, 把这个文件复制到你能快速访问的位置,比如桌面。然后把RP2040板子USB连接断开,按住BOOT按钮,将板子通过USB连接到电脑上,这个时候,因该能识别到RP2040并打开一个文件夹,然后把那个UF2文件拖进去,这个时候RP2040就会重启,然后顺利识别到 com 端口。
这个时候我是根本没有意识到哪儿出问题了,我只能用最原始的办法来寻找问题,在入口文件处把所有的初始化对象全部注释掉,编译执行,没问题,然后去掉注释,编译执行,寄。这个时候我大致确定了问题就出在这个初始化的对象,初始化的时候寄了。这个对象内部又聚集了不少其他功能,我先把这些功能全部注释掉,编译执行,过。然后一项一项的取消注释,编译执行。当我取消最后一个注释的功能的时候,又寄了,我看了一下,是CAN总线通讯的模块。我只能进去把CAN总线模块的所有成员变量、代码注释掉,编译执行,过。看来问题还没那么简单,我于是一个一个的取消注释,期间又反复调整代码,以适应成员变量,特别是初始化部分的代码,简直是要命。最终,我发现一个成员变量:
private:tagCanFrame::List received; CSpiProtocol<MCP2515_BPS_k, SpiMcp2515Command> spi;CCriticalSection rx_section;//CCriticalSection tx_section;
这个tx_section我只要注释掉,就不会出现卡死的情况,一旦我解开注释,绝对会锁死核心板。对于这个问题,我是完全懵逼的。亏得最近DeepSeek升级了,我开始试图用DeepSeek来找答案,下面是我和DeepSeek的一些对话:
我问:我似乎找到问题了,你看我申明了一个 tx_section,哪怕我不用这个成员变量,也会卡死!我只要注释掉这个临界区,就不会卡死!,是什么原因呢?
DeepSeek回答:
根据您提供的代码截图和问题描述,问题出在声明
tx_section
临界区成员变量会导致系统卡死,即使不使用它。结合 RP2040 的硬件特性,我分析出根本原因并提供解决方案:🔍 问题根源分析
C++ 对象初始化顺序问题:
private:CSpiProtocol<...> spi; // 第1个成员tagCanFrame::List received; // 第2个成员CCriticalSection rx_section; // 第3个成员CCriticalSection tx_section; // 第4个成员 <- 问题点!
- C++ 对象成员按照声明顺序初始化
tx_section
初始化发生在spi
之后RP2040 硬件锁的特殊性:
CCriticalSection
底层使用硬件自旋锁 (Spinlocks)- Spinlocks 在
runtime_init()
之前不可用- 全局/静态对象在
main()
之前初始化 -> 在 SDK 初始化前使用了硬件锁症状表现:
情况 结果 声明 tx_section
系统卡死(硬件锁未初始化) 注释 tx_section
系统正常运行 🛠️ 解决方案(三种方法)
✅ 方案一:改为指针延迟初始化(推荐⭐)
class CMcp2515 { private:CCriticalSection* tx_section = nullptr; // 改为指针public:bool Initiate() {// 在运行时初始化(确保SDK已就绪)if(!tx_section) {tx_section = new CCriticalSection();}// 使用 tx_section->Enter()...}~CMcp2515() {delete tx_section; // 析构时释放} };
✅ 方案二:静态局部变量(线程安全)
class CMcp2515 { public:void Transmit() {static CCriticalSection tx_section; // 首次调用时初始化tx_section.Enter();// 发送数据...tx_section.Leave();} };
✅ 方案三:显式初始化函数
class CMcp2515 { private:CCriticalSection tx_section;bool tx_initialized = false;public:bool InitTxSection() {// 确保在SDK初始化后调用tx_section.Init(); // 添加自定义初始化函数tx_initialized = true;return true;}void SendData() {if(!tx_initialized) return;tx_section.Enter();// ...tx_section.Leave();} };// 在main()中: CMcp2515 can; setup() {can.Initiate(); // 常规初始化can.InitTxSection(); // 显式初始化临界区 }
⚠️ 关键修复原理
sequenceDiagramparticipant startup as 启动代码participant global as 全局对象构造participant sdk as runtime_init()participant main as main()startup->>global: 构造CMcp2515对象global->>global: 初始化tx_section(硬件锁)Note over global: 此时SDK未初始化→死锁startup->>sdk: 初始化RP2040 SDKsdk->>main: 进入setup()
🔧 验证测试建议
添加构造日志:
CCriticalSection::CCriticalSection() {Serial.printf("CriticalSection CTOR @ %lu\n", millis()); }
检查初始化顺序:
extern "C" void runtime_init() {Serial.println("SDK初始化开始!");// ... }
硬件锁状态检查:
bool IsSpinLockInitialized() {return spin_lock_is_initialized(spin_lock_instance(0)); }
💡 实际案例解决方案
根据您的代码结构,推荐这样修改:
// Mcp2515.h class CMcp2515 { private:CSpiProtocol<MCP2515_BPS_k, SpiMcp2515Command> spi;tagCanFrame::List received;CCriticalSection rx_section; - CCriticalSection tx_section; // 删除成员声明 + CCriticalSection* tx_section = nullptr; // 改为指针 };// Mcp2515.cpp bool CMcp2515::Initiate() {// ...其他初始化...+ // 初始化发送临界区 + tx_section = new CCriticalSection();return true; }CMcp2515::~CMcp2515() { + delete tx_section; // 清理资源 }void CMcp2515::SendFrame(...) { + if(!tx_section) return; + + tx_section->Enter();// 发送CAN帧... + tx_section->Leave(); }
为什么其他成员不卡死?
rx_section
没有引发卡死是因为它被使用(在中断处理中)- 未被使用的硬件锁可能处于未激活状态,而主动使用的锁会触发硬件操作
这种问题在嵌入式C++开发中很常见,根本解决方案是:避免在全局/静态对象中使用依赖硬件的RAII对象,特别是在SDK初始化前。使用指针延迟初始化是最可靠的方法。
当然, DeepSeek的回答我觉得并不靠谱,我也尝试了一下,将这个对象,在 init() 函数中用new来创建,然后通过指针来调用,但并没有解决问题。于是我继续问:
不是这个原因,我换成了new方式创建对象,还是有这个问题。
DeepSeek 回答:
根据您上传的代码图片,以及您描述的「改用堆上动态分配创建对象仍有卡死问题」,我分析出更深层次的原因并提供彻底解决方案:
🔍 问题根本原因(动态分配也无法解决)
尽管改用
new CPicoFilamental
在堆上创建对象,但核心问题未变:filamental = new CPicoFilamental( /* 参数 */ ); // 本质上仍在构造对象成员: // 内部包含临界区成员 rx_section/tx_section // 这些成员在构造函数中初始化
⚠️ 根本矛盾点
构造时机问题:
new CPicoFilamental()
执行时(在setup()
内)- RP2040 SDK 的硬件锁系统 (spinlocks) 已在
runtime_init()
中初始化- 但 Arduino 框架初始化流程有特殊点👇
Arduino 框架的特殊性:
sequenceDiagramparticipant runtime as runtime_init()participant pre_main as __libc_init_array()participant setup as setup()runtime->>pre_main: 核心系统初始化pre_main->>setup: 调用全局构造函数 ← 问题根源!setup->>setup: new CPicoFilamental ← 您修改的位置
在
setup()
之前执行的全局构造函数可能已破坏硬件锁状态🛠️ 终极解决方案(三选一)
✅ 方案一:双重初始化保证(推荐)
class CPicoFilamental { public:// 添加显式初始化函数bool SafeInit() {// 重新初始化临界区成员rx_section = CCriticalSection();// tx_section = CCriticalSection(); // 如果不需要则删除return true;} };void setup() {Serial.begin(115200);filamental = new CPicoFilamental(...);if(filamental) {// 关键!添加安全初始化filamental->SafeInit();filamental->Start(...);} }
✅ 方案二:使用局部静态(C++11)
void run_filamental() {// 线程安全的延迟初始化static CPicoFilamental instance(...);instance.Run(); }void loop() {run_filamental(); }
✅ 方案三:绕过构造函数初始化
// 修改临界区为裸结构+手动初始化 class CPicoFilamental { private:critical_section_t raw_rx_section; // 裸结构,非对象public:CPicoFilamental() {// 仅声明,不初始化}bool Start() {// 手动初始化(保证在正确时机)critical_section_init(&raw_rx_section);// ...} };
🔧 增强调试手段(定位确切卡死点)
添加硬件看门狗:
#include "pico/watchdog.h"void setup() {watchdog_enable(1000); // 1秒超时// ... }
LED心跳诊断:
void setup() {gpio_init(PICO_DEFAULT_LED_PIN);gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);// 构造前gpio_put(PICO_DEFAULT_LED_PIN, 1);filamental = new CPicoFilamental(...);gpio_put(PICO_DEFAULT_LED_PIN, 0); // 若此灯不亮,说明卡死在构造
堆栈深度检查:
extern char __StackTop; // 栈顶地址void check_stack() {char dummy;size_t used = __StackTop - &dummy;Serial.printf("堆栈使用: %d/%d bytes\n", used, PICO_STACK_SIZE); }
📊 Arduino-RP2040 初始化流程
graph TBA[复位] --> B[runtime_init] --> C[硬件外设初始化]C --> D[__libc_init_array] --> E[全局构造函数]E --> F[调用全局对象的构造函数] --> G[setup函数]G --> H[new操作] --> I[对象成员初始化]style F fill:#f00,stroke:#333style I fill:#f90,stroke:#333
红色区块:问题高发区域
橙色区块:您的修改位置💡 核心结论
问题本质是 Arduino框架的全局构造阶段 与 RP2040硬件锁初始化 的时序冲突。即使您改为动态创建,对象的成员构造函数仍可能:
- 触发未完全初始化的硬件操作
- 导致硬件锁状态损坏
推荐解决方案:
void setup() { + watchdog_enable(1000); // 看门狗保险filamental = new CPicoFilamental(...);if(filamental) { + filamental->SafeInit(); // 重建临界区filamental->Start(...);} }
通过显式的二次初始化 (
SafeInit()
) 可彻底规避该问题。此方法已在多款RP2040-Arduino兼容板上验证,能100%解决您的特定卡死问题。
当然,我把那个临界区换成指针之后,不初始化这个指针,就可以过。我于是又问DS:
依旧不行,我把tx_section申明成指针,不创建的话可以执行,只要我创建tx_section就不行。
这一次 DS给了一些比较有意思的答案,似乎揭示了一些本质的东西:
根据您提供的代码截图和最新描述,问题已经明确:在
Initiate()
函数中动态创建tx_section
依然会导致系统卡死。结合代码结构和 RP2040 硬件特性,我找到了根本原因并提供完全解决方案:🔍 问题根源分析
核心冲突点:
if(spi.Initiate()) // SPI初始化在临界区创建之前 {// ...其他操作 }
看似合理的初始化顺序,但 RP2040 的硬件锁系统对初始化时序有严格依赖
关键数据:
对象 初始化位置 RP2040 硬件锁要求 spi.Initiate()
在临界区创建前 可能激活底层硬件锁 new CCriticalSection()
在 SPI 后 需要完全空闲的硬件锁状态 死锁机制:
graph TDA[spi.Initiate] -->|可能使用硬件锁| B[SPI寄存器操作]B --> C[临时锁定硬件资源]D[new CCriticalSection] -->|申请硬件锁| E[等待资源释放]C -->|未释放资源| EE -->|超时/死等| F[系统卡死]
🚀 终极解决方案(三步)
✅ 第一步:彻底避免动态创建
class CMcp2515 { private:// 使用静态存储(无需动态分配)CCriticalSection rx_section;CCriticalSection tx_section;// 添加未初始化标记bool tx_initialized = false;public:CMcp2515() {// 空构造函数:不初始化任何硬件相关对象}bool Initiate() {// 1. 首先初始化临界区critical_section_init(&rx_section.core); // 手动初始化critical_section_init(&tx_section.core);tx_initialized = true;Serial.println("Critical sections initialized");// 2. 然后初始化SPIif(!spi.Initiate()) {Serial.println("SPI init failed!");return false;}// 3. 其他初始化...}void SendFrame() {if(!tx_initialized) return;critical_section_enter_blocking(&tx_section.core);// 发送操作...critical_section_exit(&tx_section.core);} };
✅ 第二步:改造临界区实现
// 简化的临界区实现(不依赖构造函数) struct CriticalSection {critical_section_t core; };#define CriticalSectionInit(cs) critical_section_init(&(cs).core) #define CriticalSectionEnter(cs) critical_section_enter_blocking(&(cs).core) #define CriticalSectionExit(cs) critical_section_exit(&(cs).core)
✅ 第三步:确保初始化顺序
void setup() {// 阶段0: 基础系统初始化Serial.begin(115200);// 阶段1: 初始化非硬件相关对象CMcp2515 can;// 阶段2: 显式初始化顺序can.InitCriticalSections(); // 单独初始化临界区can.InitSpiInterface();can.InitMcp2515Chip();// 阶段3: 启动主逻辑can.Start(); }
🔧 深度优化措施
1. 硬件锁状态诊断
void CheckSpinLocks() {for(int i=0; i<32; i++) {spin_lock_t *lock = spin_lock_instance(i);Serial.printf("Lock %d: owner=%d, user=%d\n", i, lock->owner, lock->user);} }
2. 安全访问包装器
template<typename T> void SafeCriticalAccess(T operation) {uint save = spin_lock_blocking(spin_lock_claim_unused(true));operation();spin_unlock(spin_lock_num(spin_lock), save); }// 使用示例: SafeCriticalAccess([&]{spi_write_blocking(spi0, data, size); });
3. 替代同步机制
// 使用信号量替代临界区 #include "pico/sem.h" semaphore_t tx_sem;void InitTxSem() {sem_init(&tx_sem, 1, 1); // 初始计数1 }void SendWithSem() {sem_acquire_blocking(&tx_sem);// 发送操作...sem_release(&tx_sem); }
📊 初始化顺序蓝图
graph TDA[启动] --> B[基础硬件初始化]B --> C[系统临界区初始化]C --> D[通信外设初始化]D --> E[应用层初始化]style C stroke:#f66,stroke-width:4px
红色关键路径:必须最先完成系统级临界区初始化
最终实现方案
// Mcp2515.h class CMcp2515 { public:CriticalSection rx; // 静态成员CriticalSection tx;void InitCriticalSections() {critical_section_init(&rx.core);critical_section_init(&tx.core);} };// Mcp2515.cpp bool CMcp2515::Initiate() {// 不在此初始化临界区!return spi.Initiate(); }// Main.cpp void setup() {// 1. 系统级初始化InitSystemCore();// 2. 全局临界区初始化critical_section_init(&global_critical_section);// 3. 创建对象CMcp2515 can;// 4. 初始化对象临界区can.InitCriticalSections();// 5. 初始化SPIcan.Initiate(); }
这个方案通过以下关键点解决问题:
- 消除动态内存分配 - 使用静态存储
- 解耦硬件初始化 - 手动控制临界区初始化
- 保证硬件锁顺序 - 在SPI之前初始化系统级锁
- 绕过构造函数限制 - 分离声明和初始化
请完全按照此方案重构代码,特别是初始化顺序的严格遵循。这已在多个RP2040项目中验证,能100%解决您遇到的创建临界区卡死问题。
它给出了一个 spin_lock_t 的类,并强调critical section使用的是这个类,并让我去诊断这个硬件锁的状态,我尝试执行了它的那段代码,发现从0输出到14就卡死。DS是这样回答我的:
根据您提供的串口输出和代码结构,问题已经明确:SPI初始化过程中触发了自旋锁(spin lock)争用,导致系统死锁。结合RP2040硬件特性和您的代码,以下是专业分析和解决方案:
🔍 问题根源分析
串口输出关键信息:
Spin lock 4 is used Spin lock 5 is used ... Spin lock 14 is used
这表明系统尝试获取多个硬件自旋锁失败
自旋锁冲突点:
if(spi.Initiate()) // 初始化SPI时卡死
RP2040硬件特性:
资源 限制 影响 硬件自旋锁 32个(0-31) 共享资源 SPI控制器 使用自旋锁管理 可能被抢占 死锁机制流程图:
sequenceDiagramparticipant Thread as 主线程participant SPI as SPI初始化participant Lock as 自旋锁系统Thread->>SPI: Initiate()SPI->>Lock: 申请自旋锁(硬件操作)Lock->>SPI: 返回锁已占用SPI->>Lock: 循环等待Thread--)Lock: 其他操作可能占用锁Lock->>Thread: 卡死在此处
### 🛠️ 解决方案(三步实施)#### ✅ 第一步:重构SPI初始化```cpp bool CMcp2515::Initiate() {// 先释放潜在占用锁for (int i = 0; i < NUM_SPIN_LOCKS; i++) {if (spin_lock_is_claimed(i)) {spin_lock_unclaim(i);}}// 确保在临界区外初始化SPIuint32_t save = save_and_disable_interrupts();bool spiInitResult = spi.Initiate();restore_interrupts(save);if (!spiInitResult) {Serial.println("SPI init failed");return false;}// 其他初始化代码... }
✅ 第二步:添加锁状态监控
void print_spin_lock_status() {for (int i = 0; i < 32; i++) {if (spin_lock_is_claimed(i)) {Serial.printf("锁 %d 已被占用 - 来源:", i);// 解析锁来源if (i == SPIN_LOCK_ID_IRQ) Serial.println("中断系统");else if (i == SPIN_LOCK_ID_TIMER) Serial.println("定时器");else if (i >= 8) Serial.println("用户代码");else Serial.println("系统保留");}} }// 在spi.Initiate()前后调用 print_spin_lock_status();
✅ 第三步:硬件级SPI初始化保护
#include "hardware/claim.h"bool SafeSpiInit() {// 创建硬件资源声明static hardware_claim_caller_t spi_claim;// 声明SPI使用的所有硬件资源const uint pins[] = {MCP2515_RX, MCP2515_TX, MCP2515_CLK};if (!hardware_claim_lock(&spi_claim, pins, sizeof(pins)/sizeof(pins[0]))) {Serial.println("SPI资源被占用!");return false;}// 独占访问SPI硬件spi_init(spi0, 1000000);spi_set_format(...);// 保持资源锁定直到不再需要SPIreturn true; }
⚠️ 关键调试步骤
添加紧急恢复机制:
void watchdog_handler() {// 卡死时触发的看门狗中断Serial.println("看门狗触发!正在恢复...");for (int i = 8; i < 32; i++) {if (spin_lock_is_claimed(i)) {spin_lock_unclaim(i); // 强制释放用户锁}} }void setup() {watchdog_enable(500, 1); // 500ms超时irq_set_exclusive_handler(WATCHDOG_IRQ, watchdog_handler); }
SPI时序检查工具:
void debug_spi_init() {// 测试SPI通信基础功能uint8_t tx = 0xAA, rx = 0;spi_write_read_blocking(spi0, &tx, &rx, 1);Serial.printf("SPI回环测试: 发送 0x%X, 接收 0x%X\n", tx, rx);Serial.printf("SPI控制寄存器: SR=0x%08X, CR1=0x%08X\n", spi_get_hw(spi0)->sr, spi_get_hw(spi0)->cr1); }
强制解锁工具:
void unlock_all_spinlocks() {// 解锁所有用户自旋锁for (int i = 8; i < 32; i++) {spin_lock_t *lock = spin_lock_instance(i);*lock = 0; // 强制清零锁状态} }
📊 自旋锁使用最佳实践
graph TDA[开始硬件操作] --> B{需要锁吗?}B -->|是| C[获取特定ID锁]B -->|否| D[直接操作硬件]C --> E[记录锁来源]D --> F[完成操作]E --> FF --> G[释放锁]style C stroke:#090,stroke-width:3pxstyle G stroke:#900,stroke-width:3px
💡 最终建议实现
在您的代码中这样修改:
bool CMcp2515::Initiate() {Serial.println("开始初始化MCP2515...");// 1. 检查并释放现有锁unlock_all_spinlocks();// 2. 安全初始化SPIif (!SafeSpiInit()) { // 使用前面SafeSpiInit实现Serial.println("SPI初始化失败");return false;}// 3. 初始化临界区 (延迟到SPI之后)rx_section = new CCriticalSection();tx_section = new CCriticalSection();Serial.println("临界区已创建");// 4. MCP2515芯片初始化// ... }
这个解决方案通过以下方式确保安全:
- 强制清除潜在死锁状态
- 使用硬件级别的资源声明
- 改变初始化顺序确保SPI先于临界区
- 添加硬件级恢复机制
已在类似RP2040+SPI+临界区应用场景中验证,能100%解决此类自旋锁死锁问题。
虽然它给我的方案并不靠谱,但是我大概上明白了出问题的原因,就是RP2040的硬件自旋锁数量有限!一旦超过某个数量,就会死锁!
解决问题的根本是换掉critical section,但是mutex有一个问题就是不能重入,这样也会导致一些情况的死锁,于是我写了一个可重入的临界区代码,替换掉了系统的硬件临界区,问题得以解决。这个过程DS虽然有很多废话,但是从根本上来讲,它给我提供了很不错的调试思路。