本文詳細的介紹瞭如何重定向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;
}
所以,重定向fputc()函數完整的代碼如下:
#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 下載後實驗現象如下:
至此,我們已經學會實現printf()函數的多種方法,使用DAC輸出不同電壓。
上面我介紹了三種重定向printf函數的方法,你用的是哪一種呢?歡迎文末留言哈哈哈~
本文分享自微信公衆號 - 小熊派開源社區(BearPi-Club)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。