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循環.
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)
同樣可以觀察到循環變量改成了工作寄存器實現.
需要注意的是,這里所說的寄存器占用是指在函數或子程序執行過程中可能或肯定對這些寄存器造成破壞,即執行寫操作,對于只讀寄存器不應按占用處理.另外,參數傳遞使用的工作寄存器不必指明.