本帖最后由 会笑的星星 于 2019-12-27 15:04 编辑
在单片机程序设计中,在着手编程之前都应该想好一个大概的程序框架,有了一个好的框架,程序的复用性、可移植性、清晰度等等都能得到提高。那么, 在编程之前,如何设计程序的框架呢。为了说清楚设计方法,可以先从最简单的单片机程序架构说起,我把它叫做二层架构,如下图所示。
所谓的二层架构,指的是程序里面只分为硬件层、应用层两个层。硬件层顾名思义,就是与硬件相关的寄存器初始化、寄存器设置之类的功能, 比如定时器的初始化,I/O口的高低电平设置等。而应用层就是我们要做的产品逻辑功能。
乍看上去这种结构比较简单,却很实用。首先,硬件层从应用层剥离后,在你更换单片机时只需要更改硬件层的相关的程序,而此时应用层不用 变化(当然,根据情况也可能有少量变化)。其次,硬件层本身可以应用到其他项目中,增加代码的复用性,这无疑有利于你使用同款单片机做
其他的产品开发。
知道了二层架构的好处,现在来看看在单片机程序中如何实现这种架构。我们先来看看硬件层的设计。
硬件层主要封装硬件寄存器相关的操作,我把所有涉及硬件相关的代码全部放在了两个文件,一个为hal.c,用于存放函数实现。一个hal.h, 用于对外声明以供应用层调用。
硬件层主要分为三个部分,第一是硬件寄存器的初始化以及寄存器状态的获取的相关函数封装,第二个是硬件相关的中断,第三是硬件层的 对外接口,我分别来讨论。
首先,看函数的封装。
对于程序设计来说,为了确保代码的复用性,降低代码之间的耦合度并且使得程序结构更为清晰,设计函数时,必须满足下面两点要求: - 函数的功能要尽量单一
- 除非特殊情况,硬件层的函数中不要调用应用层的函数或者变量,更简单的说就是尽量不要出现下层调用上层的情况
按照上述两个原则,看看硬件层中函数封装的几个例子。
系统时钟初始化
单片机IO初始化
串口初始化以及相关寄存器的设置、访问
如上图中串口模块初始化所示,初始化函数hal_uart_init()的入口没有参数传递,这意味着串口模块的波特率被初始化为固定的9600, 如果调用者想修改为115200的波特率则无法通过参数传递的方式直接修改,而必须要修改这个函数中的相关寄存器(这里是_brg)。
这样做看似有点麻烦,但是并非不可取。因为串口初始化函数要支持波特率可选,必然要在该函数中根据函数形参进行一个乘除运算得出 相应的波特率(如下图所示)。而做比较复杂的乘除运算本身不是一些普通单片机擅长的,对于有些低端8位的单片机来说,在局部函数中 做乘除运算可能会占用全局空间,从而减少本身就紧张的RAM空间。当然,对于一些资源充足、性能强劲的单片机来说这一点是不用担心 的,通过函数的参数来自动完成波特率的计算是更好的选择,但也不一定非要如此,因为很多时候我们并不需要硬件层有这么好的通用性。 因此,我更倾向于在串口初始化时不传递参数
其次,来看看如何在硬件层中处理中断
中断函数与单片机是捆绑的,因此,中断最好也一起写到hal.c中,我一般集中写在文件的开头位置,以便后续好统一管理,如图1ms定时器 中断的例子
这里要注意的是,一般而言,中断中一般是要调用应用层的变量或者函数的,以便为应用层提供所需要的一些定时、串口数据接收或者发送 任务等等,这时会涉及到硬件层向应用层调用的问题。要处理这个问题,我们有两个办法:
- 直接在中断中调用应用层变量或者函数
- 通过回调函数调用应用层的函数
如果使用第一种方法,违背了我们之前说过的硬件层函数的设计原则二。如果我们需要选择第二种方法,硬件层与应用层可以很好的实现隔离, 这也是很多大型软件比如蓝牙协议栈所采用的方法 --- 底层通过回调函数的方式调用应用层函数,如此实现底层的通用性。
通过回调函数的方式实现下调上虽然想法好,但是在程序实现上往往比直接调用上层更为复杂且麻烦,一般而言还需要耗费更多的单片机资源, 有时候一些低端单片机甚至连函数指针都无法支持,这个时候根本就不可能使用回调函数。而且,对于大型软件而言,比如蓝牙协议栈,它由
于需要面向不同的开发人员以开发不同的产品,对于软件隔离要求是很高的,而我们自己设计的单片机程序相比而言对通用性则没有这么高的 要求。因此,综合来看,虽然使用第一种办法虽然违背了原则2,但是考虑到上述所说的各种原因,我认为很多时候使用方法1更好,不过调用
上层函数时依然需要遵循 --- 尽可能的少、尽肯能的简单。
最后,来看看硬件层的对外接口
硬件层的对外接口定义在hal.h文件中,这个文件声明了hal.c中所有上层会使用的函数或者宏定义,如下面几个图所示:
GPIO部分的宏定义以及函数声明
全局中断的宏定义
串口部分的宏定义以及函数声明
值得注意的是,我把相同模块相关的函数声明放在了一起,不同模块间的函数声明通过符号分割,使得整个文件组织更有结构,也更为清晰。
接口准备好后,应用层就可以直接调用了,如下图所示。
我们上面实现了硬件层代码(hal.c、hal.h),还剩下最后一个事情,那就是组织一下文件。这个很简单,没啥技术含量,如下图所示。
二层架构虽然简单,但却实用,通过这样的有结构的组织程序,使得硬件层的复用性得以提高,并且整个代码的清晰度相比没有架构的程序 要好不少,这样有利于减少程序的bug。二层架构虽然也有不少好处,但是只实现了硬件层的抽象,而应用层并没有实现抽象,这意味着应 用层的代码过于耦合,并没有多少可复用性,要解决这个问题,我们必须要将应用层也分离出两层,一层是所谓的中间层,另一层就是应用 层,这就是所谓的三层架构,这会在后面讲到。
|
很用心啊,知道咋写。说不出来