一:设备子系统
1.1 抽象结构体
我们一直强调,面向对象的思想,即对某一个硬件,抽象出一个结构体。
怎么描述一个对象?
- 它有什么属性?
- 结构体成员
- 它有什么功能?
- 函数指针
因此,我们就要概括出它的属性,抽象出它的功能。
举个例子,LED有哪些属性?
我们先说简单一点,注意这个电路图,我们可以得出几个属性?
第一,它使用哪一个引脚;
第二,这引脚输出高电平还是低电平,可以让这个灯点亮;
所以我们结构体里面就可以包含这些属性,如下:
typedef struct LEDDevice {
int group;
int pin;
int active_high;
}
对于这个LED,它使用哪一个引脚?
比如103,就得确定它属于哪一组GPIO,再确定它属于这一组里面的哪一个引脚,所以我们得到了前面两项:group、pin。
active_high
什么意思呢?
就表示这个LED,是高电平有效还是低电平有效。
这就是根据硬件的属性,抽象出结构体里面的变量成员。
这个结构体还没写完,只写了一半,我们再来看看这个LED有哪些功能:
- 开、关
- 设置颜色
- 设置亮度
对于最简单的灯,可以开,可以关。有些高级的灯,还可以调整颜色,设置亮度。
所以我们有三个功能,那么这个结构体我们就可以写得比较完善了:
typedef struct LEDDevice {
int group;
int pin;
int active_high;
/* 初始化LED设备, 成功则返回0 */
int (*Init)(struct LEDDevice *ptLEDDevice);
/* 控制LED设备, iStatus取值: 1-亮,0-灭 */
int (*Control)(struct LEDDevice *ptLEDDevice, int iStatus);
/* 未实现 */
void (*SetColor)(struct LEDDevice *ptLEDDevice, int iColor);
/* 未实现 */
void (*SetBrightness)(struct LEDDevice *ptLEDDevice, int iBrightness);
}LEDDevice, *PLEDDevice;
再看看这个结构体有什么缺点,能不能够优化?
1.亮度,颜色和当前亮灭可以在结构体里定义几个变量来表示。
以便实现下次再开灯时,直接使用上一次的亮度值。
2.作为用户,我只关心他是哪个灯,不关心它使用的是哪个GPIO组的哪个引脚,因此可以改进如下:
#define LED_WHITE 0
#define LED_BLUE 1
#define LED_GREEN 2
typedef struct LEDDevice {
int which;
int brightness;
int color;
/* 初始化LED设备, 成功则返回0 */
int (*Init)(struct LEDDevice *ptLEDDevice);
/* 控制LED设备, iStatus取值: 1-亮,0-灭 */
int (*Control)(struct LEDDevice *ptLEDDevice, int iStatus);
/* 未实现 */
void (*SetColor)(struct LEDDevice *ptLEDDevice, int iColor);
/* 未实现 */
void (*SetBrightness)(struct LEDDevice *ptLEDDevice, int iBrightness);
void (*SetFrequency)(struct LEDDevice *ptLEDDevice, int freq);
}LEDDevice, *PLEDDevice;
用户只需要传入LED_BLUE
,结构体中使用which
来指定是哪一个灯。
3.这些属性是不是可以封装成一个结构体?
这些属性,因为设备本身很复杂的,当然可以再封装出一些结构体。
但是结构体本身,也增加了使用者的难度,所以这是一个要综合考虑的事情,举个例子:
在Linux里面,怎么描述一个显示器?
显示器里面有什么: 分辨率、每个像素用多少位来表示(bpp)、显存、上下左右的边沿、刷新频率等等。
它有很多参数,你当然可以把这些参数一个一个列出来。但是, Linux里面,把这参数分为了两个结构体:
-
结构体1:这个结构体描述的是LCD的、可以变化的信息。
比如说用32位来表示一个像素,那么这32位里面,红绿蓝分别在哪里?是不是又得来描述这些属性?
在这个结构体里面,又定义了一个新的结构体:
fb_bitfield
- 结构体2(
fb_bitfield
):用这个结构里来表示红绿蓝分别在哪个位置,占据这32位里面的哪些位。
回到我们的LED,如果这些属性比较简单,就没必要再新增一个结构体了。
为了简化示例,这里只保留属性which
。
1.2 实现结构体
抽象出这个结构体之后,我们就来实现这个结构体。
前面是定义结构体的类型,下面要根据具体的LED,来定义结构体变量。
static LEDDevice g_tLEDDevices[] = {
{LED_WHITE, LEDDeviceInit, LEDDeviceControl},
{LED_BLUE, LEDDeviceInit, LEDDeviceControl}
{LED_GREEN, LEDDeviceInit, LEDDeviceControl},
}
这里定了一个数组并赋值,这是一个结构体数组。
每一个数组项是一个LEDDevice结构体,用来表示一盏LED。
里面有函数指针,怎么使用?
1.3 使用结构体
我们现在,定义了三个LED设备,也都实现了里面的函数,
作为一个使用者,我怎么去使用它呢?
上面的图里面,有一个函数:GetLEDDevice
通过这个函数,去获得一个结构体,然后就可以调用结构体里面的函数来操作LED。
举个例子,在这个文件里:
获得LED,初始化LED,控制LED:
作为应用程序开发的人,他的使用就是这么简单。
二:晚课学员提问
1. 问: 课程中这些函数怎么实现才能够比较容易扩展?
答: 要扩展什么?需要先想清楚自己要做什么事情。
比如,我想让这个工程,能够支持裸机、 FreeRTOS、RT-Thread。
当他支持裸机的时候,我想让他能够支持多款芯片:ST的、其他国产芯片。
这个时候就要考虑程序的分层,以LED的硬件初始化为例:
如果把硬件的初始化代码放在这个函数里面的话,要换一种硬件的时候,就需要来改这个函数。
换一种内核的时候,也要来改这个函数,所以我们应该分层。
这个所谓的抽象层,说的很高大上,但是里面的技术一点都不高大上。
我们先来看看分层的实现:
我们想要我们的程序,支持裸机,支持各类rtos,怎么做呢?
以初始化函数为例:
我想去初始化LED,我要调用一个KAL_LEDDeviceInit
,
在这个函数里面,通过宏开关,来调用不同的内核的函数。
现在对于裸机,我们抽象出了一个函数:CAL_LEDDeviceInit
为什么不直接去调用HAL的代码?
因为有些芯片它有HAL库,有些芯片就没有HAL库。
你用ST的HAL写出了这个程序,今年ST的芯片买不到了,用了国产的芯片,没有HAL了,是不是要头大了?
所以对裸机程序,我们又可以封装出这一层:
使用这些宏开关,来决定使用哪一套代码。
下面这个图,就是我们分层的意义:
2.问: 我记得输入子系统中您并不推荐用宏开关,而是用结构体来支持不同类型,当初还举了lcd的例子。
答: 对于这个问题,什么时候使用宏开关 ?什么时候使用结构体?
问题的核心在于:是否同时支持?
对于一个编译好的程序,我们不会同时支持裸机、支持RTOS。
所以我们可以使用宏开关,来启动一部分代码,禁止另一部分代码,不占用多余Flash。
而程序中,要支持多种输入设备,要支持多种LCD,比如程序不变,换其它规格的LCD,最好是使用链表。
因此,要同时支持,就用结构体;事先就定死只支持一个,就用宏开关。
3. 问: 老师,就是设备子系统为啥不像输入子系统那样用链表?
答: 因为各类设备不容易统一,比如:
- led: 开关
- 风扇:正转、反转、风速。
把LED设备、风扇设备、显示屏,放入一个链表的话,这个链表元素就不一样了。
所以还不如:
单独管理LED设备,你可以把多个LED设备放到LED链表里去,
单独管理风扇设备,你可以把多个风扇设备放到风扇链表里去。
4. 问: 比如一个标准库的gpio初始化是传一个整形的instance和一个pin,
然后hal库的gpio初始化是要传gpio寄存器的首地址和pin,
那cal对外封装的结构体是要封装三个参数吗?整形instance,首地址,pin?
答: 不用,这里要有“翻译”,举个例子:
5. 问: 请问,这种写法 sizeof(g_tLEDDevices) / sizeof(sizeof(g_tLEDDevices[0])) 分子和分母的数值分别是多少啊?
答: 整个数组的大小 / 单个数组项的大小。