扒一扒中断为什么不能调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
微信掃描二維碼,關注我的公眾號
總結
以上是生活随笔為你收集整理的扒一扒中断为什么不能调printf的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [内核同步]自旋锁spin_lock、s
- 下一篇: SQL 插入 CLOB类型