扒一扒中断为什么不能调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,一经查实,立即删除!

相关文章

P4555 最长双回文串

题目描述 顺序和逆序读起来完全一样的串叫做回文串。比如acbca是回文串&#xff0c;而abc不是&#xff08;abc的顺序为abc&#xff0c;逆序为cba&#xff0c;不相同&#xff09;。 输入长度为 n的串 S &#xff0c;求 S的最长双回文子串 T ,即可将 T 分为两部分 X&#xff0c; …

每日一题(34)—— 线程

1&#xff0e;线程的概念&#xff1a; 2 与进程的区别: 3 线程与进程的区别和联系? 线程是否具有相同的堆栈? 1、线程是指进程内的一个执行单元,也是进程内的可调度实体. 2、与进程的区别: (1)调度&#xff1a;线程作为调度和分配的基本单位&#xff0c;进程作为拥有资源…

如何利用MySQL加密函数保护Web网站敏感数据

如果您正在运行使用MySQL的Web应用程序&#xff0c;那么它把密码或者其他敏感信息保存在应用程序里的机会就很大。保护这些数据免受***或者窥探者的获取是一个令人关注的重要问题&#xff0c;因为您既不能让未经授权的人员使用或者破坏应用程序&#xff0c;同时还要保证您的竞争…

躺平,躺下就能赢吗?

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

第一站---大连---看海之旅

5月下旬至7月上旬 是大连最舒服的季节 还不是很热 微风拂面 神清气爽的感觉 工具/原料 坐公交的零钱防晒霜可以下水的鞋推荐一下旅游路线吧个人意见:来大连必去的几个景点分为两类:免费的: 1.星海湾景区。在星海湾景区你可以来到星海广场、星海湾浴场、星海公园&#xff0c;这…

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

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

回车 触发 提交事件

//gridview 文本框 回车 触发 Button 事件 < script type "text/javascript" language "javascript" > function clickButton() { if ( event.keyCode 13 ) { document.all ( ibtnUpdate ).click(); return false; } } < / script > < a…

每日一题(35)—— heap与stack的差别

heap与stack的差别: Heap是堆&#xff0c;stack是栈。 Stack的空间由操作系统自动分配/释放&#xff0c;Heap上的空间手动分配/释放。 Stack空间有限&#xff0c;Heap是很大的自由存储区 C中的malloc函数分配的内存空间即在堆上,C中对应的是new操作符。 程序在编译期对变量…

Scrapy学习-25-Scrapyd部署spider

Scrapyd部署爬虫项目 github项目https://github.com/scrapy/scrapyd 官方文档http://scrapyd.readthedocs.org/ 安装scrapydhttp://scrapyd.readthedocs.io/en/stable/install.html 运行# 在虚拟环境中使用scrapd命令# scrapyd是一个服务器监听在本地的6800端口scrapyd 安装scr…

耗时两年,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里…

每日一题(36)—— 什么是预编译 , 何时需要预编译?

什么是预编译 , 何时需要预编译&#xff1f; &#xff11;、总是使用不经常改动的大型代码体。 &#xff12;、程序由多个模块组成&#xff0c;所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下&#xff0c;可以将所有包含文件预编译为一个预编译头。

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

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

解决/WEB-INF目录下的jsp页面引入webRoot下的Js、css和图片的问题

通常把jsp页面放在webRoot的/WEB-INF下可以防止访问者直接输入页面。 而webRoot的/WEB-INF下的页面是受保护的&#xff0c;用户无法通过形如http://localhost:8080/test/webRoot/WEB-INF/content/xx.jsp来访问&#xff0c;他们的访问都必须透过struts处理请求才能达到。 然而&a…

[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;帮助更多想学…

BZOJ3251: 树上三角形

BZOJ3251: 树上三角形 Description 给定一大小为n的有点权树&#xff0c;每次询问一对点(u,v)&#xff0c;问是否能在u到v的简单路径上取三个点权&#xff0c;以这三个权值为边长构成一个三角形。同时还支持单点修改。Input 第一行两个整数n、q表示树的点数和操作数第二行n个整…

CSS 背景透明

一、首先说下CSS实现背景全透明 全透明比较好实现。在要实现透明功能的id或Class里&#xff0c;输入 {background:transparent;}在IE、Firefox等浏览器下都有效果。二、再来说下CSS实现背景半透明 可以实现自定义的透明程度&#xff0c;代码如下&#xff1a; { filter:alpha(op…

万能红外遥控开发

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

每日一题(38)—— 一个32位的机器,该机器的指针是多少位?

一个32位的机器,该机器的指针是多少位&#xff1f; 指针是多少位只要看地址总线的位数就行了。80386以后的机子都是32的数据总线。所以指针的位数就是4个字节了。