1.Lwrb的介紹(博主功能的實現是基于RT-thread系統實現)
? ? ? ? Lwrb是由Tilen Majerle編寫的一個線程安全的環形隊列,通常與DMA配合實現數據的無阻塞性收發,同時,配合DMA的傳輸過半中斷,傳輸完成中斷,以及串口的空閑中斷實現實現數據的無阻塞性收發,同時環形隊列,在一定程度上給CPU處理數據提供了一定的時間,具體的原理在后續的內容中會有詳細解釋。
2.Lwrb源碼的獲取及移植
? ? ? ? 源碼鏈接如下:若不能訪問,則掛載VPN即可。MaJerle/lwrb: Lightweight generic ring buffer manager libraryhttps://github.com/MaJerle/lwrb? ? ? ? Lwrb的源碼結構如下:
? ? ? ? 該文件夾內是Lwrb的源碼,以及對應源碼操作的示例。移植時只需將如下文件添加到我們的工程文件即可:
? ? ? ?這個Lwrb開源庫只需要把文件添加到工程中,就算是移植完成了。
3.環形隊列實現無阻塞性接收數據的解釋
? ? ? ? 串口是提供移位寄存器實現的數據接收,當MCU檢測到串口的RDR寄存器不為空時,串口的移位寄存器將會把RDR寄存器中的數據一個一個的移出,這時,移位寄存器移出的數據寫入至對應的環形隊列中,當串口數據接收完成時,則會進入串口的空閑中斷,我們在串口的空閑中斷中設置一個標記位(若我們的項目中已經添加了RTOS,則可以在空閑中斷中釋放一個信號量),當我們的主程序檢測到對應的標記被置位,則說明串口已經完成了數據接收,此時將環形隊列中的數據讀出,然后檢測對應的數據即可。但是這樣有一個弊端,此時沒有DMA配合時,只能通過串口的空閑中斷判斷數據傳輸是否已經完成,當串口一次性接收的數據大于環形隊列的最大長度時,則會導致環形隊列中之前接收的數據沒有被及時處理進而導致數據被覆蓋,而造成數據的丟失。若是將串口的空閑中斷和DMA的傳輸一半中斷以及傳輸完成中斷進行結合,此時就可實現數據的及時處理,實現方法如下:
? ? ? ? 這里用圖表的方式表示,具體的代碼試下在下面的章節中會有詳細的內容解釋。
? ? ? ? 解釋一下為什么這樣做:當串口一次性接收的數據太多時,接收數據的環形隊列中的數據被覆蓋,進而這樣設計,意思就是說當數據過多時,DMA接收的數據已經填充了緩沖區的一半數據,此時觸發DMA的傳輸完成一半中斷,此時將對應的數據處理標記置1,我們主程序中將會開始讀取環形隊列中的數據,這樣就會及時處理對應的數據,不會導致環形隊列中的數據被覆蓋。(注意:這要求我們數據處理的速度大于DMA搬運數據的速度)。
? ? ? ? 此方法避免了DMA使用雙緩沖區實現無阻塞性的接收而造成的內存浪費問題,同時也提高了我們的傳輸效率。
4.實現串口無阻塞性收發的具體實現
4.1?串口的配置
? ? ? ? 配置代碼具體如下:
/*-------------------------------封裝數據,減少全局變量的使用-------------------------------------*/
typedef struct Drv_uart{lwrb_t uart1_tx_rb;//串口的發送lwrb_t uart1_rx_rb;//串口的接收環形隊列uint8_t uart_tx_rb_data[UART1_TX_RB_LENGTH];//這個是發送緩沖區uint8_t uart1_rx_rb_data[UART1_RX_RB_LENGTH];//接收緩沖區volatile size_t _uart1_tx_dma_current_len;
}Drv_Uart_t;/*** @brief GPIO初始化* @retval 無*/
void Uart1_Gpio_Init(void)
{/* GPIO外設時鐘使能 */std_rcc_gpio_clk_enable(RCC_PERIPH_CLK_GPIOA);std_gpio_init_t usart_gpio_init = {0};usart_gpio_init.pin = UART1_TX_GPIO_PIN;usart_gpio_init.mode = GPIO_MODE_ALTERNATE;usart_gpio_init.output_type = GPIO_OUTPUT_PUSHPULL;usart_gpio_init.pull = GPIO_PULLUP;usart_gpio_init.alternate = GPIO_AF1_USART1;std_gpio_init(UART1_TX_GPIO_PORT, &usart_gpio_init);usart_gpio_init.pin = UART1_RX_GPIO_PIN;usart_gpio_init.mode = GPIO_MODE_ALTERNATE;usart_gpio_init.output_type = GPIO_OUTPUT_PUSHPULL;usart_gpio_init.pull = GPIO_PULLUP;usart_gpio_init.alternate = GPIO_AF1_USART1;std_gpio_init(UART1_RX_GPIO_PORT, &usart_gpio_init);
}/*** @brief USART1初始化* @retval 無*/
void _Uart1_Init(uint32_t baudrate, uint32_t par)
{/* USART1時鐘使能 */std_rcc_apb2_clk_enable(RCC_PERIPH_CLK_USART1);std_usart_init_t usart_config = {0};usart_config.direction = USART_DIRECTION_SEND_RECEIVE;usart_config.baudrate = baudrate;usart_config.wordlength = USART_WORDLENGTH_8BITS;usart_config.stopbits = USART_STOPBITS_1;usart_config.parity = par;usart_config.hardware_flow = USART_FLOWCONTROL_NONE;/* USART初始化 */if (STD_OK != std_usart_init(USART1, &usart_config)){/* 波特率配置不正確處理代碼 */while (1);}/* NVIC初始化 */NVIC_SetPriority(USART1_IRQn, 0);NVIC_EnableIRQ(USART1_IRQn);std_usart_cr1_interrupt_enable(USART1, USART_CR1_INTERRUPT_PE);std_usart_cr1_interrupt_enable(USART1, USART_CR3_INTERRUPT_ERR);std_usart_cr1_interrupt_enable(USART1, USART_CR1_INTERRUPT_IDLE);/* 使能USART DMA接收 */std_usart_dma_rx_enable(USART1); // 串口DMA接收使能std_usart_enable(USART1); // 串口使能RTT_LOG_I("USART1 init success");
}void UART1_Init(uint32_t baudrate, uint32_t par)
{/* Initialize ringbuff */lwrb_init(&Usart1.uart1_rx_rb, Usart1.uart1_rx_rb_data, sizeof(Usart1.uart1_rx_rb_data));//這是關于串口接收環形隊列的初始化lwrb_init(&Usart1.uart1_tx_rb, Usart1.uart_tx_rb_data, sizeof(Usart1.uart_tx_rb_data));//這是關于串口發送環形隊列的初始化/* 串口DMA配置 */Uart1_Dma_Init();/* GPIO初始化 */Uart1_Gpio_Init();/* UASRT1初始化 */_Uart1_Init(baudrate, par);
}
4.2?串口DMA的配置
? ? ? ? 由于CIU32L051這一系列的MCU只有兩個DMA通道,具體的通道映射有在之前的那篇CIU32關于DMA的無阻塞性收發中寫,各位需要的可以去查看對應的內容。
? ? ? ? 這里根據自己IC的情況進行配置即可,博主的配置流程具體如下:
/*** @brief DMA配置函數* @param distination DMA傳輸目的地址* @param number DMA傳輸字符數* @retval 無*/
void Uart1_Dma_Rec_Data_Cfg(uint8_t *distination)
{std_dma_config_t dma_config = {0};/* 配置DMA 源地址、目的地址和傳輸數據大小,并使能DMA */dma_config.src_addr = (uint32_t)&USART1->RDR;dma_config.dst_addr = (uint32_t)distination;// dma_config.data_number = LWUTIL_ARRAYSIZE(distination);dma_config.data_number = 128;dma_config.dma_channel = DMA_CHANNEL_0;std_dma_start_transmit(&dma_config);
}/*** @brief DMA配置函數* @param source DMA源地址* @param number DMA傳輸字符數* @retval 無*/
void Uart1_Dma_Send_Data(uint32_t *source, uint32_t number)
{std_dma_config_t dma_config = {0};/* 配置DMA 源地址、目的地址和傳輸數據大小,并使能DMA */dma_config.src_addr = (uint32_t)source;dma_config.dst_addr = (uint32_t)&USART1->TDR;dma_config.data_number = number;dma_config.dma_channel = DMA_CHANNEL_1;std_dma_start_transmit(&dma_config);
}/*** @brief DMA通道0初始化* @retval 無*/
void Uart1_Dma_Init(void)
{std_dma_init_t dma_init_param = {0};/* DMA外設時鐘使能 */std_rcc_ahb_clk_enable(RCC_PERIPH_CLK_DMA);/* dma_init_param 結構體初始化 */dma_init_param.dma_channel = UART1_DMA_RX_CHANNEL;//MDA的通道0映射為串口的接收引腳dma_init_param.dma_req_id = DMA_REQUEST_USART1_RX;//這里是指DMA傳輸的觸發條件dma_init_param.transfer_type = DMA_BLOCK_TRANSFER;//這里就是設置了DMA的傳輸類型,具體就不解釋了dma_init_param.src_addr_inc = DMA_SRC_INC_DISABLE;//失能數據遞增遞增dma_init_param.dst_addr_inc = DMA_DST_INC_ENABLE;//使能目標地址遞增dma_init_param.data_size = DMA_DATA_SIZE_BYTE;//每次傳輸一個字節dma_init_param.mode = DMA_MODE_CIRCULAR;//循環接收std_dma_init(&dma_init_param);/* dma_init_param 結構體初始化 */dma_init_param.dma_channel = UART1_DMA_TX_CHANNEL;dma_init_param.dma_req_id = DMA_REQUEST_USART1_TX;dma_init_param.transfer_type = DMA_BLOCK_TRANSFER;dma_init_param.src_addr_inc = DMA_SRC_INC_ENABLE;dma_init_param.dst_addr_inc = DMA_DST_INC_DISABLE;dma_init_param.data_size = DMA_DATA_SIZE_BYTE;dma_init_param.mode = DMA_MODE_NORMAL;std_dma_init(&dma_init_param);/* 使能接收中斷 */std_dma_interrupt_enable(UART1_DMA_RX_CHANNEL, DMA_INTERRUPT_TF);/**< DMA傳輸完成中斷 */std_dma_interrupt_enable(UART1_DMA_RX_CHANNEL, DMA_INTERRUPT_TH);/**< DMA傳輸一半中斷 */std_dma_interrupt_enable(UART1_DMA_RX_CHANNEL, DMA_INTERRUPT_TE); /**< DMA傳輸錯誤中斷 *//* NVIC初始化 */NVIC_SetPriority(UART1_DMA_RX_IRQ_CHANNEL, 0);NVIC_EnableIRQ(UART1_DMA_RX_IRQ_CHANNEL);NVIC_SetPriority(UART1_DMA_TX_IRQ_CHANNEL, 0);NVIC_EnableIRQ(UART1_DMA_TX_IRQ_CHANNEL);Uart1_Dma_Rec_Data_Cfg(_uart1_rx_dma_buffer); // DMA接收數據配置std_dma_enable(UART1_DMA_RX_CHANNEL);
}
? ? ? ? 以上內容則是關于串口的硬件配置。
4.3 串口的中斷服務函數和DMA的中斷服務函數
? ? ? ? 具體內容:
/*** @brief USART1 中斷服務函數* @retval 無*/
void USART1_IRQHandler(void)
{/* enter interrupt */rt_interrupt_enter();//這是博主使用的RT-thread系統必須要添加的中斷處理程序,若各位沒有加RT-THREAD,此處可以刪除/* 檢查到奇偶校驗錯誤中斷使能 */if (((std_usart_get_cr1_interrupt_enable(USART1, USART_CR1_INTERRUPT_PE)) && (std_usart_get_flag(USART1, USART_FLAG_PE))) != RESET){std_usart_clear_flag(USART1, USART_FLAG_PE);}/* USART 錯誤中斷(幀錯誤,噪聲錯誤,溢出錯誤) */if (((std_usart_get_cr1_interrupt_enable(USART1, USART_CR3_INTERRUPT_ERR)) && (std_usart_get_flag(USART1, USART_CR3_INTERRUPT_ERR))) != RESET){std_usart_clear_flag(USART1, USART_CR3_INTERRUPT_ERR);}/* USART 空閑中斷 */if (((std_usart_get_cr1_interrupt_enable(USART1, USART_CR1_INTERRUPT_IDLE)) && (std_usart_get_flag(USART1, USART_FLAG_IDLE))) != RESET){std_usart_clear_flag(USART1, USART_CLEAR_IDLE);rt_sem_release(uart1_rx_check_sem);//這里釋放了對應的信號量(對應裸機的標記為置1)
#ifdef DEBUG_OUTPUT_SELECT//這里是博主自己項目中添加的一些調試內容,可以省略rt_sem_release(uart1_rx_ok_sem);
#endif}/* leave interrupt */rt_interrupt_leave();//這是博主使用的RT-thread系統必須要添加的中斷處理程序,若各位沒有加RT-THREAD,此處可以刪除
}/*** @brief DMA通道0中斷服務函數 UART1 RX* @retval 無*/
void DMA_Channel0_IRQHandler(void)
{/* enter interrupt */rt_interrupt_enter();//這是博主使用的RT-thread系統必須要添加的中斷處理程序,若各位沒有加RT-THREAD,此處可以刪除if ((std_dma_get_interrupt_enable(UART1_DMA_RX_CHANNEL, DMA_INTERRUPT_TH)) && (std_dma_get_flag(DMA_FLAG_TH0))){std_dma_clear_flag(DMA_CLEAR_TH0);rt_sem_release(uart1_rx_check_sem);}if ((std_dma_get_interrupt_enable(UART1_DMA_RX_CHANNEL, DMA_INTERRUPT_TF)) && (std_dma_get_flag(DMA_FLAG_TF0))){std_dma_clear_flag(DMA_CLEAR_TF0);rt_sem_release(uart1_rx_check_sem);}if ((std_dma_get_interrupt_enable(UART1_DMA_RX_CHANNEL, DMA_INTERRUPT_TE)) && (std_dma_get_flag(DMA_FLAG_TE0)))//這里就是DMA的數據傳輸錯誤,可以編寫對應的錯誤處理程序{std_dma_clear_flag(DMA_CLEAR_TE0);}/* Implement other events when needed *//* leave interrupt */rt_interrupt_leave();//這是博主使用的RT-thread系統必須要添加的中斷處理程序,若各位沒有加RT-THREAD,此處可以刪除
}/*** @brief DMA通道1中斷服務函數* @retval 無*/
void DMA_Channel1_IRQHandler(void)
{/* enter interrupt */rt_interrupt_enter();//這是博主使用的RT-thread系統必須要添加的中斷處理程序,若各位沒有加RT-THREAD,此處可以刪除/* DMA傳輸完成中斷服務 */if ((std_dma_get_interrupt_enable(UART1_DMA_TX_CHANNEL, DMA_INTERRUPT_TF)) && (std_dma_get_flag(DMA_FLAG_TF1))){std_dma_interrupt_disable(UART1_DMA_TX_CHANNEL, DMA_INTERRUPT_TF); // 發送完成關閉DMA通道中斷std_dma_clear_flag(DMA_CLEAR_TF1);lwrb_skip(&Usart1.uart1_tx_rb, Usart1._uart1_tx_dma_current_len); /* Skip buffer, it has been successfully sent out */Usart1._uart1_tx_dma_current_len = 0; /* Reset data length */}/* leave interrupt */rt_interrupt_leave();//這是博主使用的RT-thread系統必須要添加的中斷處理程序,若各位沒有加RT-THREAD,此處可以刪除
}
4.4 DMA的發送
? ? ? ? 發送功能比較簡單,由于是單次發送,DMA傳輸完成一次后,只需將發送緩沖區更換,即換源后重新啟動DMA的發送即可實現無阻塞性發送。DMA是外設進行的數據搬運不經過CPU,由此DMA的發送才叫無阻塞性的發送。
? ? ? ? 具體源碼如下:
/*** \brief Check if DMA is active and if not try to send data* \return `1` if transfer just started, `0` if on-going or no data to transmit*/
static uint8_t _UART1_StartTxDMATransfer(void)
{uint8_t started = 0;rt_enter_critical(); // 調度器上鎖,保證DMA數據的正常發送(沒有操作系統的這里可以省略)if (Usart1._uart1_tx_dma_current_len == 0 && (Usart1._uart1_tx_dma_current_len = lwrb_get_linear_block_read_length(&Usart1.uart1_tx_rb)) > 0)//這里是用于保證環形隊列的發送緩沖區的線性安全{/* Disable channel if enabled */std_dma_disable(UART1_DMA_TX_CHANNEL); // 如果DMA通道只給串口1使用,那只需要在初始化使能就行,不需要關閉std_usart_dma_tx_disable(USART1);/* Clear all flags */std_dma_clear_flag(DMA_CLEAR_TF1);Uart1_Dma_Send_Data(lwrb_get_linear_block_read_address(&Usart1.uart1_tx_rb), Usart1._uart1_tx_dma_current_len);std_dma_interrupt_enable(DMA_CHANNEL_1, DMA_INTERRUPT_TF); // 發送時打開DMA通道中斷/* enable transfer */std_dma_enable(UART1_DMA_TX_CHANNEL);std_usart_dma_tx_enable(USART1);started = 1;}rt_exit_critical();return started;
}rt_uint32_t UART1_Write(const void *data, rt_size_t len)
{rt_uint32_t ret = 0;//獲取用于寫操作的緩沖區可用大小 是否大于需要寫入的數據長度if (lwrb_get_free(&Usart1.uart1_tx_rb) >= len){ret = lwrb_write(&Usart1.uart1_tx_rb, data, len);_UART1_StartTxDMATransfer(); /* Then try to start transfer */}return ret;
}
4.5 DMA接收數據
? ? ? ?這里接收數據的處理是輪詢檢測串口的接收狀態,再根據DMA傳輸數據的位置情況,將DMA緩沖區中的數據寫入到對應的環形隊列中。
? ? ? ? 具體源碼如下:
/*** \brief Process received data over UART1* Data are written to RX ringbuffer for application processing at latter stage* \param[in] data: Data to process* \param[in] len: Length in units of bytes*/
rt_inline void _UART1_ProcessData(const void *data, size_t len)
{/* Write data to receive buffer *///將接收到的數據寫入到接收的環形隊列中lwrb_write(&Usart1.uart1_rx_rb, data, len);
}static void _UART1_RxCheck(void)
{static size_t old_pos;size_t pos;/* Calculate current position in buffer and check for new data available */pos = LWUTIL_ARRAYSIZE(_uart1_rx_dma_buffer) - std_dma_get_transfer_data_number(UART1_DMA_RX_CHANNEL);//計算緩沖區中的當前位置并檢查可用的新數據// RTT_LOG_D("std_dma_get_transfer_data_number(DMA_CHANNEL_0): %d", std_dma_get_transfer_data_number(DMA_CHANNEL_0));/* Check change in received data */if (pos != old_pos)//說明串口接收到了對應的數據{/* Current position is over previous one */if (pos > old_pos){//_uart1_rx_dma_buffer就是DMA搬運數據的目的地址_UART1_ProcessData(&_uart1_rx_dma_buffer[old_pos], pos - old_pos);}else{_UART1_ProcessData(&_uart1_rx_dma_buffer[old_pos], LWUTIL_ARRAYSIZE(_uart1_rx_dma_buffer) - old_pos);if (pos > 0){_UART1_ProcessData(&_uart1_rx_dma_buffer[0], pos);}}old_pos = pos; /* Save current position as old for next transfers */}
}//這里是在線程中輪詢檢測串口數據的接收情況
static void Uart1_Rx_Thread_Entry(void *parameter)
{// RTT_LOG_D("Uart1_Rx_Thread_Entry");char buf[128];uint8_t len;while (1){//這相當于是裸機狀態下判斷數據是否接收完成的標記rt_sem_take(uart1_rx_check_sem, RT_WAITING_FOREVER);_UART1_RxCheck();}
}
? ? ? ? 上述內容將串口接收到的數據寫入到了接收的環形隊列中,再進行數據解析時將環形隊列中的數據讀出即可,這便實現串口數據的無阻塞性接收。(同時我們應在對應的程序中及時處理串口接收到的數據,若長時間不讀環形隊列中的數據,后續在dma的傳輸中,會將未及時讀取的數據覆蓋,最終造成數據丟失)
5.總結
? ? ? ? 環形隊列實現無阻塞性接收的原理就是,利用串口接收數據的時間間隙,處理存儲在環形隊列中的數據,循環往復的進行數據接收。
? ? ? ? 以上內容則是Lwrb環形隊列實現DMA串口無阻塞性收發的實現。
? ? ? ? 同時推理其他協議的無阻塞性接收也可以通過環形隊列實現,其原理都是相同的。
各位對于上述Lwrb環形隊列有不懂的地方,可以加博主的聯系方式相互交流。