STM32——DMA的使用(定时器触发ADC多通道扫描模式)

一、系统总体架构

        如果想完全理解DMA的作用,这里先介绍一下STM32的系统架构。

         其系统是由四个驱动单元和四个被动单元构成。

        四个驱动单元有:ARM Cortex内核的数据总线和系统总线(它们为单片机的核心部分)、两个DMA(直接存储器访问单元,这两个单元与CPU内核有着同等地位,其作用就是在硬件上绕过CPU,直接在内存与IO口设备间开辟一条数据传输的通路,从而降低CPU开销,提高效率)

        四个被动单元:单片机的内存Flash、闪存SRAM、可变的静态存储器控制器FSMC外设(常用的片载资源),驱动单元和被动单元之间通过总线矩阵以及AHB系统总线进行连接和协调。

        单片机的变量其实都存储在一个叫做SRAM的存储器中,为单片机的内存,存储速度极快,而存储变量的这个步骤是由单片机的内核通过总线来操作的。如果存在大量的数据转运将会导致CPU极其宝贵的资源都消耗在数据转移上面,为了解决转移数据占用CPU资源的这个痛点,故DMA就诞生了。

二、DMA的介绍(Direct Memory Access)

         DMA的英文直译的意思是直接内存访问,主要作用就是转移数据 ,用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输(通俗的讲就是讲数据从一个地址空间复制到另一个地址空间)。无须内核参与,数据可以通过DMA快速地移动,可以节省CPU的资源来做其他操作(比如使内核腾出手专心操作IO口或响应中断等)。在此举个例子让你们更好理解DMA带来的作用:

        比如下面的这个程序是先发送10000个串口数据,然才才能让LED闪烁。即发送数据和LED闪烁无法同时进行。

for(i = 0;i<10000;i++)
{
    TXREG = 0xaa;//串口发送0xaa
}
LED = 1;//LED灭0.5s
delay(500);
LED = 0;//LED亮0.5s
delay(500);

        但如果我们使用了DMA,就能一边发送串口数据一边让LED闪烁了。 我们需要发送的数据存储于SRAM中,只有在初始配置DMA时才需要总线参与,配置好后就无需内核参与了,DMA自动完成数据的转运,而内核就能专心处理自己的事情。

         两个DMA控制器有12个独立可配置通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自 于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。DMA的作用总结就是一句话:给CPU减负。

三、DMA框图

        DMA的框图和内核架构图一样,没啥变化。 

2.1  DMA请求:

        如果STM32的外设想要通过DMA来传输数据,必须先给DMA控制器发送DMA请求,DMA收到请求后,控制器就会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。

2.2  DMA通道

        通过DMA通道,可以实现存储器到外设或者外设到存储器。上面也介绍了DMA有DMA1和DMA2两个控制器,而DMA1有7个通道,DMA2有5个通道。下图附上各DMA的通道对应的请求(不同DMA控制器的通道对应不同的外设请求,也就是外设确定时,其通道并不是随便选的):

2.3  DMA触发方式:

        DMA的触发方式主要包括硬件触发软件触发

        硬件触发:当外设准备好数据时,会通过硬件触发源(如ADC、串口、定时器等)向DMA控制器发送信号,触发DMA进行数据传输。这种触发方式适用于与外设相关的数据传输,因为外设的数据通常具有一定的时效性,如ADC转换完成、串口接收到数据或定时时间到达等。硬件触发需要外设的DMA输出被开启,并且触发源的选择取决于外设是否启用了DMA输出。触发源进入仲裁器进行优先级判断,产生内部的DMA请求。

        软件触发:软件触发是通过软件命令或库函数来启动DMA传输。这种方式适用于存储器到存储器的数据传输,因为不需要等待特定的硬件事件。软件触发时,DMA会在接收到触发信号后开始传输数据,直到传输完成。

2.4  仲裁器

        当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。

        第一阶段属于软件阶段,可以在DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。

        第二阶段属于硬件阶段,如果两个或以上的DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。       

四、DMA配置步骤

1.使能DMA控制器(DMA1或DMA2)时钟

        RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

2.  DMA结构体初始化

    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_BufferSize = ;
    DMA_InitStructure.DMA_DIR = ;
    DMA_InitStructure.DMA_M2M = ;
    DMA_InitStructure.DMA_MemoryBaseAddr = ;
    DMA_InitStructure.DMA_MemoryDataSize = ;
    DMA_InitStructure.DMA_MemoryInc = ;
    DMA_InitStructure.DMA_Mode = ;
    DMA_InitStructure.DMA_PeripheralBaseAddr = ;
    DMA_InitStructure.DMA_PeripheralDataSize = ;
    DMA_InitStructure.DMA_PeripheralInc = ;
    DMA_InitStructure.DMA_Priority = ;

    DMA_Init(DMA1_Channel1,&DMA_InitStructure);

第一个为(DMA_BufferSize )缓冲大小,就是需要传输几个数据单元,需要传输几次。假如为一个数组,有四个数据,就填4。

第二个为(Direction)传输方向:第一个为外设站点作为目的地(destination)也就是传输方向是存储器到外设。第二个为外设站点作为源头(source)也就是外设到存储器方向传输。

第三个为(DMA_M2M )是否是存储器到存储器,其实就是选择硬件触发还是软件触发;第一个就是使用软件触发,第二个则为不使用软件触发,也就是硬件触发;怎么选可以看上文关于DMA触发的介绍。

第四个(DMA_MemoryBaseAddr )存储器站点的基地址:

第五个为(DMA_MemoryDataSize )存储器站点数据宽度:可以选择字节、半字,字为宽度。

第六个为(DMA_MemoryInc )存储器站点地址自增:可以选择自增或者不自增;由于我们设置的地址为第一个数据的内存地址,假如地址为一个数组,如果不自增,其数据操作一直在第一个。如果设置为自增,DMA传输完第一个地址上的数据,接下来就会传输第二个地址上的数据,依次类推。故一般数组就设置自增,外设地址的话就不自增了。

第七个为(DMA_Mode )指定传输计数器是否要自动重装:DMA_Mode_Circular(为自动重装)、DMA_Mode_Normal(不自动重装,自减到0后停下来)

第八个为(DMA_PeripheralBaseAddr )外设站点基地址(需要写入一个32位的地址):

第九个为(DMA_PeripheralDataSize )外设站点数据宽度:可以选择字节、半字,字为宽度。和存储器站点数据宽度一样。

第十个为(DMA_PeripheralInc )地址是否自增:外设地址一般不自增,这里我使用ADC,所以不自增。(这就需要看你DMA的配置来决定了,上面有介绍)

最后一个为(DMA_Priority )指定通道的软件优先级:有四个优先级:非常高、高、中等、低;如果有多个通道的话,可以指定一下,确保紧急的转运有更高的优先级。

3.  使能外设的DMA

        例子中是转运ADC1的数据,所以使用:ADC_DMACmd(ADC1,ENABLE);如果要开启别的相应的DMA可以找相应文件找到DMA使能函数。

4.使能DMA通道,启动传输

        DMA_Cmd(DMA1_Channel1,ENABLE);//使能DMA

至此DMA就配置好了,当然DMA是配合其他外设使用的,这里就不介绍其他外设了。

五、ADC多通道连续转换使用DMA转运完整程序

        上篇的ADC文章介绍了非扫描内部触发模式。下面来介绍一下扫描且使用外部触发——定时器触发模式的使用。这里不再介绍ADC的使用了,不明白可以看我上篇文章,这里主要是检测两个摇杆的数据。而DMA的配置上文应该讲的够清楚了,触发的定时器为定时器3,定时器初始化中无需初始化中断,只需配置更新事件就好了。

joy.c

#include "joy.h"

u16 ADC_Data[4];//ADC数据存储

void Joy_Init(void)
{
	/***** GPIO初始化 *****/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitSturcture;
	GPIO_InitSturcture.GPIO_Mode = GPIO_Mode_AIN;//通用推挽输出
	GPIO_InitSturcture.GPIO_Pin = Joystick_Ry | Joystick_Rx | Joystick_Lx | Joystick_Ly;
	GPIO_Init(GPIOA,&GPIO_InitSturcture);
	
	/***** ADC初始化	*****/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);   //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M	
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//开启ADC时钟
	ADC_InitTypeDef ADC_InitStruct;
	
	ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//不使用连续转换
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;//定时器3触发转换
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//只用ADC1,采用独立模式
	ADC_InitStruct.ADC_ScanConvMode = ENABLE;//扫描模式
	ADC_InitStruct.ADC_NbrOfChannel = 4;//转换通道4个
	ADC_Init(ADC1,&ADC_InitStruct);
	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0, 1, ADC_SampleTime_7Cycles5 );//右摇杆x方向
    ADC_RegularChannelConfig(ADC1,ADC_Channel_1, 2, ADC_SampleTime_7Cycles5 );//右摇杆Y轴方向	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_2, 3, ADC_SampleTime_7Cycles5 );//左摇杆左x轴
    ADC_RegularChannelConfig(ADC1,ADC_Channel_3, 4, ADC_SampleTime_7Cycles5 );//左摇杆Y轴
	
	ADC_Cmd(ADC1,ENABLE);
	/***** ADC校准 *****/
    ADC_ResetCalibration(ADC1);//初始化ADC校准寄存器 
    
	while(ADC_GetResetCalibrationStatus(ADC1));//等待ADC校准寄存器初始化完成
	ADC_StartCalibration(ADC1);	 //开启ADC校准
	while(ADC_GetCalibrationStatus(ADC1));	 //等待校准结束 

	ADC_ExternalTrigConvCmd(ADC1,ENABLE);//使用外部触发
	/***** DMA初始化 *****/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	DMA_DeInit(DMA1_Channel1);
	
	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)&ADC_Data;//目标地址,一个空数组
	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模式,一次Normal,循环Circular;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//使能内存到内存的传输,硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;//优先级:高	
	
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);//配置DMA通道
	
	ADC_DMACmd(ADC1,ENABLE);
	DMA_Cmd(DMA1_Channel1,ENABLE);//使能DMA
}


timer.c

void Timer_Init(void)
{
	/***** TIM3的初始化,用于ADC的扫描 *****/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
	TIM_InternalClockConfig(TIM3);
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	/***** 定时器3 *****/
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStruct.TIM_Period = 1000 - 1;
	TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);
	/***** 定时器3更新事件的配置*****/
	TIM_SelectOutputTrigger(TIM3,TIM_TRGOSource_Update);
	TIM_Cmd(TIM3,ENABLE);
}

 joy.h

#ifndef __JOY_H
#define __JOY_H

#include "stm32f10x.h"                  // Device header

#define   Joystick_Ry    GPIO_Pin_0                    
#define   Joystick_Rx    GPIO_Pin_1
#define   Joystick_Lx    GPIO_Pin_2     
#define   Joystick_Ly    GPIO_Pin_3  

void Joy_Init(void);
extern u16 ADC_Data[4];//ADC数据存储

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "timer.h"
#include "joy.h"

int main(void)
{
	OLED_Init();
	Timer_Init();
	Joy_Init();
	
	while (1)
	{
		OLED_ShowNum(0,0,ADC_Data[0],5,16,1);
		OLED_ShowNum(0,16,ADC_Data[1],5,16,1);
		OLED_ShowNum(0,32,ADC_Data[2],5,16,1);
		OLED_ShowNum(0,48,ADC_Data[3],5,16,1);
		OLED_Refresh();
	}
}

下面就能看见其现象了:

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-06 09:16:02       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-06 09:16:02       106 阅读
  3. 在Django里面运行非项目文件

    2024-06-06 09:16:02       87 阅读
  4. Python语言-面向对象

    2024-06-06 09:16:02       96 阅读

热门阅读

  1. 在[0,1]随机生成一个数

    2024-06-06 09:16:02       26 阅读
  2. 【DevOps】掌握 Helm:Kubernetes 应用程序部署指南

    2024-06-06 09:16:02       30 阅读
  3. 达梦数据库

    2024-06-06 09:16:02       30 阅读
  4. 深度学习中无监督学习

    2024-06-06 09:16:02       29 阅读
  5. 正则表达式二

    2024-06-06 09:16:02       26 阅读
  6. 自动化-selenium-元素/窗口常用方法

    2024-06-06 09:16:02       27 阅读
  7. SpringBoot集成:搭建kafka集群 + zookeeper集群

    2024-06-06 09:16:02       30 阅读
  8. 【设计模式】观察者模式(行为型)⭐⭐⭐

    2024-06-06 09:16:02       24 阅读
  9. 基于openssl实现AES ECB加解密

    2024-06-06 09:16:02       23 阅读