3、HAL接口规范:命名、参数、返回值与回调

接口规范这东西,说白了就是团队里的“通用语言”。我见过太多项目,硬件换了,驱动代码就得重写一大半——根子就在接口没定好规矩。今天咱们就把HAL接口的四个核心维度掰开揉碎讲清楚。

3.1 接口命名约定

命名这件事,我个人的习惯是:自文档化。看到函数名,不用翻文档就知道它干嘛的。

3.1.1 函数命名规则

我建议采用 模块_动作_对象 的三段式结构:

// 推荐风格
hal_uart_init(UART_HandleTypeDef *huart);
hal_uart_transmit(UART_HandleTypeDef *huart, uint8_t *data, uint16_t len);
hal_uart_receive(UART_HandleTypeDef *huart, uint8_t *data, uint16_t len);

// 不推荐风格
uart_init();           // 没有模块前缀,容易冲突
init_uart();           // 动作在后,不符合阅读习惯
HAL_UART_Transmit();   // 全大写,看着累
我的经验:前缀用 hal_ 还是 HAL_?我建议统一小写加下划线。C语言里大小写敏感,全大写宏定义留给硬件寄存器地址,函数名保持小写,一眼就能区分。

3.1.2 宏与常量命名

宏定义我习惯全大写,加模块前缀:

#define UART_FLAG_TXE    (1U << 7)
#define UART_FLAG_TC     (1U << 6)
#define UART_BAUD_115200 115200U

枚举类型用 typedef enum,值也用全大写:

typedef enum {
    UART_PARITY_NONE = 0,
    UART_PARITY_EVEN,
    UART_PARITY_ODD
} UART_ParityTypeDef;

3.2 参数传递规范

参数怎么传?这里坑不少。我踩过的第一个大坑就是结构体传值——栈直接爆了。

3.2.1 基本类型参数

简单配置用值传递,清晰明了:

// 配置GPIO引脚
void hal_gpio_write(GPIO_Pin pin, GPIO_State state);

3.2.2 复杂对象用指针

结构体必须传指针,别问为什么——问就是栈空间不够:

// 正确做法:传指针
void hal_uart_init(UART_HandleTypeDef *huart);

// 错误做法:传结构体副本
void hal_uart_init(UART_HandleTypeDef huart);  // 栈溢出警告!
注意:我曾经在一个STM32F103项目里,同事把SPI的配置结构体传值了。结果初始化函数一调用,任务栈直接溢出,系统复位。查了两天才找到原因——结构体大小128字节,任务栈才256字节。

3.2.3 const 限定符

输入参数加 const,输出参数不加。这是规矩:

// 输入参数:const指针
void hal_spi_transmit(SPI_HandleTypeDef *hspi, const uint8_t *data, uint16_t len);

// 输出参数:非const指针
void hal_spi_receive(SPI_HandleTypeDef *hspi, uint8_t *buffer, uint16_t len);

3.3 返回值与错误码定义

返回值怎么设计?我见过五花八门的做法。有人返回 int,正数表示成功、负数表示错误;有人返回 bool,成功就 true。我的建议是:统一用枚举。

3.3.1 标准错误码枚举

typedef enum {
    HAL_OK       = 0,   // 成功
    HAL_ERROR    = 1,   // 通用错误
    HAL_BUSY     = 2,   // 资源忙
    HAL_TIMEOUT  = 3,   // 超时
    HAL_INVALID  = 4    // 参数无效
} HAL_StatusTypeDef;

每个函数都返回这个枚举,调用方统一处理:

HAL_StatusTypeDef hal_uart_transmit(UART_HandleTypeDef *huart,
                                    const uint8_t *data,
                                    uint16_t len) {
    if (huart == NULL || data == NULL) {
        return HAL_INVALID;
    }
    if (huart->State == HAL_UART_STATE_BUSY) {
        return HAL_BUSY;
    }
    // ... 发送逻辑 ...
    return HAL_OK;
}
核心原则:错误码要覆盖所有异常路径。我见过一个项目,DMA传输失败返回 HAL_ERROR,但调用方不知道是超时还是参数错。后来我加上了 HAL_TIMEOUT 和 HAL_INVALID,问题定位快多了。

3.3.2 错误码使用对比

场景 推荐返回值 说明
操作成功 HAL_OK 一切正常
参数为NULL HAL_INVALID 调用方传错了
硬件正忙 HAL_BUSY 稍后重试
等待超时 HAL_TIMEOUT 硬件没响应
其他异常 HAL_ERROR 需要进一步排查

3.4 回调函数设计模式

回调函数是HAL的灵魂。为什么?因为硬件操作往往是异步的——你发个数据,硬件慢慢传,传完了通知你。这时候就需要回调。

3.4.1 回调函数注册机制

我习惯在句柄结构体里放函数指针:

typedef struct {
    USART_TypeDef *Instance;
    uint8_t *pTxBuffPtr;
    uint16_t TxXferSize;
    void (*TxCpltCallback)(struct __UART_HandleTypeDef *huart);  // 发送完成回调
    void (*RxCpltCallback)(struct __UART_HandleTypeDef *huart);  // 接收完成回调
    void (*ErrorCallback)(struct __UART_HandleTypeDef *huart);   // 错误回调
} UART_HandleTypeDef;

用户只需要实现回调函数,然后注册:

void my_uart_tx_done(UART_HandleTypeDef *huart) {
    // 发送完成,可以发下一条了
    hal_gpio_write(LED_PIN, GPIO_STATE_ON);
}

int main(void) {
    UART_HandleTypeDef huart1;
    huart1.TxCpltCallback = my_uart_tx_done;
    hal_uart_init(&huart1);
    hal_uart_transmit(&huart1, "Hello", 5);
    while(1);
}

3.4.2 弱函数默认实现

有些用户不想实现所有回调。这时候用 __weak 提供默认空函数:

__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    // 默认什么都不做
    // 用户如果实现了同名函数,这个会被覆盖
}
小技巧:弱函数里可以加个调试打印,比如 printf("Warning: TxCpltCallback not implemented\n");。这样用户忘了实现回调,调试时能立刻发现。

3.4.3 回调触发时机

回调函数在中断服务函数里调用:

void USART1_IRQHandler(void) {
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC)) {
        __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC);
        if (huart1.TxCpltCallback != NULL) {
            huart1.TxCpltCallback(&huart1);  // 在中断上下文调用
        }
    }
}
注意:回调函数在中断里执行,所以不能做耗时操作。我见过有人直接在回调里调用 printf 打印日志,结果中断时间过长,系统响应变慢。正确的做法是:回调里只设置标志位,主循环里处理。

3.5 知识体系总览

下面这张图把HAL接口规范的四个维度串起来了:

HAL接口规范知识体系 HAL接口规范 命名约定 参数传递规范 返回值与错误码 回调设计 函数命名 宏与常量 值传递 指针传递 const限定 枚举定义 错误码覆盖 注册机制 弱函数 中断触发 核心原则:统一规范 + 自文档化 + 错误覆盖 + 异步回调 好的接口规范,让硬件更换时应用层代码零修改

3.6 避坑指南

最后分享几个我踩过的坑:

  • 回调函数里做延时:我曾经在SPI发送完成回调里加了 HAL_Delay(10),结果中断响应变慢,系统看门狗复位。记住:回调里只设标志,主循环里处理。
  • 错误码不检查:有个同事调用 hal_uart_transmit 后不检查返回值,结果参数传错了,数据没发出去,查了两天。后来我强制要求:每个HAL函数调用必须检查返回值。
  • 结构体传值:前面说过了,栈溢出。用指针,用指针,用指针——重要的事情说三遍。