串口DMA接收数据基本思路

串口DMA接收基本思路

串口DMA接收数据基本思路

一、串口处理使用背景及常用处理方法

单片机经常有串口处理大量数据的场景,常用的串口处理数据方式有如下方式:

a.串口接收中断接收 + 串口阻塞发送,即使用串口接收中断对数据进行接收。然后处理完毕后在主程序中调用串口发送函数进行发送。该方式每接收一个数据便进入一次接收中断,且发送是阻塞发送,会阻碍其他处理操作运行。适合数据量不大的场景。

b.串口空闲中断(接收中断) + 环形缓冲区 + 串口阻塞发送,该方法使用串口空闲中断和接收中断配合环形缓冲区可以解决大部分的串口数据处理场景。适合数据量较大的场景。

c.串口中断 + DMA接收 + DMA发送,该方法使用了DMA可以释放CPU的占用,CPU由每次接收触发中断变成接收完毕触发或DMA发送完毕触发,减少中断触发频率。适合数据量较大的场景。

以上三种方式各有自己的优缺点,DMA串口尽管处理效率看起来是最高的,但是耗费了DMA资源。而DMA资源往往是单片机较为稀缺的资源。因此还是需要根据项目需求合理选择。本次主要是在资源富裕的情况下使用DMA进行串口的收发测试。

二、串口DMA接收相关思路

DMA串口接收,大致流程如上图所示;因为本次使用的是STM32F1系列的DMA,只有传送任务完成一半的中断(DMA1_IT_HTx )。因此借用HTx中断实现双缓冲区效果进行接收。[本篇只针对串口DMA进行解析流程,DMA相关基础知识请参考其他文章]。

如上图中,需要准备一个缓冲区(即用户自己管理的数组),这个数组需要在程序编译后有确定的地址(不要使用会被析构掉的数组地址)。原因是:开启DMA传送后,DMA传送是不经过CPU的直接地址之间的快速传送。此时CPU也会同步正常运行,而如果该地址会因CPU程序运行用作别的功能使用,则会使DMA传送数据出错。

简单点来说,串口接收DMA传送过程类似:CPU跟DMA控制器下发一个指令说,等串口收到数据了,你把收到的数据搬运到仓库里存着(发送缓冲区),这次就先搬运100个。CPU下完这个指令后,就能去执行别的指令处理任务,DMA同时也会开始搬运的工作;这样就可以实现CPU等100个数据都接收完了,再一次性处理,降低了CPU一个一个数据处理的工作量。这里的等串口收到数据了,就是DMA请求,串口每接收完毕一个数据,就会通知DMA,我收到了一个数据,你可以来搬运了。而100个就是DMA传送的数量,即这次DMA只搬运100个数据就完成本次DMA传送。

这样的好处就是,CPU只在下发DMA指令的时候占用一瞬间,便可以去干别的工作,等DMA传送完毕之后再进行数据批量处理,极大减少了CPU占用的时间。那么想想一下如下的场景,假如一次DMA传送完成了,CPU正在处理数据的时间,串口又来了新数据,这时候因为数据还没处理完,发送缓冲区还不能释放出来进行下一次DMA接收,这个时候就会导致数据丢失。于是这种机制还需要进一步改进。

假如我们将接收缓冲区Buffer人工区分成两个片区,CPU要一次性处理100个数据,分为每次处理50个数据,再借助半满中断就能实现如上图的过程。

1、CPU下发DMA传送指令,传送100个数据。此时设置接收缓冲区大小为100个。

2、当接收到50个数据时,即接收的数据填满接收缓冲区前50个数据时,进入DMA半满中断。在半满中断中通知CPU将接收到的数据搬入FIFO中(此时DMA仍旧在向后50个地址写入数据)。

3、当接收到100个数据时,进入DMA完成中断。在DMA完成中断中,首先重置DMA继续进入下一轮的100个数据接收状态(这时由于接收缓冲区前50个地址数据已经被搬入FIFO中,可以直接使能DMA进行继续接收),然后CPU将后50个数据搬入FIFO中。

4、CPU在前台对FIFO中数据进行逐一处理。

可以看到,如果整体流程变为这样时,CPU提前对数据进行搬运,耗费一定CPU资源,但能够做到串口丢数据最少的操作。当然这样能够持续进行的前提一定是CPU将数据搬入FIFO的时间,要小于DMA将数据搬入数组的时间。以串口波特率为115200(bit/s)为例,数据位8位,停止位1位,总计一个有效数据为9位。接收1个数据耗时:1/115200 * 9 = 7.8e-5 (该时间还未加上DMA搬运时间);而单片机假如按照1M的速率进行处理1Byte数据,也能到达1e-6速率。因此CPU肯定是能够在DMA传送完毕之前,完成前半个缓冲区域的数据处理。

即使这样,目前还剩最后一个问题需要解决;假设我们串口目前只有80个数据需要接收,按照我们优化后的程序去执行,想想会发生什么?由于我们CPU给DMA下发的是接收100个数据才完成。在接收到50个数据时,进入半满中断搬运;然后继续接收后面的50个数据。显而易见,DMA只能再接收到30个数据,便再也等不到数据了。这时会一直等不到DMA完成中断,等不到DMA完成中断,DMA接收的数据便无法被存在入用户FIFO中。造成数据本次不能完整的接收(只有再接收20个数据,凑满100个才能触发搬运)。

为了解决该问题,需要加入串口空闲中断,即在串口空闲时,通知CPU来将数据搬运到FIFO中,同时重置DMA接收。这样便能接收不定长的数据,下一次数据变成30个也能完成接收。而串口空闲中断需要的信息有:当前接收缓冲区的首地址(是起始地址?还是一半的地址?)和需要搬移的数量。缓冲区首地址 可以通过变量进行记录,DMA启动后起始地址为0,DMA半满中断触发后起始地址为缓冲区的一半。搬移数据量可以通过DMA计数器获得,DMA内部存在一个计数器,比如我们这里给的是初值100,DMA控制器每搬运一个,该值就会自减1,如变为99,98,97......。计数器变为50,触发半满中断;直到计数器变为0,则触发DMA完成中断。这样我们便可以通过获取该计数器值来计算本次DMA已经传送了多少数据。如上面所说的80个数据,此时计算规则为:100-20-50 = 30;100为DMA总传送数量;20为当前DMA计数器的值;50为buffer的起始地址(前50个数据已经传送完毕),程序部分如下:

c 复制代码
#define USART1_DMA_BUF_MAX_LEN 128
#define USART1_DMA_FIFO_MAX_LEN 512

_fifo_t   dma_rx_fifo_uart1;
uint8_t   dma_uart1_fifo_buf[USART1_DMA_FIFO_MAX_LEN]={0};
uint8_t   dma_uart1_rx_buf[USART1_DMA_BUF_MAX_LEN] = {0};
uint8_t   dma_uart1_tx_buf[USART1_DMA_BUF_MAX_LEN] = {0x01,0x02};
uint8_t   lock_state = 0;

uart_dev uart_dev_uart1 =
{
     USART1 ,   //串口外设1
     115200 ,   //buad
     &dma_rx_fifo_uart1,  //DMA_RX_FIFO
     dma_uart1_rx_buf  ,  //DMA_RX_BUF
     dma_uart1_tx_buf     //DMA_TX_BUF
};



/**
  * @brief  初始化串口GPIO
  * @param  
  * @retval 0:成功; 1:失败
*/
uint32_t uart_dev_gpio_init()
{
    GPIO_InitTypeDef  gpio_initstruct;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
   //PA9	TXD
    gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;
	gpio_initstruct.GPIO_Pin = GPIO_Pin_9;
	gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &gpio_initstruct);
	
	//PA10	RXD
	gpio_initstruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	gpio_initstruct.GPIO_Pin = GPIO_Pin_10;
	gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &gpio_initstruct);
        
    return 0;
}



/**
  * @brief  初始化串口外设
  * @param  
  * @retval 0:成功; 1:失败
*/
uint32_t uart_dev_uart_init (uart_dev* uart_dev)
{  
    USART_InitTypeDef usart_initstruct;
	NVIC_InitTypeDef  nvic_initstruct;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    
    usart_initstruct.USART_BaudRate = uart_dev ->baud;
	usart_initstruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//无硬件流控
	usart_initstruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;						//接收和发送
	usart_initstruct.USART_Parity = USART_Parity_No;									//无校验
	usart_initstruct.USART_StopBits = USART_StopBits_1;								    //1位停止位
	usart_initstruct.USART_WordLength = USART_WordLength_8b;							//8位数据位
	USART_Init(uart_dev->USARTx, &usart_initstruct);
    
    USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_RXNE);                       //清除默认标志位(主要是TC)
    USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);                                  //串口DMA接收使能
    USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);                                  //串口DMA发送使能
    USART_Cmd(USART1, ENABLE);													  //使能串口
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);					              //使能空闲中断
    
    nvic_initstruct.NVIC_IRQChannel = USART1_IRQn;
	nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
	nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 0;
	nvic_initstruct.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&nvic_initstruct);
    
    
    return 0;
}

/**
  * @brief  初始化串口DMA
  * @param  
  * @retval 0:成功; 1:失败
*/
uint32_t uart_dev_DMA_init(uart_dev* uart_dev)
{
    NVIC_InitTypeDef  nvic_initstruct;
	DMA_InitTypeDef   DMA_USART1_InitStructure;
    
    RCC_AHBPeriphClockCmd (RCC_AHBPeriph_DMA1,ENABLE);
    
    DMA_DeInit(DMA1_Channel5);
    DMA_USART1_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &USART1->DR           ;    //USART1数据接收寄存器
    DMA_USART1_InitStructure.DMA_MemoryBaseAddr     = (uint32_t) uart_dev ->dma_rx_buff;    //接收寄存器区间地址
    DMA_USART1_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC      ;          //从外设读取; 外设 ------> 内存
	DMA_USART1_InitStructure.DMA_BufferSize         = USART1_DMA_BUF_MAX_LEN     ;          //传送长度
    DMA_USART1_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable  ;          //外设地址不增
    DMA_USART1_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable       ;          //内存地址自增
    DMA_USART1_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;          //传送数据为8位
    DMA_USART1_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte    ;          //MemoryDataSize
    DMA_USART1_InitStructure.DMA_Mode               = DMA_Mode_Normal            ;          //普通模式 单次
    DMA_USART1_InitStructure.DMA_Priority           = DMA_Priority_VeryHigh      ;          //传送优先级非常高
    DMA_USART1_InitStructure.DMA_M2M                = DMA_M2M_Disable            ;          //从外设触发
    DMA_Init(DMA1_Channel5,&DMA_USART1_InitStructure);
    
    //开启DMA
    DMA_Cmd(DMA1_Channel5,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel5, USART1_DMA_BUF_MAX_LEN);
    DMA_Cmd(DMA1_Channel5,ENABLE);
    
    DMA_ClearFlag(DMA1_IT_TC5|DMA1_IT_HT5); 
    DMA_ITConfig(DMA1_Channel5,DMA_IT_HT|DMA_IT_TC|DMA_IT_TE,ENABLE);  //使能传送一半及全部传送完毕中断
    
    nvic_initstruct.NVIC_IRQChannel = DMA1_Channel5_IRQn;
	nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
	nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 0;
	nvic_initstruct.NVIC_IRQChannelSubPriority = 2;
	NVIC_Init(&nvic_initstruct);   
    
    return 0;
}


/**
  * @brief  清除DMAx通道
  * @param  
  * @retval 0:成功; 1:失败
*/

uint32_t uart_dev_clear_dma_channel(DMA_Channel_TypeDef* DMAy_Channelx)
{
    
    DMA_Cmd(DMAy_Channelx,DISABLE);
    DMA_SetCurrDataCounter(DMAy_Channelx,USART1_DMA_BUF_MAX_LEN);
    DMA_Cmd(DMAy_Channelx,ENABLE);
      
    return 0;
}



/*
************************************************************
*	函数名称:	USART1_IRQHandler
*
*	函数功能:	串口1收发中断
*
*	入口参数:	无
*
*	返回参数:	无
*
*	说明:		
************************************************************
*/
void USART1_IRQHandler(void)
{
    uint8_t temp = 0;
    uint16_t recv_size = 0;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
	{
        temp = USART1->DR;
        
		USART_ClearFlag(USART1, USART_FLAG_RXNE);
	}
    else if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //空闲中断
    {
        
        temp = USART1->SR; //清除中断标志
        temp = USART1->DR;
        USART_ClearFlag(USART1, USART_IT_IDLE);
        
        recv_size = USART1_DMA_BUF_MAX_LEN - DMA_GetCurrDataCounter(DMA1_Channel5) - uart_dev_uart1.dma_rx_buff_curr_addr; 
        //重置DMA
        uart_dev_clear_dma_channel(DMA1_Channel5);
        
        fifo_write(uart_dev_uart1.dma_rx_fifo, &uart_dev_uart1.dma_rx_buff[uart_dev_uart1.dma_rx_buff_curr_addr], recv_size);
        uart_dev_uart1.dma_rx_buff_curr_addr = 0;     
        
    }

}

/*
************************************************************
*	函数名称:	DMA1_Channel5_IRQHandler(串口接收中断)
*
*	函数功能:	DMA1_Channel5中断
*
*	入口参数:	无
*
*	返回参数:	无
*
*	说明:		
************************************************************
*/
void DMA1_Channel5_IRQHandler(void)
{
    uint16_t recv_size = 0;
    
    if(DMA_GetITStatus(DMA1_IT_HT5) != RESET)   //DMA传送一半中断
    {
        
        DMA_ClearITPendingBit(DMA1_IT_HT5);    // 清除传输传送一半中断中断标志位	
        
        //搬移前半个buf数据
        recv_size = USART1_DMA_BUF_MAX_LEN - DMA_GetCurrDataCounter(DMA1_Channel5); 
        
        fifo_write(uart_dev_uart1.dma_rx_fifo, uart_dev_uart1.dma_rx_buff, recv_size); 
        
        //更新当前地址
        uart_dev_uart1.dma_rx_buff_curr_addr += recv_size;

    }
    else if(DMA_GetITStatus(DMA1_IT_TC5) != RESET)  //DMA传送完成中断
    {
        
        DMA_ClearITPendingBit(DMA1_IT_TC5);    // 清除传输完成中断标志位	
                
        recv_size = USART1_DMA_BUF_MAX_LEN - DMA_GetCurrDataCounter(DMA1_Channel5) - uart_dev_uart1.dma_rx_buff_curr_addr;      
        
        //重置DMA
        uart_dev_clear_dma_channel(DMA1_Channel5);
                
        fifo_write(uart_dev_uart1.dma_rx_fifo, &uart_dev_uart1.dma_rx_buff[uart_dev_uart1.dma_rx_buff_curr_addr], recv_size);
        
        uart_dev_uart1.dma_rx_buff_curr_addr = 0;
        
    }else if(DMA_GetITStatus(DMA1_IT_TE5) != RESET)
    {
        UsartPrintf(USART2," DMA ERROR \r\n");
    }

}

三、串口DMA发送相关思路

如果理解了发送原理,接收原理相对来说便简单的多;将需要发送的数据填入发送buff,通知DMA将数据进行发送便可以实现。这里需要注意的是因为串口接收和发送DMA同属于一个DMA的不同通道,两个是没法同时进行的,如果是收发双工同时使用这一个DMA,可能会地效率有所影响。

需要注意的是,因为发送是非阻塞进行的;一定要获取该次发送完成后,再进行下一次发送;当然若发送大量数据可以仿照接收,也使用类似与双buffer的方式进行发送。

c 复制代码
/**
  * @brief  初始化串口DMA 并发送数据(发送串口)
  * @param  
  * @retval 
*/
void uart_dev_DMA_tx_data(uart_dev* uart_dev )
{
    NVIC_InitTypeDef  nvic_initstruct;
	DMA_InitTypeDef   DMA_USART1_InitStructure;
    
    //使能DMA1 时钟
    RCC_AHBPeriphClockCmd (RCC_AHBPeriph_DMA1,ENABLE);
    
    DMA_DeInit(DMA1_Channel4);
    DMA_USART1_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &USART1->DR           ;    //USART1数据接收寄存器
    DMA_USART1_InitStructure.DMA_MemoryBaseAddr     = (uint32_t) uart_dev ->dma_tx_buff;    //发送寄存器区间地址
    DMA_USART1_InitStructure.DMA_DIR                = DMA_DIR_PeripheralDST            ;  //往外设发送; 内村 ------> 外设
	DMA_USART1_InitStructure.DMA_BufferSize         = USART1_DMA_BUF_MAX_LEN           ;  //传送长度
    DMA_USART1_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable  ;  //外设地址不增
    DMA_USART1_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable       ;  //内存地址自增
    DMA_USART1_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  //传送数据为8位
    DMA_USART1_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte    ;  //MemoryDataSize
    DMA_USART1_InitStructure.DMA_Mode               = DMA_Mode_Normal            ;  //普通模式 单次
    DMA_USART1_InitStructure.DMA_Priority           = DMA_Priority_High          ;  //传送优先级高
    DMA_USART1_InitStructure.DMA_M2M                = DMA_M2M_Disable            ;  //从外设触发
    DMA_Init(DMA1_Channel4,&DMA_USART1_InitStructure);
   
    //开启DMA
    DMA_Cmd(DMA1_Channel4,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel4, USART1_DMA_BUF_MAX_LEN);
    DMA_Cmd(DMA1_Channel4,ENABLE);
   
    DMA_ClearFlag(DMA1_IT_TC4); 
    DMA_ITConfig(DMA1_Channel4,DMA_IT_TC|DMA_IT_TE,ENABLE);  //使能传送一半及全部传送完毕中断
    
    nvic_initstruct.NVIC_IRQChannel = DMA1_Channel4_IRQn;
	nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
	nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 0;
	nvic_initstruct.NVIC_IRQChannelSubPriority = 3;
	NVIC_Init(&nvic_initstruct);   
    
}

/*
************************************************************
*	函数名称:	DMA1_Channel4_IRQHandler
*
*	函数功能:	DMA1_Channel4中断
*
*	入口参数:	无
*
*	返回参数:	无
*
*	说明:		
************************************************************
*/

void DMA1_Channel4_IRQHandler(void)
{
    uint16_t recv_size = 0;
    
    if(DMA_GetITStatus(DMA1_IT_TC4) != RESET)   //DMA传送完成
    {
        uart_dev_uart1.dma_tx_idle_state = 1;
        
        DMA_ClearITPendingBit(DMA1_IT_TC4);    // 清除传输完成	
    }
    else if(DMA_GetITStatus(DMA1_IT_TE4) != RESET)
    {
        UsartPrintf(USART2," DMA uart tx ERROR \r\n");
    }
        
}
相关推荐
木宁kk3 分钟前
嵌入式 TCP/UDP/透传/固件
单片机·嵌入式硬件·面试
跳河轻生的鱼3 小时前
海思Linux(一)-Hi3516CV610的开发-ubuntu22_04环境创建
linux·单片机·学习·华为
pirateeee5 小时前
STM32G070CB的USART1_RX引脚
stm32·单片机·嵌入式硬件
韦东山5 小时前
NUTTX移植到STM32
stm32·单片机·嵌入式硬件·nuttx
桃子丫5 小时前
Howland电流源
嵌入式硬件·能源·智能硬件·硬件
老薛爱吃大西瓜5 小时前
MPU中断处理
c语言·单片机·嵌入式硬件
wenchm5 小时前
细说STM32F407单片机FSMC连接外部SRAM的方法及HAL驱动
stm32·单片机·嵌入式硬件
厉昱辰6 小时前
一文读懂51单片机的中断系统
stm32·单片机·51单片机
折途6 小时前
【STC库函数】Compare比较器的使用
单片机·嵌入式硬件
不能只会打代码7 小时前
32单片机从入门到精通之软件编程——通信协议(十一)
单片机·嵌入式硬件·32单片机