第三章:裸机调试基础

各位同学,今天咱们聊聊裸机调试。说白了,就是没有操作系统的情况下,怎么跟你的单片机“对话”。

我刚开始做航天嵌入式那会儿,最怕的就是程序跑飞了,但不知道飞哪儿去了。后来老工程师教了我几招,今天全部分享给你们。

3.1 GPIO点灯调试法

这招看起来土,但真管用。我到现在还经常用。

原理很简单:用LED的亮灭来表示程序执行到了哪里。你想想看,一个灯能表达的信息其实不少。

核心思路:把GPIO当作“示波器探头”,用眼睛看电平变化。

具体怎么做?我一般这样用:

  • 单灯指示:程序跑到关键位置,点亮LED。比如初始化完成、进入主循环、发生错误。
  • 多灯组合:用多个LED表示状态码。比如红灯+绿灯表示模式1,红灯+蓝灯表示模式2。
  • 闪烁频率:快闪表示紧急情况,慢闪表示正常运行。

举个实际例子。我在项目中遇到过一个问题:某型飞控的传感器初始化偶尔失败。用串口打印吧,失败时串口还没初始化好。怎么办?

我就在初始化函数里加了个GPIO翻转:

// 传感器初始化函数
void sensor_init(void) {
    // 第一步:上电
    GPIO_SetPin(HIGH);  // 灯亮
    delay_ms(10);
    GPIO_SetPin(LOW);   // 灯灭
    
    // 第二步:复位
    GPIO_SetPin(HIGH);
    delay_ms(5);
    GPIO_SetPin(LOW);
    
    // 第三步:读取ID
    uint8_t id = read_reg(0x00);
    if (id != EXPECTED_ID) {
        // 失败!灯快速闪烁
        while(1) {
            GPIO_Toggle();
            delay_ms(100);
        }
    }
}

小技巧:我习惯把调试用的GPIO放在不用的引脚上,比如JTAG接口的空闲引脚。这样不影响正常功能。

3.2 串口打印调试

GPIO点灯虽然直观,但信息量有限。串口打印就不一样了,你能看到具体的数据和状态。

嗯,这里要注意:串口调试有个前提——你的串口驱动必须稳定可靠。否则你看到的可能是假象。

我一般这样用串口:

  • 关键变量打印:在程序的关键节点,打印当前变量的值。
  • 状态机跟踪:打印当前状态和触发事件。
  • 错误信息输出:发生异常时,打印错误码和上下文。

举个例子,我曾经调试一个步进电机控制程序,电机总是走不准。用串口一打印,发现是速度计算时发生了整数溢出:

void motor_step(void) {
    static uint32_t step_count = 0;
    step_count++;
    
    // 打印当前步数
    debug_printf("Step: %lu\n", step_count);
    
    // 计算位置
    uint32_t position = step_count * STEP_ANGLE;  // 这里溢出了!
    debug_printf("Position: %lu\n", position);
}

看到打印结果,我立刻发现问题:step_count到一定值后,position突然变小了。这就是典型的整数溢出。

避坑指南:我曾经在串口中断里调用printf,结果导致死循环。为什么?因为printf本身可能触发中断,造成递归调用。记住:中断服务函数里尽量别用打印函数。

3.3 断点调试技巧

断点调试是最强大的调试手段,但也是最容易被滥用的。

我见过很多新手,一上来就全程序设断点,然后单步执行。结果呢?跑了一整天,啥也没发现。

我个人习惯这样用断点:

  • 条件断点:只在特定条件满足时停下。比如变量等于某个值,或者计数器达到阈值。
  • 数据断点:监控某个内存地址的读写。当数据被意外修改时,立刻停下。
  • 函数断点:在函数入口或出口设断点,观察参数和返回值。

举个例子,我调试过一个CAN总线通信问题。数据偶尔会丢包,但很难复现。我就在发送函数里设了个条件断点:

void can_send(uint32_t id, uint8_t* data, uint8_t len) {
    // 条件断点:只在ID为0x123时停下
    if (id == 0x123) {
        // 在这里设断点
        debug_printf("Sending ID 0x123\n");
    }
    
    // 正常发送逻辑
    // ...
}

这样,只有特定ID的数据发送时才会停下,大大缩小了排查范围。

核心原则:断点不是越多越好,而是要精准。一个精心设置的条件断点,胜过一百个普通断点。

3.4 三种方法的对比

这三种方法各有优劣,我整理了个表格,方便你们对比:

方法 优点 缺点 适用场景
GPIO点灯 简单可靠,不依赖外设 信息量少,只能表达状态 初始化阶段、中断服务、实时性要求高的场景
串口打印 信息丰富,可输出变量值 依赖串口驱动,可能影响时序 逻辑调试、状态跟踪、数据分析
断点调试 最强大,可观察任意变量 会暂停程序,不适合实时系统 复杂逻辑分析、内存问题排查

我个人建议:三种方法结合使用。先用GPIO点灯快速定位问题范围,再用串口打印获取详细信息,最后用断点深入分析。

我的经验:在航天项目中,我通常会在硬件上预留一个调试LED和串口接口。即使产品交付后,这些接口也能用于现场排错。记住:调试接口不是浪费,是保险。

好了,这一章就到这里。下一章咱们聊聊中断调试,那可是个容易出坑的地方。