C語言嵌入式實(shí)現(xiàn)支持浮點(diǎn)輸出的printf示例詳解
簡(jiǎn)介
mr-printf 模塊為 mr-library 項(xiàng)目下的可裁剪模塊,以C語言編寫,可快速移植到各種平臺(tái)(主要以嵌入式mcu為主)。
mr-printf 模塊用以替代 libc 中 printf, 可在較小資源占用的同時(shí)支持絕大部分 printf 功能,于此同時(shí)還支持對(duì)單獨(dú)功能模塊的裁剪以減少用戶不需要功能的資源占用。
背景
printf 大家應(yīng)該使用的比較多,但是在嵌入式平臺(tái)中,尤其是單片機(jī)中,libc中的printf對(duì)內(nèi)存的占用較高,尤其是加上浮點(diǎn)輸出功能時(shí),占用更是能翻倍。同時(shí)移植適配相對(duì)困難,不同編譯器下,需要適配的接口不同,遇到問題也因?yàn)榭床坏皆创a,無從下手。
故有了寫自己的printf想法?,F(xiàn)在網(wǎng)上也有不少自己寫printf的教程,但是在我實(shí)際按照教程編寫時(shí)又遇到了許多問題很多教程并不能正確實(shí)現(xiàn)功能,所以我把寫完的代碼開源出來,同時(shí)分享下我在編寫時(shí)遇到的問題。
C語言可變參數(shù)函數(shù)
C 語言允許定義參數(shù)數(shù)量可變的函數(shù),這稱為可變參數(shù)函數(shù)。這種函數(shù)需要固定數(shù)量的強(qiáng)制參數(shù),后面是數(shù)量可變的可選參數(shù)。 如 mr_printf(char *fmt, ...)
前面的 fmt
為 char 類型參數(shù),是固定數(shù)量的強(qiáng)制參數(shù),后面的 ...
為數(shù)量可變的可選參數(shù)。
同時(shí)我們要了解函數(shù)參數(shù)的入棧順序,例如我們調(diào)用了mr_printf("%d,%f",a,b);
那么首先 "%d,%f"
就是fmt
這個(gè)char*
,這個(gè)是確定的,然后就是兩個(gè)參數(shù) a
b
,加入我們采用的從左往右入棧,也就是fmt
先入棧然后a
b
,這就會(huì)導(dǎo)致,你拿到了棧指針,但是因?yàn)椴恢?code>a b
的類型,所以定位不到a
也就是首個(gè)參數(shù)的地址。
但是我們反過來,采用從右往左入棧,那么我們將會(huì)得到fmt
的地址,然后只需要對(duì)fmt
的地址 + fmt
的大小,就能得到a
的地址。實(shí)現(xiàn)此功能的函數(shù)也叫va_start名字也很形象,是一切的開始。然后我們通過分析 fmt
中的信息,就能通過對(duì) a
的地址 + a
的大小得到b
的地址,這一步驟也叫va_arg。
最后當(dāng)我們處理完所有的信息后我們需要把棧指針歸零防止出現(xiàn)野指針,也就是va_end。好了我們已經(jīng)學(xué)會(huì)了可變參數(shù)函數(shù)的開始和結(jié)束,那么我們就可以開始應(yīng)用了。
踩坑
typedef char * va_list; //將char*別名為va_list; #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v)) #define va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) #define va_end(ap) (ap = (va_list)0)
相信很多人在搜printf的實(shí)現(xiàn)時(shí)候都看到過這段代碼,確實(shí)這是沒有問題的,這是x86上的代碼,我們可以通過學(xué)習(xí)這個(gè)代碼的思路來了解整個(gè)可變參數(shù)函數(shù)的實(shí)現(xiàn)過程。但當(dāng)你把這段代碼移植到你的stm32等設(shè)備上時(shí)你就會(huì)發(fā)現(xiàn),同樣的代碼在電腦上跑沒有問題,但是單片機(jī)上卻不行,就是應(yīng)為這段的問題,在gcc
環(huán)境下應(yīng)該是下面這樣,并不能通過上面的函數(shù)直接去操作棧指針,當(dāng)然最好的辦法其實(shí)是引入#include <stdarg.h>
這個(gè)頭文件,其中包含了對(duì)va_list
va_start
va_end
va_arg
的定義。
typedef __builtin_va_list __gnuc_va_list; typedef __gnuc_va_list va_list; #define va_start(v,l) __builtin_va_start(v,l) #define va_end(v) __builtin_va_end(v) #define va_arg(v,l) __builtin_va_arg(v,l)
功能實(shí)現(xiàn)
首先我們需要定義一個(gè)函數(shù)將字符輸出到我們的硬件MR_WEAK void mr_putc(char data)
,MR_WEAK
為宏定義,不同平臺(tái)可能關(guān)鍵字不同,將 void mr_putc(char data)
定義為一個(gè)弱函數(shù),該函數(shù)主要功能為將data
字符輸出到例如串口等設(shè)備。
同時(shí)我們定義int mr_printf(char *fmt, ...)
函數(shù),參入?yún)?shù)為一個(gè) char *
和不定數(shù)量的可變參數(shù)。然后定義一個(gè) va_list ap
用來獲取可變參數(shù)。
我們先初始化ap
指針,方法剛剛已經(jīng)講過,即對(duì)fmt
參數(shù)偏移sizeof(fmt)
,調(diào)用va_start(ap,fmt)
即可。
接下來我們就要開始分析fmt
中的信息了,我們需要處理的只有 %x
命令,其他的通過我們自定義的輸出函數(shù)直接輸出即可。因?yàn)樽址慕Y(jié)尾都是\0
,所以我們就能寫出以下代碼:
int mr_printf(char *fmt, ...) { va_list ap; char putc_buf[20]; //輸出緩沖區(qū),減少運(yùn)算加速輸出 unsigned int u_val; int val, bits, flag; double f_val; char *str; int res = 0; /* move ap to fmt + sizeof(fmt) */ va_start(ap,fmt); while(*fmt != '\0') { if(*fmt == '%') { ++ fmt; "處理函數(shù)" } else { mr_putc(*fmt); ++ res; ++ fmt; } } /* set ap = null */ va_end(ap); return res; }
接下來我們就需要編寫中間的處理函數(shù)了,我們暫且需要支持 %d,%x,%o,%u,%s,%c,%f
這幾個(gè)指令 我們先開一個(gè)switch
switch (*fmt) { }
然后先處理最簡(jiǎn)單的 %d
/* mr_printf signed int to DEC */ case 'd': /* get value */ val = va_arg(ap,int); if(val < 0) //判斷正負(fù) { val = - val; mr_putc('-'); ++ res; } /* get value bits */ bits = 0; while(val) { putc_buf[bits] = '0' + val % 10; //獲取整型位數(shù)的同時(shí),將每一位按低位到高位存入緩沖區(qū) val /= 10; ++ bits; } res += bits; /* put value bits */ while (bits) { -- bits; mr_putc(putc_buf[bits]); //將每一位從高到低從緩沖區(qū)中輸出 } ++ fmt; continue;
同理處理下 %u
/* mr_printf unsigned int to DEC */ case 'u': /* get value */ u_val = va_arg(ap,unsigned int); /* get value bits */ bits = 0; while(u_val) { putc_buf[bits] = '0' + u_val % 10; u_val /= 10; ++ bits; } res += bits; /* put value bits */ while (bits) { -- bits; mr_putc(putc_buf[bits]); } ++ fmt; continue;
與此同時(shí) %x
和%o
也是同樣的道理僅需修改取余和除的值即可,直接貼代碼
/* mr_printf unsigned int to HEX */ case 'x': /* get value */ u_val = va_arg(ap,unsigned int); /* get value bits */ bits = 0; while(u_val) { putc_buf[bits] = '0' + u_val % 16; if(putc_buf[bits] > '9') putc_buf[bits] = 'A' + (putc_buf[bits] - '9' - 1); u_val /= 16; ++ bits; } res += bits; /* put value bits */ while (bits) { -- bits; mr_putc(putc_buf[bits]); } ++ fmt; continue; /* mr_printf unsigned int to OCT */ case 'o': /* get value */ u_val = va_arg(ap,unsigned int); /* get value bits */ bits = 0; while(u_val) { putc_buf[bits] = '0' + u_val % 8; u_val /= 8; ++ bits; } res += bits; /* put value bits */ while (bits) { -- bits; mr_putc(putc_buf[bits]); } ++ fmt; continue;
%s
和%c
就更簡(jiǎn)單了
/* mr_printf string */ case 's': str = va_arg(ap,char *); while (*str != '\0') { mr_putc(*str); ++ str; ++ res; } ++ fmt; continue; /* mr_printf char */ case 'c': mr_putc(va_arg(ap,int)); ++ res; ++ fmt; continue;
最難的其實(shí)是對(duì)float
的輸出,當(dāng)你用上面的思路一位一位取出數(shù)據(jù)的同時(shí),就會(huì)發(fā)現(xiàn),每做一個(gè)浮點(diǎn)運(yùn)算,就會(huì)引入新的誤差,所以要盡可能少的做浮點(diǎn)運(yùn)算,同時(shí)因?yàn)檫€需支持%.2f
這種指令需要在switch前面加上下面一段代碼記錄需要輸出多少位。
/* dispose %.x */ if(*fmt == '.') { ++ fmt; flag = (int)(*fmt - '0'); ++ fmt; } else { flag = 187; // N(46) + U(53) + L(44) + L(44) = NULL(187) }
/* mr_printf float */ case 'f': /* get value */ f_val = va_arg(ap,double); if(f_val < 0) { f_val = - f_val; //判斷正負(fù) mr_putc('-'); ++ res; } /* separation int and float */ val = (int)f_val; // 分離整數(shù)和小數(shù),整數(shù)將按上面處理整數(shù)的部分輸出,小數(shù)部分單獨(dú)處理 f_val -= (double)val; /* get int value bits */ bits = 0; if(val == 0) { mr_putc('0'); ++ res; } while (val) { putc_buf[bits] = '0' + val % 10; val /= 10; ++ bits; } res += bits; /* put int value bits */ while (bits) { --bits; mr_putc(putc_buf[bits]); } /* dispose float */ if(flag != 0) { mr_putc('.'); ++ res; } if(flag > 6) //判斷需要輸出幾位小數(shù) flag = 6; val = (int)((f_val * 1000000.0f) + 0.5f); //僅做一次浮點(diǎn)運(yùn)算,同時(shí)對(duì)誤差進(jìn)行處理忽略最低幾位小數(shù)引入的誤差 /* get float value bits */ bits = 0; while (bits < 6) { putc_buf[bits] = '0' + val % 10; //使用輸出整數(shù)的方法將小數(shù)輸出出去 val /= 10; ++ bits; } res += flag; /* put int value bits */ while (flag) { --flag; -- bits; mr_putc(putc_buf[bits]); } ++ fmt; continue;
好了通過上面的講解你應(yīng)該已經(jīng)會(huì)寫printf了,或者下載開源代碼使用。
開源代碼
倉(cāng)庫鏈接 gitee.com/chen-fanyi/…
路徑:master/mr-library/ device / mr_printf
以上就是C語言嵌入式實(shí)現(xiàn)支持浮點(diǎn)輸出的printf示例詳解的詳細(xì)內(nèi)容,更多關(guān)于C語言嵌入式浮點(diǎn)輸出printf的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言用函數(shù)實(shí)現(xiàn)電話簿管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言用函數(shù)實(shí)現(xiàn)電話簿管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12C語言宏函數(shù)container of()簡(jiǎn)介
這篇文章介紹了C語言宏函數(shù)container of(),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12c語言數(shù)據(jù)結(jié)構(gòu)之棧和隊(duì)列詳解(Stack&Queue)
這篇文章主要介紹了c語言數(shù)據(jù)結(jié)構(gòu)之棧和隊(duì)列詳解(Stack&Queue),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08關(guān)于C++STL string類的介紹及模擬實(shí)現(xiàn)
這篇文章主要介紹了關(guān)于C++STL string類的介紹及模擬實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下面具體的文章內(nèi)容2021-09-09基于c++11的event-driven library的理解
這篇文章主要介紹了基于c++11的event-driven library的理解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02