使用 VSCode 給STM32配置一個串口 printf 工程
gcc 重定向 printf 和 keil 不一樣。
文件准備
-
先從以前的工程中拷過一份串口的代碼來,然后在 main 函數中初始化串口並 print 一個數據吧。
-
新添加的文件需要添加到 Markfile 文件中,否則編譯肯定會報錯的。同時為了 vscode 不報錯也把 include 路徑在 c_cpp_properties.json 中放一份。
.h
文件路徑 ->Makefile
+c_cpp_properties.json
.c
文件 ->Makefile
-
之后還需要在 stm32f1xx_hal_conf.h 刪除 ...uart.h 和 ...usart.h 的注釋,然后把 HAL 庫里串口的 .c 文件添加到 Makefile .
-
-
編譯通過,但是下載進去果然不行,串口沒出來任何東西。這是因為 gcc 和 Keil 關於 printf() 函數底層的實現不一樣。在 Keil 中需要重定向的是 fputc() 函數,但是在 GCC 中不太一樣。
重定向_Printf
-
已知在 GCC 中想要使用 printf() 函數是需要重定向 _write() 函數的。
-
想要知道在 GCC 中怎么用,最好看看官方怎么說,別管哪個官方說的總會比私人說的靠譜。先試着找一下 ST 固件庫的示例工程中有沒有用到 printf() 的。很幸運的是官方提供了示例工程,在固件庫的
...\STM32Cube_FW_F1_V1.8.0\Projects\STM32F103RB-Nucleo\Examples\UART\UART_Printf
目錄中可以拿到它(每一種芯片的目錄下應該都有對應的這個例程)。 -
打開示例工程的目錄,可以看到里面有一個 readme.txt , 把 .txt 給它重命名為 .md 用 vscode 打開我們就可以十分清晰的看到它的介紹信息了。不難發現這個工程其實是讓單片機連接超級終端用的。既然要連接超級終端那想必除了要實現 printf() 還要實現 scanf() 和其他的東東吧。不過我們目前只關心 printf(), 剩下的以后再收拾。
-
我們需要用到這個工程里的兩個文件,
main.c
和syscalls.c
. main.c 的重要性沒什么可說的,而 syscalls 翻譯過來是系統調用的意思,因此我覺得所有的底層應該都是在這里實現的。-
首先看他的 main.c 文件,把所有干擾視線的注釋刪除掉可以發現其中除了各模塊初始化等我們熟悉的代碼外就多了以下兩段內容。
...... /* 在我們的工程中 __GNUC__ 肯定是定義了的,因此這一段其實就只有 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) 生效了 */ /* Private function prototypes -----------------------------------------------*/ #ifdef __GNUC__ /* With GCC, small printf (option LD Linker->Libraries->Small printf set to 'Yes') calls __io_putchar() */ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif /* __GNUC__ */ ...... /* 這里寫了 PUTCHAR_PROTOTYPE ,這不就是上面定義的那個宏嗎,也就是說這里重寫了 int __io_putchar(int ch) 這個函數,但是這跟 printf() 有什么關系呢? */ PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1, 0xFFFF); return ch; } ......
-
接下來打開 syscalls.c 文件,這個文件在
\Examples\UART\UART_Printf\SW4STM32
目錄下(SW4STM32 基於GCC的STM32的編譯調試工具鏈,看到 GCC 就覺得它很可愛🙃)。打開后果然發現這里面重寫了 _write() 函數,而且其內容就是上文重寫的 __io_putchar(int ch) 那個函數,到這里一切疑問就迎刃而解了。__attribute__((weak)) int _write(int file, char *ptr, int len) { int DataIdx; for (DataIdx = 0; DataIdx < len; DataIdx++) { __io_putchar(*ptr++); } return len; }
-
-
在搞明白原理之后我們只需用 main.c 中重定向 __io_putchar(int ch) 的部分源碼替換掉 usart.c 在 Keil 中重定向 printf() 的那部分代碼,然后將 syscalls.c 文件添加到工程並添加到 Makefile 文件中使其編譯就可以正常使用 printf() 函數了。這里其實不把 syscalls.c 整個文件拿過來只要重寫 _write() 的那部分也可以,但是看在這個文件體積也不大的份上還是把他拿過來吧,萬一以后用上也方便。
-
好了,不出意外的話現在我們就可以正常的使用 printf() 了,試驗一下吧。
-
關於無法打印浮點數的問題。試了試好像確實沒辦法打印浮點數,用 sprintf() 也不行。不過問題不大,在 Makefile 文件中找到 LDFLAGS 選項然后在里面添加
-u _printf_float
參數就可以了,添加以后printf() 和 sprintf() 正常使用。 -
Ps.日常寫程序時常用的除了 printf() 還有 sprintf() 和 sscanf() 這兩個數字和字符串互轉的函數,前面說了 sprintf() 已經可以正常用了,那么 sprintf() 呢?其實一樣的道理, sprintf() 默認也是不能轉浮點數的,但是在 Makefile 里對應的加一句
-u _scanf_float
就萬事大吉了。
使用_printf_需要注意的地方
-
printf() 只有在檢測到 '\n' 時才會從緩存區把數據發出去,因此在數據結尾一定要有 '\n', 否則數據是肯定傳不出去的。這次滯留的數據有可能會在下次發送帶 '\n' 的數據時隨它一塊過去,當然也有可能被覆蓋,因此記得'\n'. 如果真有什么特殊需求不能用 '\n' 的話在發完數據之后就要運行一次
fflush(stdout)
強制刷新一次輸出流,這樣數據也是能發出去的。 -
編碼問題。VSCode 默認使用的編碼是 UTF-8 因此如果你的輸出有中文的話請找一個支持 UTF-8 的串口助手查看,否則肯定會亂碼,實測 Windows 應用商店里的 串口調試助手 可用。雖然 VSCode 也能改成 GB2312 編碼,但我勸你還是忘記那個糟糕的東西吧。
-
剛才又發現 vscode 一個莫名奇妙的問題,他說我的串口句柄(一個變量)沒定義,扯淡我明明定義了。后來試了一下把 main.c 中最后一項 include
#include "stdio.h"
移到頂端就沒問題了。C/C++這個插件也是莫名其妙,渣渣。
總結
本篇介紹在 GCC 中重定向 printf() 方法,也順便解答了從之前的 Keil 工程中將文件移植到 GCC 項目使用的問題,總結起來步驟大概分為以下幾個:
-
復制文件。把文件復制到工程目錄下你喜歡的地方。
-
添加 includepath. 這一步需要分別在 Makefile 和 c_cpp_properties.json 文件中添加,已添加的就不用重復添加了。
-
添加 C_SOURCES . 在 Makefile 中添加你新引入的 C 文件的路徑。不添加不一定出錯,但添加上好。
-
添加外設的 HAL 庫文件。雖然 CubeMX 生成的工程中包含了完整的 HAL 庫,但默認這些庫文件並沒有全部編譯,比如我們默認生成的只配置了燈的工程自然就不會去編譯串口、ADC等這些不相干的庫文件,因此當我們需要使用串口時就需要手動把它包含進來了。
- 首先確定你的工程中的確有串口相關的 HAL 庫文件,一般在
\Drivers\STM32F1xx_HAL_Driver\Src
目錄下。 - 然后去
stm32f1xx_hal_conf.h
文件中取消掉#define HAL_UART_MODULE_ENABLED
和#define HAL_USART_MODULE_ENABLED
這兩個宏的注釋。 - 最后在 Makrfile 的 C_SOURCES 中添加上串口的 HAL 庫 C 文件。
- 首先確定你的工程中的確有串口相關的 HAL 庫文件,一般在
-
引入聲明了初始化串口函數的 .h 文件,然后在 main() 函數中初始化串口並 printf() 一個數據試試。
-
為了更好的使用 printf() 和 sscanf()、sprintf() 可以在 Makefile 中添加
-u _printf_float
和-u _scanf_float
,這樣就可以實現浮點數的轉換了。 -
使用 printf() 記得以 '\n' 結尾或使用 fflush(stdout) .