STM32单片机——DMA数据传输
- DMA相关概述
- 实验一:内存到内存搬运
- Cubemx工程配置
- Hal库程序设计及实现
- 固件库程序设计及实现
- 实验二:内存到外设(DMA串口发送)
- Cubemx工程配置
- Hal库程序设计及实现
- 固件库程序设计
- 实验三:外设到内存(DMA串口接收)
- DMA接收Cubemx工程配置
- 串口DMA接收相关函数解析
- DMA串口接收Hal库代码实现
- DMA串口接收固件库程序设计
- 写在最后
DMA相关概述
-
参考文章:STM32 DMA详解
-
1. 什么是DMA
令人头秃的描述
DMA(Direct Memory Access,直接存储器访问) 提供在外设与内存、存储器和存储器、外设与外设之间的高速数据传输使用。它允许不同速度的硬件装置来沟通,而不需要依赖于CPU,在这个时间中,CPU对于内存的工作来说就无法使用。
简单描述:
就是一个数据搬运工!! -
2. DMA的意义
代替 CPU 搬运数据,为 CPU 减负。
(1)数据搬运的工作比较耗时间;
(2) 数据搬运工作时效要求高(有数据来就要搬走);
(3) 没啥技术含量(CPU 节约出来的时间可以处理更重要的事)。 -
3. 搬运什么数据
存储器: spi、usart、iic、adc 等基于APB1 、APB2或AHB时钟的外设
外设: 自身的闪存(flash)或者内存(SRAM)以及外设的存储设备都可以作为访问地源或者目的
三种搬运方式:搬运方式 用途(案例) 存储器→存储器 复制某特别大的数据buf 存储器→外设 将某数据buf写入串口TDR寄存器 外设→存储器 将串口RDR寄存器写入某数据buf
存储器→存储器
存储器→外设
外设→存储器
- 4. DMA 控制器
STM32F103有2个 DMA 控制器,DMA1有7个通道,DMA2有5个通道。
一个通道每次只能搬运一个外设的数据!! 如果同时有多个外设的 DMA 请求,则按照优先级进行响应。
DMA1有7个通道:
DMA2有5个通道:
- 5. DMA及通道的优先级
(1)软件: 每个通道的优先级可以在DMA_CCRx寄存器中设置,有4个等级最高级>高级>中级>低级
(2)硬件: 如果2个请求,它们的软件优先级相同,则较低编号的通道比较高编号的通道有较高的优先权 - 6. DMA传输方式
模式 传输方式 DMA_Mode_Normal(正常模式) 一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次 DMA_Mode_Circular(循环传输模式) 当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式 - 7. 指针递增模式
外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值。
实验一:内存到内存搬运
Cubemx工程配置
- 时钟配置
- 采用外部高速晶振
- 时钟树配置
- 采用外部高速晶振
- DMA配置
- 打开串口
Hal库程序设计及实现
-
常用库函数解析
- 开启DMA传输
参数解析HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
参数 解析 返回值 HAL_StatusTypeDef,HAL状态( OK,busy,ERROR,TIMEOUT
)DMA_HandleTypeDef *hdma DMA通道句柄 uint32_t SrcAddress 源内存地址 uint32_t DstAddress 目标内存地址 uint32_t DataLength 传输数据长度。注意:需要乘以sizeof(uint32_t) - 获取DMA Flag的值
参数解析#define __HAL_DMA_GET_FLAG(__HANDLE__, __FLAG__) (DMA1->ISR & (__FLAG__))
参数 解析 返回值 FLAG的值( SET/RESET
)__HANDLE__
DMA通道句柄 __FLAG__
数据传输标志。 DMA_FLAG_TCx
表示数据传输完成标志,x表通道数
- 开启DMA传输
-
Hal库程序设计
// 源数组 放在flash内部 const uint32_t srcBuf[16] = { 0x00000000,0x11111111,0x22222222,0x33333333, 0x44444444,0x55555555,0x66666666,0x77777777, 0x88888888,0x99999999,0xAAAAAAAA,0xBBBBBBBB, 0xCCCCCCCC,0xDDDDDDDD,0xEEEEEEEE,0xFFFFFFFF }; // 目标数组 放置在SRAM内 uint32_t desBuf[16]; //开启数据传输 HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)srcBuf, (uint32_t)desBuf, 16 * sizeof(uint32_t)); //等待数据传输完成 while( __HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1) == RESET ); //打印目标数组内容 HAL_UART_Transmit(&huart1,(const uint8_t *)desBuf, 16 * sizeof(uint32_t), 200);
-
串口打印目标数组
固件库程序设计及实现
- dma.c存储器和外设变量定义
#defien BUF_SIZE 16 //源数组 const表常量类型,数组buf放在flash中 const uint32_t src_buf[BUF_SIZE]={ 0x00000000,0x11111111,0x22222222,0x33333333, 0x44444444,0x55555555,0x66666666,0x77777777, 0x88888888,0x99999999,0xAAAAAAAA,0xBBBBBBBB, 0xCCCCCCCC,0xDDDDDDDD,0xEEEEEEEE,0xFFFFFFFF }; //目标数组 放置在SRAM中 uint32_t des_buf[BUF_SIZE];
- M2M结构体配置
void DMA_MTM_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA时钟 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)src_buf; //外设地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)des_buf; //内存地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向 DMA_InitStructure.DMA_BufferSize = BUF_SIZE * 4; //传输数量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //外设地址增量 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增 DMA_InitStructure.DMA_PeripheralDataSize= DMA_PeripheralDataSize_Byte; //外设数据宽度 1字节 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度 1字节 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //正常模式,发送一次 DMA_InitStructure.DMA_Priority = DMA_Priority_Low; //优先级 低 DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //使能MTM DMA_Init(DMA1_Channel1, &DMA_InitStructure); //结构体初始化 DMA_ClearFlag(DMA1_FLAG_TC1); //清除对应标志位 DMA_Cmd(DMA1_Channel1, ENABLE); //使能DMA1 通道1 }
- M2M传输函数
uint8_t Buffercmp(const uint32_t *pBuffer1, uint32_t *pBuffer2, uint32_t BufferLen ) { while(BufferLen--) { if( *pBuffer1 != *pBuffer2) //对应的两个源数组不相等,返回0 return 0; pBuffer1++; //递增两个数据源的地址指针 pBuffer2++; } return 1; //完成判断 并且对应数据相对 }
- 主函数程序设计
//外部源数组、目标数组变量定义 extern const uint32_t src_buf[BUF_SIZE]; extern uint32_t des_buf[BUF_SIZE]; int main(void) { LED_Init(); Usart1_Init(115200); //串口初始化 DMA_MTM_Init(); //M2M初始化 Buffercmp(src_buf, des_buf, BUF_SIZE * 4); //M2M传输数据 //串口打印目标数组数据 Usart_SendString(USART1,(unsigned char *)des_buf, BUF_SIZE * 4); while(1) { } }
实验二:内存到外设(DMA串口发送)
Cubemx工程配置
- 时钟及串口配置(同上)
- 串口DMA配置
Hal库程序设计及实现
-
串口DMA发送函数解析
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
参数解析
参数 解析 返回值 HAL_StatusTypeDef,HAL状态( OK,busy,ERROR,TIMEOUT
)UART_HandleTypeDef *huart 串口句柄 const uint8_t *pData 待发送数据首地址 uint16_t Size 发送数据长度 -
DMA串口发送程序设计
#include <string.h> //发送缓存 unsigned char sendBuf[128] = "send by DMA\r\n"; unsigned char sendBuf[128] = "send by DMA\r\n"; //DMA发送数据,此过程不消耗CPU HAL_UART_Transmit_DMA(&huart1, sendBuf, strlen((const char *)sendBuf));
固件库程序设计
- 参考博文:STM32—DMA功能讲解串口发送
- dma.c dma结构体配置
#define SEND_SIZE 16 //USART_DMA #define USART1_DR_ADDR (USART1_BASE + 0x04) //串口发送寄存器 //发送数据变量定义 uint8_t send_buf[SEND_SIZE] = "0123456789ABCDEF"; void USART1_DMATx_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA时钟 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)send_buf; //内存地址 DMA_InitStructure.DMA_PeripheralBaseAddr= (uint32_t)USART1_DR_ADDR; //外设地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向 DMA_InitStructure.DMA_BufferSize = SEND_SIZE ; //传输数量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增 DMA_InitStructure.DMA_PeripheralDataSize= DMA_PeripheralDataSize_Byte; //外设数据宽度 1字节 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度 1字节 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //正常模式,发送一次 DMA_InitStructure.DMA_Priority = DMA_Priority_Low; //优先级 低 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //失能MTM DMA_Init(DMA1_Channel4, &DMA_InitStructure); //结构体初始化 DMA_ClearFlag(DMA1_FLAG_TC4); //清除对应标志位 DMA_Cmd(DMA1_Channel4, ENABLE); //使能DMA1 通道4 }
- main.c 使能串口DMA数据传输
USART1_DMATx_Init(); //串口DMA发送初始化 USART_DMACmd(USART1, USART_DMAReq_Tx , ENABLE ); //使能串口DMA发送请求
实验三:外设到内存(DMA串口接收)
DMA接收Cubemx工程配置
- 时钟及串口配置(同上)
- 串口DMA配置
- 开启串口中断
串口DMA接收相关函数解析
-
1使能IDLE空闲中断
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) ............
参数解析
参数 解析 __HANDLE__
串口句柄 __INTERRUPT__
需要使能的中断 返回值 无 -
串口DMA接收函数
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
参数解析
参数 解析 返回值 HAL_StatusTypeDef,HAL状态( OK,busy,ERROR,TIMEOUT
)UART_HandleTypeDef *huart 串口句柄 uint8_t *pData 接收缓存首地址 uint16_t Size 接收缓存长度 -
获取串口 Flag的值
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))
参数解析
参数 解析 返回值 FLAG的值( SET/RESET
)__HANDLE__
串口句柄 __FLAG__
需要查看的FLAG -
清除串口IDLE标识
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) __HAL_UART_CLEAR_PEFLAG(__HANDLE__)
参数 解析 __HANDLE__
串口句柄 返回值 无 -
关闭串口DMA
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)
参数 解析 UART_HandleTypeDef *huart 串口句柄 返回值 HAL_StatusTypeDef,HAL状态( OK,busy,ERROR,TIMEOUT
) -
获取未传输数据大小
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR)
参数 解析 __HANDLE__
串口句柄 返回值 未传输数据大小
DMA串口接收Hal库代码实现
如何判断串口接收是否完成?如何知道串口收到数据的长度?
使用串口空闲中断(IDLE)!
- 串口空闲时,触发空闲中断;
- 空闲中断标志位由硬件置1,软件清零
利用串口空闲中断,可以用如下流程实现DMA控制的任意长数据接收:
- 使能IDLE空闲中断;
- 使能DMA接收中断;
- 收到串口接收中断,DMA不断传输数据到缓冲区;
- 一帧数据接收完毕,串口暂时空闲,触发串口空闲中断;
- 在中断服务函数中,清除中断标志位,关闭DMA传输(防止干扰);
- 计算刚才收到了多少个字节的数据。
- 处理缓冲区数据,开启DMA传输,开始下一帧接收
#define BUF_SIZE 128
//DMA接收缓存变量定义
unsigned char rcvBuf[BUF_SIZE] = {0};
unsigned char rcvLen = 0; //接收字节数
main.c初始化
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE空闲中断
HAL_UART_Receive_DMA(&huart1, rcvBuf, BUF_SIZE); //使能DMA接收中断
//中断服务函数中,清除中断标志位,关闭DMA传输,计算数据接收长度并操作数据
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if( __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) == SET ) // 判断IDLE标志位是否被置位)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除中断标志位
HAL_UART_DMAStop(&huart1); //关闭DMA传输
rcvLen = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); //计算收到的字节数
//处理缓冲区数据 若程序复杂可立flag 在 while(1) 中处理
HAL_UART_Transmit_DMA(&huart1,rcvBuf,rcvLen);
if(strstr((const char *)rcvBuf,"open") != NULL)
HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET);
if(strstr((const char *)rcvBuf,"close") != NULL)
HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET);
HAL_UART_Receive_DMA(&huart1, rcvBuf, BUF_SIZE); //开启DMA传输
}
/* USER CODE END USART1_IRQn 1 */
}
DMA串口接收固件库程序设计
- 参考博文1:STM32_串口的DMA接收
- 参考博文2:STM32实现DMA接收串口数据
- dma结构体配置
//USART_DMA_R 接收缓存 uint8_t receive_buf[16] ={'\0'}; //USART1_DMA_Receive void USART1_DMARx_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA时钟 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)receive_buf; //内存地址 DMA_InitStructure.DMA_PeripheralBaseAddr =(uint32_t)USART1_DR_ADDR; //外设地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向 DMA_InitStructure.DMA_BufferSize = 16; //传输数量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据宽度 1字节 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度 1字节 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //循环接收 DMA_InitStructure.DMA_Priority = DMA_Priority_High; //优先级 高 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //失能MTM DMA_Init(DMA1_Channel5, &DMA_InitStructure); //结构体初始化 DMA_ClearFlag(DMA1_FLAG_TC5); //清除对应标志位 DMA_Cmd(DMA1_Channel5, ENABLE); //使能DMA1 通道5 }
- 主函数程序设计
uint8_t DMARx_flag = 0; //中断标志 extern uint8_t receive_buf[128]; //外部USART_DMA 接收缓存 int main() { Usart1_Init(115200); //串口初始化 115200 //USRAT_DMA USART1_DMARx_Init(); //初始化DMA接收 USART_DMACmd(USART1, USART_DMAReq_Rx , ENABLE);//使能串口DMA接收请求 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); //使能空闲中断 while(1) { } }
- 串口中断程序设计
void USART1_IRQHandler ( void ) { u16 Usart1_Rec_Cnt = 0; if ( USART_GetITStatus ( USART1, USART_IT_IDLE ) != RESET ) //检查是否发生空闲中断 { USART1->SR; USART1->DR; // 清USART_IT_IDLE标志 USART_ClearITPendingBit ( USART1, USART_IT_IDLE ); // 清除中断标志 DMA_Cmd ( DMA1_Channel5, DISABLE ); //关闭DMA传输,进行数据处理,数据处理完再开启 // 计算接收到的数据帧长度 Usart1_Rec_Cnt = 16 - DMA_GetCurrDataCounter ( DMA1_Channel5); printf ( "The length is %d\r\n", Usart1_Rec_Cnt ); printf ( "The data is %s", receive_buf ); Usart1_Rec_Cnt = 0; memset(receive_buf,'\0',16); DMA_SetCurrDataCounter( DMA1_Channel5, 16 ); //清除counter计数器 DMA_Cmd ( DMA1_Channel5, ENABLE ); // DMARx_flag = 1; //中断标志位 } }
写在最后
- 以上就是关于STM32单片机——DMA数据传输的相关理论及例程验证,欢迎评论区友好交流批评指正!!!