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);

这里有几个设计要点:

  1. 初始化函数要带参数。比如定时器的初始化,需要传入频率。这样设计的好处是,上层应用可以灵活配置,不需要改HAL代码。
  2. 去初始化函数不带参数。为什么?因为去初始化就是恢复默认状态,不需要额外信息。而且,带参数反而容易出错。
  3. 两个函数都返回状态码。初始化可能失败(比如硬件故障),去初始化也可能失败(比如资源释放不干净)。返回状态码,让上层应用知道发生了什么。

下面是一个完整的初始化与去初始化示例:

/* 定时器初始化 */
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等。到时候,你会看到这些规范在实际代码中是怎么用的。