STM32 串口采用DMA方式收发

cathy的头像

什么是DMA —- Directional Memory Access, 直接存储器存取用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作

我们通过以下几方面学习串口DMA:

一、如何理解DMA

对于DMA,打个比方就很好理解:

角色预设: 淘宝店主 —- STM32 MCU
快递员 —- 外设(如UART,SPI)
发货室 —- DMA

1、首先你是一个淘宝店主,如果每次发货收货都要跟快递沟通交涉会很浪费时间和精力。

2、然后你就自己建了一个发货室,发货室里有好多个货柜箱子,每个箱子上都写着快递名字(如果申通快递,顺丰快递等)。

3、每次发什么快递,你就找到对应的货柜箱子,把货物放进去即可,然后跟快递通知一声。

4、快递取走快件。

5、如果是收货,快递直接把快件放到对应的柜子,然后通知你一下。

6、你过来提取货物。

通过上面的方式,你可以不需要直接跟快递打交道,就可以轻松发货成功,DMA处理方式跟上面例子是一样的。

如果下图:

“”

二、STM32 DMA 配置

那么DMA在STM32上是具体怎么实现的呢? 我们先了解一下STM32关于DMA的相关配置。

1、两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道)

ps:对应我们例子,就是有两个大的发货室,一个有7个货柜,另个有5个货柜。

2、在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推)

ps: 店主可以跟每个快递公司签订协议,可以在货柜前贴上加急(很高),很急(高),急(中),一般(低), 如果同时有几个快递员过来取货,优先根据上面的优先级先取件。

3、独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。

ps: 指的是货件大小

4、支持循环的缓冲器管理(会把原来的数据覆盖)

5、每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。

ps: 送快递出现的异常情况(送到了一半,送完,快递出错)

解释到这里,不知道大家能不能理解呢。后面是具体的配置。

1、DMA 对应通道如下图

DMA1:

“”

DMA2:

“”

2、DMA配置

1)数据传输的目的地和来源

“”

对应我的例子,就是送快递还是取快递。

2)定义DMA通道的DMA缓存的大小
ps: 即货柜大小,能存多少个快件

3)外设地址寄存器递增与否

“”

4)内存地址寄存器递增与否

“”

5)设定了外设数据宽度

“”

6)设定了内存数据宽度

“”

7)设置了DMA的工作模式

“”

8)DMA通道的软件优先级

“”

9)使能或关闭DMA通道的内存到内存传输

“”

三、 编程

串口用DMA方式发送和接收,分以下几步:

1)串口初始化
2)DMA初始化
3)发送数据
4)接收数据

我们按部就班:

1) 串口初始化 — 使用串口一

#define  DMASIZE 1024

// 配置串口一的发送和接收的GPIO口功能,以及中断
static void _uart1_gpio_init(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA   |
                     RCC_APB2Periph_USART1  |
                         RCC_APB2Periph_AFIO, ENABLE) ;

  GPIOA->CRH&=0XFFFFF00F; 
  GPIOA->CRH|=0X000008B0;//IO状态设置 10pin_上拉输入  9pin_推挽输出

  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    /* Configure USART1 Rx as input floating */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
  GPIO_Init(GPIOA, &GPIO_InitStructure);

  /* Configure USART1 Tx as alternate function push-pull */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOA, &GPIO_InitStructure);


  /* Enable the USART1 Interrupt */
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQChannel;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

  USART_ClearFlag(USART1, USART_FLAG_TC); /* 清发送外城标志,Transmission Complete flag */

  USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);// 采用空闲中断,目的是在产生空闲中断时,说明接收或者发送已经结束,此时可以读取DMA中的数据了。
  //USART_ITConfig(USART1, USART_IT_TC, ENABLE);
  //USART_ITConfig(USART1, USART_IT_FE, ENABLE);
}
// 设置对应串口的波特率
static void _uart_setbaudrate(USART_TypeDef* USARTx,u32 value)
{
  USART_InitTypeDef USART_InitStructure;
  USART_InitStructure.USART_BaudRate =value;
  USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  USART_InitStructure.USART_StopBits = USART_StopBits_1;
  USART_InitStructure.USART_Parity = USART_Parity_No;
  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  USART_Init(USARTx, &USART_InitStructure);
  USART_Cmd(USARTx, ENABLE);
}

2)初始化DMA

u8 sendbuf[1024];
u8 receivebuf[1024];
static void _uart1_dma_configuration()
{
  DMA_InitTypeDef DMA_InitStructure;

  /* DMA1 Channel6 (triggered by USART1 Rx event) Config */
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1 ,
                        ENABLE);

  /* DMA1 Channel5 (triggered by USART1 Rx event) Config */
  DMA_DeInit(DMA1_Channel5);
  DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base;// 初始化外设地址,相当于“哪家快递”  
  DMA_InitStructure.DMA_MemoryBaseAddr =(u32)receivebuf;// 内存地址,相当于几号柜
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//外设作为数据来源,即为收快递
  DMA_InitStructure.DMA_BufferSize = DMASIZE ;// 缓存容量,即柜子大小
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增,即柜子对应的快递不变
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;// 内存递增
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设字节宽度,即快递运输快件大小度量(按重量算,还是按体积算) 
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;// 内存字节宽度,即店主封装快递的度量(按重量,还是按体质进行封装)
  DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 正常模式,即满了就不在接收了,而不是循环存储
  DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;// 优先级很高,对应快递就是加急
  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 内存与外设通信,而非内存到内存 
  DMA_Init(DMA1_Channel5, &DMA_InitStructure);// 把参数初始化,即拟好与快递公司的协议

  DMA_Cmd(DMA1_Channel5, ENABLE);// 启动DMA,即与快递公司签订合同,正式生效

  /* DMA1 Channel4 (triggered by USART1 Tx event) Config */
  DMA_DeInit(DMA1_Channel4);
  DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base;  // 外设地址,串口1, 即发件的快递
  DMA_InitStructure.DMA_MemoryBaseAddr =(u32)sendbuf;// 发送内存地址
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;// 外设为传送数据目的地,即发送数据,即快递是发件
  DMA_InitStructure.DMA_BufferSize = 0;  //发送长度为0,即未有快递需要发送
  DMA_Init(DMA1_Channel4, &DMA_InitStructure);//初始化

  USART_ITConfig(USART1, USART_IT_TC, ENABLE);// 使能串口发送完成中断
  USART_DMACmd(USART1, USART_DMAReq_Tx|USART_DMAReq_Rx, ENABLE);// 使能DMA串口发送和接受请求
}

3)数据发送

流程:串口发送数据,全部数据发送完毕后,会产生一个发送中断,所以发送数据分为两部分,
A、发送数据
B、中断处理

A、发送数据
u16 Uart_Send_Data(void* buffer, u16 size)
{
  if(!size) return 0;// 判断长度是否有效
  while (DMA_GetCurrDataCounter(DMA1_Channel4));// 检查DMA发送通道内是否还有数据
  if(buffer) memcpy(sendbuf, buffer,(size > 1024?1024:size));
  //DMA发送数据-要先关 设置发送长度 开启DMA
  DMA_Cmd(DMA1_Channel4, DISABLE);
  DMA1_Channel4->CNDTR = size;// 设置发送长度
  DMA_Cmd(DMA1_Channel4, ENABLE);  // 启动DMA发送
  return size;
}

B、中断处理
1)中断处理相关准备工作
typedef enum _UartEvent_
{
   E_uart_0 = 0,// 没有事件
   E_uart_tc=0x40,                  //发送完成
   E_uart_idle=0x80,               //接收完成
}UartEvent;
u16 receivelen = 0;// 声明接收数据长度
UartEvent event;//申明一个事件参数

//清除DMA 缓存,并终止DMA
void Uart_Dma_Clr(void)
{
    DMA_Cmd(DMA1_Channel4, DISABLE);
    DMA1_Channel4->CNDTR=0;
    DMA_Cmd(DMA1_Channel5, DISABLE);
    DMA1_Channel5->CNDTR=DMASIZE ;
    DMA_Cmd(DMA1_Channel5, ENABLE);
}
// 获取一个事件,事件分为发送完成事件和接收完成事件,可以根据事件进行进行处理
UartEvent Uart_Get_Event(void)
{
  UartEvent e;
  if(!DMA1_Channel5->CNDTR) Uart_Dma_Clr();// 如果产生一个事件后,接收数据通道已经没有了缓存空间,进行清除DMA清空
  return event;
}
// 清除对应的事件
void Uart_Clr_Event(UartEvent event_in)
{
    event&=~event_in;
}

2) 中断处理,当所有数据发送完毕,串口1产生一个发送完成中断
void Uatr1_Back_IRQHandler()
{
  u8 tem;
  if(USART_GetITStatus(USART1,USART_IT_IDLE)!= RESET)
  {
    tem=USART1->SR;//先读SR,然后读DR才能清除
    tem=USART1->DR;
    tem=tem;
    Uart_Set_Event(E_uart_idle);
    receivelen =DMASIZE - DMA1_Channel5->CNDTR;// 总的buf长度减去剩余buf长度,得到接收到数据的长度
    USART_ClearITPendingBit(USART1, USART_IT_IDLE);
  } 

  **if(USART_GetITStatus(USART1,USART_IT_TC)!= RESET) // 全部数据发送完成,产生该标记**
  {
    USART_ClearITPendingBit(USART1, USART_IT_TC);   // 清除完成标记
    DMA_Cmd(DMA1_Channel4, DISABLE); // 关闭DMA
    DMA1_Channel4->CNDTR=0;          // 清除数据长度
    Uart_Set_Event(E_uart_tc);     //设置发送完成事件
  } 
}

4)接收数据

“”

根据上图描述,流程如下:

1、串口接收到数据
2、DMA自动取走数据
3、DMA把数据存到内存receive[1024]中

“”

4、串口接收完毕后会产生一个空闲中断

根据上面流程,我们接收数据需要做到两步:

1)串口产生一个空闲中断后,设置一个接收完成事件

中断处理:

void Uatr1_Back_IRQHandler()
{
  u8 tem;
  **if(USART_GetITStatus(USART1,USART_IT_IDLE)!= RESET)**
  {
    tem=USART1->SR;//先读SR,然后读DR才能清除
    tem=USART1->DR;// 清除DR 
    tem=tem; // 防止编译器警告
    Uart_Set_Event(E_uart_idle);// 设置接收完成(空闲)事件
    receivelen =DMASIZE - DMA1_Channel5->CNDTR;// 总的buf长度减去剩余buf长度,得到接收到数据的长度
    USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 清除空闲中断
  } 

if(USART_GetITStatus(USART1,USART_IT_TC)!= RESET) // 全部数据发送完成,产生该标记  
{
    USART_ClearITPendingBit(USART1, USART_IT_TC);   // 清除完成标记
    DMA_Cmd(DMA1_Channel4, DISABLE); // 关闭DMA
    DMA1_Channel4->CNDTR=0;          // 清除数据长度
    Uart_Set_Event(E_uart_tc);     //设置发送完成事件
  } 
}

2)接收数据函数检测事件,如果发现是接收完成事件,取走数据,并且做相关清除操作

u8 Uart_Receive_Data(u8*recbuf u16 *revLen)
{
    u8 *str;
    if( event & E_uart_idle) // 是否产生空闲中断
    {
        str = Uart_Get_Data(revLen);    
        memcpy(recbuf,receivebuf,*revLen);
        Uart_Clr_Event(E_uart_idle);
        Uart_Dma_Clr();
        return TRUE;
    }
    else
    {
        revLen = 0;
        return FALSE;
    }
}

好了,到此DMA已经讲完了,有点长。

小结:

1、DMA其实就是个自动缓存器,数据来了,缓存到指定位置。发送数据则把缓存数据发送出去。
2、串口空闲中断,实测在接收完成数据后,空闲闲置时产生的,而发送数据不会产生该中断。
3、串口发送完成中断,实测在全部数据发送成功后,才会产生中断。

版权声明:本文为CSDN博主「gdjason」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/gdjason/article/details/51019219