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 头文件层级
3.4.2 具体规范
- 统一入口:每个模块提供一个
hal_模块.h,所有公共接口都在这里声明 - 移植隔离:芯片相关定义放在
hal_模块_port.h,只被移植层包含 - 禁止循环 include:用前置声明代替 include,实在不行再加
#ifndef保护 - 头文件模板:每个头文件必须包含以下结构:
#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 | ☐ 通过 |
这套规范我用了快十年,从单片机到应用处理器都验证过。你刚开始可能会觉得繁琐,但坚持下来,你会发现——接口规范不是束缚,而是你代码的「防弹衣」。