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,#0x01
C:0x0005 E4 CLR A
C:0x0006 FE MOV R6,A
C:0x0007 EF MOV A,R7
C:0x0008 2E ADD A,R6
C:0x0009 FE MOV R6,A
C:0x000A 0F INC R7
C: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,#0x0A
C:0x0011 E4 CLR A
C:0x0012 FE MOV R6,A
C:0x0013 EF MOV A,R7
C:0x0014 2E ADD A,R6
C:0x0015 FE MOV R6,A
C: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,#0x0B
C:0x0005 E4 CLR A
C:0x0006 FE MOV R6,A
C:0x0007 AD07 MOV R5,0x07
C:0x0009 1F DEC R7
C:0x000A ED MOV A,R5
C:0x000B 6005 JZ C:0012
C:0x000D EF MOV A,R7
C:0x000E 2E ADD A,R6
C:0x000F FE MOV R6,A
C:0x0010 80F5 SJMP C:0007
代码长度(字节):15,执行周期(机器周期):130
从以上三个不同程序可以看出,其运算结果都是0x37(55),但最短代码为9,最长代码为15,最快速度为53,最慢速度为130,可见三个程序的性能差异较大.
如何编出占用空间小运行效率高的循环代码呢?在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循环.