接口设计原则:高内聚低耦合、接口稳定性、可移植性、可测试性

各位同学,咱们今天聊点实在的。接口设计这件事,说白了就是定规矩。你想想看,一个RTOS里少说几十个驱动,多则上百个模块,如果没有一套统一的接口规范,那代码写起来就是各玩各的,最后集成的时候哭都来不及。

我个人习惯,在设计HAL接口之前,先把四个核心原则刻在脑子里。这四个原则就像房子的四根承重柱,缺一根都不行。

2.1 高内聚低耦合

什么叫高内聚?就是一个模块只干一件事,而且把这件事干到极致。低耦合呢?模块之间尽量少说话,说话也只通过定义好的接口。

我在项目中遇到过这样一个坑:有个同事把GPIO控制和定时器配置写在同一个函数里。当时看着挺方便,后来要换MCU平台,好家伙,改一个功能牵扯出一堆依赖。这就是典型的耦合太高。

核心原则: 每个HAL接口只封装一类硬件操作。比如GPIO的接口只管GPIO,UART的接口只管UART,别混在一起。

举个例子,一个高内聚的GPIO接口应该长这样:

// 高内聚:只做GPIO相关操作
hal_status_t hal_gpio_init(gpio_t *gpio, gpio_config_t *config);
hal_status_t hal_gpio_set_pin(gpio_t *gpio, uint8_t pin, gpio_level_t level);
hal_status_t hal_gpio_get_pin(gpio_t *gpio, uint8_t pin, gpio_level_t *level);
hal_status_t hal_gpio_deinit(gpio_t *gpio);

你看,每个函数都只做一件事。初始化、设置电平、读取电平、反初始化。清清楚楚,明明白白。

低耦合怎么体现?接口参数里不要传其他模块的句柄。比如GPIO接口里出现timer_handle_t,那就是耦合的信号。

避坑指南: 我曾经见过一个接口函数有8个参数,其中3个是其他模块的指针。这种设计,改一个模块就得改所有调用方。记住:接口参数越少,耦合越低。

2.2 接口稳定性

接口一旦发布,就不要轻易改。为什么?因为你的接口会被多个任务、多个驱动调用。改一个接口名或者参数顺序,可能引发连锁反应。

我有个习惯:接口设计阶段多花点时间想清楚,后面就能少挨骂。稳定性不是说不让改,而是改的时候要有策略。

变更类型 影响范围 处理方式
新增接口 无影响 直接添加,保留旧接口
修改参数 所有调用方 废弃旧接口,提供新接口
删除接口 所有调用方 标记为废弃,至少保留一个版本
修改返回值 所有调用方 必须同步更新,建议用枚举扩展

你看这个表,新增接口是最安全的。所以我建议,设计初期宁可多定义几个接口,也别想着"先凑合用,后面再改"。

小技巧: 用版本宏来管理接口兼容性。比如 #define HAL_GPIO_API_VERSION 2,这样上层代码可以根据版本号做适配。

2.3 可移植性

可移植性,说白了就是换芯片不换接口。你从STM32换到GD32,甚至换到NXP,上层应用代码一行都不改,这才叫可移植。

怎么做到?把硬件相关的部分全部藏在HAL层下面。上层只跟HAL接口打交道,不直接操作寄存器。

// 不可移植的写法:直接操作寄存器
#define GPIOA_BASE 0x40020000
*(volatile uint32_t *)(GPIOA_BASE + 0x14) |= (1 << 5);

// 可移植的写法:通过HAL接口
hal_gpio_set_pin(&gpio_led, 5, GPIO_LEVEL_HIGH);

嗯,这里要注意:可移植性不是免费的午餐。它需要你在设计阶段就做好抽象。我一般会先画一个硬件抽象层的分层图,把每层的职责定死。

HAL接口分层结构 应用层 (Application) RTOS内核 (任务调度/同步) HAL接口层 (稳定API) 硬件驱动层 (寄存器操作) 换芯片只需重写最底层驱动,HAL接口和上层代码完全不动

你看这个分层,每一层只跟相邻层通信。换芯片的时候,只需要重写最下面那层硬件驱动,上面的HAL接口、RTOS内核、应用代码,一行都不用动。

避坑指南: 我曾经为了省事,在HAL接口里直接暴露了寄存器地址。结果换芯片的时候,所有调用方都得改。记住:HAL接口里永远不要出现硬件相关的宏定义或地址。

2.4 可测试性

可测试性,很多人容易忽略。你想想看,一个接口如果没法单独测试,你怎么保证它没问题?

我建议,每个HAL接口都要能独立测试。怎么做?两个关键点:

  1. 接口参数可模拟:传入的参数能构造各种边界情况
  2. 返回值可验证:每个接口都有明确的返回值,方便断言检查

举个例子,一个可测试的UART发送接口:

// 可测试的接口设计
hal_status_t hal_uart_send(uart_t *uart, const uint8_t *data, uint32_t len, uint32_t timeout);

// 测试用例示例
void test_hal_uart_send_normal(void) {
    uart_t uart;
    uint8_t test_data[] = "Hello HAL";
    hal_status_t ret;
    
    ret = hal_uart_send(&uart, test_data, sizeof(test_data), 100);
    assert(ret == HAL_OK);  // 正常情况应该返回OK
}

void test_hal_uart_send_null_ptr(void) {
    hal_status_t ret;
    
    ret = hal_uart_send(NULL, NULL, 0, 100);
    assert(ret == HAL_INVALID_PARAM);  // 空指针应该返回错误
}

你看,每个测试用例只测一个场景。正常情况、异常情况、边界情况,全覆盖。

小技巧: 设计接口时,返回值一定要用枚举,不要用0和1这种魔法数。枚举可以扩展,而且可读性强。我一般这样定义:
typedef enum {
    HAL_OK = 0,
    HAL_ERROR,
    HAL_BUSY,
    HAL_TIMEOUT,
    HAL_INVALID_PARAM
} hal_status_t;

可测试性还有一个好处:方便做回归测试。每次改完底层驱动,跑一遍测试用例,马上知道有没有破坏原有功能。

2.5 四个原则的关系

这四个原则不是孤立的。它们互相支撑:

  • 高内聚低耦合 是基础,让接口职责清晰
  • 接口稳定性 是保障,让接口值得信赖
  • 可移植性 是目标,让接口跨平台可用
  • 可测试性 是手段,让接口质量可控

我做了这么多年嵌入式,发现一个规律:凡是接口设计做得好的项目,后期维护成本至少降低一半。反过来,接口设计随便糊弄的项目,后面改bug改到怀疑人生。

嗯,这一章的内容就到这里。记住这四个原则,后面咱们写接口的时候,我会反复拿它们来检验。