C51有三种循环语句即while,do-while和for,这三种循环都可以用来处理同一问题,基本上三者可以相互替换.但由于C51是针对51汇编语言的编译器,如果不注意51汇编指令的特点,不同的编程方式可能得到不同的程序性能(执行速度和代码长度).以计算1+2+3+…+9+10为例,下面做一对比.程序1:unsigned char i;unsigned char sum;for(i=1,sum=0;i<11;i++){sum+=i;}汇编代码为:C:0x0003 7F01 MOV R7,#0x01C:0x0005 E4 CLR AC:0x0006 FE MOV R6,AC:0x0007 EF MOV A,R7C:0x0008 2E ADD A,R6C:0x0009 FE MOV R6,AC:0x000A 0F INC R7C:0x000B BF0BF9 CJNE R7,#0x0B,C:0007代码长度(字节):11,执行周期(机器周期):63程序2:unsigned char i;unsigned char sum;for(i=10,sum=0;i;i–){sum+=i;}汇编代码为:C:0x000F 7F0A MOV R7,#0x0AC:0x0011 E4 CLR AC:0x0012 FE MOV R6,AC:0x0013 EF MOV A,R7C:0x0014 2E ADD A,R6C:0x0015 FE MOV R6,AC:0x0016 DFFB DJNZ R7,C:0013代码长度(字节):9,执行周期(机器周期):53程序3:unsigned char i=11;unsigned char sum=0;while(i–){sum+=i;}汇编代码为:C:0x0003 7F0A MOV R7,#0x0BC:0x0005 E4 CLR AC:0x0006 FE MOV R6,AC:0x0007 AD07 MOV R5,0x07C:0x0009 1F DEC R7C:0x000A ED MOV A,R5C:0x000B 6005 JZ C:0012C:0x000D EF MOV A,R7C:0x000E 2E ADD A,R6C:0x000F FE MOV R6,AC:0x0010 80F5 SJMP C:0007代码长度(字节):15,执行周期(机器周期):130
从以上三个不同程序可以看出,其运算结果都是0x37(55),但最短代码为9,最长代码为15,最快速度为53,最慢速度为130,可见三个程序的性能差异较大.本文引用地址:http://www.eepw.com.cn/article/201611/323326.htm
如何编出占用空间小运行效率高的循环代码呢?在C51编译环境下要写出优秀的循环代码必须熟悉51汇编语言的指令系统.观察程序2,循环控制指令使用了DJNZ循环转移指令,该指令同时完成计数和循环判断两种操作,而且只占用两个字节,是51指令系统中最为高效的循环指令,因此在设计循环程序时,应尽可能使C51将DJNZ用于循环程序中.当然DJNZ指令的循环次数是确定的,主要用在有确定循环次数的情况.
DJNZ指令的一个最大特点是递减计数,因此循环程序必须采用递减方式才有可能编译出DJNZ指令,如以上程序2.DJNZ指令的另一个特点是先减后判断,因此设计循环程序也必须坚持先减后判断的原则,否则得不到DJNZ指令,如以上程序3.如果将程序3改写为:
unsigned char i=10;
unsigned char sum=0;
while(i)
{
sum+=i;
i–;
}
就可以得到与程序2相同的汇编代码.若i–后还有其它操作,比如改为:
unsigned char i=10,j=0;
unsigned char sum=0;
while(i)
{
sum+=i;
i–;
j++;
}
也得不到DJNZ汇编指令,也就是说,循环语句在执行过程中,减1与判断必须是连续的,且减1在前,判断在后.对于while循环,当将减1与判断合成一步时,应当采用while(–i).按照以上所述,do-while循环同样可以汇编出DJNZ指令,不再一一列举.
但是当循环变量不是通过常数赋值语句完成,而是来自于另一个变量时,for和while语句无论采用何种控制流程都不能产生DJNZ指令,因为这两种循环都是先判断后执行的控制逻辑,而DJNZ的执行过程是先执行循环体后进行循环判断.按照DJNZ的控制流程,只有do-while语句符合这个条件,因此当循环次数不是常量而是变量时,就必须使用do-while循环语句了.
综上所述,若要使用DJNZ指令提高程序效率,在设计循环程序中应坚持以下三大原则:
① 采用递减计数;
② 先减后判断,减与判断连续进行;
③ 循环次数为变量时,采用do-while循环.
8051单片机有两条循环指令,即DJNZ Rn,rel和DJNZ direct,rel.对于基本型单片机而言,两者的执行时间都是2个机器周期,但两者的指令长度不同,前者占用2个字节,后者占用3个字节.循环程序还涉及到循环变量初始化操作,对于前者使用MOV Rn,#XX,2字节1周期,对于后者使用MOV direct,#XX,3字节2周期.以单层循环为例,使用工作寄存器比直接地址节省2字节1周期.除此之外,两者相比,更重要的性能差异在于后者需要再分配一个内存单元.因为通常程序模块都使用工作寄存器作局部变量,将工作寄存器用作循环变量不会增加内存占用量.总之,使用工作寄存器作循环计数器是设计循环程序应坚持的一项重要原则.
一般情况下,C51编译器将循环次数赋予工作寄存器,比如
unsigned char i;
for(i=100;i;i–)
{
dosomething();
}
但是存在下述情况之一时,C51编译的结果往往令人不满意:
① 函数dosomething是一个外部定义C语言函数;
② 函数dosomething是一个具有C语言接口,内部用汇编语言实现的,供C程序调用的外部函数.
以上两种情况循环变量i都存放在内存单元中,即采用直接寻址方式.对于局部变量i,C51编译器采用了直接地址存储,其原因在于基于这种假设,即在无任何特殊处理的情况下,C51默认外部函数占用所有工作寄存器,因此在循环的外部,不能修改这些已被占用的寄存器,C51只能将循环控制变量分配在内存地址单元中.但如果循环体语句中仅使用少数几个或甚至根本不使用工作寄存器,编译器仍按这种假设处理,那么编译器就不能显现出它的高效性了.幸运的是,C51提供了弥补这一缺陷的伪指令REGUSE.REGUSE伪指令用于告知编译器某函数或子程序占用了哪些寄存器或特殊功能寄存器SFR,编译器根据函数提供的寄存器占用信息就可能将循环变量分配到循环体未占用的寄存器中,从而达到优化设计的目的.
另外,一项开关必须打开,即Global Register Coloring,方法是勾选Project – Options for Target – C51 – Global Register Coloring.
在情况①中,应在函数dosomething所在源程序文件中添加代码(假设函数占用A和B):
#pragma asm
$REGUSE dosomething(A,B)
#pragma endasm
重新编译项目后,在汇编窗口中可以看到,循环变量已使用了工作寄存器.
在情况②中,由于是汇编程序,只需增添一行代码(也假设子程序占用A和B):
$REGUSE dosomething(A,B)
同样可以观察到循环变量改成了工作寄存器实现.
需要注意的是,这里所说的寄存器占用是指在函数或子程序执行过程中可能或肯定对这些寄存器造成破坏,即执行写操作,对于只读寄存器不应按占用处理.另外,参数传递使用的工作寄存器不必指明.