扒一扒中断为什么不能调printf
[導(dǎo)讀] 大家好,我是逸珺。
前面說會(huì)寫一下Modbus-RTU的實(shí)現(xiàn),寫了1000多字了,有興趣的稍等一下哈。前面在一個(gè)群里看到一個(gè)朋友在一個(gè)串口接收中斷里打印遇到了問題,今天聊下這個(gè)話題。
扒一扒printf
對(duì)于單片機(jī)中printf到底向哪里打印,這個(gè)不同的編譯器會(huì)有不同的處理方式。比如IAR的printf,如果是在線調(diào)試,有可能通過c-spy打印到IAR的調(diào)試終端,如果已經(jīng)將printf重映射到串口,那么會(huì)從指定的串口打印出去。
以IAR ARM開發(fā)環(huán)境為例,來擼一下printf背后究竟是怎么實(shí)現(xiàn)的:
首先寫一個(gè)簡(jiǎn)單的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,?到這里好像無法再進(jìn)行下去了,先看看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]看到了有一個(gè)printf.o模塊被編譯了,有這個(gè)文件,那么應(yīng)該有源文件,試著在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,在這里進(jìn)行可變參數(shù)的解析,而真正實(shí)現(xiàn)最終打印的函數(shù)是哪一個(gè)呢?是下面這句話在起作用:
_Printf(&_Prout,?(void?*)1,?fmt,?&ap,?0);_Printf的原型是怎樣的呢?在.\IAR Systems\Embedded Workbench 8.0\arm\src\lib\dlib\DLib.h中發(fā)現(xiàn):
__ATTRIBUTES?int?_Printf(_PrintfPfnType?*,?void?*,?const?char?*,?__Va_list?*,int);_PrintfPfnType這個(gè)是啥玩意?繼續(xù)擼下去:
#if?_DLIB_PRINTF_CHAR_BY_CHARtypedef?void?*(_PrintfPfnType)(void?*,?char); #elsetypedef?void?*(_PrintfPfnType)(void?*,?const?char?*,?_Sizet); #endif明白了,這個(gè)是一個(gè)函數(shù)指針,根據(jù)打印方式是否是逐字符打印,函數(shù)指針分了兩種模式:逐字符模式或者緩沖區(qū)模式。
在回到printf的定義處,發(fā)現(xiàn)這個(gè)指針傳的是_Prout。好接著扒下去,在
.\arm\src\lib\dlib\formatters\xprout.c發(fā)現(xiàn)了其具體的實(shí)現(xiàn):
#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 宏是根據(jù)IAR的DLIB配置做定義。
所以IAR編譯的時(shí)候會(huì)包含DLib_Defaults.h,這里就定義了逐字符模式宏,如果要采用文件方式則需要修改配置。但是一般單片機(jī)里不會(huì)這么干。所以真正的 _Prout的實(shí)現(xiàn)就是這樣的了:
void?*_Prout(void?*str,?char?c) {return?(putchar(c)?==?c???str?:?0); }這樣就定位到最終實(shí)現(xiàn)字符打印的函數(shù)是putchar了,而putchar是在哪里聲明的呢?在stdio.h中發(fā)現(xiàn)了它的蹤跡:
__ATTRIBUTES?int??putchar(int);來了一個(gè)好像沒見過的函數(shù)前綴,再繼續(xù)找一下,在.\arm\inc\c\yvals.h中找到了
#define?__ATTRIBUTES??__intrinsic?__nounwind這兩個(gè)關(guān)鍵字是編譯內(nèi)部使用的,文檔里沒有說明這個(gè)是怎么使用的,但是我猜想編譯器在編譯時(shí)可能會(huì)檢測(cè)這個(gè)函數(shù)是否用戶定義了同名函數(shù),如定義了就使用用戶定義的,沒定義就使用系統(tǒng)庫(kù)。放一個(gè)空的putchar來驗(yàn)證一下:
#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的實(shí)現(xiàn)。而如果使用庫(kù)實(shí)現(xiàn)的,從前面的map文件看到putchar.o,一找發(fā)現(xiàn)了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; }系統(tǒng)原來是調(diào)用了__write函數(shù),在.\IAR Systems\Embedded Workbench 8.0\arm\inc\c\LowLevelIOInterface.h中找到了:
?__ATTRIBUTES?size_t?__write(int,?const?unsigned?char?*,?size_t);到這里不繼續(xù)了,你如果再找就發(fā)現(xiàn)
.\8.0\arm\RTOS\SEGGER\NXP\LPC4357\Start_LPC4357_CMSIS\Setup\SEGGER_RTT_Syscalls_IAR.c
有它的實(shí)現(xiàn):
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; }其實(shí)就是各種底層具體輸出的實(shí)現(xiàn)了,比如打印到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重映射到串口的實(shí)現(xiàn),這個(gè)是一個(gè)同步查詢單字節(jié)串口輸出函數(shù)。大致就上面的分析,總結(jié)成一個(gè)圖就是這樣:
當(dāng)然這里僅僅分析了逐字符打印的串口的情況。下面回到問題本身,為什么中斷里不能調(diào)用printf?
為啥ISR不能printf
慢
首先中斷里肯定不適合調(diào)用printf,那么為什么呢?就比如上面的串口實(shí)現(xiàn)方式,就以9600,1個(gè)起始位,1個(gè)停止位,8個(gè)數(shù)據(jù)位的常見方式為例:
你看,傳輸一個(gè)字節(jié)要1個(gè)毫秒,如果打印好幾個(gè)字節(jié)就是好幾個(gè)毫秒了,所以答案幾乎就已經(jīng)很清楚了,在中斷函數(shù)里打印,會(huì)增加中斷函數(shù)執(zhí)行的時(shí)間。中斷需要快進(jìn)快出!比如是一個(gè)串口逐字節(jié)接收中斷函數(shù),外部的報(bào)文逐字節(jié)輸入,而中斷函數(shù)先打印一點(diǎn)日志,好幾個(gè)毫秒就過去了。如果UART外設(shè)是一個(gè)單字節(jié)的接收寄存器,那完了,報(bào)文指定被沖掉了。有的UART可能有多字節(jié)FIFO,但是即便是這樣,也有很大的概率會(huì)被沖掉。
這是一個(gè)中斷里不能調(diào)用printf的主要原因,執(zhí)行費(fèi)時(shí)!
在IAR的文檔里也闊以看到,如果要實(shí)現(xiàn)printf的重定向,需要用戶實(shí)現(xiàn)底層的__write函數(shù),那為啥前面又是實(shí)現(xiàn)的putchar呢,其實(shí)putchar最終是調(diào)用的__write函數(shù),所以直接覆蓋putchar肯定也是可以的。
大
另外如果編譯環(huán)境配置printf不一樣,這個(gè)內(nèi)部實(shí)現(xiàn)也可能需要很多的存儲(chǔ)空間。這對(duì)單片機(jī)而言也是不合算的。來比較一下,把printf去掉:
int?main() {return?0; }編譯出來的結(jié)果是:
????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區(qū)增加了近7K字節(jié)!當(dāng)然如果你選擇其他的printf配置,可能會(huì)小一些,比如:
不同的單片機(jī)編譯器對(duì)printf的處理會(huì)不相同,具體可以查查相關(guān)文檔。
不安全
這個(gè)printf內(nèi)部再很多編譯環(huán)境下,有可能是線程安全的。如果函數(shù)實(shí)現(xiàn)內(nèi)部有加鎖,在應(yīng)用程序中調(diào)用了printf,但還沒有執(zhí)行完。但此時(shí)中斷來了,轉(zhuǎn)而執(zhí)行中斷,中斷時(shí)是無法獲取這個(gè)鎖的,此時(shí)程序就掛了。
解決辦法
可以自己實(shí)現(xiàn)一個(gè)print系統(tǒng),開辟一個(gè)環(huán)形緩沖區(qū)。如果想在中斷里打印一點(diǎn)數(shù)據(jù),不要同步打印,先將數(shù)據(jù)打印到內(nèi)存,再設(shè)置一個(gè)標(biāo)志,然后再中斷外面實(shí)現(xiàn)真正的串口輸出。
如果是裸機(jī)程序,只需要在主循環(huán)里檢測(cè)緩沖區(qū)是否有數(shù)據(jù),有就輸出到真正的串口。
如果是RTOS應(yīng)用,可以開辟一個(gè)任務(wù),將優(yōu)先級(jí)設(shè)的低一點(diǎn),在任務(wù)內(nèi)管理這個(gè)緩沖區(qū),如果有數(shù)據(jù)就輸出到串口。需要注意的是,就如前面所說,調(diào)用接口是不能加鎖的,否則就不能在中斷里使用。
有了這個(gè)思路,要實(shí)現(xiàn)就不難了。
—END—
推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語(yǔ)言
我的知識(shí)小密圈
關(guān)注公眾號(hào),后臺(tái)回復(fù)「1024」獲取學(xué)習(xí)資料網(wǎng)盤鏈接。
歡迎點(diǎn)贊,關(guān)注,轉(zhuǎn)發(fā),在看,您的每一次鼓勵(lì),我都將銘記于心~
嵌入式Linux
微信掃描二維碼,關(guān)注我的公眾號(hào)
總結(jié)
以上是生活随笔為你收集整理的扒一扒中断为什么不能调printf的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [内核同步]自旋锁spin_lock、s
- 下一篇: Vscode 用Filter Line看