玩轉(zhuǎn) STM32 單片機,肯定離不開串口。串口使用一個稱為串行通信協(xié)議的協(xié)議來管理數(shù)據(jù)傳輸,該協(xié)議在數(shù)據(jù)傳輸期間控制數(shù)據(jù)流,包括數(shù)據(jù)位數(shù)、波特率、校驗位和停止位等。由于串口簡單易用,在各種產(chǎn)品交互中都有廣泛應(yīng)用。
但在使用串口通訊的時候,我們并不知道對方會發(fā)送多少個數(shù)據(jù),也不知道數(shù)據(jù)什么時候發(fā)送完,簡單來講就是:如何確保收到一幀完整的數(shù)據(jù)?
串口發(fā)送的數(shù)據(jù)有長有短,如果沒有接收完整,肯定會影響后續(xù)業(yè)務(wù)的處理。為了接收不定長數(shù)據(jù),常見的處理方法有:
1. 固定格式
比如雙方約定,一幀的數(shù)據(jù)以 AA BB 開頭,以 BB AA 結(jié)尾,這樣在從機接收數(shù)據(jù)的時候,一旦收到 AA BB 字符,就知道對方要發(fā)來一個數(shù)據(jù)包了,然后就把后面發(fā)來的數(shù)據(jù)保存起來,直到接收到 BB AA 為止。
這種方法簡單高效,但缺點就是需要每個字符都進行判斷,浪費 CPU 資源,增加功耗。
2. 接收中斷+超時判斷
串口接收到一個數(shù)據(jù)時,就會觸發(fā)接收中斷。但如何判斷數(shù)據(jù)已經(jīng)發(fā)送完了呢?
通常來講,兩幀數(shù)據(jù)之間,會有個時間間隔。因此,我們可以使用一個計時器,如果在一個固定的時間點里沒接收到新的字符,則認為一幀數(shù)據(jù)接收完成了。
3. 空閑中斷
串口在空閑時,也就是說串口在一段時間里沒有接收到新數(shù)據(jù),則會觸發(fā)空閑中斷。細心的同學(xué)應(yīng)該發(fā)現(xiàn)了,空閑中斷實際上跟上面的超時判斷是一樣樣的,只不過空閑中斷是硬件自帶,但超時判斷需要我們自己實現(xiàn)。
所以,一旦接收到空閑中斷,可以認為接收到一幀完整的數(shù)據(jù)。
但是,空閑中斷并不是所有的 MCU 都具備,一般高端一點的 MCU 才有,低端一些的 MCU 并沒有空閑中斷。
1. 源碼下載及前置閱讀
本文首發(fā) 良許嵌入式網(wǎng) :https://www.lxlinux.net/e/ ,歡迎關(guān)注!
本文所涉及的源碼及安裝包如下(由于平臺限制,請點擊以下鏈接閱讀原文下載):
https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-rxne-time-out.html
如果你是個零基礎(chǔ)的小白,連 STM32 都沒見過,我也給你準備了一個保姆級教程,手把手教你搭建好 STM32 開發(fā)環(huán)境,并教你如何下載程序,簡直業(yè)界良心!
https://www.lxlinux.net/e/stm32/stm32-quick-start-for-beginner.html
如果你連代碼都不知道怎么燒錄到 STM32 的,可以參考下文,提供了 5 種代碼燒錄方式:
https://www.lxlinux.net/e/stm32/five-ways-to-flash-program-to-stm32.html
如果你想自己搭一個屬于自己的工程模板,可以參考下面這篇文章:
https://www.lxlinux.net/e/stm32/create-stm32-hal-project-template.html
在本文中,我們詳細來介紹如何使用接收中斷+超時判斷完成不定長數(shù)據(jù)的接收,對于空閑中斷的接收,請查看下文:
https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-idle-dma.html
2. 什么是接收中斷?
前文已經(jīng)提到,當接收到一字節(jié)數(shù)據(jù)時,會觸發(fā)接收中斷,對應(yīng)串口狀態(tài)寄存器第 5 位被置 1 ,如下圖示。
當我們將 DR 寄存器的值讀取之后,該位又被自動清零。
3. 硬件準備
STM32 核心板
本文使用正點原子 M48Z 核心板,小巧好用,某寶 20 元出頭。
USB 轉(zhuǎn) TTL
這種設(shè)備主要作用是用來調(diào)試或下載程序。價格也很便宜,普遍 5~8 元。
ST-Link
ST-Link 是一種用于 STM32 微控制器的調(diào)試和編程工具,它可以通過 SWD 或 JTAG 接口與開發(fā)板進行通信。一般也很便宜,七八元左右。
4. 編程實戰(zhàn)
在本實驗中,我們將串口 1 作為 log 輸出端口,串口 2 作為本次實驗的接收端口。
因此我們需要提前創(chuàng)建 uart2 模塊,包含 uart2.c 及 uart2.h 兩個文件,并加載進工程模板。
4.1 串口初始化
串口的初始化大家應(yīng)該不陌生,主要步驟為:
定義串口句柄 uart2_handle ,并調(diào)用 HAL_UART_Init 進行初始化;
初始化串口底層函數(shù),調(diào)用 HAL_UART_MspInit 函數(shù)。
第一步在 uart2.c 文件里進行:
UART_HandleTypeDef uart2_handle;
void uart2_init(uint32_t baudrate)
{
uart2_handle.Instance = UART2_INTERFACE; /* UART2 */
uart2_handle.Init.BaudRate = baudrate; /* 波特率 */
uart2_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 數(shù)據(jù)位 */
uart2_handle.Init.StopBits = UART_STOPBITS_1; /* 停止位 */
uart2_handle.Init.Parity = UART_PARITY_NONE; /* 校驗位 */
uart2_handle.Init.Mode = UART_MODE_TX_RX; /* 收發(fā)模式 */
uart2_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 無硬件流控 */
uart2_handle.Init.OverSampling = UART_OVERSAMPLING_16; /* 過采樣 */
HAL_UART_Init(&uart2_handle); /* 使能UART2 */
}
第二步在 usart.c 文件里進行,其實也可以在 uart2.c 文件里做,但我懶~
在最下面一行代碼,我們使用 __HAL_UART_ENABLE_IT() 使能接收中斷。
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if (huart->Instance == USART_UX) /* 如果是串口1,進行串口1 MSP初始化 */
{
....
// 節(jié)略串口1相關(guān)代碼
....
}
else if (huart->Instance == UART2_INTERFACE) /* 如果是UART2 */
{
UART2_TX_GPIO_CLK_ENABLE(); /* 使能UART2 TX引腳時鐘 */
UART2_RX_GPIO_CLK_ENABLE(); /* 使能UART2 RX引腳時鐘 */
UART2_CLK_ENABLE(); /* 使能UART2時鐘 */
gpio_init_struct.Pin = UART2_TX_GPIO_PIN; /* UART2 TX引腳 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 復(fù)用推挽輸出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 無上下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(UART2_TX_GPIO_PORT, &gpio_init_struct); /* 初始化UART2 TX引腳 */
gpio_init_struct.Pin = UART2_RX_GPIO_PIN; /* UART2 RX引腳 */
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 輸入 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 無上下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(UART2_RX_GPIO_PORT, &gpio_init_struct); /* 初始化UART2 RX引腳 */
HAL_NVIC_SetPriority(UART2_IRQn, 0, 0); /* 搶占優(yōu)先級0,子優(yōu)先級0 */
HAL_NVIC_EnableIRQ(UART2_IRQn); /* 使能UART2中斷通道 */
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); /* 使能UART2接收中斷 */
}
}
4.2 判斷接收中斷
在串口 2 接收中斷里,我們先使用 __HAL_UART_GET_FLAG() 函數(shù)判斷 RXNE 這一位有沒有被置 1 ,如果被置 1 ,則代表接收到字符,調(diào)用 HAL_UART_Receive() 函數(shù)接收字符,并保存于臨時變量 receive_data 中。
之后,再調(diào)用 HAL_UART_Transmit() 函數(shù)將接收到的字符打印出來。
void UART2_IRQHandler(void)
{
uint8_t receive_data = 0;
if(__HAL_UART_GET_FLAG(&uart2_handle,UART_FLAG_RXNE) != RESET)
{
HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000); //串口2接收1位數(shù)據(jù)
HAL_UART_Transmit(&uart2_handle, &receive_data, 1, 1000); //將接收的數(shù)據(jù)打印出來
}
}
現(xiàn)在我們通過接收中斷就可以實現(xiàn)了自發(fā)自收,編譯后燒進板子,效果如下:
但現(xiàn)在我們只實現(xiàn)了字符的接收,并不知道一幀的數(shù)據(jù)什么時候接收完。
在下面的操作里,我們就通過超時的方法,進一步判斷數(shù)據(jù)是否完成傳輸。
4.3 數(shù)據(jù)接收完成判斷
如何判斷一幀的數(shù)據(jù)接收完成了?
在本文中,我們使用超時的方法進行判斷,這種方法雖然會耗費 CPU 資源,但因為比較簡單,所以使用也很廣泛。在下一篇文章里,我們將使用空閑中斷+DMA 的方法,更高效進行幀數(shù)據(jù)接收完成判斷。
超時判斷的思路如下:
將接收到的字符保存在接收緩沖區(qū)里,并定義一個變量 uart2_cnt 計算總共收到了多少個字符;
假如一幀的數(shù)據(jù)接收完成了,那么 uart2_cnt 變量的值應(yīng)該維持不變。
第一個步驟比較好實現(xiàn),還是在串口 2 接收中斷里,做一些小小的改動:
uint16_t uart2_cnt = 0, uart2_cntPre = 0;
void UART2_IRQHandler(void)
{
uint8_t receive_data = 0;
if(__HAL_UART_GET_FLAG(&uart2_handle, UART_FLAG_RXNE) != RESET){ //獲取接收RXNE標志位是否被置位
if(uart2_cnt >= sizeof(uart2_rx_buf)) //如果接收的字符數(shù)大于接收緩沖區(qū)大小,
uart2_cnt = 0; //則將接收計數(shù)器清零
HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000); //接收一個字符
uart2_rx_buf[uart2_cnt++] = receive_data; //將接收到的字符保存在接收緩沖區(qū)
}
}
關(guān)鍵是第二步,我們?nèi)绾闻袛?uart2_cnt 什么時候維持不變(也就是一幀的數(shù)據(jù)接收完成了)?也很簡單,我們就定時去查看一下這個變量的值,看看是否跟上一次一樣,如果一樣的話就說明數(shù)據(jù)接收完成了。
因此我們需要再借助一個新的變量 uart2_cntPre ,記錄上一次接收到的數(shù)據(jù)的長度(上面的代碼已經(jīng)定義好了)。
uint8_t uart2_wait_receive(void)
{
if(uart2_cnt == 0) //如果接收計數(shù)為0,則說明沒有處于接收數(shù)據(jù)中,所以直接跳出,結(jié)束函數(shù)
return UART_ERROR;
if(uart2_cnt == uart2_cntPre) { //如果上一次的值和這次相同,則說明接收完畢
uart2_cnt = 0; //清0接收計數(shù)
return UART_EOK; //返回接收完成標志
}
uart2_cntPre = uart2_cnt; //置為相同
return UART_ERROR; //返回接收未完成標志
}
然后我們在 main 函數(shù)里的 while 死循環(huán)定期(例如10ms)調(diào)用 uart2_wait_receive 函數(shù),如果返回值為 UART_EOK 則代表幀數(shù)據(jù)接收完成,我們就可以將數(shù)據(jù)打印出來。
while(1)
{
if(uart2_wait_receive() == UART_EOK) { //判斷串口2是否數(shù)據(jù)接收完成
printf('recv: %srn', uart2_rx_buf); //打印收到的數(shù)據(jù)
uart2_rx_clear(); //清空接收緩沖區(qū)
}
delay_ms(10); //每隔10毫秒判斷一次
}
當然,接收到的數(shù)據(jù)使用完成之后,我們就應(yīng)該清空接收緩沖區(qū),并將計數(shù)器置 0 ,方便下一次接收,所以我們調(diào)用了 uart2_rx_clear() 函數(shù),其代碼實現(xiàn)為:
上一篇:《嵌入式-STM32開發(fā)指南》第二部分 基礎(chǔ)篇 - 第3章 按鍵(HAL庫)
下一篇:《嵌入式-STM32開發(fā)指南》第二部分 基礎(chǔ)篇 - 第7章DMA(HAL庫)
推薦閱讀最新更新時間:2025-06-16 02:50



設(shè)計資源 培訓(xùn) 開發(fā)板 精華推薦
- 意法半導(dǎo)體推出用于匹配遠距離無線微控制器STM32WL33的集成的匹配濾波芯片
- ESP32開發(fā)板連接TFT顯示屏ST7789跳坑記
- 如何讓ESP32支持analogWrite函數(shù)
- LGVL配合FreeType為可變字體設(shè)置字重-ESP32篇
- 使用樹莓派進行 ESP32 Jtag 調(diào)試
- ESP32怎么在SPIFFS里面存儲html,css,js文件,以及網(wǎng)頁和arduino的通訊
- ESP32 freeRTOS使用測試
- API調(diào)用小記(Touchdesigner和ESP32)
- 關(guān)于ESP32/8266使用async-mqtt-client庫的一些基本介紹
- LT3088EDD 線性穩(wěn)壓器用于添加軟啟動的典型應(yīng)用
- NXP 12V Qi低功耗A6發(fā)射器解決方案
- 基于STC15F2K60S2的光立方設(shè)計
- 精密線性壓控電流信號發(fā)送電路
- LTC6261HDC 235 uA 電源電流運算放大器的典型應(yīng)用
- STNRG388A評估板
- 使用 NXP Semiconductors 的 MC9S08JM60CLD 的參考設(shè)計
- LT1172CT、5V/1.25A 正降壓轉(zhuǎn)換器的典型應(yīng)用
- 【物聯(lián)網(wǎng)】鴻蒙物聯(lián)網(wǎng)智能WIFI開關(guān)+4219114A
- 具有 400kHz 電荷泵開關(guān)的 LT8494IFE 寬輸入和輸出范圍 SEPIC 轉(zhuǎn)換器的典型應(yīng)用電路
- AMS2021線上研討會——新興汽車市場發(fā)展現(xiàn)狀及未來趨勢
- 合創(chuàng)資本:助半導(dǎo)體科企打贏國產(chǎn)替代攻堅戰(zhàn)
- B001-Atmega16-中斷(GCC-AVR)-(ques=3)
- 手持式頻譜儀品牌及各品牌性能參數(shù)對比
- 康普RUCKUS觀點:共享頻譜和融合網(wǎng)絡(luò)打造2023年企業(yè)連接新亮點
- 10道ARM嵌入式相關(guān)的經(jīng)典面試題
- 串口0作為調(diào)試端口,怎么改變?yōu)榻邮諗?shù)據(jù)接口
- 如何減少示波器測量的死區(qū)時間
- Cirrus Logic為PC市場帶來沉浸式音頻體驗
- 座椅、天窗、電動尾門應(yīng)用 國產(chǎn)車規(guī)級高壓霍爾效應(yīng)傳感器推薦
- 突發(fā)!又一車企車機“崩了”
- 寧德時代為陳立泉院士頒發(fā)“卓越貢獻獎”
- 一文速覽吉利雷神 AI 電混 2.0 發(fā)布會重點
- 2025年1-4月ADAS供應(yīng)商裝機量排行榜:頭部集中與國產(chǎn)突圍并存
- 國內(nèi)飛行汽車無線通信測試成功,通信安全新突破
- SPAD席卷車載激光雷達市場
- 大聯(lián)大品佳集團推出基于Microchip和ams OSRAM產(chǎn)品的10Base-T1S萬級像素大燈方案
- 哈曼推出采用三星Neo QLED技術(shù)的全新顯示屏
- Syntiant推出超低功耗汽車AI創(chuàng)新技術(shù) 提升車輛安全性和用戶體驗