“一点五编程”–以函数指针override重写 以结构体作为传参 实现多态
将应用逻辑层和硬件层通过构建接口层来实现隔离 使代码不拘于硬件、平台,只面向功能,以获得代码的复用性、移植性 且增加了代码的易读性
io_led.light_i.open(struct led* self,int flags) 由于self结构体内参数不同 实现多态效果
面向对象
将任务经过面向对象进行抽象,比如将led灯、pwm灯、rgb灯抽象为 “灯”
在"灯"中有着统一的硬件层接口 如开灯open 关灯close.应用逻辑层上 无论是点亮led灯还是pwm灯 统一调用"灯"接口中的open函数
open函数会根据传参的不同来override重写接口函数 得到C++中多态的效果
函数指针
typedef void (*p)(int args);
typedef int (*painter_draw_point_fn_t)(void *self, struct point pos, int color);
通过typedef定义一个函数指针,该函数指针指向的是某一个抽象出来的功能,如open开灯 close关灯
结构体
struct p_i
{
fn_t
}
struct painter_i {
painter_draw_point_fn_t draw_point;
};
自顶向下 丢掉嵌入式开发自底向上的刻板思维 或者理解为当前功能已经实现,只需设计应用逻辑
先设计应用层 一路往下设计 直到驱动层
比如我们项目要求是led闪烁 pwm灯随着每一次led闪烁渐明渐暗以实现呼吸灯
可以先写出应用层逻辑
io_led_open();
pwm_led_open();
delay(100);
io_led_close();
pwm_led_close();
delay(100);
上面即为应用层逻辑代码 以往的嵌入式开发思维 会认为这是两个不同的模块 其根据是通过硬件功能实现决定的
抛弃刻板思维 通过功能属性来进行模块化编程! 将io_led和pwm_led根据底层功能划分为同一个模块–“灯”
那么如何设计"灯"的接口呢 秘诀在于–函数指针和结构体
通过函数指针定义接口函数
在灯的设计中 要用到的功能有开灯(led_open) 关灯(led_close)
所以typedef函数指针
typedef void (*led_open_fn_t)(void *self, struct point pos, int color);
模块化编程
传统的嵌入式开发流程:只对功能划分 如led灯 pwm灯 oled显示 超声波模块 蓝牙模块等进行分文件分模块的设计
一点五编程:不仅对功能进行划分 同时对于具有相同抽象属性的功能进行接口设计
在应用层设计中没有iic等硬件接口的概念 只关注对象接口例如灯、传感器、显示器
如灯类功能(核心操作为开灯、关灯) 显示类功能(核心功能为打点函数)
虽然io_led和pwm_led实际驱动的方式不同 但可以通过设计相同的软件接口 通过override重写接口函数来适配硬件接口
以实现软件层面的设计统一 代码更能体现功能的特性 以及更好的可读性和修改性
通过以功能属性抽象划分的接口设计 在软件代码逻辑不变 硬件改变的情况下 只需要修改接口函数中的硬件接口实现即可
如果在当前代码基础上增加一些具有相同功能 但硬件接口的不同的模块时 比如iic_led通过iic来发送数据以控制led亮灭的功能 也能快速适配到代码
流程
接口实现(灯为例)
typedef int (*light_open_fn_t)(void *self);
typedef int (*light_close_fn_t)(void *self);
struct light_i {
light_open_fn_t light_open;
light_close_fn_t light_close;
};
将硬件进行抽象处理,提取需要操作的接口
比如led抽象为灯,对于灯只需要开启和关闭两种操作
还有无刷电机、伺服电机等,虽然控制方法不一致,但是都可以抽象为电机,也是开启和关闭两个操作
然后根据实际电机的操控方式去重写接口函数即可
对象实现(led为例)
创建led.c/.h
1、在led.h中 创建对象结构体
struct io_led
{
struct light* interface;
const char* name;
int status;
};
第一个属性为接口interface,其余属性任意设定
2、在led.c中
创建实际的接口结构体
int io_led_open(struct io_led *self);
int io_led_close(struct io_led *self);
struct light_i io_led_interface =
{
.light_open = (light_open_fn_t) io_led_open,
.light_close = (light_close_fn_t) io_led_close
};
};
编写时 只有最后的io_led_open、io_led_close视情况自己定,其余内容均和接口文件内的结构体一致,可直接复制
其格式为如下
struct 接口结构体类型 结构体名称 = {
.接口函数 = (接口函数强制转换)实际函数,
.接口函数 = (接口函数强制转换)实际函数,
};
需要注意的点
1 逗号隔开
2 强制转换
3、实际的接口函数
int io_led_open(struct io_led *self);
int io_led_close(struct io_led *self);
int io_led_open(struct io_led *self) {
// TODO: Implement open logic for the LED
self->status = 1;
printf("openLED: %s status = %d\n",self->name,self->status);
return 0;
}
int io_led_close(struct io_led *self) {
// TODO: Implement close logic for the LED
self->status = 0;
printf("closeLED: %s status = %d\n",self->name,self->status);
return 0;
}
传入的参数是定义的实际对象结构体 通过结构体传入参数 来override接口函数
4、初始化函数
int io_led_init(struct io_led *self, const char *name) {
self->interface = &io_led_interface;
self->status = 0;
self->name = name;
// Initialize LED hardware (e.g., GPIO setup)
return 0;
}
这里并未按照一点五编程中进行封装(*p)->f(p)
初始化函数没有特定格式 自己随意编写就行
4、main初始化及调用
struct io_led light1;
io_led_init(&light1, "light1");
在main函数中创建一个实例 然后调用初始化函数进行初始化
printf("test light1\n");
light1.interface->light_open(&light1);
light1.interface->light_close(&light1);
实际调用如上
为什么第一个字段必须是接口结构体
当你写下 p_light2 = (struct light_i **)light2;
时,你将 light2
(一个 struct io_led *
类型的指针)强制转换为 struct light_i **
类型的指针。这种强制转换要求 struct io_led
结构体的起始部分必须与 struct light_i
的内存布局一致,以确保通过 p_light2
访问接口时的行为是正确的。
理解结构体指针转换
在 C 语言中,结构体的内存布局是顺序的,即结构体成员在内存中是按声明顺序存储的。当你把 struct io_led *
转换为 struct light_i **
时,编译器认为 light2
所指向的内存开始部分是一个 struct light_i
类型的对象。
例子
假设你有如下两个结构体:
struct light_i {
void (*light_open)(struct light_i *self);
void (*light_close)(struct light_i *self);
};
struct io_led {
struct light_i interface;
int status;
const char *name;
};
在这个例子中:
struct io_led
的第一个成员是struct light_i interface;
,所以struct io_led
的起始部分和struct light_i
的内存布局是一致的。- 当你将
light2
强制转换为struct light_i **
时,编译器认为light2
所指向的内存地址包含一个struct light_i
对象,你可以通过p_light2
来访问这些函数指针。
为什么起始部分需要是 struct light_i
-
内存布局的一致性:如果
struct io_led
的第一个成员不是struct light_i
,那么将light2
转换为struct light_i **
后,访问(*p_light2)->light_open
会读取错误的内存区域,导致未定义行为。 -
指针解引用的正确性:当你通过
p_light2
解引用指针访问light_open
或light_close
函数时,C 语言会根据struct light_i
的布局解读light2
的内容。如果light2
的起始部分与struct light_i
不匹配,函数指针可能会指向错误的地址,导致程序崩溃或行为异常。
简单示例
考虑以下内存布局:
// 定义两个结构体
struct light_i {
void (*light_open)(struct light_i *self);
void (*light_close)(struct light_i *self);
};
struct io_led {
struct light_i interface; // 必须位于结构体的开始位置
int status;
const char *name;
};
// 创建一个 io_led 实例
struct io_led light;
// 假设 light2 是指向这个 io_led 实例的指针
struct io_led *light2 = &light;
// 强制转换为 light_i ** 类型
struct light_i **p_light2 = (struct light_i **)light2;
// 现在 p_light2 指向的区域可以安全地被解释为 struct light_i
(*p_light2)->light_open(*p_light2);
如果 struct io_led
的起始部分是 struct light_i
,那么 p_light2
可以被安全地用来访问 light_open
和 light_close
函数指针。
总结
- 当你把
struct io_led *
转换为struct light_i **
时,你是在告诉编译器,从light2
指向的内存起始位置开始,有一个struct light_i
类型的对象。 - 如果
struct io_led
的第一个成员确实是struct light_i interface;
,那么这种转换是安全的,因为内存布局保证了这一点。 - 如果
struct io_led
的起始部分不是struct light_i
,这种转换将导致访问错误内存区域,从而引发未定义行为。
通过这种设计,你可以使用通用的接口(struct light_i
)来操作不同类型的灯对象(struct io_led
),从而实现多态性和接口封装。