打印

单片机程序架构 --- 二层架构

[复制链接]
2649|14
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 会笑的星星 于 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),还剩下最后一个事情,那就是组织一下文件。这个很简单,没啥技术含量,如下图所示。

工程内的文件组织


工程外的文件组织


工程外文件,app文件夹内的内容


工程外文件,hal文件夹内的内容

二层架构虽然简单,但却实用,通过这样的有结构的组织程序,使得硬件层的复用性得以提高,并且整个代码的清晰度相比没有架构的程序
要好不少,这样有利于减少程序的bug。二层架构虽然也有不少好处,但是只实现了硬件层的抽象,而应用层并没有实现抽象,这意味着应
用层的代码过于耦合,并没有多少可复用性,要解决这个问题,我们必须要将应用层也分离出两层,一层是所谓的中间层,另一层就是应用
层,这就是所谓的三层架构,这会在后面讲到。


使用特权

评论回复
评论
hobbye501 2019-12-31 10:33 回复TA
很用心啊,知道咋写。说不出来 
评分
参与人数 1威望 +5 收起 理由
tianxj01 + 5 很给力!

相关帖子

沙发
会笑的星星|  楼主 | 2019-12-26 23:08 | 只看该作者

使用特权

评论回复
板凳
aerwa| | 2019-12-27 08:00 | 只看该作者
无私奉献,很少有人会这样细说。

使用特权

评论回复
地板
@若水| | 2019-12-28 12:01 | 只看该作者
在应用层封装成一个结构,再调用,会更方便,也更直观

使用特权

评论回复
5
@若水| | 2019-12-28 12:22 | 只看该作者
刚刚试了一下,C99标准编写时,更直观,C89有些格式不支持

使用特权

评论回复
6
kingTek| | 2019-12-28 14:26 | 只看该作者
应用层,bios层,还有个接口层,……想起多年前的那些日子

使用特权

评论回复
7
GZZXB| | 2019-12-30 22:23 | 只看该作者
在论坛都是广告满天飞的情况下,难得看到楼主有心写一些心得出来。 这些知识对于新手来说太重要了。

使用特权

评论回复
8
大黄蜂001| | 2019-12-31 09:21 | 只看该作者
谢谢,了解了

使用特权

评论回复
9
hobbye501| | 2019-12-31 10:31 | 只看该作者
这个就是通常裸奔的写法了

使用特权

评论回复
10
lihui567| | 2019-12-31 16:29 | 只看该作者
单片机程序框架写的非常好的,这个都是实战中应用的,好多初学者可以参考快速应用到项目中

使用特权

评论回复
11
老舍农夫| | 2020-1-3 16:34 | 只看该作者
学习了

使用特权

评论回复
12
gzliwangxing| | 2020-2-2 20:22 | 只看该作者
学习了。多谢!

使用特权

评论回复
13
andy520520| | 2020-12-22 17:47 | 只看该作者
写这样东西显得你无知!!!

ST提供了标准库的封装和便于移植的hal库,从硬件抽象到操作系统到应用层整个范本,国内很多单片机就模仿ST搞法

你是显得你写的比ST还好?

使用特权

评论回复
14
andy520520| | 2020-12-22 17:54 | 只看该作者
本帖最后由 andy520520 于 2020-12-22 17:55 编辑

估计你这货硬件抽象和应用层都不分开,什么app_ckl ,  app_led ,  app_key

难道不是这样不是应该是bsp_led,  bsp_key
玩了两天51单片机就飘了,你不知道自己是谁了吧?!!!

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

31

主题

96

帖子

17

粉丝