龙空技术网

STC单片机特有的函数重入问题与解决方法

杨为民 825

前言:

此时朋友们对“按照c语言规定的用户”都比较看重,看官们都想要了解一些“按照c语言规定的用户”的相关知识。那么小编在网摘上网罗了一些有关“按照c语言规定的用户””的相关资讯,希望各位老铁们能喜欢,朋友们快快来了解一下吧!

一、 函数重入问题

单片机程序中有三种类型的任务:

(1)后台任务。指C语言的“main”函数中的无限循环部分,后台任务只有一个。

(2)前台任务。指中断引起的中断服务程序对应的任务。由于单片机有多个独立的中断,每个独立的中断都对应一个前台任务,因此前台任务也有多个。

(3)实时任务。特指在RTOS中定义的多个用户任务。

(4)任务现场。对于单核的8051单片机,任何一个时刻只能执行某一个任务,当中断发生时或者任务调度时,通常要先保留当前任务执行的“现场”,然后恢复下一个要执行的任务的“现场”,然后从新任务上次的中断点开始执行新任务。这个“现场”通常指单片机CPU的全部寄存器的值与系统堆栈(以SP为指针的堆栈)中的内容。

(4)函数重入问题。然而任务的“现场”可能不止只是寄存器,比如几个任务都调用了同一个函数(比如软件延时函数和像printf这样的库函数),如果任务调度时正好在执行这个函数,那么这个函数的参数和局部变量要不要也作为“现场”一起保存起来呢?这就是在单片机/RTOS程序设计中必须给予足够的重视的函数重入问题。

(5)函数可重入定义。如果一个函数如果不受函数递归时或者被中断时被重新执行过的影响,还能继续正常运行下去,称这个函数是“可重入的”,否则称为“不可重入的”。

(6)函数的递归调用。递归是一种很重要的计算方法。当一个函数进行递归时,函数从递归点开始又从头开始执行一次本函数,当函数退出又回到递归点时,函数递归前的变量值是否还保持原来的值,是否能够再正常执行下去,这是个问题。能够正确递归的函数称为“可递归的”,可递归的意味着“可重入的”。

(7)多任务中断/切换重入。如果一个函数正在执行的过程中,被另一个任务中断了,然后该任务也调用了这个函数,导致这个函数的代码又被从头开始执行,其中使用过的变量的值都被改变了。然后等中断任务结束后,被中断的函数又从中断点继续执行。

这时存在一个极大的问题,虽然编译器将中断的寄存器现场恢复了,但是被中断的函数使用的,那些被中断任务改变了的变量,如何恢复?

(8)根据任务调度现场的保存和恢复机制,如果一个函数没有调用参数和不使用任何局部变量和公共变量(单片机的公共端口一般不含在内,因为这些端口寄存器的值本身就是在不断变化,不存在保存与恢复),只使用了通用寄存器,则该函数是“可重入的”。因为所有的通用寄存器在中断完成后都被恢复了。

(9)在PC机上的C语言教学中很少提及“函数重入问题”,全国计算机等级考试大纲中也没有这个内容,只有在很专业的C语言编程专著中才指导用户写程序时如何避免将函数写成“不可重入的”。

这不奇怪,因为在基于80x86架构、ARM架构和RISC架构CPU的DOS、Wndows和Linux等操作系统上的BC、VC或者GCC等C语言编译器都是按照函数是“可重入的”标准来编译C语言程序的,换言之,标准的C语言的函数都是可重入的。只要不是用户故意的或不小心地编程,C语言的所有函数,尤其是库函数缺省都是“可重入的”,不提也罢。

(10)由于历史上早期的8051单片机的12T的速度很慢,对变量的间接寻址存取一次要花费很多个周期,因此Keil的C51编译器缺省是函数不可重入的,包括库函数,所有的变量和函数参数都会分配一个固定的内存地址(静态变量),用直接存取指令或者DPTR专用指令来存取,以提高程序整体的运行速度。也就是说在C51程序中,函数缺省是不可重入的。Keil的C251也继承了这个特点,函数缺省是不可重入的。由于STC单片机目前主要使用C51/C251编译器,因此函数重入问题也是STC单片机特有的问题。

(11)在C51程序中要使一个函数成为“可重入的”,用户就要在编写自己的程序时强制为该函数加上“reentrant”关键字,并设置好“函数帧”指针的初始值,通常这个是51单片机XDATA的顶部。

(12)有中断的前后台任务是8051单片机最常见编程模式,在这种编程模式下用户也要重视“函数重入问题”。

不是每个不可重入的函数每次在后台和中断中同时调用时都会发生问题,会不会出问题依赖函数具体的程序结构和中断的地方。因此某个函数重入时是否会发生问题不能用具体的程序来进行实证,必须作为理论要求,在程序具体设计之前就确定解决方法。

(13)比如像C51的printf库函数,肯定是不可重入的,因为它是库函数,我们没有办法改变它,要么我们只在一个任务中使用它,绝不在两个任务中使用(这是许多同时在后台任务与中断中都使用STDIO库函数的程序经常出现莫名其妙故障的根源),这样这个函数就不存在需要重入的需求,要么就按照函数重入的要求重新写满足自己程序要求的“printf”功能。

二、函数不可重入的例子

本文下面通过具体的有中断的前后台任务的例子来介绍函数重入问题现象。该范例是一个精心策划的软件延时函数程序例子(函数重入时肯定发生问题),可以用来演示函数重入问题现象。下图是软件延时函数不可重入时的例子程序:

(14)第22行到第32行是一个利用空循环来耗时间的软件延时函数。为了说明原理和展示效果,本文例子都将C51的优化关闭,设置了优化等级为0,这样C51编译器就不会把函数参数和变量优化到寄存器组里了。

(15)上图是范例的初始化部分程序,包括作为驱动LED发光二极管的P2端口的初始化和定时器0的设置。

下图是后台任务主循环程序:

(16)本范例有前后台两个任务,端口P2连接8个下拉的LED。上图第54行到到第82行是后台任务,P27~P23五个LED按照间隔0.2秒钟依次发光。上面程序的编程方法称为“直写法”,对5个LED分5个节拍依次写出。直写法是最基础的程序结构,简单明了。

(17)下图是本范例的中断前台任务部分:

其中第86行到到第103行是中断前台任务,定时器0每10毫秒产生一次中断,每中断50次(0.5秒钟)P20P21两个LED闪烁一次,闪光时间30毫秒。

(18)第107行到到第115行是初始化程序,将定时器0设置为每10毫秒产生一次中断,。

(19)由于本范例在前后台任务中都采用同一个延时函数“DelayMS”作为软件延时用,因此该函数完全可能碰到函数重入的问题。

(20)本项目对Keil的设置为大模式和不优化,因此所有的变量均实际存储在XRAM空间,都是静态变量,因此按照前面的说明,延时函数“DelayMS”是不可重入的。

(21)使用Keil生成HEX文件,烧录到打狗棒开发板单片机上,可以观察到后台任务的P27~P23五个LED闪烁两轮后就不再闪烁了,但前台任务的P20P21两个LED仍然正常连续闪烁。

下面是本文范例用Keil的C51编译后的效果:

视频加载中...

根据程序可以判断,中断是正常工作的,在中断里的DelayMS()函数是正常工作的,但是在主循环中的DelayMS()函数却死在那里退不出来了,因此证明这个函数是不可重入的,并且导致了系统工作不正常。

(22)根据DelayMS()函数定义的第17到23行,可以看到中断中延时函数正常退出时,静态变量j和i的值为0,静态变量k的值为30。因此每次中断任务完成后,这三个变量的值未必会恢复到它们在主函数中的值。

(23)假如中断发生时后台任务的延时函数正准备执行第29行或者第30行程序,等中断退出后,i和j的值为0。这时无论做--j或者做--i,其值将变为0xFFFF,不为0,因此要继续做减一循环,程序原地踏步。但是要把一个16位的长整数减为0,要花不止1秒的时间,这时中断任务又来了,变量值又被清0,周而复始,后台程序就停留在那里循环,不会退出延迟函数,所以后台任务的LED就死在那里不再闪烁了。

同样中断退出后k的值为30,对于第25行程序,后台任务的k值一次次被清为30,很难达到循环的上限200, 这样程序就不会退出延迟函数,以后后台任务的LED就死在那里不再闪烁了。

三、解决函数不可重入的方法1——函数调用栈法

(24)在C51中解决函数不可重入的标准方法是采用为每次函数调用建立C语言函数调用栈的标准方法。首先将在1个以上任务中都有调用的不可重入的函数定义上加可重入关键字“reentrant”,见下图:

然后再启动文件“STARTUP.A51”中打开并设置函数堆栈设置,见下图:

其中设置“XBPSTACK”为1表示程序中用到可重入函数,设置“XBPSTACKTOP”为单片机的XRAM的容量,这里是8KB。

这样启动程序在启动时就会自动地设置函数调用栈指针“?C_XBP”的初始值,见下图:

(25)经过这两个设置后,函数就成为可重入的了,函数在被中断时,函数现场自动保存在以“?C_XBP”为指针的堆栈中了,并且在中断完成后,又会被自动恢复。下面是解决函数重入问题后范例的运行效果视频:

视频加载中...

在视频中可以看到整个前后台任务都得以顺畅第执行了。

(26)方法1编译代码大小分析。下图为采用原始不可重入函数的编译结果:

其中代码空间CODE=483字节。

其中代码空间CODE=591字节。比起不可重入增加了许多。

四、解决函数不可重入的方法2——重复函数法

(27)由于在一个任务里,程序是顺序执行,对一个函数不管调用多少次都不会产生函数重入问题。因此如果对每一个调用不可重入的函数的任务都独立地定义一个函数,这样尽管函数程序是同样的,但在不同的任务里调用的是不同的函数,就不会产生函数重入问题了。

具体到本文范例,有两个任务,采用本方法2,将延时函数重复定义两次即可,见下图:

然后再后台任务中继续调用第1个延时函数,在中断中调用第2个延时函数即可。

这个程序实际运行效果与上面视频2一致,两个任务都能顺利执行。

(28)采用方法2的代码空间CODE=593字节,比起不可重入的483字节增加的部分593-483=110字节就是一个软件函数的代码长度。

五、解决函数不可重入的方法3——代码插入法

(29)对于多次使用的程序代码段,一般都定义为一个统一的函数,然后再用到这个程序代码段的地方调用它就行了,比如上面程序中的软件延时函数。采用这种函数调用法的好处是在每个需要地方访问共同的代码,只需要3个字节的“LCALL”指令即可(8051单片机)代替重复的代码段,可以大大节省总代码量。

(30)在C语言RTOS程序中有些地方不允许采用函数调用的方法以及对于某些让用户自己选择定义的地方,就采用宏来定义这个代码段,然后在使用它的地方再做宏展开,将这个代码段插入到程序中指定地方。如果将这个宏定义名称写作函数的形式“()”,这种宏也被称为“宏函数”。

(31)这种代码插入法也可以用来解决函数不可重入的问题:只要在每个引用它的函数中先定义不可重入函数的局部变量,然后用宏展开在每个引用它的地方插入代码段即可。由于使用C51/C251编译器时每个非指定重入的函数的局部变量都被存储为静态变量,相当于每个函数的“现场”变量都被分别的保存起来了,这样程序被中断或者任务被切换时就不会产生函数重入的问题了。

(32)下面是方法3的实际程序例子:

其中第22行到第30行时软件延时代码的宏定义,第35行是“main()”函数的局部变量定义,第58行和的59行是这种“宏函数”的例子。

采用方法3解决函数重入问题的程序实际运行效果与上面视频2一致,两个任务都能顺利执行。

(33)采用方法3的代码空间CODE=995字节,比起方法1和方法2都大的多。方法3代码空间大原因十分简单,同一个代码段被重复地插入到整个代码序列中,而方法1和方法2只是用“LCALL”指令访问同一个的代码段。

六、结 论

(34)由于采用函数调用栈方法是用以“?C_XBP”为指针的间接寻址来访问函数的局部变量,速度比直接用DPTR存取局部变量的速度要慢,所以从速度上比较,方法2和方法3一样,方法1比它们都要慢。

(35)从占用CODE空间的大小看,方法1和方法2相差不大,但是方法3的反复插入使得其所占CODE空间远大于方法1和方法2,几乎是它们的2倍。

(36)所有操作系统上的编译器,比如Linux、Windows等上的C语言编译器,和目标为80x86、ARM和RISC-V架构CPU的C语言编译器,其C语言函数缺省的都是采用方法1的可重入函数。因此由这些架构CPU打造的计算机和单片机,都是用不断地提升CPU的速度和增加硬件专用指令的方式来克服方法1速度慢的缺点。

标签: #按照c语言规定的用户