首先,完成了串口向终端putty的打印函数ConsolePrint(),但该函数只能打印字符串,无法像stdio库中的printf函数一样打印整数和浮点数等。

因此,我先是使用了标准库stdio中的sprintf函数。该函数可以将所要打印的数字格式化成对应的字符串并存储到字符串数组中,如sprintf( str_buffer, "num : %.2f", 12.34)将字符串“num : ”及浮点数12.34转换成对应的字符串“num : 12.34”存储在str_buffer数组中,接下来就可以用串口打印函数ConsolePrint(str_buffer)将其打印到终端putty。具体的使用方法如下:

char str_buffer[  ];
sprintf( str_buffer, "num : %d", 12.34 );
ConsolePrint( str_buffer );

但是这样使用很不方便,每次使用都要先声明一个数组,调sprintf进行格式化操作,最后再打印。那么能否将这些代码封装成一个函数呢?

答案是:很难!

首先我们需要抽象一个用户函数,使其能够像printf一样使用,既能够只打印字符串,也能够打印数字,而且可以一次打印多组数字。抽象得到的函数原型为:

void my_printf( const char *format, ... )

该函数是一个具有可变参数的函数,这样才能满足打印多组数字的要求。

我们想要在my_printf中调用sprintf函数,其函数原型为:

int _EXFUN(sprintf, (char *, const char *, ...)

这时就遇到了一个较为复杂的问题:一个具有可变参数的函数其子函数也具有可变参数。此时就需要在my_printf根据打印格式对可变参数进行解析(变参数的解析使用va_arg( ap, <type>,其中type是数据类型,必须根据打印格式知道其类型后才能解析出正确的数),然后再将解析后的数传给sprintf,在传的过程中还要看情况选择是否使用sprintf的变参数部分,整体来说非常麻烦。

--------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------

要解决这个问题可以使用vsprintf函数,其函数原型为

int    _EXFUN(vsprintf, (char *, const char *, __VALIST)

vsprintf接受一个va_list类型的形参用于接受可变参数的栈指针,这样只要将my_printf中可变参数的栈指针传递给它即可,vsprintf内部有对可变参数进行解析的程序从而将数字按照指定格式转换成对应的字符串(即使调用my_Printf时没有使用可变参数,vsprintf也能够进行处理),这样my_printf函数只要将变参的栈指针传递给它进行处理即可,具体实现如下:

void my_printf( char const *format, ... )
{
char buffer[ BUFFER_SIZE ];
va_list ap; va_start( ap, format ); //将ap初始化为变参数栈的栈顶指针
vsprintf( buffer, format , ap ); //将指针ap传递给vsprintf做格式化处理
va_end( ap ); Console_Print( buffer );
}

vsprintf函数的内部实现大致如下(并非标准库的实现):

int usr_vsprintf(char *dest, const char *fmt, va_list ap)
{
char c, sign, *cp, *dp = dest;
int left_prec, right_prec, zero_fill, length, pad, pad_on_right;
char buf[];
long val; while ((c = *fmt++) != )
{
cp = buf;
length = ;
if (c == '%')
{
c = *fmt++;
left_prec = right_prec = pad_on_right = ;
if (c == '-')
{
c = *fmt++;
pad_on_right++;
}
if (c == '')
{
zero_fill = TRUE;
c = *fmt++;
}
else
{
zero_fill = FALSE;
}
while (is_digit(c))
{
left_prec = (left_prec * ) + (c - '');
c = *fmt++;
}
if (c == '.')
{
c = *fmt++;
zero_fill++;
while (is_digit(c))
{
right_prec = (right_prec * ) + (c - '');
c = *fmt++;
}
}
else
{
right_prec = left_prec;
}
sign = '\0';
/* handle type modifier */
if (c == 'l' || c == 'h')
{
c = *fmt++;
}
switch (c)
{
case 'd' :
case 'u' :
case 'x' :
case 'X' :
val = va_arg(ap, long);
switch (c)
{
case 'd' :
if (val < )
{
sign = '-';
val = -val;
}
/* fall through */
case 'u' :
length = _cvt(val, buf, , "");
break;
case 'x' :
length = _cvt(val, buf, , "0123456789abcdef");
break;
case 'X' :
length = _cvt(val, buf, , "0123456789ABCDEF");
break;
}
break;
case 's' :
cp = va_arg(ap, char *);
length = strlen(cp);
break;
case 'c' :
c = (char)va_arg(ap, long);
*dp++ = c;
continue;
case '%' : /* '%%' ==> output '%' */
*dp++ = c;
break;
default:
*dp++ = '?';
}
pad = left_prec - length;
if (sign != '\0')
{
pad--;
}
if (zero_fill)
{
c = '';
if (sign != '\0')
{
*dp++ = sign;
sign = '\0';
}
}
else
{
c = ' ';
}
if (!pad_on_right)
{
while (pad-- > )
{
*dp++ = c;
}
}
if (sign != '\0')
{
*dp++ = sign;
}
while (length-- > )
{
c = *cp++;
if (c == '\n')
{
*dp++ = '\r';
}
*dp++ = c;
}
if (pad_on_right)
{
while (pad-- > )
{
*dp++= ' ';
}
}
}
else
{
if (c == '\n')
{
*dp++= '\r';
}
*dp++ = c;
}
} *dp = '\0'; return ((int)dp - (int)dest);
}

据说,sprintf就是用vsprintf来实现的,其大致实现如下(非标准库代码):

int usr_sprintf(char *buf, char const *fmt, ...)
{
int ret;
va_list ap; va_start(ap, fmt);
ret = usr_vsprintf(buf, fmt, ap);
va_end(ap); return ret;
}

总结一下:

为什么不用sprintf而是vsprintf?引用https://blog.csdn.net/heybeaman/article/details/80495846#commentBox博主的话来说,就是因为 vsprintf() 比 sprintf() 更加接近底层(栈)。

为什么void my_printf( const char *format, ... )调用int _EXFUN(sprintf, (char *, const char *, ...)会难以处理?使用 sprintf() 只能原始的为它输入所有的参数而不能以传参的方式给它。

--------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------

最后说说所谓的“坑儿”。my_printf函数定义在console_print.c文件中,在main函数调用,此时如果不在main中声明my_printf函数或者在consol_print.h中声明,而在main函数中include(声明如下),会发生变参数部分解析错误的情况,一般是把变参数解析为0。

extern void my_printf( const char *format, ... );

基于串口通信做my_printf时遇到的坑儿的更多相关文章

  1. [python] 3 、基于串口通信的嵌入式设备上位机自动测试程序框架(简陋框架)

    星期一, 20. 八月 2018 01:53上午 - beautifulzzzz 1.前言 做类似zigbee.ble mesh...无线网络节点性能测试的时候,手动操作然后看表象往往很难找出真正的原 ...

  2. VS2008基于对话框的MFC上位机串口通信(C++实现)简单例程

    首先,在 vs2008 环境下创建 MFC 运用程序 设置项目名称为 ComTest(这个地方随意命名,根据个人习惯),点击确定后,点击下一步 出现如下界面 选择"基于对话框"模式 ...

  3. 基于FPGA的红外遥控解码与PC串口通信

    基于FPGA的红外遥控解码与PC串口通信 zouxy09@qq.com http://blog.csdn.net/zouxy09 这是我的<电子设计EDA>的课程设计作业(呵呵,这个月都拿 ...

  4. C#中缓存的使用 ajax请求基于restFul的WebApi(post、get、delete、put) 让 .NET 更方便的导入导出 Excel .net core api +swagger(一个简单的入门demo 使用codefirst+mysql) C# 位运算详解 c# 交错数组 c# 数组协变 C# 添加Excel表单控件(Form Controls) C#串口通信程序

    C#中缓存的使用   缓存的概念及优缺点在这里就不多做介绍,主要介绍一下使用的方法. 1.在ASP.NET中页面缓存的使用方法简单,只需要在aspx页的顶部加上一句声明即可:  <%@ Outp ...

  5. [stm32][ucos] 1、基于ucos操作系统的LED闪烁、串口通信简单例程

    * 内容简述: 本例程操作系统采用ucos2.86a版本, 建立了5个任务            任务名                                             优先级 ...

  6. 【Delphi】基于状态机的串口通信

    通信协议 串行通信接口(如RS232.RS485等)作为计算机与单片机交互数据的主要接口,广泛用于各类仪器仪表.工业监测及自动控制领域中. 通信协议是需要通信的双方所达成的一种约定,它对包括数据格式. ...

  7. C#做一个简单的进行串口通信的上位机

    C#做一个简单的进行串口通信的上位机   1.上位机与下位机 上位机相当于一个软件系统,可以用于接收数据.控制数据.即可以对接收到的数据直接发送操控命令来操作数据.上位机可以接收下位机的信号.下位机是 ...

  8. (转载)用vs2010开发基于VC++的MFC 串口通信一*****两台电脑同一个串口号之间的通信

    此文章以visual C++数据採集与串口通信測控应用实战为參考教程 此文章适合VC++串口通信入门 一.页面布局及加入控件 1, 安装好vs2010如图 2, 新建一个基于VC++的MFC项目com ...

  9. 基于Arduino和python的串口通信和上位机控制

    引言 经常的时候我们要实现两个代码之间的通信,比如说两个不同不同人写的代码要对接,例如将python指令控制Arduino控件的开关,此处使用串口通信是非常方便的,下面笔者将结合自己踩过的坑来讲述下自 ...

随机推荐

  1. 使用Typescript重构axios(十六)——请求和响应数据配置化

    0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...

  2. AutoCad 二次开发 .net 之层表的增加 删除 修改图层颜色 遍历 设置当前层

    AutoCad 二次开发 .net 之层表的增加 删除 修改图层颜色 遍历 设置当前层 AutoCad 二次开发 .net 之层表的增加 删除 修改图层颜色 遍历 设置当前层我理解的图层的作用大概是把 ...

  3. Nginx正则配置

    Nginx配置中Location的语法规则 location [ = | ~ | ~* | ^~ | !~ | !~* ] /uri/{ - } = 表示精确匹配 ~ 表示区分大小写正则匹配 ~* 表 ...

  4. 『题解』Codeforces1142A The Beatles

    更好的阅读体验 Portal Portal1: Codeforces Portal2: Luogu Description Recently a Golden Circle of Beetlovers ...

  5. 微擎 pdo_fetchall() 函数

    微擎 pdo_fetchall() 函数 注意点: 该函数内部直接执行原生 SQL 语句 如果在传递表名的时候使用了 tablename .则不加 ims_ 前缀 参数的传递通过 :param 的形式 ...

  6. suseoj 1208: 排列问题 (STL, next_permutation(A.begin(), A.end()))

    1208: 排列问题 时间限制: 1 Sec  内存限制: 128 MB提交: 2  解决: 2[提交][状态][讨论版][命题人:liyuansong] 题目描述 全排列的生成就是对于给定的字符集或 ...

  7. Mybatis实现数据的增删改查

    Mybatis实现数据的增删改查 1.项目结构(使用maven创建项目) 2.App.java package com.GetcharZp.MyBatisStudy; import java.io.I ...

  8. nyoj 274-正三角形的外接圆面积 (R = PI * a * a / 3)

    274-正三角形的外接圆面积 内存限制:64MB 时间限制:1000ms 特判: No 通过数:14 提交数:22 难度:0 题目描述: 给你正三角形的边长,pi=3.1415926 ,求正三角形的外 ...

  9. python day 1 homework 2

    多级菜单 1 三级菜单 2 可依次选择进入各子菜单 3 所需新知识点,列表,字典 province_info = {":{"name":"黑龙江", ...

  10. 在 Vue中使用layui日历控件,标注重要日子

    因为在vue文件中通过import加载,不知道为什么打包后会找不到js, 所以通过这种方法引入 在index.html中引入 文件目录 需要放在static目录下 <input type=&qu ...