学习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啦。
在学习过程中还看到了可变参数宏,大概是这样的。
只要懂得##是连接符,就明白什么意思了。