前言
最近自學STC公司的8051系列單片機,編程中如流水燈等非精確延時多用軟件延時實現,寫了幾個類似DelayX10us(unsigned char x)的函數方便調用,函數內部的語句多是用STC官方延時程序再自己套一個for或者do..while循環改造而成,像這樣:
//非精確延時10*Xus
//@12.000MHz 12T模式 void DelayX10us(unsigned char x) { unsigned char i; for (; x > 0; x--) { _nop_(); i = 2; while (--i); } }
由於不懂匯編,所以對代碼的實際延時時間一直沒有深究,每次都是憑感覺摸索個大概。今天突然心血來潮在keil仿真中執行了一下以上代碼,觀察了一下延時時間,得到結果如下:
X | 延時目標(us) | 實際延時(us) | 誤差 |
1 | 10 | 24 | 140% |
10 | 100 | 150 | 50% |
100 | 1000 | 1410 | 41% |
OMG,100us誤差達到50%,延時1000us誤差也有41%,這還真是“非(常的)精確”啊。
突然覺得有必要研究一下匯編代碼,搞懂這個延時是怎么誤差這么大的。學習嘛,就不該留盲點,也正好借此機會了解一下匯編語言,對理解單片機底層應該有一定幫助。如果編程人員對自己寫的代碼底層如何實現一清二楚,那溢出、內存泄漏什么的bug就絕不會存在了。當然,要達到這個理想情況是很難的,只能朝着這個方向多努力了。
寫了一段代碼做研究用,如下:

#include <reg52.h> #include <intrins.h> void DelayX10us(unsigned char x); void main() { DelayX10us(1); DelayX10us(10); DelayX10us(100); while (1); } //@12.000MHz 12T void DelayX10us(unsigned char x) { unsigned char i; for (; x > 0; x--) { _nop_(); i = 2; while (--i); } }
反匯編代碼
順便說一下,軟件環境:Keil uvison 4。
上述代碼編譯完后,點擊"Start Debug"開始調試,Disassembly窗口中就顯示出了相應的反匯編代碼,還顯示了C語言與匯編代碼的對應關系,比在Linux環境下調試方便多了。
main()函數:
DelayX10us()函數
查芯片手冊中指令系統部分內容可知,上述代碼中LCALL、SJMP、JC、DJNZ、RET這幾個指令是2機器周期指令,其余是1機器周期指令。現在開始來計算延時時間:
x=1:
main()中 | for循環 | 返回 | 總 計 | |
機器周期 | 1+2 | (1+1+1+2 +1+1+2*2 +1+2)*1 +1+1+1+2 | 2 | 24 |
說明:1、main()中傳值和跳轉兩個操作周期為1+2。
2、0x0016 SUBB A,0x00 為執行借位減法,可以簡單理解為將A-0x00-Cy(進位借位標識,也就是上一句中的C)的結果裝入A,並判斷如果夠減(結果>=0),Cy=0(未產生借位);如果不夠減(結果<0),Cy=1(產生借位)。所以當A>=1時,都夠減,Cy=0,下一句JC不會跳轉,直到A=0不夠減時才跳轉。(A就是X的值)
3、for循環中,第一次從0x0014到0x0020執行完,周期數為1+1+1+2 +1+1+2*2 +1+2,此時R7寄存器中存儲的x值為0;此時已跳轉到0x0014繼續執行,直到0x0018,跳轉到0x0022,周期數為1+1+1+2。返回main()函數又花兩個周期。所以main()中"DelayX10us(1);"共耗費24個,12M/12T模式下即為24us。
同理,x=10:
main()中 | for循環 | 返回 | 總 計 | |
機器周期 | 1+2 | (1+1+1+2 +1+1+2*2 +1+2)*10 +1+1+1+2 | 2 | 150 |
x=100時同理1+2 +(1+1+1+2+1+1+2*2+1+2)*100 +1+1+1+2 +2 = 1410
小結
綜上可看出,單純的在官方延時函數基礎上套for循環而得到的延時相當不精確。分析誤差原因可知,main()中的3個周期、子函數返回的2個周期、for循環末尾的(1+1+1+2)個周期,這10個機器周期是固定誤差值,最關鍵的在於塗黃部分共14個周期,超出了預期的10us倍增的延時。把這部分稍微改一下,使括號內塗黃部分變為10個機器周期,這樣子就能使所有的x倍延時的誤差值都為固定誤差10us了。更改后的代碼如下:
//非精確延時10*X us,固定誤差10us //@12.000MHz 12T模式 void DelayX10us(unsigned char x) { unsigned char i; for (; x > 0; x--) { _nop_(); _nop_(); } }
更改后的延時機器周期數=1+2 +(1+1+1+2 +1+1 +1+2)*X +1+1+1+2 +2 = 10*X+10。X在1~255取值范圍內,誤差均為固定10us。
PS:本文所有延時都是在12MHz晶振、12T模式下計算,1個機器周期=1us。
反匯編代碼為Keil軟件內代碼優化等級level 8下編譯后的反匯編。不同優化等級編譯的代碼反匯編后有稍許差別,再次不做論述。