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

本文深入探讨了单片机中printf函数的工作原理及其在不同编译环境下的实现细节,并分析了为何中断处理程序不宜使用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  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(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 memory
  1 024 bytes of readwrite data memory

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

  7 470 bytes of readonly  code memory
     34 bytes of readonly  data memory
  1 037 bytes of readwrite data memory

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

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

不安全

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

解决办法

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

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

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

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

END

往期精彩推荐,点击即可阅读

万变不离其宗之单片机串口问题 

▲万变不离其宗之I2C总线要点总结

万变不离其宗之SPI总线要点总结

▲长文图解工业HART总线协议

<think> 我们之前的问题是关于C语言中计算平均成绩的代码,其中提到了将printf放在循环内外的区别。现在用户的问题是:“为什么不能printf放在for循环里”,但实际上在修正后的代码中,我们将printf放在了循环内部(用于提示输入每科的成绩),而另printf(提示输入五科成绩)放在了循环外部。所以用户可能误解了,认为printf不能放在循环内。实际上,printf可以放在循环内,但需要根据具体需求决定。 问题分析: 用户可能注意到原错误代码中,循环内有printf提示输入,而修正后的代码在循环外也有printf。因此用户可能困惑:为什么有时候把printf放在循环内,有时候放在循环外?或者是否有什么情况下不能放在循环内? 回答: printf函数可以放在循环内部,也可以放在循环外部,这取决于程序的需求。在循环内部,每次循环都会执行printf;在循环外部,则只执行次。 例如,在修正后的代码中: - 循环外部的printf("请输入五科成绩:\n"); 只执行次,作为整体提示。 - 循环内部的printf("第%d科成绩: ", i); 则执行五次,每次提示输入科。 所以,并不是不能放在循环内,而是要根据实际需求选择。 但是,如果错误地放置printf可能会导致: 1. 不必要的重复提示:比如如果只需要提示次,却放在循环内,就会重复出现相同提示。 2. 性能问题:如果循环次数非常多,频繁printf(涉及I/O操作)会降低程序运行速度。 因此,正确做法是根据程序逻辑决定printf的位置。 举例说明: 情况1:循环外提示次(整体提示) ```c printf("请输入五科成绩:\n"); for (int i=0; i<5; i++) { // 输入成绩 } ``` 情况2:循环内每次提示(每次循环都提示) ```c for (int i=0; i<5; i++) { printf("请输入第%d科成绩:", i+1); // 输入成绩 } ``` 情况3:混合使用(整体提示+每次提示) ```c printf("请输入五科成绩:\n"); for (int i=0; i<5; i++) { printf("第%d科:", i+1); // 输入成绩 } ``` 所以,没有绝对不能放在循环内的说法,而是根据需求。 但是,如果放在循环内的printf语句是用于打印固定不变的内容,且循环次数很多,那么就会输出大量重复内容,可能不是我们想要的。例如: ```c for (int i=0; i<10000; i++) { printf("正在处理第%d次循环...\n", i); // 这会产生万行输出,可能影响性能 } ``` 在大量循环中频繁输出,不仅可能使控制台输出变得冗长,还会降低程序运行速度(因为I/O操作相对较慢)。因此,在不需要实时监控的情况下,通常避免在循环次数较多的循环中频繁使用printf。 结论:printf可以放在循环内,但需要根据实际需求以及性能影响来决定。 相关问题: 根据以上分析,提出以下相关问题: §§相关问题§§: 1. 在C语言中,如何减少频繁printf对程序性能的影响? 2. 如果需要在循环中输出进度信息(例如每10%输出次),应如何实现? 3. 在循环中使用printf输出日志时,有哪些最佳实践? 4. 除了printf,C语言中还有哪些常用的输出函数?它们有什么区别? </think> 在C语言中,`printf` **完全可以放在`for`循环内部**使用,但需要注意使用场景和效率问题。您可能是看到某些代码避免在循环中使用`printf`,这通常是出于以下原因: --- ### 为什么有时避免在循环中使用`printf` 1. **性能问题**(主要考虑): ```c for(int i=0; i<1000000; i++) { printf("%d\n", i); // 百万次I/O操作极慢 } ``` - `printf`涉及系统用和I/O操作(硬盘/终端) - 每次用都有固定开销(上下文切换、缓冲区管理) - 在大型循环中使用会使程序变慢数十甚至数百倍 2. **输出缓冲区刷新问题**: ```c for(int i=0; i<5; i++) { printf("Progress: %d", i); // 注意:没有\n // 某些系统下输出可能不会立即显示 } ``` - `printf`默认使用行缓冲(遇到`\n`才刷新) - 无换行符时,输出可能卡在缓冲区不显示 3. **代码可读性**: ```c // 较差的做法 for(int i=0; i<100; i++) { printf("Value: %d, Square: %d\n", i, i*i); } // 更好的做法(减少重复文本) printf("Values and Squares:\n"); for(int i=0; i<100; i++) { printf("%d -> %d\n", i, i*i); } ``` --- ### 何时可以在循环中使用`printf` ```c // 完全可接受的使用场景 for(int i=1; i<=5; i++) { printf("请输入第%d科成绩: ", i); // 用户交互需要即时反馈 scanf("%lf", &score); } ``` ✅ **适合在循环中使用的情况**: 1. 需要实时用户交互(如输入提示) 2. 循环次数较少(如 < 1000次) 3. 试时输出关键变量值 4. 需要逐步显示进度(如进度条) ❌ **应避免的情况**: 1. 高性能计算中的大型循环 2. 需要频繁输出的日志系统 3. 嵌入式系统等资源受限环境 --- ### 优化建议 如果必须在循环中输出,可通过缓冲区优化: ```c // 使用缓冲区减少I/O次数 char buffer[1024]; int offset = 0; for(int i=0; i<100000; i++) { offset += snprintf(buffer+offset, sizeof(buffer)-offset, "%d\n", i); if(offset > 1000) { // 缓冲区快满时刷新 fwrite(buffer, 1, offset, stdout); offset = 0; } } // 刷新剩余内容 if(offset > 0) fwrite(buffer, 1, offset, stdout); ``` --- ### 正确示例(成绩输入场景) ```c #include <stdio.h> int main() { double total = 0.0; // 完全合理:循环内printf用于交互提示 for(int i=1; i<=5; i++) { double score; printf("请输入第%d科成绩: ", i); // ✅ 必要输出 scanf("%lf", &score); total += score; } printf("平均分: %.2f\n", total/5); return 0; } ```
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嵌入式客栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值