C語言嵌入式實現(xiàn)支持浮點輸出的printf示例詳解
簡介
mr-printf 模塊為 mr-library 項目下的可裁剪模塊,以C語言編寫,可快速移植到各種平臺(主要以嵌入式mcu為主)。
mr-printf 模塊用以替代 libc 中 printf, 可在較小資源占用的同時支持絕大部分 printf 功能,于此同時還支持對單獨功能模塊的裁剪以減少用戶不需要功能的資源占用。
背景
printf 大家應該使用的比較多,但是在嵌入式平臺中,尤其是單片機中,libc中的printf對內存的占用較高,尤其是加上浮點輸出功能時,占用更是能翻倍。同時移植適配相對困難,不同編譯器下,需要適配的接口不同,遇到問題也因為看不到源代碼,無從下手。
故有了寫自己的printf想法?,F(xiàn)在網(wǎng)上也有不少自己寫printf的教程,但是在我實際按照教程編寫時又遇到了許多問題很多教程并不能正確實現(xiàn)功能,所以我把寫完的代碼開源出來,同時分享下我在編寫時遇到的問題。
C語言可變參數(shù)函數(shù)
C 語言允許定義參數(shù)數(shù)量可變的函數(shù),這稱為可變參數(shù)函數(shù)。這種函數(shù)需要固定數(shù)量的強制參數(shù),后面是數(shù)量可變的可選參數(shù)。 如 mr_printf(char *fmt, ...) 前面的 fmt為 char 類型參數(shù),是固定數(shù)量的強制參數(shù),后面的 ... 為數(shù)量可變的可選參數(shù)。
同時我們要了解函數(shù)參數(shù)的入棧順序,例如我們調用了mr_printf("%d,%f",a,b); 那么首先 "%d,%f"就是fmt這個char*,這個是確定的,然后就是兩個參數(shù) a b ,加入我們采用的從左往右入棧,也就是fmt 先入棧然后a b,這就會導致,你拿到了棧指針,但是因為不知道a b的類型,所以定位不到a 也就是首個參數(shù)的地址。
但是我們反過來,采用從右往左入棧,那么我們將會得到fmt的地址,然后只需要對fmt的地址 + fmt的大小,就能得到a 的地址。實現(xiàn)此功能的函數(shù)也叫va_start名字也很形象,是一切的開始。然后我們通過分析 fmt中的信息,就能通過對 a的地址 + a的大小得到b的地址,這一步驟也叫va_arg。
最后當我們處理完所有的信息后我們需要把棧指針歸零防止出現(xiàn)野指針,也就是va_end。好了我們已經(jīng)學會了可變參數(shù)函數(shù)的開始和結束,那么我們就可以開始應用了。
踩坑
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的實現(xiàn)時候都看到過這段代碼,確實這是沒有問題的,這是x86上的代碼,我們可以通過學習這個代碼的思路來了解整個可變參數(shù)函數(shù)的實現(xiàn)過程。但當你把這段代碼移植到你的stm32等設備上時你就會發(fā)現(xiàn),同樣的代碼在電腦上跑沒有問題,但是單片機上卻不行,就是應為這段的問題,在gcc環(huán)境下應該是下面這樣,并不能通過上面的函數(shù)直接去操作棧指針,當然最好的辦法其實是引入#include <stdarg.h>這個頭文件,其中包含了對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)
功能實現(xiàn)
首先我們需要定義一個函數(shù)將字符輸出到我們的硬件MR_WEAK void mr_putc(char data),MR_WEAK為宏定義,不同平臺可能關鍵字不同,將 void mr_putc(char data)定義為一個弱函數(shù),該函數(shù)主要功能為將data字符輸出到例如串口等設備。
同時我們定義int mr_printf(char *fmt, ...) 函數(shù),參入?yún)?shù)為一個 char *和不定數(shù)量的可變參數(shù)。然后定義一個 va_list ap 用來獲取可變參數(shù)。
我們先初始化ap指針,方法剛剛已經(jīng)講過,即對fmt參數(shù)偏移sizeof(fmt),調用va_start(ap,fmt)即可。
接下來我們就要開始分析fmt中的信息了,我們需要處理的只有 %x命令,其他的通過我們自定義的輸出函數(shù)直接輸出即可。因為字符串的結尾都是\0,所以我們就能寫出以下代碼:
int mr_printf(char *fmt, ...)
{
va_list ap;
char putc_buf[20]; //輸出緩沖區(qū),減少運算加速輸出
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 這幾個指令 我們先開一個switch
switch (*fmt)
{
}
然后先處理最簡單的 %d
/* mr_printf signed int to DEC */
case 'd':
/* get value */
val = va_arg(ap,int);
if(val < 0) //判斷正負
{
val = - val;
mr_putc('-');
++ res;
}
/* get value bits */
bits = 0;
while(val)
{
putc_buf[bits] = '0' + val % 10; //獲取整型位數(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;
與此同時 %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就更簡單了
/* 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;
最難的其實是對float的輸出,當你用上面的思路一位一位取出數(shù)據(jù)的同時,就會發(fā)現(xià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; //判斷正負
mr_putc('-');
++ res;
}
/* separation int and float */
val = (int)f_val; // 分離整數(shù)和小數(shù),整數(shù)將按上面處理整數(shù)的部分輸出,小數(shù)部分單獨處理
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); //僅做一次浮點運算,同時對誤差進行處理忽略最低幾位小數(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;
好了通過上面的講解你應該已經(jīng)會寫printf了,或者下載開源代碼使用。
開源代碼
路徑:master/mr-library/ device / mr_printf
以上就是C語言嵌入式實現(xiàn)支持浮點輸出的printf示例詳解的詳細內容,更多關于C語言嵌入式浮點輸出printf的資料請關注腳本之家其它相關文章!
相關文章
超詳細VScode調試教程tasks.json和launch.json的設置
vscode是一個輕量級的文本編輯器,但是它的擴展插件可以讓他拓展成功能齊全的IDE,這其中就靠的是tasks.json和launch.json的配置,下面這篇文章主要給大家介紹了關于超詳細VScode調試教程tasks.json和launch.json設置的相關資料,需要的朋友可以參考下2022-10-10
C++利用Socket實現(xiàn)主機間的UDP/TCP通信
這篇文章主要為大家詳細介紹了C++如何利用Socket實現(xiàn)主機間的UDP/TCP通信功能,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2023-01-01

