DMA直接存储器可以直接访问STM32内部的存储器,包括外设寄存器(一般指外设的数据寄存器DR,如ADC的数据寄存器、串口数据寄存器等)、运行内存SRAM(存储运行变量)、程序存储器FLASH(存储程序代码)等。DMA可以提供 外设寄存器和存储器 或者 存储器和存储器之间 的高速数据传输,无须CPU干预,节省了CPU的资源。翻译成人话就是,是一个数据转运小助手,主要用来协助CPU完成数据转运的工作。下面是stm32中DMA的一些配置:
- stm32系列芯片共有12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)。
- 每个通道都支持软件触发和特定的硬件触发。
- 软件触发应用场景:数据源中的数据已确定。如将FLASH中的数据转运到SRAM中(存储器–>存储器),一次触发后会将数据以最快的速度全部转运完毕。
- 硬件触发应用场景:数据源中的数据没有全部确定,需要在特定的时机转运数据。如将ADC数据转运到存储器(外设寄存器–>存储器),等对应ADC通道的数据转换完成后,才硬件触发一次DMA转运数据。
- STM32F103C8T6型号的DMA资源:DMA1(7个通道)。
存储映像:
计算机有五大组成部分:运算器,控制器,存储器,输入设备,输出设备。计算机的核心关键部分就是CPU和存储,上述的运算器和控制器回合在一起组成CPU,而存储器主要关心存储器的内容和地址?。
- ?程序存储器FLASH:下载程序的位置,程序一般也是从主闪存里开始运行。若某变量地址为0x0800_xxxx,那么它就是属于主闪存的数据。
- 系统存储器:存储BootLoader程序(俗称“刷机”),芯片出厂时自动写入,一般不允许修改。
- 选项字节:存储的主要是FLASH的读保护、写保护、看门狗等配置。下载程序可以不刷新选项字节的内容,从而保持相应配置不变。
- 运行内存SRAM:在程序中定义变量、数组、结构体的地方,类似于电脑的内存条。
- 外设寄存器:初始化各种外设的过程中,最终所读写的寄存器就属于这个区域。
- 内核外设寄存器:就是NVIC和SysTick。由于不是同一个厂家设计,所以专门留出来内核外设的地址空间,和其他外设的地址空间不一样。
- 注:由于stm32是32位的系统,所以寻址空间最大可达4GB(每个地址都代表1Byte),而stm32的存储器硬件最多也就是KB级别的,所以实际上4GB的寻址空间使用率远远低于1%。
注:上表中前三者存储介质都是FLASH,但是一般说“FLASH”就是代指“主闪存FLASH”,而不是另外两块区域。
注:上表中后三者存储介质也是SRAM,但是一般将“SRAM”就是代指“运行内存”,“外设寄存器”就直接叫“寄存器”。
?
DMA电路框图:?
将从以下加粗的四大部分介绍DMA的电路结构。
- 总线矩阵: 为了高效有条理的访问存储器,设置了总线矩阵。左端是主动单元,拥有存储器的访问权;右端是被动单元,它们的存储器只能被左端的主动单元读写。
- 总线矩阵内部的仲裁器:如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。但是总线仲裁器仍然会保证CPU得到一半的总线带宽,以确保CPU正常工作。
- 主动单元:
- Cortex-M3核心(左上角):包含了CPU和内核外设。剩下的所有东西都可以看成是存储器,比如Flash是主闪存、SRAM是运行内存、各种外设寄存器也都可以看成是一种SRAM存储器。
- ICode总线:指令总线。加载程序指令。
- DCode总线:数据总线,专门用来访问Flash。
- 系统总线:是访问其他东西的。
- DMA总线:用于访问各个存储器,包括DMA1总线(7个通道)、DMA2总线(5通道)、以太网外设的私有DMA总线。由于DMA要转运数据,所以DMA也有访问的主动权。
- DMA1、DMA2:各个通道可以分别设置转运数据的源地址和目的地址,所以各个通道可以独立的进行数据转运。
- 仲裁器:调度各个通道,防止产生冲突。虽然多个通道可以独立地转运数据,但是DMA总线只有一条,所以所有的通道都只能 分时复用 这一条DMA总线,若通道间产生冲突,就会由仲裁器根据通道的优先级决定使用顺序。
- AHB从设备:用于配置DMA参数,也就是DMA自身的寄存器。DMA的外设配置寄存器直接连接在了被动单元侧的AHB总线上。所以DMA既是总线矩阵上的主动单元,可以读写各种寄存器;同时也是AHB总线上的被动单元。CPU配置DMA的线路:“系统”总线–>总线矩阵–>AHB总线–>DMA中的AHB从设备。
- 被动单元:
- Flash:主闪存,只读存储器。若直接通过总线访问(无论是CPU还是DMA),都只能读取数据而不能写入。若DMA的目的地址为FLASH区域,那么转运就会出错。要想对Flash写入,可以通过“Flash接口控制器”。
- SRAM:运行内存,通过总线可以任意读写。
- 各种外设寄存器(右侧两个方框):需要对比参考手册中的描述,这些寄存器的类型可能为 只读/只写/读写。不过日常主要使用的数据寄存器,都是可以正常读写的。
- DMA请求: 用于硬件触发DMA的数据转运。“请求”就是“触发”的意思,此线路右侧的触发源是各个外设,所以这个“DMA请求”就是 DMA的硬件触发源,如ADC转换完成、串口接收到数据等信号。
?
所以寄存器是一种特殊的存储器,寄存器的两大作用:
- 存储数据。被CPU或DMA等读写,就像读写运行内存一样。
- 控制电路。寄存器的每一位都接了一个导线,可以用于控制外设电路的状态,比如置引脚的高低电平、导通或断开开关、切换数据选择器,或者多位结合起来当做计数器、数据寄存器等。
?
所以寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。既然外设相当于寄存器,寄存器又是存储器,那么使用DMA转运数据,本质上就是从某个地址取数据,再放到另一个地址去。?
任务是将SRAM数组DataA转运到另一个SRAM数组DataB(存储器到存储器)。
- 下面给出各参数的配置说明:
- 两个站点的参数:外设地址->DataA数组首地址、存储器地址->DataB数组首地址;数据宽度都是8位;为保证数据的一一对应,“外设”和“存储器”都设置为地址自增。
- “方向”参数:默认是“外设”–>“存储器”,当然也可以将方向反过来。
- 传输计数器:数组大小为7,所以计数器为7。
- 自动重装:不需要。
- 触发源:软件触发。由于是“存储器->存储器”的触发,所以不需要等待转运时机。
- 开关控制:最后调用DMA_Cmd开启DMA转运。
- 注:上述为“复制转运”,转运完成后DataA数据不会消失。
?
期望将外设ADC多通道(扫描模式)的数据,依次搬运到SRAM中的ADValue数组中(外设到存储器)。
注意下图左侧给出了ADC的扫描模式示意图,ADC每触发一次,7个通道依次进行ADC数据转换,每个通道转换完成时,都会将转换结果放到ADC_DR数据寄存器中,也就是ADC的所有通道共用一个ADC数据寄存器。所以每个通道转换完成后,都需要DMA立即转运一次,防止新来的通道数据将之前的通道数据覆盖掉。
?
- 下面给出各参数的配置说明:
- 起始地址:“外设站点”地址设置为ADC_DR寄存器的地址;“存储器站点”地址为ADValue(SRAM)的首地址。
- 数据宽度:“外设站点”和“存储器站点”都设置为16位(HalfWord)。
- 地址自增:“外设站点”不自增,“存储器站点”自增。
- 方向:“外设”站点为发送端。
- 传输计数器:按照ADC需要扫描的通道数来,即为7。
- 计数器是否自动重装:ADC单次扫描,可以不自动重装;ADC连续扫描,DMA就使用自动重装,此时ADC启动下一轮转换,DMA同时也启动下一轮转运,可以实现同步工作。
- 触发选择:选择硬件触发——ADC单通道转换完成。虽然前面说过,只有当所有通道都转换完成后,才会触发转换完成EOC标志,其余时间没有任何中断/标志,但实际上,硬件中保留了单个通道针对DMA的请求(虽然参考手册只字不提)。
?
?
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "DMA_User.h"
#include "Delay.h"
uint8_t DataA[] = {0x11,0x22,0x33,0x44};//源端数组
uint8_t DataB[] = {0x00,0x00,0x00,0x00};//目的端数组
int main(void){
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"DataA:");
OLED_ShowHexNum(1,7,(uint32_t)DataA,8);
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_ShowString(3,1,"DataB:");
OLED_ShowHexNum(3,7,(uint32_t)DataB,8);
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);
// //验证存储器映像-注意srm32中地址都是32位的
// uint8_t aa = 0x66;//存储在运行内存SRAM中
// const uint8_t bb = 0x55;//存储在Flash中
// OLED_ShowHexNum(1,1,aa,2);
// OLED_ShowHexNum(1,4,(uint32_t)&aa,8);//SRAM地址0x2000开头
// OLED_ShowHexNum(2,1,bb,2);
// OLED_ShowHexNum(2,4,(uint32_t)&bb,8);//Flash地址0x0800开头
// OLED_ShowHexNum(3,1,(uint32_t)&ADC1->DR,8);//ADC外设寄存器地址
//DMA 初始化
DMA_User_Init((uint32_t)&DataA, (uint32_t)&DataB, 4);
while(1){
//改变数据并显示
DataA[0]++;
DataA[1]++;
DataA[2]++;
DataA[3]++;
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);
//转运数据并显示
DMA_User_Transfer();
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);
};
}
?DMA_User.h
#ifndef __DMA_USER_H
#define __DMA_USER_H
void DMA_User_Init(uint32_t AddrA, uint32_t AddrB, uint16_t BuffSize);
void DMA_User_Transfer(void);
#endif
- DMA_User.c
#include "stm32f10x.h" // Device header
uint16_t DMA_User_BuffSize;//传输计数器
//DMA初始化-DMA1_Channel1-从AddrA到AddrB转运,不使能
void DMA_User_Init(uint32_t AddrA, uint32_t AddrB, uint16_t BuffSize){
DMA_User_BuffSize = BuffSize;
//1.开启RCC
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//2.初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_StructInit(&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 = BuffSize;//传输计数器
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//方向:外设作为源端
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//软件触发
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//是否使用自动重装
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//DMA多通道才会用到
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
//3.开关控制使能
DMA_Cmd(DMA1_Channel1, DISABLE);
//4.开启硬件触发源(按需求选做)
}
//手动触发一次DMA的转运
void DMA_User_Transfer(void){
//触发转运
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,DMA_User_BuffSize);
DMA_Cmd(DMA1_Channel1, ENABLE);
//判断转运完成,并清除相应标志位
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
DMA_Cmd(DMA1_Channel1, DISABLE);
}
??main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "ADC_User.h"
int main(void){
uint16_t ADC_value[4] = {0,0,0,0};
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"C1:+0.00V");
OLED_ShowString(2,1,"C2:+0.00V");
OLED_ShowString(3,1,"C3:+0.00V");
OLED_ShowString(4,1,"C4:+0.00V");
//ADC扫描模式初始化
ADC_User_InitMuti((uint32_t)&ADC_value,4);
ADC_User_Start();
while(1){
OLED_ShowFloat(1,4,(float)ADC_value[0]*3.3/4095,1,2);
OLED_ShowFloat(2,4,(float)ADC_value[1]*3.3/4095,1,2);
OLED_ShowFloat(3,4,(float)ADC_value[2]*3.3/4095,1,2);
OLED_ShowFloat(4,4,(float)ADC_value[3]*3.3/4095,1,2);
};
}
ADC_User.c新增函数
//ADC多通道初始化-ADC1的通道0~3-PA0~PA3共四个通道
void ADC_User_InitMuti(uint32_t AddrB, uint16_t BuffSize){
//1.开启外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//6分频使得ADC时钟为12MHz
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//2.配置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);
//3.配置ADC多路开关,选择通道进入规则组
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_1Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_1Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_1Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_1Cycles5);
//4.配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//不使用外部触发(软件触发)
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//独立模式
ADC_InitStructure.ADC_NbrOfChannel = BuffSize;//通道总数(非扫描模式,此参数不起作用)
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//连续转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描模式
ADC_Init(ADC1, &ADC_InitStructure);
//5.配置ADC开关控制
ADC_Cmd(ADC1, ENABLE);
//6.进行ADC校准
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET);
//7.开启ADC1硬件触发源
ADC_DMACmd(ADC1, ENABLE);
//8.配置DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//(uint32_t)ADC_Value;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_BufferSize = BuffSize;//传输计数器
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//方向:外设作为源端
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//硬件触发
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//是否使用自动重装
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//DMA多通道才会用到
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
//9.DMA开关控制使能
DMA_Cmd(DMA1_Channel1, ENABLE);
}
//记得在头文件中声明