轉自:https://mculover666.blog.csdn.net/article/details/99842909
本文詳細的介紹了如何重定向printf輸出到串口輸出的多種方法,包括調用MDK微庫(MicroLib)的方法,調用標准庫的方法,以及適用於 GNUC
系列編譯器的方法。
1.printf與fputc
對於 printf 函數相信大家都不陌生,第一個C語言程序就是使用 printf 函數在屏幕上的控制台打印出Hello World
,之后使用 printf 函數輸出各種類型的數據,使用格式控制輸出各種長度的字符,甚至輸出各種各樣的圖案。
除此之外,在程序出錯的時候,懶得調試,直接簡單粗暴的加個 printf 找bug,有時候也不失為一種有效的方法。
對於已經習慣的 printf 函數,你了解多少呢?
printf 定義在 <stdio.h>
頭文件中,如下:
int printf(const char *format, ...);
printf 函數根據 format
字符串給出的格式打印輸出到 stdout
(標准輸出)中,當然,printf 函數是不會一個字符一個字符去輸出,它會調用更底層的 I/O 函數:fputc
去逐個字符打印。
fputc 也定義於頭文件 <stdio.h>
中,如下:
int fputc(int ch, FILE *stream);
fputc 函數寫入字符 ch 到給定輸出流 stream,printf函數在調用該函數時,會向stream參數傳入stdout
從而打印數據到標准輸出。
那么,要實現printf打印到串口就變得非常簡單了,只需要重新定義fputc函數,在fputc的函數中將數據通過串口發送,稱之為:fputc重定向或者printf重定向。
2.在MDK中使用MicroLib重定向printf
勾選Use MicroLib
MicroLib是對標准C庫進行了高度優化之后的庫,供MDK默認使用,相比之下,MicroLIB的代碼更少,資源占用更少:
重定義fputc到串口
重新實現fputc函數,編寫代碼將這個字符通過串口發送,因為發送每個字符時都會調用該函數,所以為了效率,不再調用庫函數 HAL_UART_Transmit
發送,而是直接操作寄存器發送。
- 檢測串口當前狀態
STM32L431的USART串口外設有一個 ISR
寄存器,全名 Interrupt and status register
, 用來指示當前串口的狀態,如圖:
其中 BIT6 TC
用來指示當前串口是否發送完成,如圖:
可以通過判斷該位來判斷串口當前是否處於發送狀態,代碼如下:
while((USART1->ISR & 0X40) == 0);
串口發送字符ch,同樣,為了提高發送效率,直接使用寄存器來操作:
USART1->TDR = (uint8_t) ch;
最后實現fputc函數就變的非常簡單了,這里我放在usart.c
文件的末尾:
/* USER CODE BEGIN 1 */ #if 1 #include <stdio.h> int fputc(int ch, FILE *stream) { /* 堵塞判斷串口是否發送完成 */ while((USART1->ISR & 0X40) == 0); /* 串口發送完成,將該字符發送 */ USART1->TDR = (uint8_t) ch; return ch; } #endif /* USER CODE END 1 */
測試printf
在main函數中測試一下printf函數是否可以正常使用:
/* USER CODE BEGIN 2 */ printf("Hello, i am %s\n", "mculover666"); printf("Test int: i = %d", 100); printf("Test float: i = %f", 1.234); printf("Test hex: i = 0x%2x",100); /* USER CODE END 2 */
結果如下:
3.在MDK中使用標准庫重定向printf
printf 函數使用了半主機模式,所以直接使用標准庫會導致程序無法運行,因此必須提前告知編譯器不使用半主機模式:
- 不使用半主機模式
/* 告知連接器不從C庫鏈接使用半主機的函數 */ #pragma import(__use_no_semihosting) /* 定義 _sys_exit() 以避免使用半主機模式 */ void _sys_exit(int x) { x = x; }
所以,重定向fputs()函數完整的代碼如下:
#if 1 #include <stdio.h> /* 告知連接器不從C庫鏈接使用半主機的函數 */ #pragma import(__use_no_semihosting) /* 定義 _sys_exit() 以避免使用半主機模式 */ void _sys_exit(int x) { x = x; } /* 標准庫需要的支持類型 */ struct __FILE { int handle; }; FILE __stdout; /* */ int fputc(int ch, FILE *stream) { /* 堵塞判斷串口是否發送完成 */ while((USART1->ISR & 0X40) == 0); /* 串口發送完成,將該字符發送 */ USART1->TDR = (uint8_t) ch; return ch; } #endif
測試printf
測試printf函數的代碼不變,在MDK設置中取消勾選USE MICROLIB
,然后重新編譯,下載代碼后試驗現象如下:
4.在GCC中使用標准庫重定向printf
不同的編譯器對於C庫的底層實現機制是不同的,所以上面兩種在MDK中的實現方法,在使用Gcc編譯器的時候是不可行的。
在Gcc中重定向printf函數時注意兩個關鍵點:
- 與重定義fputs()函數一樣,在使用Gcc編譯器的時候,需要重新定義
_write
函數; - Gcc中沒有MicroLib,只能使用標准庫;
所以重定向printf函數的代碼如下:
/* USER CODE BEGIN 1 */ #if 1 #include <stdio.h> int _write(int fd, char *ptr, int len) { HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, 0xFFFF); return len; } #endif /* USER CODE END 1 */
使用STM32CubeMX生成makefile,然后使用arm-none-eabi-gcc編譯沒有問題,再使用STM32 ST-LINK utility 下載后實驗現象如下: