目錄
UART通信協議的介紹
實現串口數據發送
CubeMX配置
printf重定向代碼編寫
實現串口數據接收
輪詢方式實現串口數據接收
接收單個字符
接收不定長字符串(接收的字符串以\n結尾)
中斷方式實現串口數據接收
CubeMX配置
UART中斷方式接收數據函數是一次性的
HAL_UART_Receive_IT() 調用位置
接收單個字符
接收不定長字符串
UART通信協議的介紹
UART (Universal Asynchronous Receiver/Transmitter) 是一種串行異步全雙工通信協議。
UART通信的核心思想是異步和串行,全雙工。
-
異步 (Asynchronous):通信雙方不需要共享一個時鐘信號來同步數據傳輸。取而代之的是,通過起始位 (Start Bit) 標記數據幀的開始,通過停止位 (Stop Bit) 標記數據幀的結束,以及預先約定好的波特率 (Baud Rate) 來協調數據的采樣時間。
? -
串行 (Serial):數據一位一位地按順序在線路上傳輸,而不是并行多位同時傳輸。這使得只需要兩根線。
異步詳細介紹
發送方: 按照預設的波特率,精確地控制每個數據位、起始位和停止位的持續時間,并將其串行發送出去。
?接收方: 同樣按照預設的波特率,在檢測到起始位后,精確地計算每個數據位的采樣時間點,從而正確讀取數據。
進行異步通信,通信的每一方都必須擁有自己的內部時鐘,如果沒有各自的內部時鐘,發送方就無法知道何時發送下一個比特,接收方也無法知道何時去讀取數據線上的電平。
通信雙方的波特率必須相同,否則將導致數據錯亂。
使用串口助手進行串口通信的時候,是滿足這里的要求的,電腦上面有時鐘,同時開發板內部的UART也有時鐘。
串行,全雙工詳細介紹
標準的UART通信通常只需要兩根信號線:
TX (Transmit):發送數據線,用于發送數據。
RX (Receive):接收數據線,用于接收數據。
連接方式是交叉連接:發送方的TX連接到接收方的RX,發送方的RX連接到接收方的TX。
由于發送和接收使用不同的物理線路,它們互不干擾,所以發送方可以在向接收方發送數據的同時,接收方也可以向發送方發送數據。UART協議是全雙工通信協議
實現串口數據發送
CubeMX配置
配置好時鐘源之后,選擇USART1之后選擇模式為異步模式就可以了,剩下的配置不需要動。
選完之后我們會發現這里多了兩個選中的引腳,?TX(發送),RX(接收)。
如果選擇同步通信方式的話還會有一根時鐘線用于同步操作。
在USART協議中我們其實是可以選擇使用同步方式(synchronous)或者異步方式(Asynchronous)進行數據的發送和接收的 ,不過一般異步方式用的多,同步方式通信一般會選擇其他類型協議如 SPI、I2C。
printf重定向代碼編寫
串口調試助手在沒有顯示屏的嵌入式系統中有著很好的應用,可以利用函數重定向功能,調用printf函數,將開發板中獲取到的數據通過串口刷出到PC,為程序調試和串口通信提供了很大的便利。
我們不能直接調用printf將數據輸出到串口,要想實現printf重定向功能,還需要進行重寫一下fputc函數。
為什么 printf 默認不能輸出到串口?
在 C 標準庫中,
printf()
的本質是將格式化后的字符串輸出到一個默認的標準輸出設備,這個設備在 PC 上通常是終端(屏幕),但在 MCU(如 STM32)里:
沒有默認的標準輸出設備(沒有屏幕、沒有操作系統);
所以
printf()
其實調用的是底層函數如fputc()
、_write()
,但這些在裸機環境下是 空實現 或 報錯實現。
fputc的函數原型照抄就行,這里的第二個參數用不到,不需要處理。
fputc函數的作用是輸出單個字符,所以這里使用HAL_UART_Transmit實現fputc輸出字符的時候,參數選擇字節數為1就行。這樣實現出來的輸出字符串到串口助手的效果非常好,還很簡便,不需要我們判斷發送到串口的字符串長度邏輯(printf幫我們做了,我們只需要實現單個字符發送就可以!)
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);return ch;
}
?HAL_UART_Transmit函數介紹
- UART_HandleTypeDef *huart (第一個參數):
這是一個指向 UART_HandleTypeDef 結構體的指針。這個結構體包含了特定 UART 外設的所有配置信息和狀態。
?- uint8_t *pData (第二個參數):
這是一個指向要發送數據的緩沖區的指針。uint8_t 表示數據是以字節(8位)的形式傳輸。函數會從這個地址開始讀取數據并發送出去。
?- uint16_t Size (第三個參數):
這是一個 uint16_t 類型的變量,表示要發送的數據的字節數(長度)。
?- uint32_t Timeout (第四個參數):
- 這是一個 uint32_t 類型的變量,表示數據發送操作的超時時間,單位是毫秒(ms)。
- 如果在指定的時間內數據沒有發送完成,函數將返回 HAL_TIMEOUT。
- 使用 HAL_MAX_DELAY 表示無限等待,直到數據發送完成(或發生錯誤),這在不希望有超時限制的情況下很常用。
由于使用了printf這個函數,所以我們需要在main.c文件中包含對應的頭文件:
#include "stdio.h"
/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */uint16_t cnt=0;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */printf("this is a message from stm32 cnt : %d\n", cnt++);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
Keil中對文件勾選配置:Use MicroLIB
效果展示:?
實現串口數據接收
輪詢方式實現串口數據接收
接收單個字符
uint8_t receive_data;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */HAL_UART_Receive(&huart1, &receive_data, 1, HAL_MAX_DELAY);printf("receive data: %c\r\n", receive_data);/* USER CODE BEGIN 3 */}
接收不定長字符串(接收的字符串以\n結尾)
uint8_t receive_data;uint8_t receive_buf[256] = {0};uint8_t index = 0;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */HAL_UART_Receive(&huart1, &receive_data, 1, HAL_MAX_DELAY);if(receive_data != '\n'){receive_buf[index++] = receive_data;}else{receive_buf[index++] = '\0';printf("receive data: %s\r\n", receive_buf);index = 0;memset(receive_buf, 0, sizeof(receive_buf));}/* USER CODE BEGIN 3 */}
代碼介紹?
這段代碼是典型的使用 HAL 庫的輪詢(阻塞)模式接收串口數據,并以換行符 \n 作為結束標志 的實現。
這段代碼的主要功能是:
- 在無限循環 while(1) 中,阻塞式地等待 接收 UART1 的一個字節數據。
? - 如果接收到的字節不是換行符 \n,則將其存儲到 receive_buf 緩沖區中。
? - 如果接收到換行符 \n,則認為一幀數據接收完畢:
- 在緩沖區末尾添加字符串結束符 \0。
- 通過 printf 打印接收到的字符串。
- 重置 index 為 0,準備接收下一幀數據。
- 清空 receive_buf 緩沖區。
代碼優缺點:
優點
簡單易懂: 對于初學者來說,這種輪詢加阻塞的模式是最容易理解和實現的。邏輯直接,容易跟蹤。
便于調試: 由于是阻塞式的,每次接收一個字節,單步調試時可以清晰地看到數據流。
處理明確的結束符: 使用
\n
作為幀結束符是文本協議中常見且有效的方式,使得數據包的邊界清晰。缺點
CPU 效率極低(最主要的問題):
HAL_UART_Receive(&huart1, &receive_data, 1, HAL_MAX_DELAY); 這行代碼會無限期阻塞,直到接收到一個字節。這意味著在沒有數據到達時,CPU 會一直停在這里等待,無法執行其他任何任務。
這對于任何需要做其他事情(比如控制LED、讀取傳感器、處理按鍵等)的實時嵌入式系統來說都是不可接受的。CPU 的大部分時間都會被浪費在等待串口數據上。
注意看,我們在使用串口助手向開發板發送數據的時候,需要加上一個換行符,表示發送的這串字符串結束了。開發板內部的串口處理程序,順序收到/n之后,根據判斷邏輯就會輸出緩沖區積攢的字符串。
中斷方式實現串口數據接收
CubeMX配置
相比于上面采用非中斷模式來實現串口數據接收,采用中斷方式的話,需要將Uart1對應的中斷打開。
UART中斷方式接收數據函數是一次性的
HAL_StatusTypeDef? HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函數作用:啟動 UART1 的中斷接收,期望接收 uint16_t Size 個字節
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 推薦每次只接收一個字節
雖然調用一次函數可以選擇接收多個字節數據,但是我們通常選擇調用一次UART中斷接收數據函數只接收一個字節,會在中斷回調里處理并再次啟動接收函數。
為什么說UART 中斷接收函數為何說是“一次性的”?
- 這句話的意思是,當你調用
HAL_UART_Receive_IT(&huart, pData, Size);
這個函數后,它只會啟動一次中斷接收過程,并等待接收指定數量(Size
)的字節。
?- 一旦 UART 接收到這
Size
個字節的數據,這個函數所啟動的當前接收任務就完成了。UART 接收中斷會相應地被觸發,執行你的回調函數HAL_UART_RxCpltCallback()。
?- 但是,此時 UART 并不會自動開始接收下一組數據。 如果你想繼續接收數據,你需要再次調用
HAL_UART_Receive_IT()
來“重新武裝”UART 硬件,讓它準備好接收下一批數據,通常我們會選擇在回調函數HAL_UART_RxCpltCallback中再次啟動接收函數。
HAL_UART_Receive_IT() 調用位置
HAL_UART_Receive_IT() 函數推薦在以下幾個地方調用:
1. main() 函數的初始化部分
這是最常見和推薦的調用位置。 在你的 main.c 文件中,通常在所有硬件初始化完成(例如 MX_GPIO_Init()、MX_USARTx_UART_Init() 等)之后,while(1) 無限循環之前,調用一次 HAL_UART_Receive_IT() 來啟動串口的首次接收。
int main(void)
{// ... 系統初始化 ...HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();// ... 其他外設初始化 .../* USER CODE BEGIN 2 */// 啟動 UART1 的中斷接收,期望接收 SOME_BUFFER_SIZE 個字節// 或者通常只接收一個字節,在中斷回調里處理并再次啟動HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 推薦每次只接收一個字節/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */
2.?UART 接收完成回調函數 HAL_UART_RxCpltCallback() 中
// 這個函數是弱聲明,你需要重寫它
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1){// 假設這里處理了接收到的數據 rx_buffer// Process_Received_Data(rx_buffer);// 重新啟動下一次中斷接收HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 再次啟動接收一個字節}
}
接收單個字符
讓我們用一個小栗子來進行實現一下中斷接收單個字符吧。
代碼功能:
這里我們想要實現的是通過串口助手,每次發送一個字符1/2/3/4來控制LED1/2/3/4,比如發送一個1,LED1電平就翻轉,再次輸入1LED1電平就再次翻轉。LED2/3/4同理。
這里為了展現出中斷方式接收數據不阻塞主程序執行,我們在main函數中還持續用向串口發送字符串"hello world"
代碼實現如下:
在main.c中定義成一個全局變量
/* USER CODE BEGIN PV */uint8_t receive_data;//存放UART1接收到的數據
/* USER CODE END PV */函數聲明部分
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
int fputc(int ch, FILE *f);
/* USER CODE END PFP */main函數里面
/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */HAL_UART_Receive_IT(&huart1, &receive_data, 1);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */printf("hello world\r\n");HAL_Delay(1000);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}main函數中定義的函數的具體實現
/* USER CODE BEGIN 4 */
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);return ch;
}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){if(huart->Instance == USART1){if(receive_data == '1'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_10);}else if(receive_data == '2'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_2);}else if(receive_data == '3'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_1);}else if(receive_data == '4'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);}}HAL_UART_Receive_IT(&huart1, &receive_data, 1);
}
/* USER CODE END 4 */
代碼編寫時候需要注意的就是:
- HAL_UART_Receive_IT()需要在main函數初始化的時候調用,
? - 還需要在?HAL_UART_RxCpltCallback中調用
接收不定長字符串
代碼實現思路:
- 每次只接收一個字節: 在中斷模式下,將 HAL_UART_Receive_IT() 的 Size 參數設置為 1。這樣每接收到一個字節,就會觸發一次接收完成中斷。
?- 使用緩沖區 (Buffer): 在中斷服務程序中,將接收到的每一個字節存入緩沖區。
?- 在主循環中判斷數據完整性: 主循環會不斷地檢查緩沖區中是否有完整的字符串幀(通常通過查找特定的結束符,如 \n )。
?- 重新啟動接收: 在每次接收完成中斷回調函數中,都需要再次調用 HAL_UART_Receive_IT() 來“重新武裝”串口,準備接收下一個字節。
CubeMX配置
上面已經說過了,主要需要注意的是記得在NVIC 設置中,勾選開啟對應 UART 全局中斷的 Enabled。
代碼實現
額外包含頭文件
#include "stdio.h"
#include "string.h"main.c中定義的全局變量
/*
這里定義成全局變量的原因:
回調函數的實現以及main函數中都要用到這些變量
*/
/* USER CODE BEGIN PV */uint8_t receive_data;uint8_t receive_buf[256] = {0};uint8_t receive_buf_index = 0;
/* USER CODE END PV */函數聲明
/* USER CODE BEGIN PFP */
int fputc(int ch, FILE* f);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
/* USER CODE END PFP */main函數中代碼
HAL_UART_Receive_IT(&huart1, &receive_data, 1);/* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */printf("hello world\r\n");HAL_Delay(1000);/* USER CODE BEGIN 3 */}代碼實現部分
int fputc(int ch, FILE* f){HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY); return ch;
}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){if(huart->Instance == USART1){if(receive_data != '\n'){receive_buf[receive_buf_index++] = receive_data;}else{receive_buf[receive_buf_index++] = '\0';printf("receive data: %s\r\n", receive_buf);receive_buf_index = 0;memset(receive_buf, 0, sizeof(receive_buf));}HAL_UART_Receive_IT(&huart1, &receive_data, 1);}
}