扒一扒中断为什么不能调printf

[导读] 大家好,我是逸珺。

前面说会写一下Modbus-RTU的实现,写了1000多字了,有兴趣的稍等一下哈。前面在一个群里看到一个朋友在一个串口接收中断里打印遇到了问题,今天聊下这个话题。

扒一扒printf

对于单片机中printf到底向哪里打印,这个不同的编译器会有不同的处理方式。比如IAR的printf,如果是在线调试,有可能通过c-spy打印到IAR的调试终端,如果已经将printf重映射到串口,那么会从指定的串口打印出去。

以IAR ARM开发环境为例,来撸一下printf背后究竟是怎么实现的:

首先写一个简单的hello world开始:

#include <stdio.h>
int main()
{printf("Hello world");return 0;
}

接着来查找一下printf的出处,在stdio.h中找到了其声明:

__EFF_NW1  __ATTRIBUTES   void perror(const char *);
__EFF_NW1  __DEPREC_PRINTF int  printf(const char *_Restrict, ...);
__EFF_NW1  __ATTRIBUTES   int  puts(const char *);
__EFF_NW1  __DEPREC_SCANF  int  scanf(const char *_Restrict, ...);
__EFF_NR1NW2 __DEPREC_PRINTF int  sprintf(char *_Restrict,                                              const char *_Restrict, ...);
__EFF_NW1NW2 __DEPREC_SCANF int  sscanf(const char *_Restrict, 

到这里好像无法再进行下去了,先看看map文件,这里只放了map的一部分:

dl7M_tln.a: [3]XShttio.o     60  3  9abort.o       6exit.o        4low_level_init.o   4printf.o       40putchar.o       32xfail_s.o       64        1        4xprintffull_nomb.o   3 618xprout.o        22-------------------------------------------------Total:       3 850        4       13......
printf  0x00001be9   0x28  Code  Gb  printf.o [3]
putchar  0x00001c6d   0x20  Code  Gb  putchar.o [3]

看到了有一个printf.o模块被编译了,有这个文件,那么应该有源文件,试着在IAR的安装目录下找找,果然有:

.\IAR Systems\Embedded Workbench 8.0\arm\src\lib\dlib\file\printf.c

int printf(const char * _Restrict fmt, ...)
{ /* print formatted to stdout */int ans;va_list ap;  va_start(ap, fmt);ans = _Printf(&_Prout, (void *)1, fmt, &ap, 0);va_end(ap);return ans;
}

printf通过使用va_list/va_start/va_end,在这里进行可变参数的解析,而真正实现最终打印的函数是哪一个呢?是下面这句话在起作用:

_Printf(&_Prout, (void *)1, fmt, &ap, 0);

_Printf的原型是怎样的呢?在.\IAR Systems\Embedded Workbench 8.0\arm\src\lib\dlib\DLib.h中发现:

__ATTRIBUTES int _Printf(_PrintfPfnType *, void *, const char *, __Va_list *,int);

_PrintfPfnType这个是啥玩意?继续撸下去:

#if _DLIB_PRINTF_CHAR_BY_CHARtypedef void *(_PrintfPfnType)(void *, char);
#elsetypedef void *(_PrintfPfnType)(void *, const char *, _Sizet);
#endif

明白了,这个是一个函数指针,根据打印方式是否是逐字符打印,函数指针分了两种模式:逐字符模式或者缓冲区模式。

在回到printf的定义处,发现这个指针传的是_Prout。好接着扒下去,在

.\arm\src\lib\dlib\formatters\xprout.c发现了其具体的实现:

#if _DLIB_PRINTF_CHAR_BY_CHAR
void *_Prout(void *str, char c)
{return (putchar(c) == c ? str : 0);
}
#else#if _DLIB_FILE_DESCRIPTORvoid *_Prout(void *str, const char *buf, size_t n){return fwrite(buf, 1, n, stdout) == n ? str : 0;}#elsevoid *_Prout(void *str, const char *buf, size_t n){return __write(_LLIO_STDOUT, (unsigned char const *)buf, n) == n ? str : 0;}#endif
#endif

_DLIB_PRINTF_CHAR_BY_CHAR 宏是根据IAR的DLIB配置做定义。

所以IAR编译的时候会包含DLib_Defaults.h,这里就定义了逐字符模式宏,如果要采用文件方式则需要修改配置。但是一般单片机里不会这么干。所以真正的 _Prout的实现就是这样的了:

void *_Prout(void *str, char c)
{return (putchar(c) == c ? str : 0);
}

这样就定位到最终实现字符打印的函数是putchar了,而putchar是在哪里声明的呢?在stdio.h中发现了它的踪迹:

__ATTRIBUTES int  putchar(int);

来了一个好像没见过的函数前缀,再继续找一下,在.\arm\inc\c\yvals.h中找到了

#define __ATTRIBUTES  __intrinsic __nounwind

这两个关键字是编译内部使用的,文档里没有说明这个是怎么使用的,但是我猜想编译器在编译时可能会检测这个函数是否用户定义了同名函数,如定义了就使用用户定义的,没定义就使用系统库。放一个空的putchar来验证一下:

#include <stdio.h>
int putchar(int c)
{return(c);
}int main()
{printf("Hello world");return 0;
}

然后再看看map文件:

dl7M_tln.a: [3]abort.o         6exit.o          4low_level_init.o     4printf.o         40xfail_s.o        64    4xprintffull_nomb.o   3 618xprout.o        22------------------------------------------------Total:       3 758   4.......putchar   0x00001bbd    0x2  Code  Gb  main.o [1]   

putchar使用了main.o的实现。而如果使用库实现的,从前面的map文件看到putchar.o,一找发现了putchar.c文件:

int putchar(int c)
{ /* put character to stdout */unsigned char uc = c;if (__write(_LLIO_STDOUT, &uc, 1) == 1){return uc;}return EOF;
}

系统原来是调用了__write函数,在.\IAR Systems\Embedded Workbench 8.0\arm\inc\c\LowLevelIOInterface.h中找到了:

 __ATTRIBUTES size_t __write(int, const unsigned char *, size_t);

到这里不继续了,你如果再找就发现

.\8.0\arm\RTOS\SEGGER\NXP\LPC4357\Start_LPC4357_CMSIS\Setup\SEGGER_RTT_Syscalls_IAR.c

有它的实现:

size_t __write(int handle, const unsigned char * buffer, size_t size) {(void) handle;  /* Not used, avoid warning */SEGGER_RTT_Write(0, (const char*)buffer, size);return size;
}

其实就是各种底层具体输出的实现了,比如打印到c-spy,或者打印到串口。

比如在:

.\8.0\arm\src\flashloader\ST\FlashSTM32F10x\Flash_stm32f10xx.c

int putchar(int c)
{USART1->DR = c;while(0 == (USART1->SR & (1UL << 7)));return(c);
}

这就是printf重映射到串口的实现,这个是一个同步查询单字节串口输出函数。大致就上面的分析,总结成一个图就是这样:

当然这里仅仅分析了逐字符打印的串口的情况。下面回到问题本身,为什么中断里不能调用printf?

为啥ISR不能printf

首先中断里肯定不适合调用printf,那么为什么呢?就比如上面的串口实现方式,就以9600,1个起始位,1个停止位,8个数据位的常见方式为例:

你看,传输一个字节要1个毫秒,如果打印好几个字节就是好几个毫秒了,所以答案几乎就已经很清楚了,在中断函数里打印,会增加中断函数执行的时间。中断需要快进快出!比如是一个串口逐字节接收中断函数,外部的报文逐字节输入,而中断函数先打印一点日志,好几个毫秒就过去了。如果UART外设是一个单字节的接收寄存器,那完了,报文指定被冲掉了。有的UART可能有多字节FIFO,但是即便是这样,也有很大的概率会被冲掉。

这是一个中断里不能调用printf的主要原因,执行费时!

在IAR的文档里也阔以看到,如果要实现printf的重定向,需要用户实现底层的__write函数,那为啥前面又是实现的putchar呢,其实putchar最终是调用的__write函数,所以直接覆盖putchar肯定也是可以的。

另外如果编译环境配置printf不一样,这个内部实现也可能需要很多的存储空间。这对单片机而言也是不合算的。来比较一下,把printf去掉:

int main()
{return 0;
}

编译出来的结果是:

    152 bytes of readonly  code memory1024 bytes of readwrite data memory

加上后,编译出来是这样:

  7470 bytes of readonly  code memory34 bytes of readonly  data memory1037 bytes of readwrite data memory

看就这么一句printf,code区增加了近7K字节!当然如果你选择其他的printf配置,可能会小一些,比如:

不同的单片机编译器对printf的处理会不相同,具体可以查查相关文档。

不安全

这个printf内部再很多编译环境下,有可能是线程安全的。如果函数实现内部有加锁,在应用程序中调用了printf,但还没有执行完。但此时中断来了,转而执行中断,中断时是无法获取这个锁的,此时程序就挂了。

解决办法

可以自己实现一个print系统,开辟一个环形缓冲区。如果想在中断里打印一点数据,不要同步打印,先将数据打印到内存,再设置一个标志,然后再中断外面实现真正的串口输出。

  • 如果是裸机程序,只需要在主循环里检测缓冲区是否有数据,有就输出到真正的串口。

  • 如果是RTOS应用,可以开辟一个任务,将优先级设的低一点,在任务内管理这个缓冲区,如果有数据就输出到串口。需要注意的是,就如前面所说,调用接口是不能加锁的,否则就不能在中断里使用。

有了这个思路,要实现就不难了。

END


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465877.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

躺平,躺下就能赢吗?

之前在群里讨论这个话题&#xff0c;说躺平挺好的&#xff0c;没那么大压力&#xff0c;我也觉得躺平是好事&#xff0c;每个人都要追求理想的权力&#xff0c;那么反过来&#xff0c;每个人也有不追求理想的权力。躺平如果说的好听一些&#xff0c;也可以认为是躺赢&#xff0…

感觉stm32太简单是一种自负吗?

其实简单或者复杂都不重要&#xff0c;重要的是通过STM32我们能学习到什么&#xff1f;做一个键盘/鼠标&#xff0c;可以学习USB协议。做一个联网设备&#xff0c;需要学习以太网&#xff0c;TCP/IP协议的底层实现。做一个无线设备&#xff0c;可能需要学习蓝牙、WIFI或者zigbe…

耗时两年,19岁小伙采用230片纯74逻辑芯片搭建出32位处理器!可玩贪吃蛇

从设计 CPU、制作原型机、最终成品到软件编程&#xff0c;19 岁极客小伙用了整整两年的时间。RISC-V 是一个基于精简指令集&#xff08;RISC&#xff09;原则的开源指令集架构&#xff08;ISA&#xff09;&#xff0c;它是对应开源软件运动的一种「开源硬件」。该项目于 2010 年…

VC++ 6.0 与VS2008 C++ DEBUG工具(Windows)介绍

在VC 6.0 里面&#xff0c;debug工具有这些&#xff1a; 请看大图展示&#xff1a; 这里面几个工具都是经常用到的。比如&#xff1a;上下文信息&#xff0c;可以查看当前变量的值和地址Memory&#xff1a; 可以输入地址查看里面的内容。对调试和检查指针特别有用。 在vs2008里…

数组与指针不能混用的情况

扫描二维码获取更多精彩嵌入式杂牌军编辑|追梦星空公众号|嵌入式杂牌军✎ 编 者 悟 语正当性的出发点应该是少目的性、不带偏见、以尊重为前提的多方考虑。文 章 导 读C中的数组有能混用的情况&#xff0c;有不能混用的情况&#xff0c;今天就带小伙伴们探究一下&#xff01;阅…

[CTO札记]盛大游戏上市,是对《文化产业振兴规划》的响应

重要国策《文化产业振兴规划》于9月26日正式对外公布。巧合的是&#xff0c;就在前一天&#xff08;9月25日 &#xff09;&#xff0c;盛大游戏&#xff08;SDG&#xff09;成功在NASDAQ上市&#xff0c;并且创下了美股IPO规模之最&#xff08;10亿美元&#xff09;。1&#xf…

Linux内核入门之路 (非广告)

笔者从开始接触 Linux 内核应该有 4 ~ 5 年了&#xff0c;虽然不敢说非常了解 Linux 内核&#xff0c;但起码也有了点眉目。所以&#xff0c;本文主要想分享一下我的 Linux 内核入门之路&#xff0c;如果对大家有帮助的话&#xff0c;希望能够转发一下&#xff0c;帮助更多想学…

万能红外遥控开发

下班刚回到家&#xff0c;天气很热~空调遥控器呢&#xff1f;找不到&#xff01;躺平不想动~风扇遥控器呢&#xff1f;找不到&#xff01;想看电视&#xff0c;不想动~电视遥控器呢&#xff1f;找不到&#xff01;好不容易找到了&#xff0c;遥控器没电&#xff1f;崩溃&#x…

[Lydsy1805月赛] 对称数

挺不错的一道数据结构题QWQ。 一开始发现这个题如果不看数据范围的话&#xff0c;妥妥的树上莫队啊23333&#xff0c;然鹅10组数据是不可能让你舒舒服服的树上莫队卡过的23333 于是想了想&#xff0c;这个题的模型就是&#xff0c;把u到v链上的权值出现奇偶次的01串搞出来&…

linux下被遗忘的gpio_keys按键驱动

我们新项目硬件设计上使用gpio口做按键&#xff0c;所以我就需要搞定这个驱动&#xff0c;本来想自己写一个gpio口的按键驱动&#xff0c;然后看了下内核下面的代码&#xff0c;已经有现成的了。Linux内核下游很多很多的现成驱动&#xff0c;只要你想得到的&#xff0c;基本都是…

如何通过审计安全事件日志检测密码喷洒(Password Spraying)攻击

许多渗透测试人员和攻击者通常都会使用一种被称为“密码喷洒&#xff08;Password Spraying&#xff09;”的技术来进行测试和攻击。对密码进行喷洒式的攻击&#xff0c;这个叫法很形象&#xff0c;因为它属于自动化密码猜测的一种。这种针对所有用户的自动密码猜测通常是为了避…

单片机编程技巧—状态机编程

摘要&#xff1a;不知道大家有没有这样一种感觉&#xff0c;就是感觉自己玩单片机还可以&#xff0c;各个功能模块也都会驱动&#xff0c;但是如果让你完整的写一套代码&#xff0c;却无逻辑与框架可言&#xff0c;上来就是开始写&#xff01;东抄抄写抄抄。说明编程还处于比较…

啥叫旁路电容?啥叫去耦?可以不再争论了吗

1. 旁路和去耦先谈两个比较重要的概念&#xff1a;旁路电容(Bypass Capacitor)&#xff0c;去耦电容(Decoupling Capacitor)。只要是设计过硬件电路的同学肯定对这两个词不陌生&#xff0c;但真正理解这两个概念的可能并不多。我刚毕业时就问过我的师傅&#xff0c;为什么总是在…

H5活动产品设计指南基础版

本文来自 网易云社区 。 H5一般页面不会很多&#xff0c;看似简单&#xff0c;实际上会有很多细节需要注意&#xff0c;我自己在做过了几个H5之后&#xff0c;发现了一些常犯的问题&#xff0c;做了小结&#xff0c;希望给新开始做H5的产品相关的同学提供一些帮助。 首先说说一…

图解丨在嵌入式设备上实现HTTP服务器

您好呀&#xff0c;我是小二。本期为大家带来一个 WiFi 应用的实际场景&#xff0c;其实在之前「我对 WiFi 驱动移植过程&#xff0c;做了一次总结复盘」这篇文章中有简单提过&#xff0c;但由于内容较多&#xff0c;就单独摘出来了。来自读者的催更????????????&a…

maven详解之仓库

在Maven中&#xff0c;任何一个依赖、插件或者项目构建的输出&#xff0c;都可以称之为构件。 Maven在某个统一的位置存储所有项目的共享的构件&#xff0c;这个统一的位置&#xff0c;我们就称之为仓库。&#xff08;仓库就是存放依赖和插件的地方&#xff09; 任何的构件都有…

Linux内核代码,第一次看到这样使用的宏

晚上看内核代码&#xff0c;看到一个有意思的宏&#xff0c;我之前没有见到过&#xff0c;当然&#xff0c;肯定有人见到过&#xff0c;我写出来是给那些没有看到过的人看的。我说是深夜&#xff0c;那就肯定是深夜代码是从内核里面看到的我们正常使用宏是这样的C语言、嵌入式中…

flex柱状图和折线图的混合图使用

<?xml version"1.0"?> <mx:Application xmlns:mx"http://www.adobe.com/2006/mxml"> <mx:Script> <!--[CDATA[ import mx.collections.ArrayCollection; [Bindable] public var data1:ArrayCollectionnew ArrayCollection([{date:&…

给高通提个问题解决为啥那么久?

我第一次接触高通芯片是在中兴那&#xff0c;我们用的是一款很老的芯片&#xff0c;高通的文档非常多&#xff0c;资料非常丰富&#xff0c;如果有问题的话我一般都会从文档里面找答案。但是&#xff0c;但是&#xff0c;并不是所有的问题都是能自己搞定&#xff0c;比如遇到一…

新公司上班第一天

大家好&#xff0c;文章转自我的朋友helloworld&#xff0c;文中的我并不是我&#xff0c;感谢大家阅读&#xff0c;转载&#xff0c;在看。Hello 各位小伙伴&#xff0c;周一愉快~今天是我到新公司上班的第一天&#xff0c;入职新公司&#xff0c;就好像刚刚谈恋爱一样&#x…