嵌入式裸机架构的探索与崩塌

news2024/11/18 17:29:53

为什么会想着探索下嵌入式裸机的架构呢?是因为最近写了一个项目,项目开发接近尾声时,发现了一些问题:

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个,就定义成结构体吧。

这也是一种简单的封装。

后续再优化架构估计就是在这上面琢磨了。

总之,先把上面三种架构版本熟练掌握。

裸机架构的崩塌

到了这里,要说一点感想:裸机根本就没有架构,或者说,裸机本身就是一种前后台架构。而操作系统本身也是一种架构,那就不再是裸机的架构了。

为什么有这种感想呢?

今天,我根据上述的教程,自己在裸机项目里用了下回调函数。

定义函数指针

注意,typedef函数指针时,上面的名称就是该函数指针的别名,别再后面再取个名了,一定要注意。

注册函数

中断触发时调用

业务层定义处理函数

主函数里注册

以上就是使用的过程。

得出几点结论:

1、可以确定的是,中断里回调函数类似于中断嵌套,会打断主循环的执行

2、接下来要确定的是,是否会阻塞中断。

经验证:

回调函数在裸机里几乎没有作用,跟直接调用上层函数没啥区别。

我在回调处理函数中做了5秒延时,不管是直接调用,还是回调函数调用,都会阻塞中断。

也就是说,搞了半天,绕了一大圈,结果在裸机里,使用回调函数,增加复杂度不说,而且没有任何改进。

再想想,网上说了回调函数的很多好处

什么灵活、实时性强、易于封装、移植性好……

就是没人说,这个并不适用于裸机。。。。。。。。。。。。

常见于操作系统环境使用,正好我看的就是一个轻量级的操作系统的课程。。。。。。

常常是,系统有一个函数指针,用户重写这个函数,并且注册传入底层的函数,就可以实现底层调用上层的目的了,灵活性挺高。

但是,还是那句话,适用于操作系统环境。

这么一想,探索了一段时间的所谓裸机架构,其实是个几乎不存在的东西。

可以这么说,回调函数常见于操作系统的设计中,应用代码几乎用不到。

裸机中的最大特点就是,任务是依次执行的,必需先执行完上一个任务,才能再执行下一个任务。想通了这一层,就理解了裸机中回调函数也会和普通调用一样阻塞中断。

操作系统的最大特点就是并发执行。

前后台系统的回归

裸机,直接while里循环调度即可,不要搞些花里胡哨的东西。

我们能做的就是遵循前后台系统开发的原则,然后在此基础上,做好分层,做好头文件和全局变量管理,提高代码的规范性和可读性。

裸机头文件统一管理

头文件可以统一管理application.h

可以放在PROJECT里

在这个头文件里统一管理所有用到的头文件

之前以为头文件放在一起再包含,会导致很多c文件会包含很多不需要用到的头文件,进而导致内存占用更多,目标文件更大。

但经过验证,并不会影响内存占用和目标文件的大小。

头文件没有统一管理时的空间占用以及hex文件大小

头文件统一管理之后的空间占用以及hex文件大小

通过对比可以发现,二者一模一样。

为什么呢?

因为头文件是预处理环节处理,只是进行单纯的文本代替,就是让我们能找到c文件中的宏定义或者类型定义表示什么含义而已,跟运行没关系,并不会包含进最后的烧录文件中。

另外,如果头文件中需要什么头文件,单独定义即可,一般头文件中只有声明或者类型定义,大多数情况下只需要#include <stdint.h>,如果重复包含,可能会导致循环嵌套。如果头文件中再调用application.h就会陷入循环,而单独定义,再经由application.h包含到其他文件时,无非就是重复包含的问题,而重复包含,头文件已经通过头尾的宏定义排除过了

分层思想(非常重要)

这里的思想很重要

1、业务层的横拆和纵拆:各独立模块之间是横拆,如果有数据交换,就通过全局变量,模块到common之间是竖拆,主要是将共用的部分分离出来。

2、分离时,要向下分离,提供给上层调用,当向下分层后,即使有全局变量,也可以通过函数参数传递下去,而不用在底层去extern上层的内容,记住,底层永远不要去引用上层的东西。想一想c库,难道他的库还要你给他个全局变量才能执行?就算能给,难道你要改库的源码,在源码里extern?而且,很多库根本就不开放源代码,这样,库也不可能去主动调上层用户的程序。

3、高内聚,低耦合,直观体现就是,任何一个函数,最好只依靠本c文件的内容以及其他任意头文件的内容来实现,而不必依赖其他c文件中的内容,比如其他文件的全局变量。想一想c标准库,或者stm32固件库,都是一个个独立的文件,几乎可以独立编译,不需要依靠其他c文件。

4、越往上层,越抽象,越少实现过程,越少细节,越多函数调用,最好到main主函数中时,没有任何实现过程,只有一个一个的任务函数。

5、越共用的东西,越应该放到下层,这样才能方便地被上层调用。比如APP也可以有个驱动层,app_driver,再上面就是app_common,再上面就是各模块,再就是综合应用,越共用的越往下放。最好就封装成一个调用库。但是也没必要分太细,差不多就行。最优的情况是,直接调用底层函数就能完成功能,再就是允许底层暴露一些全局数据。不过,上层不应该暴露数据给底层。

各模块之间独立,要想模块独立,就得将共用的东西往下分离。

书写再规范

变量小驼峰,函数大驼峰

前缀ST E g pInt pFun

判断的变量前加个is前缀,比如,isSelected

无参的地方都加上void以显式表明

等等

代码重复量太大的,强烈建议整合,减少冗余代码

全局变量的管理

头文件统一管理之后,头文件的内容确实就不再是问题,可以重点关注全局变量的管理。

裸机中很难避免使用全局变量,我们要尽量做好全局变量的管理。

那么,有哪些技巧呢?

1、如果全局变量超过3个,就建议使用结构体封装起来;

2、通过上面讲的分层思想,减少各文件之间全局变量的相互纠缠,全局化越大的变量越往下层放,下层永远也不要去引用上层的东西,就当下层是个只能被调用并且不开放源码的库,思考这种情况下应当如何设计全局变量;

3、做好全局变量的注释;

4、全局变量是主动在头文件中extern出去,然后谁包含了谁就能用,还是谁要用谁自己去自己的c文件里extern呢?我想,如果是下层的全局变量,那么可以extern出去,供上层使用,这样,不用每个上层文件使用时都得extern,如果是同层次之间的,建议还是谁要用谁自己extern。

5、这一点很重要

我们想一想固件库,里面是不是几乎看不到显式的全局变量?
固件库的方式,下层定义相关参数结构体,在对应函数中定义结构体形参,然后直接对数据进行操作,上层调用函数时,定义结构体局部变量然后将结构体指针传入给底层函数进行操作。
但也是因为固件库基本都是对寄存器赋值,才能更好地操作,虽然上层没有寄存器,不过我们可以借鉴这种思路。
//其实仔细想想,寄存器其实就相当于最底层的全局变量,如果说我们把APP最底层的全局变量都定义在app最底层,就当这些全局变量是寄存器,我们需要的时候就去取底层寄存器的值,上层也可以方便地去修改底层寄存器的值,
//然后寄存器的值甚至可以定义设置和获取的函数,就和固件库里的有些set以及get函数一样。
//这样的话,甚至可以进行位操作。
//还是那句话,通用的变量分离到下层,专用的在自己的文件里定义。
上层向下层传递函数形参,下层向上层提供全局变量
app或者各模块
app_common
app_softregister

另外,同一文件中,越共用的函数越往上放,将下层的函数放在文件上面,上层的函数放在文件下面,这样就不用进行太多的函数声明了。
关于上层和下层的这些思维,对于头文件也是一样的,越底层越往下放,你想想,固件库难道要你上层提供一个数据类型才能用?

//底层越集成越好,上层假如有100个地方要用,如果要改,不用去改100个上层处,只用改一个下层即可。

这里有一篇论坛可以参考下,差不多就是我这里说的思路

如何尽量地避免使用全局变量呢? (amobbs.com 阿莫电子论坛 - 东莞阿莫电子网站)

总结来说就是以下几点:

1、底层驱动尽可能独立;

2、将APP中所有全局变量放在一个底层c中,同时,提供get和set接口函数来提供给上层访问;

3、能封装成结构体的一些变量就封装起来,然后上层通过传递结构体指针的方式来修改这些变量;

4、通用的变量分离到下层,专用的在自己的文件里定义;

5、总之,就是,尽量不要使用开放的全局变量;

6、不过,相对于直接使用全局变量,这样操作效率相对较低,但是基本没什么影响;

7、最怕在多个模块中直接操作全局变量,这样会把各个模块之间的逻辑关系搞复杂!

做好模块化、层次化。

使用这些方法好处非常多,不仅不会降低效率,还会降低代码尺寸,实现对变量的访问权限控制(只读,只写),可以一劳永逸的实现对变量的原子保护,可以在读写的时候进行有效性检查,最后调试的时候方便追踪谁对变量进行了访问,也可以填写调试值。这就是面向接口开发的好处。

最后,总结原则就是:不要让任何一个全局变量暴露出来。

随便说两句……………………………… 

代码的合理化规范化其实是我们人类的需求,并不是机器的需求,机器只要是最后得到的二进制数是对的,就可以了,不管代码写的多烂,哪怕把所有内容全都塞在一个文件里,对计算机来说,是没有什么差别的,但是对于我们人类的阅读开发维护等,就是极大的灾难了。这就是为什么有的代码写的很烂,但是功能也能实现。不过,我们的目标是,开发既是一门技术,也要尽量做成一门艺术。就好比踢足球,本身是个技术活,但是梅西能把足球踢成艺术,就是一种巨大的成功。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1035973.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

6年Android开发前10月的总结,写给正在求职的安卓开发

进入大厂工作对许多人来说已经是一种挑战&#xff0c;但只要充分准备&#xff0c;很多问题都可以逐步解决。当然&#xff0c;运气也起到了一定的作用&#xff0c;但最终还是与自身的努力密不可分。运气是实力的一部分&#xff0c;因为自助者天助。 每到10月进行总结时&#xff…

华为ICT——第二章-数字图像处理私人笔记

目录 1&#xff1a;计算机视觉&#xff1a;​编辑 2&#xff1a;计算机视觉应用&#xff1a;​编辑 3&#xff1a;计算机视界核心问题&#xff1a;​编辑 4&#xff1a;相关学科&#xff1a; 5&#xff1a;计算机视觉与人工智能&#xff1a; 最成熟的技术方向是图像识别 6…

Django应用及分布式路由

Django应用及分布式路由 应用 应用在Django项目中一个完全独立的业务模块&#xff0c;可以包含自己的路由&#xff0c;视图&#xff0c;模板&#xff0c;模型 应用配置 在这里面添加你自定义的应用 INSTALLED_APPS [django.contrib.admin,django.contrib.auth,django.cont…

如何使用docker快速部署MinDoc文档系统

MinDoc是非常优秀的知识分享系统&#xff0c;但是很多刚接触的人会一脸懵逼&#xff0c;而且官方文档写的也并不清晰&#xff0c;所以和大家分享一下快速部署MinDoc的方法。 首先docker环境先自行安装好&#xff0c;这里不再赘述。 拉取docker镜像&#xff1a; docker pull …

解决 Github port 443 : Timed out

解决方法 打开代理页面 打开 设置 --> 网络与Internet --> 查找代理 记录下当前系统代理的 IP 地址和端口号 如上图所示&#xff0c;地址与端口号为&#xff1a;127.0.0.1:7890 注意修改成自己的IP和端口号 git config --global http.proxy http://127.0.0.1:7890 gi…

面试总结之微服务篇

一、概览 1、微服务常用组件 微服务给系统开发带来了诸多问题和挑战&#xff0c;如服务间通信和调用的复杂性、数据一致性和事务管理、服务治理和版本管理等&#xff0c;为解决应对这些问题和挑战&#xff0c;各种微服务组件应运而生微服务的常见组件和实现&#xff1a; 1…

解决react报错“JSX 表达式必须具有一个父元素“

现象如下&#xff1a; 原因&#xff1a; 新插入的dom元素跟已有的dom元素平级了&#xff0c;必须创建一个共有的根元素 解决办法&#xff1a; 使用<> </>标签作为根元素&#xff0c;把所有子元素包裹起来 <> ....原代码 </> 问题解决&#xff01;…

sun.reflect.annotation.TypeNotPresentExceptionProxy

解决方法 找到TypeNotPresentExceptionProxy类,在sun.reflect.annotation下在TypeNotPresentExceptionProxy方法里打断点debug项目,查看断点错误 如图是缺少redis依赖导致的错误 https://www.cnblogs.com/qingmuchuanqi48/p/11716706.html

Spring学习笔记7 Bean的生命周期

Spring其实就是一个管理Bean对象的工厂.它负责对象的创建,对象的销毁. 这样我们才可以知道在哪个时间节点上调用了哪个类的哪个方法,知道代码该写在哪里 Bean的生命周期之粗略5步 Bean生命周期的管理可以参考Spring的源码: AbstractAutowireCapableBeanFactory Bean的生命周期…

乐得瑞推出1拖2功率分配快充线方案,支持数据传输

随着PD3.1协议的市场应用越来越多&#xff0c;一些充电器的Type-C接口的输出功率达到百瓦及以上&#xff0c;如何充分利用好这类充电器设备&#xff0c;乐得瑞电子推出1拖2快充线缆解决方案&#xff0c;支持智能功率分配策略。 上图是乐得瑞1拖2功率分配快充线样线实物&#…

力扣刷题-链表-设计链表

题意&#xff1a; 在链表类中实现这些功能&#xff1a; get(index)&#xff1a;获取链表中第 index 个节点的值。如果索引无效&#xff0c;则返回-1。 addAtHead(val)&#xff1a;在链表的第一个元素之前添加一个值为 val 的节点。插入后&#xff0c;新节点将成为链表的第一个节…

哈希 -- 开散列(哈希桶)

拉链法 这里我们要是用string&#xff08;string来做key&#xff09;来取模&#xff0c;再增加一个模板参数&#xff0c;配一个取模的仿函数即可

leetcode 133. 克隆图

leetcode 133. 克隆图 给你无向 连通 图中一个节点的引用&#xff0c;请你返回该图的 深拷贝&#xff08;克隆&#xff09;。 图中的每个节点都包含它的值 val&#xff08;int&#xff09; 和其邻居的列表&#xff08;list[Node]&#xff09;。 class Node { public int val;…

Flex布局是什么?

一、Flex布局是什么&#xff1f; Flex是Flexible Box的缩写&#xff0c;意为”弹性布局”&#xff0c;用来为盒状模型提供最大的灵活性。 注意&#xff0c;设为Flex布局以后&#xff0c;子元素的float、clear和vertical-align属性将失效。 二、基本概念 采用Flex布局的元素…

基于JavaWeb(Servlet+jsp)的个人通讯录管理系统(含实验报告)

基于JavaWeb&#xff08;Servletjsp&#xff09;的个人通讯录管理系统&#xff08;含实验报告&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 本项目为简单的基于Javaweb实现的个人通讯录管理系统&#xff0c;实现主要功能为用户注册登录、首页展示、用户联…

C++——如何正确的使用STL中的vector?

什么是vector&#xff1f; 在STL&#xff08;标准模板库&#xff09;中&#xff0c;vector是一种动态数组容器&#xff0c;可根据需要自动增长或缩小。它可以存储任意类型的元素&#xff0c;并且支持快速的随机访问。 vector是表示可变大小数组的序列容器vector采用的是连续的…

Java面试题整理(带答案)

目录 TCP和UDP的区别 get和post的区别 Cookie和session的区别 Java的基本类型有哪些&#xff1f; 抽象类和接口区别&#xff1f; 对于堆栈的理解 和equals区别 如何理解Java多态&#xff1f; 创建线程都有哪些方式 脏读、不可重复度、幻读都是什么&#xff1f; Jav…

YUM 升级 PHP7

文章目录 YUM 升级 PHP71. 查看当前 PHP 信息2. YUM 安装 PHP73. 查看 PHP 版本4. 启动PHP-FPM YUM 升级 PHP7 参考地址&#xff1a;网站地址 参考地址&#xff1a;网站地址 1. 查看当前 PHP 信息 # 查看 PHP 版本信息 php -v# 查看 yum 源中 PHP 信息 yum list | grep php2. …

最美壁纸小程序源码全新修复版 带激励广告

最美壁纸小程序源码全新修复版&#xff0c;带激励广告&#xff0c;刚刚修复的&#xff0c;程序包里有安全域名与广告id配置说明&#xff0c;这个源码无PHP后台发布后添加合法域名就可以了。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88368720

【vscode设置ctrl+滑轮调节代码字体大小】

vscode设置ctrl滑轮调节代码字体大小 打开设置之后在搜索框中输入"editor.mouseWheelZoom": true&#xff0c;勾选上对勾就OK了&#xff01; “editor.mouseWheelZoom”: true