注明:文章转载于知乎专栏“单片机嵌入式软件开发经验分享”,跟大家分享优质内容。

1、单片机如何执行程序?

我原先对计算机是如何执行程序的非常的痴迷,希望能够明白最底层的道理,我在单片机这个设备上找到了一点影子。

对单片机执行程序的理解,我读了王爽老师写的《汇编语言》这本书, 然后结合自己的思考。

我认为

程序执行 = 不停的写不同的寄存器。

上一篇文章写道,嵌入式软件开发最一般的操作就是控制类似P0OUT这样与实际底层相关的寄存器。写嵌入式软件代码,实际上就是写一系列操作底层寄存器的语句。

一般会将P0OUT这样的寄存器映射到某一个地址上,这个地址是实际的寄存器物理地址。

在单片机通用的.h文件会给出他的物理地址定义。

例如,笔者使用的MSP430系列单片机,他在<msp430x32x.h>中就定义了各个寄存器的物理地址,此类文件一般由厂家提供。

#define P1OUT_ (0x0021u) /* Port 1 Output */DEFC( P1OUT , P1OUT_)

此段代码,定义了P11OUT关键词与0x0021u地址的联结。

那么,希望P1口输出全是高电平的C代码则为

P1OUT=0XFF;

那么汇编如何写呢?

# mov 00 ff 00 21

//把00ff数据移动到0021物理地址上

于是此时的底层硬件会将00 ff 放入00 21为地址的寄存器中,此时三极管动作,切换底层硬件电路,实现需求功能。

(嵌入式开发实际理解到汇编这一层代码即可。再往下,那就是非常深奥的电路原理,即电路是如何做到把 ff移动到0021地址寄存器的,以及寄存器是如何工作的,等等这样的问题)

嵌入式软件开发还有一个最一般的操作--------数值的数学运算

那么数值的数学运算是不是也是对底层寄存器的配置呢

通过数字电路的学习,我学习到了加法器等逻辑电路,其底层的运算原理已经能和寄存器的知识联结了。

在计算机中有一个叫“算术逻辑单元”的概念

算术逻辑单元_百度百科

这个单元算是把很多算术电路整合在一起了。我们可以通过操作寄存器来使用它。

例如 加法操作,a+b 以及返回结果 c

定义 a 的物理地址为 0x0000

b 的物理地址为 0x0001

C的物理地址为 0x0002

那么

#mov a 0000 //把数据a放入 0000#mov b 0001//把数据b放入0001#read c 0002 //读取0002寄存器的数据放入c中,这里的read指令是我瞎写的,汇编一定有这样的指令

于是c就是加法器的结果输出,得到输出,完成了计算。

嵌入式软件开发还有一个最一般的操作--------数值比较,参考数字比较器的逻辑电路原理

对应程序中的if操作,比较a与b的大小。

例如,if(a>b) 语句,或者 if(a)语句,实际都是把a和b放入数字比较器的两端,获得比较器的输出!

数字比较器

于是嵌入式软件最基础的三个操作(算术运算、硬件电路切换、数值比较

)都是对寄存器的操作。

所以我得出结论,计算机的程序执行,就是不停的写入寄存器数据,并且取出寄存器值得过程。

2、封装思想

我在理解了程序运行就是对寄存器的操作之后,紧接着我就想到,如何建立一个抽象的层面,让我不需要考虑,我这样的寄存器操作是实现怎样硬件操作,而直接达成自己的目的?

于是我想到了封装,工作两年多,持续增长的技能实际上就是封装的技巧。以后的分享中大多数就是对封装的思考。

这篇文章中仅举一个例子,来说明封装的意义。

上一篇文章中提到,跑马灯电路,当P0OUT的低四位为0时,灯会灭,当P0OUT的低四位为1时,灯会亮。

那么寄存器与实际效果的对应关系,我不想时刻记住,该如何去做呢?

于是想到使用函数封装。写下如下代码

typedef enum{Off = 0,On,}led_status_t;void open_led(led_status_t led_0 ,led_status_t led_1 ,led_status_t led_2,led_status_t led_3){uint8_t led_p=0;led_p |= led_0;led_p |= led_1 << 1;led_p |= led_2 << 2;led_p |= led_3 << 3;P0OUT = led_p;}

在任意时刻,我想点亮0号和3号灯则写

open_led(On,Off,Off,On);

此时函数完全与硬件无关,完全到了抽象层,使用此函数这,根本不知道其底层电路原理。实际上当做项目的时候,就算代码是你自己写的,你往往也会忘记底层电路原理的,封装之后,就不用记这些了!

作者思考

笔者理解的嵌入式计算机工作原理,就是不停的写各种功能的寄存器。于是想写好这么多名目繁多的寄存器,一定要挨个封装起来,用更加简明的函数名来标记,这样才能抛开底层电路原理对自己的束缚,完全的放飞自己的想象。

————————————————————————————————————————————

作者以压力开关为例,继续阐述单片机代码思想。

先前的文章提到,作者认为,嵌入式软件开发等效于不停地读写底层寄存器。因此作者意识到,想要写出优良的代码,必须具备封装的思想,将不同寄存器的操作,用简明的函数名来封装,这样可以在软件开发的过程当中,不在关注底层执行状况,专心的思考算法层面。

作者学习的方法是实践,任何知识都喜欢动手去操作,然后遇到问题通过网络解决,所以在分享自己的学习思考的时候,也想以一个工程实例来展开。

这篇文章,仅是一个开头。

分享的工程名 : 压力开关软件工程 (PressMeter Prj)

首先,一个良好的软件工程师,在开始着手构建代码之前,就应当思考其结构,因此,当我着手这个工程的时候,需要做非常多的准备工作,并且确定构架。

首先为读者简单介绍计量设备的硬件情况与功能。

压力开关是用来计量气压值,并且做出开关动作的设备。举个实用的简单的例子,例如,我希望某一个密闭空间保持特定压力值100KPA,那么我需要为这个密闭空间接入一个大于100KPA的气源,并且有一个机械开关,控制气管通断,此时将设备接入系统,当密闭空间的气压达到100KPA,那么压力开关输出一个信号,控制机械开关动作,关闭气源。这是一个典型的压力控制负反馈上的一环设备。

硬件结构,简单介绍一下,这一部分都是硬件工程师再弄。是压力传感器,把压力转化为模拟电压信号,并入单片机提供的AD口,三极管动作的控制信号,接入单片机的GPIO口,液晶显示屏,连接单片机,还有几个按键,连在GPIO上。与单片机有关的就这四块电路。

我们已经选好型,MCU采用MSP430,液晶屏控制器SSD1306,AD采用430内部的SD16。

因此,接下来要思考的就是软件构架,作者脑海里面有三个软件层级。

第一级 CSP(Chip Support Program),硬件层,是通过读MCU的DATASHEET,封装单片机各个寄存器的作用,方便调用,本工程封装了,CLK,TIMER,WTD,GPIO,FLASH,SD16这些功能,下文会展开举例。

第二级 DSP(device Support Program),硬件层,思考MCU通过GPIO或者IIC等协议与外部芯片通信的函数封装,例如MCU外部通过GPIO接入了一个液晶显示屏,那么这个液晶显示屏就属于DSP,因为这一部分设备不属于单片机内部,通常调用CSP函数。

第三级 算法层,针对不同的应用,调用CSP DSP代码,实现灵活的编程。

针对同一款单片机,作者从不写第二次CSP,因为完全可以多次重用,这样会提升工程的开发速度。

作者读过一些LINUX的源码,以及很多嵌入式软件高手的代码。脑海里面有操作系统的一些简单思想,所以,希望自己的代码,能够高效快速,实现类似操作系统的多线程功能,因此,在构建软件的时候,还区分了三个层面。这次是根据代码特点区分的三个层面。

第一层 硬件层,代码仅用一次,访问硬件设备用的,被上一级算法调用的层面

第二层 驱动层, 在主函数中反复被调用,是不断被重复的代码。

第三层 算法层, 最高层算法的堆砌

作者喜欢 handle 这个单词,经常在代码中使用这个单词,此单词的中文翻译是句柄。虽然我对最先提出这个概念的作者真正意图没有完全领会,但是通过其代码,加上一些自己的领会我总结了一些用法。

参看我的主函数的写法

单片机程序执行与封装思想

其中包括了 三个init 和两个handle ,init 函数仅在程序之初执行一次,handle函数在主函数中不断的被执行,至于handle里面都在做什么事?那就是我接下来很长一段时间想表达的了。写好handle 程序就写好了!

作者,想表达自己对这三个层级,三个层面的理解,但是却不能用一两句表达,但是这也正是我继续写下去的动力吧。思想决定了代码,以后的文章会渐渐表述确切的!

作者的软件结构图如下

单片机程序执行与封装思想

其中箭头代表的调用方向。

按照顺序,我会首先封装好硬件层的代码。

硬件层代码包含了CSP代码,和DSP代码,在此工程中,需要用到单片机的功能(ADC模块 GPIO模块 WTD模块 FLASH模块 定时器模块)以及外部电路连接的液晶显示屏控制电路模块(SSD1306)。

封装到怎样的程度我算满意呢?

贴一段我对GPIO的封装代码

typedef enum{P1 = 0,P2,}port_sel;typedef enum{out = 0,in,}port_dir;typedef enum{high=0,low,}port_v;typedef enum{falling =0,rasing,}edge_t;void set_port_dir(port_sel port_id , uint8_t pin_id ,port_dir dir);void set_port_v(port_sel port_id , uint8_t pin_id ,port_v v);uint8_t get_port_v(port_sel port_id , uint8_t pin_id);void set_port_edge_int(port_sel port_id , uint8_t pin_id,edge_t edge);void clear_port_flag(port_sel port_id , uint8_t pin_id);void set_port_inter_enable(port_sel port_id , uint8_t pin_id,bool_t able);

这是.h文件,意味着我提供了哪些工具给上一级。.c文件提供了函数具体的实现方法。这六个函数大体完成了所有需要的功能封装。

当然,至于怎样封装,有什么可以分享的技巧,以后的文章会继续写。

作者思考

有时候,我总是会做重复的工作,例如不同的项目,用的同一个单片机,却要重复写TIMER模块代码。我在思考,为啥不能重用之前的代码呢?于是,泾渭分明的封装开始变得重要了。什么代码与硬件(单片机型号)密切相关,什么完全无关,做好区分。如果一旦换单片机,与硬件无关的代码完全移植走,看都别看第二眼,完全修改CSP函数就可以了,例如第一篇文章写的open_led函数,就是与硬件无关的代码!使用者看到了,仅仅知道,这函数是开一个灯的函数,谁会想他怎样实现的?封装的时代,我们不会关心手机的原理,就可以开心的玩游戏。我们不会关心,为什么和恋人在一起,心里总会那么舒爽,但牵着她的手就好了。那么多复杂的奥秘,我们不需要完全理解,多么幸福的事情呢?

相关文章