【RTOS训练营】程序框架、预习、课后作业和晚课提问

一:程序框架

我们使用HAL库来开发项目,如果框架设计的好的话,在rtos上面代码不需要改动太多。

程序框架可以参考这本书,我在中兴的时候基本上人手一本。

请添加图片描述

我们来看看这个产品,可以通过手机发送网络数据到开发板上,
开发板根据这些指示来点灯、转风扇。

功能比较简单,但是我们的框架可以做的有很多层次。

很多同学都是过程化的编程,今天我们要介绍的是模块化的编程。

要引入面向对象的思想,我们先来讲一下理论知识。

一个程序,怎么设计?

今天的内容需要大家互动,需要大家把工作中的经验分享出来。

在《代码大全》第5章中,把程序设计分为这几个层次:

* 第1层:软件系统,就是整个系统、整个程序
* 第2层:分解为子系统或包。比如我们可以拆分为:输入子系统、显示子系统、业务系统
* 第3层:分解为类。在C语言里没有类,可以使用结构体来描述子系统
* 第4层:分解成子程序:实现那些结构体(结构体中有函数指针)

这几句话我用一个图来表示:

最外面这一层就整个系统,在里面我们又画了两个大圆圈,就是两个子系统。

子系统里面又出现出了类或者结构体。

我们在C语言里面用结构体,在C++里面用类。

在单片机的开发中,我们只能够用C,用不了C++,所以我们来讲结构体。

第3层是结构体,以前我们讲结构体的时候,说结构里里面可以放函数指针

一个结构体里面可以有:各种变量成员、有函数指针。
我们可以使用一个结构体来表示一个设备、一个处理、一个操作。

第4层就是结构体里面的函数了。

这都是一些比较虚的概念,我们来举例说明。

只讨论开发板上的程序,这个产品我们可以拆分成几个子系统?

并没有标准答案,我来讲一下我的分法。

我把这个系统分成了6个子系统:

我是怎么得出这6个子系统的呢?我们可以一步一步来。

按照数据的流向,分为输入和输出:

至少有两个系统,对于输入部分我们又可以细分:

对于输入:用户可以点击按键,点击触摸屏。

那传感器呢?传感器检测到火灾的时候,发出报警信号,这也是输入。

甚至说我们还有远程控制,就像我们举的例子,你可以使用手机来控制开发板。

所以对于输入部分,我们还可以细分成各类子系统。

对于输出,我们也可以继续细分:

输出,并不仅仅是我们在屏幕上看到的内容。

比如说去点灯、控制这些设备,它也是一种输出。

再比如说数据的保存,也算是一种输出。

所以输出也可以拆分成很多子系统。

谁把这些输入和输出组合起来?

我们又可以抽象出另外一个子系统:业务子系统

有同学称之为:输入,输出,控制逻辑三部分,基本上就是这三大类。

还有同学从应用和驱动程序的角度来:应用层、中间层,驱动层,这比较适合用来实现某一个硬件模块。

我们以LCD为例:

对于显示这么一个功能,他可以拆分成三层。

在Linux系统中,在驱动开发,有一个原则:驱动只提供功能,不提供策略。

这句话是什么意思呢?以点灯为例,
驱动程序,它可以提供开灯关灯的功能。
什么时候开灯什么时候关灯,这叫策略,这不应该由驱动程序来决定。

回到我们上面的这个图,为什么这个显示的功能,要拆分成三层?

看看最底下,最底下是驱动程序,他应该提供硬件的功能:像素操作。

就是在xy某个坐标上,设置像素的颜色,但是怎么显示字符、显示多大、在哪显示,这不关驱动的事。

各司其职,不要越界。驱动就只做驱动的事。

中间是文字、图片的显示,通过库函数或者某些功能函数来实现,提供显示字符、显示图片的功能。

但是显示什么字符、在哪显示,这不关中间层的事。

显示一个字符的时候,就显示一个字符的点阵。
怎么得到点阵,功能函数来实现;
怎么显示像素,驱动程序来实现。

但是,显示什么字符,在哪里显示?
显示什么图片?在哪里显示

跟驱动程序没有关系,跟功能函数也没有关系。

由最上面的那一层来决定:APP。

我们去设计一个子系统的时候,也要明白:想让子系统比较通用,比较独立的话,就不要去做无关的事情。

下面我们就来讲讲怎么写代码实现各类子系统。
请添加图片描述

对这个输入子系统,在上图里我只把它拆分成两层。

但是后面随着编程的进行,我最终把它分成了5层。

所以这些程序的划分,一开始我们可能想的不够全面,但是只要记住一个原则:可移植性、减少依赖、独立

分层的事情我们等会再说,现在假设这输入子系统,就分为两层。

怎么写出来呢?

首先我们要使用面向对象的思想,抽象出一些结构体。

就比如说我们要问你一个问题:我从输入子系统里面可以得到什么?

得到:按键、触摸屏的点击,甚至说网络数据。

那么能不能用一个结构体来抽象出这些数据?

举个例子,这里抽象出了一个InputEvent:

这个名字、还有里面的大部分内容,来自于Linux,后面这个字符串是我扩充的。

首先它有个类型,可以分辨是按键、还是触摸屏,还是网络数据。

对于按键的话,有意义的成员:iKey, iPressure。

就比如说是按键a、还是按键b,是按下还是松开。

里面还有一个时间,可以记录这个按键按下或者松开的时间,就可以用来识别长按还是短按。

对于触摸屏,点击哪个触点?使用xy坐标来表示。

是点击还是松开,用iPressure来表示。

后面这个str数组是我扩充的,我们通过手机给开板发送数据时,
输入事件就是网络数据:网络数据就可以保存在这个str数组里。

我们抽象出了输入事件这么一个核心的结构。

你问我怎么知道这个结构体?我是学习的linux后,再来教大家的。

所以对于初学者,一开始的时候先模仿。

来看这框图,底层的这个按键、网络、串口,都会向上面传递InputEvent。

请添加图片描述

那么对一些不同的硬件,比如说按键、网络输入设备、串口,

以面向对象的编程思想,也应该抽象出一个结构体。

这个结构体长什么样?需要想想怎么去操作这些硬件。

首先得有初始化:比如说设置gpio为中断功能;比如说设置串口的波特率。

所以这个结构体里面肯定会有一个初始化函数。

上层的代码,可以通过这个input device来获得数据,

可能每种设备去获得数据的方法都不一样,所以这个input device里面应该提供一个:获得数据的函数。

所以这个结构体我就抽象为:

里面有名字,名字在我们的程序里面不重要。

重要的是那三个函数指针,最后还有一个链表项。

为什么要加上一个链表?因为我想把多个输入设备统一管理。

就比如说,我想去初始化的时候,我就可以从链表里面把他们一个一个的取出来,调用它的DeviceInit函数。

我们已经抽象出两个结构体了,足够了吗?

我们下面的输入设备,会不断的产生数据。

就比如说我连续不断的按下按键,就会产生很多数据。

为了不让这些数据丢失,我们还需要一个缓冲区,

于是,我又抽象出另外一个结构体:环形缓冲区。

这里面有读和写的位置,就一个input even数组。

我们已经把整个系统,拆分成了几个子系统。

对于子系统,也抽象出了结构体。

最后,就是去实现结构体里面的函数。

简单的说,就是去写.h文件和 .c文件。

二:预习安排

布置一下预习的视频和文档:

10-5  输入子系统_实现按键输入
10-5  输入子系统_实现按键输入
10-7  输入子系统_单元测试

三:课后作业

- 作业1

10_6_input_unittest 中实现了按键功能:
在按键中断函数中,构造InputEvent,放入Buffer
请参考它实现:串口输入功能。
思路:
找到串口的接收中断函数
当串口接收到回车换行时,表示得到了一个完整的数据
将数据构造为InputEvent,放入Buffer

- 作业2

请思考,怎么设计"设备子系统",比如LED、风扇、OLED,它们的操作并不相同。
怎么抽象出一个结构体,可以支持它们?
写出这个结构体。

写好的作业,想老师批改的,请放在QQ群里。

请添加图片描述

四: 晚课学员提问

1. 问: 怎么理解何为硬件模块?

答: 比如说LCD、 Flash、各类传感器,这些都是单功能的硬件模块。

2. 问: 设备子系统是属于输出吗?网络子系统和字体子系统是属于输出还是输入?

答: 对于设备,有些设备只能够输出,有些设备只能够输入,有些设备既能输出也能输入。所以一个设备子系统,有时候并不能够简单的把它划入输入、或者输出。比如U盘,你可以写入数据,可以读出数据。这个时候单纯把它划为输入或者输出都不恰当。

3. 问: 按照什么去分层?

答: 先划出子系统,在实现子系统的时候再考虑分层。比如我把系统分为输入和输出,分成两个子系统,在实现输入子系统的时候,再考虑分层。所以我们首先要练的是,怎么把整个系统拆分成多个子系统。怎么拆分成多个子系统,刚才我们已经介绍了方法:

先把它拆分成:输入、输出、控制逻辑(业务)三个子系统。

再去细分这三个子系统,得到更多、功能更加独立的子系统。

我再举一个例子:

我一开始设计这个系统的时候,并没有这个字体子系统。

后来一想,我怎么得到字符的点阵?

我可以从点阵字库里面得到,也可以从Free type字库里面得到。

去显示文字的时候,字库的来源应该独立出来。

所以我就把它分成了显示子系统,字体子系统:
字体子系统,提供字模;
显示子系统,根据字模来显示文字。

甚至有时候在编程的时候发现,这个子系统功能不大纯粹,又去拆分它。

4. 问: 比如flash保存参数,这也算输出系统,怎么抽象,编程?老师项目上能加上这一个模块吗?

答: 后面的esp32芯片上会有。

5. 问: 这是属于项目一开始就做全局规划了,实际工作中感觉还是蛮难的?

答: 先从小项目开始练。

6. 问: 数据成员都不会同一时刻使用,可以用共用体吗?union?

答: 可以用union,也推荐使用它。

7. 问: 分层的第一步是用结构体去勾画对象吗?

答: 分成的第一步,你要去理清楚功能。
就比如说输入子系统:我之所以把它拆分成两层,主要是:

  1. 最下面是数据源
  2. 上层是汇总

汇总、管理,所以我就简单的把输入子系统划分为上下两层。

划分出上下两层之后,再去考虑结构体。

8. 问: 三个不同的输入内容都揉在一起嘛,需要再分类清晰点吗,比如结构体里再包括三个结构体?

答: 不管你怎么做,你得有一个分类type。你当然可以在里面再放三个结构体,就是比较浪费空间。

9. 问: 我如果有几个端口输入数据,例如uart,spi和网络,那应该创建几个不同的buf吧?

答: 每个输入设备,都可以产生自己的InputEvent,里面有自己的buff。定义类型的时候并不需要有出多个buff。每一个设备它都可以定义自己的InputEvent。

10. 问: 老师,头文件的开头将 用到的变量、函数指针封装成一个结构体有什么好处呢?还有#pragma pack(1) 解释下?

答: 分装成结构体,就使用面向对象的编程思想,用一个结构体来实现一个功能。

以后我去升级或者更换其他硬件,去修改这个结构体就可以了。

我来举一个例子,这个例子我以前曾经举过:

假设你们公司的产品会用到两个LCD,一开始的时候你这样写代码:

你使用一个宏,来决定使用lcd A还是lcd B。

在这个程序里面,他要么支持lcda,要么支持lcdb,不能够既支持a也支持b。

那如果你们公司的产品它既可以支持lcda,也可以支持lcdb的话,怎么办?

首先程序必须可以分辨LCD的类型,比如说可以去读取gpio,知道LCD的类型,代码就像上面一样。

这个代码它只有两款LCD,如果你们公司的产品支持100款LCD,怎么办?

请添加图片描述

这个时候,就可以使用结构体了,在结构体里面放函数指针。

请添加图片描述

对于第二个问题,我们可以试一下,不加这个pack的话,这个结构体是多大:

请添加图片描述

其实这个结构体,它加不加那个pack都没有影响。

去解析某些文件的头部的时候,这个pack才有用,比如BMP头部。

我给大家找一下这个BMP头部:

BMP文件的头部,它就是这么一个结构。

如果不加pack的话,或者说不加上那些attibute的话,bfType占据4字节(浪费2字节)。

使用这个结构体去构造头部,并且写入文件的时候,就会出错。

结构体的大小,比bmp文件的头部,增大了。

11. 问: 结构体的声明放在.h还是.c里好,如果在.h里,开放接口函数的时候,别人是不是也可以引用这个结构体了?

答: 你想让别人看见的东西,就放在头文件里。

12. 问: 只构造一个环形缓冲区怎么同时接收多个设备的数据呢?

答: 多个设备往里面放数据,多个设备调用:PutInputEvent。

13. 问: 如果只有按键输入的话,那创建的事件结构体岂不是有点浪费空间了?

答: 是的,浪费空间,所以使用union会比较好。

14. 问: 我用同一套板卡,但是不同的课题会用到不同的外设,不同的IO 这样底层硬件就理解为不同吗?不同的课题的话任务也不同。 这样该怎么考虑框架设计?

答: 我说一下我的想法。

同一套板卡,但是不同的课题会用到不同的外设,不同的IO:
这句话就可以细分,细分成两种情况:

第1种情况:他们都是使用这个引脚的gpio功能,项目一里面是用来输出,项目二里面是用来输入

请添加图片描述

这个时候,我们的程序就可以这样拆分:

请添加图片描述

看上面这个驱动,他可以兼容你的两个项目。

这个时候,这两个程序只有业务上的差别。

我们再来举第2个例子:

第2个项目,这个引脚可以用来控制灯,也可以用来作为adc,就是读取模拟信号

请添加图片描述

这个时候,框架就这样的:

请添加图片描述

我觉得没有必要把他们强制融合在一起

再来讲讲第3种情况,你们的程序既有业务1,也有业务2,
业务1把引脚当做gpio,
业务2把引脚当做adc

我们可以考虑这样一种框架:这是我临时想的,有可能考虑不周。

首先你得有一个输入,这个输入是用来触发一个切换的动作:

这时候就得把这个引脚,设置为普通的gpio,或者设置为adc。

业务1会使用到gpio子系统,

业务2使用到adc子系统,

如果非要在一个程序里面,即实现业务1,也实现业务2,那么里面必定有一个“切换的子系统”。

15. 问: 我感觉两个业务不一定要放一起,但是希望框架移植方便,不同的任务可以很方便移植,不伤筋动骨?

答: 实际上我也建议这个两个业务分开,我们同事前阵子还讨论过这个框架。

我们在做一个lvgl的桌面,

一种方法是:点击桌面上每一个图标,就启动一个独立的APP

另外一种方法是:点击桌面上的每一个图标,就加载一个动态库,

后来我们决定使用第1种方法,让这些APP尽可能独立。

16. 问: 老师,我的项目里面有can 422 flash,按照你的方法,是不是可以划分为输入,输出子系统,两个子系统中都有can 422 flash,但是这看起来很多余,有更好的方法吗?

答: 我们说的输入,是指那些可以直接影响到控制逻辑的,
一般的传感器我们只是去读取它的数据,显示它的数据,这些传感器不应该归到输入子系统。

不同功能的设备,我们干嘛要把它强制的放入同一个系统。

请添加图片描述

你就直接有4个系统:业务、存储、422、CAN不就可以了?

在我举的例子里面,开发板就在等待用户的按键、或者手机发来的数据。

等待这些数据,然后作出反应,所以我把按键、网络输入,还有串口输入,放到输入子系统。

422、CAN,没有必要强迫他们融合在一起。

17. 问: 这个环形缓冲器前面看视频觉得是收到的数据缓冲,现在怎么感觉是事件集呢?

答: 如果你使用rtos之后,事件集不能传递数据,用queue比较合适。

18. 问: 现在讲的架构 主要是针对外设的,那在做项目的时候,我们应该有个基本架构,根据项目不同进行裁剪,这个基本架构怎么做呢?

答: 这些基本的架构,我曾经做过。
我实现了很多比较独立的子系统:文件读写、图像文件解析、字模提取等。

这些子系统,你做得比较独立的话,在你的项目中基本上就是把他们组装起来就可以了,再加上你的业务逻辑。

19. 问: int (*DeviceInit)(void);中,函数指针如果函数名这个位置不加框号,默认是什么情况?

答: 不加括号的话它就是个函数声明,写在结构体里面是一个错误的用法。

20. 问: InputDevice可以放在设备子系统里吗?

答: InputDevice在rtos里面,我将会为每一个设备创建一个任务,所以把它放到设备子系统去,不合适。

InputDevice,会调用设备子系统的函数,去获得硬件数据。

在裸机程序里, InputDevice解析数据,设备子系统提供原始的数据,也不应该把他们放在一起。

比如说,设备子系统,他可以提供说哪一个gpio被按下、被松开。

但是,这个gpio,对应哪一个按键,什么时候发生,不应该由它来做。

应该有更上一层的InputDevice,根据gpio电平、根据时间,构造出InputEvent。

这就回到我们刚才说的原则:各司其职,不要越界。

21. 问: 老师,能总结一下今天的课程吗?程序设计的时候是 列出功能模块>划分为子系统>子系统分层?>定义每一个子系统的数据结构和接口>开始写.c?

答:

列出功能模块>划分为子系统>子系统分层>定义每一个子系统的数据结构和接口>开始写.c
列出功能模块>划分为子系统>定义每一个子系统的数据结构和接口>子系统分层>开始写.c。

我用的是后面这种流程。