Keil C是非常优秀的C51编译器,可能是最好的C51编译器,提供各种优化模式,对变量的优化和地址安排做得非常好。这是用C语言写代码的好处之一,如果用汇编写,得费一大番功夫给各个变量安排内存物理地址,还得时刻记住哪些地址的内存单元是已经分配了,新增加的变量就不能占用那些已经分配了的单元,以免产生内存交叠冲突和溢出。我一直非常信赖Keil C51的编译结果,在我的印象里,它对内存的分配是完美的,只要代码用它编译时没有报告任何warning和error,代码运行时不可能内存冲突或溢出的现象。
但,今天发生的事情证明我错了。
手头上有个产品的代码,代码量很大。程序跑起来的效果不大好,因此打算把代码优化一下。代码量越大,通常可优化的地方也越多。对8051来说,访问芯片内部的data区(0~7FH)内存速度是最快的,直接访问,一条指令就能读写,而idata区(80H~FFH)虽然还是内存区,但由于地址分配上跟特殊寄存器SFR重合,只能间接地址访问,两条指令才能读写,速度稍慢点,而外存xdata区(0~7FFFH)必须使用DPTR指针才能访问,速度是最慢的。很明显,优化的原则就是尽量把频繁读写的变量优先安排在data区,然后是idata区,最后才是xdata区。
当我做完变量手工优化工作后,把编译模式设为SMALL,这样C51编译器会自动把那些我没手工指定存放区的变量优先安排进data区,如果超出有效地址范围,它会报错,因此我大可以放心。按下rebuild all按钮后,编译器提示:
Program Size: data=236.2 xdata=19321 code=43372
"ipphone_main" - 0 Error(s), 0Warning(s).
编译器提示的data区包括了idata在内,按以往的经验来看,data区有256个byte,程序才使用了236.2个,还剩下19个,没有溢出,而xdata有32k,现在才使用了19k,远没有溢出,编译结果一切很正常。
把代码烧录进芯片跑起来后,结果出人意料,从现象来看,上电约1秒后就自动重启,重启后过1秒又重启,非常有规律的重启。
我没有怀疑是编译器的原因,当时第一念头是怀疑是看门狗,代码里上电后就打开了看门狗,可能某些子程序代码执行时间过长,看门狗复位了,于是在有怀疑的地方插入了喂狗代码,重新编译后再测试,依然自动重启。于是干脆就把看门狗的代码注释了,不使用看门狗,以为这回没问题了吧,结果出人意料,还是重启。
我仔细想了一下,能造成8051的重启的原因不多,一是看门狗引起的重启,这点可以排除;二是某些8051支持重启指令,我手头上用的这款虽然支持,但我没用过那指令,这点也可以排除;三是8051被强干扰,把取指寄存器PC的内容改变了,改成0,于是就重启了,这点也可以排除,因为如果现场有强干扰,没优化前也会重启才对。
由于没想出来是什么原因,于是开始折腾,把优化的变量一个个恢复成未恢复优化的状态,每恢复一步就重新测试一次。终于在恢复一个16字节的数组时发现程序正常了,仔细看了一下,那数组定义在xdata区的时候程序就完全正常,而定义在idata区的时候程序就复位了,虽然奇怪的是,定义在idata区时,编译器并没有报告内存溢出。跟踪汇编指令也没发现异常,无论定义在idata还是xdata,编译器为该数组分配的地址证明确实都是有效地址,确实没有溢出,编译器的安排还是正确。
虽然还没找到根源,但问题既然是出现在内存上,我于是决定查看当那个数组指定为idata类型时的内存分配。Keil C51在编译时会输出一个M51文件,该文件包含了大量的内存分配信息,非常详细,包括哪个变量被编译器分配到哪个内存地址,占用多少个字节,哪些变量是局部变量,可以重复利用……这个M51文件里都有详细的列表。
从列表里的变量分配地址一路看下来,都没错,边看还边惊叹编译器对变量的分配安排非常精确,但看到最后一个堆栈指针的安排时,终于发现问题所在了,它是这样安排的:
TYPE BASE LENGTH RELOCATION SEGMENT NAME
----------------------------------------------------------------------------------------------
IDATA 0080H 0034H UNIT _IDATA_GROUP_
IDATA 00B4H 0022H UNIT ?ID?IPPHONE_MAIN
IDATA 00D6H 001FH UNIT ?ID?DNS_NICRCV?IPPHONE_DNS
IDATA 00F5H 0004H UNIT ?ID?DISP
IDATA 00F9H 0001H UNIT ?STACK
这上面标有STACK的段就是堆栈分配,上面的数据表明,SP堆栈指针安排在F9H这个地址,堆栈空间是1个字节!表面看没有溢出,但我的程序里使用了中断服务,进入中断服务时,至少需要8个字节的堆栈空间(保存R0~R7寄存器)来进行保护现场,8051使用的是递增压栈的设计,堆栈指针往往被安排在内存空间的后面可用部分,每压栈一个字节,SP指针往上加1,进中断服务时,至少压栈8个字节,F9H+8,超出了FFH,堆栈指针不能超过FFH,也就是说堆栈溢出了!原来这就是导致程序不断重启的原因,不是变量内存溢出,而是堆栈溢出!
而当我把那个数组指定为xdata类型后,由于该数组不再占用idata区,于是IDATA一下子多了16个字节的可用空间,重新编译后的M51这样安排:
IDATA 0080H 0024H UNIT _IDATA_GROUP_
IDATA 00A4H 0022H UNIT ?ID?IPPHONE_MAIN
IDATA 00C6H 001FH UNIT ?ID?DNS_NICRCV?IPPHONE_DNS
IDATA 00E5H 0004H UNIT ?ID?DISP
IDATA 00E9H 0001H UNIT ?STACK
从这组数据来看,SP指针安排到在E9H这个地址,堆栈空间有FFH-E9H+1=23个字节,对于程序来说已经够用,因此程序运行正常。
多次调整变量类型的编译结果表明,C51对于堆栈空间需求大小不作计算,任何代码都只是按堆栈空间只有1个字节需求来分配(在我眼里看来这明显是胡来,稍复杂点的子程序调用都不可能只要1个字节就能完成现场保护),由于堆栈只能分配在data区和idata区,因此当一个程序为了优化而data区占用太多时,虽然编译器能编译成功,但往往SP堆栈指针被分配在data区的最后面,很容易造成堆栈空间不够而溢出。为保险起见,最好保证编译后的SP值安排在F0H之前,那样至少有16个字节的堆栈空间,才能最大限度保证程序不会跑飞。
看样子不能太相信Keil C51,以后编译完后,还得查看一下M51才能确保程序的质量,不知道这个算不算Keil C51的bug。
- Keil C51 中的函数指针和再入函数
函数指针是C语言中几个难点之一.由于8051的C编译器的独特要求,函数指针和再入函数有更多的挑战需要克服.主要由于函数变量的传递.典型的(绝大部分8051芯片)函数变量通过堆栈的入栈和出栈命令来传递. ...
- Keil C51与Keil ARM共存
转自:http://blog.chinaunix.net/uid-20734916-id-3988537.html Keil和MDK共存,按照以下步骤:1 先安装 Keil C51,安装目录改为:&q ...
- Keil C51中函数指针的使用
函数指针在C语言中应用较为灵活.在单片机系统中,嵌入式操作系统.文件系统和网络协议栈等一些较为复杂的应用都大量地使用了函数指针.Keil公司推出的C51编译器是事实上80C51 C编程的工业标准,它针 ...
- keil C51 指针总结
变量就是一种在程序执行过程中其值能不断变化的量.要在程序中使用变量必须先用标识符作为变量名,并指出所用的数据类型和存储模式,这样编译系统才能为变量分配相应的存储空间.定义一个变量的格式如下: [存储种 ...
- Keil C51 中指针的使用
指针是C语言中比较难的一个内容,Keil C51在指针方面有和标准C不一样的地方,今天看了一些资料学习了一下Keil C51 中指针的使用. keil51的指针,包含两种指针:普通指针,兼容标准C:内 ...
- 深入剖析keil c51 --- 从汇编到c51
第一节 main()函数和启动代码 汇编是从org 0000h开始启动,那么keil c51是如何启动main()函数的?keil c51有一个启动程序startup.a51,它总是和c程序一起编译和 ...
- keil c51中C程序的启动过程
汇编是从org 0000h开始启动,那么keil c51是如何启动main()函数的?keil c51有一个启动程序startup.a51,它总是和c程序一起编译和链接.下面看看它和main()函数是 ...
- Keil C51内存分配与优化
C51的内存分配不同于一般的PC,内存空间有限,采用覆盖和共享技术.在Keil编译器中,经过编译后,会形成一个M51文件,在其内部可以详细的看到内存的分配情况. C51内存常见的两个误区: A.变量超 ...
- KEIL C51高级编程
第一节 绝对地址访问C51提供了三种访问绝对地址的方法: 1. 绝对宏:在程序中,用“#include”即可使用其中定义的宏来访问绝对地址,包括:CBYTE.XBYTE.PWORD.DBYTE.CWO ...
随机推荐
- CCI_chapter 4 trees and Grapths
4.1Implement a function to check if a tree is balanced For the purposes of this question,a balanced ...
- SQL使用单引号
SQL> select 'xxxx'oooo' from dual; ERROR: ORA-01756: quoted string not properly terminated SQL> ...
- libeXosip2(2-2) -- eXosip2 network API
eXosip2 network API General purpose API. Functions int eXosip_transport_set (osip_message_t *msg, c ...
- 支持多文件上传,预览,拖拽,基于bootstra的上传插件fileinput 的ajax异步上传
首先需要导入一些js和css文件 <link href="__PUBLIC__/CSS/bootstrap.css" rel="stylesheet"&g ...
- 如何在 Android 手机上实现抓包?
如何在 Android 手机上实现抓包? http://www.zhihu.com/question/20467503 我想知道某个应用究竟在数据提交到哪里,提交了什么.网上的教程太复杂,不想麻烦.有 ...
- Chrome浏览器查看cookie
原文:http://jingyan.baidu.com/article/6b18230954dbc0ba59e15960.html 1. 查看页面的cookie 方法: a). 点击地址栏前面的文档薄 ...
- STL采用的标准模板库
一.map,set set集合容器实现了红黑树(Red-Black Tree)的平衡二叉检索树的数据结构,在插入元素时,它会自动调整二叉树的排列,把该元素放到适当的位置,以确保每个子树根节点的键值大于 ...
- [转]Jquery中AJAX错误信息调试参考
下面是Jquery中AJAX参数详细列表: 参数名 类型 描述 url String (默认: 当前页地址) 发送请求的地址. type String (默认: "GET") 请求 ...
- DotNet程序汉化过程--SnippetCompiler奇葩的字符串
开篇前言 汉化的过程总会遇到各种各样的问题,让人抓狂,这一篇我就来讲解一下一个特殊的单词的汉化以及我的“艰辛历程”. 起因介绍 在SnippetCompiler有这么一个奇葩的字符串“查找>&g ...
- http断点续传原理
断点续传一是断点,一续传. 断点是在下载时,将下载文件分多片,同时进行多片一起下载,如果任务被暂停,暂停的位置就是断点. 续传就是未完成的下载再次开始时,会从上次的断点继续传送. 在下载(或上传)过程 ...