STM32 DMA直接存储器存取(STM32笔记)
目录
DMA
简介:
- DMA(Direct Memory Access)直接存储器存取
- DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
- 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
- 每个通道都支持软件触发和特定的硬件触发
- STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器和存储器之间用软件触发,软件可以用很快的速度一股脑的转运完成。
外设和存储器之间用硬件触发,因为外设是有一定的时机的,要等到ADC外设数模转运完成才可以转运
并且每个DMA的通道,对应的硬件触发源不一样。使用某个外设的硬件触发源,就要使用它连接的那个通道,而不能任意选择通道。
存储器映像

stm32有以上这些存储器
ROM:只读存储器,是一种非易失性,掉电不丢失的存储器。
RAM就是随机存储器,是一种易失性、掉电丢失的存储器
(人话:RAM是手机运行内存,ROM是手机硬盘内存,例如:我的手机是IQOO Neo 9 16+256g的手机,那么RAM = 16g,ROM = 256g)
Flash储存程序代码和const定义的常量。
我们定义的变量储存在SRAM里。
DMA框图:

其中除了标红的区域包含了CPU和内核外设等等,其余的都可看做是存储器
寄存器是一种特殊的存储器
一方面CPU可以对寄存器进行读写操作,就像读写运行内存一样。
另一方面,寄存器的每一位背后都连接了一根导线,这些导线可以用于控制外设电路的状态。
所以寄存器是软件和硬件的桥梁,CPU读写寄存器就是在控制硬件电路的执行。
为了高效有条理的访问存储器,中间设计了一个总线矩阵。
左边:主动单元,拥有存储器的访问权。
右边:被动单元,它们的存储器只能被左边的主动单元读写。
左边:
DCode总线:专门访问Flash的
系统总线是访问其他的
DMA1和DMA2分别有一条线路连接到总线上。
其中,DMA1有7个通道,DMA2有5个通道。各个通道可以设置他们转运数据的源地址和目标地址。
在DMA1和DMA2框图中,都显示一个仲裁器。因为虽然多个通道可以独立转运数据,但是最终DMA总线只有一条。所以所有的通道都只能分时复用这一条DMA总线。如果产生冲突,就会由仲裁器根据优先级决定谁先用,谁后用。
在总线矩阵中也会有一个仲裁器,就是当CPU和DMA同时访问同一个目标,那么DMA就会停止CPU的访问,防止冲突。
AHB从设备,也就是DMA自身的寄存器。CPU可以通过线路对DMA进行配置。
两个红框中的各个外设,引出两条DMA请求线路。这个DMA请求就是DMA的硬件触发源。比如:ADC转换完成,串口接收到数据。需要触发DMA转运数据的时候,发出请求。
注:图中的Flash是只读存储器,通过总线访问都是只读的,只能读取不能写入。但是可以通过配置Flash接口控制器来对Flash进行写入,只不过过程就会有点麻烦了
DMA基本结构:

参数1:方向:外设---->存储器 / 存储器---->存储器
参数2:外设和存储器的起始地址
参数3:数据宽度。指定一次转运要按多大的数据宽度来转运
参数4:地址是否自增。当一次转运完成后,地址是否++。(相当于指针指向数组第一个数,转运后指针++指向第二个数)
参数5:传输计数器:指定可以传输几次。当它的值为5时,每传输一次,它的值就-1,直到减为零,就不再进行数据传输。此时的地址就会变为新的起始地址,以方便新的一轮转换。
参数6:自动重装值:当传输计数器值减为0时,是否要让他自动恢复到最初的值。
参数7:M2M:决定触发源是由硬件触发还是有软件触发
软件触发:以最快的速度,连续不断的触发DMA,争取早日把传输计数器清零。完成这一轮的转换
开关控制:DMA_Cmd();
注意:写传输计数器时,必须要先关闭DMA_Cmd();
DMA请求:

每个通道都有硬件触发和软件触发。每个通道的硬件触发源都是不同的。
数据宽度与对齐:

当两个站点的数据宽度不一致时;
小的数据转大的数据,高位补零;
大的数据转小的数据,舍高位;
补充:
我们上一篇说过,ADC扫描模式下,每个通道的数据转运完后,不会置标志位也不会请求中断。只有在所有数据都转运完后才会置标志位和申请终端。
但是在每个通道的数据转运完后,会申请一个DMA请求,来保存此次转运的数据。(因为在扫描模式下,规则组只能输出最后一个转运的数据,前面的数据都会被覆盖掉)
DMA库函数:
/* 复位DMA配置 */
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);
/* 开启DMA中断 */
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数据转换)代码:
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
MyDMA.c模块:
#include "stm32f10x.h" // Device header /* * 1.RCC开启DMA的时钟。 * 2.调用DMA_Init()初始化各个参数。 * 3.DMA_Cmd()。 * (如果调用硬件触发,要在对应的外设调用相应的XXX_DMACMd,开启一下触发信号的输出) * (如果需要DMA的中断,那就调用DMA_ITConfig(),开启中断输出,再在NVIC里配置相应的终端通道,写相应的中断函数) * 结束后再想给传输计数器赋值的话,就DMA_Cmd()失能,赋值,DMA_Cmd()使能。 */ /* * * DMA启动的三个条件: * 1.传输计数器 > 0; * 2.触发源有触发信号 * 3.DMA使能 * */ 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_InitTypeDef DMA1_Initstructure; /* 外设 */ DMA1_Initstructure.DMA_PeripheralBaseAddr = ADDrA; //起始地址 DMA1_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度 DMA1_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //地址是否自增 /* 存储器 */ DMA1_Initstructure.DMA_MemoryBaseAddr = ADDrB; //起始地址 DMA1_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度 DMA1_Initstructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //地址是否自增 DMA1_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向 DMA1_Initstructure.DMA_BufferSize = size; //传输计数器 DMA1_Initstructure.DMA_Mode = DMA_Mode_Normal; //传输模式,其实就是是否使用自动重装 DMA1_Initstructure.DMA_M2M = DMA_M2M_Enable; //选择是硬件触发还是软件触发 DMA1_Initstructure.DMA_Priority = DMA_Priority_Medium; //配置优先级 DMA_Init(DMA1_Channel1, &DMA1_Initstructure); //软件触发,任意1~7通道都可以 DMA_Cmd(DMA1_Channel1, DISABLE); } //再启动一次DMA 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); //没完成RESET,完成SET DMA_ClearFlag(DMA1_FLAG_TC1); }
main.c模块:
#include "stm32f10x.h" // Device header #include "Delay.h" #include "Oled.h" #include "MYDMA.h" uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04}; uint8_t DataB[] = {0, 0, 0, 0}; 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] ++; //转运前的DataA[] 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); //转运前的DataB[] OLED_ShowNum(4, 1, DataB[0], 2); OLED_ShowNum(4, 4, DataB[1], 2); OLED_ShowNum(4, 7, DataB[2], 2); OLED_ShowNum(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_ShowNum(4, 1, DataB[0], 2); OLED_ShowNum(4, 4, DataB[1], 2); OLED_ShowNum(4, 7, DataB[2], 2); OLED_ShowNum(4, 10, DataB[3], 2); Delay_ms(1000); } }
(DMA数据转换+AD多通道)代码:
AD.h模块:
#ifndef __AD__H
#define __AD__H
extern uint16_t AD_Value[4];
void AD_Init(void);
void ADC_GetValue(void);
#endif
AD.c模块:
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
//1.开启RCC时钟,包括ADC和GPIO的时钟。ADCCLK的分频器也要配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //ADCCLK时钟最大14MHz 6分频后是12MHz
//2.配置GPIO为模拟输入模式
GPIO_InitTypeDef GPIOA_InitStructure;
/*这里的模拟输入模式是ADC的专属模式,让GPIO的输入输出失效,信号直接进入板子里面的ADC外设*/
GPIOA_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIOA_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIOA_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIOA_InitStructure);
//3.配置多路开关,把左边的通道接入右边的规则组里面(点菜)
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
/*ADC1的菜单里,菜品:通道0,排在第一位,采样时间是55.5个ADC周期*/
//还想再点几个菜的话,就多调用计次这个函数
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);
//4.配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //配置ADC的工作模式(独立模式/双ADC模式)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //设置右对齐还是左对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发转换选择
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //选择 连续/单次 转换模式 ENABLE(连续)/DISABLE(单次)
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //选择扫描模式(ENABLE)还是非扫描模式(DISABLE)
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数目:在扫描模式下,总共会用到几个通道
ADC_Init(ADC1, &ADC_InitStructure);
DMA_InitTypeDef DMA1_Initstructure;
/* 外设 */
DMA1_Initstructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //起始地址(ADC转换完成后放在ADC->DR中)
DMA1_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度
DMA1_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //地址是否自增
/* 这里选择不自增是因为,ADC的规则组里面的寄存器只有一个。多个菜品只在这个寄存器里面的多位自增 */
/* 存储器 */
DMA1_Initstructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //起始地址
DMA1_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度
DMA1_Initstructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //地址是否自增
DMA1_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向
DMA1_Initstructure.DMA_BufferSize = 4; //传输计数器
DMA1_Initstructure.DMA_Mode = DMA_Mode_Normal; //传输模式,其实就是是否使用自动重装
DMA1_Initstructure.DMA_M2M = DMA_M2M_Disable; //选择是硬件触发还是软件触发,这里是硬件出发
DMA1_Initstructure.DMA_Priority = DMA_Priority_Medium; //配置优先级
DMA_Init(DMA1_Channel1, &DMA1_Initstructure); //软件触发,ADC外设接在了通道1上,所以这里只能使用通道1
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);//开启ADC到DMA的输出
ADC_Cmd(ADC1, ENABLE);
//5.对ADC进行校准
ADC_ResetCalibration(ADC1); //复位校准
/*软件置1,开启复位校准,复位校准完成后,硬件自动清零*/
while(ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位校准完成(未完成则一直开始等待)
ADC_StartCalibration(ADC1); //开始校准
/*软件置1,开启校准,校准完成后,硬件自动清零*/
while(ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
}
//多通道简易实现,将函数的参数改为ADC规则通道配置的参数;
void ADC_GetValue(void)
{
//写入传输计数器
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, 4);
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件开启转换命令
//等待DMA完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //没完成RESET,完成SET
DMA_ClearFlag(DMA1_FLAG_TC1);
}
main.c模块:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Oled.h"
#include "AD.h"
int main(void)
{
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)
{
ADC_GetValue();
OLED_ShowNum(1,5,AD_Value[0],5);
OLED_ShowNum(2,5,AD_Value[1],5);
OLED_ShowNum(3,5,AD_Value[2],5);
OLED_ShowNum(4,5,AD_Value[3],5);
Delay_ms(500);
}
}
DMA数据转换+AD多通道:

扫描模式可以对多个进行转换,但是转换后的数据存在ADC_DR寄存器中,不及时取出数据就会导致数被下一个通道转换的数据覆盖。
更多推荐

所有评论(0)