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

嵌入式资讯精选 2021-08-16 11:46

前面说会写一下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  9
    abort.o       6
    exit.o        4
    low_level_init.o   4
    printf.o     40
    putchar.o    32
    xfail_s.o      64        1        4
    xprintffull_nomb.o   3 618
    xprout.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_CHAR
  typedef void *(_PrintfPfnType)(void *, char);
#else
  typedef 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_DESCRIPTOR
  void *_Prout(void *str, const char *buf, size_t n)
  {
    return fwrite(buf, 1, n, stdout) == n ? str : 0;
  }
  #else
  void *_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         6
    exit.o          4
    low_level_init.o    4
    printf.o         40
    xfail_s.o       64    4
    xprintffull_nomb.o   3 618
    xprout.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(intconst 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 memory
  1024 bytes of readwrite data memory

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

  7470 bytes of readonly  code memory
     34 bytes of readonly  data memory
  1037 bytes of readwrite data memory

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

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

不安全

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

解决办法

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

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

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

1.国产嵌入式操作系统,你最看好哪一个?

2.杨福宇专栏|温州特斯拉失控案EDR数据的深度解读

3.【视频上线】嵌入式实时操作系统学习之道

4.学习STM32单片机,绕不开的串口

5.RISC-V产业的现状与未来~

6.当MCU死机了,先把硬件抓过来~

免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。

嵌入式资讯精选 掌握最鲜资讯,尽领行业新风
评论
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 204浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 223浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 117浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 58浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 92浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 70浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 164浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 124浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 108浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 141浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 61浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦