stm32 利用DMA+串口空闲中断接受任意长数据
阅读原文时间:2021年04月20日阅读:1

目录

在进行stm32开发时,有时会遇到这种情况:需要在设备间进行数据传输,由于stm32串口RDR和TDR寄存器都是8位有效的,我们往往需要定义传输协议(如一帧数据中,包含包含帧头、帧ID、数据帧、校验帧等若干8位数据)。我们希望可以一次收到一帧数据,并进行解码操作。利DMA+串口空闲中断可以有效完成上述任务。

  • 基于stm32f4
  • DMA接受
  • 串口空闲中断

一、DMA

1、简介

  • DMA(直接存储器访问)是一种数据传输方法,利用DMA控制器,将数据直接从一个地址空间复制到另一个地址空间。
  • DMA在硬件ROM和IO设备间开辟直接传输数据的通道,不需要CPU主控芯片控制,也不需要类似中断处理那种保留现场&恢复现场的操作。这大大减小了CPU的负担。

2、使用场景

DMA用在只需要传输数据,不需要处理数据的地方,有三种传输方式:

  • 外设→存储器(例:从串口RDR寄存器写入某数据buf)
  • 存储器→外设(例:从某数据buf写入串口TDR寄存器)
  • 存储器→存储器(例:复制某特别大的数据buf)

3、主要特性

在中文参考手册9.2节详细说明了DMA特性

4、DMA控制器结构

  • stm32f4最多有:2个DMA控制器,各8个数据流,每个数据流有8个通道(或请求),每个通道有一个仲裁器,用于处理请求的优先级。如下图所示
  • 来自各个外设的DMA请求连接到8个通道(请求)上。DMA_SxCR寄存器中CHSEL[2:0]位域,控制某时刻是哪个外设连接到此数据流上。如下图所示:

5、DMA请求映射

  • 注意:如果要开启某外设的DMA传输,其库函数通常在该外设相应的.c文件中,在配置外设时注意开启。
  • DMA通道/请求外设不是任意连接的,不同的型号的芯片需要查询相应的映射表。如下:

6、指针递增

  • 在传输数据时,可以配置指向传输双方数据的指针是否自动向后递增。
  • 通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。

通常如下图配置:

方向

指针情况

外设 → 存储器

外设指针不变,存储buf指针递增

存储器 → 外设

存储buf指针不变, 外设指针递增

存储器 → 存储器

都递增

7、循环模式

  • 循环模式可用于处理循环缓冲区连续数据流(例如 ADC 扫描模式)。可以使用 DMA_SxCR寄存器中的 CIRC 位使能。文章最前提出的例子也适用此设置
  • 当激活循环模式时,要传输的数据项的数目在数据流配置阶段自动用设置的初始值进行加载,并继续响应 DMA 请求。

8、其他

关于DMA还有双缓冲区模式、突发传输等等其他设置,一般用不到,具体查询《stm32中文参考手册》

9、示例代码

文章开头情景的示例代码如下(DMA配置部分):

//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7
//chx:DMA通道选择,@ref DMA_channel DMA_Channel_0~DMA_Channel_7
//par:外设地址
//mar:存储器地址
//ndtr:数据传输量  
void DMA_Config(DMA_Stream_TypeDef *DMA_Streamx,uint32_t chx,uint32_t par,uint32_t mar,uint32_t dir,u16 ndtr)
{ 

    DMA_InitTypeDef  DMA_InitStructure;

    if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
    {
      RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能 

    }else 
    {
      RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能 
    }
  DMA_DeInit(DMA_Streamx);

    while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}//等待DMA可配置 

  /* 配置 DMA Stream */
  DMA_InitStructure.DMA_Channel                     = chx;                              //通道选择
  DMA_InitStructure.DMA_PeripheralBaseAddr             = par;                              //DMA外设地址
  DMA_InitStructure.DMA_Memory0BaseAddr             = mar;                              //DMA 存储器0地址
  DMA_InitStructure.DMA_DIR                         = dir;                              //direction of transmit.
  DMA_InitStructure.DMA_BufferSize                     = ndtr;                             //数据传输量 
  DMA_InitStructure.DMA_PeripheralInc                = DMA_PeripheralInc_Disable;        //外设非增量模式
  DMA_InitStructure.DMA_MemoryInc                     = DMA_MemoryInc_Enable;             //存储器增量模式
  DMA_InitStructure.DMA_PeripheralDataSize             = DMA_PeripheralDataSize_Byte;      //外设数据长度:8位
  DMA_InitStructure.DMA_MemoryDataSize                 = DMA_MemoryDataSize_Byte;          //存储器数据长度:8位
  DMA_InitStructure.DMA_Mode                         = DMA_Mode_Normal;                  // 使用普通模式 
  DMA_InitStructure.DMA_Priority                     = DMA_Priority_High;                //中等优先级
  DMA_InitStructure.DMA_FIFOMode                     = DMA_FIFOMode_Disable;         
  DMA_InitStructure.DMA_FIFOThreshold                 = DMA_FIFOThreshold_Full;
  DMA_InitStructure.DMA_MemoryBurst                 = DMA_MemoryBurst_Single;           //存储器突发单次传输
  DMA_InitStructure.DMA_PeripheralBurst             = DMA_PeripheralBurst_Single;       //外设突发单次传输
  DMA_Init(DMA_Streamx, &DMA_InitStructure);
  DMA_Cmd(DMA_Streamx,ENABLE);
} 

//开启一次DMA传输
void DMA_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
{

    DMA_Cmd(DMA_Streamx, DISABLE);                      //先关闭DMA,才能设置它

    while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}  //等待传输结束

    DMA_SetCurrDataCounter(DMA_Streamx,ndtr);          //设置传输数据长度 

    DMA_Cmd(DMA_Streamx, ENABLE);                      //开启DMA
}      

二、串口空闲中断

先看一下串口中断表

1、常用的串口接收中断

  • 简单的串口接受一般使用串口接受中断,对应事件标志为RXEN
  • 一旦发生中断,即可使USART_ReceiveData(USART_TypeDef* USARTx)函数接受最新收到的一位数据
  • 通过对 USART_DR 寄存器执行读入操作将RXNE位清零。也可以通过向该位写入零来清零。
  • 这种接受方式,需要自行编程实现数据帧识别,且无法使用DMA,速度较慢。

示例代码如下

void My_USART1_Init(void)
{
    GPIO_InitTypeDef GPIO_Initstructure;
    USART_InitTypeDef USART_Initstructure;
    NVIC_InitTypeDef NVIC_Initstrcuture;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE );
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA ,ENABLE );

    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);

    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Initstructure.GPIO_OType = GPIO_OType_PP;
    GPIO_Initstructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOA,&GPIO_Initstructure);
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_10;//IO³õʼ»¯RX
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Initstructure.GPIO_OType = GPIO_OType_PP;
    GPIO_Initstructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOA,&GPIO_Initstructure);

    USART_Initstructure.USART_BaudRate = 9600;
    USART_Initstructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Initstructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Initstructure.USART_Parity = USART_Parity_No;
    USART_Initstructure.USART_StopBits = USART_StopBits_1;
    USART_Initstructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1,&USART_Initstructure);

    USART_Cmd(USART1,ENABLE);

    USART_ITConfig (USART1,USART_IT_RXNE ,ENABLE);//开启串口接受中断

    NVIC_Initstrcuture.NVIC_IRQChannel = USART1_IRQn;
    NVIC_Initstrcuture.NVIC_IRQChannelCmd = ENABLE ;
    NVIC_Initstrcuture.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_Initstrcuture.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_Initstrcuture);

}

void USART1_IRQHandler(void)
{
    u8 res=0;
    if(USART_GetITStatus(USART1,USART_IT_RXNE))//串口非空标志位为1,收到数据
    {
        res = USART_ReceiveData(USART1);//读取最新一个收到的数据
        USART_SendData(USART1,res );//发送数据
    }
}

2、串口空闲中断

  • 串口空闲中断,对应事件标志为IDLE
  • 检测到空闲线路时,该位由硬件置 1。如果 USART_CR1 寄存器中 IDLEIE = 1,则会生成中断
  • 该位由软件序列清零(读入 USART_SR 寄存器,然后读入 USART_DR 寄存器

利用串口空闲中断,可以用如下流程实现DMA控制的任意长数据接收:

  • 0、开启串口DMA接收
  • 1、串口收到数据,DMA不断传输数据到存储buf
  • 2、一帧数据发送完毕,串口暂时空闲,触发串口空闲中断
  • 3、在中断服务函数中,可以计算刚才收到了多少个字节的数据
  • 4、解码存储buf,清除标志位,开始下一帧接收

示例代码如下:

void USART1_Init(uint32_t bound)//DMA2_Stream2
{
    GPIO_InitTypeDef GPIO_Initstructure;
    USART_InitTypeDef USART_Initstructure;
    NVIC_InitTypeDef NVIC_Initstrcuture;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE );
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA ,ENABLE );

    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);

    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Initstructure.GPIO_OType = GPIO_OType_PP;
    GPIO_Initstructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOA,&GPIO_Initstructure);
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Initstructure.GPIO_OType = GPIO_OType_PP;
    GPIO_Initstructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(GPIOA,&GPIO_Initstructure);

    USART_Initstructure.USART_BaudRate = bound;
    USART_Initstructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Initstructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Initstructure.USART_Parity = USART_Parity_No;
    USART_Initstructure.USART_StopBits = USART_StopBits_1;
    USART_Initstructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1,&USART_Initstructure);

    NVIC_Initstrcuture.NVIC_IRQChannel = USART1_IRQn;
    NVIC_Initstrcuture.NVIC_IRQChannelPreemptionPriority=1;
    NVIC_Initstrcuture.NVIC_IRQChannelSubPriority =2;       
    NVIC_Initstrcuture.NVIC_IRQChannelCmd = ENABLE;     
    NVIC_Init(&NVIC_Initstrcuture); 

//    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启接收中断
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启空闲中断
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);//开启DMA接收
    USART_Cmd(USART1, ENABLE);

    //initialize the DMA channel.
    DMA_Config(DMA2_Stream2,DMA_Channel_4, 
                         (uint32_t)&(USART1->DR),     //串口DR寄存器
                         (uint32_t)USART1_Rx_Buffer,//自定义的接收数据buf
                         DMA_DIR_PeripheralToMemory,//外设到存储器方向
                         USART1_RX_BUFFER_SIZE/2);//长度

}

void USART1_IRQHandler(void) 
{
    uint8_t rc_tmp;
    uint16_t rc_len;
    uint16_t i;
    if(USART_GetITStatus(USART1,USART_IT_IDLE)!=RESET)
    {
      rc_tmp=USART1->SR;
      rc_tmp=USART1->DR;//软件序列清除IDLE标志位
      DMA_Cmd(DMA2_Stream2, DISABLE);关闭DMA,准备重新配置
      DMA_ClearITPendingBit(DMA2_Stream2, DMA_IT_TCIF2);    // Clear Transfer Complete flag
      DMA_ClearITPendingBit(DMA2_Stream2, DMA_IT_TEIF2);    // Clear Transfer error flag    
      rc_len = USART1_RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA2_Stream2);//计算接收数据长度

      for(i=0;i<rc_len;i++)//输出每一字节的数据观察
      {
         printf("%d ",USART1_Rx_Buffer[i]);
         usart1.rx_buf[i]=USART1_Rx_Buffer[i];
      }
      printf("\n");

      Data_Decode(USART1_Rx_Buffer);//解码收到的数据
    }
    DMA_Enable(DMA2_Stream2,USART1_RX_BUFFER_SIZE);//开启下一次DMA接收

}

上述代码经stm32f407平台测试通过

三、纠正

感谢qq_20246035在评论区提出问题。之前一、9部分的实例代码中,DMA_Config函数定义时,数据传输方向直接写死到配置中了,而二、2部分的示例代码中,调用DMA_Config时把数据传输方向作为参数了,运行应该会报错示例和声明不匹配。
两种改正方法:1.调用时不要写传输方向参数 ; 2.修改DMA_Config。目前已按方法二改正

再次感谢读者帮我找到错误,谢谢!
如果大家发现还有什么问题,欢迎在评论区告诉我

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章