前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Cubemx与HAL库系列教程|串口DMA收发详解

Cubemx与HAL库系列教程|串口DMA收发详解

作者头像
用户8913398
发布2021-09-14 11:47:00
3.1K0
发布2021-09-14 11:47:00
举报

小调查

文章开始之初,小飞哥想先做个小调查,大家平时使用串口接收,都是采用什么方式处理的,有什么优缺点?欢迎添加小飞哥好友,进群一起交流!

STM32 DMA简介

DMA,全称为:Direct Memory Access,即直接存储器访问, DMA 传输将数据从一个 地址空间复制到另外一个地址空间。当 CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器 来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的 内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工 作。

DMA 传输对于高效能嵌入式系统算法和网络是很重要的。DMA 传输方式无需 CPU 直接 控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备 开辟一条直接传送数据的通路, 能使 CPU 的效率大为提高。

如图 所示, 两个 DMA 控制器有 12 个通道(DMA1 有 7 个通道, DMA2有5个通道)。DMA 控制器和 Cortex-M3 核心共享系统数据总线,执行直接存储器数据传输。当 CPU 和 DMA 同时访问相同的目标(RAM 或外设) 时, DMA 请求会暂停 CPU 访问系统总线达若干个周期,总线仲裁器执行循环跳读,以保证CPU 至少可以得到一半的系统总线带宽。

  • DMA 处理

在发送一个事件后,外设向 DMA 发送一个请求信号 的箭头。DMA 控制器根据通道的优先权处理请求。当 DMA 需 送请求的外设时, DMA 控制器立即发送给它一个应答信号。外 号后,立即释放它的请求, 同时 DMA 控制器撤销应答信号

  • 仲裁器

一个 DMA 控制器对应 8 个数据流,数据流包含要传输数据的源地址、目标地址、数据等信息。如果我们需要同时使用同一个 DMA 控制器多个外设请求时,那必然需要同时使用多个数据流,其中哪个数据流优先, 此时由仲裁器来选定。

仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别,可以在 DMA_CCRx 寄存器中设置, 有最高优先级、高优先级、中等优先级和低优先级四个等级。第二阶段是硬件,如果两个请求有相同的软件优先级,则较低编号的通道比高编号的通道有较高的优先权。例如:通道 2 优先于通道 4。

  • DMA 通道

每个通道都可以在由固定地址的外设寄存器和存储器之间执行DMA 传输。DMA 的传输数据量是可编程, 可以通过 DMA_CCRx 寄存器中的PSIZE 和 MSIZE 位来进行编程,数据量最大可以达到 65535。DMA 的外设繁多, 例如 DMA1 控制器,从外设产生 7 个请求,通过逻辑或(例如通道 1 的三个 DMA 请求,这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个)输入到 DMA1 控制器,此时只有一个请求有效。

STM32 的 DMA 有以下一些特性:

●每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能 通过软件来配置。

● 在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如 在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。

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

● 支持循环的缓冲器管理

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

● 存储器和存储器间的传输

● 外设和存储器,存储器和外设的传输

● 闪存、 SRAM、外设的 SRAM、 APB1 APB2 和 AHB 外设均可作为访问的源和目标。

● 可编程的数据传输数目:最大为 65536

STM32串口DMA使用详解

本次我们使用的硬件环境是之前开源的板子,falling-star board,使用串口1。

cubemx配置

关于时钟配置、串口基本配置请参看:cubemx的正确打开方式一文

接下来直接进入配置串口DMA:

选择串口1,基本参数如图,都是老生常谈了,easy~

选择DMA Settings,主要有一下几个地方,基本上不需要改动,根据自己的使用情况确认即可,需要注意的是,发送和接收并不是一定要成对出现的,可以只选择DMA发送或者DMA接收

中断设置,DMA中断可以配置,可以不配置,同样也是根据自己的实际需求情况,串口中断需要配置,下面会用到,优先级根据自己任务的优先级确定,分好“轻重缓急”即可

配置非常简单,主要是在此前串口功能基础上添加DMA功能,over

串口DMA代码设计

串口DMA源码API介绍

上面提到的配置项,都封装在一个结构体里面

代码语言:javascript
复制
/** @defgroup DMA_Exported_Types DMA Exported Types
  * @{
  */

/**
  * @brief  DMA Configuration Structure definition
  */
typedef struct
{
  uint32_t Direction;                 /*!< Specifies if the data will be transferred from memory to peripheral, 
                                           from memory to memory or from peripheral to memory.
                                           This parameter can be a value of @ref DMA_Data_transfer_direction */

  uint32_t PeriphInc;                 /*!< Specifies whether the Peripheral address register should be incremented or not.
                                           This parameter can be a value of @ref DMA_Peripheral_incremented_mode */

  uint32_t MemInc;                    /*!< Specifies whether the memory address register should be incremented or not.
                                           This parameter can be a value of @ref DMA_Memory_incremented_mode */

  uint32_t PeriphDataAlignment;       /*!< Specifies the Peripheral data width.
                                           This parameter can be a value of @ref DMA_Peripheral_data_size */

  uint32_t MemDataAlignment;          /*!< Specifies the Memory data width.
                                           This parameter can be a value of @ref DMA_Memory_data_size */

  uint32_t Mode;                      /*!< Specifies the operation mode of the DMAy Channelx.
                                           This parameter can be a value of @ref DMA_mode
                                           @note The circular buffer mode cannot be used if the memory-to-memory
                                                 data transfer is configured on the selected Channel */

  uint32_t Priority;                  /*!< Specifies the software priority for the DMAy Channelx.
                                           This parameter can be a value of @ref DMA_Priority_level */
} DMA_InitTypeDef;

关于DMA的函数不多,本次用到DMA的初始化,开始发送函数

串口中关于DMA的部分主要有这几个函数,还有一些关于中断、DMA标志等的一些宏定义,就不在一一列举了,需要用的时候大家知道去库函数中去找就可以了

串口DMA初始化部分:

代码语言:javascript
复制

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = User_UART_TX_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(User_UART_TX_GPIO_Port, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = User_UART_RX_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(User_UART_RX_GPIO_Port, &GPIO_InitStruct);

    /* USART1 DMA Init */
    /* USART1_TX Init */
    hdma_usart1_tx.Instance = DMA1_Channel4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_tx.Init.Mode = DMA_CIRCULAR;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);

    /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA1_Channel5;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }
}

DMA发送设计

发送非常简单,都不知道该怎么介绍...发送对应DMA的方向是内存到外设,我们只需要把数据装入相应的内存中,调用发送的API即可:

代码语言:javascript
复制
/******************************************************
* Brief     : DMA传输函数
* Parameter : 
*           *pData: 要传输的数据
* Return    : None.
*******************************************************/
void User_uartdma_Transmit(const char *pData)
{ 
 HAL_UART_Transmit_DMA(&huart1,(uint8_t *)pData,strlen(pData));
}

根据DMA的原理,我们发送的时候是不影响CPU的正常工作的,效果应该类似于操作系统的多任务执行,现在我们只需要在初始化时候打开DMA发送数据,串口助手监控,主函数什么都不干,数据不断在发送

此时如果添加两个LED流水灯,是完全不受发送数据影响的,效果就不放了,相信小飞哥...

不信你看~

发送的是非常简单的,好像这里也没体现出来,使用DMA发送有什么好处,其实在LCD驱动的时候,当有图片等大数据量的数据需要传输的时候,使用DMA是种非常好的方式

DMA接收设计

聊起串口数据接收,我们最先接触的可能是,中断接收,来一字节进一次中断,通过定时器超时判断一帧数据结束。

但是小伙伴们有没有考虑过一个事情,数据量很大,串口一直进中断,对MCU带来的负荷是非常大的,这种方式就显得不那么美好了。

哲学上讲,矛盾是推动社会进步的源泉,没错,鉴于此种情况,我们换一种方式来处理,DMA+串口空闲中断的方式,我相信,这种方式你一用就会喜欢上~

具体的设计思路是:

1、开启串口1中断

2、开启串口1空闲中断

3、打开串口DMA接收

4、判断空闲中断标志是否置位

5、数据接收完成,主函数打印接收到的数据

先来封装几个函数:

代码语言:javascript
复制
/******************************************************
* Brief     : 串口DMA 初始化,初始化除了cubemx配置之外的部分
* Parameter : 
*           : 
* Return    : None.
*******************************************************/
void User_uartdma_Init(void)
{
 //失能串口DMA传输
 HAL_UART_DMAStop(&huart1);
 //使能串口1接收中断
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);
 //使能串口1空闲中断
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
 //使能串口DMA接收
 HAL_UART_Receive_DMA(&huart1,UserUartDma.RxBuffer,Max_RecLen);
}
代码语言:javascript
复制
/**
  * @brief  DMA接收函数
  * @param  htim TIM Base handle
  * @retval HAL status
  */
void User_uartdma_Receive(uint8_t *recData,uint16_t rec_len)
{
 HAL_UART_Receive_DMA(&huart1,recData,rec_len);
}

在stm32f1xx_it.c中

代码语言:javascript
复制
/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
 uint32_t idle_flag_temp = 0;
 uint16_t len_temp = 0;
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
 idle_flag_temp = __HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE);
 
 if(idle_flag_temp)
 {
  __HAL_UART_CLEAR_FLAG(&huart1,UART_FLAG_IDLE);
  HAL_UART_DMAStop(&huart1);
  
  len_temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
  
  UserUartDma.RecDat_len = Max_RecLen - len_temp;  
  UserUartDma.rec_endFlag = 1;
  
 }
  __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);
  HAL_UART_Receive_DMA(&huart1,UserUartDma.RxBuffer,Max_RecLen);  
  /* USER CODE END USART1_IRQn 1 */
}

主函数:

代码语言:javascript
复制
 while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  if(UserUartDma.rec_endFlag)
  {
   UserUartDma.rec_endFlag = 0;
   User_uart_Transmit((char *)UserUartDma.RxBuffer);
   
   memset(UserUartDma.RxBuffer,0,UserUartDma.RecDat_len);
   UserUartDma.RecDat_len = 0;
  }
//  if(__HAL_DMA_GET_FLAG(&huart1,DMA_FLAG_TC4))
//  {
//   memset(UserUartDma.RxBuffer,0,UserUartDma.RecDat_len);
//   UserUartDma.RecDat_len = 0;
//   
//   HAL_UART_DMAStop(&huart1);
//  }  
  }

演示效果:

经验交流

欢迎添加小飞哥好友,进群一起交流,后台回复“串口DMA”即可获取本次实验源码

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小飞哥玩嵌入式 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 小调查
  • STM32 DMA简介
  • STM32串口DMA使用详解
    • cubemx配置
    • 串口DMA代码设计
      • 串口DMA源码API介绍
        • DMA发送设计
          • DMA接收设计
          • 经验交流
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档