在嵌入式里面实现printf()类似的功能

学习C语言大多数都是从printf("hello world")开始的,对于printf的熟悉程度最高,在嵌入式编程中,实现printf函数有一种很标准的办法就是实现putch,绑定对应的串口输出,设置好波特率,使能串口就可以了,使用mircolib效果更加,但是随着工程的实践中,有着另外的使用需求。

嵌入式接口资源比较紧张,一般的cpu也就自带四个串口,往往外设很多,如果独立使用一个串口用来调试,这样的IO资源浪费和成本是不能忍受的,所以只能复用。一般的串口数据传输函数接口为usartSend(char buf[],size_t len);// 表示要传输的数据和长度,这个可以很好的满足跟外设通信的接口要求,但是调试的时候很不方便。如果我要看几个float变量的值,那就无法直接输出,之前采用的办法是sprintf()格式转换,再次输出,这样用到调试的地方至少要写3行代码,如果加上调试宏和必要的延时等待发送完成,那就需要5-6行代码。如果不嫌弃的话,也可以这样做,我这样实现了一年之后,决定换一个方法来减轻调试时候的代码量。

方法是这样的(需要GNU编译器支持,keil中已经集成了GNU编译器,用起来特别好用):

 (1)使用__attribute__扩展format属性,关于扩展语法可以看这篇文章(GNU C扩展语法_风一样的航哥的博客-CSDN博客

先给一个例子:void LOG(const char * fmt,...) __attribute__((format(printf(1,2)));

这个属性告诉编译器,请按照printf函数的参数格式对LOG函数进行参数检查。...就表示可变参数了,那么如果读取可变参数和使用呢?继续往下看。

(2)函数实现,使用封装好的宏即可获取参数列表,在头文件<stdarg.h>中提供了4个很有用的宏。分别是va_list、va_start、va_arg、va_end。

va_list:变量类型,用于创建一个 va_list 类型变量解析可变参数.va_list args;
va_start(args,fmt):根据参数fmt的地址,获取fmt后面参数的地址,并保存在args指针变量中。

C 库宏 void va_start(va_list ap, last_arg) 初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。

这个宏必须在使用 va_arg 和 va_end 之前被调用。

va_arg(args,int):使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项,int表示自动增加sizeof(int)的长度,参考其他文献好像只能支持int和double两种类型,就是整型都是int,不管是char还是short,浮点型都是double,使用float会得不到想要的结果。
va_end(args):使用宏 va_end 来清理赋予 va_list 变量的内存,并指向NULL。

下面给一个例子,遍历double类型的可变参数,实现返回所有值的sum操作。(int类型的例子其他帖子写的不错)。

void *fun01(double num, ...)
{
	int i;
	double res = 0;
	va_list v1;				//v1实际是一个字符指针,从头文件里可以找到 
	
	va_start(v1, num);		//使v1指向可变列表中第一个值,即num后的第一个参数 
	
	printf("*v = %lf\n",(double)*v1);
	
	for(i = 0; i < (int)num; i++)	//num 是为了防止下标超限 
	{
		res += va_arg(v1, typeof(num));		//该函数返回v1指向的值,并是v1向下移动一个int的距离,使其指向下一个int 
		printf("res = %lf, v1 = %p\n",res, v1); 
	} 
	va_end(v1);				//关闭v1指针,使其指向null
	return &res;
}

(3)实现格式化输出,知道了参数如果处理之后,就可以格式化输出了,本来我使用的是sprintf函数来处理后面的参数,结果一直不对。经过查询和反思,最终明白了库里面提供了专门的函数来处理va_list的变量,是vprintf系列。

C语言printf家族函数的成员:

#include <stdio.h>

int printf(const char *format, ...); //输出到标准输出
int fprintf(FILE *stream, const char *format, ...); //输出到文件
int sprintf(char *str, const char *format, ...); //输出到字符串str中
int snprintf(char *str, size_t size, const char *format, ...);
                                     //按size大小输出到字符串str中
  
以下函数功能与上面的一一对应相同,只是在函数调用时,把上面的...对应的一个个变量用va_list调用所替代。在函数调用前ap要通过va_start()宏来动态获取。

#include <stdarg.h>

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);     int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

于是就有了这样的版本:

void __attribute__((format(printf(1,2)))) my_printf(char *fmt, ...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
}

(4)函数嵌入式移植,上述版本已经差不多可以用了,只要将vprintf换成vsnprintf,再调用嵌入式的串口发送函数即可。

int vsnprintf (char * sbuf, size_t n, const char * format, va_list arg );

参数sbuf:用于缓存格式化字符串结果的字符数组

参数n:限定最多打印到缓冲区sbuf的字符的个数为n-1个,因为vsnprintf还要在结果的末尾追加\0。如果格式化字符串长度大于n-1,则多出的部分被丢弃。如果格式化字符串长度小于等于n-1,则可以格式化的字符串完整打印到缓冲区sbuf。一般这里传递的值就是sbuf缓冲区的长度。

参数format:格式化限定字符串

参数arg:可变长度参数列表

返回:成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。

于是产生了下面的版本。

#include "stdio.h"
#include "stdarg.h"
#include "string.h"

void __attribute__((format(printf(1,2)))) my_printf(char *fmt, ...)
{
#ifdef    __DEBUG
    char sendbuf[512]={0};
    va_list args;
    va_start(args,fmt);
    vsnprintf(sendbuf,sizeof(sendbuf),fmt,args);
    va_end(args);
    Usart(sendbuf,strlen(sendbuf));    // 调用串口发送函数,实际情况改动
    delayms(strlen(sendbuf));           // 延时确保发送结束,以9600波特率为参考
#endif
}

上述代码中,__DEBUG表示调试宏,发布程序的时候关闭这个宏就可以了。一般的全局的调试宏在下图所示的地方定义。

 总结:通过可变参数函数,就实现了在嵌入式上熟悉的printf函数,还与正式发布的串口传输函数不冲突,带来的代价就是占用了更多的内存,发布的时候取消宏就OK啦。

在学习过程中还看到了可变参数宏,大概是这样的。

 只要懂得##是连接符,就明白什么意思了。

猜你喜欢

转载自blog.csdn.net/weixin_41579872/article/details/128117784