這一篇來講講混合編程的問題,在網上找了一下,講混合編程的文件章也有不少,但進行實例操作講解的不多也不完整,本來書上混合編程的內容看著就讓人覺得抽象難懂,再沒有個實際操作圖例,就很讓人覺得云里霧里。在這里我就針對KEIL做個混合編程的實例的文章希望對初學者有所幫助。先搞清幾個問題。
①混合編程的必要性:也就是為什么需要混合編程,初學者一定會覺得,我C用的好好的為什么要混進匯編呢,不是自找麻煩嗎?其實不然,最簡單的例子就是延時子程序,用C寫的話連你自己也不知道幾層的循環后確切地用多少時間吧?但用匯編寫你就能很準確地計算出要延時的時間。還有當你要對那些時序要求很高IC模塊或步進電機行操作時用匯編來寫就能做到操控的直接與精準。
②在進行實際操作前要弄清C與匯編之間的調用關系,C的函數大家都會用了,主要分為無反回參數的和有反回參數的,例如 void delay(void);就是無反回參數的,int readdata(void);就是有返回參數的。還有就是有參數傳遞和無參數傳遞的,void delay(void);就是無參數傳遞的,unsigned int add(unsigned char aa,unsigned char bb);就是有參數傳遞的函數。在教材上講起C與匯編的混合編程就會說起寄存器最多傳遞三個函數,這樣可以產生高效代碼。

在參數返回時寄存器的傳遞規律為:

下面我們用實際的混合編程操作來講講如何實現函數的調用及參數的傳遞。
打開KEIL,我的用的版本是綠色免安裝2.0中文版,編譯器為7.0:無程序代碼長度限制。現在有3.0版也是綠色免安裝版本,好處是已支持雙字節中文注釋,但是英文版。用哪個版本都無所謂,只要用著習慣功能夠用就行。

下面是版本信息:

在網上經常有朋友說為什么我下載了KEIL解壓出目錄后運行卻不能編譯呢,老是報告出錯:
--- Error: can't execute 'E:\old_pc\txz001\單片機c51\KEIL4\C51\BIN\C51.EXE'
--- 錯誤: 不能執行 'E:\old_pc\txz001\單片機c51\KEIL2_70\Keil2\C51\BIN\C51.EXE'
這是由于編譯時,C51.exe編譯器沒能在你給出的路徑上找到。你需要修改路徑。
在選擇KEIL的菜單欄“工程”--“文件擴展名、書籍和編譯環境屬性”--“環境設置”的如下圖:

看到上圖的“使用TOOLS.INI設定”前的鉤了嗎?對了,它是按照你TOOLS.INI里給出的路徑去找的。因此的得打開那個tools.ini文件修改它。KEIL的目錄結構一般是這樣的:

我們KEIL軟件運行主程序uvision2是在目錄UV2里,而那個設置文件TOOLS.INI文件是在它的上一級目錄Keil里,見上圖。用記事本打開這個TOOLS.INI文件:

看見紅筆圈出的[C51]下的路徑了嗎?將它修改正確指向你硬盤上KEIL下C51目錄,存盤,運行KEIL。就可以正確編譯了。(廢話又多了。。。)好!言歸正傳。
我們在KEIL里創建一個新的工程TEST1。在這個工程里我們添加了兩個文件,main.c和delay.c,程序如下:
文件main.c:
#include <AT89X52.H>
extern void delay(void);
main(void)
{
delay();
}
文件delay.c
#define uchar unsigned char
void delay(void)
{ uchar i;
for(i=255,i>0,i--);
}
可以看出,這兩個文件里的程序很簡單,主程序里先定義了一個外部函數delay();然后就調用了這個無參數函數。而文件delay.c里也就是用for循環做了255次循環。
下面我們先進行編譯,調試讓程序正確,通過編譯。然后我們選擇左邊工程窗口,選中文件delay.c,鼠標右擊它出現下圖。

選擇“文件'delay.c'屬性”后如下圖:

見上圖,有“產生匯編文件”和“匯編源代碼文件”兩項前的鉤選框是灰色的,分別點擊它們兩次使它呈黑色鉤選狀態。如下圖。

點擊下面的確認鈕,回到主界面。這時你再進行一次全部的重新編譯,就會發現在你建立這個工程的目錄下將多產生一個delay.src文件。

用記事本打開這個delay.src文件。發現它就是一個匯編文件。
; .\delay.SRC generated from: delay.c
; COMPILER INVOKED BY:
; E:\old_pc\txz001\單片機c51\KEIL2_70\Keil\C51\BIN\C51.EXE delay.c BROWSE DEBUG OBJECTEXTEND SRC(.\delay.SRC)
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
; #define uchar unsigned char
; void delay(void)
RSEG ?PR?delay?DELAY
delay:
USING 0
; SOURCE LINE # 2
; { uchar i;
; SOURCE LINE # 3
; for(i=255;i>0;i--);
; SOURCE LINE # 4
;---- Variable 'i?040' assigned to Register 'R7' ----
MOV R7,#0FFH
?C0001:
DJNZ R7,?C0001
; }
; SOURCE LINE # 5
?C0004:
RET
; END OF delay
END
可以看出原來的C程序都變成了匯編的注釋了。我們將注釋都去掉。
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
USING 0
MOV R7,#0FFH
?C0001:
DJNZ R7,?C0001
?C0004:
RET
END
現在看看是不是很簡呢。在標號delay:前是程序的說明,就是定義函數的名字,將代碼放在哪里等,看不懂也沒關系,別亂改它就行。從delay:標號后就是匯編的程序部分了。里面的標號最好也別亂改。添加你要操作的程序就行了,好!我們先不改動程序,就將上面十行匯編別存為delay.asm文件。回到KEIL界面,我們在工程窗里(是KEIL主界面左邊的工程窗口而不是在工程目錄里)的將delay.c刪除。然后再添加上delay.asm程序,如下圖:

這樣,你再進行編譯,你會發現你已經通過了混合編程的編譯,雖然這次你對程序的功能什么都沒有改變,但你已經知道如何做出一個C程序調用匯編子程序的例子了。下面我們可以對這個匯編了程序進行一些修改看它是否仍能很好的工作。
今天我們就來對那個匯編的delay子程序進行修改,為了讓運行的結果能顯示出來,我先加進一個LCD的顯示子程序12864put.c。

我們先修改主程序如下:
//****************
// 主函數
//****************
main(void)
{ uchar aa,bb;
TMOD=0x01;//定義T0為模式1即16位計數方式
TH0=0;//將計數器高位初值清0
TL0=0;//將計數器低位初值清0
TR0=1;//計數器開始計數
//delay(); //調用匯編的子函數
TR0=0;//停止計數
aa=TH0;//把計數的值高位交給aa
bb=TL0;//把計數的值低位交給aa
LcmInit();//初始化LCD12864
LcmClear();//清屏LCD
LcmPutstr( 0,28,"C&A TEST" );//顯示
LcmPutstr( 3,0,"TH0:" );
LcmPutstr( 3,24,uchartostr(aa) );
LcmPutstr( 3,46,"TL0:");
LcmPutstr( 3,70,uchartostr(bb) );
LcmPutstr( 5,0,"BLOG:http://" );
LcmPutstr( 6,18,"hi.baidu.com/txz01" );
LcmPutstr( 7,8,"Email:TXZ001@139.com" );
看見上面的程序了嗎?我用了T0在調用匯編子函數delay()前開始計數,調用完后就關掉,然后看計數器內的計數值來知道我們這個子函數的精確程度。我先把delay()函數給注釋掉,看看開始計數后就立即關掉要用去多少時間。結果顯示為1,就是說用了一個脈沖的時間。12M的晶振就是一微秒。見下圖:

看到沒有,用了TR0=1;TR0=0;本身就用去了一個脈沖。好!現在我們將那個調用匯編子函數delay()語句啟用,但我將匯編內的語句給清空。也就是說我把delay.asm這個子程序讓它什么也沒做。是個空函數,看它要用掉幾個脈沖時間。匯編程序如下:
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
RET
END
看到了嗎?標號delay:下面什么也沒有了,直接就RET返回了。好!編譯,燒寫,運行!如下圖:
結果是用了5個脈沖,其中一個是調用計數器本身用的,也就是說調用一個空函數用了4個脈沖時間。好!我們再來修改一下匯編程序:
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
mov r7,#100
djnz r7,$
RET
END
在標號delay:下面我加了兩行,我們計算一下,第一行MOV r7,#100要用一個機器周期,也就是一個脈沖。第二行djnz r7,$要循環100次每次用2個機器周期,這樣算來共是201個脈沖再加上剛才我們計算過的調用函數要4個脈沖和開關計數器用1個,總共是206個。編譯,燒寫,運行!
看來計算的沒錯呀!我們再循環多些:
NAME DELAY
?PR?delay?DELAY SEGMENT CODE
PUBLIC delay
RSEG ?PR?delay?DELAY
delay:
mov r7,#100 ;1
loop:mov r6,#50 ;100
djnz r6,$ ;50×100×2
djnz r7,loop ;100×2
RET
END
這次的計算應該是1+100+50×100×2+100×2+5=10306。再次編譯燒寫運行!

高位數值為40,低位數值為66,則總數=40×256+66=10306。精準吧!好了!無參函數的調用就討論到此。
下面接著說說帶參數據函數的調用:
我們重新建立一個目錄TEST2(因為一個項目有很多個文件如果都放在一個目錄里會很混亂,以后想挪到U盤帶到其它機子上用時就很困難了),建立新的項目test2.Uv2,里面還是main.c主程序和12864put.c顯示子程序:
主函數main()如下:
#include <AT89X52.H>
#include <intrins.H>
#define uchar unsigned char
#define uint unsigned int
extern void LcmClear( void ); //清屏
extern void LcmInit( void ); //初始化
extern void LcmPutstr( uchar row,uchar y,uchar * str ); //在設定位置顯示字符串
//row:是LCD的行數(0-7)
//y:是LCD的列數(0-127)
//str:是字符串的首地址
extern uint add(uchar aa,uchar bb);
extern void inttostr(uint intval,uchar data * str);
uchar str[6];//定義四個字節空間用來存放數值轉換成的字符值
//****************
// 主函數
//****************
main(void)
{ uchar aa,bb;
uint cc;
aa=145;
bb=236;
cc=add(aa,bb);
LcmInit();//初始化LCD12864
LcmClear();//清屏LCD
LcmPutstr( 0,28,"C&A TEST" );//顯示
inttostr(aa,str);
LcmPutstr( 3,0,str );
LcmPutstr( 3,18," + " );
inttostr(bb,str);
LcmPutstr( 3,36,str);
LcmPutstr( 3,54," = ");
inttostr(cc,str);
LcmPutstr( 3,72,str);
//LcmPutstr( 3,46,"TL0:");
//LcmPutstr( 3,70,uchartostr(bb) );
LcmPutstr( 5,0,"BLOG:http://" );
LcmPutstr( 6,18,"hi.baidu.com/txz01" );
LcmPutstr( 7,8,"Email:TXZ001@139.com" );
while(1);
}
項目中還有uinttostr.c是無符號整型轉字符串子程序和我們要做匯編調用的這個有返回參數有傳遞參數的子程序add.c,子程序add.c如下。
#define uchar unsigned char
#define uint unsigned int
uint add(uchar aa,uchar bb)
{
uint cc;
cc=aa+bb;
return(cc);
}
我們主要目的是為了表達清楚怎樣在C程序里去調用匯編子函數,所以程序還是很簡單,就是把主程序傳過來的無符號字符型變量aa和bb相加,相加的結果交給無符號整型變量cc返回給主程序。編譯前我們還是點取add.c文件屬性,讓它產生src文件。上面的圖已顯示了編譯的過程信息。現在我們打開這個add.src文件:
; .\add.SRC generated from: add.c
; COMPILER INVOKED BY:
; E:\old_pc\txz001\單片機c51\KEIL2_70\Keil\C51\BIN\C51.EXE add.c BROWSE DEBUG OBJECTEXTEND SRC(.\add.SRC)
NAME ADD?
?PR?_add?ADD SEGMENT CODE
PUBLIC _add
; #define uchar unsigned char
; #define uint unsigned int
;
; uint add(uchar aa,uchar bb)
RSEG ?PR?_add?ADD
_add:
USING 0
; SOURCE LINE # 4
;---- Variable 'bb?041' assigned to Register 'R5' ----
;---- Variable 'aa?040' assigned to Register 'R7' ----
; {
; SOURCE LINE # 5
; uint cc;
; cc=aa+bb;
; SOURCE LINE # 7
MOV A,R5
ADD A,R7
MOV R7,A
CLR A
RLC A
MOV R6,A
;---- Variable 'cc?042' assigned to Register 'R6/R7' ----
; return(cc);
; SOURCE LINE # 8
; }
; SOURCE LINE # 9
?C0001:
RET
; END OF _add
END
我們還是將注釋的部分刪去,這樣便于我們分析:
NAME ADD?
?PR?_add?ADD SEGMENT CODE
PUBLIC _add
RSEG ?PR?_add?ADD
_add:
USING 0
MOV A,R5
ADD A,R7
MOV R7,A
CLR A
RLC A
MOV R6,A
RET
END
現在我們首先來看函數名,上面我們講過的那個無參數函數delay()的調用,產生的匯編子函數名就是delay,而這次我我們原來C的函數名add變成了匯編的_add。前面多了個下劃線,這就是有參數函數的特征。C語言函數名轉變為匯編函數名的規律為:無參數傳遞時void func(void)----FUNC。寄存器參數傳遞時char func(char)----_FUNC。再入函數使用時void func(void) reentrant----_?FUNC。
不過這些名字的變化規律記沒記住好象關系并不大。我們想要用到匯編調用時,就先用C做個假函數然后產生匯編文件名字就自然出來,并不用我們去管它的命名,然后去修改成我們想做的匯編程序就行了。
但是,這參數傳遞的位置規律就必須得知道,否則你就無法使用這個匯編了,我們看上面的匯編程序,第一句是將寄存器R5的值傳到A中,第二句將A與寄存器R7相加,第三句將相加的結果A的值傳給R7,后面的幾句是將剛才相加的進位值C,傳給R6,然后返回。對照本篇最上面給的那兩張表我們可以看出C子函數add.c的第一個參數aa被傳到了匯編的R7,第二個參數bb被傳到了R5,將它們相加后,返回值的低位交給了R7,高位交給了R6。完全符合參數傳遞表和返回值表所述。下面我們將匯編子程序另存為asm文件后替換掉原來的C子程序:
編譯、燒寫后運行:
這個加法函數我們沒有改動任何參數當然運行起來是不會錯的,下面我們將在匯編里將它改成乘法試試,
將標號_add:下面的語句全都改掉,程序如下?
NAME ADD?
?PR?_add?ADD SEGMENT CODE
PUBLIC _add
RSEG ?PR?_add?ADD
_add:
USING 0
MOV A,R7
MOV B,R5
MUL AB ;A與B相乘,乘積的高位值在B中,低位值在A中
MOV R7,A ;將低位值傳給R7
MOV R6,B ;將高位值傳給R6
RET
END
上面的改動我們已將原來的加法依照寄存器的傳遞規律改為乘法函數,看看是否還能正常運行并正確,改完后仍編譯燒寫運行:
哈哈!完全正確。
總結:
我們可以經常性地采用在C中建立簡單的子函數,轉成匯編后看它的操作方法和傳遞規律,慢慢地熟悉掌握和運用如何在C中調用匯編函數。
修改于(2009.3.15)
|