STM32 DMA直接存储器存取
目录
- 概念
- 存储器映像
- DMA基本结构
- DMA请求
- 数据宽度与对齐
- DAM转运
- 硬件触发
- 相关库函数
- 内存到内存DMA转运代码
- ADC+DMA
- ADC单次扫描+DMA单次转运
- ADC多次扫描+DMA多次转运
概念
DMA是协助CPU完成数据转运工作的,DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。
STM32有12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
从一个地方移动到另一个地方,需要占用一个通道,DMA多通道进行运转时候,之间可以各转各的互不干扰
每个通道都支持软件触发和特定的硬件触发
- 若是存储器到存储器的数据转运,比如把flash里的一批数据转运到SRAM中去,就需要软件触发,触发后,DMA就会一股脑的转运,以最快的速度全部转运好。
- 若是外设到存储器的数据转运,不能一股脑的转运,因为外设的数据是有一定时机的,需要硬件触发。比如转运ADC的数据,就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次转运一次,才能保证数据正确。
- 特定的硬件触发:每个DMA的通道,它的硬件出发源是不一样的,当使用某个外设的硬件出发源,就得使用它连接的那个通道,而不能任意选择通道。
STM32F103C8T6 拥有的DMA资源:DMA1(7个通道)
存储器映像
DMA是在存储器之间进行数据转运的,STM32的存储器和他们的地址如下
STM32的CPU是32位的,有4G的寻址能力,但是芯片的存储器都是KB级别的,所以就有很多空间没有使用到,图中灰色部分[Reserved]都是没有使用到的地址。
可以发现外设的地址都是0x4000开头,然后后面的地址再分是哪一个寄存器,这样每一个外设寄存器包括其存储内容都有对应地址了,其他的存储器也都是一样的。
BOOT引脚的选择控制启动区域
整理出如下
系统存储器:BootLoader程序是芯片出厂自动写入的,一般不允许我们修改
选项字节:用于存储一些独立于程序代码的配置参数,位置是在ROM区的最后面,下载程序可以不刷新选项字节的内容,这样选项字节的配置就可以保持不变,选项字节里,存的主要是flash的读保护、写保护,还有看门狗等等的配置。
内核外设寄存器:内核外设就是NVIC和SysTick,内核外设和其他外设不是同个厂家设计的,所以地址被分开。
图中的仲裁器,由于DMA总线只有一条,所以所有的分支都只能分时复用这一条总线,如果产生冲突就由仲裁器决定先后使用顺序,红色的总线矩阵里面也有仲裁器,如果DMA总线和系统总线都要访问同一个目标,那么DMA就会让CPU停止访问,但是仲裁器仍会将总线一半的带宽给CPU,使CPU正常工作。
DAM模块中还有AHB从设备,AHB从设备也就是DMA的寄存器,DMA作为一个外设也有自己的配置寄存器,这个寄存器连接在了AHB总线上,用粉色标记。
DAM既是总线矩阵的主动单元,读写其他寄存器,也是AHB总线的被动单元,被读写。
芯片整体结构如上,Flash是只读不可写的,只有配置Flash接口才能写入。但是这只是在正常运行模式下,并不意味着我们不能下载程序进去,在编程模式一样可以下载程序。
DMA基本结构
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转运会停不下来。存储器到存储器的转运一般使用软件触发,因为存储器之间的转运不需要时机,而且越快越好。硬件触发则需要看时机,一般用于存储器和外设之间的转运。硬件触发源可以选择ADC、串口收到数据、定时时间到等等,所以需要硬件触发。
DMA转运的条件:
- 开关控制,DMA_Cmd必须使能
- 传输计数器必须大于0
- 触发源必须有触发信号,触发一次转运一次,传输计数器自减一次。当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。 即,软件触发时,因为软件触发不能和自动重装一起使用,所以此时就需要再次设置。
此时就需要DMA_Cmd给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,在DMA_Cmd给Elable开启DMA,DMA才能继续工作,写传输计数器时必须要先关闭DMA再进行,不能在DMA开启时写传输计数器,这是手册里的规定。
DMA请求
看图可知ADC1的DMA请求通道就是通道一,每一个外设硬件触发的请求通道都是固定对应的通道。但是软件触发的话就不需要选择通道,因为每个通道都可以软件触发。
如果使用ADC请求通道,那么使能这个DMA通道就要用ADC_DMAcmd函数使能,使用定时器也有TIM_DMAcmd使能,使用什么外设就要用其对应的DMA使能函数使能DMA,如果三个通道都开启了,我们可以看到这个或门,理论上三个通道都是有效的,但是一般只开启一个。
七个DMA通道只有一个总线,这里也有一个仲裁器来控制优先级,默认的是通道号越小优先级越高,也可以在程序中配置优先级。
数据宽度与对齐
源端宽度(转运起点),目标宽度(转运终点),传输数目(要转运几个数据)
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
DAM转运
上图就是定义两个SRAM数组,并把数组地址放进站点,两个站点地址都自增,转运7次,软件触发,单次模式(不用自动重装器),数据宽度示情况而定,优先级配置也示情况而定。
硬件触发
DMA配置(以ADC为例):把ADC_DR的地址塞进外设站点,定义的数组塞进存储器站点,如果ADC是连续转换则设置为循环模式,非连续则设置为单次模式,通道有几个就转运几次,并且每一次转运完成都硬件触发一次DMA请求进行DMA转运(对应外设要对应器通道),ADC_DR地址不自增,存储器地址自增,数据宽度都为半字,优先级配置示情况而定。
相关库函数
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
void DMA_ClearFlag(uint32_t DMAy_FLAG);
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
void DMA_ClearITPendingBit(uint32_t DMAy_IT);
有些函数都使用了很多次了,看见参数应该就知道功能了。
DMA_StructInit
初始化结构体的,上面DMA结构中各种参数都是使用结构体进行配置的。
DMA_ITConfig
是中断输出使能,需要DMA中断的话就需要这个函数。
DMA_SetCurrDataCounter
设置当前数据寄存器,就是给传输计数器写数据的。
DMA_GetCurrDataCounter
DMA获取当前数据寄存器,就是返回当前传输计数器的值。
剩下四个是获取标志位状态,清楚标志位,获取中断状态,清楚中断挂起位。
内存到内存DMA转运代码
MyDMA.c:
#include "Config.h"
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t MemAddr, uint32_t PerAddr, uint32_t Size)
{
MyDMA_Size = Size;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA是AHB总线上的设备
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 = MemAddr;
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 = PerAddr;
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, DISABLE);
}
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);//转运完成退出循环
DMA_ClearFlag(DMA1_FLAG_TC1);
}
main.c:
#include "Config.h"
char arr1[] = {11, 12, 13, 43};
char arr2[4] = {0};
int main(void)
{
OLED_Init();
MyDMA_Init((uint32_t)arr2, (uint32_t)arr1, 4);
while(1)
{
for(int i = 0; i < 4; i++)
{
arr1[i]++;
}
OLED_ShowNum(1, 1, arr1[0], 2);
OLED_ShowNum(1, 4, arr1[1], 2);
OLED_ShowNum(1, 7, arr1[2], 2);
OLED_ShowNum(1, 10, arr1[3], 2);
OLED_ShowNum(2, 1, arr2[0], 2);
OLED_ShowNum(2, 4, arr2[1], 2);
OLED_ShowNum(2, 7, arr2[2], 2);
OLED_ShowNum(2, 10, arr2[3], 2);
Delay_s(1);
MyDMA_Transfer();
OLED_ShowNum(4, 1, arr2[0], 2);
OLED_ShowNum(4, 4, arr2[1], 2);
OLED_ShowNum(4, 7, arr2[2], 2);
OLED_ShowNum(4, 10, arr2[3], 2);
Delay_s(1);
}
}
ADC+DMA
ADC单次扫描+DMA单次转运
此时ADC的配置需要配置四个通道,DMA也要配置为硬件触发。
AD.c:
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
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_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_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
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_InitStructure.ADC_NbrOfChannel = 4;
ADC_Init(ADC1, &ADC_InitStructure);
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);
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
main.c:
#include "Config.h"
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD1:");
OLED_ShowString(2, 1, "AD2:");
OLED_ShowString(3, 1, "AD3:");
OLED_ShowString(4, 1, "AD4:");
while(1)
{
AD_GetValue();
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(1000);
}
}
ADC多次扫描+DMA多次转运
此时就不需要AD_GetValue了,因为ADC触发后是一直工作的,DMA也是一直工作的。
AD.c:
#include "Config.h"
uint16_t AD_value[4];
void AD_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA是AHB总线上的设备
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//独立模式还是双ADC模式
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_InitStructure.ADC_NbrOfChannel = 4;//通道数目,在扫描模式下用到几个通道
ADC_Init(ADC1, &ADC_InitStructure);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_BufferSize = 4;//计数器大小
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//数据传输方向,这个参数指定外设站点是源端还是目的地
DMA_InitStructure.DMA_M2M = DMA_M2M_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_Mode = DMA_Mode_Circular;//是否循环,即是否重装
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_Priority = DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//必须使用DMA1的通道1,因为ADC1在这个通道
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);//复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位校准完成
ADC_StartCalibration(ADC1);//开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
main.c:
#include "Config.h"
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD1:");
OLED_ShowString(2, 1, "AD2:");
OLED_ShowString(3, 1, "AD3:");
OLED_ShowString(4, 1, "AD4:");
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);
}
}
还可以和定时器进行结合,定时器触发ADC,ADC转换后就会自动触发DMA,此时就完全自动了,且不过多消耗CPU资源。