为什么会想着探索下嵌入式裸机的架构呢?是因为最近写了一个项目,项目开发接近尾声时,发现了一些问题:
1、项目中,驱动层和应用层掺杂在一起,虽然大部分是应用层调用驱动层,但是也存在驱动层调用业务层的情况,这导致了层次间的耦合;
2、应用程序全都放在了一个app.c文件夹里,代码高达1万行,实在是过于庞大,我想着将代码拆分下,发现实在是太困难,牵一发动全身;
3、全局变量满天飞,代码量大了之后,自己都晕了,虽然写了注释,但是想想,如果注释没写清楚,那么时间久了,自己回来看都不知道是啥~~~~~~;
那么,如何在后续项目中有所改进呢?
架构1.0
关于程序的架构和规范化,要做到:
层次分明,模块化,高內聚低耦合,风格规范易懂。
自顶向下设计,自底向上开发,花一两天来设计,设计好之后再开发。
层次分明
根据需求,有各种各样的功能要实现,但是因为嵌入式不仅涉及到软件,还会涉及到硬件,所以,需要分层,思维才能更清晰,更有利于后期的开发和维护。
根据我自己的开发经验,先说下我的最初裸机分层习惯。
将整体的架构设计分成3层,再多层次对于裸机感觉没什么必要了。
模版示例:
APP存放业务层代码;
DRIVER存放硬件驱动层代码;
SYSPERIPHERALS存放系统外设代码;
FWLIB存放固件库;
CORE存放一些板级核心代码;
OBJ存放keil的输出文件;
MIDDLEWARE存放中间件;
RESOURCE存放一些资源比如字库等;
USER存放工程;
UTILITIES存放其他内容;
除了一些固定的文件,在开发时分为系统外设、驱动层,再加上一个业务层。
系统外设层主要是对用到的各种片上外设进行初始化,之前经常跟驱动层写到一起,但时间久了就发现二者其实是不同的层次,写到一起容易混乱。
理想情况下,系统外设层向驱动层提供接口,驱动层向业务层提供接口。
系统外设层的各个硬件口,最后都用宏定义给重命名,如果要移植,就只用该硬件口就行,而不用去动驱动层,比如,如果就用GPIOA去开发,那么如果换了板子,就要改驱动层的书写,但是如果重命名,就只用改系统外设层的头文件即可。
另外,对于业务层来说,不推荐将所有的功能都放在同一个文件中,虽然比较方便,但是这会导致文件特别大,不利于后续开发和维护。最好按照功能模块进行开发,然后有一些各模块共用的功能,可以抽离出来,单独一个文件。通常,拆一个总的入口文件,再按大模块拆一拆,然后就是共用的部分。按模块其实是同级拆分,将共用功能分离出来,其实是上下层次的拆分,不过,也没啥必要再分不同目录来放了。
上面实例中,其实拆的太多了,就多了文件跟文件之间的纠缠,后续也很难理清。
前期一定就要做好设计和规划,不要试图想着先开发,后续再修改,惨痛的教训告诉你,修改比重新开发更让人烦躁,很费时间,分分钟有牵一发动全身的风险。
总之就是,越往上层,就应当越抽象。
层数越多,越复杂。
请合理平衡。
关于系统外设和驱动层的初始化,如果系统外设是和具体的驱动关联的,就可以放在驱动里,如果不能跟具体的驱动关联,就直接在系统外设层定义初始化接口即可,比如定时器。
另外,注意编码规范,如果太随意,越往后代码量越大就越难开发。就按照常规推荐的那些编码规范来写就行了,也不必特立独行。
关于变量还有头文件中的宏定义,有共用的,有专用的,专用的肯定是放在自己的c中,共用的可以放在common中,该static的就static。关于程序中的全局变量,建议如果超过3个,就用结构体封装起来,函数最好也是用函数指针结构体封装起来(借鉴硬件家园的风格)。区分仅自己使用和需要共享使用的情况,然后决定是用static限定或者加入到相应结构体中。
模块化就比较好理解,各个模块单独开发,最好可以实现独立编译。
如果是已经写好的代码,不要试图去重构,这会让你陷入无尽的烦恼之中,不必重新开发更轻松。
已经写好的,就将就用吧。
另外就是,不要试图追求完美。
架构2.0
改进点:不要将硬件驱动层再分两层了。
看了很多的代码,发现也没有将驱动层分成系统外设和驱动层的。
其实,将二者合并在一起的好处也是有的:
1、减少了层次间的相互调用,而且,代码量也不会增加多少;
2、各系统外设的初始化本来就是外设的一部分,直接放在驱动文件里,也是合理的,更清晰明了,如果单独把所有外设的初始化都放在一起,也容易搞混;
3、不用考虑中断响应函数到底放在哪一层;
4、初始化时,直接按外设模块来进行即可,不用纠结到底放在哪一层来初始化;
5、照样可以用宏定义来定义。
基于以上几点考虑,还是将架构就分为两层,即硬件驱动层和业务层。
注意,将USER改名为PROJECT了,不过不重要。
架构3.0
要实现的目标:
1、硬件驱动层,各模块之间可以独立编译,互不影响;
2、硬件驱动层不会反向调用业务层的API;
3、硬件驱动层不会向外暴露自身的全局变量;
以上三点,我们来依次看一下。
第一点,很容易做到,只要各模块独立c和h即可;
第二点,开发时注意些就行,千万不要反向调用;
第三点,要多说一些。
通常,驱动层和业务层的关系,分成两种:
一种是业务层主动调用驱动层的API,比如业务层调用驱动层的打开LED函数实现点亮LED,或者主动调用数据发送函数发送数据等;
还有一种是被动响应式的,即驱动层响应之后,需要向业务层上报,此时业务层就是被动响应的,有很多的例子,比如按键按下,串口接收数据,ADC采集等等,都是驱动层响应后,需要向业务层上报数据。
我们通常的做法是,在驱动层定义一个全局变量,然后声明出去,业务层的任务中循环判断这些全局变量,从而做出相应的动作。
可参考:单片机模块化编程框架篇-编写回调函数及产品应用_哔哩哔哩_bilibili
这里说的就是业务层主动发起的调用。
那么,业务层被动响应式的情况呢?
那么,回调函数的开发思路是怎么样的呢?
说实话,回调函数其实是个不太好理解的东西。
这名字听着就不知道啥意思。
其实,在本文的场景下,我们可以这样理解:业务层调用驱动层时,是直接调用的,但是业务层被动响应的情况下,驱动层基本都是由中断来触发的,通常如果直接在驱动层的中断里调用业务层的函数,一来不符合中断快进快出的理念,二来不符合下层不应该调用上层的理念。
这种情况下,我们可以在驱动层间接调用业务层的处理函数。
在驱动层定义一个回调函数的函数指针,函数里传入的是需要传递的全局变量
同时定义一个注册函数
还要在业务层定义一个跟函数指针同类型的处理函数
然后在业务层调用注册函数,将业务层的处理函数传入驱动层的函数指针
然后在中断里只需要调用函数指针即可实现间接调用业务层的目的
但实际上,访问的只是驱动文件中的函数指针。
因为,这个实现了下层调用上层的目的,是在上层定义,但是由下层调用,所以,被叫做回调函数,也是很合理的。
至此,就进步了一个台阶,至少,解决了驱动层和业务层之间的全局变量的传递问题。
另外,建议如果全局变量超过3个,就定义成结构体吧。
这也是一种简单的封装。
后续再优化架构估计就是在这上面琢磨了。
总之,先把上面三种架构版本熟练掌握。