声明:本人跟随b站江科大学习,本文章是观看完视频后的一些个人总结和经验分享,也同时为了方便日后的复习,如果有错误请各位大佬指出,如果对你有帮助可以点个赞小小鼓励一下,本文章建议配合原视频使用❤️
如果你也正在学习STM32可以订阅本专栏,后续将不定期更新( ˘ ³˘)❤️
文章目录
前言
还是在观看文章之前给大家留下几个问题,你可以在读完文章后问问自己有没有弄清楚这几个问题,如若清楚了,那么恭喜你已经基本了解了DMA的使用
- 外设和存储器概念
- DMA的作用
- DMA的转运实际上是地址的转运,为什么
- 软件触发与外部中断的软件触发的区别
- 什么是特定的硬件触发
- 站点的三个参数如何选择
- 如何根据实际情况配置DMA
理论部分
1.DMA简介
- 可以直接访问运行内存SRAM,程序存储器Flash,寄存器等存储器
- 外设:一般是数据寄存器DR(Digital Register)
- 存储器:内存SRAM,程序存储器Flash
- 外设到存储器的转运:由于外设的数据是有时机的,所以这里不能够随便转运,我们要采取硬件触发,例如要等ADC转换完成,串口收到数据,定时时间到之后硬件触发一次DMA才能使用DMA转运,触发一次转运一次。(类似中断的更新事件)
- 存储器到存储器的转运:一般使用软件触发
- 特定的硬件触发:对应的硬件触发有固定的通道,等下具体介绍
- 这里的软件触发和外部中断以及ADC的软件触发不一样,等下解释
- 该芯片只有DMA1
2.存储器映像
- 计算机的组成:运算器,控制器,存储器,输入和输出设备五个部分,其中运算器和控制器合称CPU,其中核心部分是CPU和存储器。存储器中的核心部分是存储器的内容和存储器的地址,上述表就是STM32中所具有的所有类型的存储器和其地址
- 起始地址:就是给存储器分配的固定开头地址,起始地址后面的地址都是存储器内存的扩展
- STM32的ROM存储器的存储介质其实都是FLASH,但是我们一般说的Flash指的是主闪存,也就是程序储存器Flash
- 选项字节:主要存储的是Flash的读保护,写保护和看门狗等等配置
- 临时变量:定义的变量,数组,结构体等等统称为临时变量
- 内核外设:NVIC,SysTick等等
- STM32F103的CPU是32位的,其存储器有4G的寻址空间,但是存储器都是KB级别的,所以就有很多空间没有使用到,图中灰色部分[Reserved]都是没有使用到的地址
- 中间一列是ROM,左边一列是RAM,右边一列是Peripherals外设
- 可以发现外设的地址都是0x4000开头,然后后面的地址再分是哪一个寄存器,这样每一个外设寄存器包括其存储内容都有对应地址了,其他的存储器也都是一样的
- BOOT引脚的选择控制启动区域
3.DMA框图
- 核心:包括CPU和内核外设
- 红色:主动单元,拥有访问存储器的权限,其中Dcode是专门用来访问Flash的,系统总线是用来访问APB1,APB2,AHBx等等总线上的存储器的,DMA总线什么都可以访问,有3个DMA分支通向DMA总线,用绿色标记
- 褐色:被动单元,只能被主动单元读写
- 紫色:仲裁器,由于DMA总线只有一条,所以所有的分支都只能分时复用这一条总线,如果产生冲突就由仲裁器决定先后使用顺序,红色的总线矩阵里面也有仲裁器,如果DMA总线和系统总线都要访问同一个目标,那么DMA就会让CPU停止访问,仲裁其会将总线一半的带宽给CPU,使CPU正常工作
- 黄色:AHB从设备也就是DMA的寄存器,DMA作为一个外设也有自己的配置寄存器,这个寄存器连接在了AHB总线上,用粉色标记
- DAM既是总线矩阵的主动单元,读写其他寄存器,也是AHB总线的被动单元,被读写
- Flash特殊情况下可以写,一般只能读,SRAM可以读写,外设寄存器是否能够读写要看手册,不过一般都是操作数据寄存器,可以读写
4.DMA基本结构
- 图中红色框框就是我们的站点,一个是外设站点,一个是存储器站点,站点是存储要交换的数据的地方,究竟是外设到存储器还是存储器到外设,这个方向我们也有参数可以控制。我们也可以存储器到存储器,但是由于Flash是只读存储器,所以只能Flash到SRAM或者SRAM到SRAM。
- 站点都有三个参数:起始地址,数据宽度,地址是否自增。起始地址是前面讲的每个存储器特有的地址,用来确认数据从哪里来,到哪里去;数据宽度就是被交换数据的大小,这里宽度有三种,分别是字节Byte,半字Half Word,字Word,大小分别为8位,16位,32位,例如我们的AD值是12位的,这里就要选择半字的宽度;地址是否自增是指每次转运完成之后,起始地址是否自增,比如说我们的ADC_DR(ADC数据寄存器),这个转运后肯定不能自增吧,改了地址可能下次就去转运其他寄存器了,但是存储器肯定要自增吧,不然这次存在存储器这个地址,下次还是存在这个地址,上一次存进来的值就会被覆盖了,数据寄存器本来就不用担心被覆盖,它的值已经转运到存储器部分被记录下来了,直接读取存储器就能知道上一次存的值是什么。
- 这个外设站点的起始地址也不一定非要是外设寄存器的,写存储器的也行,只要改变一下控制方向的参数就行了,而且进行存储器之间的转运的话,两个站点就都是存储器了
- 传输寄存器是用来控制转运次数的,我们可以给该寄存器一个值,这个值会自减,减到0之后DMA就不会再转运了,并且减到0之后之前站点自增的地址又会回到增长之前
- 自动重装器是用来使传输寄存器的起始值清零后复原的,如果不使用的话,就只能够进行一次转运,传输寄存器清零后就不能够再次转运了,也就是单次模式,如果使用,就能够一直转运,也就是循环模式
- M2M(Memory to Memory):即存储器到存储器。当给M2M置1时就是软件触发,置0就是硬件触发,这个软件触发的逻辑是以最快的速度连续不断触发DMA使传输寄存器自减为0,完成这一轮的转换,所以这个模式不能和循环模式一起使用,不然DMA转运会停不下来。所以存储器到存储器的转运一般使用软件触发,因为存储器之间的转运不需要时机,而且越快越好,硬件触发则需要看时机,一般用于存储器和外设之间的转运
- DMA转运的条件:使能DMA,传输寄存器不为0,有触发源(软件或者硬件)
- 如果没有使用循环模式,那么传输寄存器自减清0之后就需要先失能DAM才能够再次设置初始值,再使能DMA使之启动转运
5.DMA请求
- 这个图表示的是几个外设硬件触发产生的7个DMA请求
- 红色部分是M2M和EN(cmd)使能,这里的排版比较奇怪,一般在数据选择器侧边的是控制通道的引脚,左边是通道,这里EN在侧边但是表示的是使能,M2M才是控制通道的引脚2
- 上图蓝色部分的也就是上面讲述过的特定的硬件触发,看图可知ADC1的DMA请求通道就是通道一,每一个外设硬件触发的请求通道都是固定对应的
- 如果使用ADC请求通道,那么使能这个DMA通道就要用ADC_DMAcmd函数使能,使用定时器也有TIM_DMAcmd使能,使用什么外设就要用其对应的DMA使能函数使能DMA,如果三个通道都开启了,我们可以看到这个或门,理论上三个通道都是有效的,但是一般只开启一个
- 最后七个DMA通道只有一个总线,这里也有一个仲裁器来控制优先级,默认的是通道号越小优先级越高,也可以在程序中配置优先级
- DMA_cmd使能DMA转运,xxxDMA_cmd使能DMA请求通道
6.数据宽度与对齐
- 源端宽度(转运起点),目标宽度(转运终点),传输数目(要转运几个数据)
- B0[7:0]表示B0这个数据是8位的,B0[15:0]表示B0这个数据是16位的
- 0x1,0x2是源端和目标的地址
- 这个传输数目都为4,所以每一行都是,源端0x0处的B0数据传输到目标0x0处,源端0x1处的B1数据传输到目标0x1处,传输4个数据也就是一直到B4
- 结论:
当源端宽度=目标宽度时,数据高低位一一对应,eg.都是8位大小,0x0B0->0x0B0
当源端宽度>目标宽度时,舍弃高位,低位对齐,eg.源端16位大小,目标8位大小,0x0B1B0->0x0B0
当源端宽度<目标宽度时,高位补0,低位对齐,eg.源端8位大小,目标16位大小,0x0B0->0x000B0
7.DAM转运
软件触发
DMA的配置:定义两个SRAM数组,并把数组地址放进站点,两个站点地址都自增,转运7次,软件触发,单次模式(不用自动重装器),数据宽度示情况而定,优先级配置也示情况而定
硬件触发
DMA配置(以ADC为例):把ADC_DR的地址塞进外设站点,定义的数组塞进存储器站点,如果ADC是连续转换则设置为循环模式,非连续则设置为单次模式,通道有几个就转运几次,并且每一次转运完成都硬件触发一次DMA请求进行DMA转运(对应外设要对应器通道),ADC_DR地址不自增,存储器地址自增,数据宽度都为半字,优先级配置示情况而定
8.补充
- 位段:两个位段区映射了外设寄存器和SRAM全部的位,也就是可以位寻址(外设寄存器和SRAM中每一位都分配了地址),操作位段区的地址就相当于单独操作外设或者SRAM的某一位,SRAM位段区的起始地址是0x2200,外设寄存器是0x4200
- 真实地址=基地址+偏移地址
看下面代码可以发现固件库代码的条理非常清晰,除了第一个外设基地址是固定值,其他的基地址都是通过 上一级基地址+偏移 计算出来的,最后GPIOA是一个 指定地址强制转换结构,也就是把这个地址强制转换成一个结构体指针,这个地方实现偏移的方法非常巧妙,因为结构体里存放寄存器的顺序刚好对应GPIOA实际上寄存器顺序,也就是说只要结构体的地址刚好为GPIOA_BASE的地址,由于结构体内容的地址也是在结构体地址基础上的延续,那么结构体寄存器的顺序就刚好就实现了在GPIOA_BASE基地址上的偏移,从而得到每一个寄存器的真实地址
#define PERIPH_BASE ((uint32_t)0x40000000) //外设基地址
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) //APB2总线基地址
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800) //GPIOA 基地址
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) //GPIOA结构
代码部分
DMA数据转运
MyDMA.c
#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使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}
/**
* 函 数:启动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); //清除工作完成标志位
}
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 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)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); //DMA初始化,把源数组和目的数组的地址传入
/*显示静态字符串*/
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);
while (1)
{
DataA[0] ++; //变换测试数据
DataA[1] ++;
DataA[2] ++;
DataA[3] ++;
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
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); //显示数组DataB
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000); //延时1s,观察转运前的现象
MyDMA_Transfer(); //使用DMA转运数组,从DataA转运到DataB
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
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); //显示数组DataB
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000); //延时1s,观察转运后的现象
}
}
DMA实现AD多通道
AD.c
#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就可以一直连续不断地工作
}
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 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,手动增加一些转换的间隔时间
}
}
标签:DMA,存储器,多通道,STM32,地址,InitStructure,ADC,外设
From: https://blog.csdn.net/2401_87412882/article/details/145228636