当前位置: 首页 > news >正文

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 的硬件特性,我分析出根本原因并提供解决方案:

🔍 问题根源分析

  1. ​C++ 对象初始化顺序问题​​:

    private:CSpiProtocol<...> spi;       // 第1个成员tagCanFrame::List received;   // 第2个成员CCriticalSection rx_section; // 第3个成员CCriticalSection tx_section; // 第4个成员 <- 问题点!
    • C++ 对象成员按照​​声明顺序初始化​
    • tx_section 初始化发生在 spi 之后
  2. ​RP2040 硬件锁的特殊性​​:

    • CCriticalSection 底层使用硬件自旋锁 (Spinlocks)
    • Spinlocks 在 runtime_init() 之前不可用
    • 全局/静态对象在 main() 之前初始化 -> ​​在 SDK 初始化前使用了硬件锁​
  3. ​症状表现​​:

    情况结果
    声明 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()

🔧 验证测试建议

  1. ​添加构造日志​​:

    CCriticalSection::CCriticalSection() {Serial.printf("CriticalSection CTOR @ %lu\n", millis());
    }
  2. ​检查初始化顺序​​:

    extern "C" void runtime_init() {Serial.println("SDK初始化开始!");// ...
    }
  3. ​硬件锁状态检查​​:

    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
//   这些成员在构造函数中初始化

⚠️ 根本矛盾点

  1. ​构造时机问题​​:

    • new CPicoFilamental() 执行时(在 setup() 内)
    • RP2040 SDK 的硬件锁系统 (​​spinlocks​​) 已在 runtime_init() 中初始化
    • ​但​​ Arduino 框架初始化流程有特殊点👇
  2. ​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);// ...}
};

🔧 增强调试手段(定位确切卡死点)

  1. ​添加硬件看门狗​​:

    #include "pico/watchdog.h"void setup() {watchdog_enable(1000); // 1秒超时// ...
    }
  2. ​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); // 若此灯不亮,说明卡死在构造
  3. ​堆栈深度检查​​:

    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硬件锁初始化​​ 的时序冲突。即使您改为动态创建,对象的成员构造函数仍可能:

  1. 触发未完全初始化的硬件操作
  2. 导致硬件锁状态损坏

​推荐解决方案​​:

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 硬件特性,我找到了根本原因并提供完全解决方案:

🔍 问题根源分析

  1. ​核心冲突点​​:

    if(spi.Initiate()) // SPI初始化在临界区创建之前
    {// ...其他操作
    }

    看似合理的初始化顺序,但 ​​RP2040 的硬件锁系统对初始化时序有严格依赖​

  2. ​关键数据​​:

    对象初始化位置RP2040 硬件锁要求
    spi.Initiate()在临界区创建前可能激活底层硬件锁
    new CCriticalSection()在 SPI 后需要完全空闲的硬件锁状态
  3. ​死锁机制​​:

    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();
}

这个方案通过以下关键点解决问题:

  1. ​消除动态内存分配​​ - 使用静态存储
  2. ​解耦硬件初始化​​ - 手动控制临界区初始化
  3. ​保证硬件锁顺序​​ - 在SPI之前初始化系统级锁
  4. ​绕过构造函数限制​​ - 分离声明和初始化

请完全按照此方案重构代码,特别是初始化顺序的严格遵循。这已在多个RP2040项目中验证,能100%解决您遇到的创建临界区卡死问题。

        它给出了一个 spin_lock_t 的类,并强调critical section使用的是这个类,并让我去诊断这个硬件锁的状态,我尝试执行了它的那段代码,发现从0输出到14就卡死。DS是这样回答我的:

根据您提供的串口输出和代码结构,问题已经明确:​​SPI初始化过程中触发了自旋锁(spin lock)争用,导致系统死锁​​。结合RP2040硬件特性和您的代码,以下是专业分析和解决方案:

🔍 问题根源分析

  1. ​串口输出关键信息​​:

    Spin lock 4 is used
    Spin lock 5 is used
    ...
    Spin lock 14 is used

    这表明系统尝试获取多个硬件自旋锁失败

  2. ​自旋锁冲突点​​:

    if(spi.Initiate()) // 初始化SPI时卡死
  3. ​RP2040硬件特性​​:

    资源限制影响
    硬件自旋锁32个(0-31)共享资源
    SPI控制器使用自旋锁管理可能被抢占
  4. ​死锁机制流程图​​:

    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;
}

⚠️ 关键调试步骤

  1. ​添加紧急恢复机制​​:

    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);
    }
  2. ​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);
    }
  3. ​强制解锁工具​​:

    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芯片初始化// ...
}

这个解决方案通过以下方式确保安全:

  1. 强制清除潜在死锁状态
  2. 使用硬件级别的资源声明
  3. 改变初始化顺序确保SPI先于临界区
  4. 添加硬件级恢复机制

已在类似RP2040+SPI+临界区应用场景中验证,能100%解决此类自旋锁死锁问题。

        虽然它给我的方案并不靠谱,但是我大概上明白了出问题的原因,就是RP2040的硬件自旋锁数量有限!一旦超过某个数量,就会死锁!

        解决问题的根本是换掉critical section,但是mutex有一个问题就是不能重入,这样也会导致一些情况的死锁,于是我写了一个可重入的临界区代码,替换掉了系统的硬件临界区,问题得以解决。这个过程DS虽然有很多废话,但是从根本上来讲,它给我提供了很不错的调试思路。

相关文章:

  • 思维力三阶 · 序章:从认知碎片到系统思维——点亮内心的“认知操作系统”蓝图
  • 江科大睡眠,停止,待机模式hal库实现
  • 长参考帧LTR
  • [Java 基础]面向对象-多态
  • C语言到底使用什么编码
  • git删除本地分支和远程分支
  • 【力扣】3403. 从盒子中找出字典序最大的字符串 I
  • 2025年6月4日收获
  • 佳易王钟表手表维修养护管理系统:高效便捷的维修管理解决方案
  • NX948NX951美光固态芯片NX956NX960
  • 《江西棒垒球》败方mvp叫什么·棒球1号位
  • Python数据可视化科技图表绘制系列教程(二)
  • 高防IP可以防护什么攻击类型?企业网络安全的第一道防线
  • zookeeper 学习
  • 尝试使用gocryptfs实现大模型加密部署
  • 如何利用乐维网管进行IP管理
  • Python中的`with`语句用法
  • 如何选择合适的分库分表策略
  • MQTT协议:物联网时代的通信基石
  • 《manacher》题集
  • 电子商务网站建设需要什么/广告公司推广平台
  • 建立一个购物网站/seo网站推广收费
  • 今日国内最新头条新闻/百度seo服务公司
  • 国外网站开发发展历程/网站seo的方法
  • 一个人可以建设几个网站/内容营销的4个主要方式
  • 深圳知名网站建设/十大免费excel网站