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接口规范的四个维度串起来了:
3.6 避坑指南
最后分享几个我踩过的坑:
- 回调函数里做延时:我曾经在SPI发送完成回调里加了
HAL_Delay(10),结果中断响应变慢,系统看门狗复位。记住:回调里只设标志,主循环里处理。 - 错误码不检查:有个同事调用
hal_uart_transmit后不检查返回值,结果参数传错了,数据没发出去,查了两天。后来我强制要求:每个HAL函数调用必须检查返回值。 - 结构体传值:前面说过了,栈溢出。用指针,用指针,用指针——重要的事情说三遍。