1.头大的常规概念
DMA是Direct Memory Access(直接内存访问)的缩写,它是一种计算机系统的特性,允许某些硬件子系统在不经过中央处理器(CPU)的情况下直接访问主系统内存。使用DMA,CPU可以在传输过程中执行其他操作,而不必占用整个读写周期。这样可以提高数据传输的效率,减少CPU的负担。12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道) 每个通道都支持软件触发和特定的硬件触发。对应的使用DMA的类型有,外设->存储器,存储器->外设,存储器->存储器
作者云”:DMA一般都是和外设,存储器搭配使用,而目前市面上主流的开发板配套例程(某火,某原)都是和串口或者ADC一起呈现,当然后续我们也会用上,但本文中我仅单独进行写, STM32在硬件上设计了这么多的DMA通道是煞费苦心的,用好DMA控制器,可以大
大地提高系统的吞吐量。在PC上,将硬盘数据复制到内存,就采用DMA模式,把复制数
据的任务交给DMA来执行,CPU就可以去做别的事情了。在STM32下也是如此,在编程中
不采用DMA模式,全靠CPU去辛苦劳作,是巨大的损失。
2.勉强的结构讲解
虽然学到STM你可以直接学HAL库这些封装程度比较高的库,但是我们想想,有多长时间没有体会到真正恍然大悟感觉,那很奇妙。所以结构应该去了解。
从这个图我们可以看到我们使用DMA需要配置哪些参数,我们学习每个外设最后都要配置对应的结构体,这个图不是官方的图,是江科大的(他们的视频真的不错)
-
DMA_InitTypeDef结构体:用于定义DMA通道的初始化参数,包括以下字段:
- DMA_PeripheralBaseAddr:外设基地址,用于指定DMA源或目的地址。
- DMA_MemoryBaseAddr:存储器基地址,用于指定DMA源或目的地址。
- DMA_DIR:DMA传输方向,可以是DMA_DIR_PeripheralSRC(外设作为源)或DMA_DIR_PeripheralDST(外设作为目的)。
- DMA_BufferSize:DMA传输的数据量,单位为字节。
- DMA_PeripheralInc:外设地址寄存器递增模式,可以是DMA_PeripheralInc_Enable(使能)或DMA_PeripheralInc_Disable(禁止)。
- DMA_MemoryInc:存储器地址寄存器递增模式,可以是DMA_MemoryInc_Enable(使能)或DMA_MemoryInc_Disable(禁止)。
- DMA_PeripheralDataSize:外设数据宽度,可以是DMA_PeripheralDataSize_Byte(8位)、DMA_PeripheralDataSize_HalfWord(16位)或DMA_PeripheralDataSize_Word(32位)。
- DMA_MemoryDataSize:存储器数据宽度,可以是DMA_MemoryDataSize_Byte(8位)、DMA_MemoryDataSize_HalfWord(16位)或DMA_MemoryDataSize_Word(32位)。
- DMA_Mode:DMA传输模式,可以是DMA_Mode_Circular(循环模式)或DMA_Mode_Normal(正常模式)。
- DMA_Priority:DMA通道优先级,可以是DMA_Priority_VeryHigh(很高)、DMA_Priority_High(高)、DMA_Priority_Medium(中)或DMA_Priority_Low(低)。
- DMA_M2M:存储器到存储器传输模式,可以是DMA_M2M_Enable(使能)或DMA_M2M_Disable(禁止)。
3.常用函数插入
-
DMA_Init函数:用于根据指定的参数初始化DMA通道,原型如下:
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
其中,DMAy_Channelx是要初始化的DMA通道,可以是DMA1_Channel1到DMA1_Channel7或DMA2_Channel1到DMA2_Channel5。DMA_InitStruct是指向一个DMA_InitTypeDef结构体的指针,用于指定DMA通道的初始化参数。
- DMA_Cmd函数:用于使能或禁止指定的DMA通道,原型如下:
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
其中,DMAy_Channelx是要操作的DMA通道,可以是DMA1_Channel1到DMA1_Channel7或DMA2_Channel1到DMA2_Channel5。NewState是要设置的DMA通道的新状态,可以是ENABLE(使能)或DISABLE(禁止)。
-
DMA_ITConfig函数:用于配置指定的DMA通道的中断,原型如下
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
其中,DMAy_Channelx是要操作的DMA通道,可以是DMA1_Channel1到DMA1_Channel7或DMA2_Channel1到DMA2_Channel5。DMA_IT是要配置的DMA通道的中断源,可以是以下值的组合:
* DMA_IT_TC:传输完成中断 * DMA_IT_HT:半传输中断 * DMA_IT_TE:传输错误中断
NewState是要设置的DMA通道的中断的新状态,可以是ENABLE(使能)或DISABLE(禁止)。
- DMA_GetFlagStatus函数:用于检查指定的DMA标志是否被设置,原型如下:
-
FlagStatus DMA_GetFlagStatus(uint32_t DMA_FLAG);
4.代码哐当登场
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = ; //外设基地址,给定形参
DMA_InitStructure.DMA_PeripheralDataSize = ; //外设数据宽度,选择字节
DMA_InitStructure.DMA_PeripheralInc = ; //外设地址自增,选择使能
DMA_InitStructure.DMA_MemoryBaseAddr =; //存储器基地址,给定形参
DMA_InitStructure.DMA_MemoryDataSize = ; //存储器数据宽度,选择字节
DMA_InitStructure.DMA_MemoryInc = ; //存储器地址自增,选择使能
DMA_InitStructure.DMA_DIR = ; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_BufferSize = ; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,选择正常模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器,选择使能
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
#include "stm32f10x.h"
#define BUFFER_SIZE 32 //定义缓冲区大小
uint32_t aSRC_Const_Buffer[BUFFER_SIZE]; //定义源缓冲区
uint32_t aDST_Buffer[BUFFER_SIZE]; //定义目标缓冲区
void RCC_Configuration(void); //时钟配置函数
void GPIO_Configuration(void); //GPIO配置函数
void DMA_Configuration(void); //DMA配置函数
void NVIC_Configuration(void); //中断配置函数
void Error_Handler(void); //错误处理函数
int main(void)
{
RCC_Configuration(); //配置时钟
GPIO_Configuration(); //配置GPIO
DMA_Configuration(); //配置DMA
NVIC_Configuration(); //配置中断
/* 填充源缓冲区 */
for (uint32_t i = 0; i < BUFFER_SIZE; i++)
{
aSRC_Const_Buffer[i] = 0x55555555 + i;
}
/* 使能DMA通道1 */
DMA_Cmd(DMA1_Channel1, ENABLE);
/* 等待传输完成 */
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
/* 清除传输完成标志 */
DMA_ClearFlag(DMA1_FLAG_TC1);
/* 比较源缓冲区和目标缓冲区是否相同 */
for (uint32_t i = 0; i < BUFFER_SIZE; i++)
{
if (aSRC_Const_Buffer[i] != aDST_Buffer[i])
{
/* 缓冲区不匹配 */
Error_Handler();
}
}
/* 缓冲区匹配,程序正常运行 */
while (1)
{
/* 点亮LED */
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
}
/* 时钟配置函数 */
void RCC_Configuration(void)
{
/* 使能GPIOC时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
/* 使能DMA1时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
}
/* GPIO配置函数 */
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 配置PC13为推挽输出模式 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* 关闭LED */
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
/* DMA配置函数 */
void DMA_Configuration(void)
{
DMA_InitTypeDef DMA_InitStructure;
/* 复位DMA1通道1 */
DMA_DeInit(DMA1_Channel1);
/* 配置DMA1通道1的初始化参数 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)aSRC_Const_Buffer; //外设基地址为源缓冲区地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aDST_Buffer; //存储器基地址为目标缓冲区地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设作为源
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; //传输数据量为缓冲区大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //外设地址递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; //外设数据宽度为32位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; //存储器数据宽度为32位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //正常模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //高优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器模式
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //初始化DMA1通道1
/* 配置DMA1通道1的传输完成中断 */
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
}
/* 中断配置函数 */
void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 配置NVIC优先级分组为4 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/* 配置DMA1通道1中断 */
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn; //DMA1通道1中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级为0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级为0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断
NVIC_Init(&NVIC_InitStructure); //初始化NVIC
}
/* DMA1通道1中断服务函数 */
void DMA1_Channel1_IRQHandler(void)
{
/* 判断DMA1通道1是否发生传输完成中断 */
if (DMA_GetITStatus(DMA1_IT_TC1) == SET)
{
/* 清除DMA1通道1传输完成中断标志 */
DMA_ClearITPendingBit(DMA1_IT_TC1);
/* 关闭LED */
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
}
/* 错误处理函数 */
void Error_Handler(void)
{
/* 用户可以在这里添加自己的代码 */
while(1)
{
/* 闪烁LED */
GPIO_ToggleBits(GPIOC, GPIO_Pin_13);
Delay(1000); //延时1秒
}
}
这个代码实例的功能是将一个32字节的源缓冲区的内容复制到一个目标缓冲区,然后比较两个缓冲区是否相同。如果相同,程序正常运行,点亮LED,如果不同,程序进入错误处理函数,闪烁LED。
这个代码实例的步骤如下:
- 包含所需的头文件,定义缓冲区大小,声明源缓冲区和目标缓冲区,声明配置函数和错误处理函数。
- 在main函数中,调用配置函数,配置时钟、GPIO、DMA和中断,填充源缓冲区的内容,使能DMA通道1,等待传输完成,清除传输完成标志,比较源缓冲区和目标缓冲区的内容,根据结果执行不同的操作。
- 定义一个时钟配置函数,使能GPIOC时钟和DMA1时钟。
- 定义一个GPIO配置函数,配置PC13为推挽输出模式,关闭LED。
- 定义一个DMA配置函数,复位DMA1通道1,配置DMA1通道1的初始化参数,配置DMA1通道1的传输完成中断。
- 定义一个中断配置函数,配置NVIC优先级分组为4,配置DMA1通道1中断。
- 定义一个DMA1通道1中断服务函数,判断DMA1通道1是否发生传输完成中断,清除传输完成中断标志,关闭LED。
- 定义一个错误处理函数,用户可以在这里添加自己的代码,闪烁LED。
5.总结
- 注意DMA的传输方向:DMA的传输方向是指数据是从外设到存储器,还是从存储器到外设,还是从存储器到存储器。根据不同的传输方向,需要配置相应的源地址和目的地址,以及DMA请求的类型。如果传输方向配置错误,会导致数据传输失败或混乱。
- 注意DMA的数据宽度:DMA的数据宽度是指外设和存储器的数据位数,可以是8位、16位或32位。根据不同的数据宽度,需要配置相应的数据对齐方式,以及源地址和目的地址的增量模式。如果数据宽度配置错误,会导致数据传输不完整或错误。
- 注意DMA的传输模式:DMA的传输模式是指DMA是以循环模式还是正常模式进行数据传输。循环模式是指当传输完成后,DMA会自动重新开始传输,直到停止或中断。正常模式是指当传输完成后,DMA会自动停止传输,等待下一次启动。根据不同的传输模式,需要配置相应的传输数量和中断事件。如果传输模式配置错误,会导致数据传输过多或过少,或者无法停止或启动。
- 注意DMA的优先级:DMA的优先级是指当多个DMA通道同时有传输请求时,哪个通道优先进行传输。DMA的优先级分为四级,分别是很高、高、中和低。根据不同的优先级,需要配置相应的控制寄存器。如果优先级配置错误,会导致数据传输的延迟或冲突。
- 注意DMA的服务范围:DMA的服务范围是指DMA可以访问的外设和存储器的范围。不同的DMA控制器和通道有不同的服务范围,需要根据芯片的总线架构和功能框图来确定。如果服务范围配置错误,会导致数据传输无法进行或异常。
- 注意DMA的缓存问题:DMA的缓存问题是指当使用带有缓存的内存区域作为DMA的源端或目的端时,可能会出现数据不一致或不更新的问题。这是因为CPU和DMA访问内存的方式不同,CPU可能会使用缓存来加速访问,而DMA直接访问实际的存储器。这样会导致缓存和存储器之间的数据不同步,或者缓存的数据被DMA覆盖。为了解决这个问题,需要在适当的时机清除或失效缓存,或者使用透写或共享的缓存策略,或者禁用缓存。