• 在嵌入式里面实现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)函数实现,使用封装好的宏即可获取参数列表,在头文件中提供了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类型的例子其他帖子写的不错)。

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

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

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

    #include

    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

    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);

    于是就有了这样的版本:

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

    (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。如果格式化解析失败,则返回负数。

    于是产生了下面的版本。

    1. #include "stdio.h"
    2. #include "stdarg.h"
    3. #include "string.h"
    4. void __attribute__((format(printf(1,2)))) my_printf(char *fmt, ...)
    5. {
    6. #ifdef __DEBUG
    7. char sendbuf[512]={0};
    8. va_list args;
    9. va_start(args,fmt);
    10. vsnprintf(sendbuf,sizeof(sendbuf),fmt,args);
    11. va_end(args);
    12. Usart(sendbuf,strlen(sendbuf)); // 调用串口发送函数,实际情况改动
    13. delayms(strlen(sendbuf)); // 延时确保发送结束,以9600波特率为参考
    14. #endif
    15. }

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

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

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

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

  • 相关阅读:
    算法2:链表的逆转
    小红书信息流和薯条的区别是什么?各自有什么作用
    带你了解S12直播中的“黑科技”
    Cesium 问题:加载 geojson 数据量大浏览器会崩,使用primitive方式加载
    Doris学习笔记之介绍、编译安装与部署
    Linux-Docker-Elasticsearch安装
    爪哇,我初学乍道
    持续集成部署-k8s-服务发现-Ingress
    TinyWebServer学习笔记-Config
    Java中SpringBoot四大核心组件是什么
  • 原文地址:https://blog.csdn.net/weixin_41579872/article/details/128117784