【江协STM32】8-1/2 DMA直接存储器存取、DMA数据转运&DMA+AD多通道
江协STM32学习笔记:8-1/2 DMA直接存储器存取、DMA数据转运&DMA+AD多通道
1. DMA简介
- DMA(Direct Memory Access)直接存储器存取
- DMA可以提供外设(一般是外设的数据寄存器,比如ADC的数据寄存器、串口的数据寄存器)和存储器(运行内存SRAM和程序存储器Flash)或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
- 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
- 每个通道都支持软件触发(一般用于存储器到存储器的数据转运)和特定的硬件触发(一般用于外设到存储器的数据转运)
- STM32F103C8T6 DMA资源:DMA1(7个通道)
2. 存储器映像

3. DMA框图
- 仲裁器:用于调度各个通道,防止产生冲突
- AHB从设备:用于配置DMA参数
- DMA请求:用于硬件触发DMA的数据转运

4. DMA基本结构
- 由于Flash是只读的,所以DMA不可以进行SRAM到Flash,或者Flash到Flash的转运操作。
- 数据宽度:指定一次转运要按多大的数据宽度来进行,可以选择字节Byte(8位)、半字HalfWord(16位)和字Word(32位)。
- 地址是否自增:指定一次转运完成后,下一次转运是否要把地址移动到下一个位置。
- 如果要进行存储器到存储器的数据转运,就需要把其中一个存储器的地址放在外设站点(左侧表格)。
- 传输计数器:指定转运次数,是一个自减计数器。当减到0后DMA就不会再进行数据转运了,之前自增的地址也会恢复到起始地址的位置。
- 自动重装器:指定传输计数器自减到0后,是否要自动恢复到最初的值。如果不重装,就是正常的单次模式;如果重装,就是循环模式。
- M2M(Memory to Memory):选择触发源(决定DMA在什么时机开始转运),置1为软件触发,置0为硬件触发。
- 软件触发:以最快的速度,连续不断地触发DMA,快速清零传输计数器,完成本轮转换。软件触发和自动重装器的循环模式不能同时使用,会导致DMA一直工作。一般用于存储器到存储器的转运。
- 硬件触发:触发源可以选择ADC、串口、定时器等。
- 写传输计数器时,必须先关闭DMA,再进行。

每个通道的硬件触发源不同, 必须使用它所在的通道。使用软件触发,通道可以任意选择。


5. 例:数据转运+DMA
将SRAM里的数组DataA,转运到另一个数组DataB中。
- 外设地址:DataA数组的首地址;存储器地址:DataB数组的首地址
- 数据宽度:两个数组的类型都是uint8_t,所以数据宽度按8位字节传输
- 地址是否自增:左右两个数组都自增
- 方向:由外设站点到存储器站点
- 传输计数器:需要转运7次,所以值给7
- 软件触发:因为是存储器到存储器的数据转运,不需要等待硬件时机
- 自动重装器:不需要,转运7次后传输计数器自减到0,DMA停止,转运完成
这里的数据转动是一种复制转运,转运完成后DataA的数据并不会消失。

6. 例:ADC扫描模式+DMA
左边是ADC扫描模式的执行流程,触发一次后,7个通道依次进行AD转换,转换结果均放在ADC_DR数据寄存器内。需要在每个单独的通道转换完成后,进行一次DMA数据转运,并且目的地址进行自增。
- 外设地址:写入ADC_DR寄存器的地址
- 存储器地址:可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址
- 数据宽度:因为ADC_DR和SRAM数组,要的都是uint16_t的数据,所以数据宽度都是16位的半字传输
- 地址是否自增:外设地址不自增,存储器地址自增
- 方向:外设站点到存储器站点
- 传输计数器:有7个通道,所以计数7次
- 自动重装器:如果ADC是单次扫描,那么DMA的传输计数器可以不自动重装;如果ADC是连续扫描,那么DMA就可以使用自动重装
- 触发选择:ADC_DR的值在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发选择ADC的硬件触发(ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,但应该会产生DMA请求去触发DMA转运)

7. DMA数据转运
7.1 接线图

7.2 代码
定义一个变量:uint8_t aa = 0x66;变量被存储的地址为2000 0000,其存储的位置是SRAM区。如果在变量前面加const关键字:const uint8_t aa = 0x66;变量被存储的地址为0800 0DF8,其存储的位置是Flash(只读)。当程序中出现大量不需要更改的数据时(比如查找表、字库数据),为了节省SRAM的空间,可以加const,将其定义在Flash内。
DMA初始化步骤:
- RCC开启DMA时钟(AHB总线设备)
- 直接调用DMA_Init函数,初始化各项参数
- 开关控制DMA_Cmd。如果选择硬件触发,需要在对应外设调用XXX_DMACmd,开启触发信号的输出
- 如果需要DMA的中断,调用DMA_ITConfig,开启中断输出。再在NVIC里配置相应的中断通道,然后写中断函数
- 如果转运完成,传输计数器清零了,这时想再给传输计数器赋值,需要DMA失能、写传输计数器、DMA使能
MyDMA.c(直接起DMA.c会与系统函数冲突)
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设站点的起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设站点的数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设站点的是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//存储器站点的起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器站点的数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点的是否自增
DMA_InitStructure.DMA_BufferSize = Size;//缓冲区大小(传输计数器)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向。外设站点作为数据源
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//选择硬件触发or软件触发。使用软件触发
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//传输模式(自动重装器)。不自动重装
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//因为是存储器到存储器的转运,使用的是软件触发,所以通道可以任意选择
DMA_Cmd(DMA1_Channel1, DISABLE);
}
// 调用一次就再启动一次DMA转运。要更改传输计数器的值,需要首先给DMA失能
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
DMA_Cmd(DMA1_Channel1, ENABLE);
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);// 检查DMA1通道1转运完成标志位。转运完成置1
DMA_ClearFlag(DMA1_FLAG_TC1);// 这个标志位需要手动清除
}
MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif
main.c
#include "stm32f10x.h" // Device
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};// DMA转运的源端数组
uint8_t DataB[] = {0, 0, 0, 0};// DMA转运的目的数组
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "DataA");
OLED_ShowString(3, 1, "DataB");
OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);// 显示地址
OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);// 数组名称就是地址,所以不需要加取地址符号
while(1)
{
DataA[0]++;
DataA[1]++;
DataA[2]++;
DataA[3]++;
// 显示转运前的DataA和DataB
OLED_ShowHexNum(2, 1, DataA[0], 2);
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000);
MyDMA_Transfer();
// 显示转运后的DataA和DataB
OLED_ShowHexNum(2, 1, DataA[0], 2);
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000);
}
}
其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具,第5节)、 Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)
转运后结果出现“04 00 00 00”的原因:设置数据宽度和是否自增的参数,外设站点(DMA_PeripheralDataSize_Byte、DMA_PeripheralInc_Enable)和存储器站点(DMA_MemoryDataSize_Byte、DMA_MemoryInc_Enable)的参数名称是不一样的。
8. DMA+AD多通道
8.1 接线图

8.2 代码
基于AD多通道代码修改(【江协STM32】7-1 ADC模数转换器、AD单通道&AD多通道,第3节)。
把数组AD_Value作为一个外部可调用数组,也放到头文件中声明。
ADC单次扫描+DMA单次转运:
AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
// 1、开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2、配置ADDCLK的分频器
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 3、配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入。在AIN模式下,GPIO口是无效的,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰,所以AIN模式就是ADC的专属模式
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);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
// 4、配置多路开关,选择规则组的输入通道
//ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 2, ADC_SampleTime_55Cycles5);// 在序列2的位置选择其他通道
// 5、配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;// 连续转换模式。连续转换(ENABLE)or单次转换(DISABLE)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;// 数据对齐。左对齐or右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 触发控制的触发源。不使用外部触发源,使用软件触发
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// 配置ADC是工作在独立模式or双ADC模式
ADC_InitStructure.ADC_NbrOfChannel = 4;// 通道数目。指定在扫描模式下,总共会用到几个通道。1~16
ADC_InitStructure.ADC_ScanConvMode = ENABLE;// 扫描转换模式。扫描模式(ENABLE)or非扫描模式(DISABLE)
ADC_Init(ADC1, &ADC_InitStructure);
// 初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度。想要DR寄存器低16位的数据,所以用半字16位来转运
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点的是否自增。不自增,如果自增源头地址就不是DR了
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器站点的数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点的是否自增
DMA_InitStructure.DMA_BufferSize = 4;//缓冲区大小(传输计数器)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向。外设站点作为数据源
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择硬件触发or软件触发。不使用软件触发
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//传输模式(自动重装器)。不自动重装
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//ADC1的硬件触发只接在了DMA1的通道1上
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);//开启ADC到DMA的输出
// 6、调用ADC_Cmd函数,开启ADC
ADC_Cmd(ADC1, ENABLE);
// 7、校准
ADC_ResetCalibration(ADC1);// 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET);// 返回复位校准的状态。如果没校准完成,就在while空循环内一直等待。软件置1,硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清零
ADC_StartCalibration(ADC1);// 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);// 获取校准状态
}
// 调用后ADC开始转换,连续扫描4个通道,DMA也同步进行转运,AD转换结果依次存放在AD_Value数组里
void AD_GetValue(void)
{
// 因为DMA也是单次模式,所以在触发ADC之前,需要重新写入一下传输计数器
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, 4);
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);// 软件触发转换
// 因为转运完成是在转换完成之后,所以等待ADC转换完成的代码就不需要了,只需要等待转运完成
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);// 检查DMA1通道1转运完成标志位。转运完成置1
DMA_ClearFlag(DMA1_FLAG_TC1);// 这个标志位需要手动清除
}
AD.h
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
void AD_GetValue(void);
#endif
main.c
#include "stm32f10x.h" // Device
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1,1,"AD0:");
OLED_ShowString(2,1,"AD1:");
OLED_ShowString(3,1,"AD2:");
OLED_ShowString(4,1,"AD3:");
while(1)
{
AD_GetValue();// 数据直接存储到AD_Value数组中
OLED_ShowNum(1, 5, AD_Value[0], 4);
OLED_ShowNum(2, 5, AD_Value[1], 4);
OLED_ShowNum(3, 5, AD_Value[2], 4);
OLED_ShowNum(4, 5, AD_Value[3], 4);
Delay_ms(100);// 减缓刷新速度
}
}
ADC连续扫描+DMA循环转运:
把ADC触发直接放在初始化的最后一行,当ADC触发之后,ADC连续转换,DMA循环转运。这样GetValue函数也就不需要了。
AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
// 1、开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2、配置ADDCLK的分频器
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 3、配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入。在AIN模式下,GPIO口是无效的,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰,所以AIN模式就是ADC的专属模式
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);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
// 4、配置多路开关,选择规则组的输入通道
//ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 2, ADC_SampleTime_55Cycles5);// 在序列2的位置选择其他通道
// 5、配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;// 连续转换模式。连续转换(ENABLE)or单次转换(DISABLE)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;// 数据对齐。左对齐or右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 触发控制的触发源。不使用外部触发源,使用软件触发
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// 配置ADC是工作在独立模式or双ADC模式
ADC_InitStructure.ADC_NbrOfChannel = 4;// 通道数目。指定在扫描模式下,总共会用到几个通道。1~16
ADC_InitStructure.ADC_ScanConvMode = ENABLE;// 扫描转换模式。扫描模式(ENABLE)or非扫描模式(DISABLE)
ADC_Init(ADC1, &ADC_InitStructure);
// 初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度。想要DR寄存器低16位的数据,所以用半字16位来转运
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点的是否自增。不自增,如果自增源头地址就不是DR了
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器站点的数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点的是否自增
DMA_InitStructure.DMA_BufferSize = 4;//缓冲区大小(传输计数器)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向。外设站点作为数据源
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择硬件触发or软件触发。不使用软件触发
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//传输模式(自动重装器)。自动重装
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//ADC1的硬件触发只接在了DMA1的通道1上
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);//开启ADC到DMA的输出
// 6、调用ADC_Cmd函数,开启ADC
ADC_Cmd(ADC1, ENABLE);
// 7、校准
ADC_ResetCalibration(ADC1);// 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET);// 返回复位校准的状态。如果没校准完成,就在while空循环内一直等待。软件置1,硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清零
ADC_StartCalibration(ADC1);// 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);// 获取校准状态
ADC_SoftwareStartConvCmd(ADC1, ENABLE);// 软件触发转换
}
AD.h
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif
main.c
#include "stm32f10x.h" // Device
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
OLED_Init();
AD_Init();
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);
OLED_ShowNum(2, 5, AD_Value[1], 4);
OLED_ShowNum(3, 5, AD_Value[2], 4);
OLED_ShowNum(4, 5, AD_Value[3], 4);
Delay_ms(100);// 减缓刷新速度
}
}
其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具,第5节)、 Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)
更多推荐




所有评论(0)