DMA --为cpu减负
DMA简介
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调DMA请求的优先权。
存储器和存储器之间的数据转运用软件触发。
外设到存储器的数据转运,一般使用硬件触发。
STM32103C8T6 DMA资源:只有DMA1(7个通道)
存储器映像
计算机系统的五大组成部分:运算器、控制器、存储器、输入设备、输出设备,
CPU:运算器和控制器
ROM就是只读存储器,是一种非易失性掉电不丢失的存储器
RAM就是随机存储器,是一种易失性、掉电丢失的存储器
程序存储器Flash,也就是主闪存
DMA主要特性
● 12个独立的可配置的通道(请求):DMA1有7个通道,DMA2有5个通道
● 每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
● 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推)。
● 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
● 支持循环的缓冲器管理
● 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
● 存储器和存储器间的传输
● 外设和存储器、存储器和外设之间的传输
● 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标。
● 可编程的数据传输数目:最大为65535
DMA框图
总线矩阵的左端,是主动单元,也就是拥有存储器的访问权。右边的这些,是被动单元,他们的存储器只能被左边的主动单元读写。主动单元 这里,内核有DCode和系统总线,可以访问右边的存储器。DCode总线是专门访问Flash的,系统总线是访问其他东西的。另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。那主动单元,除了内核CPU,剩下的就是DMA总线了。
DMA1有一条总线,DMA也有一条总线,下面还有一条DMA总线,这是以太网外设自己私有的DMA,这个可以不用管。
DMA1:有7个通道;DMA2:有5个通道。各个通道可以分别设置他们转运数据的源地址和目的地址,这样它们就可以独立的工作
DMA,即使总线矩阵的主动单元,可以读写各种寄存器,也是AHB总线上的被动单元。CPU通过这一条线路,就可以对DMA进行配置了。
DMA请求:请求就是触发的意思,这条线路右边的触发源,是各个外设,所以DMA请求就是DMA的硬件触发源。(比如ADC转换完成、串口接收到数据,需要触发DMA转运数据的时候,就会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了)
DMA基本结构
数据转运的两大站点:
外设寄存器站点、存储器站点(Flash,SRAM)
在这里可以看出,DMA的数据转运,可以是从外设到存储器,也可以从存储器到外设。另外,还有一种转运方式,就是存储器到存储器。由于Flash是只读的,所以DMA不可以进行SRAM到Flash,或者Flash到Flash的转运操作。
既然要进行数据转运,那肯定就要指定从哪里转到哪里,所以外设和存储器两个站点,就都有3个参数。第一个参数:外设起始地址、存储器起始地址,这两个参数决定了数据是从哪里来,到哪里去。第二个数据参数:数据宽度,这个参数的作用是,指定一次转运要按多大的数据宽度来进行转运。它可以选择字节、半字、字。字节是8位,也就是一次转运一个uint8_t,这么大的数据;半字是16位,就是一次转运一个uint16_t这么大的数据;字是32位,就是一次转运一个uint32_t这么大的数据。(比如转运ADC的数据,ADC的结果是uint6_t这么大的数据,所以这个参数就要选择半字)。第三个参数:地址是否自增,这个参数的作用就是,指定一次转运完成后,下一次转运,是不是要把地址移动到下一个位置。(比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边,显然地址是不用自增的,如果自增,那下一次转运就跑到别的寄存器哪里了。存储器,地址就需要自增,每转运一个数据后,就往后挪个坑,要不然下次在转就把上次的覆盖掉了。)
传输计数器:
这个东西就是用来指定,我们总共需要转运多少次。他是一个自减计数器(比如你写一个5,那DMA就只能进行5次数据转运,转运过程中,每转运一次,计数器的数就会减1,当减到0之后,DMA就不会进行数据转运了。另外,它减到0之后,之前自增的地址,就会恢复到起始地址的位置,以方便之后DMA开始新一轮的转运)
自动重装器:
传输计数器减到0之后,是否要自动恢复到最初的值(比如最初传输计数器给5,如果不使用自动重装器,那转运5次后,DMA就结束了;如果使用,就恢复到初值5)。
触发源:
硬件触发、软件触发,具体选择那个,由M2M这个参数决定(M2M是存储器到存储器的意思),当给M2M位1时,DMA就会选择软件触发,这个软件触发并不是调用某一个函数一次,触发一次,他这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换(软件触发不能和自动重装器同时使用,如果同时使用,DMA就停不下来了)。软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运,是软件启动、不需要时机,并且想尽快完成的任务,所以M2M位给1,就是软件触发。当M2M位给0,那就是使用硬件触发。硬件触发源可以选择ADC、串口、定时器等等。使用硬件触发的转运,一般都是与外设有关的转运,这些转运需要一定的时机(比如ADC转换完成、串口收到数据、定时时间到等),所以需要使用硬件触发,在硬件达到这些时机时,传个信号过来,来触发DMA进行转运。
开关控制(DMA_Cmd函数):
当给DMA使能后,DMA就准备就绪,可以进行转运了。DMA进行转运,有几个条件。第一:就是开关控制,DMA_Cmd必须使能;第二:传输计数器必须大于0;第三:必须有触发源信号,触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会在进行转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA,在为传输计数器写入一个大于0的数,在DMA_Cmd,给ENABLE,开启DMA。(注意:写传输计数器时,必须要先关闭DMA,在进行,不能在DMA开启时,写传输计数器)。
DMA请求
上面做这张图这是DMA1的请求映像,有7个通道。每个通道都有一个数据选择器,可以选择硬件触发或软件触发。
EN控制位就相当于开关控制:EN=0时不工作,EN=1时工作。
当M2M位=1时,选择软件触发。
通道1的硬件触发时ADC1、定时器2的通道3和定时器4的通道1,触发源应该怎么选择是对应的外设是否开启了DMA输出来决定的(比如使用ADC1,哪会有个库函数叫ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出,它才有效;如果现在定时器2的通道3,那也会有TIM_DMACmd函数,用来进行DMA输出控制)。总之触发源,具体使用那个,就把那个外设的DMA输出开启。之后,这7个触发源(DMA1有7个通道),进入到仲裁器,进行优先级判断,最终产生内部的DMA1请求(默认优先级是通道号越小,优先级越高)。
数据转运+DMA(软件触发)
将SRAM里的数组DataA转运到另一个数组DataB中
地址自增:两个数组地址都要自增
方向:外设站点转运到寄存器站点
传输计数器:在这里,显然要转运7次,所以传输计数器给7,自动重装暂时不需要。
触发选择:软件触发,因为这是存储器到存储器的数据转运,不需要等待硬件时机的,尽快转运完成就行了。最后调用DMA_Cmd,给DMA使能。这里的转运是一种复制转运,转运完成后DataA的数据并不会消失。
代码例程
接线图
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.H"
#include "dma.h"
uint8_t data[] = {0x01,0x02,0x03,0x04}; //源地址数组
uint8_t buff[] = {0,0,0,0}; //作为目的地址使用,存放转运后的结果
int main(void)
{
MYDMA_Init((uint32_t)data,(uint32_t)buff,4);
OLED_Init();
OLED_ShowString(1,1,"data");
OLED_ShowString(3,1,"buff");
OLED_ShowHexNum(1,8,(uint32_t)data,8);
OLED_ShowHexNum(3,8,(uint32_t)buff,8);
OLED_ShowHexNum(2,1,data[0],2);
OLED_ShowHexNum(2,4,data[1],2);
OLED_ShowHexNum(2,7,data[2],2);
OLED_ShowHexNum(2,10,data[3],2);
OLED_ShowHexNum(4,1,buff[0],2);
OLED_ShowHexNum(4,4,buff[1],2);
OLED_ShowHexNum(4,7,buff[2],2);
OLED_ShowHexNum(4,10,buff[3],2);
while(1)
{
data[0]++;
data[1]++;
data[2]++;
data[3]++;
OLED_ShowHexNum(2,1,data[0],2);
OLED_ShowHexNum(2,4,data[1],2);
OLED_ShowHexNum(2,7,data[2],2);
OLED_ShowHexNum(2,10,data[3],2);
OLED_ShowHexNum(4,1,buff[0],2);
OLED_ShowHexNum(4,4,buff[1],2);
OLED_ShowHexNum(4,7,buff[2],2);
OLED_ShowHexNum(4,10,buff[3],2);
Delay_ms(1000);
MYDMA_Transfer();
OLED_ShowHexNum(2,1,data[0],2);
OLED_ShowHexNum(2,4,data[1],2);
OLED_ShowHexNum(2,7,data[2],2);
OLED_ShowHexNum(2,10,data[3],2);
OLED_ShowHexNum(4,1,buff[0],2);
OLED_ShowHexNum(4,4,buff[1],2);
OLED_ShowHexNum(4,7,buff[2],2);
OLED_ShowHexNum(4,10,buff[3],2);
Delay_ms(1000);
}
}
#include "stm32f10x.h" // Device header
#include "DMA.h"
uint16_t MYDMA_size;
void MYDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t size)
{
MYDMA_size = size;
//开启时钟 DMA是AHB总线的设备,所以要用AHB开启时钟的函数
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
//定义结构体
DMA_InitTypeDef DMA_InitStruct = {0};
//给结构体赋值
DMA_InitStruct.DMA_BufferSize = size;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; //指定外设地址是源地址还是目的地址
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;
DMA_InitStruct.DMA_MemoryBaseAddr = AddrB;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; //自动重装
DMA_InitStruct.DMA_PeripheralBaseAddr = AddrA; //外设站点的基地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //传输字节
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //地址自增
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
//将参数写入寄存器
//这个是存储器到存储器的转运,软件触发,通道可以任意选择
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
//使能DMA
DMA_Cmd(DMA1_Channel1,DISABLE);
}
void MYDMA_Transfer(void)
{
//传输计数器赋值,必须要先给DMA使能
DMA_Cmd(DMA1_Channel1,DISABLE);
//给传输计数器赋值
DMA_SetCurrDataCounter(DMA1_Channel1,MYDMA_size);
DMA_Cmd(DMA1_Channel1,ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
#ifndef _DMA_H_
#define _DMA_H_
void MYDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t size);
void MYDMA_Transfer(void);
#endif
ADC扫描模式+DMA(硬件触发)
在这里有7个通道,触发一次,7个通道一次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里。在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,所以在这里DMA的配置就是,外设地址,写入ADC_DR这个寄存器的地址。存储器的地址,可以在SRAM中定义一个数组,然后把这个数组的地址当作存储器的地址。之后数据宽度,因为ADC_DR和SRAM数组,我们要的都是uint16_t的数据,所以数据宽度都是16位的半字传输。地址自增:外设地址不要自增,存储器地址自增。传输方向,是外设站点到存储器站点。传输计数器,这里通道有7个,所以需要计数7次。计数器是否自动重装,这里可以ADC的配置,ADC如果是单次扫描,那DMA的传输计数器就不要配置自动重装,如果ADC是连续扫描模式,那DMA的传输计数器就需要自动重装。
代码例程
接线图
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
OLED_ShowNum(1, 5, AD_Value[0], 4); //显示转换结果第0个数据
OLED_ShowNum(2, 5, AD_Value[1], 4); //显示转换结果第1个数据
OLED_ShowNum(3, 5, AD_Value[2], 4); //显示转换结果第2个数据
OLED_ShowNum(4, 5, AD_Value[3], 4); //显示转换结果第3个数据
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4]; //定义用于存放AD转换结果的全局数组
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换,使能,每转换一次规则组序列后立刻开始下一次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,使能,扫描规则组的序列,扫描数量由ADC_NbrOfChannel确定
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,选择半字,对应16为的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,始终以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,给定存放AD转换结果的全局数组AD_Value
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,选择半字,与源数据宽度对应
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
DMA_InitStructure.DMA_BufferSize = 4; //转运的数据大小(转运次数),与ADC通道数一致
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式,与ADC的连续转换一致
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发转运到存储器
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA和ADC使能*/
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA1的通道1使能
ADC_DMACmd(ADC1, ENABLE); //ADC1触发DMA1的信号使能
ADC_Cmd(ADC1, ENABLE); //ADC1使能
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
/*ADC触发*/
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif