3、接口定义规范:HAL接口命名规则、参数传递约定、返回值与错误码定义标准

好,咱们接着聊。上一节我们把HAL的分层结构搭起来了,这一节要解决一个更实际的问题——接口到底该怎么定?

说实话,我早年做POS机的时候,吃过不少接口混乱的亏。有一次项目赶进度,三个工程师各写各的驱动,结果A写的SPI接口叫spi_transfer,B写的叫spi_send_recv,C写的叫SPI_ReadWrite。最后集成的时候,光改接口名就花了两天。你说冤不冤?

所以后来我定了一条死规矩:HAL接口必须统一命名规则。谁不遵守,谁请全组喝咖啡。

3.1 命名规则:一看就懂,一写就对

我个人习惯用这种格式:

HAL_[模块名]_[操作名]

比如GPIO模块:

  • HAL_GPIO_Init
  • HAL_GPIO_WritePin
  • HAL_GPIO_ReadPin
  • HAL_GPIO_TogglePin

UART模块:

  • HAL_UART_Init
  • HAL_UART_Transmit
  • HAL_UART_Receive
  • HAL_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表示有符号。比如pDatauTimeout。虽然老派,但确实管用。

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层的初始化流程,那里面也有不少门道。