发布时间:2024-03-12 12:01
嵌入式架构有多重要?
要做到嵌入式应用的代码逻辑清晰,且避免重复的造轮子,没有好的应用架构怎么行?
如果没有好的架构,移植将会是一件很痛苦的事情。
如果没有好的架构,复用是最大的难题,没法更大限度的复用原有的代码。
接下来嵌入式ARM便和大家分享一下,
嵌入式架构那些事儿……
嵌入式系统一般由软件和硬件两个部分组成,基中嵌入式处理器、存储器和外部设备构成整个系统的硬件基础。嵌入式系统的软件部分可以分为三个层次,分别是系统软件、支撑软件和应用软件,其中系统软件和支撑软件是基础,应用软件是最能体现整个嵌入式系统的特点和功能部分。
嵌入式系统的核心部件是各种类型的嵌入式处理器:
(1)嵌入式微处理器:在功能上跟普通微处理器基本一致,但是它具有体积小、功耗低、成本低及可靠性高的优点。
(2)嵌入式微控制器:双称单片机,一般以某一种微处理器内核为核心,整个计算机系统都集成到一块芯片中,与嵌入式微处理器相比,最大特点是单片化。
(3)嵌入式数字信号处理器:一种专门用于信号处理的处理器,DSP(Digital Signal Processor)是芯片内部采用程序和数据分开的结构,具有专门的硬件乘法器,广泛采用流水线操作,提供特殊的DSP指令。
(4)嵌入式片上系统:一种在一块芯片上集成很多功能模块的复杂系统,在大量生产时,生产成本也远远低于单片部件组成的电路板系统。
大多数人参与的是嵌入式软件设计,更多的是接触的是上层软件系统部分,可以分为两大类型嵌入式软件应用工程师以及嵌入式驱动工程师。
前者主要负责 linux APP 设计,负责应用层业务开发,主要具备如下几个专业技能:
1.熟悉网络编程、TCP/IP协议、IIC、SPI协议
2.熟悉多线程管理、进程间通信、文件IO操作
3.了解基本的shell编程
4.熟悉数据库操作
5.了解QT或者Android
后者负责驱动开发,更加涉及底层。
1.熟悉uboot和Linux内核,完成Linux内核裁剪定制以及系统的固件更新
2.熟悉Linux驱动模型
3.熟悉ARM架构
4.熟悉基本的电路原理
02
嵌入式程序设计思路
现在的小朋友都爱玩搭积木的游戏,一个模块一个模块的拼装起来,快速组成各种不同的模型。现在的产品设计也很少从零开始。大都复用现有成熟的模块,专注于某个擅长领域。
我的嵌入式应用架构思路来源与此,即功能模块设计与分层。
把API分为驱动层和应用层API,而不是所有程序都调用驱动层API。(整个应用中都调用驱动层API会导致应用中驱动调用随处可见,无法移植和最大限度的复用)
先把一个应用进行功能模块划分,并对整体结构进行分层,然后设计出功能独立的各个模块(如算法模块,文件库模块,通信库模块),在模块之上开放公共接口。
驱动层提供出公共接口供上层调用。各个功能模块可以独立编译(如算法模块纯ANSI C,可在任意平台复用),或者调用驱动层接口(文件库模块调用了驱动读写Flash),总而言之,言而总之,封装出各个功能独立的可复用的功能模块。
总体分:硬件驱动层-->功能模块层-->应用接口层-->业务逻辑层-->应用层
总体结构示意框图:
应用层,为程序的总体的运行框架,组织调用业务逻辑。可以用某种嵌入式操作系统实现几种任务 。如定时任务,卡处理任务,菜单任务,通信任务。
业务逻辑层,如CPU卡处理,交通部卡处理,银联卡处理,M1卡处理,通信记录上传,黑名单下载,票价参数下载等。
应用接口层,提供公共的api接口供应用接口供上层调用。这些接口也可由下层的功能模块开放出来,应用接口层负责汇总。
功能模块层,可以封装不同的功能模块。如算法库,文件库,通信库,银联库,向上提供应用接口层的接口,向下调用驱动接口。
硬件驱动层,由各个驱动模块组成,向上提供统一的接口。
遵循一些约定:
1.每个模块提供出的接口要统一,后续只能增,不能改原来的接口。
2.模块与模块之间相互独立,互不影响,不能相互调用,只能调用它下层的接口。
3.由模块构成层,层与层之间不能跨级调用。如在应用层中不能看到直接调用驱动层的代码。
4.模块中又可以继续分层,如接口层,驱动层,硬件层。
如果驱动变动了,或者换不同平台,只需更改驱动层,应用层不受影响。
如果功能模块变动了,只需升级功能功能模块,其他的模块不受影响,应用层也不受影响。
按照这种逻辑设计好之后,主要的工作就是在业务逻辑层。应用层则为程序的总体流程和框架,主要调用业务逻辑层实现不同的功能。
我们现在的代码结构,基本是按这个思路来的。
硬件驱动层-->功能模块层-->应用接口层-->业务逻辑层-->应用层。
看看以下两种风格的代码,你更喜欢哪个。
另一种风格:
同样是保存参数,非要拆成 AlgCRC16 ,WritePraFlash( (unsigned char *)&NetPra , NETPRA_ADDR , sizeof(_NetPra) )两步吗?
还有AH_Para_Verify这个,在应用层中真是多余啊,检测失败又从Flash读取。关于参数,一开机就应该检测合法性了。
既然都是要保存参数,就应该做个封装,如上图所示,把系统用到的不同参数做个规划。应用层调用APP_Open_UseFile 或者APP_Read_UseFile,
而不是直接的去读写Flash。
来看看赫赫有名的谷歌的android架构,虽然很复杂,但从框图上看,也像是搭积木,各个功能模块独立,层次分明。最低层建立在linux Kernel基础上,然后是各个组件库libraries,再往上是应用框架和应用。
以NC_FileLib,文件库模块为例,如果要用在其他平台,如EH0918手持机设备,只需要移植几个硬件层接口即可。
休息一下,推荐一篇文章《怎样混好嵌入式/MCU/ARM/DSP这一行?》
03
一个引以为戒的实例
一、错误的示范
近公司新招了一个做嵌入式软件开发的同事,该同事是从上海的某一个上市公司出来的,因为我们这边人手不够,因此把他安排了去负责一个新产品的研发,前期让他负责加速度计、NB-IOT、舵机、外置Flash的功能测试,测试完成之后,准备让他做一个该产品的概要设计。然后他花了2个星期的时间,给我们写出来一个概要设计,说实话,我看到这个概要设计,我就觉得是刚毕业的大学生写的。
版本一的架构设计
2.1系统体系结构
系统分为两层:硬件驱动层、应用层。
2.1.1硬件驱动层
硬件驱动层包含板载硬件资源正常运行所需的所有驱动程序。
1)MCU初始化
2)I2C数据存取
3)SPI数据读取
4)加速度计初始化
5)蓝牙模块启动
6)BC95模块启动
7)485通讯模块启动
2.2.2应用层
1)Mcu运行模式切换
2)震动及倾斜
3)数据解析
4)开/关锁
5)数据发送
6)历史数据保存
看到版本一的架构设计之后,说实话,我还是第一次见到这样来写架构设计的,居然是以序号来写的,这个让别人读起来,特别的别扭。
版本二的架构设计
看到版本二的架构设计之后,虽然颇感欣慰,但是想到达到我们所要求的,还要很大的一段距离,该架构设计,主要有以下几点问题:
1.对架构的理解还不是很清晰,既然是做架构设计,那就应该从整体来看,而不是仅仅只是局限于一个模块,或者功能里面。
2.还是每个层次的理解也还不是很清晰,比如讲MCU的初始化,归于硬件驱动层里面。MCU的初始化,严格意义上来说,是属于流程的一部分了,而不是驱动。比如电脑的开启启动,把这个归于硬件的驱动里面,肯定是属于牛头不对马嘴的。
3.还有就是各个模块的启动,也是不能属于硬件驱动层的,也都是业务流程的一部分了,都不应该属于驱动层的一部分。
4.还有就是总线数据的读写,虽然驱动的作用也就是读写,但是数据总线的读写不能写成硬件驱动。
5.应用层的系统参数初始化,也还是属于流程。
6.数据的解析和数据的发生,都是属于通信功能里面的,不应该单独独立出来,属于单个的应用。
二、更改版基本框架图
(1)架构设计的目的
1.应用的代码逻辑清晰,且避免重复造轮子。
2.如果没有好的架构,移植将会是一件很痛苦的事情,因此一个好的架构设计,方便软件的移植。
3.最大限度地复用。
4.高内聚低耦合。
(2)设计思路
如何把硬件的驱动和一个功能封装成一个个的模块,然后可以像小朋友搭积木一个,一个个模块可以快速的拼接起来,组成一个个不同的模型。
我们的嵌入式架构思路也是来源于此,即功能模块化设计、分层设计。
这个设计和WEB开发的MVC模式类似,都是注重分层设计。
模块化设计:将收集到的需求,进行归类,总结和分析,将这些需求概括为一个个单独的功能,每一个功能,做成一个单独的功能模块。
分层设计一句话不好直接表达,其主要体现在一下几方面:
1.功能模块对外调用的模块封装成一个个API,将底层驱动做个API以供功能模块调用。(各个功能模块可以独立编译(如通信模块纯ANSI C,可在任意平台复用),或者调用驱动层接口(日志库模块调用了驱动读写Flash),总而言之,言而总之,封装出各个功能独立的可复用的功能模块。)
2.API分为驱动层API和应用层API,而不是所有程序都调用驱动层API。(整个应用中都调用驱动层API会导致应用中驱动调用随处可见,无法移植和最大限度的复用)
总体分 硬件驱动层-->功能模块层-->业务逻辑层-->应用层
总体结构示意框图:
说明:
1.层与层之间不能跨层调用。
2.模块与模块各自独立,无依赖关系。
3.模块提供统一的接口供上层调用,模块的内外接口分明。
4.模块的功能只能增,不能改。
5.各个功能模块层也还可以进行继续分层,比如接口层、驱动层、硬件层。
(3)模块层次说明
硬件驱动层
硬件驱动层包含板载硬件资源正常运行所需的所有驱动程序并提供API给功能模块调用。
功能模块层
功能模块层包括实现具体功能的函数,通过调用驱动层API实现相应功能,同时提供可调用的API给业务逻辑层。
业务逻辑层
业务逻辑层包括产品整体功能的各个业务流程,通过调用功能模块层的API实现。
应用层
应用层将各个业务逻辑进行整合调用,完成整个产品的功能。
(4)优势
如果驱动变动了,或者换不同平台,只需更改驱动层,应用层不受影响。
如果功能模块变动了,只需升级相应的功能模块,其他的模块不受影响,应用层也不受影响。
按照这种逻辑设计好之后,主要的工作就是在业务逻辑层。应用层则为程序的总体流程和框架,主要调用业务逻辑层实现不同的功能。
04
给嵌入式代码也来个分层
代码结构也会有缺陷:
(1)开发效率低:每次使用片内的某一资源(例如定时器等),笔者都要去查询CC2430中文手册,比较eggache~
(2)代码重复较多:每个实验源码中,诸如 xtal_init ,led_init 等初始化函数每次都要编写
(3)不易修改:代码中的业务逻辑与SFR的操作混在一起,可读性较差,修改起来也费力
正是由于以上问题,笔者决定暂停了该系列博文的续写,抽出时间来思考一下解决办法。
笔者在学习嵌入式编程之前,曾有过 ASP.NET 网站开发经验,对其分层理论也有所实践,下面简单提一下:
一般的有一定复杂度的网站可分为以下三层:
(1)数据接入层(DAL):负责与数据库的交互,供业务逻辑层调用
(2)业务逻辑层(BLL):调用数据接入层以获取数据,并为具体的业务需求提供支持
(3)用户界面层(UIL):负责呈现最终的用户界面
总之,分层以后,大大提高了代码的复用性与扩展性。
那么在嵌入式开发中,能否也利用分层的思想,来提高开发效率,增强其可维护性与可扩展性呢?下面,是一些笔者思考后的浅见。
当然不能照搬ASP.NET 的具体分层思想,具体问题得具体分析嘛~
首先,嵌入式开发的核心就是芯片,它提供固定的片内资源共开发者使用。而且它具有一个很重要的特点就是,不随项目的需求变动而变动。所以应将其作为最底层,为上层提供基础支持。我们将其命名为 硬件抽象层(Hardware Abstract Layer)。
芯片有了当然还不够,通常我们会在片外扩展一些功能模块来满足具体的项目需求,例如:传感器、键盘、LCD屏等。这一层的特点是,随项目的变动而以模块为单位动态增减。这一层的运作需要芯片内部资源的支持,所以应处于硬件抽象层之上,并为上层调用。我们将其命名为 功能模块层(Functional Module Layer)。
OK,现在原材料都准备齐了:芯片+扩展模块,接下来就要开始真正的加工了:我们需要灵活调用之前两层所提供的接口,实现具体的项目需求。我们将其命名为应用程序层(Application Layer)。
图文:
(1)硬件抽象层(HAL)
实现对片内资源 (如定时器、ADC、中断、I/O等) 的通用配置,隐藏具体的SFR操作细节,为上层提供简单清晰的调用接口。
嵌入式开发的核心就是芯片,它提供固定的片内资源(常用的有I/O,ISR,TIMER等,稍微好点的还有ADC,SPI等硬件资源,不需要芯片外围ADC采集芯片或模拟SPI)共开发者使用。而且它具有一个很重要的特点就是,不随项目的新增需求变动而变动。所以应将其作为最底层,为上层提供基础支持。
(2)硬件驱动层(HDL)
嵌入式开发基本都会使用片外资源,如AT24C02,W25Q128等常见的外围EEPROM芯片,需要SPI通信(硬件SPI或I/O模拟的SPI)发送相应指令驱动该芯片,实现该芯片能正常工作。因此驱动这部分的API函数实现程序即为硬件驱动层。即使换了MCU,也只需将调用过硬件抽象层的API函数替换即可。
(3)功能模块层(FML)
通过调用 HAL,实现项目中所涉及到的各片外功能模块,隐藏具体的模块操作细节,并为上层提供简单清晰的调用接口。
硬件抽象层和驱动层主要就是为功能模块层提供的,实现该项目需要的功能,比如KEY、LED和EEPROM等功能,其中LEY、LED基本调用硬件抽象层的API函数(更复杂的可能通过片外芯片获取/控制等,因此可能也需要使用硬件驱动层),EEPROM调用硬件驱动层的API函数,即使EEPROM芯片更换(AT24C02或W25Q128等),也不影响EEPROM之前编写含的功能代码程序(前提是AT24C02,W25Q128提供的API函数提供的是统一标准)。
(4)应用程序层(APL)
通过调用 HAL 与 FML,实现最终的应用功能。
负责的就是功能模块的使用和之间的逻辑关系处理等等,比如用户交互界面应用程序可能需要KEY、LED、LCD等。
四、硬件抽象层和硬件驱动层的主要区别
硬件抽象层使用的芯片内本身的资源(芯片手册都有介绍),而硬件驱动层使用的是芯片本身不存在的资源,而且需要编写相应代码才能实现的资源。
比如正点原子STM32中CAN使用的TJA1050芯片,CAN属于STM32的片内资源,TJA1050属于片外资源,但由于TJA1050不需要额外的代码就能通过STM32中CAN本身提供API函数正常 工作;因此可以认为TJA1050不属于硬件驱动层,而若使用TJA1041,则需要编写额外代码才能使正常工作才能使STM32中CAN本身提供API函数正常工作,因此可以将TJA1041归为硬件驱动层。
若不要分这么细,可以将硬件抽象层和硬件驱动层统一归为硬件抽象层。
五、功能模块层和硬件抽象层、硬件驱动层的主要区别
功能模块层是按照项目需求提取出来的功能,需要硬件抽象层和硬件驱动层的硬件支持才能实现,功能模块层根据项目的功能需求改变而改变,而硬件抽象层和硬件驱动层则是项目需求书中的功耗等硬件相关的需求变动而改变,当然,若子功能的增加而硬件不支持,则也需更换硬件驱动。
比如项目中的数据储存功能,硬件支持有AT24C02、W25Q128和芯片本身的FLASH,都可以支持数据储存功能,即使后期因为功耗或节约成本等问题,硬件的更换也不影响数据储存功能的实现(前提规划好标准规范的API函数定义)且避免了重写该功能代码所带来的各种问题,保证了该功能的稳定性。
分层结构示意图