DMA简介
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
DMA特性
拥有12个独立可配置通道:DMA1(7个通道),DMA2(5个通道)
STM32F103C8T6 DMA资源:DMA1(7个通道)
每个通道都直接连接专用的硬件DMA请求,也就是硬件触发,也支持软件触发
对于有多个请求的同时,可以利用软件编程设置优先级的先后顺序,优先级相等的情况下由硬件决定
对于DMA来说,需要传输的源头和传输的目的地,传输的大小称为传输宽度,一般有字节(8bit)、半字(16bit)、字(32bit)。
每个通道都有3个事件标志(DMA半传输、 DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断。为应用的灵活性考虑,通过设置寄存器的不同位来打开这些中断。
DMA框图
先看左上角,DMA与Cortex™-M3核心共享系统数据总线,通过总数据库,可以直接执行存储器的数据传输(Flash、SRAM、外设寄存器);当DMA与Cortex™-M3同时访问相同的目标时,会通过一个仲裁器,来给它们两个循环调度访问目标,而CPU最终会得到至少一半的带宽,也就访问权限。
接着往下看,AHB从设备连接着AHB总线,可以控制总裁器和通道的选择。相当于AHB放长线,放出一个设备来管理DMA。
总裁器是可以根据通道请求的优先级来启动外设/存储器的访问。
可以通过软件编程设置4个不同的等级:最高优先级、高优先级、中等优先级、低优先级。
相同等级的情况下,则较低编号的通道比较高编号的通道有较高的优
先权。举个例子,通道2优先于通道4。
右边都是一些存储器和外设寄存器,通过DMA的请求,就可以直接对数据进行传输了。
这是存储器对应的起始地址和作用。
DMA基本结构
DMA的处理:在发生一个事件后,外设向DMA控制器发送一个请求信号。 DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时, DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求, DMA控制器同时撤销应答信号。如果有更多的请求时,外设可以启动下一个周期。
我们首先会从要传输的外设寄存器或者是储存器获取它的起始地址和传输的数据宽度,然后再存数据到外设寄存器或者存储器指示的存储器地址,并确定数据宽度;
首先这里要明确DMA的数据转运是用复制去传输,也就是原位置的数据是不会改变的。地址自增是考虑到一般给出的地址只是首元素地址或者说是起始地址,要对下一个数据进行传输,就需要对地址进行增加,才能获取到下一个数据。
传输计数器是来统计要传输多少个数据宽度的,一开始先赋予传输计数器一个初始值。
在没有启动自动重装器的情况下,这个初始值会逐渐递减,当计数器的值变为0时,就停止传输。
如果启动了自动重装器,当初始值逐渐递减为0时,会重新恢复到初始值,而对应的传输地址,也会恢复到初始地址的状态;
M2M是决定使用软件触发或者硬件触发的寄存器。
DMA请求
这是DMA1的请求映像,对于DMA请求来说,不同的外设是有要求不同对应通道的,没有由我们所决定哪个硬件外设对应哪条通道;而软件触发的则每条通道都允许。
数据宽度对齐
这里是小端存储。
对以上的总结:
当两端宽度相等时:那么传输数据不变;
当源端宽度小于目标宽度时:目标宽度高位补0;
当源端宽度大于目标宽度时:对源端宽度进行截断,保留低位的数据。
DMA数据转运工程
连接方式:
通过DMA的数据搬运,将一个数组中的内容搬运到另一个数组中,并且原数组会随时间不断增加,DMA也不断的进行数据搬运;
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初始化 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通道工作模式 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通道优先级 DMA_Init(DMA1_Channel1,&DMA_InitStructure); //DMA是否启动 DMA_Cmd(DMA1_Channel1,DISABLE); } //数据转运函数 void MyDMA_Transfer() { DMA_Cmd(DMA1_Channel1,DISABLE); DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size); //设计DMA通道数据单元数 DMA_Cmd(DMA1_Channel1,ENABLE); //检查DMA通道上的标志位,运送完成标志 while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET); //需要手动清除标志位 DMA_ClearFlag(DMA1_FLAG_TC1); }
DMA.h
#ifndef __DMA_H__ #define __DMA_H__ void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size); void MyDMA_Transfer(); #endif
size表示传输的个数,大小由源端数据宽度决定,地址自增将会使数组下标进行移动;
规定软件触发不能启动自动重装器,也就是DMA的模式
数据转运函数:由于需要不断的数据转运,而没有启动自动重装器,所以需要我们自己编程来进行重装,也就是将Size恢复为初始值,地址会随着Size的变化进行变化。
手动将Size重装之前,需要先对运行控制禁用,全部操作设置完才可以启用运行控制
#include "stm32f10x.h" // Device header #include "Delay.h" #include "Buzzer.h" #include "DMA.h" #include "OLED.h" uint8_t DataA[]={0x01,0x02,0x03,0x04}; uint8_t DataB[]={0,0,0,0}; int main() { 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(1,8,(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_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(); 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+ADC多通道
接线方式:
ADC的规则通道会将多通道结果放在规则通道寄存器上,每当扫描到一个通道,结果就放在ADC_DR上,结果会覆盖掉上一个储存的数据,DMA就会将结果转运到DMA存储器地址上。所以对于ADC_DR不用实现地址自增,而DMA储存器需要实现地址自增。
AD.c
#include "stm32f10x.h" // Device header uint16_t AD_Value[4]; void AD_Init() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); //配置ADC时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72M/6=12MHz 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); //为所选ADC规则通道配置其序列器对应等级和采样时间 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); //ADC结构体成员 ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_ContinuousConvMode=ENABLE; //指定通道模式为连续转换或者单转换 ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right; //数据对齐方式 ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None; //启动规则通道模拟电压到数字转换的外部触发器 ADC_InitStructure.ADC_Mode=ADC_Mode_Independent; //配置ADC为独立模式或者双模式 ADC_InitStructure.ADC_NbrOfChannel=4; ADC_InitStructure.ADC_ScanConvMode=ENABLE; //选择是否为扫描模式 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; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 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_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 4; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_Init(DMA1_Channel1, &DMA_InitStructure); //ADC运行控制 DMA_Cmd(DMA1_Channel1,ENABLE); ADC_DMACmd(ADC1,ENABLE); ADC_Cmd(ADC1,ENABLE); //重置所选ADC校准寄存器 ADC_ResetCalibration(ADC1); //获取ADC复位状态,复位后为0 while(ADC_GetResetCalibrationStatus(ADC1)); //开始校准 ADC_StartCalibration(ADC1); //获取ADC所选标准位状态,校准需要时间,校准好后置0 while(ADC_GetCalibrationStatus(ADC1)); //启动ADC软件转换 ADC_SoftwareStartConvCmd(ADC1,ENABLE); }
AD.h
#ifndef __AD_H__ #define __AD_H__ extern uint16_t AD_Value[4]; void AD_Init(); #endif
这里采用的是连续转换+扫描模式。这样就可以直接实现数据转换的自动化。
这里需要加上ADC_DMACmd函数,因为上面DMA请求中通道一的硬件触发有3个选择,这里表示选择ADC1.
ADC为软件触发方式,DMA为硬件触发方式。
main.c
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "AD.h" int main() { 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); } }