佛山建设外贸网站公司,可信网站图标,wordpress做的外贸网站,怎么样推广一个网站[导读] 大家好#xff0c;我是逸珺。前面说会写一下Modbus-RTU的实现#xff0c;写了1000多字了#xff0c;有兴趣的稍等一下哈。前面在一个群里看到一个朋友在一个串口接收中断里打印遇到了问题#xff0c;今天聊下这个话题。扒一扒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 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.cint 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.cint putchar(int c)
{USART1-DR c;while(0 (USART1-SR (1UL 7)));return(c);
}
这就是printf重映射到串口的实现这个是一个同步查询单字节串口输出函数。大致就上面的分析总结成一个图就是这样当然这里仅仅分析了逐字符打印的串口的情况。下面回到问题本身为什么中断里不能调用printf为啥ISR不能printf 慢首先中断里肯定不适合调用printf那么为什么呢就比如上面的串口实现方式就以96001个起始位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
看就这么一句printfcode区增加了近7K字节当然如果你选择其他的printf配置可能会小一些比如不同的单片机编译器对printf的处理会不相同具体可以查查相关文档。不安全这个printf内部再很多编译环境下有可能是线程安全的。如果函数实现内部有加锁在应用程序中调用了printf但还没有执行完。但此时中断来了转而执行中断中断时是无法获取这个锁的此时程序就挂了。解决办法 可以自己实现一个print系统开辟一个环形缓冲区。如果想在中断里打印一点数据不要同步打印先将数据打印到内存再设置一个标志然后再中断外面实现真正的串口输出。如果是裸机程序只需要在主循环里检测缓冲区是否有数据有就输出到真正的串口。如果是RTOS应用可以开辟一个任务将优先级设的低一点在任务内管理这个缓冲区如果有数据就输出到串口。需要注意的是就如前面所说调用接口是不能加锁的否则就不能在中断里使用。有了这个思路要实现就不难了。—END—推荐阅读专辑|Linux文章汇总专辑|程序人生专辑|C语言我的知识小密圈关注公众号后台回复「1024」获取学习资料网盘链接。欢迎点赞关注转发在看您的每一次鼓励我都将铭记于心~嵌入式Linux微信扫描二维码关注我的公众号