datasheet

Keil C51里关于堆栈指针的处理

2016-09-25来源: eefocus关键字:Keil  C51  堆栈指针
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  堆栈指针

编辑:什么鱼 引用地址:http://www.eeworld.com.cn/mcu/article_2016092529775.html
本网站转载的所有的文章、图片、音频视频文件等资料的版权归版权所有人所有,本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如果本网所选内容的文章作者及编辑认为其作品不宜公开自由传播,或不应无偿使用,请及时通过电子邮件或电话通知我们,以迅速采取适当措施,避免给双方造成不必要的经济损失。

上一篇:51常用程序
下一篇:一个51单片机实现数字时钟(1602显示)

关注eeworld公众号 快捷获取更多信息
关注eeworld公众号
快捷获取更多信息
关注eeworld服务号 享受更多官方福利
关注eeworld服务号
享受更多官方福利

推荐阅读

为Keil添加注释的快捷键

Keil刚装上是没有注释快捷键的,可以自己添加,“Edit”-“Configuration”,然后选择“Shortcut Keys”标签页,下拉找到“Comment Selection”,然后点右边的“Create Shortcut”就可以自己输入快捷键了,确定后即可生效。“Comment Selection”是添加注释快捷键,可以设为Ctrl+Q“Uncomment Selection”是添加取消注释快捷键,可以设为Ctrl+W
发表于 2019-04-16
为Keil添加注释的快捷键

用keil烧写现成的hex文件

1、将hex文件转成elf文件,方法要自行先搜索一下。2、新建一个工程,选这好CPU,不要添加启动代码。3、把生成的hex文件或elf文件复制到新工程的目录下。4、在工程的options->Output选项里,把Name of Executable:设成您的hex文件名。5、设置好options->Utilities的选项。6、直接点Load就行了。
发表于 2019-04-16

STM32 Keil MDK数据类型定义

/* Copyright (C) ARM Ltd., 1999 *//* All rights reserved */ /* * RCS $Revision: 138251 $ * Checkin $Date: 2008-10-07 12:02:11 +0100 (Tue, 07 Oct 2008) $ * Revising $Author: agrant $ */ #ifndef __stdint_h#define __stdint_h   #ifndef __STDINT_DECLS  #define __STDINT_DECLS 
发表于 2019-04-16

stm32 keil4建立工程

在安装完Keil vision4、配置好MDK仿真环境后,我们就开始建立工程啦!一.新建工程1.在桌面创建TEST文件夹,在文件夹中创立以下文件夹;2.启动Keil v4,新建我们的工程文件,文件取名为STM-DEMO,保存在文件夹USER下;3.选择芯片,选择STM公司的STM32101VE;4.问是否复制STM32的启动代码到工程文件中,点否,我们有库自己手动添加;5.工程建立成功,但我们的工程中没有任何文件;6.把STM库里的文件复制到TEST的这五个文件夹里:(1)USER:放工程文件(2)FWLIB:放STM32库里面的inc 和src两个文件夹,用于库外设驱动。(3)CMSIS:用来存放库为我们自带的启动文件和M3系列
发表于 2019-03-28
stm32 keil4建立工程

STM32CubeMX(Keil5)开发之路——5定时器中断TIM

为了方便调试,重定向printf,进行usart设置1——点击USART1进行设置2——模式选择Asynchronous异步传输3——可以看到右边自动出现了Tx和Rx4——可以自行设置波特率,停止位,校验位等参数1——点击Clock Configuration进行设置2——注意这几个地方的时钟(后面进行分频设置的时候需要)点击TIM1进行设置1——时钟源Clock Source选择Intemal Clock2——预分频选择36000-1也就是35999(注意16位最大只能表示65535)3——自动重装载值设置为2000-1也就是1999讲解:定时器更新中断的频率=时钟频率/(预分频+1)/(自动重装载值+1)即 :72000000/
发表于 2019-03-25
STM32CubeMX(Keil5)开发之路——5定时器中断TIM

关于Keil5编译成功但是会有红叉的问题

很多人在用keil5时编译项目无错误无警告但是程序右边会出现红色叉号,如下图所示这是因为keil新增的同步查错功能,想要关闭的方法是: Edit - Configurations - Text Completion- Dynamic Syntax Checking然后把使能关掉就可以了。
发表于 2019-02-18
关于Keil5编译成功但是会有红叉的问题

小广播

何立民专栏

单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2019 EEWORLD.com.cn, Inc. All rights reserved