我们使用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 输入子系统_单元测试
三:课后作业
10_6_input_unittest 中实现了按键功能:
在按键中断函数中,构造InputEvent,放入Buffer
请参考它实现:串口输入功能。
思路:
找到串口的接收中断函数
当串口接收到回车换行时,表示得到了一个完整的数据
将数据构造为InputEvent,放入Buffer
请思考,怎么设计"设备子系统",比如LED、风扇、OLED,它们的操作并不相同。
怎么抽象出一个结构体,可以支持它们?
写出这个结构体。
写好的作业,想老师批改的,请放在QQ群里。
四: 晚课学员提问答: 比如说LCD、 Flash、各类传感器,这些都是单功能的硬件模块。
答: 对于设备,有些设备只能够输出,有些设备只能够输入,有些设备既能输出也能输入。所以一个设备子系统,有时候并不能够简单的把它划入输入、或者输出。比如U盘,你可以写入数据,可以读出数据。这个时候单纯把它划为输入或者输出都不恰当。
答: 先划出子系统,在实现子系统的时候再考虑分层。比如我把系统分为输入和输出,分成两个子系统,在实现输入子系统的时候,再考虑分层。所以我们首先要练的是,怎么把整个系统拆分成多个子系统。怎么拆分成多个子系统,刚才我们已经介绍了方法:
先把它拆分成:输入、输出、控制逻辑(业务)三个子系统。
再去细分这三个子系统,得到更多、功能更加独立的子系统。
我再举一个例子:
我一开始设计这个系统的时候,并没有这个字体子系统。
后来一想,我怎么得到字符的点阵?
我可以从点阵字库里面得到,也可以从Free type字库里面得到。
去显示文字的时候,字库的来源应该独立出来。
所以我就把它分成了显示子系统,字体子系统:
字体子系统,提供字模;
显示子系统,根据字模来显示文字。
甚至有时候在编程的时候发现,这个子系统功能不大纯粹,又去拆分它。
答: 后面的esp32芯片上会有。
答: 先从小项目开始练。
答: 可以用union,也推荐使用它。
答: 分成的第一步,你要去理清楚功能。
就比如说输入子系统:我之所以把它拆分成两层,主要是:
汇总、管理,所以我就简单的把输入子系统划分为上下两层。
划分出上下两层之后,再去考虑结构体。
答: 不管你怎么做,你得有一个分类type。你当然可以在里面再放三个结构体,就是比较浪费空间。
答: 每个输入设备,都可以产生自己的InputEvent,里面有自己的buff。定义类型的时候并不需要有出多个buff。每一个设备它都可以定义自己的InputEvent。
答: 分装成结构体,就使用面向对象的编程思想,用一个结构体来实现一个功能。
以后我去升级或者更换其他硬件,去修改这个结构体就可以了。
我来举一个例子,这个例子我以前曾经举过:
假设你们公司的产品会用到两个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文件的头部,增大了。
答: 你想让别人看见的东西,就放在头文件里。
答: 多个设备往里面放数据,多个设备调用:PutInputEvent。
答: 是的,浪费空间,所以使用union会比较好。
答: 我说一下我的想法。
同一套板卡,但是不同的课题会用到不同的外设,不同的IO:
这句话就可以细分,细分成两种情况:
第1种情况:他们都是使用这个引脚的gpio功能,项目一里面是用来输出,项目二里面是用来输入
这个时候,我们的程序就可以这样拆分:
看上面这个驱动,他可以兼容你的两个项目。
这个时候,这两个程序只有业务上的差别。
我们再来举第2个例子:
第2个项目,这个引脚可以用来控制灯,也可以用来作为adc,就是读取模拟信号
这个时候,框架就这样的:
我觉得没有必要把他们强制融合在一起
再来讲讲第3种情况,你们的程序既有业务1,也有业务2,
业务1把引脚当做gpio,
业务2把引脚当做adc
我们可以考虑这样一种框架:这是我临时想的,有可能考虑不周。
首先你得有一个输入,这个输入是用来触发一个切换的动作:
这时候就得把这个引脚,设置为普通的gpio,或者设置为adc。
业务1会使用到gpio子系统,
业务2使用到adc子系统,
如果非要在一个程序里面,即实现业务1,也实现业务2,那么里面必定有一个“切换的子系统”。
答: 实际上我也建议这个两个业务分开,我们同事前阵子还讨论过这个框架。
我们在做一个lvgl的桌面,
一种方法是:点击桌面上每一个图标,就启动一个独立的APP
另外一种方法是:点击桌面上的每一个图标,就加载一个动态库,
后来我们决定使用第1种方法,让这些APP尽可能独立。
答: 我们说的输入,是指那些可以直接影响到控制逻辑的,
一般的传感器我们只是去读取它的数据,显示它的数据,这些传感器不应该归到输入子系统。
不同功能的设备,我们干嘛要把它强制的放入同一个系统。
你就直接有4个系统:业务、存储、422、CAN不就可以了?
在我举的例子里面,开发板就在等待用户的按键、或者手机发来的数据。
等待这些数据,然后作出反应,所以我把按键、网络输入,还有串口输入,放到输入子系统。
422、CAN,没有必要强迫他们融合在一起。
答: 如果你使用rtos之后,事件集不能传递数据,用queue比较合适。
答: 这些基本的架构,我曾经做过。
我实现了很多比较独立的子系统:文件读写、图像文件解析、字模提取等。
这些子系统,你做得比较独立的话,在你的项目中基本上就是把他们组装起来就可以了,再加上你的业务逻辑。
int (*DeviceInit)(void);
中,函数指针如果函数名这个位置不加框号,默认是什么情况?答: 不加括号的话它就是个函数声明,写在结构体里面是一个错误的用法。
答: InputDevice在rtos里面,我将会为每一个设备创建一个任务,所以把它放到设备子系统去,不合适。
InputDevice,会调用设备子系统的函数,去获得硬件数据。
在裸机程序里, InputDevice解析数据,设备子系统提供原始的数据,也不应该把他们放在一起。
比如说,设备子系统,他可以提供说哪一个gpio被按下、被松开。
但是,这个gpio,对应哪一个按键,什么时候发生,不应该由它来做。
应该有更上一层的InputDevice,根据gpio电平、根据时间,构造出InputEvent。
这就回到我们刚才说的原则:各司其职,不要越界。
答:
列出功能模块>划分为子系统>子系统分层>定义每一个子系统的数据结构和接口>开始写.c
列出功能模块>划分为子系统>定义每一个子系统的数据结构和接口>子系统分层>开始写.c。
我用的是后面这种流程。