?? printf
字號(hào):
printf在uCOS51上的移植和浮點(diǎn)數(shù)顯示
asdjf@163.com 2003/10/20
printf函數(shù)是C語(yǔ)言里應(yīng)用最為廣泛的函數(shù)之一,我們初學(xué)C語(yǔ)言時(shí)實(shí)現(xiàn)的第一個(gè)程序《Hello the world》,就包含printf語(yǔ)句。它的應(yīng)用十分靈活,可以打印各種類型數(shù)據(jù),可變數(shù)量的變量,表達(dá)式,是非常理想的輸出函數(shù),廣泛用于結(jié)果輸出,中間變量顯示,調(diào)試等。然而,編譯器將其作為標(biāo)準(zhǔn)庫(kù)函數(shù),不提供源代碼,其本身代碼量也偏大,無(wú)法實(shí)現(xiàn)嵌入式系統(tǒng)按需裁減的要求,并且有些printf庫(kù)代碼不支持重入。
解決方法是把Linux里的相關(guān)源碼簡(jiǎn)化后移植到C51里。關(guān)鍵點(diǎn)在于理解變參函數(shù)、參數(shù)傳遞規(guī)則、浮點(diǎn)數(shù)存儲(chǔ)格式。
C編譯器一般將函數(shù)參數(shù)按從右至左的順序依次壓入堆棧(C51在使用reentrant關(guān)鍵字后也這么處理),函數(shù)內(nèi)部處理參數(shù)變量時(shí)直接在堆棧上尋址,局部變量緊跟在參數(shù)后面存放,函數(shù)返回時(shí)出棧,參數(shù)和局部變量所占用空間自動(dòng)釋放。例如:
fun(char *fmt,char a,int b long c,float d) reentrant
的堆棧結(jié)構(gòu)如圖1所示:
------------------
|float d 4 bytes |
+10 ------------------
|long c 4 bytes |
+6 ------------------
|int b 2 bytes |
+4 ------------------
|char a 1 bytes |
+3 ------------------
|char *fmt 3bytes|
SP+0-->------------------
| 局部變量 |
------------------
圖1.fun函數(shù)參數(shù)和局部變量在堆棧里的結(jié)構(gòu)
C51編譯器從右向左依次將float/long/int/char/char *壓入仿真堆棧,各種數(shù)據(jù)類型所占空間大小如圖1,例如char占1字節(jié),float占4字節(jié)等。值得一提的是,常數(shù)壓棧的格式:0-255按1字節(jié)壓棧,256-32767壓成2字節(jié),32768(8000H)或以上壓成4字節(jié),帶有l(wèi)/L結(jié)尾的常數(shù)占4字節(jié)。
上面的函數(shù)fun內(nèi)部可以通過(guò)函數(shù)名稱訪問(wèn)各個(gè)變量,C編譯器自動(dòng)把函數(shù)名轉(zhuǎn)換成地址,如:訪問(wèn)long c轉(zhuǎn)換成訪問(wèn)SP+6,訪問(wèn)char a轉(zhuǎn)換成訪問(wèn)SP+3等。寫成表達(dá)式為:
c=0x12345678;======>(SP+6)=0x12345678
a='y';=============>(SP+3)='y'
總之,上面的函數(shù)通過(guò)顯式地指定函數(shù)名和數(shù)據(jù)類型完成參數(shù)的傳遞和訪問(wèn),內(nèi)部細(xì)節(jié)由C編譯器完成,對(duì)用戶透明。
這種方式的好處是表達(dá)清晰,結(jié)構(gòu)嚴(yán)謹(jǐn),屏蔽底層細(xì)節(jié);壞處是不夠靈活,參數(shù)必須在處理前顯式確定并固定不變,這給我們用同一函數(shù)處理不同情況帶來(lái)了困難,C的解決方案是引入“變參函數(shù)”(詳見C語(yǔ)言大全),如下:
fun(char *fmt,...) reentrant
...表示有0到N個(gè)可變數(shù)量參數(shù),C編譯器此時(shí)不檢查參數(shù)匹配,傳遞參數(shù)規(guī)律與一般函數(shù)相同。如果我們用這個(gè)函數(shù)取代前一個(gè)函數(shù),但仍按前一函數(shù)的調(diào)用方式調(diào)用,那么,參數(shù)在堆棧里的位置仍如圖1所示。此時(shí),函數(shù)形參只有“...”沒(méi)有具體變量名,如何引用形參變量呢?觀察圖1堆棧結(jié)構(gòu)可知,如果知道堆棧內(nèi)第一個(gè)參數(shù)的起址和每個(gè)參數(shù)的數(shù)據(jù)類型及他們的排列順序,就可以通過(guò)指針訪問(wèn)指定的變量。例如:
知道堆棧內(nèi)第一個(gè)參數(shù)的起址SP和每個(gè)參數(shù)的數(shù)據(jù)類型及排列順序(char*/char/int/long/float),就可以通過(guò)SP,SP+3,SP+4,SP+6,SP+10訪問(wèn)原來(lái)必須通過(guò)參數(shù)名訪問(wèn)的fmt,a,b,c,d變量。寫成C語(yǔ)言就是:
fun("yy",'y',(int)2,5L,-12.5);
fun(char *fmt,...) reentrant
{
void *p;
p=&fmt;
//此時(shí)*p指向字符串"yy"首址,**p是字符串第一個(gè)字符'y'。
p=(char **)p+1;
//此時(shí)*((char *)p)為字符'y'。
p=(char *)p+1;
//此時(shí)*((int *)p)為0x0002。
p=(int *)p+1;
//此時(shí)*((long *)p)為0xC1480000,即-12.5的IEEE-754標(biāo)準(zhǔn)格式。
p=(float *)p+1;
}
測(cè)試代碼:
void fun(char *fmt,...) reentrant
{
void *p;
p=&fmt;
PrintChar(**((char **)p));
p=((char **)p) +1;
PrintChar(*((char *)p));
p=((char *)p) +1;
PrintLong(*((int *)p));
p=((int *)p) +1;
PrintLong(*((long *)p));
p=((long *)p) +1;
PrintLong(*((long *)p));
p=((float *)p) +1;
}
顯示結(jié)果:yy0000000200000005C1480000
由上面知,在C里不用顯式使用SP等堆棧指針,而是使用void指針指向各種類型數(shù)據(jù)。變參函數(shù)的參數(shù)傳遞和獲取就是這樣運(yùn)做的,知道了它的原理,就不難理解printf的實(shí)現(xiàn)了。
我所移植的printf支持標(biāo)準(zhǔn)或長(zhǎng)二進(jìn)制/八進(jìn)制/十進(jìn)制/十六進(jìn)制/無(wú)符號(hào)整數(shù),支持字符、字符串、浮點(diǎn)數(shù)、百分號(hào)%顯示。其中,浮點(diǎn)數(shù)在整個(gè)范圍內(nèi)被完全支持,統(tǒng)一采用科學(xué)記數(shù)法顯示。對(duì)應(yīng)的指示符如下:
c 字符 f 浮點(diǎn)數(shù) s 字符串 % 百分號(hào)顯示
d/ld 2字節(jié)/4字節(jié)有符號(hào)整數(shù) u/lu 2字節(jié)/4字節(jié)無(wú)符號(hào)整數(shù)
x/lx 2字節(jié)/4字節(jié)十六進(jìn)制數(shù) o/lo 2字節(jié)/4字節(jié)八進(jìn)制數(shù)
b/lb 2字節(jié)/4字節(jié)二進(jìn)制數(shù)
printf的功能是字符串化數(shù)據(jù),它的第一個(gè)參數(shù)是格式化字符串fmt,用其指示第一個(gè)參數(shù)在堆棧里的起址和其后各個(gè)參數(shù)的數(shù)據(jù)類型。知道了參數(shù)堆棧起址和各個(gè)參數(shù)的類型和排放次序,就可以依次取出各個(gè)參數(shù)并字符串化。詳細(xì)過(guò)程參見yyprintf源代碼。同時(shí),注意到參數(shù)是依靠起址和數(shù)據(jù)長(zhǎng)度信息依次讀出來(lái)的,那么,yyprintf的參數(shù)必須與格式化參數(shù)的指示相同,否則參數(shù)數(shù)據(jù)會(huì)亂掉。對(duì)于不能肯定的轉(zhuǎn)化數(shù)據(jù)類型建議加上強(qiáng)制類型定義,如(int) 2。特別是常數(shù)的轉(zhuǎn)換類型容易搞錯(cuò)。
printf大部分代碼與硬件無(wú)關(guān),只有參數(shù)堆棧結(jié)構(gòu)和打印一個(gè)字符putchar()函數(shù)是硬件相關(guān)的。移植printf時(shí)只要修改putchar()函數(shù)和堆棧結(jié)構(gòu)即可。putchar()函數(shù)的功能一般是向串口輸出一個(gè)字符,也可以向其他顯示設(shè)備輸出一個(gè)字符,取決于你的驅(qū)動(dòng)程序。我已經(jīng)在uCOS51里實(shí)現(xiàn)了PrintChar函數(shù),直接調(diào)用就可以了。其實(shí),在X86、POWERPC、ARM等32位CPU上移植printf更加有效和方便。
測(cè)試舉例:
float r=1.9835671E-10,pi=3.1415926;
yyprintf("R=%f Circle area=%f\n",r,pi*r*r);
結(jié)果:
R=1.983567E-10 Circle area=1.236071E-19
源代碼:
//============================================================================================
//
//============================================================================================
void yyprintf(char *fmt,...) reentrant //自編簡(jiǎn)單printf等效函數(shù)
{
void *p; //任意指針,可以指向任何類型,C語(yǔ)法不對(duì)其嚴(yán)格要求。
char ch;
unsigned char j;
p=&fmt;
p=(char **)p+1; //此處p是指向指針的指針,fmt是字符串指針,p是指向fmt的指針
while(1){
while((ch=*fmt++)!='%'){
if(ch=='\0') return;
else if(ch=='\n'){PrintChar(10);PrintChar(13);}
else if(ch=='\t'){
for(j=0;j<TABNum;j++)
PrintChar(' ');
}
else PrintChar(ch);
}
ch=*fmt++;
switch(ch){
case 'c':
PrintChar(*((char *)p));
p=(char *)p+1;
break;
case 'd':
PrintN(*((int *)p),10);
p=(int *)p+1;
break;
case 'x':
PrintN(*((int *)p),16);
p=(int *)p+1;
break;
case 'o':
PrintUN(*((int *)p),8);
p=(int *)p+1;
break;
case 'b':
PrintUN(*((int *)p),2);
p=(int *)p+1;
break;
case 'l':
ch=*fmt++;
switch(ch){
case 'd':
PrintLN(*((long *)p),10);
p=(long *)p+1;
break;
case 'o':
PrintLUN(*((long *)p),8);
p=(long *)p+1;
break;
case 'u':
PrintLUN(*((unsigned long *)p),10);
p=(unsigned long *)p+1;
break;
case 'b':
PrintLUN(*((long *)p),2);
p=(long *)p+1;
break;
case 'x':
PrintLN(*((long *)p),16);
p=(long *)p+1;
break;
default:
return;
}
break;
case 'f':
DispF(*((float *)p));
p=(float *)p+1;
break;
case 'u':
PrintUN(*((unsigned int *)p),10);
p=(unsigned int *)p+1;
break;
case 's':
PrintStr(*((char **)p));
p=(char **)p+1;
break;
case '%':
PrintChar('%');
p=(char *)p+1;
break;
default:
return;
}
}
}
void PrintN(int n,int b) reentrant //十進(jìn)制顯示整形數(shù)
{
if(b==16){PrintWord(n);return;}
if(n<0){PrintChar('-');n=-n;}
if(n/b)
PrintN(n/b,b);
PrintChar(n%b+'0');
}
void PrintUN(unsigned int n,unsigned int b) reentrant //十進(jìn)制顯示無(wú)符號(hào)整形數(shù)
{
if(b==16){PrintWord(n);return;}
if(n/b)
PrintUN(n/b,b);
PrintChar(n%b+'0');
}
void PrintLN(long n,long b) reentrant //十進(jìn)制顯示長(zhǎng)整形數(shù)
{
if(b==16){PrintLong(n);return;}
if(n<0){PrintChar('-');n=-n;}
if(n/b)
PrintLN(n/b,b);
PrintChar(n%b+'0');
}
void PrintLUN(unsigned long n,unsigned long b) reentrant //十進(jìn)制顯示無(wú)符號(hào)長(zhǎng)整形數(shù)
{
if(b==16){PrintLong(n);return;}
if(n/b)
PrintLUN(n/b,b);
PrintChar(n%b+'0');
}
參考文獻(xiàn):
1。《ROM版本下系統(tǒng)調(diào)試信息的一種顯示方法》合肥工業(yè)大學(xué) 彭良清 《單片機(jī)與嵌入式系統(tǒng)應(yīng)用》p22頁(yè)2002(1-6)
TO BE CONTINUED...
浮點(diǎn)數(shù)顯示
?? 快捷鍵說(shuō)明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號(hào)
Ctrl + =
減小字號(hào)
Ctrl + -