基于串口通信做my_printf时遇到的坑儿
首先,完成了串口向终端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时遇到的坑儿的更多相关文章
- [python] 3 、基于串口通信的嵌入式设备上位机自动测试程序框架(简陋框架)
星期一, 20. 八月 2018 01:53上午 - beautifulzzzz 1.前言 做类似zigbee.ble mesh...无线网络节点性能测试的时候,手动操作然后看表象往往很难找出真正的原 ...
- VS2008基于对话框的MFC上位机串口通信(C++实现)简单例程
首先,在 vs2008 环境下创建 MFC 运用程序 设置项目名称为 ComTest(这个地方随意命名,根据个人习惯),点击确定后,点击下一步 出现如下界面 选择"基于对话框"模式 ...
- 基于FPGA的红外遥控解码与PC串口通信
基于FPGA的红外遥控解码与PC串口通信 zouxy09@qq.com http://blog.csdn.net/zouxy09 这是我的<电子设计EDA>的课程设计作业(呵呵,这个月都拿 ...
- 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 ...
- [stm32][ucos] 1、基于ucos操作系统的LED闪烁、串口通信简单例程
* 内容简述: 本例程操作系统采用ucos2.86a版本, 建立了5个任务 任务名 优先级 ...
- 【Delphi】基于状态机的串口通信
通信协议 串行通信接口(如RS232.RS485等)作为计算机与单片机交互数据的主要接口,广泛用于各类仪器仪表.工业监测及自动控制领域中. 通信协议是需要通信的双方所达成的一种约定,它对包括数据格式. ...
- C#做一个简单的进行串口通信的上位机
C#做一个简单的进行串口通信的上位机 1.上位机与下位机 上位机相当于一个软件系统,可以用于接收数据.控制数据.即可以对接收到的数据直接发送操控命令来操作数据.上位机可以接收下位机的信号.下位机是 ...
- (转载)用vs2010开发基于VC++的MFC 串口通信一*****两台电脑同一个串口号之间的通信
此文章以visual C++数据採集与串口通信測控应用实战为參考教程 此文章适合VC++串口通信入门 一.页面布局及加入控件 1, 安装好vs2010如图 2, 新建一个基于VC++的MFC项目com ...
- 基于Arduino和python的串口通信和上位机控制
引言 经常的时候我们要实现两个代码之间的通信,比如说两个不同不同人写的代码要对接,例如将python指令控制Arduino控件的开关,此处使用串口通信是非常方便的,下面笔者将结合自己踩过的坑来讲述下自 ...
随机推荐
- 建议收藏备用:.net core使用QRCoder生成普通二维码和带Logo的二维码详细使用教程,源码已更新至开源模板
随着互联网越来越生活化,二维码的使用越来越普遍,不论是扫码支付还是扫码关注引流,似乎我们总是离不开二维码,那么很多需要推广的文章或社区想要自己的二维码,那么你是不是需要在网站直接提供给用户呢?很多开发 ...
- NOIP模拟赛18 皇帝的烦恼O(∩_∩)O 二分+DP
题目描述 经过多年的杀戮,秦皇终于统一了中国.为了抵御外来的侵略,他准备在国土边境安置n名将军.不幸的是这n名将军羽翼渐丰,开始展露他们的狼子野心了.他们拒绝述职.拒绝接受皇帝的圣旨. 秦皇已经准备好 ...
- My Android 学习之旅--开始
其实,很早就想写写博客了,一直懒到现在. 学习android也不是今天才开始的,大概在2月份过完年之后就开始了,买了我认为还可以的书<Android从入门到精通>,花了不到一个月的时间,把 ...
- cmd命令查看Python模块函数等帮助文档和介绍
dir函数式可以查看对象的属性 使用方法很简单,举os类型为例,在Python命令窗口输入 dir(‘os’) 即可查看os模块的属性 打开cmd命令窗口 输入python(注意:计算机需要有Py ...
- python内置模块collections介绍
目录 python内置模块collections介绍 1.namedtuple 2.deque 3.defaultdict 4.OrderedDict 5.ChainMap 6.Counter 7.小 ...
- swoole不断的切换前端链接方法 防止攻击
php不断的切换前端链接方法 防止攻击 swoole写法 每分钟生成一次url后缀 返回到客户端让他们更新 //定时器要写在WorkerStart这个里面哦$ws->on('WorkerStar ...
- Forsaken Mail创建临时邮箱系统| 手把手教程
场景需求 不需要长时间使用的邮箱 需要大量创建临时邮箱 使用匿名邮箱 环境说明 **` Forsaken Mail是一个临时邮箱系统,可以供任何人接受邮件,即收即毁,支持自定义邮箱地址前缀,这里就说下 ...
- PHP常用的header头部定义
<?php header('HTTP/1.1 200 OK'); // ok 正常访问 header('HTTP/1.1 404 Not Found'); //通知浏览器 页面不存在 heade ...
- java中线程同步的几种方法
1.使用synchronized关键字 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法.在调用该方法前,需要获得内置锁,否则就处于阻塞状态. 注: synchro ...
- Jenkins 与Docker/Kubernetes的自动化CI流水(笔记)
一.CI/CD 持续集成(continuous Integration,CI):代码合并.构建.部署.测试都在一起.不断执行这个过程,并对结果反馈. 持续部署(Continuous Deploymen ...