笔记来源于江科协议的视频
芯片采用与stm32F103C8T6
DMA简介
简介
DMA主要用于协助CPU,完成数据转运的工作,DMA可以直接访问STM32内部的存储器,包括运行内存SRAM,程序存储器Flash,寄存器等,DMA可以提供外设和存储器或者存储器与存储器之间的高速数据传输,无需CPU干预,节省CPU资源。外设包括外设存储器DR,如:ADC的数据存储器、串口的数据寄存器,存储器指SRAM和Flash ,外设与存储器、存储器与存储器之间传输就可以用DMA进行转运。
STM32有12个独立可配置的通道,DMA1:7个通道 DMA2:5个通道,这个通道就是转运数据的路径,从一个地方转运另一个地方,就需要另一个通道,多通道进行转运时,它们之间互不干扰,
每个通道支持软件触发和特定的硬件触发,如:DMA要进行存储器到存储器之间的数据搬运,Falsh到SRAM里就需要软件触发,使用软件触发时,DMA会直接以最快的速度转运完成, 如果时外设到存储器的转运,就不能直接转运了,因为外设数据有一定时机,所以需要硬件触发,如:转运ADC数据,就需要ADC每个通道转换完成之后进行搬运,硬件触发一次DMA,之后DMA再进行转运,触发一次,转运一次,
存储器到存储器之间一般用软件触发,外设到存储器一般用硬件触发,
STM32F103C8T6只有DMA1资源,没有DMA2。
DMA存储器映像
ROM:只读存储器,是一种非易失性,掉电不丢失的存储器,
RAM:随机存储器,易失性、掉电容易丢失的存储器
ROM:分为了3部分,第一块是程序存储器Flash,主闪存,用于存储C语言编译后的程序代码,下载程序的部分,运行程序 ,一般也是从主闪存里开始运行的,起始地址0x0800 0000,然后剩余字节的地址依次增长,每个字节都分配独一无二的地址,系统存储器和选项字节,也是ROM的一种,掉电不丢失,实际上存储介质也是Flash,地址都是0x1FFF开头,这两块区域都在ROM区域的最后面,系统存储器的作用用于存储BootLoader,用于串口下载,BootLoader是芯片出厂自动写入的,一般不允许修改, 选项字节的作用是用于存储一些独立于程序代码的配置参数,选项字节,存储的主要是Flash的读保护、写保护、还有看门狗的配置等。
RAM:运行内存SRAM,分配的地址是0x2000 0000 作用是存储运行过程中的临时变量,也就是在程序中定义变量、数组、结构体的地方。类比电脑,就是电脑的内存条, 外设寄存器,作用是存储各个外设的配置参数,初始化各个参数,最终读写的东西,外设存储器也是存储器的一种,存储介质其实也是SRAM, 内核外设寄存器,地址是0xE000 0000 作用是存储内核各个外设的配置参数。内核外设就是NVIC和SysTick,因为内核外设和其他外设不是一个厂家设计的,所以地址也是分开的。
DMA框图
左上角是Cortex-M3的内核,包含了CPU和内核外设等,其他的所有东西都可以看成存储器,Flash是主闪存,SRAM是运行内存,各个外设都可以看成存储器,也是SRAM存储器,寄存器是一种特殊的存储器,CPU可以对其进行读写,就像写运行内存一样,每个寄存器都连接了一根导线,每根导线可以用于控制外设电路的状态,如:引脚的高低电平、导通和断开开关、切换数据选择器, 所以寄存器是连接软件和硬件的桥梁 ,软件读写寄存器,就相当于控制硬件的执行。
总线矩阵的左端是主动单元,也是拥有存储器的访问权,右边是被动单元,他们的存储单元只能被左边的存储器读取,内核有DCode、系统总线,可以访问右边的存储器,DCode专门用于访问Flash,系统总线用于访问其他东西的,DMA要转运数据,DMA也拥有主动访问权,主动单元有内核CPU、DMA总线,DMA1和DMA2都各有一条DMA总线,DMA1有7个总线,DMA2有5个总线,各个通道可以设置他们转运数据的源地址和目的地地址, 多个通道可以独立转运数据,但DMA总线只有一条,所有通道都只能分时复用一条DMA总线,如果产生了冲突,那就由仲裁器来根据通道的优先级决定通道的使用顺序, 总线矩阵这里也有一个总裁器,如果CPU和DMA访问同一目标,那么DMA就会停止访问,防止冲突,不过总线总裁器,仍然会保证得到CPU一半的总线带宽。
仲裁器下面是AHB从设备,也就是DMA自身的寄存器,DMA作为一个外设,它自己也会有相应的配置寄存器,这里连接了AHB总线上, DMA,即是总线矩阵的主动单元,可以读写各种存储器,也是AHB的被动单元,CPU可以对其进行配置,DMA请求是DMA的硬件资源,可以得到各个外设触发的DMA请求。
右上角的Flash是ROM的一种,如果通过总线直接访问,无论是DMA、CPU都是只读,而不能写入。而SRAM可以任意读写。外设寄存器得看参考手册,有的寄存器只能读,有的寄存器只能写。
DMA逻辑框图
两大数据转运站点,左边是外设寄存器,右边是存储器站点,包括Flash,SRAM, DMA的数据转运可以从外设寄存器到存储器,也可以从存储器到外设寄存器, 具体方向是可以由参数控制,外设和存储器都有三个参数,第一个是起始地址,有外设端的起始地址,和存储端的起始地址,这两个参数决定了数据从哪里来到哪里去,第二个参数是数据宽度,指一次转运要按多大的数据宽度进行,可以选择字节Byte,半字HalfWord和字Word,字节8位(uint8_t),半字16位(uint16_t),字32位,第三个参数是地址是否自增。第一次数据转运后,地址是否增加到第二位,。 如果要进行存储器到存储器之间的转运,就需要把其中一个存储器的地址,放到外设这个站点处。这样就能进行存储器到存储器之间的转运了。只要你在外设起始地址写Flash或者SRAM的地址,那他就回去Flash或者SRAM找数据。
传输计数器,用来指定总共需要转运几次,是一个自减计数器,每转运一次,计数器就会减1,一直到0,DMA就不会在转运数据了。减到0之后,之前自增的地址,也会恢复到起始地址的位置,在传输计数器的右边还有一个自动重装器,作用就是传输计数器减到0之后,是否要恢复到最初的值,不重装,DMA转运完就结束了,如果重装,传输计数器会恢复到初始值,如:给个5,减到0之后,传输计数器恢复到5.一直循环。
下面的部分就是触发部分,决定了DMA在什么时机进行运行,触发源有硬件触发和软件触发,由M2M这个参数来决定。给1时就是软件触发,这个软件触发的逻辑是以最快的速度,连续不断的触发DMA,争取早点把DMA清零,完成这一轮的转换,软件触发和循环模式不能同时用,如果同时用,DMA就停不下来,一般用于存储器到存储器之间的搬运。给0时,就是硬件触发,硬件触发源可以选择ADC、串口、定时器等等。 还有右下角的开关控制。
DMA转运的条件:第一、开关控制 第二、传输计数器必须大于0 第三、必须有触发信号, 注意:传输计数器减到0之后,再写传输计数器,必须先关闭DMA,再写。不能在DMA 开启的时候写DMA。
一些细节问题
DMA1的请求映像,有7个通道,每个通道都有数据选择器,可以选择硬件触发和软件触发,EN是决定这个选择器要不要工作,en=0,不工作,en=1,数据选择器工作,M2M等于1时,选择软件触发,最左边的一列时硬件触发源,每个通道的硬件触发源都是不同的,如果用ADC1触发,那只能选择通道1。如果需要定时器2的更新事件来触发的话,那就必须选择通道2。如果用软件触发的话,那么通道就随便选择了,因为每个通道的软件触发都一样。 最后七个通道进入仲裁器进行判断,最终产生内部的DMA请求。 通道号越小,通道优先级越高,也可以在程序中配置优先级。
数据宽度对齐: 数据转运时,数据宽度都一样,正常转运,如果不一样,上个表就是说传输宽度不一样时,会产生哪些反应。如果目标数据宽度比源端数据宽度大,那就在目标数据宽度多出来的空位补0.。当目标数据宽度比源端数据宽度小时,那么多出来的高字节部分就舍弃掉。
逻辑运行例子
数据转运加DMA : 将SRAM里的DataA转运到DataB里 首先是外设站点和存储器站点,地址、数据宽度、是否自增三个参数。在这里,外设A填写DataA的地址,存储器则写DataB的地址。数据宽度两个都是uint8_t 都是8字节传输。两边地址都应自增。传输计数器给7,自动重装则不需要。触发选择则选择软件触发,M2M给1。这里转运是一种复制转运,转运完成之后,数据并不会消失。
ADC扫描加DMA 左边是ADC扫描模式的执行流程,有7个通道,触发后依次进行通道AD转化,转换完成之后都放在ADC_DR寄存器里面(规则组通道存储寄存器),在每个单独通道转换完成后,进行一次数据搬运,目的地址进行自增,防止数据被覆盖, 配置如下:外设地址,写入ADC_DR地址,存储器站点写DMA存储器的地址,要配置地址自增。 数据宽度都要uint16的数据,都是半字传输。外设地址步自增,存储器地址自增。传输方向是ADC_DR外设地址到DMA存储器地址 。传输计数器7次,计数器是否重装,要看ADC配置,如果是单次扫描,计数器就不重装,如果是连续扫描,DMA就重装,ADC和DMA同时工作,触发源选择,ADC_DR的值在ADC单个通道转换完成才有效,DMA搬运数据的时机要和ADC单个通道转换才有效,ADC扫描模式触发转换后,它不会产生中断和标志位信号,但会产生DMA请求信号。
DMA配置步骤
第一步:打开时钟
第二步:配置参数
第三步:使能
代码
DMA软件触发,
存储器到存储器搬运
#include "stm32f10x.h" // Device header
uint16_t Mysize;
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t size)
{
Mysize = size;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_BufferSize = size;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向外设-》存储器
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //软件触发
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //以字节传输
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //地址自增
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式 正常模式
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //存储器地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //以字节传输
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //地址自增
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级中等
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
DMA_Cmd(DMA1_Channel1,ENABLE);
}
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1,ENABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,Mysize); //写入计数器
DMA_Cmd(DMA1_Channel1,ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //等待标志位完成
DMA_ClearFlag(DMA1_FLAG_TC1);
}
主函数
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Delay.h"
#include "MyDMA.h"
uint8_t DataA[] = {0xFF,0x11,0x90,0x45};
uint8_t DataB[] = {0x00,0x00,0x00,0x00};
int main(void)
{
OLED_Init();
MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
/*显示静态字符串*/
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配合ADC
#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就可以一直连续不断地工作
}
主函数
#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,手动增加一些转换的间隔时间
}
}