干貨|高質量代碼是怎么寫出來的?串口環形隊列

摘要串口是通信中最常用的通信方式,可能寫串口的驅動,能寫幾十種方法, 查詢方式,中斷方式,DMA方式,定時器方式。可能也其中幾種方式的組合形式,經典的用法是:發送用查詢方式,接收用中斷方式,或者DMA+空閑中斷。本篇不講串口是啥,現在還在講串口是啥,估計會被噴。今天來聊一聊串口常用的幾種方式,最簡單的方法就不說了。

一、經典方法

  1. 查詢方式 可靠性很高,要考慮下個數據包覆蓋上一個數據包的問題,小數據量,在10個字節以內,可以這樣考慮, 很簡單,很方便,很可靠。但是在數據量大的時候,程序阻塞的時間特別長,影響其他比較重要的外設的處理。

  2. 中斷方式 中斷方式 , 不占用系統資源,但是如果數據量大,會頻繁中斷cpu, 會其他高優先的數據處理造成影響。但是沒有DMA不占用資源的好處, 如果沒有串口隊列的實現,必須通過標志位判斷上一個包數據是否發送完成,在把新的數據覆蓋到串口的緩沖區。

  3. DMA方式 優點:  不占用系統資源,減少CPU對中斷的響應。如何不建立數據包的隊列,還是會出現,需要等待阻塞的問題。

二、環形隊列

隊列這個詞在數據局結構中出現的比較多,與之對應的就是堆棧,但是兩者的讀取方式又完全不同。

FIFO 是First-In First-Out的縮寫,它是一個具有先入先出特點的緩沖區。串口設計FIFO的目的是為了提高串口的通訊性能。如果沒有FIFO或者說緩沖區的長度只有1字節,那么使用接收中斷,就意味著每次收到一個字節的數據就要進一次中斷,這樣頻繁進中斷會占用CPU資源。另外如果沒有及時讀走數據,那么下一個字節數據就會覆蓋之前的數據,導致數據丟失,這在通訊速率高的場合很有可能出現

使用FIFO,可以在連續接收若干個數據后才產生一次中斷,然后一起進行處理。這樣可以提高接收效率,避免頻繁進中斷,適用于大數據傳輸。你可能會想到如果FIFO中的數據沒有達到指定長度而無法產生中斷怎么辦,通常MCU會有接收超時中斷,即在一定的時間內沒有接收到數據會進入中斷,可以利用這個中斷把不足FIFO長度的數據最后都讀取完。

FIFO類似售票排隊窗口,先到的人看到能先買到票,然后先走,后來的人只能后買到票。

干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖1

在計算機中,每個信息都是存儲在存儲單元中的,當有大量數據的時候,我們不能存儲所有的數據,那么計算機處理數據的時候,只能先處理先來的,那么處理完后呢,就會把數據釋放掉,再處理下一個。那么,已經處理的數據的內存就會被浪費掉。因為后來的數據只能往后排隊,如過要將剩余的數據都往前移動一次,那么效率就會低下了,肯定不現實,所以,環形隊列就出現了。

點擊下方視頻動態演示出隊入隊干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖2

1、環形隊列的實現

在計算機中,是沒有環形的內存的,只不過是我們將順序的內存處理過,讓某一段內存形成環形,使他們首尾相連,簡單來說,這其實就是一個數組,只不過有兩個指針,一個指向列隊頭,一個指向列隊尾。指向列隊頭的指針是緩沖區可讀的數據,指向列隊尾的指針是緩沖區可寫的數據,通過移動這兩個指針即可對緩沖區的數據進行讀寫操作了,直到緩沖區已滿(頭尾相接),將數據處理完,可以釋放掉數據,又可以進行存儲新的數據了。

實現的原理

視頻來自正在一名考研的UP主:禿頭少女王某。計算機專業考研這個是必考點,視頻講的很棒,祝她一戰成碩,金榜題名!干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖3干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖4

串口環形緩沖區收發:在初學單片機的時候我們知道的串口收發都是:接收一個數據,觸發中斷,然后把數據發回來。這種處理方式是沒有緩沖的,當數量太大的時候,亦或者當數據接收太快的時候,我們來不及處理已經收到的數據,那么,當再次收到數據的時候,就會將之前還未處理的數據覆蓋掉。那么就會出現丟包的現象了,對我們的程序是一個致命的創傷。

那么如何避免這種情況的發生呢,很顯然,上面說的一些隊列的特性很容易幫我們實現我們需要的情況。將接受的數據緩存一下,讓處理的速度有些許緩沖,使得處理的速度趕得上接收的速度,上面又已經分析了普通隊列與環形隊列的優劣了,那么我們肯定是用環形隊列來進行實現了。下面就是代碼的實現:

2、定義一個結構體

typedef struct
{

    uint16_t usWrite;
    uint16_t usRead;
    uint16_t usLenght;
    /* FIFO 結構 */
    uint8_t  ucRing_Buff[RINGBUFF_LEN];

}RingBuff_T;

extern RingBuff_T g_ringBuff;

3、初始化隊列

初始化結構體相關信息:使得我們的環形緩沖區是頭尾相連的,并且里面沒有數據,也就是空的隊列,所有元素清0。

void RingBuff_Init(void)
{
    g_ringBuff.usWrite = 0;
    g_ringBuff.usRead = 0;
    g_ringBuff.usLenght = 0;
}

4、數據壓入隊列

/**
* @brief  Write_RingBuff
* @param  uint8_t _ucWriteData
* @return 0:環形緩沖區已滿,寫入失敗;1:寫入成功
* @note   往環形緩沖區寫入uint8_t類型的數據
*/

uint8_t Write_RingBuff(uint8_t _ucWriteData)
{
    if(g_ringBuff.usLenght >= RINGBUFF_LEN) /*判斷緩沖區是否已滿*/
    {
        return 0;
    } 
    g_ringBuff.ucRing_Buff[g_ringBuff.usRead] = _ucWriteData;
    g_ringBuff.usRead = (g_ringBuff.usRead + 1) % RINGBUFF_LEN; /*防止越界非法訪問*/
    g_ringBuff.usLenght++;
    return 1;
}

5、從隊列中讀出數據

/**
* @brief  Read_RingBuff
* @param  u8 *rData,用于保存讀取的數據
* @return 0:環形緩沖區沒有數據,讀取失敗; 1:讀取成功
* @note   從環形緩沖區讀取一個uint8_t類型的數據
*/

uint8_t Read_RingBuff(uint8_t *_usReadData)
{
    if(g_ringBuff.usLenght == 0)/*判斷非空*/
    {
        return 0;
    }
    *_usReadData = g_ringBuff.ucRing_Buff[g_ringBuff.usWrite];/*先進先出FIFO,從緩沖區頭出*/
    g_ringBuff.usWrite = (g_ringBuff.usWrite + 1) % RINGBUFF_LEN; /*防止越界非法訪問*/
    g_ringBuff.usLenght--;
    return 1;
}

對于讀寫操作需要注意的地方有兩個:

1:判斷隊列是否為空或者滿,如果空的話,是不允許讀取數據的,返回0。如果是滿的話,也是不允許寫入數據的,避免將已有數據覆蓋掉。那么如果處理的速度趕不上接收的速度,可以適當增大緩沖區的大小,用空間換取時間。2:防止指針越界非法訪問,程序有說明,需要使用者對整個緩沖區的大小進行把握。

四、環形緩沖器

環形緩沖器(ringr buffer),也稱作圓形隊列(circular queue),循環緩沖區(cyclic buffer),圓形緩沖區(circula buffer),是一種用于表示一個固定尺寸、頭尾相連的緩沖區的數據結構,適合緩存數據流。

圓形緩沖區的一個有用特性是:當一個數據元素被用掉后,其余數據元素不需要移動其存儲位置。相反,一個非圓形緩沖區(例如一個普通的隊列)在用掉一個數據元素后,其余數據元素需要向前搬移。換句話說,圓形緩沖區適合實現先進先出緩沖區,而非圓形緩沖區適合后進先出緩沖區。

那么如何將環形緩沖器ringr buffer應用到串口上面呢?這里我們使用RT-Thread的源碼。

1、定義一個結構體

/* ring buffer */
struct rt_ringbuffer
{

    uint8_t *buffer_ptr;
    uint16_t read_mirror : 1;
    uint16_t read_index : 15;
    uint16_t write_mirror : 1;
    uint16_t write_index : 15;
    uint16_t buffer_size;
};

2、初始化ringbuffer

void rt_ringbuffer_init(struct rt_ringbuffer *rb,
                        uint8_t           *pool,
                        uint16_t            size)

{
    RT_ASSERT(rb != NULL);
    RT_ASSERT(size > 0);

    /* initialize read and write index */
    rb->read_mirror = rb->read_index = 0;
    rb->write_mirror = rb->write_index = 0;

    /* set buffer pool and size */
    rb->buffer_ptr = pool;
    rb->buffer_size = RT_ALIGN_DOWN(size, RT_ALIGN_SIZE);
}

3、將數據壓入ringbuffer

unsigned long rt_ringbuffer_put(struct rt_ringbuffer *rb,
                            const uint8_t     *ptr,
                            uint16_t           length)

{
    uint16_t size;

    RT_ASSERT(rb != NULL);

    /* whether has enough space */
    size = rt_ringbuffer_space_len(rb);

    /* no space */
    if (size == 0)
        return 0;

    /* drop some data */
    if (size < length)
        length = size;

    if (rb->buffer_size - rb->write_index > length)
    {
        /* read_index - write_index = empty space */
        memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);
        /* this should not cause overflow because there is enough space for
         * length of data in current mirror */

        rb->write_index += length;
        return length;
    }

    memcpy(&rb->buffer_ptr[rb->write_index],
           &ptr[0],
           rb->buffer_size - rb->write_index);
    memcpy(&rb->buffer_ptr[0],
           &ptr[rb->buffer_size - rb->write_index],
           length - (rb->buffer_size - rb->write_index));

    /* we are going into the other side of the mirror */
    rb->write_mirror = ~rb->write_mirror;
    rb->write_index = length - (rb->buffer_size - rb->write_index);

    return length;
}

4、從ringbuffer中讀數據

unsigned long rt_ringbuffer_get(struct rt_ringbuffer *rb,
                            uint8_t           *ptr,
                            uint16_t           length)

{
    unsigned long size;

    RT_ASSERT(rb != NULL);

    /* whether has enough data  */
    size = rt_ringbuffer_data_len(rb);

    /* no data */
    if (size == 0)
        return 0;

    /* less data */
    if (size < length)
        length = size;

    if (rb->buffer_size - rb->read_index > length)
    {
        /* copy all of data */
        memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);
        /* this should not cause overflow because there is enough space for
         * length of data in current mirror */

        rb->read_index += length;
        return length;
    }

    memcpy(&ptr[0],
           &rb->buffer_ptr[rb->read_index],
           rb->buffer_size - rb->read_index);
    memcpy(&ptr[rb->buffer_size - rb->read_index],
           &rb->buffer_ptr[0],
           length - (rb->buffer_size - rb->read_index));

    /* we are going into the other side of the mirror */
    rb->read_mirror = ~rb->read_mirror;
    rb->read_index = length - (rb->buffer_size - rb->read_index);

    return length;
}

5、移植ringbuffer

1.首先將RT-Thread的ringbuffer.c和ringbuffer.h文件加入到我們的MDK工程中去。

干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖5
干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖6

2.編寫串口相關的底層硬件bsp代碼,也就是初始化GPIO和串口相關的配置,這個就很簡單,大家應該都會。在串口初始化代碼中記得要手動將串口的非空中斷和空閑中斷打開。

UART_HandleTypeDef huart1;//這個要定義為全局的變量
void uart_init(u32 bound)
{
 huart1.Instance=USART1;         //USART1
 huart1.Init.BaudRate=bound;        //波特率
 huart1.Init.WordLength=UART_WORDLENGTH_8B;  //字長為8位數據格式
 huart1.Init.StopBits=UART_STOPBITS_1;     //一個停止位
 huart1.Init.Parity=UART_PARITY_NONE;   //無奇偶校驗位
 huart1.Init.HwFlowCtl=UART_HWCONTROL_NONE;   //無硬件流控
 huart1.Init.Mode=UART_MODE_TX_RX;      //收發模式
 HAL_UART_Init(&huart1);//HAL_UART_Init()會使能UART1  
 
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);/*使能串口接收非空中斷 */ 
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE); /*使能串口接收空閑中斷 */
 
}

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    //GPIO端口設置
 GPIO_InitTypeDef GPIO_InitStruct;
 
 if(huart->Instance==USART1)//如果是串口1,進行串口1 MSP初始化
 {
  __HAL_RCC_USART1_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  GPIO_InitStruct.Pin = GPIO_PIN_9;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = GPIO_PIN_10;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
        //使用中斷肯定要配置NVIC
  HAL_NVIC_SetPriority(USART1_IRQn, 10);
  HAL_NVIC_EnableIRQ(USART1_IRQn);

 }
}

3.定義一個結構頭rt_ringbuffer 類型的變量ring_buf,變量名隨便取,阿貓阿狗都可以,只要你自己認得就行。

struct rt_ringbuffer ring_buf; 

4.定義一個串口接收緩沖區數組,數組名隨便取,阿貓阿狗都可以,只要你自己認得就行。

static uint8_t  s_USART1_RxBuf[256];

5.初始化ringbuffer

rt_ringbuffer_init(&ring_buf,s_USART1_RxBuf,256);

位置放在哪里都可以,我這里就放在串口串口初始化之前了。

干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖7

6.編寫中斷服務函數

void USART1_IRQHandler(void)                 

 uint8_t receive_char;
 
 if((__HAL_USART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)) //接收非空中斷
    {
  HAL_UART_Receive(&huart1, &receive_char, 11000);//將收到的數據放入receive_char  
  rt_ringbuffer_put(&ring_buf,&receive_char,1);//將receive_char壓入到ring_buf中
  USART1_Len++;  
  __HAL_USART_CLEAR_FLAG(&huart1,UART_FLAG_RXNE);//清除接收非空中斷
 }
  
 if((__HAL_USART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)) //接收空閑中斷
    {
 
  rt_ringbuffer_get(&ring_buf,s_USART1_RxBuf, USART1_Len);//將rring_buf的數據讀出到到s_USART1_RxBuf中
  s_USART1_RxBuf[USART1_Len] = '\0';
  printf("%s\r\n",s_USART1_RxBuf);
  USART1_Len = 0 ;//數據幀長度復位
  __HAL_USART_CLEAR_IDLEFLAG(&huart1);
 }
}

這里面的代碼我寫的應該很簡單了,首先我們在初始化中是能了接收中斷和空閑中斷,那么如果有數據過來,就會觸發中斷。進入中斷服務函數,進來之后首先判斷接收中斷標志位是否置位為1,如果是1說明收數據來了,通過HAL_UART_Receive函數將數據存入receive_char中,再通過rt_ringbuffer_put將數據壓入隊列之中。如果數據接收完了就會觸發空閑中斷,這時通過rt_ringbuffer_get函數將數據讀出到我們定義的數組中打印出來。

7.主函數

int main(void)

 HAL_Init(); 
 Stm32_Clock_Init();
 delay_init(72);
 uart_init(115200);  
 while(1)
 { 
 }
}

主函數啥都不要寫,完了。

干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖8

五、隊列FIFO

隊列 (Queue):是一種先進先出(First In First Out ,簡稱 FIFO)的線性表,只允許在一端插入,在另一端進行刪除。

FIFO一般用于不同時鐘域之間的數據傳輸,比如FIFO的一端是AD數據采集,另一端是計算機的PCI總線,假設其AD采集的速率為16位 100K SPS,那么每秒的數據量為100K×16bit=1.6Mbps,而PCI總線的速度為33MHz,總線寬度32bit,其最大傳輸速率為1056Mbps,在兩個不同的時鐘域間就可以采用FIFO來作為數據緩沖。另外對于不同寬度的數據接口也可以用FIFO,例如單片機位8位數據輸出,而DSP可能是16位數據輸入,在單片機與DSP連接時就可以使用FIFO來達到數據匹配的目的。

1、定義一個結構體

/* 串口設備結構體 */
typedef struct
{

 USART_TypeDef *uart;  /* STM32內部串口設備指針 */
 uint8_t *pTxBuf;   /* 發送緩沖區 */
 uint8_t *pRxBuf;   /* 接收緩沖區 */
 
 uint16_t usTxBufSize;  /* 發送緩沖區大小 */
 uint16_t usRxBufSize;  /* 接收緩沖區大小 */
 
 __IO uint16_t usTxWrite; /* 發送緩沖區寫指針 */
 __IO uint16_t usTxRead;  /* 發送緩沖區讀指針 */
 __IO uint16_t usTxCount; /* 等待發送的數據個數 */

 __IO uint16_t usRxWrite; /* 接收緩沖區寫指針 */
 __IO uint16_t usRxRead;  /* 接收緩沖區讀指針 */
 __IO uint16_t usRxCount; /* 還未讀取的新數據個數 */

}UART_T;

2、初始化FIFO

static void Uart_FIFO_Init(void)
{
 g_tUart1.uart = USART1;      /* STM32 串口設備 */
 g_tUart1.pTxBuf = g_TxBuf1;     /* 發送緩沖區指針 */
 g_tUart1.pRxBuf = g_RxBuf1;     /* 接收緩沖區指針 */
 g_tUart1.usTxBufSize = 1024;       /* 發送緩沖區大小 */
 g_tUart1.usRxBufSize = 1024;    /* 接收緩沖區大小 */
 g_tUart1.usTxWrite = 0;      /* 發送FIFO寫索引 */
 g_tUart1.usTxRead = 0;      /* 發送FIFO讀索引 */
 g_tUart1.usRxWrite = 0;      /* 接收FIFO寫索引 */
 g_tUart1.usRxRead = 0;      /* 接收FIFO讀索引 */
 g_tUart1.usRxCount = 0;      /* 接收到的新數據個數*/
 g_tUart1.usTxCount = 0;      /* 待發送的數據個數 */
}

3、初始化串口

UART_HandleTypeDef UartHandle; 
static void Uart_GPIO_Config(void)
{
 GPIO_InitTypeDef  GPIO_InitStruct; 

 /* 使能 GPIO TX/RX 時鐘 */
 USART1_TX_GPIO_CLK_ENABLE();
 USART1_RX_GPIO_CLK_ENABLE();
 
 /* 使能 USARTx 時鐘 */
 USART1_CLK_ENABLE(); 

 /* 配置TX引腳 */
 GPIO_InitStruct.Pin       = USART1_TX_PIN;
 GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
 GPIO_InitStruct.Pull      = GPIO_PULLUP;
 GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
 HAL_GPIO_Init(USART1_TX_GPIO_PORT, &GPIO_InitStruct); 
 
 /* 配置RX引腳 */
 GPIO_InitStruct.Pin = USART1_RX_PIN;
 GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
 GPIO_InitStruct.Pull = GPIO_NOPULL;
 HAL_GPIO_Init(USART1_RX_GPIO_PORT, &GPIO_InitStruct);  

 UartHandle.Instance        = USART1;
 UartHandle.Init.BaudRate   = 115200;
 UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
 UartHandle.Init.StopBits   = UART_STOPBITS_1;
 UartHandle.Init.Parity     = UART_PARITY_NONE;
 UartHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
 UartHandle.Init.Mode       = UART_MODE_TX_RX; 
 HAL_UART_Init(&UartHandle); 
    
 __HAL_USART_CLEAR_FLAG(&UartHandle, USART_FLAG_TC);  /* 清除TC發送完成標志 */
 __HAL_USART_CLEAR_FLAG(&UartHandle, USART_FLAG_RXNE);/* 清除RXNE接收標志 */
 __HAL_USART_ENABLE_IT(&UartHandle, USART_IT_RXNE);   /* 使能接收數據寄存器非空中斷 */ 
 
 /* 配置NVIC the NVIC for UART */   
 HAL_NVIC_SetPriority(USART1_IRQn, 01);
 HAL_NVIC_EnableIRQ(USART1_IRQn); 
}

4、將數據壓入FIFO

什么時候要將數據壓入FIFO?當然是在向串口發送數據的時候將數據壓入FIFO去啦,這里面的邏輯也很簡單就是將要發送的數據壓入FIFO緩沖區,之后打開發送中斷就可以了。

void UartSendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
{
 uint16_t i;

 for (i = 0; i < _usLen; i++)
 {
  /* 如果發送緩沖區已經滿了,則等待緩沖區空 */
  while (1)
  {
   __IO uint16_t usCount;

   usCount = g_tUart1.usTxCount;

   if (usCount < g_tUart1.usTxBufSize)
   {
    break;/*如果發送緩沖區沒有滿就跳出去,讓發送緩沖區填滿*/
   }
   else if(usCount == g_tUart1.usTxBufSize)/* 數據已填滿緩沖區 */
   {
    if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TXE)== RESET)
    {
     __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TXE);  /* 使能發送中斷(緩沖區空) */
    }  
   }
  }
  /* 將新數據填入發送緩沖區 */
  g_tUart1.pTxBuf[g_tUart1.usTxWrite] = _ucaBuf[i];
  DISABLE_INT();
  if (++g_tUart1.usTxWrite >= g_tUart1.usTxBufSize)
  {
   g_tUart1.usTxWrite = 0;
  }
  g_tUart1.usTxCount++;
  ENABLE_INT();
 }
 __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TXE); /* 使能發送中斷(緩沖區空) */
}

如果要發送的數據沒有超過發送緩沖區大小,實現起來還比較容易,直接把數據填到 FIFO 里面,并使能發送空中斷即可。如果超過了 FIFO 大小,就需要等待有空間可用,針對這種情況有個重要的知識點,就是當緩沖剛剛填滿的時候要判斷發送空中斷是否開啟了,如果填滿了還沒有開啟,就會卡死在 while 循環中,所以多了一個剛填滿時的判斷,填滿了還沒有開啟發送空中斷,要開啟下。

5、從FIFO中讀出數據

什么時候要從FIFO中讀出?當然是在從串口獲取數據的時候將從FIFO中讀數據啦。

uint8_t UartGetChar(uint8_t *_pByte)

 uint16_t usCount;
 /* usRxWrite 變量在中斷函數中被改寫,主程序讀取該變量時,必須進行臨界區保護 */
 DISABLE_INT();
 usCount = g_tUart1.usRxCount;
 ENABLE_INT();

 /* 如果讀和寫索引相同,則返回0 */
 if (usCount == 0/* 已經沒有數據 */
 {
  return 0;
 }
 else
 {
  *_pByte = g_tUart1.pRxBuf[g_tUart1.usRxRead];/* 從串口接收FIFO取1個數據 */

  /* 改寫FIFO讀索引 */
  DISABLE_INT();
  if (++g_tUart1.usRxRead >=g_tUart1.usRxBufSize)
  {
   g_tUart1.usRxRead = 0;
  }
  g_tUart1.usRxCount--;
  ENABLE_INT();
  return 1;
 }
}

6、中斷服務函數

void USART1_IRQHandler(void)

 
 /* 處理接收緩沖區中斷  */
 if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_RXNE)!= RESET)
 {
  /* 從串口接收數據寄存器讀取數據存放到接收FIFO */
  uint8_t ch;

  ch = READ_REG(g_tUart1.uart->DR);
  g_tUart1.pRxBuf[g_tUart1.usRxWrite] = ch;
  if (++g_tUart1.usRxWrite >= g_tUart1.usRxBufSize)
  {
   g_tUart1.usRxWrite = 0;
  }
  if (g_tUart1.usRxCount < g_tUart1.usRxBufSize)
  {
   g_tUart1.usRxCount++;
  }
 }

 /* 處理發送緩沖區空中斷 */
 if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TXE)!= RESET)
 {

  if (g_tUart1.usTxCount == 0)
  {
   /* 發送緩沖區的數據已取完時, 禁止發送緩沖區空中斷 (注意:此時最后1個數據還未真正發送完畢)*/
   CLEAR_BIT(g_tUart1.uart->CR1, USART_CR1_TXEIE);

   /* 使能數據發送完畢中斷 */
   __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TC);/* 使能數據發送完畢中斷 */
   
  }
  else
  {   
   /* 從發送FIFO取1個字節寫入串口發送數據寄存器 */
   g_tUart1.uart->DR = g_tUart1.pTxBuf[g_tUart1.usTxRead];
   if (++g_tUart1.usTxRead >= g_tUart1.usTxBufSize)
   {
    g_tUart1.usTxRead = 0;
   }
   g_tUart1.usTxCount--;
  }

 }
 /* 數據bit位全部發送完畢的中斷 */
 if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TC)!= RESET)
 {
  if (g_tUart1.usTxCount == 0)
  {
   /* 如果發送FIFO的數據全部發送完畢,禁止數據發送完畢中斷 */
   __HAL_USART_CLEAR_FLAG(&UartHandle,USART_FLAG_TC);/*發送完畢中斷*/
  }
}

7、主函數

const char buf1[] = "接收到串口命令1\r\n";
const char buf2[] = "接收到串口命令2\r\n";
const char buf3[] = "接收到串口命令3\r\n";
const char buf4[] = "接收到串口命令4\r\n";

int main(void)
{
    uint8_t  read[100] = {0} ; 
    bsp_Init();    

    while(1)
    {
        /* 接收到的串口命令處理 */
        if (UartGetChar(read)!=0)
        {
   UartSendBuf((uint8_t *)read, sizeof(read)/sizeof(uint8_t));
            switch (read[0])
            {
                case '1':
                    UartSendBuf((uint8_t *)buf1, strlen(buf1));
                    break;

                case '2':
                    UartSendBuf((uint8_t *)buf2, strlen(buf2));
                    break;

                case '3':
                    UartSendBuf((uint8_t *)buf3, strlen(buf3));
                    break;

                case '4':
                    UartSendBuf((uint8_t *)buf4, strlen(buf4));
                    break;

                default:
                    break;
            }
        }
    }
}
干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖9

8、關于串口掃盲

串口掃盲就是幾個狀態標志位不好理解,只要理解好這張圖就好辦了,其他的請參考相關的教程。

干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖10
干貨|高質量代碼是怎么寫出來的?串口環形隊列的圖11
  1. USART_FLAG_TXE當發送數據寄存器里的數據被全部取完時,該寄存器是空的,那么該標志位就會被置1。通過這個標志位的值可以判斷發送數據寄存器中的數據有沒有完全被取走,當該寄存器是空的時候,可以提醒CPU繼續往該寄存器里存入新的數據;

  2. USART_FLAG_TC當發送移位寄存器里的每個字節通過TX腳一位一位發送出去之后,該標志位值就會被置1。通過這個標志位的值可以判斷發送移位寄存器里的數據有沒有被全部發送出去;

  3. USART_FLAG_TXEUSART_FLAG_TC之間的聯系 結合上面流程圖來進行說明,實際上發送移位寄存器通過TX腳發送數據這個過程是比較耗時的,所以在此過程進行時,可通過判斷當USART_FLAG_TXE = 1,即發送數據寄存器里的數據已被全部轉入發送移位寄存器時,就讓CPU往發送數據寄存器轉入新的數據。當發送移位寄存器把數據幀全部發送出去之后,可通過判斷USART_FLAG_TC = 1,證明數據幀的最后一個字節都已經通過TX腳發送完了。

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP