3、接口定义规范:HAL接口命名规则、参数传递约定、返回值与错误码定义标准
好,咱们接着聊。上一节我们把HAL的分层结构搭起来了,这一节要解决一个更实际的问题——接口到底该怎么定?
说实话,我早年做POS机的时候,吃过不少接口混乱的亏。有一次项目赶进度,三个工程师各写各的驱动,结果A写的SPI接口叫spi_transfer,B写的叫spi_send_recv,C写的叫SPI_ReadWrite。最后集成的时候,光改接口名就花了两天。你说冤不冤?
所以后来我定了一条死规矩:HAL接口必须统一命名规则。谁不遵守,谁请全组喝咖啡。
3.1 命名规则:一看就懂,一写就对
我个人习惯用这种格式:
HAL_[模块名]_[操作名]
比如GPIO模块:
HAL_GPIO_InitHAL_GPIO_WritePinHAL_GPIO_ReadPinHAL_GPIO_TogglePin
UART模块:
HAL_UART_InitHAL_UART_TransmitHAL_UART_ReceiveHAL_UART_DeInit
这里有几个要点:
- 模块名全大写:GPIO、UART、I2C、SPI,别写成Gpio、Uart这种半吊子
- 操作名首字母大写:Init、WritePin、ReadPin,驼峰式
- 动词在前:WritePin而不是PinWrite,这样更符合「做什么」的直觉
- 避免缩写歧义:比如别用
HAL_GPIO_Cfg,用HAL_GPIO_Config全称
核心原则:接口名应该让一个没看过文档的工程师,也能猜出八九分功能。
3.2 参数传递约定:别让调用者猜
参数怎么传?我踩过的坑太多了。简单说几条铁律:
第一,结构体传参优于散列参数。
你想想看,如果一个初始化函数有七八个参数,调用的时候谁记得住顺序?
// 不推荐
HAL_UART_Init(1, 115200, 8, 1, 0, 0, 0);
// 推荐
UART_InitTypeDef uart_config;
uart_config.Instance = USART1;
uart_config.BaudRate = 115200;
uart_config.WordLength = UART_WORDLENGTH_8B;
uart_config.StopBits = UART_STOPBITS_1;
uart_config.Parity = UART_PARITY_NONE;
HAL_UART_Init(&uart_config);
我在项目中遇到过,有人把波特率和数据位传反了,结果串口怎么调都不通。用结构体传参,这种低级错误基本就杜绝了。
第二,指针参数要明确方向。
用const修饰输入参数,不加const的是输出参数。这是C语言的基本功,但很多人不重视。
// 输入参数用 const
HAL_StatusTypeDef HAL_UART_Transmit(const uint8_t *pData, uint16_t Size);
// 输出参数不加 const
HAL_StatusTypeDef HAL_UART_Receive(uint8_t *pData, uint16_t Size);
第三,超时参数统一用毫秒。
别这个接口用毫秒,那个接口用微秒,另一个用TickCount。统一用毫秒,0表示不等待,0xFFFFFFFF表示无限等待。
小技巧:我习惯在参数命名上加前缀——p表示指针,u表示无符号,s表示有符号。比如pData、uTimeout。虽然老派,但确实管用。
3.3 返回值与错误码:别让调用者猜第二遍
返回值怎么定?我见过最离谱的做法是:成功返回0,失败返回-1。然后调用者只知道「出错了」,但不知道错在哪。
我的做法是统一用枚举类型:
typedef enum {
HAL_OK = 0x00, // 成功
HAL_ERROR = 0x01, // 通用错误
HAL_BUSY = 0x02, // 资源忙
HAL_TIMEOUT = 0x03, // 超时
HAL_INVALID = 0x04 // 参数无效
} HAL_StatusTypeDef;
每个HAL函数都返回这个类型。调用者拿到返回值,至少知道下一步该怎么做:
HAL_OK:继续往下走HAL_BUSY:等一下再试HAL_TIMEOUT:重试或报错HAL_INVALID:检查参数
注意:千万不要把错误码定义成宏,比如#define SUCCESS 0。宏没有类型检查,万一有人定义了#define SUCCESS 1,整个项目就乱套了。
我曾经在一个项目里,看到有人把错误码定义成:
#define RET_OK 0
#define RET_FAIL -1
#define RET_TIMEOUT -2
结果后来加了一个#define RET_BUSY -3,但有个模块的RET_FAIL被改成了#define RET_FAIL -3。排查了整整一天,最后发现是宏定义冲突。用枚举,这种事就不会发生。
3.4 一个完整的接口示例
说了这么多,咱们看一个完整的I2C接口定义:
// I2C初始化结构体
typedef struct {
uint32_t ClockSpeed; // 时钟频率,单位Hz
uint16_t Address; // 设备地址
uint8_t AddressingMode; // 7位或10位地址
} I2C_InitTypeDef;
// HAL接口
HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c, const I2C_InitTypeDef *init);
HAL_StatusTypeDef HAL_I2C_DeInit(I2C_HandleTypeDef *hi2c);
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
const uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
HAL_StatusTypeDef HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint32_t Trials,
uint32_t Timeout);
你看,每个接口都遵循了前面说的规则:命名统一、结构体传参、const修饰输入、返回值用枚举。调用者拿到这个头文件,基本不需要看文档就能用。
总结一下:接口定义规范不是花架子,它是团队协作的基石。命名规则让接口可读,参数约定让调用安全,错误码让调试高效。这三样做好了,HAL层就成功了一半。
嗯,这一节就到这。下一节我们聊聊HAL层的初始化流程,那里面也有不少门道。