深入理解STM32 DMA工作原理,附HAL库和标准库编程示例
1、DMA简介
DMA,全称为:Direct Memory Access,即直接存储器访问。
DMA传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,减轻CPU负担,使 CPU 的效率大为提高。
DMA可用于存储器和存储器、外设和存储器、存储器和外设之间的传输。(也可用于外设与外设之间的传输,但感觉用的较少),这里的外设指的是外设的数据寄存器DR(Data Register),存储器指的是运行内存SRAM和闪存Flash。
闪存(Flash)、SRAM、外设的 SRAM、APB1、APB2 和 AHB 外设均可作为访问的源和目标。
DMA的通道
STM32F103有12个独立可配置的通道(数据转运的路径):DMA1(7个通道),DMA2(5个通道)每个通道都支持软件触发和特定的硬件触发。
软件触发
例如:存储器(Flash)—>存储器(SRAM)/存储器(SRAM)—>存储器(SRAM),软件触发以后,DMA就会以最快的速度,将指定个数的数据全部转运完成。指定个数:DMA传输计数器的数量(最大数量:65536)
特定的硬件触发
一般外设的数据转换完成是有一定时机的。例如:外设(ADC的DR)–>存储器(SRAM),用DMA来转运ADC的数据,ADC每个通道AD转换完成后,就硬件触发一次DMA,DMA再转运数据,触发一次转运一次,这样转运的数据才是正确的。
特定:每个DMA通道的硬件触发源不同。数据从一个地方转运到另一个地方,需要一个通道,各个通道相互独立、互不干扰。
2、存储器映像(STM32F103)
本图片源自B战江科大STM入门教程PPT
ROM和RAM
ROM:只读存储器,存储的数据掉电不丢失。
RAM:随机存储器(可读写寄存器),数据可以被动态地读取、写入和修改。
ROM用于存储固化的程序和数据,且数据不易变更,RAM用于存储运行中的程序和临时数据,两种存储器相互补充以实现有效的数据处理和存储。
Flash:存储C语言编译后的程序代码和常量数据。
系统存储器和选项字节,实际上他们的存储介质也是Flash,但为了区别主闪存(程序存储器Flash),这两块的位置位于ROM区的最后。
系统存储器:一般由厂家写入,不允许修改。 选项字节:存的主要是Flash的写保护、读保护、看门狗等配置。
外设寄存器和内核外设寄存器,实际上他们的存储介质也是SRAM,但为了区别运行内存SRAM,这两块的位置同样位于RAM区的最后
内核外设:如NVIC和Systick
Flash与SRAM使用注意点
1、在程序中定义一个变量:int aa=0x66; 变量aa就存储在SRAM里面 变量地址以0x20开头。
在程序中定义一个const变量(常量): const int aa=0x66; 常量aa就存储在Flash里面 变量地址以0x08开头。
当我们的程序中出现一大批不需要修改的数据(查找表、字库数据等)时,就可以定义成常量,从而节省SRAM的空间。
2、想查找某个寄存器地址:外设地址+寄存器偏移地址 如:ADC1->DR(ADC1是结构体指针,访问结构体成员时会用到->)
0地址的别名区
这里需要把我们要执行的程序映射到0地址来,如果映射在Flash区,就从Flash执行,如果映射在系统存储器区,就从系统存储器执行,如果映射到SRAM就从SRAM启动,怎么选择,由BOOT0和BOOT1两个引脚决定。
存储器完整映像(STM32F103)
程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。
对于各种寄存器的理解
寄存器是一种特殊的存储器。一方面,CPU可以对寄存器进行读写,就像读写运行内存SRAM一样。另一方面,寄存器的每个位之后都连接了一根导线,这些导线可以控制外设电路的状态,比如引脚的高低电平、导通和断开开关、切换数据选择器或者多位结合起来当做计数器、数据寄存器。寄存器是连接软件和硬件的桥梁,软件读写寄存器就相当于控制硬件的执行。
3、DMA框图
主动单元与被动单元
主动单元拥有存储器的访问权,主动单元有内核(CPU)和DMA。
被动单元他们的存储器只能被主动单元读写,被动单元是各种存储器。
仲裁器:虽然多个通道可以独立转运数据,但最终DMA总线只有一条,所有的通道只能分时复用这一条DMA总线。在总线矩阵,也有一个仲裁器,如果DMA与CPU要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突,不过总线仲裁器,仍然保证CPU得到一半的总线带宽,使CPU正常工作。
AHB从设备:DMA自身的寄存器,用于配置DMA参数,连接在了总线矩阵右边的AHB总线上。所以DMA既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元,CPU可以对DMA进行配置。
DMA请求:用于硬件触发DMA的数据转运。
有无DMA参与数据转运的传输路径比较
没有DMA参与的数据传输(使用CPU)
CPU传输数据以内核作为中转站,用ADC采集数据转移到运行内存SRAM举例说明。
1.内核通过系统总线经总线协调,获取ADC采集的数据。
2.内核再通过系统系统经总线协调,把数据存放SRAM中。
使用DMA对数据进行传输(不使用CPU,减轻CPU负担)
DMA的传输过程(ADC通过DMA转运数据到运行内存SRAM):
1.外设向DMA发出请求
2.DMA产生应答
3.外设释放请求
4.启动DMA传输
DMA传输具体路径(个人理解):
①DMA控制器从AHB外设获取ADC采集的数据,存储在DMA通道中
②使用AHB把外设ADC采集的数据经由DMA通道存放在SRAM中。
DMA传输过程总结
每次DMA传送由3个操作组成,
1.取数据。第一次从源地址(如ADC的DR)取数据,之后在哪取由DMA参数地址是否自增决定。
2.存数据。第一次在目的地址(如运行内存SRAM定义的数组)存数据,之后在哪存由DMA参数地址是否自增决定。
3.传输计数器减1。
相关注意点
Flash是ROM(只读存储器)的一种,如果通过总线直接访问的话,无论是CPU还是DMA,都是只读的,只能读取数据而不能写入。如果的DMA的目的地址填了Flash的地址,那么转运时就会出错。Flash也不是绝对不可写入,要配置接口控制器对Flash进行写入。
小结
DMA转运本质都是存储器与存储器之间的数据转运,从某个地址取内容,再放到另一个地址去。
CPU和DMA作为主动单元可访问各种存储器。
对于DMA来说:闪存Flash:只可以读,不可以写。 运行内存SRAM:任意读写。
外设寄存器:有的寄存器是可读/可写的,具体由不同的寄存器决定。
但大多数我们用的是数据寄存器(DR),数据寄存器是可以正常读写的。
4、DMA基本结构
本图片源自B战江科大STM入门教程PPT
两个站点:外设站点和存储器站点
两个站点只是那么规定的,其实外设站点也可以写存储器的地址,表示存储器–>存储器。存储器站点也可以写外设地址。
STM32手册中的存储器是特指Flash和SRAM,不包括外设寄存器。外设寄存器一般称为外设。
方向:外设–>存储器 存储器–>外设 存储器–>存储器(Flash–>SRAM、SRAM–>SRAM两种,因为Flash是只读的)
实际传输方向(从某个地址传到另一个地址)由两个站点地址和方向参数决定。
数据宽度:指定一次转运的数据宽度 8位:字节 16位:半字 32位:字(字的概念看后面的拓展)
地址是否自增:指定一次转运完成后,下次转运是不是要把地址移动到下一位置去,相当于指针p++。
例如:ADC连续转换模式,利用DMA转运数据,外设地址是ADC_DR寄存器地址,显然外设地址是不需要自增的,如果自增,那么下次转运就跑到其他地址上了。
存储器(如一个数组)这边,地址就需要自增,每转运一个数据后,就往后移一下地址,便于将新数据存储起来,否则就会将上次的数据覆盖掉。
传输计数器:DMA每转运一次,计数器就减1,减到零就不会进行数据转运了。
另外计数器减到0之后,之前自增的地址也会恢复到起始地址的位置。
故DMA传输完成一次(计数器减到零),再次开启,数据会从之前的源地址和目的地址开始传递。
再次开启的方法:1.重置传输计数器的值,重置前要失能DMA 2.打开DMA自动重装器,即软件上开启循环模式。
自动重装器:如果设置了自动重装(循环模式/正常模式),当计数器减为0后,就会自动重装计数器的起始值。
采用什么触发:由M2M(memory to memory)参数决定,
1:软件触发(不是调用函数,是以最快的速度,连续不断的触发DMA)。
0:硬件触发时还在相应的硬件外设中调用XXX_DMACmd,开启触发信号的输出。
软件触发和循环模式不能同时用(否则DMA转运就停不下来)
开关控制:DMA_cmd(DMA_ENABLE/DMA_DISABLE) DMA使能后,DMA就准备就绪,可以进行转运了。
DMA转运的前提:
1.开启DMA。(标准库:DMA_ENABLE HAL库:相关开启函数)
2.传输计数器大于0 。
3.触发源:必须有触发信号。(HAL库DMA参数似乎没有规定DMA由什么触发)
相关注意点:
当传输计数器等于0且没有自动重装时,无论是否触发都不会进行转运了,此时就要DMA_DISABLE关闭DMA,再在计数器写入计数值(写传输计数器时必须先DMA_DISABLE),再DMA_ENABLE,开启DMA,DMA才能继续工作。
5、DMA的触发源
DMA1触发源:
DMA2触发源:
原则是当一个通道有多个硬件触发时,多个硬件都可以触发(或门),但一般只有一个硬件进行触发。
6、DMA数据宽度对齐
如果是小的数据转到大的数据,高位就补0
如果把大的数据转到小的数据,高就会被舍弃掉
7、DMA中断
8、DMA转运实例
1、利用DMA将数据从运行内存SRAM转运到另一个SRAM中。(数组数据传到另一个数组中去)
本图片源自B战江科大STM入门教程PPT
DMA相关配置
外设地址:位于SRAM的数组名DataA
外设地址是否自增:是
数据宽度:8位
存储器地址:位于SRAM的数组名DataB
存储器地址是否自增:是
数据宽度:8位
传输计数器个数:7次(与数组个数相符)
自动重装器:设置为0,转运一次即可
触发方式:软件触发
标准库具体程序
①初始化函数及DMA开始转运函数
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size; //定义全局变量,用于记住Init函数的Size,供Transfer函数使用
/**
* 函 数:DMA初始化
* 参 数:AddrA 原数组的首地址
* 参 数:AddrB 目的数组的首地址
* 参 数:Size 转运的数据大小(转运次数)
* 返 回 值:无
*/
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size; //将Size写入到全局变量,记住参数Size
/*开启时钟*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
//外设数据宽度,选择字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设地址自增,选择使能
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器基地址,给定形参AddrB
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
//存储器数据宽度,选择字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_BufferSize = Size; //转运的数据大小(转运次数)
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使能*///这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
DMA_Cmd(DMA1_Channel1, DISABLE);
}
/**
* 函 数:启动DMA数据转运
* 参 数:无
* 返 回 值:无
*/
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA失能,在写入传输计数器之前,需要DMA暂停工作
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);//写入传输计数器,指定将要转运的次数
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,开始工作
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //等待DMA工作完成
DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志位
}
②主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04}; //定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0}; //定义测试数组DataB,为数据目的地
int main(void)
{
//1、DMA初始化,把源数组和目的数组的地址传入
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
//2、OLED显示转运之前数组DataB
OLED_ShowHexNum(2, 1, DataB[0], 2);
OLED_ShowHexNum(2, 4, DataB[1], 2);
OLED_ShowHexNum(2, 7, DataB[2], 2);
OLED_ShowHexNum(2, 10, DataB[3], 2);
//3、使能DMA转运,从DataA转运到DataB
MyDMA_Transfer();
//上面的MyDMA_Transfer();可用下面注释的代码代替,区别在于下面使用CPU对数据进行转运
//uint8_t i = 0;
//for(i = 0; i < 4; i++)
// {
// DataB[i]=DataA[];
// }
//4、OLED显示转运之后数组DataB
OLED_ShowHexNum(2, 1, DataB[0], 2);
OLED_ShowHexNum(2, 4, DataB[1], 2);
OLED_ShowHexNum(2, 7, DataB[2], 2);
OLED_ShowHexNum(2, 10, DataB[3], 2);
}
HAL库具体程序
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/led/led.h"
#include "./BSP/beep/beep.h"
#include "./BSP/key/key.h"
#include "./BSP/WWDG/wwdg.h"
#include "string.h"//里面有memset(dest_buf, 0, 10);函数
DMA_HandleTypeDef g_dma_handler;
//内存到内存 DMA传输 所有通道都支持
uint8_t src_buf[10]={0x0a, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
uint8_t dest_buf[10];
void dma_init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();//DMA1通道使能
g_dma_handler.Instance=DMA1_Channel1;
g_dma_handler.Init.Direction=DMA_MEMORY_TO_MEMORY;
g_dma_handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE;
g_dma_handler.Init.MemInc=DMA_MINC_ENABLE; //内存地址自增
g_dma_handler.Init.Mode=DMA_NORMAL; //内存到内存不支持循环模式
g_dma_handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE;
g_dma_handler.Init.PeriphInc=DMA_PINC_ENABLE; //外设地址自增 这里其实还是内存
g_dma_handler.Init.Priority=DMA_PRIORITY_HIGH;
HAL_DMA_Init(&g_dma_handler);
HAL_DMA_Start(&g_dma_handler,(uint32_t)src_buf,(uint32_t)dest_buf,0);
//最后一个参数是传输长度,初始化时不让他传输
}
void enble_dma_transmit(uint16_t cndtr)
{
__HAL_DMA_DISABLE(&g_dma_handler); //失能DMA,设置传输长度
DMA1_Channel1->CNDTR=cndtr; //或g_dma_handler.Instance->CNDTR=cndtr;
__HAL_DMA_ENABLE(&g_dma_handler); //使能DMA
//也可以仿照标准库函数,在传输完成函数里判断是否传输完成,这样在主函数中就不需要再判断了
//while(__HAL_DMA_GET_FLAG(&g_dma_handler,DMA_FLAG_TC1)==RESET);
//__HAL_DMA_CLEAR_FLAG(&g_dma_handler,DMA_FLAG_TC1);//赋值传输长度并等待转换完成
}
int main(void)
{
uint8_t key=0;
HAL_Init(); //初始化HAL库
sys_stm32_clock_init(RCC_PLL_MUL9);//9倍频,72MHz
delay_init(72); //延时初始化
led_init();
beep_init();
key_init();
usart_init(115200);
dma_init();
while(1)
{
key=key_scan(0);
if (key == WKUP_PRES)
{
//将0赋给地址为dest_buf的前10个元素/或者直接在数组定义时赋0
memset(dest_buf, 0, 10);
enble_dma_transmit(10);
while(1)
{
if(__HAL_DMA_GET_FLAG(&g_dma_handler,DMA_FLAG_TC1))//通道1
printf("传输完成\r\n");
__HAL_DMA_CLEAR_FLAG(&g_dma_handler,DMA_FLAG_TC1);
}
}
}
}
此标准库与HAL库程序的异同
1.判断DMA传输完成函数既可以放在DMA_transfer()函数里面判断,也可以放在主函数中判断,大同小异,个人倾向于放在传输函数里面,这样一来只要出了传输函数,DMA就已经传输完成了,方便进行其他的操作。
2.启动DMA传输的条件相同,标准库和HAL库都是1.失能DMA(只有失能才能对传输计数器的个数进行设置) 2.对传输计时器个数赋值 3.使能DMA开始传输。另外赋传输个数时标准库使用函数赋个数,HAL库示例直接使用寄存器操作。再次重述一下DMA启动传输的3个条件:①对DMA进行使能 ②传输计数器个数不为0 ③必须有触发信号(软件触发/硬件触发)。
3.触发DMA的参数标准库有特别指出,参数是M2M。而HAL库并没有特别指出。在后面的外设与DMA配合转运数据的实验中,HAL库用__HAL_LINKDMA(&g_uart1_handler, hdmatx,g_dma_handle)函数来连接硬件与DMA。后面会详细说。
2、ADC配合DMA进行数据转运
DMA第1种运用在实际运用的很少(为了理解DMA的使用),DMA用的最多的就是和外设相互配合,用DMA实现对数据的转运,最典型的就是ADC+DMA相互配合,ADC每转换完成一次数据,就触发DMA对转换的数据进行转运,实现对ADC转换数据的保存。(ADC只有一个数据寄存器,每次转换的结果存放在数据寄存器中,当有多个ADC通道需要转换时,最新ADC转换的数据会覆盖之前的转换数据。)
本图片源自B战江科大STM入门教程PPT
DMA相关配置:
外设地址:ADC_DR寄存器地址
外设地址是否自增:否
数据宽度:16位(半字)
存储器地址:位于SRAM的数组名ADValue
存储器地址是否自增:是
数据宽度:16位(半字) 由ADC_DR的位数决定
传输计数器个数:7次(与ADC通道数对应)
自动重装器:若要连续扫描图中的7个通道就设置为1,否则设置为0
触发方式:ADC硬件触发
ADC连续模式的缺点就是每个通道转换完成后就会覆盖ADC_DR的值。
ADC的连续扫描模式可以与DMA的的循环模式是相互配合的。
此实验代码会在后续文章中讲解。
9、相关拓展
因为STM32的CPU是32位的,寻址范围也是32位的范围(0x0000 0000-0xffff ffff)最大存储空间4G。
32位机/64位机:字长是32位/字长是64位。
32位机的地址总线也是32根。(每个地址对应一个字节的数据,例如0x0000 00ff(地址):0000 0001(8位数据))
CPU的字长表示每次处理数据的能力,总是以8的倍数为单位。
欢迎提问讨论,共同进步,望诸君共勉!
作者:文析