打印

USART+DMA+循环队列接收不定长数据

[复制链接]
486|7
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本文将介绍基于串口+DMA循环模式+循环队列的接收过程。其最大的优点是每次接收完成后不需要禁止MDA通道然后重新配置接收长度。并且还支持接收不定长数据。

当本机串口检测到IDLE中断时,只是说明对方已经结束了一串连续的串口帧发送过程,而不一定说明对方已经完成了一包应用层数据的发送。首先串口通信本身没有规定串口帧之间的间隔时间,可以没有间隔,或者可以有任意长间隔。其次,对方在发送一包应用层数据的时候,可能在发送过程中被中断打断,导致发送产生间隔,从而会让本机产生IDLE中断。

在DMA接收方式下,IDLE中断用于告诉应用程序,对方已经完整发送了一包数据,或者,对方只发送了一包数据的一部分。因此,在IDLE中断发生时,应该检查收到数据的完整性,只有收到了完整的数据,才去分析处理这个数据。最好的做法是在应用层设计一个串口通信协议,使用帧头和帧尾以及校验来协助检查数据的完整性和合法性。

我们使用IDLE中断而不是RBNE中断,是因为在接收一包应用层数据时,发生IDLE中断的次数远低于RXNE中断,这样接收起来就更高效,在最好的情况下,接收一包数据只会发生一次IDLE中断。而且在对方彻底完成一包数据的发送后,一定会产生IDLE中断。最重要的原因是,我们要接收不定长数据,则不能使用DMA的传输完成中断。

也可以使用接收超时中断替换IDLE中断。如果你的串口支持接收超时中断(RT)则更好,因为接收超时中断可以设置接收超时时间阈值,而IDLE的超时时间固定为一个串口帧的时间,RT比IDLE更加灵活。

我们也不需要使用DMA的接收完成中断。整个接收过程只需要使用IDLE中断(或者接收超时中断)。

使用特权

评论回复
沙发
花间一壶酒sd|  楼主 | 2023-9-30 19:41 | 只看该作者
DMA的循环模式
关于DMA循环模式

使用循环模式的好处是,当指定长度的串口数据通过DMA接收完成后,DMA硬件自动重新设置传输长度,同时开启下一个接收过程,当接收缓冲区满了后,会自动从接收缓冲区数组的开始存储。接收过程不会停止,因为DMA通道总是处于使能状态。否则,我们需要在每次传输完成后,通过代码禁止DMA通道,配置下一次的传输长度,最后使能通道,而在这个过程,需要CPU的介入,最严重的问题是,可能错过串口数据的接收导致丢数据。

使用特权

评论回复
板凳
花间一壶酒sd|  楼主 | 2023-9-30 19:41 | 只看该作者
循环队列缓冲区
循环队列就是将数组的首位在逻辑上连接起来,臆造成环形形态。

如下图所示,假设DMA接收缓冲区定义为字节数组DMA_buf,长度为6,用DMA_RX_BUF_SIZE表示。把接收缓冲区当做一个循环队列来使用:定义2个循环队列头指针和尾指针数组索引变量front和rear,用于指示当前接收的串口数据在DMA接收缓冲数组中的起始和结束位置。

使用特权

评论回复
地板
花间一壶酒sd|  楼主 | 2023-9-30 19:41 | 只看该作者
front初始化为0,且在处理收到的串口数据包时更新:每次取一个队列字节数据后,更新为:front = (front+1)%DMA_RX_BUF_SIZE。软件从缓冲区取数据实现了出队操作。

使用特权

评论回复
5
花间一壶酒sd|  楼主 | 2023-9-30 19:41 | 只看该作者
每次IDLE中断时,代表收到一包数据,在中断中可以计算出rear的值,rear=DMA_RX_BUF_SIZE -  DMA通道的剩余传输长度CNT寄存器的值。DMA硬件将串口数据放到缓冲区实现了入队操作。

使用特权

评论回复
6
花间一壶酒sd|  楼主 | 2023-9-30 19:42 | 只看该作者
有了front和rear,就可以计算出本次接收到的数据的长度:len = (DMA_RX_BUF_SIZE  - front + rear) % DMA_RX_BUF_SIZE 。同时需要注意,采用这种计算接收长度的算法时,能正确识别的最大长度为缓冲区长度-1,因为可以发现,当接收长度刚好等于DMA_RX_BUF_SIZE  时,计算出的长度为0。所以如果你的应用层最大的数据报文长度为MAX_LEN,则应该将接收缓冲区数组长度定义为DMA_RX_BUF_SIZE  = MAX_LEN+1。另外,由于数据处理和接收都是在同一个缓冲区进行的,所以如果存在处理不及时的情况(处理慢接收快)则新接收的数据可能会覆盖正在处理的数据导致错乱,因此在这种情况下建议将接收缓冲区扩大为2倍,这样接收缓冲数组长度为DMA_RX_BUF_SIZE  = (MAX_LEN+1)*2。

使用特权

评论回复
7
花间一壶酒sd|  楼主 | 2023-9-30 19:42 | 只看该作者
图(a):现在接收到了3个字符(A,B,C)

图(b):现在接收到了2个字符(h,g)

图(c):现在接收到了3个字符(T,N,Z)

使用特权

评论回复
8
花间一壶酒sd|  楼主 | 2023-9-30 19:42 | 只看该作者
代码实现:GD32F303的USART0+keil
#include "gd32f30x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>


void RCU_config(void)
{
        rcu_periph_clock_enable(RCU_GPIOA);
        rcu_periph_clock_enable(RCU_GPIOB);
        rcu_periph_clock_enable(RCU_USART0);
        rcu_periph_clock_enable(RCU_DMA0);
}


void NVIC_config(void)
{
        nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);
        nvic_irq_enable(USART0_IRQn,1,0);
}


void GPIO_config(void)
{
        //PA9 USART0_Tx
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
    //PA10 USART0_Rx
    gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
}

#define USART0_DMA_TXBUF_SIZE  100
#define USART0_DMA_RXBUF_SIZE  100
uint8_t USART0_DMA_txbuf[USART0_DMA_TXBUF_SIZE];
uint8_t USART0_DMA_rxbuf[USART0_DMA_RXBUF_SIZE];

void USART0_config(void)
{
        usart_deinit(USART0);
    usart_baudrate_set(USART0, 115200U);
       
        //--接收超时设置
        usart_receiver_timeout_threshold_config(USART0,100);
        usart_interrupt_enable(USART0,USART_INT_RT);
        usart_receiver_timeout_enable(USART0);
       
        //=====配置USART使用的DMA通道======
        //USART0_TX:DMA0_CH3
        dma_parameter_struct dma_init_struct;  //DMA初始化结构体
        dma_deinit(DMA0, DMA_CH3);        //复位DMA通道为默认配置
        dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL;   //传输方向
        dma_init_struct.memory_addr = (uint32_t)USART0_DMA_txbuf; //存储器基地址
        dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //增量式存储器地址生成模式
        dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;    //存储器单个数据宽度
        dma_init_struct.number = 0;                             //传输长度
        dma_init_struct.periph_addr = (uint32_t)&(USART_DATA(USART0)); //外设基地址(寄存器地址)
        dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //增量式外设地址生成模式
        dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; //外设单个数据宽度
        dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;       //通道的优先级
        dma_init(DMA0, DMA_CH3, &dma_init_struct);               //进行配置
        dma_circulation_disable(DMA0, DMA_CH3);          //配置循环传输模式(单次模式和循环模式)
        //dma_interrupt_enable(DMA0,DMA_CH3,DMA_INT_FTF);  //DMA中断使能
        //dma_channel_enable(DMA0, DMA_CH3);               //使能DMA通道
       
       
        //USART0_RX:DMA0_CH4
        dma_deinit(DMA0, DMA_CH4);        //复位DMA通道为默认配置
        dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY;   //传输方向
        dma_init_struct.memory_addr = (uint32_t)USART0_DMA_rxbuf; //存储器基地址
        dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //增量式存储器地址生成模式
        dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;    //存储器单个数据宽度
        dma_init_struct.number = USART0_DMA_RXBUF_SIZE;          //传输长度
        dma_init_struct.periph_addr = (uint32_t)&(USART_DATA(USART0)); //外设基地址(寄存器地址)
        dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //增量式外设地址生成模式
        dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; //外设单个数据宽度
        dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;       //通道的优先级
        dma_init(DMA0, DMA_CH4, &dma_init_struct);               //进行配置
        dma_circulation_enable(DMA0, DMA_CH4);          //配置循环传输模式(单次模式和循环模式)
        //dma_interrupt_enable(DMA0,DMA_CH4,DMA_INT_FTF);  //DMA中断使能
        dma_channel_enable(DMA0, DMA_CH4);               //使能DMA通道
       
        //=====使能串口的DMA发送和接收=====
        usart_dma_transmit_config(USART0, USART_DENT_ENABLE); //使能串口的DMA发送
        usart_dma_receive_config(USART0,USART_DENR_ENABLE);  //使能串口的DMA接收
       
       
    usart_receive_config(USART0, USART_RECEIVE_ENABLE);
    usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
    usart_enable(USART0);
       
}


void USART0_printf(const char*format , ...)
{
        int tx_len;
        va_list args;
       
        //如果dma传输通道是使能的,且传输没完成
        while((DMA_CHCTL(DMA0,DMA_CH3)&0x01) &&  (!dma_flag_get(DMA0,DMA_CH3,DMA_FLAG_FTF)));  //等待DMA传输完成
        dma_flag_clear(DMA0,DMA_CH3,DMA_FLAG_FTF);       //清除传输完成标志
       
        va_start(args,format);
        tx_len = vsnprintf((char*)USART0_DMA_txbuf,USART0_DMA_TXBUF_SIZE,format,args);
        va_end(args);
       
        if(tx_len>0)
        {
                dma_channel_disable(DMA0,DMA_CH3);     //关闭DMA通道,这样才能设置传输长度
                dma_transfer_number_config(DMA0,DMA_CH3,tx_len); //设置本次DMA传输长度
                dma_channel_enable(DMA0,DMA_CH3);     //使能USART0_TX使用的DMA通道,开始DMA传输               
        }
}


volatile uint32_t USART0_get_msg=0;
volatile uint32_t USART0_rear=0;   
volatile uint32_t USART0_front=0;   

int main(void)
{
    RCU_config();
        NVIC_config();
        GPIO_config();
        USART0_config();
       
       
    while(1)
    {
                if(USART0_get_msg)
                {
                        USART0_get_msg=0;
                       
                        uint32_t len = (USART0_DMA_RXBUF_SIZE - USART0_front + USART0_rear)%USART0_DMA_RXBUF_SIZE;//本次消息长度
                        USART0_printf("\nlen=%u:",len);
                        //打印出收到的字符
                        for(uint32_t i=0;i<len;i++)
                        {
                                USART0_printf("[%u]%c ",i,USART0_DMA_rxbuf[USART0_front]);  //处理第i个字节
                                USART0_front = (USART0_front+1)%USART0_DMA_RXBUF_SIZE;  //更新front指针
                        }
                       
                }
               
    }
}


void USART0_IRQHandler(void)
{
        //接收超时中断(也可以使用IDLE中断实现)
        if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RT))
        {
                //清除中断标志
                usart_interrupt_flag_clear(USART0,USART_INT_FLAG_RT);
                //计算rear的值
                USART0_rear = USART0_DMA_RXBUF_SIZE - dma_transfer_number_get(DMA0,DMA_CH4);
                //通知main收到了一包数据
                USART0_get_msg=1;
        }

}

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

80

主题

997

帖子

2

粉丝