3. HAL 接口定义规范:从命名到组织的完整体系

接口定义规范这东西,说白了就是团队协作的「通用语言」。我早年吃过不少亏——同一个芯片,不同驱动工程师写的接口风格天差地别,移植时改得想骂人。后来我总结了一套规矩,今天全盘托出。

核心原则:接口规范不是为了限制你,而是为了让你和未来的自己少吵架。

3.1 命名规范:一眼看懂,不用猜

命名是接口的第一张脸。我习惯用「模块_动作_属性」三段式,比如 GPIO_SetPinLevel。你想想看,看到这个名字,是不是立刻知道它在干什么?

3.1.1 函数命名

模式 示例 说明
HAL_模块_动作 HAL_UART_Init 初始化类操作
HAL_模块_属性_Get/Set HAL_GPIO_PinLevel_Get 属性读写
HAL_模块_状态_Is HAL_TIM_Channel_IsBusy 状态查询,返回布尔
HAL_模块_回调_事件 HAL_UART_RxCallback 异步回调
我的习惯:函数名控制在 5 个单词以内。超过这个长度,说明你的模块划分可能有问题。

3.1.2 宏定义命名

宏定义我坚持全大写 + 下划线分隔。曾经有个项目,有人用 MaxBufSize,有人用 MAX_BUF_SIZE,结果预编译时冲突了三次。从那以后,我立了规矩:

  • 模块前缀:HAL_模块_含义,如 HAL_UART_BAUDRATE_115200
  • 位掩码:HAL_模块_FLAG_位名,如 HAL_SPI_FLAG_TX_EMPTY
  • 错误码:HAL_ERR_模块_错误类型,如 HAL_ERR_UART_TIMEOUT

3.1.3 枚举类型命名

枚举我偏爱 HAL_模块_类型_取值 的格式。举个例子:

typedef enum {
    HAL_GPIO_DIR_INPUT  = 0,
    HAL_GPIO_DIR_OUTPUT = 1,
    HAL_GPIO_DIR_ALT    = 2
} HAL_GPIO_Direction_t;

注意那个 _t 后缀,我要求所有自定义类型都带上。为什么?因为看代码时一眼就能区分「这是类型」还是「这是变量」。

3.2 参数规范:把话说清楚

参数是接口的「输入输出通道」。我见过最坑的接口,参数名就叫 param1、param2,文档还丢了。嗯,这种接口谁敢用?

3.2.1 输入输出标注

我强制要求每个参数前面加方向修饰:

  • in_:输入参数,函数只读
  • out_:输出参数,函数写
  • io_:输入输出参数,先读后写
HAL_Status_t HAL_UART_Transmit(
    HAL_UART_Handle_t*  io_handle,    // 句柄,读写
    const uint8_t*      in_data,      // 数据缓冲区,只读
    uint32_t            in_len,       // 长度,只读
    uint32_t            out_sent_len  // 实际发送长度,输出
);
避坑指南:我曾经遇到一个工程师把输出参数写成 uint32_t* len,没加 out_ 前缀。结果调用方以为传 NULL 也没事,直接传了 NULL,函数里解引用就崩了。加个 out_,调用方自然知道要传有效地址。

3.2.2 参数范围约束

每个参数都要有明确的取值范围。我习惯在头文件里用注释写明:

/**
 * @brief 设置GPIO引脚电平
 * @param in_pin    引脚编号,范围 0-15
 * @param in_level  电平值,0=低,1=高
 * @return HAL_OK 成功,其他见错误码定义
 */
HAL_Status_t HAL_GPIO_SetPinLevel(uint8_t in_pin, uint8_t in_level);

对于枚举参数,我建议用 assert 做运行时检查:

void HAL_I2C_SetSpeed(HAL_I2C_Speed_t in_speed) {
    assert(in_speed <= HAL_I2C_SPEED_FAST_PLUS);
    // ...
}

3.3 返回值规范:统一错误码,别玩花样

返回值是接口的「健康报告」。我见过有人返回 -1 表示失败,有人返回 NULL,有人返回 false。移植时每个接口都要查文档,效率极低。

3.3.1 标准错误码定义

错误码 值 含义
HAL_OK 0 成功
HAL_ERR_PARAM -1 参数错误
HAL_ERR_TIMEOUT -2 超时
HAL_ERR_BUSY -3 资源忙
HAL_ERR_NODEV -4 设备不存在
HAL_ERR_UNSUPPORTED -5 不支持的操作

我要求所有接口统一返回 HAL_Status_t 类型。这样上层代码可以统一处理:

HAL_Status_t ret = HAL_UART_Init(&handle);
if (ret != HAL_OK) {
    // 统一错误处理
    ErrorHandler(ret);
}
我的经验:错误码用负数,成功用 0。这样 if (ret) 就能判断失败,简洁直观。

3.4 头文件组织规范:别让 include 变成灾难

头文件组织是接口规范的最后一道防线。我见过一个项目,头文件互相 include,编译一次要 10 分钟。后来我定了三条铁律:

3.4.1 头文件层级

HAL 头文件层级结构 hal.h (统一入口) hal_gpio.h (模块接口) hal_uart.h (模块接口) hal_gpio_port.h (移植层) hal_uart_port.h (移植层) hal_core.h (芯片抽象) 组织原则 hal.h 只 include 模块接口头文件,不暴露移植层细节 用户只需 #include "hal.h",无需关心底层实现

3.4.2 具体规范

  1. 统一入口:每个模块提供一个 hal_模块.h,所有公共接口都在这里声明
  2. 移植隔离:芯片相关定义放在 hal_模块_port.h,只被移植层包含
  3. 禁止循环 include:用前置声明代替 include,实在不行再加 #ifndef 保护
  4. 头文件模板:每个头文件必须包含以下结构:
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

/* 头文件保护 */
#ifdef __cplusplus
extern "C" {
#endif

/* 依赖的头文件 */
#include "hal_common.h"

/* 宏定义 */
#define HAL_GPIO_PIN_MAX  16

/* 枚举类型 */
typedef enum {
    HAL_GPIO_DIR_INPUT,
    HAL_GPIO_DIR_OUTPUT
} HAL_GPIO_Direction_t;

/* 函数声明 */
HAL_Status_t HAL_GPIO_Init(HAL_GPIO_Handle_t* io_handle);
HAL_Status_t HAL_GPIO_SetPinLevel(uint8_t in_pin, uint8_t in_level);

#ifdef __cplusplus
}
#endif

#endif /* HAL_GPIO_H */
我曾经踩过的坑:有个项目把结构体定义放在 .c 文件里,结果其他模块想用这个结构体,只能自己再声明一遍。类型不一致,链接时出了诡异错误。从那以后,我规定所有公共类型必须放在头文件里,私有实现细节才放 .c。

3.5 规范落地检查清单

最后,我分享一个自用的检查清单。每次写完接口,我都会过一遍:

检查项 标准 自检结果
函数名 HAL_模块_动作,5 词以内 ☐ 通过
宏定义 全大写,模块前缀 ☐ 通过
枚举 HAL_模块_类型_取值,带 _t 后缀 ☐ 通过
参数方向 in_/out_/io_ 前缀 ☐ 通过
返回值 统一 HAL_Status_t,使用标准错误码 ☐ 通过
头文件 有保护宏、有 extern "C"、无循环 include ☐ 通过

这套规范我用了快十年,从单片机到应用处理器都验证过。你刚开始可能会觉得繁琐,但坚持下来,你会发现——接口规范不是束缚,而是你代码的「防弹衣」。