12 框架整合:应用、驱动与平台的分层
C 语言面向对象系列
总纲:C 语言面向对象设计概览
封装:结构体与多实例接口
信息隐藏:公共接口与内部实现
模块组织:命名约定与对象生命周期
数据组织:实例状态、模块状态与常量
HAL 实例:STM32 外设接口中的封装设计
结构体嵌套:公共数据与行为的复用
函数指针:可替换行为与回调
操作表:使用 ops 组织对象接口
多态:统一接口、行为分发与类型转换
接口约定:必选操作、可选操作与默认实现
框架整合:应用、驱动与平台的分层 (当前篇)
工程扩展:自动注册与 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 参数。当替换后的设备能满足相同接口和行为要求时,可以保持这段应用逻辑,调整具体驱动和板级配置。
驱动需要的底层操作由平台接口提供:
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。
这里比较的是接口适配关系,不是程序运行的时间复杂度,也不是实际代码量的测量结果。前提是共同接口能够覆盖这些应用需要的能力。
组织方式
新增硬件时的修改位置
应用直接调用硬件
可能需要修改多个应用
应用通过抽象接口调用
主要增加驱动实现,并调整板级创建和绑定
分层的数量应由项目需要决定。简单项目可以先明确模块接口和初始化流程;出现多种硬件实现、移植或仿真需求时,再引入相应的分层。