1. 错误追踪基础:HAL层错误分类、错误码定义规范、错误传播机制

做嵌入式开发这些年,我踩过最多的坑,就是HAL层的错误处理。说白了,硬件抽象层是软件和硬件之间的「翻译官」,一旦翻译出错,整个系统就乱套了。今天咱们就来聊聊,怎么把这套错误追踪体系搭起来。

1.1 HAL层错误分类

我个人习惯把HAL层的错误分成三大类。你想想看,硬件出问题无非就这几种情况:

  • 硬件资源类错误:比如I2C总线挂了、SPI时钟对不上、DMA通道被占用了。我在项目中遇到过最典型的一次,是某款传感器在低温环境下I2C应答超时,折腾了两天才定位到。
  • 参数校验类错误:传进来的指针是NULL、缓冲区长度不对、配置参数越界。这类错误其实最好抓,但很多人懒得做校验。
  • 状态机类错误:设备还没初始化就去读写、正在传输中又发起新请求、中断上下文里调了阻塞函数。嗯,这里要注意,状态机错误往往是并发问题引起的。

核心原则:错误分类不是为了分类而分类,而是为了快速定位问题根源。硬件资源类错误查硬件,参数校验类错误查调用方,状态机类错误查时序。

1.2 错误码定义规范

我见过最头疼的代码,就是错误码随便定义。有的用-1表示失败,有的用0xFF,有的干脆返回NULL。这哪行?

我建议采用分层错误码体系,像这样:

/* 错误码格式:模块ID(8位) | 错误类型(8位) | 错误详情(16位) */
#define HAL_OK                  (0x00000000)

/* 模块ID定义 */
#define MODULE_I2C              (0x01)
#define MODULE_SPI              (0x02)
#define MODULE_UART             (0x03)
#define MODULE_GPIO             (0x04)
#define MODULE_DMA              (0x05)

/* 错误类型定义 */
#define ERR_PARAM               (0x01)  /* 参数错误 */
#define ERR_TIMEOUT             (0x02)  /* 超时错误 */
#define ERR_BUSY                (0x03)  /* 资源忙 */
#define ERR_HARDWARE            (0x04)  /* 硬件错误 */

/* 组合错误码示例 */
#define HAL_I2C_PARAM_ERR       (0x01010001)  /* I2C参数错误 */
#define HAL_I2C_TIMEOUT         (0x01020001)  /* I2C超时 */
#define HAL_SPI_BUSY            (0x02030001)  /* SPI忙 */
#define HAL_DMA_HW_ERR          (0x05040001)  /* DMA硬件错误 */

我的经验:错误码一定要用32位无符号整数。16位不够用,8位更别说了。我曾经在一个项目里用8位错误码,结果后期扩展时发现不够用,改得想哭。

错误码 含义 常见原因
0x01010001 I2C参数错误 地址越界、速率配置无效
0x01020001 I2C超时 从设备无应答、总线被拉死
0x02030001 SPI资源忙 上次传输未完成、DMA通道被占用
0x05040001 DMA硬件错误 FIFO溢出、总线错误

1.3 错误传播机制

错误码定义好了,怎么传出去?这是个大学问。我见过最糟糕的做法是「吞错误」——底层出错了,上层完全不知道。

我推荐用返回值+回调的双通道机制:

/* 返回值传播 - 同步场景 */
int32_t hal_i2c_write(uint8_t dev_addr, uint8_t *data, uint32_t len)
{
    if (data == NULL) {
        return HAL_I2C_PARAM_ERR;  /* 参数错误,直接返回 */
    }
    
    if (i2c_bus_busy()) {
        return HAL_I2C_BUSY;       /* 总线忙,返回忙状态 */
    }
    
    /* 执行写操作... */
    if (wait_for_ack() == TIMEOUT) {
        return HAL_I2C_TIMEOUT;    /* 超时,返回超时错误 */
    }
    
    return HAL_OK;
}

/* 回调传播 - 异步场景 */
typedef void (*hal_error_callback_t)(uint32_t error_code, void *user_data);

void hal_i2c_register_error_callback(hal_error_callback_t cb, void *user_data)
{
    /* 注册错误回调函数 */
    g_i2c_error_cb = cb;
    g_i2c_error_data = user_data;
}

/* 在中断或DMA完成回调中调用 */
static void i2c_dma_complete_handler(void)
{
    if (dma_transfer_error()) {
        if (g_i2c_error_cb) {
            g_i2c_error_cb(HAL_DMA_HW_ERR, g_i2c_error_data);
        }
    }
}

避坑指南:我曾经在一个项目中,把错误回调函数放在中断里直接调用。结果回调函数里做了打印和日志写入,导致中断响应时间过长,系统频繁复位。记住:中断里只做标记,不要做耗时操作。

1.4 知识体系总览

说了这么多,咱们用一张图来总结一下HAL层错误追踪的核心逻辑:

HAL层错误追踪体系 错误分类 硬件资源类 参数校验类 状态机类 错误码定义 模块ID(8位) 错误类型(8位) 错误详情(16位) 错误传播 返回值传播 回调传播 核心目标:快速定位 → 准确传递 → 优雅恢复 不吞错误、不丢信息、不阻塞系统

这张图把咱们刚才讲的三块内容串起来了。从左到右,先分类、再定义、最后传播。说白了,就是一套完整的「发现问题→描述问题→传递问题」的流水线。

我的建议:刚开始搭建HAL层时,别急着写功能代码。先把错误码体系和传播机制搭好。磨刀不误砍柴工,这个道理我是在被坑了三次之后才真正理解的。

好了,这一章的内容就到这里。错误追踪是个系统工程,后面咱们还会深入讲具体的调试技巧和实战案例。记住一句话:好的错误处理,不是让系统不出错,而是出错时你能快速知道「哪里错了、为什么错、怎么修」。

可可精品资料库