第一节 绝对地址访问
C51提供了三种访问绝对地址的方法:
1. 绝对宏:
在程序中,用“#include”即可使用其中定义的宏来访问绝对地址,包括:CBYTE、XBYTE、PWORD、DBYTE、CWORD、XWORD、PBYTE、DWORD,具体使用可看一看absacc.h便知。
例如:
rval=CBYTE[0x0002]; //指向程序存贮器的0002h地址 rval=XWORD[0x0002]; //指向外RAM的0004h地址
2. _at_关键字
直接在数据定义后加上_at_ const即可,但是注意:
(1)绝对变量不能被初使化;
(2)bit型函数及变量不能用_at_指定。
例如:
idata struct link list _at_ 0x40; //指定list结构从40h开始。 xdata char text[25b] _at_ 0xE000; //指定text数组从0E000H开始
提示:如果外部绝对变量是I/O端口等可自行变化数据,需要使用volatile关键字进行描述,请参考absacc.h。
3. 连接定位控制
此法是利用连接控制指令code xdata pdata \data bdata对“段”地址进行,如要指定某具体变量地址,则很有局限性,不作详细讨论。
第二节 Keil C51与汇编的接口
1. 模块内接口
方法是用#pragma语句具体结构是:
#pragma asm //汇编行 #pragma endasm
这种方法实质是通过asm与ndasm告诉C51编译器中间行不用编译为汇编行,因而在编译控制指令中有SRC以控制将这些不用编译的行存入其中。
2. 模块间接口
C模块与汇编模块的接口较简单,分别用C51与A51对源文件进行编译,然后用L51将obj文件连接即可,关键问题在于C函数与汇编函数之间的参数传递问题,C51中有两种参数传递方法。
(1) 通过寄存器传递函数参数
最多只能有3个参数通过寄存器传递,规律如下表:
传递的参数 | char、1字节指针 | int、2字节指针 | long、float | 一般指针 |
第一个参数 | R7 | R6,R7 | R4~R7 | R1,R2,R3 |
第二个参数 | R5 | R4,R5 | R4~R7 | R1,R2,R3 |
第三个参数 | R3 | R2,R3 | 无 | R1,R2,R3 |
(2) 通过固定存储区传递(fixed memory)
这种方法将bit型参数传给一个存储段中:?function_name?BIT
将其它类型参数均传给下面的段:?function_name?BYTE,且按照预选顺序存放。至于这个固定存储区本身在何处,则由存储模式默认。
(3) 函数的返回值
函数返回值一律放于寄存器中,有如下规律:
返回值类型 |
寄存器 |
说明 |
Bit |
C |
由具体标志位返回 |
char/unsigned char / 1 byte 指针 |
R7 |
|
int/unsigned int / 2 byte 指针 |
R6&R7 |
高位在R6 |
long/unsigned long / 3 byte 指针 |
R4-R7 |
高位在R4 |
float |
R4-R7 |
32bit IEEE格式,指数和符号位在R7 |
通用指针 |
R1-R3 |
存储类型在R3,高位在R2 |
(4) SRC控制
该控制指令将C文件编译生成汇编文件(.SRC),该汇编文件可改名后,生成汇编.ASM文件,再用A51进行编译。
第三节 Keil C51软件包中的通用文件
在C51\LiB目录下有几个C源文件,这几个C源文件有非常重要的作用,对它们稍事修改,就可以用在自己的专用系统中。
1. 动态内存分配
init_mem.C:此文件是初始化动态内存区的程序源代码。它可以指定动态内存的位置及大小,只有使用了init_mem( )才可以调回其它函数,诸如malloc calloc,realloc等。
calloc.c:此文件是给数组分配内存的源代码,它可以指定单位数据类型及该单元数目。
malloc.c:此文件是malloc的源代码,分配一段固定大小的内存。
realloc.c:此文件是realloc.c源代码,其功能是调整当前分配动态内存的大小。
2. C51启动文件STARTUP.A51
启动文件STARTUP.A51中包含目标板启动代码,可在每个project中加入这个文件,只要复位,则该文件立即执行,其功能包括:
定义内部RAM大小、外部RAM大小、可重入堆栈位置
清除内部、外部或者以此页为单元的外部存储器
按存储模式初使化重入堆栈及堆栈指针
初始化8051硬件堆栈指针
向main( )函数交权
开发人员可修改以下数据从而对系统初始化
常数名 意义
IDATALEN 待清内部RAM长度
XDATA START 指定待清外部RAM起始地址
XDATALEN 待清外部RAM长度
IBPSTACK 是否小模式重入堆栈指针需初始化标志,1为需要。缺省为0
IBPSTACKTOP 指定小模式重入堆栈顶部地址
XBPSTACK 是否大模式重入堆栈指针需初始化标志,缺省为0
XBPSTACKTOP 指定大模式重入堆栈顶部地址
PBPSTACK 是否Compact重入堆栈指针,需初始化标志,缺省为0
PBPSTACKTOP 指定Compact模式重入堆栈顶部地址
PPAGEENABLE P2初始化允许开关
PPAGE 指定P2值
PDATASTART 待清外部RAM页首址
PDATALEN 待清外部RAM页长度
提示:如果要初始化P2作为紧凑模式高端地址,必须:PPAGEENAGLE=1,PPAGE为P2值,例如指定某页1000H-10FFH,则PPAGE=10H,而且连接时必须如下:
L51 PDATA(1080H),其中1080H是1000H-10FFH中的任一个值。
以下是STARTUP.A51代码片断,红色是经常可能需要修改的地方:
;------------------------------------------------------------------------------ ; This file is part of the C51 Compiler package ; Copyright KEIL ELEKTRONIK GmbH 1990 ;------------------------------------------------------------------------------ ; STARTUP.A51: This code is executed after processor reset. ; ; To translate this file use A51 with the following invocation: ; ; A51 STARTUP.A51 ; ; To link the modified STARTUP.OBJ file to your application use the following ; L51 invocation: ; ; L51 , STARTUP.OBJ ; ;------------------------------------------------------------------------------ ; ; User-defined Power-On Initialization of Memory ; ; With the following EQU statements the initialization of memory ; at processor reset can be defined: ; ; ; the absolute start-address of IDATA memory is always 0 IDATALEN EQU 80H ; the length of IDATA memory in bytes. ; XDATASTART EQU 0H ; the absolute start-address of XDATA memory XDATALEN EQU 0H ; the length of XDATA memory in bytes. ; PDATASTART EQU 0H ; the absolute start-address of PDATA memory PDATALEN EQU 0H ; the length of PDATA memory in bytes. ; ; Notes: The IDATA space overlaps physically the DATA and BIT areas of the ; 8051 CPU. At minimum the memory space occupied from the C51 ; run-time routines must be set to zero. ;------------------------------------------------------------------------------ ; ; Reentrant Stack Initilization ; ; The following EQU statements define the stack pointer for reentrant ; functions and initialized it: ; ; Stack Space for reentrant functions in the SMALL model. ; set to 1 if small reentrant is used. ; set top of stack to highest location+1. ; ; Stack Space for reentrant functions in the LARGE model. ; set to 1 if large reentrant is used. ; set top of stack to highest location+1. ; ; Stack Space for reentrant functions in the COMPACT model. ; set to 1 if compact reentrant is used. ; set top of stack to highest location+1. ; ;------------------------------------------------------------------------------ ; ; Page Definition for Using the Compact Model with 64 KByte xdata RAM ; ; The following EQU statements define the xdata page used for pdata ; variables. The EQU PPAGE must conform with the PPAGE control used ; in the linker invocation. ; ; set to 1 if pdata object are used. ; define PPAGE number. ; ;------------------------------------------------------------------------------
3. 标准输入输出文件
putchar.c是一个低级字符输出子程,开发人员可修改后应用到自己的硬件系统上,例如向CLD或LEN输出字符。
缺省:putchar.c是向串口输出一个字符XON|XOFF是流控标志,换行符“\*n”自动转化为回车/换行“\r\n”。
getkey函数是一个低级字符输入子程,该程序可用到自己硬件系统,如矩阵键盘输入中,缺省时通过串口输入字符。
4. 其它文件
还包括对Watch-Dog有独特功能的INIT.A51函数以及对8×C751适用的函数,可参考源代码。
第四节 段名协定与程序优化
1. 段名协定(Segment Naming Conventions)
C51编译器生成的目标文件存放于许多段中,这些段是代码空间或数据空间的一些单元,一个段可以是可重定位的,也可以是绝对段,每一个可重定位的段都有一个类型和名字,C51段名有以下规定:
每个段名包括前缀与模块名两部分,前缀表示存储类型,模块名则是被编译的模块的名字,例如:
?CO?main1 :表示main1模块中的代码段中的常数部分
?PR?function1?module 表module模块中函数function1的可执行段,具体规定参阅手册。
2. 程序优化
C51编译器是一个具有优化功能的编译器,它共提供六级优化功能。确保生成目标代码的最高效率(代码最少,运行速度最快)。具体六级优化的内容可参考帮助。
在C51中提供以下编译控制指令控制代码优化:
OPTIMIZE(SJXE):尽量采用子程序,使程序代码减少。
NOAREGS:不使用绝对寄存器访问,程序代码与寄存器段独立。
NOREGPARMS:参数传递总是在局部数据段实现,程序代码与低版本C51兼容。
OPTIMIZE(SIZE)AK OPTIMIZE(speed)提供6级优化功能,缺省为: OPTIMIZE(6,SPEED)。
第五节 Keil C51的代码效率
一、存储模式的影响
存储模式决定了缺省变量的存储空间,而访问各空间变量的汇编代码的繁简程度决定了代码率的高低。
例如:一个整形变量i,如放于内存18H、19H空间,则++i的操作编译成四条语句:
INC 0x19 MOV A,0x19 JNZ 0x272D INC 0x18 0x272D:
而如果放于外存空间0000H、0001H则++i的操作编译成九条语句:
MOVX A,@ DPTR INC A MOVX @ DPTR,A MOVX A,@DPTR INC A MOVX @ DPTR,A
就汇编之后的语句而言,对外部存储器的操作较内部存储器操作代码率要低得多,生成的语句为内存的两倍以上,而程序中有大量的这种操作,可见存储模式对代码率的响了。因此程序设计的原则是
1、存储模式从small-Compact-large依次选择,实在是变量太多,才选large模式。
2、即使选择了large模式,对一些常用的局部的或者可放于内存中的变量,最好放于内存中,以尽量提高程序的代码率。
二、 程序结构的影响
程序的结构单元包括模块、函数等等。同样的功能,如果结构越复杂,其所涉及的操作、变量、功能模块函数等就越多,较之结构性好,代码简单的程序其代码率自然就低得多。
此外程序的运行控制语句,也是影响代码率的关键因素,例如:switch -case语句,许多编译器都把它们译得非常复杂,Keil C51也不例外,相对较为简易的Switch-case语句,编译成跳转指令形式,代码率较高,但对较为复杂的Switch-Case,则要调用一个系统库函数?C?ICASE进行处理,非常复杂。
再如if( ),while( ),等语句也是代码相对较低的语句,但编译以后比switch-case要高得多。因此建议设计者尽量少用switch-case之类语句来控制程序结构,以提高代码率。
除以上两点外,其它因素也会对代码率产生影响,例如:
是否用寄存器传递参数 即NOAREGS选项是否有
是否包括调试信息:即DEBUG选项
是否包括扩展的调试信息:即BJECTEXTEND
第六节 如何优化C语言代码(程序员必读)
1、选择合适的算法和数据结构
应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率。.选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。
数组与指针语句具有十分密码的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。但是在Keil中则相反,使用数组比使用的指针生成的代码更短。
2、使用尽量小的数据类型
能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。
在ICCAVR中,可以在Options中设定使用printf参数,尽量使用基本型参数(%c、%d、%x、%X、%u和%s格式说明符),少用长整型参数(%ld、%lu、%lx和%lX格式说明符),至于浮点型的参数(%f)则尽量不要使用,其它C编译器也一样。在其它条件不变的情况下,使用%f参数,会使生成的代码的数量增加很多,执行速度降低。
3、使用自加、自减指令
通常使用自加、自减指令和复合赋值表达式(如a-=1及a+=1等)都能够生成高质量的程序代码,编译器通常都能够生成inc和dec之类的指令,而使用a=a+1或a=a-1之类的指令,有很多C编译器都会生成二到三个字节的指令。在AVR单片适用的ICCAVR、GCCAVR、IAR等C编译器以上几种书写方式生成的代码是一样的,也能够生成高质量的inc和dec之类的的代码。
4、减少运算的强度
可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下:
(1)、求余运算。
a=a%8;
可以改为:
a=a&7;
说明:位操作只需一个指令周期即可完成,而大部分的C编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n方的余数,均可使用位操作的方法来代替。
(2)、平方运算
a=pow(a,2.0);
可以改为:
a=a*a;
说明:在有内置硬件乘法器的单片机中(如51系列),乘法运算比求平方运算快得多,因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR单片机中,如ATMega163中,乘法运算只需2个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。
如果是求3次方,如:
a=pow(a,3.0);
更改为:
a=a*a*a;
则效率的改善更明显。
(3)、用移位实现乘除法运算
a=a*4;
b=b/4;
可以改为:
a=a<<2;
b=b>>2;
说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:
a=a*9
可以改为:
a=(a<<3)+a
5、循环
(1)、循环语
对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init的初始化程序中进行。
(2)、延时函数:
通常使用的延时函数均采用自加的形式:
void delay (void)
{
unsigned int i;
for (i=0;i<1000;i++)
;
}
将其改为自减延时函数:
void delay (void)
{
unsigned int i;
for (i=1000;i>0;i--)
;
}
两个函数的延时效果相似,但几乎所有的C编译对后一种函数生成的代码均比前一种代码少1~3个字节,因为几乎所有的MCU均有为0转移的指令,采用后一种方式能够生成这类指令。在使用while循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3个字母。但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。
(3)while循环和do…while循环
用while循环时有以下两种循环形式:
unsigned int i;
i=0;
while (i<1000)
{
i++;
//用户程序
}
或:
unsigned int i;
i=1000;
do
i--;
//用户程序
while (i>0);
在这两种循环中,使用do…while循环编译后生成的代码的长度短于while循环。
6、查表
在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启了,减少了程序执行过程中重复计算的工作量。
7、其它
比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化