小调查
文章开始之初,小飞哥想先做个小调查,大家平时使用串口接收,都是采用什么方式处理的,有什么优缺点?欢迎添加小飞哥好友,进群一起交流!
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介绍
上面提到的配置项,都封装在一个结构体里面
/** @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初始化部分:
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即可:
/******************************************************
* 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、数据接收完成,主函数打印接收到的数据
先来封装几个函数:
/******************************************************
* 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);
}
/**
* @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中
/**
* @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 */
}
主函数:
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);
// }
}
演示效果: