先说一下
首先问一个问题,C51中函数调用时参数是怎么传递的?
你肯定会说是通过堆栈把实参压栈传递的对吧?
不对哦,8051单片机内存十分有限,没有软件堆栈,函数调用不通过堆栈来传递形参,而硬件堆栈空间也十分有限,程序里所有的局部变量以及全局变量都是编译的时候固定存储在某个地址的存储空间的,函数调用的时候就不用压栈了,函数的形参通过存储地址或者寄存器来传递,查了一下:
(1)少于3个参数的通过寄存器(R0~R7即系统的硬件堆栈) 传递(寄存器不够时通过存储区传递);
(2)多于3个时一部分通过存储区传递;
(3) 重入函数通过堆栈传递(后面会讲)。
既然内存空间十分有限,(比如用过的方案RAM是内部 256byte+外部1280byte/4K),那如果程序稍微大一点,变量和函数多一些,能保证所有变量都分配分到地址吗?
为了省内存空间,BL51/LX51链接器会对用户应用程序进行分析,生成关于函数调用关系的调用树(call tree),有些变量的地址是会覆盖的,能覆盖的存储地址遵循下面的原则:
(1)对于有调用关系的函数局部变量地址不会互相覆盖,无调用关系的函数局部变量地址可以复用。
如上图,箭头表示调用关系,A指向B表示A调用B,如果实际A函数调用了B、D函数,B调用了C,D调用了C,那么A、B、C、D之间是有调用关系的,他们函数内部的局部变量分配的地址不会互相覆盖,E函数调用F函数,但E、F与A、B、C、D无调用关系所以E、F函数中局部变量地址与A、B、C、D函数中局部变量地址可以互相复用。
(2)对于中断服务函数调用链中用到的局部变量,不与任何普通函数中局部变量复用地址,因为中断随时可能发生。
问题
对于回调函数或者其他形式函数指针,调用方式是通过的函数指针调用的,不是直接调用函数本身,即间接调用,对于间接调用的函数,链接器无法识别出调用关系,函数指针他以为是一个简单的变量,因此,间接调用的函数链接器没有识别出他们的调用关系。
那么问题来了,没有调用关系的的函数局部变量可以互相覆盖,可能是回调函数里的变量覆盖了调用他的上一级函数里的变量,可能是覆盖别的函数也不一定,这样程序可能会出错。
曾经遇到一个问题,程序里面用了回调函数,调用回调函数时,程序出现bug了,单步调试发现,调用回调函数的地方有一个局部变量A,比如本来他的值是1,调用完回调函数就变了,原因就是上面说的,链接器不知道回调函数的与上一级函数的调用关系,回调函数里的某个局部变量地址与上一级函数里的这个局部变量A地址重复了,调用回调函数后A的值就被回调函数里改变了。查询生成的MAP文件发现,确实这两个局部变量的地址重复了。
还遇到另外一个问题,外部中断服务函数中调用了一个回调函数,也出现bug,不断地自动产生了按键事件。原因也是跟上面说的一样,链接器不知道中断服务函数与回调函数的调用关系,外部中断服务函数调用的这个回调函数里的局部变量与按键模块中的一个局部变量地址重复了。
怎么解决?
解决方法一:不用回调函数
在原来调用回调函数指针的地方直接调用函数,要调用的函数先在调用的地方用“extern”声明,函数的实现仍然在原来的地方实现;
解决方法二:修正调用树
回调函数调用的地方,编译器没有生成调用函数与被调用函数的调用关系,因此增加他们的调用关系。
参考keil官网函数指针说明,用“overlay”指令在keil界面中增加二者调用用关系
举例如下:
由于 purifier_uart_init函数里注册了一个函数指针,实际函数名是purifier_uart_event_handle,但实际上purifier_uart_init函数对purifier_uart_event_handle没有调用关系,但系统生成的调用树是有调用关系,因为在函数purifier_uart_init中传进去了这个指针,系统就认为有调用关系,而实际对他有调用关系的是
void uart1_cmd_handle(plat_uart_cmd_info_t*cmd_info)函数,
因此要在调用树中,purifier_uart_init函数下去掉purifier_uart_event_handle,
在uart1_cmd_handle函数下增加purifier_uart_event_handle,
“~”符号是删除调用关系,“!”符号是增加调用关系
增加指令界面
参考:
https://blog.csdn.net/jemofh159/article/details/6628231
参考《Keil C51单片机高级语言应用编程与实践》第6章,以下情况需要用OVERLAY命令对数据覆盖进行调整:
(1) 将函数作为参数进行传递或返回(回调函数属于这种)
(2)使用了已初始化了的函数指针数组
(3)用户程序中包含实时操作系统
另外本书还讲了如果为了避免覆盖可以用“NOOVERLAY”指令来禁止整个应用程序变量的覆盖,具体可自行查阅。
解决方法三 用“reentrant”关键字生成模拟栈
参考官网说明,用“reentrant”关键字(或者“large reentrant”,因为内存模式是large)把函数声明为可再入函数,那么该函数被调用时会生成模拟栈,就不用担心自己的局部变量被别人覆盖了,或者自己意外把别人覆盖了。在这个例子中,调用回调函数的函数以及被调用的函数指针的实体函数都应该声明为“reentrant”
另外还需要在startup文件中,初始化模拟栈的大小。如下图
但是这种做法的缺点是,增加一个“reentrant”关键字会增加100多byte的code消耗,所以对于Flash已经快不够用的情况这种方法可能不行。
另外发现,在函数指针类型定义中增加“reentrant”编译出来的code大小并没有增加(如下图),应该是没有真正起作用。
附keil官网关于C51函数指针使用的文档链接:
https://www.keil.com/appnotes/docs/apnt_129.asp
1.其实,机器人的发展与嵌入式系统密不可分~
2.HarmonyOS到底是不是Android套皮?
3.代码防御性编程的十条技巧~
4.几种基于RTOS的实用工具
5.单片机编程如何查看版本之间代码的不同?
6.从硬件转向软件设计,请牢记这十大技巧!
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。