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(); // 显式初始化临界区
}

⚠️ 关键修复原理

sequenceDiagram
    participant 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 SDK
    sdk->>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 框架的特殊性​​:

    sequenceDiagram
        participant 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 TB
    A[复位] --> B[runtime_init] --> C[硬件外设初始化]
    C --> D[__libc_init_array] --> E[全局构造函数]
    E --> F[调用全局对象的构造函数] --> G[setup函数]
    G --> H[new操作] --> I[对象成员初始化]
    
    style F fill:#f00,stroke:#333
    style 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 TD
        A[spi.Initiate] -->|可能使用硬件锁| B[SPI寄存器操作]
        B --> C[临时锁定硬件资源]
        D[new CCriticalSection] -->|申请硬件锁| E[等待资源释放]
        C -->|未释放资源| E
        E -->|超时/死等| 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. 然后初始化SPI
        if(!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 TD
    A[启动] --> 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. 初始化SPI
    can.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. ​死锁机制流程图​​:

    sequenceDiagram
        participant 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);
        }
    }
    
    // 确保在临界区外初始化SPI
    uint32_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(...);
    
    // 保持资源锁定直到不再需要SPI
    return 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 TD
    A[开始硬件操作] --> B{需要锁吗?}
    B -->|是| C[获取特定ID锁]
    B -->|否| D[直接操作硬件]
    C --> E[记录锁来源]
    D --> F[完成操作]
    E --> F
    F --> G[释放锁]

    style C stroke:#090,stroke-width:3px
    style G stroke:#900,stroke-width:3px

💡 最终建议实现

在您的代码中这样修改:

bool CMcp2515::Initiate() {
    Serial.println("开始初始化MCP2515...");
    
    // 1. 检查并释放现有锁
    unlock_all_spinlocks();
    
    // 2. 安全初始化SPI
    if (!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虽然有很多废话,但是从根本上来讲,它给我提供了很不错的调试思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值