【RTOS训练营】设备子系统、晚课学员提问

一:设备子系统

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])) 分子和分母的数值分别是多少啊?

答: 整个数组的大小 / 单个数组项的大小。