Keil C使用经验小结.doc_第1页
Keil C使用经验小结.doc_第2页
Keil C使用经验小结.doc_第3页
Keil C使用经验小结.doc_第4页
Keil C使用经验小结.doc_第5页
已阅读5页,还剩14页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Keil C使用经验小结秦建勋2005/7/29目录引言1语言扩展1数据存储器区域1存储器模型2指针3函数3参数传递3自动变量分配8C51的陷阱9运算溢出9编译与链接控制10编译时控制10定制文件10Startup.A5110INIT.A5110L51_BANK.A5111链接时控制11代码或常数的绝对定位11和_at_关键字的比较11Overlay12如何控制overlay15overlay相关的其它问题15可重入函数16Vision IDE使用简介16简单的代码习惯17头文件(.h文件)习惯17C模块的习惯18CVS的使用18引言2004年6月以来开始接触Keil C,原因是MUSE项目控制器采用了目前8位微控制器领域最经典、使用最广泛的8051 MCU。从一年多来的使用实践看,我们的8051 IP还是比较可靠的,那么以后可能有更多的产品需要用到该IP,因此看起来还是需要在相当长一段时间需要与Keil C打交道。本文就简单小结我对Keil C的一些理解以及使用过程中的一些经验、教训,试图起到抛砖引玉的作用,为大家总结Keil C使用经验提供一个讨论的起点。语言扩展所谓语言扩展是指相对标准C(如ANSI C严格说来,主要是针对ANSI C,因为Keil C显然不支持C+,一些语言特性,如“/”注释是C 99标准引入的)进行的针对8051硬件平台进行的扩展,这样的C编译器才是真正为8051量身订做的:资源的使用是紧凑的、编译出来的代码是高效的,另外,Keil 公司好象也出了针对ARM的C编译器,所以,从命名来说,看起来C51更准确些,实际上Vision也是这么做的。数据存储器区域8051的数据存储器分为片内和片外存储器两大类,片内RAM分为直接访问和间接访问两部分,片外RAM由可有页寻址和普通区域两部分,总是让人混淆,下面我结合C51的数据存储器区域给一个图示解释FF007F80FF只能间接访问,idata直接访问:data间接访问:idataSFRs,只能直接访问,sfrFFFF00FF0000pdata, movx Rnxdatamovx DPTR片内数据存储器片外数据存储器,DMbdataRegister banks1F2F图1 8051数据存储器对应的C51存储器模型我们可以看到,data作为可存放数据的空间只有128字节,还要排除掉32字节的寄存器组和16字节的可位寻址空间,实际只有80字节的变量可分配空间,所以有时候会出现链接时报告片内数据只使用100字节却数据空间溢出的情况(使用small存储器编译模型),原因就在于程序引入的附加data型的数据量已经超过了80字节,此时,把data型变量改为idata就可以通过链接,只要变量总量不超过片内的256字节(还应该考虑运行时堆栈大小 J)。存储器模型C51的存储器模型用于确定:函数参数、自动变量以及不带显式存储器类型说明变量声明的缺省存储器类型,有如下几种模型:小模型(small model),在该模型下,所有变量缺省都驻留在8051片内存储器中(如同声明为data类型),变量的访问效率非常高。然而,所有对象,包括堆栈(运行时的最大堆栈容量)必须都放在片内RAM中,一般来说在程序规模小且使用overlay技术(后面会讲到)、没有声明可重入函数并多次嵌套调用情况下是最佳的。紧凑模型(compact model),该模型下,所有变量,缺省驻留在外部DM的一个page之内,(通过MOVX Ri指令访问,见图说明),紧凑模型的效率逊于小模型、优于打模型。这里自然产生一个问题问题,当紧凑模型下使用的变量超过一个页大小(256字节)怎么办?多余的部分,Keil C手册说,使用P2的设置以使用另外的page,但实际情况是我们的8051只支持0页,我向徐国柱核实过,这体现在MOVX Ri的时候,16为DM地址总线的高8位(我们的8051与经典的不一样,经典的由于考虑到封装的要求,使用P0和P2,P0还是地址、数据复用,我们的8051和DM都是集成在片内的,没有封装带来的限制)始终为0,可否搞一个sfr来产生MOVX Ri指令执行时候的高8位地址,复位地址为0。我们现在的程序规模就采用了这种模式。大模型(large model),该模型下,所有变量缺省情况下都驻留在DM中,就象使用xdata存储器类型修饰符声明一样(通过MOVX DPTR指令访问,寻址采用DPTR),这种方式速度慢,产生的代码也最多,而且对于需要频繁运算的变量,效率更低。指针通用指针以标准C指针的方式声明,如:char *s; /* string ptr */int*numptr; /* int ptr */long*state; /* Texas */ 通用指针总是以三字节进行存储,第一个字节是存储器类型,第二字节是指针代表的地址的高字节,第三字节是地址的低字节。通用指针的好处就是可以访问8051存储器空间中任何存储器区域中的变量,因为这个原因,很多Cx51库例程使用这种指针类型。存储器专用指针就是在指针声明的时候,指定对应存储器类型,如:char data*str; /* ptr to string in data */int xdata*numtab; /* ptr to int(s) in xdata */long code*powtab; /* ptr to long(s) in code */由于存储器类型在编译期指定了,通用指针需要的存储器类型字节就不再需要了,而且,通用指针的指针地址部分必须取所有指针类型中最长的,2个字节,而存储器专用指针则根据实际的指针类型确定指针大小,对于idata, data, bdata, 和 pdata是一个字节,对于code和xdata是两字节,这样可以加快速度和减少代码,用于函数参数的传递时候效果更佳。这里需要澄清两个概念:存储器专用指针,和位于特定存储区中的指针变量,前着是指指针的值(地址)指向的特定存储区的,后者是指指针本身(指针本身也是一个变量,只不过其值是个地址)存放在特定的存储区域,看下面的例子char data *str; /* 指向位于data空间的字符类型,指针本身的位置没说明,依据编译采用的存储器模型 */char data * xdata str; /* 指向位于data空间的字符类型,指针本身位于xdata中 */函数参数传递经典8051的堆栈指针只是以间接方式访问片内数据存储器,因此可以使用整个片内256字节的存储器。8051的堆栈指针是向上增长的(即PUSH指令执行时是先增加栈指针SP,与x86相反),C51编译器把堆栈指针初始化到紧邻片内存储器所有变量之后的位置。由于堆栈空间最多为256字节,实际上还远小于这个值,因此C51编译器并不使用堆栈传递参数,而是为函数参数分配一个固定的存储器位置,函数调用发生时,调用者先把参数拷贝到该位置,然后被调用需要参数时就从该固定位置抽取参数,只有返回值是存储在堆栈上的(位返回值放在CF中)。这个固定存储器位置是怎么确定的呢?第一种情况,当参数满足下列条件的时候,采用寄存器传递参数这里特别需要注意的是,如果函数第一参数是bit类型,则后面的其它参数不能通过寄存器传递,因为这种情况下破坏了上面的编号方案,因此,bit型的变量应尽量作为参数列表的末尾。那么,当2字节大小参数多于3个,或包含4字节大小的参数多于一个时,C51怎么传递参数呢?对于需要接收寄存器方式以外传递参数的函数,C51会自动产生一个参数传递数据段,命名方式是 “?函数名?BYTE”和“?函数名?BIT”,位参数在调用参数前先拷贝到?函数名?BIT段,所有其它参数拷贝到?函数名?BYTE段,在这些段中所有参数都分配空间,即使有些参数通过寄存器传递(是否效率低?),参数传递以它们声明的顺序存储在这些参数传递段中。需要说明的是,这些用于参数传递的固定存储器可能位于内部数据存储器或外部数据存储器,具体情况依赖于使用的编译存储器模型:SMALL使用片内RAM,COMPACT使用DM的pdata,LARGE使用普通DM。特别需要注意的是,对于4字节(32位)大小、非寄存器、位于xdata(compact和large模型编译)参数的传递,C51是调用一个内部例程?C?LSTKXDATA来实现参数的拷贝(到对应参数数据段)的,效率相当低,所以我要建议诸如32位的运算都传地址,地址最多16位。#include bastype.hINT32 testF(INT16 arg1, INT16 arg2, INT16 arg3, INT16 arg4, INT32 arg5)return (arg1 + arg2 + arg3 + arg4 + arg5);void main()testF(11, 22, 33, 44, 55);搞清楚参数是怎么传递后,我们就可以消除一个警告,因为以秦氏理解,C语言的参数永远是传值的(即使对于传指针,从指针作为变量这个意义上仍旧就传递的是值,C+由于引用的引入,从形式上改变了这一情况,尽管编译器实现仍旧使用指针),而且这个值还是在固定的存储位置,所以我们不能对实参(值)本身指定存储区位置INT16 testF(INT16 xdata arg, INT16 xdata * arg1, INT16 * data arg2);在上例中,第一个形参声明指定arg放在xdata空间,这违背了编译器选择固定存储器位置传递参数的原则;第二个形参声明是正确的,因为它只是说指针arg1指向的地址(即把指针视为一个变量的话,变量的值是一个指向xdata的地址);第三个形参对指针本身的位置进行限定,本质上犯了形参1声明的错误。?PR?_testF?TEST SEGMENT CODE ?XD?_testF?TEST SEGMENT XDATA OVERLAYABLE ?PR?main?TEST SEGMENT CODE EXTRNCODE (?C_STARTUP)EXTRNCODE (?C?LSTKXDATA)PUBLICmainPUBLIC?_testF?BYTEPUBLIC_testFRSEG ?XD?_testF?TEST?_testF?BYTE: arg1?040: DS 2 arg2?041: DS 2 arg3?042: DS 2 arg4?043: DS 2 arg5?044: DS 4; #include bastype.h; ; INT32 testF(INT16 arg1, INT16 arg2, INT16 arg3, INT16 arg4, INT32 arg5)RSEG ?PR?_testF?TEST_testF:USING0; SOURCE LINE # 3;- Variable arg1?040 assigned to Register R6/R7 -;- Variable arg3?042 assigned to Register R2/R3 -;- Variable arg2?041 assigned to Register R4/R5 -; ; SOURCE LINE # 4; return (arg1 + arg2 + arg3 + arg4 + arg5); SOURCE LINE # 5MOV A,R7ADD A,R5MOV R7,AMOV R7,AMOV A,R6ADDC A,R2MOV R6,AMOV DPTR,#arg4?043+01HMOVX A,DPTRADDC A,R4MOV R4,A; ; SOURCE LINE # 6?C0001:RET ; END OF _testF; ; void main()RSEG ?PR?main?TESTmain:USING0; SOURCE LINE # 8; ; SOURCE LINE # 9; testF(11, 22, 33, 44, 55); SOURCE LINE # 10MOV DPTR,#?_testF?BYTE+06HCLR AMOVX DPTR,AINC DPTRMOV A,#02CH;44MOVX DPTR,AINC DPTRLCALL?C?LSTKXDATADB 00HDB 00HDB 00HDB 037H;55MOV R3,#021H;33MOV R2,#00HMOV R5,#016H;22MOV R4,#00HMOV R7,#0BH;11MOV R6,#00HLJMP _testF; END OF main自动变量 自动变量在标准C里是自动分配和释放的变量,一般对应函数栈帧上分配的、函数调用期间存在返回后消失的变量,从这个意义上说C51几乎没有自动变量(可重入函数除外),这里的自动变量可理解为函数内部的非静态存储局部变量。分配前面已经提到“C51 把函数的自动变量放在固定的存储器位置”,那么这个固定存储器位置是哪里呢?首先,速度最快、访问最简单的当推当前的寄存器组了,INT8 test(INT16 arg)/ line 18INT8 temp = 0;/ line 20temp = 2;/ line 22if ( arg = 0 )return temp;elsereturn 0; FUNCTION _test (BEGIN) ; SOURCE LINE # 18;- Variable arg assigned to Register R6/R7 - ; SOURCE LINE # 19 ; SOURCE LINE # 20;- Variable temp assigned to Register R5 -0000 E4 CLR A ; SOURCE LINE # 220001 25E0 ADD A,ACC0003 25E0 ADD A,ACC0005 FD MOV R5,A自动变量采用寄存器进行分配的原则是什么?视是否能容纳。超过的自动变量会在前面讨论参数传递超过寄存器部分一样,在固定存储器位置创建一个数据段来容纳这些自动变量,而不是放在栈上,这些数据段属性具有OVERLAYABLE的,所以进行overlay,怎么overlay,请参阅文档后部分。创建的数据段视当前的编译采用的存储器模型来确定,small是data,compact是pdata,large是普通xdataC51的陷阱运算溢出看下面例子UINT32 testF(UINT16 a, UINT16 b)return a * b; ; FUNCTION _testMul (BEGIN) ; SOURCE LINE # 3;- Variable b assigned to Register R4/R5 -;- Variable a assigned to Register R6/R7 - ; SOURCE LINE # 4 ; SOURCE LINE # 50000 120000 E LCALL ?C?IMUL0003 E4 CLR A0004 FC MOV R4,A0005 FD MOV R5,A ; SOURCE LINE # 60006 ?C0001:0006 22 RET可以看到,C51在执行上述乘法的时候,尽管存放结果的变量是32位的,并没有对两个乘数进行整数提升(到32位),而是直接进行16位乘,得到16位结果后符号扩展(这里是无符号数,符号为0)到32位,如果testF(257, 258)就会溢出,结果非我们所愿。克服方法是对乘数预先强制整型提升:UINT32 testF(UINT16 a, UINT16 b)return (UINT32)a * (UINT32)b;编译与链接控制编译时控制主要时指定C源码编译的优化级别,需要注意的各优化级别是一个递进的关系而不是“或”的关系。中断向量的基地址,缺省条件下,C51会根据经典8051的中断向量基地址0进行编译,看下例子:isr_ext0() interrupt 0 using 1/ External interrupt 0产生如下汇编代码:CSEGAT00003HLJMPisr_ext0;使用跳转,这样代码可以做到最大可浮动RSEG ?PR?isr_ext0?ISR;可浮动段USING1isr_ext0:PUSH ACCPUSH B如果指定INTVECTOR(0x1000),则上述的中断入口地址语句变为“CSEG AT 01003H”,这主要用在和bootloader代码的中断向量的衔接。定制文件Startup.A51指定复位后执行点CSEG AT 0x6000INIT.A51初始化C静态数据L51_BANK.A51Bank切换配置文件链接时控制代码或常数的绝对定位代码的全部定位很简单,如全部定位在0x1000(如我们的2A01、2B01)以后,使用CODE (0x1000)即可,但这只是把可重定位的代码放在0x1000以后,中断向量(起始点),复位后的执行点这些代码缺省的基地址都是0x0000,也需要配合使用:CSEG AT 0x1000INTVECTOR(0x1000)和_at_关键字的比较_at_关键字是在源代码中使用,比较直观,缺点是不能对该绝对定位变量进行初始化,而且只能用于变量,不能用于函数。UINT8xdata _run_mode_at_ 0x0100;UINT32xdata _RsvCDLBNum_at_ 0x0101;* * * * * * * X D A T A M E M O R Y * * * * * * * XDATA 0000H 0001H INPAGE _PDATA_GROUP_ XDATA 0001H 00FFH ABSOLUTE XDATA 0100H 0001H ABSOLUTE XDATA 0101H 0004H ABSOLUTE函数在链接时绝对定位要使用它们的段名(segment name)形式,使用相对稍微不方便。对于作为数据的变量,由于在链接阶段变量都只是符号,没有单个变量实体存在,链接操纵的实体是段,所以绝对定位只能以段进行,这种情况下需要单独为需要绝对定位的变量单独创建一个.c文件,最后对这个.c基名命名的段进行链接时绝对定位。下面的例子给出如何利用链接产生的.M51文件,查找对应函数的对应的段名,然后添加链接选项来控制函数的绝对定位。其实常数、函数的命名是有规律可寻的,在熟练的情况下,完全可以不用查阅.M51文件就可以猜处内部的段名,具体可参考链接有关的帮助void BOT_Init(void)p_rbc_parms = RBC_Init( &(BOTBlk.BOT_CmdBlock.cdbRBC) );CODE 4914H 001CH UNIT ?PR?BOT_INIT?BOT.CODE(?PR?BOT_INIT?BOT(0x1000), )Overlay由于x51可用的堆栈空间相当有限,C51的自动变量和函数参数存放在固定存储器位置而不是堆栈上。通常,链接器分析程序的结构,创建调用树并对包含自动变量和函数参数的数据段做overlay(覆盖)处理。该技术通常情况下工作得相当不错,提供一个和常规基于堆栈帧相同得性能,从而该技术也被称为编译时堆栈,因为堆栈的布局在编译器和链接器运行的时候是固定的。缺省情况下,只要函数彼此不相互调用,链接器会把这些函数的自动变量和参数进行overlay。因此,链接器需要分析程序的结构,创建函数调用树,互不调用(包括间接调用,A调B,B调C,则A间接调用C)的函数就会在不同的调用树上,不同树上的函数内的自动变量、非寄存器方式传递的参数所在固定存储器段都可以相互overlay,在某些直接调用情况下,链接器也会智能地进行overlay,看下图:int FuncA(void)long temp;FuncB(10);temp = 20;temp = 1;temp = FuncC();return temp;void FuncB(int a)long temp;FuncD(a);temp = FuncC();FuncD(temp);temp += FunC();FuncD(temp);但是,在下面几种情况下,链接器就不会智能地分析出函数之间地依赖关系,从而产生overlay,导致程序运行时错误。1、中断处理例程情况,看下面例子timer0_isr() interrupt 1 / Timer0 interruptFuncA();void main()while (1) FuncB();overlay(FuncA !*)在这种情况下,链接器会认为FuncA和FuncB互不调用,完全可以overlay,甚至不会产生L15警告(多重调用),因为不是在主循环和ISR中调用同一函数。但实践表明,还是有可能出错,设想FuncB执行了一半,timer0_ISR开始执行的情形.,所以安全的做法是ISR中调用的例程不能在主循环中被调用,另起炉灶!解决办法:链接时关闭中断调用例程的overlay,最好不要让例程同时被ISR、main调用,除非声明为reentrant。2、函数指针作为参数情况:bit indirectfunc1 (void) /* indirect function 1 */unsigned char n1, n2;return (n1 n2);bit indirectfunc2 (void) /* indirect function 2 */unsigned char a1, a2;return (a1 - 0x41) (a2 - 0x41);void execute (bit (*fct) () /* sort routine */unsigned char i;for (i = 0; i 10; i+) if (fct () i = 10;void main (void) if (SWITCH) /* switch: defines function */execute (indirectfunc1);elseexecute (indirectfunc2);:OVERLAY (main (indirectfunc1, indirectfunc2),execute ! (indirectfunc1, indirectfunc2)这种情况下,链接器依据函数名称认为main函数依赖indirectfunc1和indirectfunc2,而execute不依赖上述两个函数,实际情况恰相反,从而有可能i和n1、n2以及a1、a2产生错误的overlay。解决办法:消除错误的依赖,建立正确的依赖3、函数指针数组static code PF_GENERIC_FUNC RomFuncTableMAX_FUNC_ENTRY_NUM = FlashInit, /*0*/(PF_GENERIC_FUNC)StartUDisk,InitFTL,#definePFE_LOC_InitFTL(ROM_FUNCTION_TABLE_LOC + PFE_SIZE * 2)#defineInitFTL_ROM()(*(PF_GENERIC_FUNC)*(PPF_GENERIC_FUNC)PFE_LOC_InitFTL)()main () InitFTL_ROM();overlay(?CO?mod_name (FlashInit, StartUDisk , ), main !(InitFTL, ), )在这种情况下,main函数实际调用(依赖)InitFTL,但链接器分析不出来,因为main函数实际使用的是一个函数指针数组中的一个元素而已。反而在某些情况下会产生一个“Recursive call to segment”调用警告,可以看实际工程chap9的一个例子。解决办法:消除错误的依赖,建立正确的依赖。如何控制overlayOVERLAY (sfname ! | sfname, )OVERLAY (sfname ! | (sfname, sfname, ), )OVERLAY (sfname ! *)OVERLAY (* ! sfname)幸运的是,overlay使用C代码的函数名,而不是内部段名!overlay相关的其它问题神秘问题,引起大家注意,特征:FTL的两个例程,EraseFlash和InitList都用到了data型的自动变量,在非ERASE_FLASH情形(即ERASE_FLASH宏定义不启用情形)下,上述两个例程并不被调用,于是是产生下面的警告:* WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS SEGMENT: ?PR?INITLIST?FLASHLIST* WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS SEGMENT: ?PR?CHECKPMSIG?FTL该警告的意思是,该代码段并没有被调用,于是排除在overlay过程之外(即并不对该代码使用自动变量和参数数据段进行overlay),按Keil C手册言,这种情况只是浪费一些存储器空间,并没有副作用,但实际上产生了一个神秘问题:就是闪存写的时候会出错,而且8051运行到非有效程序区域,我想应该是函数调用的时候堆栈出错,导致无效返回地址(或者链接器出错,可能性较小),更为奇怪的是,这个问题会随工程规模的扩大而消失。目前的几种解决办法:1把上述两个函数放入到条件编译中,2把上述两个函数中的局部data型变量声明改为xdata或使用缺省(pdata)。可重入函数常常可以看到下面的链接警告* WARNING L15: MULTIPLE CALL TO SEGMENT SEGMENT: ?PR?_USBT_SYNREADEP?USBT CALLER1: ?PR?ISR_EXT0?ISR CALLER2: ?C_C51STARTUP因为?C_C51STARTUP是main函数的祖先,所以上述警告实际上提示我们,main函数直接或间接调用了USBT_SynReadEP,中断处理例程ISR_ext0也调用了USBT_SynReadEP,C51链接器会比较智能地检查ISR和其它根(单任务是main,多任务是RTX的_task_)是否调用同一例程。本质上是C51不使用堆栈传递参数和分配自动变量引起的,从这个意义上来说,函数递规调用也会有问题,不过产生的警告号是L13* WARNING L13: RECURSIVE CALL TO SEGMENT SEGMENT: ?CO?CHAP9 CALLER: ?PR?CHAP9_GETDESCRIPTOR?CHAP9声明可重入函数采用C51扩展关键字reentrant。Vision IDE使用简介1、 创建Project,关键是选择Device,Device的选取最主要参考指标是device的RAMSIZE,其它主要是一些特殊厂家的器件扩展(如支持双DPTR),实质也是选用这些device后,编译器自动加入一些特殊编译选项而已;2、 设置Project:Target:存储器模型(small/compact/large)Output:指定是否创建HEX文件,还可以指定make之后做后续动作(如转换成asc文件、bin文件

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论