4. HAL接口规范定义:数据类型定义、函数命名规范、错误码定义、初始化与去初始化接口
好,咱们进入正题。这一章要聊的是HAL接口的规范定义。说白了,就是给起搏器的底层硬件抽象层定规矩——数据类型怎么起名、函数怎么命名、错误码怎么定义、初始化与去初始化接口长什么样。
我个人习惯,做嵌入式系统尤其是医疗设备,第一件事就是把规矩定死。规矩定好了,后面移植、维护、团队协作才不会乱。你想想看,一个起搏器项目,代码量动辄几十万行,要是命名风格五花八门,数据类型定义随心所欲,那调试起来简直要命。
4.1 数据类型定义
先聊数据类型。嵌入式系统里,数据类型定义直接关系到可移植性和可读性。我见过不少项目,直接用int、long、unsigned char,结果换一个编译器就出问题。为什么?因为不同平台下这些基础类型的长度可能不一样。
所以,我的做法是:统一使用自定义的、长度明确的数据类型。这样不管移植到ARM Cortex-M还是RISC-V,代码都不用改。
下面是我在起搏器HAL中常用的数据类型定义:
/* 数据类型定义 - 明确长度,跨平台一致 */
typedef unsigned char uint8_t; /* 8位无符号整数 */
typedef signed char int8_t; /* 8位有符号整数 */
typedef unsigned short uint16_t; /* 16位无符号整数 */
typedef signed short int16_t; /* 16位有符号整数 */
typedef unsigned long uint32_t; /* 32位无符号整数 */
typedef signed long int32_t; /* 32位有符号整数 */
typedef float float32_t; /* 32位浮点数 */
typedef double float64_t; /* 64位浮点数 */
/* 布尔类型 - 起搏器里常用 */
typedef uint8_t bool_t;
#define TRUE (1)
#define FALSE (0)
/* 状态类型 - 用于函数返回值 */
typedef uint8_t status_t;
这里有个细节:bool_t我用了uint8_t而不是int。为什么?因为有些编译器对int的处理不一样,有的16位,有的32位。用uint8_t就一个字——稳。
另外,status_t这个类型,我建议统一用uint8_t。起搏器的错误码不会太多,8位足够用了。省空间,也省心。
pacer_types.h。这样所有模块都引用同一个定义,改一处就全改。我曾经在一个项目里,因为数据类型定义分散在多个文件,结果改了一个没改全,排查了整整两天。
4.2 函数命名规范
函数命名,说白了就是给函数起个好名字。好名字的标准是什么?一看就知道这个函数是干什么的。
我建议采用模块名_动作_对象的命名方式。比如:
hal_timer_init— 定时器模块的初始化hal_gpio_set— GPIO设置hal_adc_read— ADC读取
这样命名有什么好处?你想想看,当你在代码里看到hal_timer_init,你立刻就知道:这是HAL层的定时器初始化函数。不需要翻文档,不需要猜。
下面是我在起搏器项目中常用的命名规范:
| 模块 | 前缀 | 示例 |
|---|---|---|
| 定时器 | hal_timer_ | hal_timer_init, hal_timer_start |
| GPIO | hal_gpio_ | hal_gpio_set, hal_gpio_get |
| ADC | hal_adc_ | hal_adc_init, hal_adc_read |
| SPI | hal_spi_ | hal_spi_transmit, hal_spi_receive |
| 看门狗 | hal_wdt_ | hal_wdt_init, hal_wdt_feed |
这里要注意一点:函数名不要太长。我见过有人写hal_timer_configure_and_start_with_interrupt,这名字是够清楚了,但写起来累死人。我的建议是,函数名控制在20个字符以内,如果功能太复杂,拆成多个函数。
init,另一个也叫 init,结果链接的时候报错。从那以后,我坚持所有HAL函数都加上模块前缀。
4.3 错误码定义
错误码,是HAL接口的灵魂。没有错误码,你都不知道函数调用到底成功了没有。
起搏器是医疗设备,错误码的定义要格外谨慎。我的原则是:宁可多,不可少。每个可能的错误场景都要有对应的错误码。
下面是我常用的错误码定义:
/* 错误码定义 */
#define HAL_OK (0x00) /* 操作成功 */
#define HAL_ERROR (0x01) /* 通用错误 */
#define HAL_ERROR_TIMEOUT (0x02) /* 超时错误 */
#define HAL_ERROR_BUSY (0x03) /* 资源忙 */
#define HAL_ERROR_INVALID (0x04) /* 参数无效 */
#define HAL_ERROR_NULL (0x05) /* 空指针 */
#define HAL_ERROR_NOMEM (0x06) /* 内存不足 */
#define HAL_ERROR_HARDWARE (0x07) /* 硬件错误 */
#define HAL_ERROR_NOT_INIT (0x08) /* 未初始化 */
#define HAL_ERROR_ALREADY (0x09) /* 已经初始化 */
这里我特别想强调两个错误码:
HAL_ERROR_NOT_INIT— 这个太重要了。很多bug都是因为调用了未初始化的模块。有这个错误码,调试时一眼就能看出来。HAL_ERROR_ALREADY— 防止重复初始化。起搏器里,重复初始化定时器可能会导致意想不到的问题。
另外,错误码最好用枚举类型而不是宏定义。为什么?因为枚举类型在调试器里可以直接显示名字,而宏定义只能看到数字。不过,考虑到有些编译器对枚举的支持不太好,我一般还是用宏定义。嗯,这里要看具体项目需求。
4.4 初始化与去初始化接口
初始化与去初始化,是HAL接口里最基础、也最重要的两个接口。为什么?因为所有硬件模块在使用前都要初始化,使用后都要去初始化。这是规矩。
我建议的接口原型如下:
/* 初始化接口 */
status_t hal_timer_init(uint32_t freq_hz);
/* 去初始化接口 */
status_t hal_timer_deinit(void);
这里有几个设计要点:
- 初始化函数要带参数。比如定时器的初始化,需要传入频率。这样设计的好处是,上层应用可以灵活配置,不需要改HAL代码。
- 去初始化函数不带参数。为什么?因为去初始化就是恢复默认状态,不需要额外信息。而且,带参数反而容易出错。
- 两个函数都返回状态码。初始化可能失败(比如硬件故障),去初始化也可能失败(比如资源释放不干净)。返回状态码,让上层应用知道发生了什么。
下面是一个完整的初始化与去初始化示例:
/* 定时器初始化 */
status_t hal_timer_init(uint32_t freq_hz)
{
status_t status = HAL_OK;
/* 参数检查 */
if (freq_hz == 0)
{
return HAL_ERROR_INVALID;
}
/* 检查是否已经初始化 */
if (timer_initialized == TRUE)
{
return HAL_ERROR_ALREADY;
}
/* 配置硬件寄存器 */
TIMER->CR1 = 0x00; /* 先关闭定时器 */
TIMER->PSC = calculate_prescaler(freq_hz);
TIMER->ARR = calculate_autoreload(freq_hz);
TIMER->CR1 |= TIM_CR1_CEN; /* 使能定时器 */
/* 标记已初始化 */
timer_initialized = TRUE;
return HAL_OK;
}
/* 定时器去初始化 */
status_t hal_timer_deinit(void)
{
status_t status = HAL_OK;
/* 检查是否已经初始化 */
if (timer_initialized == FALSE)
{
return HAL_ERROR_NOT_INIT;
}
/* 关闭定时器 */
TIMER->CR1 = 0x00;
/* 清除中断标志 */
TIMER->SR = 0x00;
/* 标记未初始化 */
timer_initialized = FALSE;
return HAL_OK;
}
这里有个小细节:timer_initialized这个标志位。我建议每个模块都维护一个初始化标志。这样做的目的是:防止重复初始化和未初始化就操作。你想想看,如果某个模块被初始化了两次,第一次配置的寄存器可能被第二次覆盖掉,导致硬件状态混乱。
好了,这一章的内容就到这里。数据类型定义、函数命名规范、错误码定义、初始化与去初始化接口,这四个方面是HAL接口规范的核心。规矩定好了,后面的开发就会顺畅很多。
下一章,我们会聊具体的硬件模块抽象,比如GPIO、定时器、ADC等。到时候,你会看到这些规范在实际代码中是怎么用的。