C语言嵌入式实现支持浮点输出的printf示例详解
MacRsh 人气:0简介
mr-printf 模块为 mr-library 项目下的可裁剪模块,以C语言编写,可快速移植到各种平台(主要以嵌入式mcu为主)。
mr-printf 模块用以替代 libc 中 printf, 可在较小资源占用的同时支持绝大部分 printf 功能,于此同时还支持对单独功能模块的裁剪以减少用户不需要功能的资源占用。
背景
printf 大家应该使用的比较多,但是在嵌入式平台中,尤其是单片机中,libc中的printf对内存的占用较高,尤其是加上浮点输出功能时,占用更是能翻倍。同时移植适配相对困难,不同编译器下,需要适配的接口不同,遇到问题也因为看不到源代码,无从下手。
故有了写自己的printf想法。现在网上也有不少自己写printf的教程,但是在我实际按照教程编写时又遇到了许多问题很多教程并不能正确实现功能,所以我把写完的代码开源出来,同时分享下我在编写时遇到的问题。
C语言可变参数函数
C 语言允许定义参数数量可变的函数,这称为可变参数函数。这种函数需要固定数量的强制参数,后面是数量可变的可选参数。 如 mr_printf(char *fmt, ...)
前面的 fmt
为 char 类型参数,是固定数量的强制参数,后面的 ...
为数量可变的可选参数。
同时我们要了解函数参数的入栈顺序,例如我们调用了mr_printf("%d,%f",a,b);
那么首先 "%d,%f"
就是fmt
这个char*
,这个是确定的,然后就是两个参数 a
b
,加入我们采用的从左往右入栈,也就是fmt
先入栈然后a
b
,这就会导致,你拿到了栈指针,但是因为不知道a
b
的类型,所以定位不到a
也就是首个参数的地址。
但是我们反过来,采用从右往左入栈,那么我们将会得到fmt
的地址,然后只需要对fmt
的地址 + fmt
的大小,就能得到a
的地址。实现此功能的函数也叫va_start名字也很形象,是一切的开始。然后我们通过分析 fmt
中的信息,就能通过对 a
的地址 + a
的大小得到b
的地址,这一步骤也叫va_arg。
最后当我们处理完所有的信息后我们需要把栈指针归零防止出现野指针,也就是va_end。好了我们已经学会了可变参数函数的开始和结束,那么我们就可以开始应用了。
踩坑
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的实现时候都看到过这段代码,确实这是没有问题的,这是x86上的代码,我们可以通过学习这个代码的思路来了解整个可变参数函数的实现过程。但当你把这段代码移植到你的stm32等设备上时你就会发现,同样的代码在电脑上跑没有问题,但是单片机上却不行,就是应为这段的问题,在gcc
环境下应该是下面这样,并不能通过上面的函数直接去操作栈指针,当然最好的办法其实是引入#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)
功能实现
首先我们需要定义一个函数将字符输出到我们的硬件MR_WEAK void mr_putc(char data)
,MR_WEAK
为宏定义,不同平台可能关键字不同,将 void mr_putc(char data)
定义为一个弱函数,该函数主要功能为将data
字符输出到例如串口等设备。
同时我们定义int mr_printf(char *fmt, ...)
函数,参入参数为一个 char *
和不定数量的可变参数。然后定义一个 va_list ap
用来获取可变参数。
我们先初始化ap
指针,方法刚刚已经讲过,即对fmt
参数偏移sizeof(fmt)
,调用va_start(ap,fmt)
即可。
接下来我们就要开始分析fmt
中的信息了,我们需要处理的只有 %x
命令,其他的通过我们自定义的输出函数直接输出即可。因为字符串的结尾都是\0
,所以我们就能写出以下代码:
int mr_printf(char *fmt, ...) { va_list ap; char putc_buf[20]; //输出缓冲区,减少运算加速输出 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; "处理函数" } else { mr_putc(*fmt); ++ res; ++ fmt; } } /* set ap = null */ va_end(ap); return res; }
接下来我们就需要编写中间的处理函数了,我们暂且需要支持 %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; //获取整型位数的同时,将每一位按低位到高位存入缓冲区 val /= 10; ++ bits; } res += bits; /* put value bits */ while (bits) { -- bits; mr_putc(putc_buf[bits]); //将每一位从高到低从缓冲区中输出 } ++ 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
的输出,当你用上面的思路一位一位取出数据的同时,就会发现,每做一个浮点运算,就会引入新的误差,所以要尽可能少的做浮点运算,同时因为还需支持%.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; // 分离整数和小数,整数将按上面处理整数的部分输出,小数部分单独处理 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) //判断需要输出几位小数 flag = 6; val = (int)((f_val * 1000000.0f) + 0.5f); //仅做一次浮点运算,同时对误差进行处理忽略最低几位小数引入的误差 /* get float value bits */ bits = 0; while (bits < 6) { putc_buf[bits] = '0' + val % 10; //使用输出整数的方法将小数输出出去 val /= 10; ++ bits; } res += flag; /* put int value bits */ while (flag) { --flag; -- bits; mr_putc(putc_buf[bits]); } ++ fmt; continue;
好了通过上面的讲解你应该已经会写printf了,或者下载开源代码使用。
开源代码
路径:master/mr-library/ device / mr_printf
加载全部内容