C语言嵌入式实现支持浮点输出的printf示例详解

目录
  • 简介
  • 背景
  • C语言可变参数函数
  • 踩坑
  • 功能实现

简介

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了,或者下载开源代码使用。

开源代码

仓库链接 gitee.com/chen-fanyi/…

路径:master/mr-library/ device / mr_printf

以上就是C语言嵌入式实现支持浮点输出的printf示例详解的详细内容,更多关于C语言嵌入式浮点输出printf的资料请关注我们其它相关文章!

(0)

相关推荐

  • C语言中的字符型数据与ASCII码表

    目录 1.字符型常量 2.字符型变量 3.字符型数据的输入输出 (1)scanf()和printf()函数输入输出字符 (2)字符输入函数getchar() 总结 1.字符型常量 字符型常量指单个字符,是用一对单引号及其所括起来的字符表示. 例如:‘A’.‘a’.‘0’.’$‘等都是字符型常量. C语言的字符使用的就是ASCII字符集,总共有128个,每个相应的ASCII码都表示一个字符: (1)每一个字符都有唯一的次序值,即ASCII码. (2)数字’0’,‘1’,‘2’,…,‘9’.大写字母

  • C语言实现求解素数的N种方法总结

    目录 前言 必备小知识 C语言详解<试除法>求解素数 试除法境界1 试除法境界2 试除法境界3 试除法境界4 C语言详解<筛选法>求解素数 筛选法境界5 前言 哈喽各位友友们,我今天又学到了很多有趣的知识,现在迫不及待的想和大家分享一下!我仅已此文,手把手带领大家探讨利用试除法.筛选法求解素数的n层境界!都是精华内容,可不要错过哟!!! 必备小知识 质数又称素数.一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数:否则称为合数(规定1既不是质数也不是合数).这里

  • C语言软件iic虚拟总线中间层设计详解

    目录 简介 IIC-协议 接线方式 总线 工作本质 虚拟总线(中间层)设计 使用示例 简介 mr-soft-iic 模块为 mr-library 项目下的可裁剪模块,以C语言编写,可快速移植到各种平台(主要以嵌入式mcu为主). mr-soft-iic 模块通过 io 模拟实现 iic 协议. IIC-协议 SPI 一般为一主多从设计.由2根线组成:CLK(时钟).SDA(数据). 接线方式 主机 从机 CLK CLK SDA SDA 主机从机一 一对应相接. 总线 IIC 通过地址码识别设备,

  • 通俗易懂的C语言快速排序和归并排序的时间复杂度分析

    目录 快速排序和归并排序的时间复杂度分析——通俗易懂 归并排序的时间复杂度分析 快速排序的时间复杂度 快速排序的最坏情况O(n^2) 总结 快速排序和归并排序的时间复杂度分析——通俗易懂 今天面试的时候,被问到归并排序的时间复杂度,这个大家都知道是O(nlogn),但是面试官又继续问,怎么推导出来的.这我就有点懵了,因为之前确实没有去真正理解这个时间复杂度是如何得出的,于是就随便答了一波(理解了之后,发现面试的时候答错了......). 归并排序和快速排序,是算法中,非常重要的两个知识点,同时也

  • C语言利用goto语句设计实现一个关机程序

    目录 前言 一.什么是goto语句 二.goto语句的作用是什么 三.goto语句的缺点 四.goto语句的结构与用法 五.goto语句的巧用实例——关机小程序 总结撒花 前言 goto语句其实在平常中我们 除了学习分支语句和循环语句时,介绍循环语句时,才会知道有goto语句这个用法,那读者可能会问:我们还有学习的必要吗? 答案是显而易见的,正如黑格尔所说的:存在即合理!既然存在,就会有存在的必要!虽然我们现在不会遇到且用到 ,当在搞Linux硬件驱动等的时候,其内核含有较多的goto语句,如果

  • C语言嵌入式实现支持浮点输出的printf示例详解

    目录 简介 背景 C语言可变参数函数 踩坑 功能实现 简介 mr-printf 模块为 mr-library 项目下的可裁剪模块,以C语言编写,可快速移植到各种平台(主要以嵌入式mcu为主). mr-printf 模块用以替代 libc 中 printf, 可在较小资源占用的同时支持绝大部分 printf 功能,于此同时还支持对单独功能模块的裁剪以减少用户不需要功能的资源占用. 背景 printf 大家应该使用的比较多,但是在嵌入式平台中,尤其是单片机中,libc中的printf对内存的占用较高

  • Go语言基础switch条件语句基本用法及示例详解

    目录 概述 语法 第一种[switch 带上表达式] 第二种[switch 不带表达式] 第三种[switch 初始化,表达式] 注意 示例一[根据今天的日期打印今天星期几] 示例二[根据分数打印A,B,C,D] 示例三[算数] 概述 传入条件的不同,会执行不同的语句 每一个case分支都是唯一的,从上到下逐一测试,直到匹配为止. 语法 第一种[switch 带上表达式] switch 表达式 { case 表达式1,表达式2, ... : 语句块1 case 表达式3, 表达式4, ... :

  • Go语言学习教程之goroutine和通道的示例详解

    目录 goroutine 通道 Range 和 Close Select 官方留的两道练习题 等价的二叉树 网络爬虫 源码地址 goroutine goroutine是由Go运行时管理的轻量级线程. go f(x, y, z)在一个新的goroutine中开始执行f(x, y,z). goroutines运行在相同的地址空间中,所以对共享的内存访问必须同步.sync包提供了基本的同步原语(synchronization primitives),比如互斥锁(mutual exclusion loc

  • Go语言leetcode题解953验证外星语词典示例详解

    目录 题目描述 思路分析 AC 代码 题目描述 953. 验证外星语词典 某种外星语也使用英文小写字母,但可能顺序 order 不同.字母表的顺序(order)是一些小写字母的排列. 给定一组用外星语书写的单词 words,以及其字母表的顺序 order,只有当给定的单词在这种外星语中按字典序排列时,返回 true:否则,返回 false. 示例 1: 输入:words = ["hello","leetcode"], order = "hlabcdefgi

  • C语言编程题杨氏矩阵算法快速上手示例详解

    目录 题目概要 一.解题思路 二.具体代码 题目概要 有一个数字矩阵,矩阵的每行从左到右都是递增的,矩阵从上到下都是递增的,请编写程序在这样的矩阵中查找某个数字是否存在? 一.解题思路 对于查找一个数组中元素是否存在,很多同学第一想法就是从头到尾遍历一遍.这样的想法优点是代码简单且无脑容易上手,但是这样的缺点也很明显,比如是m *n的数组,你从头到尾遍历,最坏情况要找m *n次.题目给的相关条件比如从左向右递增,从上向下递增你也完全没有使用,这样的暴力求解显然不是我们想看到的 我们来介绍一种方法

  • Go语言基础Json序列化反序列化及文件读写示例详解

    目录 概述 JSON序列化 结构体转JSON map转JSON 切片转JSON JSON反序列化 JSON转map JSON转结构体 JSON转切片 写JSON文件 map写入JSON文件 切片写入JSON文件 结构体写入JSON文件 读JSON文件 解码JSON文件为map 解码JSON文件为切片 解码JSON文件为结构体 示例 概述 JSON(JavaScript Object Notation,JavaScript对象表示法)是一种轻量级的.键值对的数据交换格式.结构由大括号'{}',中括

  • Go语言基础for循环语句的用法及示例详解

    目录 概述 语法 注意 示例一  死循环,读取文件 示例二  打印乘法表 示例三  遍历字符串,数组,map 概述 for循环是一个循环控制结构,可以执行指定次数的循环 语法 第一种 for {} //无线循环 第二种 for 条件语句{} 第三种 for 初始语句; 条件语句; 迭代后语句 {} 第四种 for key,value:=range 表达式/变量{} 注意 省略初始条件,相当于while循环体必须用 { } 括起来初始化语句和后置语句是可选的如果只剩下条件表达式了,那么那两个分号也

  • C/C++语言八大排序算法之桶排序全过程示例详解

    基本思路是将所有数的个位十位百位一直到最大数的最高位一步步装桶,先个位装桶然后出桶,直到最高位入桶出桶完毕. 首先我们要求出一个数组的最大数然后求出他的最大位数 //求最大位数的函数 int getmaxweisu(int* a,int len)// { int max = a[0]; for (int i = 0; i < len; i++) { if (max < a[i]) { max = a[i]; } } int count = 1; while (max/10) { count++

  • Flutter实现一个支持渐变背景的Button示例详解

    目录 Flutter中的按钮 不完美的地方 在child中处理 外面套一个wrapper MaterialStateProperty MaterialStatesController 边距问题 EnhancedButton Flutter中的按钮 自Flutter 1.20 新增了ButtonStyleButton 系列按钮,可以说非常好用了,默认样式比之前漂亮了许多,扩展性也增加了很多.按钮样式统一由ButtonStyle这个类提供,支持根据各种状态(MaterialState)变化的属性,也

  • go语言定时器Timer及Ticker的功能使用示例详解

    目录 定时器1-"*/5 * * * * *" 设置说明 定时器2-Timer-Ticker Timer-只执行一次 Ticker-循环执行 Timer延时功能 停止和重置定时器 定时器Ticker使用 定时器1-"*/5 * * * * *" package main import ( "fmt" "github.com/robfig/cron" ) //主函数 func main() { cron2 := cron.New

随机推荐