STM32串口接收不定長(zhǎng)數(shù)據(jù)(接收中斷+超時(shí)判斷)

玩轉(zhuǎn) STM32 單片機(jī),肯定離不開(kāi)串口。串口使用一個(gè)稱為串行通信協(xié)議的協(xié)議來(lái)管理數(shù)據(jù)傳輸,該協(xié)議在數(shù)據(jù)傳輸期間控制數(shù)據(jù)流,包括數(shù)據(jù)位數(shù)、波特率、校驗(yàn)位和停止位等。由于串口簡(jiǎn)單易用,在各種產(chǎn)品交互中都有廣泛應(yīng)用。

但在使用串口通訊的時(shí)候,我們并不知道對(duì)方會(huì)發(fā)送多少個(gè)數(shù)據(jù),也不知道數(shù)據(jù)什么時(shí)候發(fā)送完,簡(jiǎn)單來(lái)講就是:如何確保收到一幀完整的數(shù)據(jù)?

串口發(fā)送的數(shù)據(jù)有長(zhǎng)有短,如果沒(méi)有接收完整,肯定會(huì)影響后續(xù)業(yè)務(wù)的處理。為了接收不定長(zhǎng)數(shù)據(jù),常見(jiàn)的處理方法有:

1. 固定格式

比如雙方約定,一幀的數(shù)據(jù)以 AA BB 開(kāi)頭,以 BB AA 結(jié)尾,這樣在從機(jī)接收數(shù)據(jù)的時(shí)候,一旦收到 AA BB 字符,就知道對(duì)方要發(fā)來(lái)一個(gè)數(shù)據(jù)包了,然后就把后面發(fā)來(lái)的數(shù)據(jù)保存起來(lái),直到接收到 BB AA 為止。

這種方法簡(jiǎn)單高效,但缺點(diǎn)就是需要每個(gè)字符都進(jìn)行判斷,浪費(fèi) CPU 資源,增加功耗。

2. 接收中斷+超時(shí)判斷

串口接收到一個(gè)數(shù)據(jù)時(shí),就會(huì)觸發(fā)接收中斷。但如何判斷數(shù)據(jù)已經(jīng)發(fā)送完了呢?

通常來(lái)講,兩幀數(shù)據(jù)之間,會(huì)有個(gè)時(shí)間間隔。因此,我們可以使用一個(gè)計(jì)時(shí)器,如果在一個(gè)固定的時(shí)間點(diǎn)里沒(méi)接收到新的字符,則認(rèn)為一幀數(shù)據(jù)接收完成了。

3. 空閑中斷

串口在空閑時(shí),也就是說(shuō)串口在一段時(shí)間里沒(méi)有接收到新數(shù)據(jù),則會(huì)觸發(fā)空閑中斷。細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn)了,空閑中斷實(shí)際上跟上面的超時(shí)判斷是一樣樣的,只不過(guò)空閑中斷是硬件自帶,但超時(shí)判斷需要我們自己實(shí)現(xiàn)。

所以,一旦接收到空閑中斷,可以認(rèn)為接收到一幀完整的數(shù)據(jù)。

但是,空閑中斷并不是所有的 MCU 都具備,一般高端一點(diǎn)的 MCU 才有,低端一些的 MCU 并沒(méi)有空閑中斷。

1. 源碼下載及前置閱讀

本文首發(fā) 良許嵌入式網(wǎng)https://www.lxlinux.net/e/ ,歡迎關(guān)注!

本文所涉及的源碼及安裝包如下(由于平臺(tái)限制,請(qǐng)點(diǎn)擊以下鏈接閱讀原文下載):

https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-rxne-time-out.html

如果你是個(gè)零基礎(chǔ)的小白,連 STM32 都沒(méi)見(jiàn)過(guò),我也給你準(zhǔn)備了一個(gè)保姆級(jí)教程,手把手教你搭建好 STM32 開(kāi)發(fā)環(huán)境,并教你如何下載程序,簡(jiǎ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

如果你想自己搭一個(gè)屬于自己的工程模板,可以參考下面這篇文章:

https://www.lxlinux.net/e/stm32/create-stm32-hal-project-template.html

在本文中,我們?cè)敿?xì)來(lái)介紹如何使用接收中斷+超時(shí)判斷完成不定長(zhǎng)數(shù)據(jù)的接收,對(duì)于空閑中斷的接收,請(qǐng)查看下文

https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-idle-dma.html

2. 什么是接收中斷?

前文已經(jīng)提到,當(dāng)接收到一字節(jié)數(shù)據(jù)時(shí),會(huì)觸發(fā)接收中斷,對(duì)應(yīng)串口狀態(tài)寄存器第 5 位被置 1 ,如下圖示。

當(dāng)我們將 DR 寄存器的值讀取之后,該位又被自動(dòng)清零。

3. 硬件準(zhǔn)備

  • STM32 核心板

本文使用正點(diǎn)原子 M48Z 核心板,小巧好用,某寶 20 元出頭。

  • USB 轉(zhuǎn) TTL

這種設(shè)備主要作用是用來(lái)調(diào)試或下載程序。價(jià)格也很便宜,普遍 5~8 元。

  • ST-Link

ST-Link 是一種用于 STM32 微控制器的調(diào)試和編程工具,它可以通過(guò) SWD 或 JTAG 接口與開(kāi)發(fā)板進(jìn)行通信。一般也很便宜,七八元左右。

4. 編程實(shí)戰(zhàn)

在本實(shí)驗(yàn)中,我們將串口 1 作為 log 輸出端口,串口 2 作為本次實(shí)驗(yàn)的接收端口。

因此我們需要提前創(chuàng)建 uart2 模塊,包含 uart2.c 及 uart2.h 兩個(gè)文件,并加載進(jìn)工程模板。

4.1 串口初始化

串口的初始化大家應(yīng)該不陌生,主要步驟為:

  1. 定義串口句柄 uart2_handle ,并調(diào)用 HAL_UART_Init 進(jìn)行初始化;
  2. 初始化串口底層函數(shù),調(diào)用 HAL_UART_MspInit 函數(shù)。

第一步在 uart2.c 文件里進(jìn)行:

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;             /* 校驗(yàn)位 */
    uart2_handle.Init.Mode         = UART_MODE_TX_RX;              /* 收發(fā)模式 */
    uart2_handle.Init.HwFlowCtl    = UART_HWCONTROL_NONE;          /* 無(wú)硬件流控 */
    uart2_handle.Init.OverSampling = UART_OVERSAMPLING_16;         /* 過(guò)采樣 */
    HAL_UART_Init(&uart2_handle);                                  /* 使能UART2 */
}

第二步在 usart.c 文件里進(jìn)行,其實(shí)也可以在 uart2.c 文件里做,但我懶~

在最下面一行代碼,我們使用 __HAL_UART_ENABLE_IT() 使能接收中斷。

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;

    if (huart->Instance == USART_UX)                            /* 如果是串口1,進(jìn)行串口1 MSP初始化 */
    {
        ....
        // 節(jié)略串口1相關(guān)代碼
        ....
    }
    else if (huart->Instance == UART2_INTERFACE)                /* 如果是UART2 */
    {
        UART2_TX_GPIO_CLK_ENABLE();                             /* 使能UART2 TX引腳時(shí)鐘 */
        UART2_RX_GPIO_CLK_ENABLE();                             /* 使能UART2 RX引腳時(shí)鐘 */
        UART2_CLK_ENABLE();                                     /* 使能UART2時(shí)鐘 */

        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;                  /* 無(wú)上下拉 */
        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;                  /* 無(wú)上下拉 */
        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)先級(jí)0,子優(yōu)先級(jí)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 這一位有沒(méi)有被置 1 ,如果被置 1 ,則代表接收到字符,調(diào)用 HAL_UART_Receive() 函數(shù)接收字符,并保存于臨時(shí)變量 receive_data 中。

之后,再調(diào)用 HAL_UART_Transmit() 函數(shù)將接收到的字符打印出來(lái)。

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ù)打印出來(lái)
  }
}

現(xiàn)在我們通過(guò)接收中斷就可以實(shí)現(xiàn)了自發(fā)自收,編譯后燒進(jìn)板子,效果如下:

但現(xiàn)在我們只實(shí)現(xiàn)了字符的接收,并不知道一幀的數(shù)據(jù)什么時(shí)候接收完。

在下面的操作里,我們就通過(guò)超時(shí)的方法,進(jìn)一步判斷數(shù)據(jù)是否完成傳輸。

4.3 數(shù)據(jù)接收完成判斷

如何判斷一幀的數(shù)據(jù)接收完成了?

在本文中,我們使用超時(shí)的方法進(jìn)行判斷,這種方法雖然會(huì)耗費(fèi) CPU 資源,但因?yàn)楸容^簡(jiǎn)單,所以使用也很廣泛。在下一篇文章里,我們將使用空閑中斷+DMA 的方法,更高效進(jìn)行幀數(shù)據(jù)接收完成判斷。

超時(shí)判斷的思路如下:

  1. 將接收到的字符保存在接收緩沖區(qū)里,并定義一個(gè)變量 uart2_cnt 計(jì)算總共收到了多少個(gè)字符;
  2. 假如一幀的數(shù)據(jù)接收完成了,那么 uart2_cnt 變量的值應(yīng)該維持不變。

第一個(gè)步驟比較好實(shí)現(xiàn),還是在串口 2 接收中斷里,做一些小小的改動(dòng):

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標(biāo)志位是否被置位
        if(uart2_cnt >= sizeof(uart2_rx_buf))                           //如果接收的字符數(shù)大于接收緩沖區(qū)大小,
            uart2_cnt = 0;                                              //則將接收計(jì)數(shù)器清零
        HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000);        //接收一個(gè)字符
        uart2_rx_buf[uart2_cnt++] = receive_data;                       //將接收到的字符保存在接收緩沖區(qū)
    }
}

關(guān)鍵是第二步,我們?nèi)绾闻袛?uart2_cnt 什么時(shí)候維持不變(也就是一幀的數(shù)據(jù)接收完成了)?也很簡(jiǎn)單,我們就定時(shí)去查看一下這個(gè)變量的值,看看是否跟上一次一樣,如果一樣的話就說(shuō)明數(shù)據(jù)接收完成了。

因此我們需要再借助一個(gè)新的變量 uart2_cntPre ,記錄上一次接收到的數(shù)據(jù)的長(zhǎng)度(上面的代碼已經(jīng)定義好了)。

uint8_t uart2_wait_receive(void)
{
    if(uart2_cnt == 0)                                      //如果接收計(jì)數(shù)為0,則說(shuō)明沒(méi)有處于接收數(shù)據(jù)中,所以直接跳出,結(jié)束函數(shù)
        return UART_ERROR;

    if(uart2_cnt == uart2_cntPre) {                         //如果上一次的值和這次相同,則說(shuō)明接收完畢
        uart2_cnt = 0;                                      //清0接收計(jì)數(shù)
        return UART_EOK;                                    //返回接收完成標(biāo)志
    }

    uart2_cntPre = uart2_cnt;                               //置為相同
    return UART_ERROR;                                      //返回接收未完成標(biāo)志
}

然后我們?cè)?main 函數(shù)里的 while 死循環(huán)定期(例如10ms)調(diào)用 uart2_wait_receive 函數(shù),如果返回值為 UART_EOK 則代表幀數(shù)據(jù)接收完成,我們就可以將數(shù)據(jù)打印出來(lái)。

while(1)
{
    if(uart2_wait_receive() == UART_EOK) {      //判斷串口2是否數(shù)據(jù)接收完成
        printf("recv: %s\r\n", uart2_rx_buf);   //打印收到的數(shù)據(jù)
        uart2_rx_clear();                       //清空接收緩沖區(qū)
    }

    delay_ms(10);                               //每隔10毫秒判斷一次
}

當(dāng)然,接收到的數(shù)據(jù)使用完成之后,我們就應(yīng)該清空接收緩沖區(qū),并將計(jì)數(shù)器置 0 ,方便下一次接收,所以我們調(diào)用了 uart2_rx_clear() 函數(shù),其代碼實(shí)現(xiàn)為:

void uart2_rx_clear(void)
{
    memset(uart2_rx_buf, 0, sizeof(uart2_rx_buf));          //清空接收緩沖區(qū)
    uart2_cnt = 0;                                          //接收計(jì)數(shù)器清零
}

uart2.h 文件內(nèi)容如下:

#include <stdint.h>
#include "usart.h"

/* 引腳定義 */
#define UART2_TX_GPIO_PORT           GPIOA
#define UART2_TX_GPIO_PIN            GPIO_PIN_2
#define UART2_TX_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

#define UART2_RX_GPIO_PORT           GPIOA
#define UART2_RX_GPIO_PIN            GPIO_PIN_3
#define UART2_RX_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

#define UART2_INTERFACE              USART2
#define UART2_IRQn                   USART2_IRQn
#define UART2_IRQHandler             USART2_IRQHandler
#define UART2_CLK_ENABLE()           do{ __HAL_RCC_USART2_CLK_ENABLE(); }while(0)

/* 錯(cuò)誤代碼 */
#define UART_EOK                     0   /* 沒(méi)有錯(cuò)誤 */
#define UART_ERROR                   1   /* 通用錯(cuò)誤 */
#define UART_ETIMEOUT                2   /* 超時(shí)錯(cuò)誤 */
#define UART_EINVAL                  3   /* 參數(shù)錯(cuò)誤 */

/* UART收發(fā)緩沖大小 */
#define UART2_RX_BUF_SIZE            128
#define UART2_TX_BUF_SIZE            64

void uart2_init(uint32_t baudrate);
uint8_t uart2_wait_receive(void);
void uart2_rx_clear(void);

一切判斷就緒后,我們就可以將代碼燒進(jìn)板子,現(xiàn)象如下:

5. 小結(jié)

STM32 串口通訊在項(xiàng)目中使用的頻率非常高,但由于不知道數(shù)據(jù)發(fā)送方會(huì)發(fā)送多少數(shù)據(jù)量,所以串口接收不定長(zhǎng)數(shù)據(jù)成了一個(gè)急需解決的問(wèn)題。

本文使用串口的接收中斷+超時(shí)判斷方法解決了此問(wèn)題,并給出了詳細(xì)的教程,希望對(duì)讀者朋友有所幫助。

另外,想進(jìn)大廠的同學(xué),一定要好好學(xué)算法,這是面試必備的。這里準(zhǔn)備了一份 BAT 大佬總結(jié)的 LeetCode 刷題寶典,很多人靠它們進(jìn)了大廠。

刷題 | LeetCode算法刷題神器,看完 BAT 隨你挑!

有收獲?希望老鐵們來(lái)個(gè)三連擊,給更多的人看到這篇文章

推薦閱讀:

歡迎關(guān)注我的博客:良許嵌入式教程網(wǎng),滿滿都是干貨!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容