嵌入式架构有多重要?
保证嵌入式应用的代码逻辑清晰,
并防止重复造轮,
没有好的应用架构怎么办?
如果没有良好的结构,
移民将会是一件非常郁闷的事情。
如果没有良好的结构,
重用是最大的困境,
不可能更大程度地复用原来的代码。
如果没有良好的结构,
一旦更换了驱动程序,
每一处都需要改变
费时、费力且容易出错。
如果没有良好的结构,
硬件驱动层的代码穿插在应用层网站内嵌应用程序设计,
将会变得一团糟
逻辑不清楚,
代码维护可能很困难。
只有搭建了嵌入式框架,才能展现出编程之美……
01 嵌入式系统基本结构
嵌入式系统通常由软件和硬件两部分组成。 嵌入式处理器、存储器和外部设备构成了整个系统的硬件基础。 嵌入式系统的软件部分可分为三个层次,即系统软件、支持软件和应用软件。 其中,系统软件和支撑软件是基础,应用软件是最能体现整个嵌入式系统特点和功能的部分。 。
硬件架构
嵌入式系统的核心组件有多种类型:
(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。 (在整个应用程序中调用驱动层API会导致应用程序中到处可见驱动程序调用,无法最大程度地移植和复用)
首先定义应用程序的功能模块,并对整体结构进行分层,然后设计具有独立功能的模块(例如算法模块、文件库模块、通信库模块),并在模块之上开放公共套接字。
驱动层提供公共socket供下层调用。 每个功能模块可以独立编译(例如算法模块是纯ANSI C,可以在任何平台上复用),也可以调用驱动层socket(文件库模块调用驱动读写Flash)。 可重复使用的功能模块。
总体要点:硬件驱动层-->功能模块层-->应用socket层-->业务逻辑层-->应用层
整体结构框图:
应用层是程序的整体运行框架,组织业务逻辑的调用。 嵌入式操作系统可以完成多项任务。 如定时任务、卡片处理任务、菜单任务、通讯任务等。
业务逻辑层,如CPU卡处理、交通部卡处理、银联卡处理、M1卡处理、通讯记录上传、黑名单下载、票价参数下载等。
应用套接字层提供公共API套接字,供下层调用应用套接字。 这些套接字也可以由上层的功能模块打开,由应用套接字层负责汇总。
功能模块层可以封装不同的功能模块。 如算法库、文件库、通信库、银联库,向上提供应用socket层的socket,向下调用驱动socket。
硬件驱动层由各种驱动模块组成,向上提供统一的socket。
遵循一些约定:
1、各模块提供的插座必须统一,只能以后添加,不能更改原有插座。
2、模块之间相互独立,互不影响。 它们之间不能互相调用,只能调用上层的socket。
3、层由模块组成,层与层之间不能跨层调用。 比如直接调用驱动层的代码在应用层是看不到的。
4、模块可以继续分层,比如socket层、驱动层、硬件层。
如果驱动发生变化,或者换了不同的平台,只需要修改驱动层,应用层不会受到影响。
如果功能模块发生变化,只需要升级功能模块,其他模块和应用层不受影响。
按照这些逻辑进行设计后,主要工作就在业务逻辑层。 应用层是程序的整体流程和框架,主要调用业务逻辑层来实现不同的功能。
我们现在的代码结构基本上就是基于这个思想。
硬件驱动层-->功能模块层-->应用套接字层-->业务逻辑层-->应用层。
看看下面两种风格的代码,你更喜欢哪一种。
另一种风格:
同样是保存参数,难道要拆成AlgCRC16、WritePraFlash((unsigned char *)&NetPra, NETPRA_ADDR, sizeof(_NetPra))两步吗?
还有AH_Para_Verify,在应用层确实是多余的。 如果检测失败,则从Flash中读取。 对于参数,开机后应立即检查其合法性。
由于要保存所有参数,所以要打一个包,如上图所示,为系统中使用的不同参数做好规划。 应用层调用APP_Open_UseFile或APP_Read_UseFile,
而不是直接读写Flash。
我们来看看著名的微软的android架构。 虽然它非常复杂,但从框图上看,它就像积木一样。 每个功能模块都是独立且定义明确的。 最底层是在linux Kernel的基础上构建的,然后是各种组件库,然后是应用框架和应用程序。
以NC_FileLib文件库模块为例,如果想在其他平台上使用,比如EH0918手持设备,只需要移植几个硬件层套接字即可。
休息一下,推荐一篇文章《》
03
一个警示性的例子
1. 错误的例子
最近公司招聘了一位新朋友,是从事嵌入式软件开发的。 这位同学来自北京一家上市公司。 由于人手不足,他被安排负责新产品的开发。 前期让他负责加速度计、NB-IOT、舵机、外接Flash的功能测试。 测试完成后,我打算请他做产品的总体设计。 然后他花了两周的时间为我们写了一个概要设计。 说实话,当我听到这个大纲设计时,我以为这是一个刚毕业的大学生写的。
第一版架构设计
2.1 系统架构
系统分为两层:硬件驱动层和应用层。
2.1.1 硬件驱动层
硬件驱动程序层包含板载硬件资源正常运行所需的所有驱动程序。
1)单片机初始化
2)I2C数据访问
3)SPI数据读取
4) 加速度计初始化
5)蓝牙模块启动
6)BC95模块启动
7)485通讯模块启动
2.2.2 应用层
1)MCU运行模式切换
2) 振动和倾斜
3)数据分析
4) 开/关锁
5)数据发送
6)历史数据保存
看到版本1的架构设计后,说实话,我还是第一次看到这样写的架构设计,而且居然是用序号写的。 这对于其他人来说读起来特别尴尬。
第二版架构设计
看到版本2的架构设计后,虽然颇为沮丧,但我觉得距离达到我们的要求还有很长的路要走。 架构设计主要存在以下问题:
1.对架构的理解不是很清楚。 既然是做架构设计,就应该把它看成一个整体,而不是仅仅局限于某个模块或者功能。
2.各个层次的理解还不是很清楚。 例如,MCU的初始化就归属于硬件驱动层。 严格来说,MCU的初始化是进程的一部分,而不是驱动程序。 比如笔记本的启动,把这个归咎于硬件驱动,肯定是错误的。
3、另外,各个模块的启动不能属于硬件驱动层,也是业务流程的一部分,不应该属于驱动层。
4、还有总线数据的读写。 虽然驱动程序的作用是读写,但是数据总线的读写不能写成硬件驱动程序。
5、应用层的系统参数初始化仍然属于进程。
6、数据的分析和数据的生成属于通信功能,不应该是独立的,属于单个应用程序。
2. 修改版基本框架图
(一)架构设计的目的
1、应用代码逻辑清晰,防止重复造轮子。
2、如果没有好的架构,移植将会是一件非常痛苦的事情,所以好的架构设计方便软件移植。
3. 最大限度地重复利用。
4、高内聚、低耦合。
(二)设计思路
如何将硬件驱动程序和一个功能封装成单独的模块,然后就可以像孩子一样搭积木,模块可以快速拼接在一起,形成不同的模型。
我们的嵌入式架构思想也是源于此,即功能模块化设计和分层设计。
这种设计与WEB开发的MVC模式类似,都强调分层设计。
模块化设计:对收集到的需求进行分类、归纳、分析,将需求归纳为各个功能,将每个功能做成独立的功能模块。
分层设计并不容易用一句话直接表达出来,主要表现在以下几个方面:
1、将功能模块外部调用的模块封装成API,将底层驱动做成API供功能模块调用。 (每个功能模块可以独立编译(例如通讯模块是纯ANSI C,可以在任何平台上复用),或者调用驱动层socket(日志库模块调用驱动读写Flash),总之一句话,反正把每个功能都封装成独立的可复用的功能模块。)
2. API分为驱动层API和应用层API,并不是所有程序都调用驱动层API。 (在整个应用程序中调用驱动层API会导致应用程序中到处可见驱动程序调用,无法最大程度地移植和复用)
一般分为硬件驱动层-->功能模块层-->业务逻辑层-->应用层
整体结构框图:
阐明:
1. 层不能跨层调用。
2、模块之间相互独立,没有依赖关系。
3、模块提供统一的socket供下层调用,模块内部和外部的socket定义明确。
4. 模块的功能只能添加,不能更改。
5、各功能模块层还可以进一步分层,如socket层、驱动层、硬件层。
(3) 模块级别说明
硬件驱动层
硬件驱动层包含板载硬件资源正常运行所需的所有驱动程序,并提供API供功能模块调用。
功能模块层
功能模块层包括实现具体功能的函数,通过调用驱动层API实现相应的功能,并向业务逻辑层提供可调用的API。
业务逻辑层
业务逻辑层包括产品整体功能的各个业务流程,通过调用功能模块层的API来实现。
应用层
应用层集成并调用各种业务逻辑来完成整个产品的功能。
(四)优势
如果驱动发生变化,或者换了不同的平台,只需要修改驱动层,应用层不会受到影响。
如果功能模块发生变化,只需要升级相应的功能模块,其他模块和应用层不受影响。
按照这些逻辑进行设计后,主要工作就在业务逻辑层。 应用层是程序的整体流程和框架,主要调用业务逻辑层来实现不同的功能。
04
给嵌入代码一层
1、遇到的问题
代码结构也可能存在缺陷:
(1)开发效率低:每次使用芯片中的某个资源(比如定时器等),笔者都要查询CC2430英文指南来比较蛋~
(2)代码重复较多:每个实验源码中,每次都要编译xtal_init、led_init等初始化函数
(3)不易更改:代码中的业务逻辑与SFR的操作混合在一起,可读性差,修改困难
正是因为上述问题,笔者决定暂停继续该系列博文,花时间思考解决方案。
二、网站分层引发的思考
笔者在学习嵌入式编程之前,有过ASP.NET网站开发的经验,也实践过其分层理论。 这里简单提一下:
一般来说,具有一定复杂度的网站可以分为以下三层:
(1)数据访问层(DAL):负责与数据库交互,供业务逻辑层调用
(2)业务逻辑层(BLL):调用数据访问层获取数据,为具体业务需求提供支持
(3)用户界面层(UIL):负责呈现最终的用户界面
总之,分层之后,代码的复用性和扩展性都大大提高了。
那么在嵌入式开发中,我们是否也可以采用分层思维来提高开发效率,增强其可维护性和可扩展性呢? 以下是笔者思考后的一些拙见。
3.嵌入式项目也分层
当然,我们不能模仿ASP.NET的具体分层思想。 具体问题还要具体分析~
首先,嵌入式开发的核心是芯片,芯片提供固定的片上资源供开发者使用。 而且它有一个很重要的特点就是不随着项目的需要而改变。 因此,应将其作为底层,为下层提供基础支撑。 我们将其命名为硬件抽象层。
光有芯片是不够的,通常我们会在芯片外扩展一些功能模块来满足特定的项目需求,比如传感器、键盘、液晶屏等。这一层的特点是以单位动态增加或减少随着项目的变化模块的数量。 该层的运行需要芯片内部资源的支持,因此应位于硬件抽象层之上,由下层调用。 我们将其命名为功能模块层。
OK网站内嵌应用程序设计,现在原材料都准备好了:芯片+扩展模块,接下来就要开始真正的加工了:我们需要灵活调用前两层提供的socket来满足具体的项目需求。 我们将其命名为应用层(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可以归类为硬件驱动层。
如果不是那么详细的话,可以将硬件抽象层和硬件驱动层统一为硬件抽象层。
5、功能模块层与硬件抽象层、硬件驱动层的主要区别
功能模块层是根据项目需求提取出来的功能,需要硬件抽象层和硬件驱动层的硬件支持才能实现。 功能模块层根据项目的功能需求而变化,而硬件抽象层和硬件驱动层则是项目需求。 书中的帧速率和其他硬件相关要求可能会发生变化。 当然,如果子功能减少了但硬件不支持,也必须更换硬件驱动程序。
比如项目中的数据存储功能,硬件支持AT24C02、W25Q128以及芯片本身的FLASH,都可以支持数据存储功能。 即使后期造成帧率或成本节约问题,硬件更换也不会影响数据存储功能的实现(前提是规划好标准规范的API函数定义),避免重画功能代码带来的各种问题,保证功能的稳定性。
层次结构示意图
推荐阅读(点击标题可跳转阅读)【编程之美】用C语言实现状态机(实用)10 个常用的软件架构模式学会读源码,很重要!