论嵌入式单片机软件架构


这注定是一篇耗时很长的文章,做个标记,文章始于2019年1月8日,不知何时完结。
为什么会想到写这篇文章呢?
因为作为一个从单片机汇编语言开始,到单片机C编程、再到ARM实时操作系统、一步一步摸爬滚打走到Linux世界的嵌入式软件工程师,也有十二年了;而今主要在Linux环境下写嵌入式软件,我不想把单片机领域积累的知识就此慢慢遗忘,遂决定把我以往的知识做一下总结,提炼到一篇文章当中,这不是一件容易的事;写到这里,以下都不知从何写起,待我构思几天吧…

经过几天的回忆与思考,分析了我的成长路径,选择一些我自认为是成长关键点的位置,总结一个关键字,一步一步来讨论关于嵌入式单片机软件的架构。
我先把总结出的关键字写出来吧:流水式、中断前后台、任务式、状态机、模块、分层、封装,操作系统多任务;这些就是我总结出的关键字,写这篇博客我决定不参照其他博客,只把我心中所想以及以前的代码示例或者记录翻出来,再加修改,然后呈现出来,供大家品鉴。

下面就从我的第一份工作说起吧。

流水式

我的第一份工作是从PIC单片机开始的,使用汇编语言;当时刚刚毕业,在学校学的是51单片机,毕业设计使用51单片机做一个飞利浦的RC50013.56MHz的读卡器,那时对编程还处于朦胧状态,不存在架构的思想,就是按照功能一步一步的写。

所谓流水式,就是按照工作的流程一步一步实现,直到完成流程内的所有事情,最后再回到起始,从头开始。我用这样的思想写的第一份代码,是控制一个双向交流电机平移门,平移门就是开门与关门。流程如下:
在这里插入图片描述
由以上流程可以看出,我的代码就是按照这个流程一步一步堆砌出来的,哪里来的编程思想,这样的程序有哪些缺点呢?

  1. 会有很多位置检测相同的信号,例如遥控信号、限位限号
  2. 有多个位置执行电机控制动作
  3. 每次用软件信号抖动,都是直接的硬延时(所谓硬延时就是,直接delay,在delay过程中什么也不做)
  4. 在控制电机的过程不好扩展,功能也不好扩展(还好当时功能也比较简单)

这种简单的工作流代码堆砌,没什么好说的,工作流越长,逻辑越多,程序的分支越多,代码堆砌的越长。
呵呵,我这个代码可是配合硬件卖了一千多套呢!

中断前后台式

后来经过学习,接触到了中断与前后台编程方法;可是有的人说中断是前台,主程序是后台;有的人说中断是后台,主程序是前台;其实我觉得无所谓前台与后台;主要是程序被分为了顺序循环与异步中断两部分。那么就以我理解的前后台与中断来说一说吧。

  • 前台:程序主循环,除去while(1)前面必要的初始化部分外,主循环内完成程序的业务逻辑;至于如何完成业务逻辑、业务逻辑上有哪些是需要一些触发点、需要什么事件来推动业务逻辑的执行等,可由后台中断部分来设置触发条件、事件等。
  • 后台中断:由于中断的特点,发生时间是未知的,例如按键、遥控信号等;无法预知在什么时候发生,但它却是推动业务执行触发点。
  • 典型的由中断配合定时器可以很好地解决按键消抖问题,在按键按下产生中断后,设置并启动一个定时器,在定时器中断里面再次检测按键的值是否一样,就可进行抖动消除,同时又不占用主程序的执行时间。

举一个电机控制上例子:直流电机的软启动。什么叫软启动呢?直流电机在启动时,若直接给出额定电压,由于电机启动的瞬间属于堵转状态,会有很大的电流尖峰,有可能造成MOS烧坏,所以可以在电机启动时,不直接给出全部的电压,而是从%60的电压开始,经过一点时间慢慢抬升到额定电压(假设时间为0.5S);这也称为PWM调速。
要做到这样,需要在MOS管的驱动端给出PWM信号,这样的程序逻辑怎么由前后台完成呢?

  1. 首先前台主循环检测到电机启动触发信号之后,先设置电机的工作状态,同时开启一个PWM定时器,只不过此时PWM的初始占空比为%60。到此前台要做的事情就完毕了。
  2. 后台定时器中断,假设定时器中断为1ms,每1ms产生一次中断,向PWM定时器写入新的值,提高PWM占空比,直到在500ms处将占空比提高到%100,电机全速运行。

整个过程,前台主循环只负责将定时器开启,不具体执行电机的控制动作,剩下的电机开启以及PWM调速全部都在中断里完成。

有的人说,在中断里不能做的很复杂,不能做耗时很长的事情,基本上说的没错,但是有一些应用却并不如此,例如使用MSP430系列单片机完成超低功耗应用,主程序就是休眠的,在中断里完成业务逻辑也是可以的。

随着程序的功能越来越多,逻辑越来越复杂,简单的前后台也不足以胜任,但是以后的软件框架都要基于前后台,不可能再次脱离此结构,只不过在程序主循环与中断上做的文章也越来越多而已。

任务式

当嵌入式系统的功能越来越多,我们肯定不想把所有的代码全部都简单堆积在while(1)中,超过500行的代码,简直都没法阅读,逻辑也不清晰;因此我们会想,程序都有哪些功能呢,能不能把保业务逻辑分一分,哪些事情是相对独立的,划分出不同的任务来,由这些任务组合去完成整体的功能。
假设一个小型的嵌入式系统有以下功能:

  • 液晶显示(点整或段式液晶):显示数据内容、参数等信息
  • 控制操作:执行电机控制、继电器的开关等操作
  • 按键菜单:操作按键,在液晶上显示菜单、修改参数等
  • 数据存储:存储必要的运行参数、用户信息等
  • 命令接收与处理:接收串口的命令,处理后恢复,并根据相应的命令执行动作,如参数修改、电机控制等

那么我们应该怎样设计嵌入式软件的框架呢?我总不能把这些事情全部堆在主循环代码中吧?

其实根据以上列出的几点,可以看出,这些功能各自要完成的事情相关性不大,没有必要全部交叉在一起;很自然的就可以想到,将程序框架划分为这几个任务,每个任务各自做好自己的事情,不代劳、不偷懒,再利用前后台的思想,设置一些中断、全局变量,做好任务之间事件的触发、信号的传递、数据的衔接与交换,可以用一个框架图来表示如下:
在这里插入图片描述
注意:以上图中有的箭头是双向的有的是单向的,那么设计这个框架的关键点以及需要注意些什么事情呢?

  • 任务的确定:一个框架到底要划分出哪些任务呢?首先我们要从宏观上对嵌入式系统的整体功能有明确的了解,弄清楚业务逻辑,然后根据功能与业务逻辑抽象出具体的任务、最后设计任务之间的数据交互的接口,基本就设计好一个框架了。
  • 任务职责的划分:每个任务要做的事情必须分清楚,不能少做,也不能多做,任务与任务之间的的接口最好只是数据的交换,好的任务划分使任务要完成的事情是单一的、明确的、逻辑清晰的,这样可以降低任务之间的耦合性,在功能扩展与问题定位时会很明确。
  • 数据接口的定义:确定好任务之后,最重要的就是数据接口的定义了,这实际上涉及到具体的应用细节了,不过我们可以采用一点面向对象的思想,将数据进行封装,对每一件事物设置一个数据结构;明确各任务之间数据交互的对象与规则,例如谁生成数据、谁使用数据。
  • 框架之下是细节:对应具体的任务,要具体的分析;具体设计某一个任务怎么实现,我们需要抓住这几点:完成任务需要的资源、任务由谁触发、任务执行的条件、任务的输入是什么、任务的输出是什么、谁来接收任务的输出;抓住这几点任务实现就已经很明确了。

在以上示例图中需要说明的是,有些情况下,可以不设计单独的数据存储任务,而是提供简单的存储器的读写函数即可;但是当涉及的存储器操作比较频繁,且类似于SPI或IIC接口的Flash时,因为写操作会耗时,因此最好不要再写函数中使用delay方式,进行硬延时;设计一个存储器管理任务,统一管理数据的读写,在任务内部完成读写操作,而向外部提供读写操作的数据接口与触发条件。

关于段式液晶的显示,我不得不说一说我的一个心得。

段式液晶显示一般都是一个液晶驱动芯片,来驱动4(COM)*32(SEG)=128段液晶显示,芯片内部有一个RAM区,来对应128的显示与否。
那么这个显示任务应该怎么实现呢?
首先关于芯片的寄存器读写就不说了,我们主要关注实现显示任务的环节。
根据以上示意图可以看出,需要从显示数据缓冲区中取出数据然后显示,但是基本上显示的数据内容并不能完美的匹配到液晶的显示段上,总会存在差别;那么我们怎样由要显示的数据内容,转换到液晶显示段呢?那就是增加一个显示数据内容到显示段的映射过程。那么整个显示任务可以用一下几步完成:

  • 显示任务从数据缓冲区中取出数据
  • 映射过程将数据内容映射到映射数据缓冲区
  • 由芯片读写函数将映射数据缓冲区数据写入驱动芯片。

那么当一些显示段需要闪烁怎么办呢?
其实液晶显示任务,不必要实时向芯片写入显示内容,只需要每250ms或者500ms,向芯片写入一次数据即可,这样我们可以对需要闪烁的显示段做标记,然后按位取反即可完成闪烁的目的。
其实我还实现了没有液晶驱动芯片,全靠代码实现4(COM)*32(SEG)=128段液晶显示需要的驱动波形输出,只不过这个代码随着我的电脑被偷而消失了,这个代码我当时完成后,成就感可是很高的啊。

这里说的任务式编程框架,还没有对任务的实现方法做具体的讨论,简单的任务可以将代码堆砌在一个函数中完成,那么复杂的任务怎么办呢?下一节我将说一说任务实现的思想:状态机。

状态机

状态机,我还是在学数字电子的时候,设计过一个自动售货机的状态机实现,那时觉得特了不起;后来在编程的世界里,又一次让它武装了我的编程思想。
什么是状态机呢?
用数字电子的术语来说就是:有限状态的无限循环,在代码世界我们可以说,完成一个任务就是在不同状态之间切换,每个状态做一点简单的事情,多个状态联合完成一件复杂的事情。

这里我要说到一个思想:分解,所谓分解就是将复杂的事情简单化,将一个复杂的任务用递归法一步一步分解,直到不能再分解为止,由每一小步完成一点事情,最终合起来完成这个复杂的任务。

说起分解,其实在任务式编程中,就已经用到了分解的思想,只不过不够细致;细致的分解需要我们做到,首先从宏观上理解系统的功能,然后划分宏观的任务;然后在每一个任务上在细分子任务、子子任务、直到任务的每一处细节,最后设计数据结构与变量,将任务的细节串联起来;这就叫所谓的面向过程编程吧。

再说任务被我们分解之后,我们怎样表示这些被分解的步骤呢:状态
我称每一个被分解出的步骤叫做:状态,同一步骤下再细分的步骤称为:子状态;所有这些状态的联合,称为状态机。

设计状态机的要点:

  • 状态机的状态始终是有限个数的。
  • 状态机的状态是收敛的,即状态永远是可以循环的,不存在未知的状态
  • 状态机内每一个状态的进入与退出条件是明确的,不能存在状态内的死循环
  • 状态机内的每一个状态都必须存在上一状态与下一状态,可以有多个上下状态
  • 单一状态内,完成的事情不能过多,耗时很长;否则就要将此状态继续分解为多个子状态

使用状态机编程后,代码的特点

  • 状态机编程,基本上在程序的主循环将看不到任何硬延时,到这个阶段我们也不允许代码中出现任何硬延时
  • 由各种状态以及子状态编写出每一个函数基本不会超过200行;我们可以此为标准,当一个函数的代码需要超过20行时,会增加阅读的难度,可以增加子状态,在子状态中分步完成。

我们以GPRS模块的管理为例来说明一下状态机编程吧!
管理一个GPRS模块需要完成以下事情:

  • GPRS模块的开机
  • SIM卡识别
  • 网络注册
  • 建立网络连接
  • 网络数据收发
  • 短信的收发
  • GPRS模块关机

实际上以上列出步骤,就可以作为GPRS模块管理的基本状态,具体到每一个状态下,再细分到每一个条AT指令作为一个步骤,这样我们就可以在GPRS模块管理任务中无任何延时等待的情况下完成。
状态机中的延时等待如何完成:这需要借助定时中断与状态机数据结构,在定时中断里完成计时操作,状态自行检查是否计时完毕。

不得不说一下这个定时中断,定时中断不能做太多的事情,而是在定时中断里设置一个标志位,表明发生了定时中断,在程序的主循环设置一个定时中断任务,检测这个定时中断标志位,确定是否执行定时中断任务。到这里,我们发现,其实中断前后台、任务式、状态机,这几种编程方式在这里就都用到了。

最后要说的一点是,再借助一点模块与封装的思想,设计一个数据收发与短信接口和相关的数据接口与变量,将其封装为一个接口,使用信号通知方式,外部任务就可以很好的通过这个接口完成数据与短信的收发,而不用关心其细节。

模块、分层与封装

随着嵌入式系统功能越多,平台体系越来越复杂,代码文件也越来越多;我们不仅需要好的编程方法,也需要好好管理我们的代码文件;我们还需要借助一点面向对象的编程思想,来助力我们设计更好的框架。那么模块、分层、封装这三点就可以很好的帮助我们。

模块

什么是模块呢?有时候模块与任务很难分清楚。

  • 从代码层面来说,当完成一件事情只需要一个任务时,那么模块与任务就没有多大区别,可以称为单任务的模块;当完成一件事情需要多个任务时,我称这件事情为一个模块,也称为多任务模块,所以说模块处于任务的上一层,在宏观一点的层面上表述一个功能,也叫作功能模块。
  • 从文件结构组织上说,一般将这个功能模块所包含的代码文件组织在一起,放在一个文件夹下方便管理,使得文件结构清晰,易于维护。

分层

这个概念我想大家都很熟悉,最常见的就是ISO网络7层结构,以及经典的5层TCP/IP架构了。可是为什么要分层,怎样分层,分层可以为我们带来什么好处呢?

  • 为什么要分层:宏观上讲,分层可以让代码的业务逻辑更加清晰,每一层专注于自己事情,有利于任务的分解,设计出更好的框架
  • 怎样分层:对于分层我们怎么确定要分哪些层呢?其实分层,就是我们在对任务进行划分并分解时,可以看看这些任务中有没有一些共性或者相同的操作,我们把这些相同的地方提取出来,单独作为一个模块,让这个模块为那些任务提供操作支持,那么这个模块在经过封装后,就可以作为一个层次存在;所以层次就是经过封装的模块;最简单的嵌入式系统可以分为:驱动层、应用层。
  • 分层的优点:分层是一种任务的抽象,它可以将众多任务中相同的功能代码集中在一起,单独作为一个抽象层,为其它任务提供服务,这样可以减少重复性的代码,提高代码的利用率;另外分层使逻辑层次更加清晰,问题的定位、功能的扩展与维护更加容易

其实分层之后,我们更倾向于把每一层叫做一个模块,所谓模块化编程,就是靠分层实现的。

封装

什么是封装呢?一般我们说一个层次,必然有它的上一层与下一层,那么它怎样与上一层和下一层打交道呢?一个层次完成一些事情,它需要什么资源、触发条件是什么,什么时候完成,完成之后输出结果给谁?

对于每一层,可以这样总结,它对下一层提出请求(request),下一层对上接收请求(request),处理(process)这个请求要完成事情,然后对上层给出一个应答(reponse),

这样我们对每一层要做的事情可以总结为:请求(request)——>处理(process)——>应答(reponse)

那么我们怎样完成每一层之间的衔接与交互呢,封装便出现了。我们将每一层完成各种任务所需要的资源、触发条件、输出结果等,抽象成各种不同的数据结构,用于不同层次之间的交互,层次对外提供这些可支持的数据接口,这就是封装了,封装可以让我们屏蔽每一层的细节,减小任务之间的耦合性。

架构示例

就以我比较熟悉的ZigBee网络为例,来说说模块、分层与封装吧。以下就是一个ZigBee节点的网络框架图,看似简单的4层结构,实际就是对ZigBee网络规范的提炼。
在这里插入图片描述
是不是与TCP/IP的网络分层很像,我就是通过它,在学习TCP/IP的网络时,才有了豁然开朗的感觉。
根据ZigBee组网的规范,将它分为物理层、MAC层、网络层、应用层,并将每一层提供的功能封装成一个一个的原语,例如物理层的数据收发原语、MAC层的信道检测原语,网络层节点之间数据的传输原语、应用层网络请求原语等等,下面简单说一下每一层具体的任务以及功能吧。

  • 物理层:负责物理信道的管理,它接收MAC层的数据发送请求,将RF数据通过无线信道发送出去,同时实时侦听信道上数据,并将接收到的数据上报给MAC层。
  • MAC层:负责信道检测、信标帧发送、邻居节点数据可靠传输的功能,主要完成802.15.4标准所要完成的事情;
  • 网络层:负责网络的建立、路由存储、节点间数据传输等功能
  • 应用层:负责发起网络建立、完成不同节点业务上的功能,另外还需要完成网络管理上的一些功能

每一层要完成的任务,经过提炼之后,利用状态机思想,进行任务分解、分解后的每一个状态执行都对应设计好的原语,由原语数据完成信号与数据的传递,推动每一层所要完成任务的状态机的执行。

我这里再列出2个小问题,以及解决办法:

  • ZigBee网路上的重复帧如何解决
    什么是重复帧?ZigBee网络节点与节点之间的路径不是唯一的,因此存在同一条数据,同不同路径到达目标节点的可能,那么怎样滤除这种重复帧呢,又由谁滤除呢?
    首先我们需要确定重复帧的标志:当源地址、帧序列号都相同时,基本就可以认为这是一个重复帧;
    根据分层任务的规则,物理层只负责数据的收发,并不判断数据的内容,因此物理层不合适
    当数据帧到达MAC层,由此可以解析出数据帧的源地址
    我设计一个可以存储32项数据的队列,队列里的每一项都保存帧的源地址、帧序列号、帧age续存时间、帧校验和
    这样当队列为空时,直接将帧相关信息提取出来,保存进队列即可,
    若队列非空,则以此比较队列中有效数据项,查看是否存在重复帧,是则可以丢弃此帧数据,否则可以按照先进先出的原则,从队列剔除一项数据,然后保存新接收的帧信息
    当长时间未接收到数据时,按照age续存时间,age时间期满,则踢出队列

  • 关于串口驱动的封装
    以上示例图中的框架,仅仅是关于ZigBee网络的框架,放在整个嵌入式系统中,它只是其中一部分,在很多网关应用中需要使用很多串口、例如蓝牙模块交互、GPRS模块交互、调试串口、红外串口、维护串口等,可能涉及到6个及以上串口,有些串口使用固定协议帧格式,有些则可能使用多种协议帧格式、有些则可能没有帧格式,我们怎样对串口进行驱动封装能?
    所有串口的驱动合集,称为串口驱动模块,我们需要在驱动模块内部再分层,首先我们要将串口的接收与发送分开,然后分别对发送与接收进行处理。
    在这里插入图片描述
    接收部分:很容易想到,每接收一个字节产生一个中断,但是如果采用这种中断方式,6个串口同时接收数据,根据波特率的不同,在接收数据短时间内产生大量中断,这样可能造成某些串口中断无法响应,接收溢出,数据丢失,对整个系统来说,中断也会偏多,如果这是某些还口还需要发送数据,那中断就更多了,系统中断负荷会很大。
    这时我们就需要使用DMA进行串口数据接收处理了,可以这样设置DMA接收:设置DMA每接收到128字节产生一次中断,然后根据串口的波特率,设置一个定时器,若在规定的时间内未接收到128 字节,则将此次DMA接收到的数据拷贝到帧缓冲区,再次开启DMA接收与定时器。这样可以大大减少系统产生的中断,减小中断负荷,同时将数据接收与帧判断分隔开。
    发送部分:自然我们也不要使用每发送1个字节,产生一次中断,而是使用DMA发送,当DMA放完毕时,产生一次中断,然后在DMA中断服务中开启串口发送完成中断,直到串口发送完成中断,代表这一次的数据发送完毕。为什么要在DMA中断服务中开启串口发送完成中断呢?应为很多情况下,DMA发送完成,只是表示DMA吧最后1个字节发送USART外设,这1个字节还需要串口发送出去,如果此时关闭发送,则可能最后1字节没能发送出去。

到这里,在我的认知上,中断、任务、状态机、模块、分层、封装,可能就是无操作系统的嵌入式编程终极大法了;关键就要看怎么使用这些方法了。
下面就要关注,在带有操作系统的应用环境中怎么构建嵌入式软件框架了,我比较熟悉的是UCOS-II,所以就以它来讨论吧。

uCOS-II操作系统多任务

到此我们终于到操作系统了,其实我们前面总结的任务、状态机、模块、分层、封装,它们已经可以组合成一个简单的多任务操作系统了,因为它们之间也有任务的调度、任务之间的同步、任务间的通信、信号传递、互斥锁,消息队列等等,但不能称之为实时操作系统。
那么什么是实时操作系统能?
所谓实时系统,就是对执行的事件必须在规定的时间内完成,超出时间限制则结果就无效了。

但其实在一般的嵌入式系统中,没有这么硬性实时性要求,我们更关注的是嵌入式操作系统其它特性,以及怎样利用操作系统为我们提供的各种功能,设计更好的框架,但是框架的设计思想,不外乎就是以上列出的任务、状态机、模块、分层、封装这几点了。
那么uCOS-II都有哪些特性呢?

  • 任务调度方式:我们要明白一个操作提供的任务调度方式,是抢占式还是非抢占式,uCOS-II属于抢占式调度,高优先级任务可以打断低优先级任务,比如在执行一些延时、信号传递、中断发生时就会发生任务调度;其实无操作系统的多任务,其任务调度可以说是非抢占式,只能有任务自己主动退出,其它任务才能执行
  • 任务之间的同步:几乎所有的操作系统都需要通过互斥锁、信号量实现任务的同步,我们主要注意任务之间不能形成互锁,造成死锁;其实无操作系统的多任务,也需要我们自行设计一些互斥锁、信号之类的变量来完成任务之间的同步。
  • 任务之间的通信:uCOS-II主要依靠消息邮箱、消息队列实现任务之间的通信;除此之外我们还可以自行设计一些全局变量,利用uCOS-II提供的信号完成任务之间的通信,其实这就是无操作系统的办法,只不过uCOS-II在传递信号时,就可以发生任务调度,而后者只能等待任务退出后,轮询到信号所传递到的目标任务
  • 任务优先级:uCOS-II支持任务预先设计优先级,并且高优先级任务可以打断低优先级任务,任务的优先级还可以临时性的修改,而无操作系统的就没有任务优先级一说,每个任务只能等待轮询到之后才执行,因此我们可以利用系统提供的功能设计出实时性更好的框架
  • 系统消耗的资源:uCOS-II系统支持每个任务单独设立私有的栈,这时我们就要考虑,整系统硬件的资源,主要是RAM空间资源,为每个任务分配多大的栈stack空间才能保证任务栈不会溢出,预留多大空间作为堆heap空间,全局变量占用多大空间;当我们在程序中不使用动态空间分配函数时,可以通过链接文件将heap空间设置为0,而无操作系统的方式,除了因程序嵌套调用需要使用一部分栈stack空间外,剩余的则都是全局变量和heap空间,同样我们可以通过链接文件将heap空间设置为0。
  • 系统的移植:要想移植一个uCOS-II系统,那么必须对目标芯片的建构由清晰和深刻的理解,至少要明白CPU的架构、中断向量的分配、CPU寄存器的用法,以及基本的汇编知识,本篇文章不是介绍操作系统怎么移植的,所以就不做介绍了。

从以上可以看出,中断、任务、状态机、模块、分层、封装,已经有了绝大部分操作系统所有的功能,只不过特性不一样,操作系统也并没为我们设计软件框架,它只是在更高层次将我们需要用到的中断、任务调度、任务同步、任务通信、资源访问等进行了抽象,写成一个系统,供我们使用,我们利用系统提供的功能更加方便,但仍然要使用状态机、模块、分层、封装设计思想,设计嵌入式软件架构。

随着工作年限的增长,渐渐形成一套我自己的做事方法,就嵌入式软件来说,不管遇到的是简单的系统还是复杂的系统,要完成这件事情,总是存在两个方面:宏观与细节
宏观上,我要对系统有个整体的把控,理清系统的方方面面,这样才能更好的划分整个系统,设计它的任务、层次、模块;这个过程就是框架的设计过程,这个过程不可能一次完成,中间必然要经历多次修改,所谓宏观就是框架
细节上,当我们设计好一个框架之后,就要设计怎么实现框架所描述的内容了,比如任务实现的步骤、数据结构、变量,接口等,在细节实现上,可能会发现框架设计的不合理之处,这时反过来就要去修改框架,如此反复几次,框架与细节才能更好的吻合。

用一句话总结:自顶向下逐层设计宏观框架,然后自底向上逐层设计细节实现,将宏观与细节完美吻合,这样我们才能对系统了如指掌。

编程规范

在框架之外,我还想说简单一下嵌入式编程一些规范上的事情,内容我也说不出来很多,只是写出我自己的一点心得。

  • 文件组织:文件组织按照功能模块,放在不同的文件夹,文件夹的名称要与文件夹内容符合,多人协作开发时,可按功能模块各自维护自己的文件夹
  • 文件包含:我们在定义与放置头文件时有两种方式,1、将所有公用头文件放置在一个文件夹内 2、按照功能模块将属于此模块头文件与其代码文件放置在同一文件夹内;另外在头文件设计上我们还要避免交叉包含,如果实在避免不了,说明功能模块划分不够彻底,还要细分。
  • 命名规范:命名规范说起来也是一门学问,涉及到文件名、函数名、变量名、其它类似宏定义名等;总的来说是采用windows的驼峰方式,还是Linux的小写加下划线方式,这个各有所好,说不好孰优孰虑;我们更应该关注的是名称本身。好的名称必然是见名知意的,不要让阅读者需要根据上下文来猜测用途,见名知意的命名方式会让我们的后续编码、维护大为受益
  • 语句表达式:不要写逻辑复杂、不易理解的表达式,这样的代码可阅读性差,从代码执行角度说,逻辑复杂的表达式,执行效率也不会很高,我们应该做到,将复杂的事情简单化
  • 代码行数:单个函数的代码行数尽量不要超过200行,也就是两屏所能包含的行数,单个函数代码量行数越多,要完成的事情越多,逻辑越复杂,出错的可能性越大,说明任务的分解不够完善
  • 函数扁平化:当一个函数内部完成一件事情,需要的条件很多时,我不倾向于根据条件逐层推进,这样代码嵌套很深,而应该先判断条件不满足,即可返回,这样函数内的嵌套层次就更加扁平化,易于阅读与理解。
  • 代码注释:说起这个,众说纷纭,我的说法是:如果前面的几条遵循的好,写出的代码将是自己对自己的注释,代码本身就是一个英文语句,我们设计好函数名、数据结构名称、变量名称,当我们逐层推进时,它就是一条语句,不用费心去写注释,而更应该关注的是我们的框架设计文档、以及对这个框架设计细节的描述、任务如何分解的描述、任务状态机的描述、分层的描述、封装的描述、最后的最后就是实现细节的描述、将代码与文档对应起来,言行一致,文档才是最好的注释。

好了,到此关于单片机的嵌入式软件架构讨论,我能总结出来的也写完了,当然了嵌入式软件架构远不止我说的这些,每个人的成长路径不一样、得到的感悟也就不一样,希望看到这篇文章的大牛们,可以多多的评论此篇文章,指点一二,使我可以从你们那里学到我不曾接触的知识,开阔我的眼界;我们的追求就是,在有限资源的嵌入式世界里,写出优雅的代码。
本文完结于2019年1月23日。

猜你喜欢

转载自blog.csdn.net/zhaoyun_zzz/article/details/86093744