CIU32L051 DMA+Lwrb环形队列实现串口无阻塞性数据的收发 + 数据百分百不丢失的实现
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环形队列有不懂的地方,可以加博主的联系方式相互交流。