12 框架整合:应用、驱动与平台的分层

C 语言面向对象系列

  1. 总纲:C 语言面向对象设计概览
  2. 封装:结构体与多实例接口
  3. 信息隐藏:公共接口与内部实现
  4. 模块组织:命名约定与对象生命周期
  5. 数据组织:实例状态、模块状态与常量
  6. HAL 实例:STM32 外设接口中的封装设计
  7. 结构体嵌套:公共数据与行为的复用
  8. 函数指针:可替换行为与回调
  9. 操作表:使用 ops 组织对象接口
  10. 多态:统一接口、行为分发与类型转换
  11. 接口约定:必选操作、可选操作与默认实现
  12. 框架整合:应用、驱动与平台的分层(当前篇)
  13. 工程扩展:自动注册与 Linux 内核中的对象组织

前面分别介绍了封装、结构体嵌套、函数指针和操作表。本节将它们放进一个 LED 框架,让应用通过统一接口使用设备,由具体驱动处理硬件差异。

下面给出各层的主要代码片段,部分初始化、释放函数和错误处理没有展开。

从应用中的直接硬件调用开始

如果应用直接操作 GPIO:

1
2
3
4
5
6
void app_run(void)
{
gpio_write(5, true);
delay_ms(100);
gpio_write(5, false);
}

LED 改用 PWM 或 I2C 灯带后,应用中的硬件调用就需要跟着修改。可以把点亮、关闭等操作与具体硬件实现分开,让应用只表达需要执行的动作。

确定各部分的职责

运行时的设备调用主要经过四层:

层次 职责
应用层 根据业务需要调用 LED 操作
抽象接口层 提供 led_on/off/set_brightness 等统一接口
具体驱动层 分别实现 GPIO、PWM、I2C 等设备操作
平台层(Platform) 对接芯片或系统提供的底层功能

板级代码(Board)负责创建当前板子上的设备、完成初始化,并将设备提供给应用。它与运行时的调用分层配合,集中说明当前工程使用哪些具体实现。

抽象接口只保留公共数据和操作

基础类型保存各类 LED 共用的状态,操作表声明需要支持的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct LedBase LedBase_t;

typedef struct {
int (*on)(LedBase_t *me);
int (*off)(LedBase_t *me);
int (*set_brightness)(LedBase_t *me, uint8_t brightness);
int (*deinit)(LedBase_t *me);
} LedOps_t;

struct LedBase {
const LedOps_t *ops;
uint8_t brightness;
bool is_on;
bool initialized;
};

int led_on(LedBase_t *me);
int led_off(LedBase_t *me);
int led_set_brightness(LedBase_t *me, uint8_t brightness);
int led_deinit(LedBase_t *me);

与前面的简化示例相比,这里的 LedBase_t 不再保存 pin。引脚和 PWM 通道分别放到具体类型中,应用只接触公共接口和 LedBase_t *。

GPIO 驱动负责引脚操作

GPIO LED 在基础成员之外保存引脚号,通过平台接口输出电平:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
typedef struct {
LedBase_t base;
uint8_t pin;
} GpioLed_t;

static int gpio_led_on(LedBase_t *base)
{
GpioLed_t *me = container_of(base, GpioLed_t, base);

me->base.is_on = true;
platform_gpio_write(me->pin, true);

return 0;
}

static int gpio_led_off(LedBase_t *base)
{
GpioLed_t *me = container_of(base, GpioLed_t, base);

me->base.is_on = false;
platform_gpio_write(me->pin, false);

return 0;
}

static const LedOps_t gpio_led_ops = {
.on = gpio_led_on,
.off = gpio_led_off,
.set_brightness = NULL,
.deinit = NULL,
};

int gpio_led_init(GpioLed_t *me, uint8_t pin)
{
if (me == NULL) {
return -1;
}

me->base.ops = &gpio_led_ops;
me->base.brightness = 100;
me->base.is_on = false;
me->base.initialized = true;
me->pin = pin;

platform_gpio_init(pin);
platform_gpio_write(pin, false);

return 0;
}

初始化时绑定 gpio_led_ops,记录引脚和初始状态,并配置 GPIO。点亮或关闭时,具体函数通过 container_of 取得 GpioLed_t,再访问引脚成员。

这里没有提供 set_brightness 和 deinit。统一入口需要按第 11 篇的约定处理这些空指针;如果工程要求释放 GPIO 资源,还需要提供相应的释放实现。

PWM 驱动负责通道和占空比

PWM LED 使用同一基础类型,具体成员保存 PWM 通道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef struct {
LedBase_t base;
uint8_t channel;
} PwmLed_t;

static int pwm_led_on(LedBase_t *base)
{
PwmLed_t *me = container_of(base, PwmLed_t, base);

me->base.is_on = true;
platform_pwm_set_duty(me->channel, me->base.brightness);

return 0;
}

static int pwm_led_set_brightness(LedBase_t *base, uint8_t brightness)
{
PwmLed_t *me = container_of(base, PwmLed_t, base);

me->base.brightness = brightness;

if (me->base.is_on) {
platform_pwm_set_duty(me->channel, brightness);
}

return 0;
}

点亮时,驱动将保存的亮度写入 PWM 占空比。设置亮度时,先更新对象数据;只有 is_on 为真时,才立即更新输出。

这里展示了点亮和亮度设置,完整驱动还需要配套的初始化、关闭操作及操作表。应用无需直接访问通道号。

Board 代码集中创建和初始化设备

板级代码保存当前板子上的状态灯和背光灯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct {
GpioLed_t status_led;
PwmLed_t backlight_led;
} Board_t;

static Board_t g_board;

int board_init(void)
{
platform_init();

gpio_led_init(&g_board.status_led, 5);
pwm_led_init(&g_board.backlight_led, 1);

return 0;
}

LedBase_t *board_get_status_led(void)
{
return &g_board.status_led.base;
}

LedBase_t *board_get_backlight_led(void)
{
return &g_board.backlight_led.base;
}

board_init() 先初始化平台,再初始化具体设备。应用通过 board_get_xxx() 取得基础指针,状态灯或背光灯具体使用哪种硬件由这里决定。

板级代码同时承担设备配置和接口提供的职责。示例省略了初始化返回值的处理,实际使用时需要将失败结果传给调用方。

应用通过设备用途取得接口

应用按“状态灯”“背光灯”取得对象,再执行所需操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
void app_run(void)
{
LedBase_t *status = board_get_status_led();
LedBase_t *backlight = board_get_backlight_led();

led_on(status);
led_set_brightness(backlight, 80);

delay_ms(100);

led_off(status);
led_set_brightness(backlight, 20);
}

其中,状态灯执行开启和关闭,背光灯更新亮度值。按照前面的 PWM 实现,背光灯只有已经开启时才会立即改变输出;如果尚未开启,需要先调用 led_on(backlight)。

应用不再直接写入 GPIO 或 PWM 参数。当替换后的设备能满足相同接口和行为要求时,可以保持这段应用逻辑,调整具体驱动和板级配置。

Platform 接口对接具体芯片

驱动需要的底层操作由平台接口提供:

1
2
3
4
5
int platform_gpio_init(uint8_t pin);
int platform_gpio_write(uint8_t pin, bool value);
int platform_pwm_init(uint8_t channel);
int platform_pwm_set_duty(uint8_t channel, uint8_t duty);
void platform_delay_ms(uint32_t ms);

在单片机上,这些函数可以调用芯片厂商 HAL;在 PC 仿真中,可以用 printf("GPIO %u HIGH\n", pin) 等方式记录操作。驱动通过 platform_xxx() 使用底层功能,移植时集中调整平台实现。

这对换芯片、板卡复用和 PC 仿真都有帮助。延时等平台相关操作,也应按同样方式统一组织。

在接口稳定的前提下减少适配

假设有 N 个应用场景、M 种硬件实现。如果每个应用分别适配每种硬件,就可能需要处理接近 N×M 组适配关系。使用统一接口后,N 个应用对接接口,M 种硬件实现接口,可以将这种关系粗略表示为 N+M。

这里比较的是接口适配关系,不是程序运行的时间复杂度,也不是实际代码量的测量结果。前提是共同接口能够覆盖这些应用需要的能力。

组织方式 新增硬件时的修改位置
应用直接调用硬件 可能需要修改多个应用
应用通过抽象接口调用 主要增加驱动实现,并调整板级创建和绑定

分层的数量应由项目需要决定。简单项目可以先明确模块接口和初始化流程;出现多种硬件实现、移植或仿真需求时,再引入相应的分层。