PIC單片機 C編程技巧
PICC和MPLAB集成:
PICC有自己的文本編輯器,不過是DOS風格的,看來PICC的工程師 要專業(yè)冷到酷底了...
大家大可不必用它,如果你沒什么癖好的話,你不會不用UltraEdit 吧?
1:建立你的工作目錄:
建 議在C盤根目錄下建立一個以A開頭的文件夾做為工作目錄.因為你會發(fā)現(xiàn)它總是在你查找文件時候第
一個跳入你眼中.
2:MPLAB調用 PICC.(以MPLAB5.7版本為例子)
啟動MPLAB.在Project-->Install Language Tool:
Language Suite----->hi-tech picc
Tool Name ---->PICC Compiler
Executable ---->c:hi-picinpicc.exe (假如你的PICC是默認安裝的)
選Command-line
最后OK.
上 面這步只需要設定一次,除非你重新安裝了MPLAB.
3:創(chuàng)建你的項目文件:(假如你實現(xiàn)用EDIT編輯好了一個叫AA.C的C代碼文件)
Project-->New Project-->File Name--->myc (假如我們把項目文件取名字叫MYC.PJT)
右邊窗口當然要選擇中你的 工作目錄.然后OK.
4:設定你的PICC工作參數(shù):
Project-->Edit Project
上面4個欄目就用默認 的,空的也就讓它空著,無所謂的.
需要修改的是:
Development Mode---->選擇你的PIC型號.當然要選擇Mplab SIM Simulator
讓你可以用軟件仿真.
Language Tool Suite--->HI-TECH PICC
上面的步驟,你可能會遇見多個提示條,不要管它,一路確定.
下面是 PICC編譯器的選擇項:
雙擊Project Files 窗口里面的MYC.HEX,出現(xiàn)一個選擇攔目.命令很多,大家可以看PICC文本編
輯 器里面的HELP,里面有詳細說明.
下面就推薦幾個常用也是建議用的:
Generate debug info 以及下面的2項.
Produce assembler list file
就在它們后面打勾即可,其它的不要管,除非你有特殊要求.
5:添加你的C代碼文件:
當 進行了前面幾步后,按Add Node 找到AA.C文件就OK了.
6:編譯C代碼:
最簡單的一步:直接按下F10.
編譯完后, 會出現(xiàn)各種調試信息.C代碼對應的匯編代碼就是工作目錄里面的AA.IST,用EDIT
打開可以看見詳細的對比.
7:其它,要是一切都沒 問題,那么你就可以調試和燒片了,和以往操作無異.
2、如何從匯編轉向PICC
首先要求你要有C 語言的基礎。PICC 不支持C++,這對于習慣了C++的朋友還得翻翻C 語言的書。C
代碼的頭文件一定要有#i nclude
入相應的其它頭文件。這點比匯編 好用。載入的頭文件中其實是聲明芯片的寄存器和一些函數(shù)。順便摘抄
一個片段:
static volatile unsigned char TMR0 @ 0x01;
static volatile unsigned char PCL @ 0x02;
static volatile unsigned char STATUS @ 0x03;
可以看出和匯編的頭文件中定義寄存器是差不多的。如下:
TMR0 EQU 0X01;
PCL EQU 0X02;
STATUS EQU 0X03;
都是把無聊的地址定義為大家公認的名字。
一: 怎么附值?
如對TMR0 附值,匯編中:
MOVLW 200;
MOVWF TMR0;
當然得保證當前頁面在0,不然會出 錯。
C 語言:
TMR0=200;//無論在任何頁面都不會出錯。
可以看出來C 是很直接了當?shù)?。并且最大好處是操作一個寄存器時候,不用考慮頁面的問題。一切由
C 自動完成。
二:怎么位操作?
匯編中的位操作 是很容易的。在C 中更簡單。C 的頭文件中已經對所有可能需要位操作的寄存器的每
一位都有定義名稱:
如:PORTA 的每一個I/O 口定義為:RA0、RA1、RA2。。。RA7。OPTION 的每一位定義為:PS0、
PS1、PS2 、PSA 、T0SE、T0CS、INTEDG 、RBPU??梢詫ζ渲苯舆M行運算和附值。
如:
RA0=0;
RA2=1;
在匯編中 是:
BCF PORTA,0;
BSF PORTA,2;
可以看出2 者是大同小異的,只是C 中不需要考慮頁面的問題。
三: 內存分配問題:
在匯編中定義一個內存是一件很小心的問題,要考慮太多的問題,稍微不注意就會出錯。比如16 位的
運算等。用C 就不需要考慮太多。下面給個例子:
16 位的除法(C 代碼):
INT X=5000;
INT Y=1000;
INT Z=X/Y;
而在匯編中則需要花太多精力。
給一個小的C 代碼,用RA0 控制一個LED 閃爍:
#i nclude
void main()
{
int x;
CMCON=0B111; //掉A 口比較器,要是有比較器功能的話。
ADCON1=0B110; //掉A/D 功能,要是有A/D 功能的話。
TRISA=0; //RA 口全為輸出。
loop:RA0=!RA0;
for(x=60000;--x;){;} //延時
goto loop;
}
說 說RA0=!RA0 的意思:PIC 對PORT 寄存器操作都是先讀取----修改----寫入。上句的含義是程序先
讀RA0,然后取反,最后 把運算后的值重新寫入RA0,這就實現(xiàn)了閃爍的功能。
3、淺談PICC 的位操作
由于PIC 處理器對位操作是最高效的,所以把一些BOOL 變量放在一個內存的位中,既可以達到運算
速度快,又可以達到最大限度節(jié)省空間的目的。在C 中的位操作有多種選擇。
*********************************************
如:char x;x=x|0B00001000; /*對X 的4 位置1。*/
char x;x=x&0B11011111; /*對X 的5 位清0。*/
把上面的變成公式則是:
#define bitset(var,bitno)(var |=1<
char x;bitclr(x,5)
*************************************************
但 上述的方法有缺點,就是對每一位的含義不直觀,最好是能在代碼中能直觀看出每一位代表的意思,
這樣就能提高編程效率,避免出錯。如果我們想用X 的0-2 位分別表示溫度、電壓、電流的BOOL 值可以
如下:
unsigned char x @ 0x20; /*象匯編那樣把X 變量定義到一個固定內存中。*/
bit temperature@ (unsigned)&x*8+0; /*溫度*/
bit voltage@ (unsigned)&x*8+1; /*電壓*/
bit current@ (unsigned)&x*8+2; /*電流 */
這樣定義后X 的位就有一個形象化的名字,不再是枯燥的1、2、3、4 等數(shù)字了??梢詫 全局修改,
也可以對每一位進行操作:
char=255;
temperature=0;
if(voltage)......
*****************************************************************
還 有一個方法是用C 的struct 結構來定義:
如:
struct cypok{
temperature:1; /*溫度*/
voltage:1; /*電壓*/
current:1; /*電流*/
none:4;
}x @ 0x20;
這樣就可以用
x.temperature=0;
if(x.current)....
等 操作了。
**********************************************************
上面 的方法在一些簡單的設計中很有效,但對于復雜的設計中就比較吃力。如象在多路工業(yè)控制上。
前端需要分別收集多路的多路信號,然后再設定控制多路的 多路輸出。如:有2 路控制,每一路的前端信
號有溫度、電壓、電流。后端控制有電機、喇叭、繼電器、LED。如果用匯編來實現(xiàn)的話,是很頭疼的事
情, 用C 來實現(xiàn)是很輕松的事情,這里也涉及到一點C 的內存管理(其實C 的最大優(yōu)點就是內存管理)。
采用如下結構:
union cypok{
struct out{
motor:1; /*電機*/
relay:1; /*繼電器*/
speaker:1; /*喇叭*/
led1:1; /*指示燈*/
led2:1; /*指示燈*/
}out;
struct in{
none:5;
temperature:1; /*溫度*/
voltage:1; /*電壓*/
current:1; /*電流*/
}in;
char x;
};
union cypok an1;
union cypok an2;
上面的結構有什么好處呢?
細分了信號的路an1 和an2;
細 分了每一路的信號的類型(是前端信號in 還是后端信號out):
an1.in ;
an1.out;
an2.in;
an2.out;
然 后又細分了每一路信號的具體含義,如:
an1.in.temperature;
an1.out.motor;
an2.in.voltage;
an2.out.led2; 等
這樣的結構很直觀的在2 個內存中就表示了2 路信號。并且可以極其方便的擴充。
如添加更多路的信號,只需要添加:
union cypok an3;
union cypok an4;
從上面就可以看出用C 的巨大好處
4、PICC 之延時函數(shù)和循環(huán)體優(yōu)化。
很多朋友說C 中不能精確控制延時時間,不能象匯編那樣直觀。其實不然,對延時函數(shù)深入了解一下
就能設計出一個 理想的框價出來。一般的我們都用for(x=100;--x;){;}此句等同與x=100;while(--x){;};
或for(x=0; x<100;x++){;}。
來寫一個延時函數(shù)。
在這里要特別注意:X=100,并不表示只運行100 個指令時間就跳出循環(huán)。
可 以看看編譯后的匯編:
x=100;while(--x){;}
匯編后:
movlw 100
bcf 3,5
bcf 3,6
movwf _delay
l2 decfsz _delay
goto l2
return
從代碼可以看出 總的指令是是303 個,其公式是8+3*(X-1)。注意其中循環(huán)周期是X-1 是99 個。這
里總結的是x 為char 類型的循環(huán)體,當x 為int 時候,其中受X 值的影響較大。建議設計一個char 類型的
循環(huán)體,然后再用一個循環(huán)體來調用它,可以實現(xiàn)精確的長時間的延時。下 面給出一個能精確控制延時的
函數(shù),此函數(shù)的匯編代碼是最簡潔、最能精確控制指令時間的:
void delay(char x,char y){
char z;
do{
z=y;
do{;}while(--z);
}while(--x);
}
其 指令時間為:7+(3*(Y-1)+7)*(X-1)如果再加上函數(shù)調用的call 指令、頁面設定、傳遞參數(shù)
花掉的7 個指令。則是:14+(3*(Y-1)+7)*(X-1)。如果要求不是特別嚴格的延時,可以用這個函數(shù):
void delay(){
unsigned int d=1000;
while(--d){;}
}
此函數(shù)在4M 晶體下產生10003us 的延時,也就是10MS。如果把D 改成2000,則是20003us,以此類
推。有朋友不明白,為什么不用while(x--)后減量,來控制 設定X 值是多少就循環(huán)多少周期呢?現(xiàn)在看看編
譯它的匯編代碼:
bcf 3,5
bcf 3,6
movlw 10
movwf _delay
l2
decf _delay
incfsz _delay,w
goto l2
return
可 以看出循環(huán)體中多了一條指令,不簡潔。所以在PICC 中最好用前減量來控制循環(huán)體。
再談談這樣的語句:
for(x=100;--x;) {;}和for(x=0;x<100;x++){;}
從字面上看2 者意思一樣,但可以通過匯編查看代碼。后者代碼雍長,而前者就很好的匯編出了簡潔的代
碼。所以在PICC 中最好用前者的形式來寫循環(huán)體,好的C 編譯器會自動把增量循環(huán)化為減量循環(huán)。因為
這是由處理器硬件特性決定的。PICC 并不是一個很智能的C 編譯器,所以還是人腦才是第一的,掌握一些
經驗對寫出高效,簡潔的代碼是有好處的。
5、深入探討PICC之位操作
一:用位操作來 做一些標志位,也就是BOOL變量.可以簡單如下定義:
bit a,b,c;
PICC會自動安排一個內存,并在此內存中自動安排一位來對 應a,b,c.由于我們只是用它們來簡單的
表示一些0,1信息,所以我們不需要詳細的知道它們的地址\位究竟是多少,只管拿來就用好了.
二: 要是需要用一個地址固定的變量來位操作,可以參照PIC.H里面定義寄存器.
如:用25H內存來定義8個位變量.
static volatile unsigned char myvar @ 0x25;
static volatile bit b7 @ (unsigned)&myvar*8+7;
static volatile bit b6 @ (unsigned)&myvar*8+6;
static volatile bit b5 @ (unsigned)&myvar*8+5;
static volatile bit b4 @ (unsigned)&myvar*8+4;
static volatile bit b3 @ (unsigned)&myvar*8+3;
static volatile bit b2 @ (unsigned)&myvar*8+2;
static volatile bit b1 @ (unsigned)&myvar*8+1;
static volatile bit b0 @ (unsigned)&myvar*8+0;
這樣即可以對MYVAR操作,也可以對B0--B7直接位操作.
但不好的是,此招在 低檔片子,如C5X系列上可能會出問題.
還有就是表達起來復雜,你不覺得輸入代碼受累么?呵呵
三:這也是一些常用手法:
#define testbit(var, bit) ((var) & (1 <<(bit)))
//測試某一位,可以做BOOL運算
#define setbit(var, bit) ((var) |= (1 << (bit))) //把某一位置1
#define clrbit(var, bit) ((var) &= ~(1 << (bit))) //把某一位清0
付上一段代碼,可 以用MPLAB調試觀察
#i nclude
#define testbit(var, bit) ((var) & (1 <<(bit)))
#define setbit(var, bit) ((var) |= (1 << (bit)))
#define clrbit(var, bit) ((var) &= ~(1 << (bit)))
char a,b;
void main(){
char myvar;
myvar=0B10101010;
a=testbit(myvar,0);
setbit(myvar,0);
a=testbit(myvar,0);
clrbit(myvar,5);
b=testbit(myvar,5);
if(!testbit(myvar,3))
a=255;
else
a=100;
while(1){;}
}
四: 用標準C的共用體來表示:
#i nclude
union var{
unsigned char byte;
struct {
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
} bits;
};
char a,b;
void main(){
static union var myvar;
myvar.byte=0B10101010;
a=myvar.bits.b0;
b=myvar.bits.b1;
if(myvar.bits.b7)
a=255;
else
a=100;
while(1){;}
}
五: 用指針轉換來表示:
#i nclude
typedef struct {
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
} bits; //先定義一個變量的位
#define mybit0 (((bits *)&myvar)->b0) //取myvar
的地址(&myvar)強制轉換成 bits 類型的指針
#define mybit1 (((bits *)&myvar)->b1)
#define mybit2 (((bits *)&myvar)->b2)
#define mybit3 (((bits *)&myvar)->b3)
#define mybit4 (((bits *)&myvar)->b4)
#define mybit5 (((bits *)&myvar)->b5)
#define mybit6 (((bits *)&myvar)->b6)
#define mybit7 (((bits *)&myvar)->b7)
char myvar;
char a,b;
void main(){
myvar=0B10101010;
a=mybit0;
b=mybit1;
if(mybit7)
a=255;
else
a=100;
while(1){;}
}
[NextPage]
#i nclude
typedef struct {
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
} bits;
#define _paste(a,b) a##b
#define bitof(var,num) (((bits *)&(var))->_paste(b,num))
char myvar;
char a,b;
void main(){
a=bitof(myvar,0);
b=bitof(myvar,1);
if(bitof(myvar,7))
a=255;
else
a=100;
while(1){;}
}
有 必要說說#define _paste(a,b) a##b 的意思:
此語句是粘貼符號的意思,表示把b 符號粘貼到a 符號之后.
例子 中是
a=bitof(myvar,0);--->(((bits
*)& (myvar))->_paste(b,0))--->(((bits *)&(var))->b0)
可以看出 來,_paste(b,0)的作用是把0 粘貼到了b 后面,成了b0 符號.
總結:C語言的優(yōu)勢是能直接對低層硬件操作,代碼可以非常非常接近 匯編,上面幾個例子的位操作代碼
是100%的達到匯編的程度的.另一個優(yōu)勢是可讀性高,代碼靈活.上面的幾個位操作方法任由你選,
你不必 擔心會產生多余的代碼量出來.
6、在PICC 中使用常數(shù)指針。
常數(shù)指針使用非常靈活,可以給編程帶來很多便利。我測試過,PICC 也支持常數(shù)指針,并且也會自動
分頁,實在是一大喜事。
定義一個指向8 位RAM 數(shù)據(jù)的常數(shù)指針(起始為0x00):
#define DBYTE ((unsigned char volatile *) 0)
定義一個指向16 位RAM 數(shù)據(jù)的常數(shù)指針(起始為0x00):
#define CWORD ((unsigned int volatile *) 0)
((unsigned char volatile *) 0)中的0 表示指向RAM 區(qū)域的起始地址,可以靈活修改它。
DBYTE[x]中的x 表示偏移量。
下面是一段代碼1:
char a1,a2,a3,a4;
#define DBYTE ((unsigned char volatile *) 0)
void main(void){
long cc=0x89abcdef;
a1=DBYTE[0x24];
a2=DBYTE[0x25];
a3=DBYTE[0x26];
a4=DBYTE[0x27];
while(1);
}
2:
char a1,a2,a3,a4;
#define DBYTE ((unsigned char volatile *) 0)
void pp(char y){
a1=DBYTE[y++];
a2=DBYTE[y++];
a3=DBYTE[y++];
a4=DBYTE[y];
}
void main(void){
long cc=0x89abcdef;
char x;
x=&cc;
pp(x);
while(1);
}
3:
char a1,a2,a3,a4;
#define DBYTE ((unsigned char volatile *) 0)
void pp(char y){
a1=DBYTE[y++];
a2=DBYTE[y++];
a3=DBYTE[y++];
a4=DBYTE[y];
}
void main(void){
bank1 static long cc=0x89abcdef;
char x;
x=&cc;
pp(x);
while(1);
}
7、 PICC 關于unsigned 和 signed 的幾個關鍵問題!
unsigned 是表示一個變量(或常數(shù))是無符號類型。signed 表示有符號。它們表示數(shù)值范圍不一樣。
PICC 默認所有變量都是unsigned 類型的,哪怕你用了signed 變量。因為有符號運算比無符號運算耗資源,
而且MCU 運算一般不涉及有符號運算。在PICC 后面加上-SIGNED_CHAR 后綴可以告訴PICC 把signed
變量當作有符號處理。
在PICC 默認的無符號運算下看這樣的語句:
char i;
for(i=7;i>=0;i--){
; //中間語句
}
這樣的C 代碼看上去是沒有丁點錯誤的,但編譯后,問題出現(xiàn)了:
movlw 7
movwf i
loop
// 中間語句
decf i //只是遞減,沒有判斷語句!??!
goto loop
原因是當i 是0 時候,條件還成立,還得循環(huán)一次,直到i 成負1 條件才不成立。而PICC 在默認參數(shù)下是
不能判斷負數(shù)的,所以編譯過程出現(xiàn)問題。那么采用這 樣的語句來驗證:
char i;
i=7;
while(1){
i--;
//中間語句
if(i==0)break; //告訴PICC 以判斷i 是否是0 來作為條件
}
編譯后代碼正確:
movlw 7
movwf i
loop
// 中間語句
decfsz i //判斷是否是0
goto loop
再編譯這樣的語句:(同樣循環(huán)8 次)
for(i=8;i>0;i--){
;
}
movlw 8
movwf i
loop
decfsz i //同上編譯的代碼。
goto loop
再次驗證了剛才的分析。
在PICC 后面加上-SIGNED_CHAR 后綴,則第一個示例就正確編譯出來了,更證明了剛才的分析是正確的。
代碼如下:
movlw 7
movwf i
loop
//中間語句
decf i //遞減
btfss i,7 //判斷i 的7 位來判斷是否為負數(shù)
goto l94
總結:在PICC 無符號編譯環(huán)境下,對于遞減的for 語句的條件判斷語句不能是>=0 的形式。
最后談談PICC 的小竅門:
在PICC 默認的無符號環(huán)境下,對比如下代碼:
a 語句:
char i,j[8];
i=7;
while(1){
j[i]=0;
i--;
if(i==0)break;
}
b 語句:
char i,j[8];
for(i=8;i>0;i--){
j[i-1]=0;
}
表面看上去, 一般會認為下面的代碼編譯后要大一點點,因為多了j[i-1]中的i-1。
其實編譯后代碼量是一摸一樣的。
原因如下:
movlw 8 或7 //a 語句是7,b 語句是8
movf i
loop
//a 語句在這里提取i 給j 數(shù)組
//i 遞減判斷語句
//b 語句在這里提取i 給j 數(shù)組
goto loop
可以看出只是代碼位置不同而已,并沒添加代碼量。b 語句同樣達到了從7 到0 的循環(huán)。
小總結:對于遞減到0 的for 語句推薦用>0 判斷語句來實現(xiàn),不會出現(xiàn)編譯錯誤的問題,并且不會增加代
碼量,尤其對于數(shù)組操作的方面。
另:對于PICC 或CCS,在其默認的無符號編譯環(huán)境下,如果出現(xiàn)負數(shù)運算就會出問題。
如(-100)+50 等,所以在編寫代碼時候要特別小心?。。?br />8、 用PICC 寫高效的位移操作。
在許多模擬串行通信中需要用位移操作。
以1-W 總線的讀字節(jié)為例,原廠的代碼是:
unsigned char read_byte(void)
{
unsigned char i;
unsigned char value = 0;
for (i = 0; i < 8; i++)
{
if(read_bit()) value| = 0 x 01<// reads byte in, one byte at a time and then
// shifts it left
delay(10); // wait for rest of timeslot
}
return(value);
}
雖 然可以用,但編譯后執(zhí)行效率并不高效,這也是很多朋友認為C 一定不能和匯編相比的認識提供了
說法。其實完全可以深入了解C 和匯編之間的關系,寫出非常高效的C 代碼,既有C 的便利,又有匯編的
效率。首先對 for (i = 0; i < 8;
i++) 做手術,改成遞減的形式:for(i=8;i!=0;i--),因為CPU 判斷一個數(shù)是否是0
(只需要一個指令),比判斷一個數(shù)是多大來的快 (需要3 個指令)。再對value| = 0 x 01<value| = 0 x 01<
仔細研究C 語言的位移操作,可以發(fā)現(xiàn)C 總是先把標志位清0,然后再把此位移入字節(jié)中,也就是說,當
前移動進字節(jié)的位一定是0。那么,既然已經是0 了,我們就只剩下一個步驟:判斷總線狀態(tài)是否是高來
決定是否改寫此位,而不需要判斷總線是低的情況。于是改寫如下代碼:
for(i=8;i!=0;i--){
value>>=1; //先右移一位,value 最高位一定是0
if(read_bit()) value|=0x80; //判斷總線狀態(tài),如果是高,就把value 的最高位置1
}
這樣一來,整個代碼變得極其高效,編譯后根本就是匯編級的代碼。再舉一個例 子:
在采集信號方面,經常是連續(xù)采集N 次,最后求其平均值。
一般的,無論是用匯編或C,在采集次數(shù)上都推薦用8,16,32、64、 128、256 等次數(shù),因為這些數(shù)都比
較特殊,對于MCU 計算有很大好處。
我們以128 次采樣為例:注:sampling()為外部采樣函數(shù)。
unsigned int total;
unsigned char i,val;
for(i=0;i<128;i++){
total+=sampling();
}
val=total/128;
以 上代碼是很多場合都可以看見的,但是效率并不怎么樣,狂浪費資源。
結合C 和匯編的關系,再加上一些技巧,就可以寫出天壤之別的匯編級的C 代碼出來,首先分析128 這個
數(shù)是0B10000000,發(fā)現(xiàn)其第7 位是1,其他低位全是0,那么就可以判斷第7 位的狀態(tài)來判斷是否到了128
次采樣次數(shù)。在分析除以128 的運算,上面的代碼用了除法運算,浪費了N 多資源,完全可以用右移的方
法 來代替之,val=total/128 等同于val=(unsigned
char)(total>>7);再觀察下 去:total>>7 還可以變通成
(total<<1)>>8,先左移動一位,再右移動8 位,不就成了右移7 位了么?可知道位移1,4,8 的操作只需要
一個指令哦。有上面的概驗了,就可以寫出如下的代碼:
unsigned int total;
unsigned char i=0
unsigned char val;
while(!(i&0x80)){ //判斷i 第7 位,只需要一個指令。
total+=sampling();
i++;
}
val=(unsigned char)((total<<1)>>8); //幾個指令就代替了幾十個指令的除法運算
哈哈,發(fā)現(xiàn)什么?代碼量竟然 可以減少一大半,運算速度可以提高幾倍。
再回頭,就可以理解為什么采樣次數(shù)要用推薦的一些特殊值了。
9、C 程序優(yōu)化
對程序進行 優(yōu)化,通常是指優(yōu)化程序代碼或程序執(zhí)行速度。優(yōu)化代碼和優(yōu)化速度實際上是一個予
盾的統(tǒng)一,一般是優(yōu)化了代碼的尺寸,就會帶來執(zhí)行時間的增加,如果 優(yōu)化了程序的執(zhí)行速度,通常會帶
來代碼增加的副作用,很難魚與熊掌兼得,只能在設計時掌握一個平衡點。
一、程序結構的優(yōu)化
1、程 序的書寫結構
雖然書寫格式并不會影響生成的代碼質量,但是在實際編寫程序時還是應該尊循一定的書寫規(guī)則,一
個書寫清晰、明了的程序,有利 于以后的維護。在書寫程序時,特別是對于While、for、do…while、if…elst、
switch…case 等語句或這些語句嵌套組合時,應采用“縮格”的書寫形式,
2、標識符
程序中使用的用戶標識符除要遵循標識符的命名規(guī)則以外,一般不要用代 數(shù)符號(如a、b、x1、y1)作
為變量名,應選取具有相關含義的英文單詞(或縮寫)或漢語拼音作為標識符,以增加程序的可讀性,如:
count、 number1、red、work 等。
3、程序結構
C 語言是一種高級程序設計語言,提供了十分完備的規(guī)范化流程控制結構。因此在采用C 語言設計單
片機應用系統(tǒng)程序時,首先要注意盡可能采用結構化的 程序設計方法,這樣可使整個應用系統(tǒng)程序結構清
晰,便于調試和維護。于一個較大的應用程序,通常將整個程序按功能分成若干個模塊,不同模塊完成不
同 的功能。各個模塊可以分別編寫,甚至還可以由不同的程序員編寫,一般單個模塊完成的功能較為簡單,
設計和調試也相對容易一些。在C 語言中,一個函數(shù)就可以認為是一個模塊。所謂程序模塊化,不僅是要
將整個程序劃分成若干個功能模塊,更重要的是,還應該注意保持各個模塊之間變量 的相對獨立性,即保
持模塊的獨立性,盡量少使用全局變量等。對于一些常用的功能模塊,還可以封裝為一個應用程序庫,以
便需要時可以直接調 用。但是在使用模塊化時,如果將模塊分成太細太小,又會導致程序的執(zhí)行效率變低(進
入和退出一個函數(shù)時保護和恢復寄存器占用了一些時間)。
4、 定義常數(shù)
在程序化設計過程中,對于經常使用的一些常數(shù),如果將它直接寫到程序中去,一旦常數(shù)的數(shù)值發(fā)生
變化,就必須逐個找出程序中所有的 常數(shù),并逐一進行修改,這樣必然會降低程序的可維護性。因此,應
盡量當采用預處理命令方式來定義常數(shù),而且還可以避免輸入錯誤。
5、減少 判斷語句
能夠使用條件編譯(ifdef)的地方就使用條件編譯而不使用if 語句,有利于減少編譯生成的代碼的長度。
6、表達式
對 于一個表達式中各種運算執(zhí)行的優(yōu)先順序不太明確或容易混淆的地方,應當采用圓括號明確指定它
們的優(yōu)先順序。一個表達式通常不能寫得太復雜,如果表 達式太復雜,時間久了以后,自己也不容易看得
懂,不利于以后的維護。
7、函數(shù)
對于程序中的函數(shù),在使用之前,應對函數(shù)的類型進行 說明,對函數(shù)類型的說明必須保證它與原來定
義的函數(shù)類型一致,對于沒有參數(shù)和沒有返回值類型的函數(shù)應加上“void”說明。如果果需要縮短代碼的 長
度,可以將程序中一些公共的程序段定義為函數(shù),在Keil 中的高級別優(yōu)化就是這樣的。如果需要縮短程序
的執(zhí)行時間,在程序調試結束 后,將部分函數(shù)用宏定義來代替。注意,應該在程序調試結束后再定義宏,
因為大多數(shù)編譯系統(tǒng)在宏展開之后才會報錯,這樣會增加排錯的難度。
8、 盡量少用全局變量,多用局部變量。因為全局變量是放在數(shù)據(jù)存儲器中,定義一個全局變量,MCU 就
少一個可以利用的數(shù)據(jù)存儲器空間,如果定義了太 多的全局變量,會導致編譯器無足夠的內存可以分配。
而局部變量大多定位于MCU 內部的寄存器中,在絕大多數(shù)MCU 中,使用寄存器操作速度比數(shù)據(jù)存儲器快,
指令也更多更靈活,有利于生成質量更高的代碼,而且局部變量所的占用的寄存器和數(shù)據(jù)存儲器在不同的
模 塊中可以重復利用。
9、設定合適的編譯程序選項
許多編譯程序有幾種不同的優(yōu)化選項,在使用前應理解各優(yōu)化選項的含義,然后選用最合適的一 種優(yōu)
化方式。通常情況下一旦選用最高級優(yōu)化,編譯程序會近乎病態(tài)地追求代碼優(yōu)化,可能會影響程序的正確
性,導致程序運行出錯。因此應熟悉 所使用的編譯器,應知道哪些參數(shù)在優(yōu)化時會受到影響,哪些參數(shù)不
會受到影響。
在ICCAVR 中,有“Default”和“Enable Code Compression”兩個優(yōu)化選項。
在CodeVisionAVR 中,“Tiny”和“small”兩種內存模式。
在IAR 中,共有7 種不同的內存模式選項。
在GCCAVR 中優(yōu)化選項更多,一不小心更容易選到不恰當?shù)倪x項。
二、代碼的優(yōu)化
1、 選擇合適的算法和數(shù)據(jù)結構
應該熟悉算法語言,知道各種算法的優(yōu)缺點,具體資料請參見相應的參考資料,有很多計算機書籍上
都有介紹。將比較 慢的順序查找法用較快的二分查找或亂序查找法代替,插入排序或冒泡排序法用快速排
序、合并排序或根排序代替,都可以大大提高程序執(zhí)行的效率。.選 擇一種合適的數(shù)據(jù)結構也很重要,比如
你在一堆隨機存放的數(shù)中使用了大量的插入和刪除指令,那使用鏈表要快得多。
數(shù)組與指針具有十分密碼的 關系,一般來說,指針比較靈活簡潔,而數(shù)組則比較直觀,容易理解。對于大
部分的編譯器,使用指針比使用數(shù)組生成的代碼更短,執(zhí)行效率更高。但是在 Keil 中則相反,使用數(shù)組比
使用的指針生成的代碼更短。
2、 使用盡量小的數(shù)據(jù)類型
能夠使用字符型(char)定義的變量, 就不要使用整型(int)變量來定義;能夠使用整型變量定義的變量就
不要用長整型(long int),能不使用浮點型(float)變量就不要使用浮點型變量。當然,在定義變量后不要超過
變量的作用范圍,如果超過變量的范圍賦值,C 編譯器并不報錯,但程序運行結果卻錯了,而且這樣的錯
誤很難發(fā)現(xiàn)。在ICCAVR 中,可以在Options 中設定使用printf 參數(shù),盡量使用基本型參數(shù)(%c、%d、%x、
%X、%u 和%s 格式說明符),少用長整型參數(shù)(%ld、%lu、%lx 和%lX 格式說明符),至于浮點型的參數(shù)(%f)
則盡量不要使用,其它C 編譯器也一樣。在其它條件不變的情況下,使用%f 參數(shù),會使生成的代碼的數(shù)量
增 加很多,執(zhí)行速度降低。
3、 使用自加、自減指令
通常使用自加、自減指令和復合賦值表達式(如a-=1 及a+=1 等)都能夠生成高質量的程序代碼,編譯器
通常都能夠生成inc 和dec 之類的指令,而使用a=a+1 或a=a-1 之類的指令,有很多C 編譯器都會生成二到
三個字節(jié)的指令。在AVR 單片適用的ICCAVR、GCCAVR、IAR 等C 編譯器以上幾種書寫方式生成的代
碼 是一樣的,也能夠生成高質量的inc 和dec 之類的的代碼。
4、減少運算的強度
可以使用運算量小但功能相同的表達式替換原來復雜的的 表達式。如下:
(1)、求余運算。
a=a%8;
可以改為:
a=a&7;
說明:位操作只需一個指令周期即 可完成,而大部分的C 編譯器的“%”運算均是調用子程序來完成,代碼
長、執(zhí)行速度慢。通常,只要求是求2n 方的余數(shù),均可使用位操作的方法來代替。
(2)、平方運算
a=pow(a,2.0);
可以改為:
a=a*a;
說 明:在有內置硬件乘法器的單片機中(如51 系列),乘法運算比求平方運算快得多,因為浮點數(shù)的求平方
是通過調用子程序來實現(xiàn)的,在自帶硬件乘法 器的AVR 單片機中,如ATMega163 中,乘法運算只需2 個
時鐘周期就可以完成。既使是在沒有內置硬件乘法器的AVR 單片機中,乘法運算的子程序比平方運算的子
程序代碼短,執(zhí)行速度快。
如果是求3 次方,如:
a=pow(a,3.0);
更 改為:
a=a*a*a;
則效率的改善更明顯。
(3)、用移位實現(xiàn)乘除法運算
a=a*4;
b=b/4;
可 以改為:
a=a<<2;
b=b>>2;
說明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在 ICCAVR 中,如果乘以2n,都可以生
成左移的代碼,而乘以其它的整數(shù)或除以任何數(shù),均調用乘除法子程序。用移位的方法得到代碼比調用乘
除 法子程序生成的代碼效率高。實際上,只要是乘以或除以一個整數(shù),均可以用移位的方法得到結果,如:
a=a*9
可以改為:
a=(a<<3)+a
5、 循環(huán)
(1)、循環(huán)語
對于一些不需要循環(huán)變量參加運算的任務可以把它們放到循環(huán)外面,這里的任務包括表達式、函數(shù)的調用、
指針運 算、數(shù)組訪問等,應該將沒有必要執(zhí)行多次的操作全部集合在一起,放到一個init 的初始化程序中
進行。
(2)、延時函數(shù):
通常 使用的延時函數(shù)均采用自加的形式:
void delay (void)
{
unsigned int i;
for (i=0;i<1000;i++)
;
}
將其改為自減延時函數(shù):
void delay (void)
{
unsigned int i;
for (i=1000;i>0;i--)
;
}
兩個函數(shù)的延時效果相似,但幾乎所有的C 編譯對后一種函數(shù)生成的代碼均比前一種代碼少1~3 個字節(jié),
因為幾乎所有的MCU 均有為0 轉移的指令,采用后一種方式能夠生成這類指令。
在 使用while 循環(huán)時也一樣,使用自減指令控制循環(huán)會比使用自加指令控制循環(huán)生成的代碼更少1~3 個字
母。
但是在循環(huán)中有通過循環(huán)變 量“i”讀寫數(shù)組的指令時,使用預減循環(huán)時有可能使數(shù)組超界,要引起注意。
(3)while 循環(huán)和do…while 循環(huán)
用while 循環(huán)時有以下兩種循環(huán)形式:
unsigned int i;
i=0;
while (i<1000)
{
i++;
// 用戶程序
}
或:
unsigned int i;
i=1000;
do
i--;
//用戶程序
while (i>0);
在這兩種循環(huán)中,使用do…while 循環(huán)編譯后生成的代碼的長度短于while 循環(huán)。
6、查表
在程序 中一般不進行非常復雜的運算,如浮點數(shù)的乘除及開方等,以及一些復雜的數(shù)學模型的插補運算,
對這些即消耗時間又消費資源的運算,應盡量使用查表的 方式,并且將數(shù)據(jù)表置于程序存儲區(qū)。如果直接
生成所需的表比較困難,也盡量在啟動時先計算,然后在數(shù)據(jù)存儲器中生成所需的表,后以在程序運行直
接 查表就可以了,減少了程序執(zhí)行過程中重復計算的工作量。
7、其它
比如使用在線匯編及將字符串和一些常量保存在程序存儲器中,均有利于優(yōu) 化。
評論