DMA中断
实现目的
DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。
中断是指CPU在执行程序的过程中,出现了某些突发事件时CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU又返回源程序被中断的位置并继续执行。
利用DMA实现大量数据传输,配合中断对数据进行解析,可以大量节省CPU资源并在大工程中能够有效避免数据处理对程序的卡顿现象。
DMA简介
1. DMA介绍
(1)DMA基本概念:
DMA,全称Direct MemoryAccess,即直接存储器访问。DMA传输将数据从一个地址空间复制到另一个地址空间,-用于在外设与存储器之间以及存储器与存储器之间进行高速数据传输。DMA传输过程的初始化和启动由CPU完成,传输过程由DMA控制器来执行,无需CPU参与,从而节省CPU资源,提高利用率。
(2)STM32的DMA控制器特点:
- STM32F401微控制器具备两个DMA控制器:DMA1和DMA2,每个控制器有8个数据流,每个数据流可以映射到8个通道(或请求);
- 每一个DMA控制器用于管理一个或多个外设的存储器访问请求,并通过总线仲裁器来协调各个DMA请求的优先级;
- 数据流(stream)是用于连接传输源和传输目标的数据通路,每个数据流可以配置为不同的传输源和传输目标,这些传输源和传输目标称为通道(Channel);
- 具备16字节的FIFO。使能FIFO功能后,源数据先送入FIFO,达到FIFO的触发阈值后,再传送到目标地址。
(3)DMA的主要特征:
在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
支持循环的缓冲器管理,即FIFO模式,用于在源数据传输到目标地址之前临时存放这些数据,阈值有1/4、1/2、3/4 和满;
每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;
存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输,如图所示;
闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;
可编程的数据传输数目:最大为65535。
(4)DMA的数据传输方式:
- 普通模式:
传输结束后(即要传输数据的数量达到零),将不再产生DMA操作。若开始新的DMA传输,需在关闭DMA通道情况下,重新启动DMA传输。 - 循环模式:
可用于处理环形缓冲区和连续数据流(例如ADC扫描模式)。当激活循环模式后,每轮传输结束时,要传输的数据数量将自动用设置的初始值进行加载, 并继续响应DMA请求。
2. DMA原理图
这是DMA工作流程图:
DMA1通道图如下:
DMA2通道图如下:
DMA系统框图如下:
DMA结构体
typedef struct {
uint32_t DMA_Channel; //通道选择,通道0~7
uint32_t DMA_PeripheralBaseAddr; //外设地址
uint32_t DMA_Memory0BaseAddr; //存储器地址
uint32_t DMA_DIR; //传输方向
uint32_t DMA_BufferSize; //数据数目,设定待传输数据数目
uint32_t DMA_PeripheralInc; //外设递增
uint32_t DMA_MemoryInc; //存储器递增
uint32_t DMA_PeripheralDataSize; //外设数据宽度(字节、半字、全字)
uint32_t DMA_MemoryDataSize; //存储器数据宽度(字节、半字、全字)
uint32_t DMA_Mode; //模式选择(普通模式和循环模式)
uint32_t DMA_Priority; //优先级(很高、高、中等和低)
uint32_t DMA_FIFOMode; //FIFO模式
uint32_t DMA_FIFOThreshold; //FIFO阈值
uint32_t DMA_MemoryBurst; //存储器突发传输
uint32_t DMA_PeripheralBurst; //外设突发传输
} DMA_InitTypeDef;
DMA结构体初始化
#define JY60_len ((uint32_t)30) //陀螺仪数据缓存区长度
uint8_t JY60_Buff[JY60_len] = {0}; // 陀螺仪接收数据缓存区(自定义内存)
void DMA_USART2_Config(void)
{
/*开启DMA时钟*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);
/*使能DMA1的数据流1*/
DMA_DeInit(DMA1_Stream5);
/*等待DMA可配置*/
while(DMA_GetCmdStatus(DMA1_Stream5) != DISABLE){};
/*DMA结构体初始化*/
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_Channel=DMA_Channel_4;//使能DMA数据流1的通道4
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&(USART2->DR);//设置DMA外设基地址
DMA_InitStructure.DMA_Memory0BaseAddr=(uint32_t)JY60_Buff;//设置内存基地址 (要传输的变量的指针)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//传输方向:从外设到内存
DMA_InitStructure.DMA_BufferSize = (uint32_t)JY60_len;//DMA通道的DMA缓存的大小
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 模式:正常传输
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//优先级:高
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;//禁用 FIFO
/*此处为FIFO模式功能,已被禁用,无需设置*/
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;//FIFO 阈值:满
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发传输 16 个节拍
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发传输 1 个节拍
DMA_Init(DMA1_Stream5, &DMA_InitStructure);
/*使能完成中断*/
DMA_ITConfig(DMA1_Stream5, DMA_IT_TC, ENABLE);
/*使能DMA*/
DMA_Cmd(DMA1_Stream5,ENABLE);
}
==注意要点:==
- 需要使能DMA时钟和数据流,一般禁用FIFO模式。
- 外设—>存储器(内存):外设基地址不增,内存基地址自增。
- 存储器(内存)—>外设:内存基地址不增,外设基地址自增。
- DMA_BufferSize需要根据数据实际解算频率设置,否者会影响程序运行。
- DMA用于数据传输时,一般用普通模式,循环模式常用于ADC信号扫描。
DMA中断函数及数据解析
/*-------------------------------开启一次DMA传输-------------------------*/
//此函数为DMA普通模式下对DMA进行重新初始化,保证DMA可以连续解析数据
void DMA_Enable(DMA_Stream_TypeDef* DMAy_Streamx,uint32_t DMA_FLAG_TCIFx,uint32_t DMA_FLAG_TEIFx,uint32_t DMA_FLAG_HTIFx,uint16_t length)
{
DMA_Cmd(DMAy_Streamx, DISABLE); //关闭DMA
DMA_ClearFlag(DMAy_Streamx,DMA_FLAG_TCIFx| DMA_FLAG_TEIFx|DMA_FLAG_HTIFx);//清除DMAy_Streamx传输完成标志
while (DMA_GetCmdStatus(DMAy_Streamx) != DISABLE){} //等待传输结束
DMA_SetCurrDataCounter(DMAy_Streamx,length); //设置传输数据长度
DMA_Cmd(DMAy_Streamx, ENABLE); //开启DMA
}
/*-------------------------DMA1_Stream6-USART2-JY60-中断函数-------------------------*/
void DMA1_Stream5_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_Stream5,DMA_IT_TCIF5)==SET)
{
JY60_Data_Out(JY60_Buff,JY60_len);//JY60数据解析
for(int i=0;i<JY60_len;i++)//将数据缓存区清零
{
JY60_Buff[i]=0;
}
//清除DMA各种中断标志位,重装载DMA,方便下次数据传输
DMA_Enable(DMA1_Stream5,DMA_FLAG_TCIF5,DMA_FLAG_TEIF5,DMA_FLAG_HTIF5,JY61_len);
}
}
float fAcc[3]={0}; //加速度
float fGyro[3]={0}; //角速度
float fAngle[3]={0}; //角度
float Temp=0; //温度
uint8_t JY60_flag=0; //JY60通讯成功标志位
float yaw=0; //航向角
void JY61_Data_Out(uint8_t* Rebuf,uint32_t Size)
{
uint8_t Sum=0;
uint8_t i,j;//接收缓冲器下标索引
for(i=0;i<Size;i++)
{
if(Rebuf[i]==0x55)//检查帧头
{
for(j=0;j<10;j++)
{
Sum = Sum+Rebuf[i+j];//对数据求和
}
switch(Rebuf[i+1])
{
case 0x51://加速度数据解析
fAcc[0] = ((short)Rebuf[i+3]<<8| Rebuf[i+2])/32768.0*16;
fAcc[1] = ((short)Rebuf[i+5]<<8| Rebuf[i+4])/32768.0*16;
fAcc[2] = ((short)Rebuf[i+7]<<8| Rebuf[i+6])/32768.0*16;
Temp = ((short)Rebuf[i+9]<<8| Rebuf[i+8])/340.0+36.25;
break;
case 0x52://角速度数据解析
fGyro[0] = ((short)Rebuf[i+3]<<8| Rebuf[i+2])/32768.0*2000;
fGyro[1] = ((short)Rebuf[i+5]<<8| Rebuf[i+4])/32768.0*2000;
fGyro[2] = ((short)Rebuf[i+7]<<8| Rebuf[i+6])/32768.0*2000;
Temp = ((short)Rebuf[i+9]<<8| Rebuf[i+8])/340.0+36.25;
break;
case 0x53://角度数据解析
fAngle[0] = ((short)Rebuf[i+3]<<8| Rebuf[i+2])/32768.0*180;
fAngle[1] = ((short)Rebuf[i+5]<<8| Rebuf[i+4])/32768.0*180;
fAngle[2] = ((short)Rebuf[i+7]<<8| Rebuf[i+6])/32768.0*180;
Temp = ((short)Rebuf[i+9]<<8| Rebuf[i+8])/340.0+36.25;
break;
}
//对数据进行求和校验,并对角度数据进行处理
if(Rebuf[i+10]==Sum&&Rebuf[i+1]==0x53)
{
JY60_flag=1;
}
if(JY60_flag==1)
{
JY60_flag=0;//JY60标志位清零
//将角度由0°~180°~360°转化为-180°~0°~180°,方便后续对角度的运用
if(fAngle[2]>=0.0f&&fAngle[2]<=180.0f)
{
fAngle[2]=fAngle[2];
}
if(fAngle[2]>180.0f&&fAngle[2]<=360.0f)
{
fAngle[2]=fAngle[2]-360.0f;
}
//防止其他数据对yaw的干扰
if(fAngle[2]!=0)
{
yaw= fAngle[2];
}
}
}
}
}
==注意要点:==
- DMA中断和串口接收中断对数据解析有所不同,DMA中断是直接对内存中已有数据进行for循环解析,而串口接收中断是需要先存储数据再解析。
- DMA中断名是规定的,不可随便更改。
- 每次DMA解析结束时,需要对DMA重初始化,否则将无法进行下次数据传输。
- 解析结束时,记得将数据缓存区清零。
编程要点及总结
- 首先定义一个全局数组用于存放DMA传输数据。
- 接着初始化DMA,开启DMA相关时钟,配置普通模式并开启DMA完成中断。
- 写DMA中断函数,并将DMA解析函数和DMA重装载函数放入中断里。
- 接着在main 函数延时500ms打印解析数据至上位机。
==总结:==
熟练掌握DMA中断处理数据,将大大减少数据处理对程序的卡顿现象,并且可以有效避免多个中断存在时引起数据处理不及时问题,因此DMA非常重要!