重要的内容写在前面:

  1. 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
  2. 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
  3. 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
  4. 如有错漏欢迎指出。

视频链接:[8-1] DMA直接存储器存取_哔哩哔哩_bilibili

一、STM32中的DMA及存储分布

1、DMA(Direct Memory Access)直接存储器存取概述

(1)DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,而且传输可以是双向的,数据传输过程无须CPU干预,CPU只需设置传输方向、起点和终点的地址等参数。

(2)DMA进行的数据传输需要基于通道,每个通道都支持软件触发和特定的硬件触发(被触发即开始数据传输)。

(3)STM32F103C8T6的DMA资源:DMA1(7个通道)。

2、存储器映像

(1)单片机中有不同的存储器,有的用来存储程序,有的用来存储运行过程中的临时变量,有的是外设寄存器,CPU要想访问它们,就需要对它们进行编址,按照地址访问其中的内容。

(2)STM32F103C8T6的存储器映像如下表所示:

类型

起始地址

存储器

用途

ROM(掉电不丢失)

0x0800 0000

程序存储器Flash

存储C语言编译后的程序代码和常量数据(比如const修饰的变量)

0x1FFF F000

系统存储器

存储BootLoader,用于串口下载

0x1FFF F800

选项字节

存储一些独立于程序代码的配置参数

RAM(掉电丢失)

0x2000 0000

运行内存SRAM

存储运行过程中的临时变量

0x4000 0000

外设寄存器

存储各个外设的配置参数

0xE000 0000

内核外设寄存器

存储内核各个外设的配置参数

3、DMA框图

(1)除了内核以外,其它模块基本都可以视作存储器,它们之间的数据传输都可以由DMA完成。

(2)为了高效且有条理地访问存储器,STM32设计了一个总线矩阵,总线矩阵左侧是主动单元,它们拥有存储器的访问权,右侧是被动单元,它们的存储器只能被左边的主动单元读写。

①内核可以通过DCode和系统总线访问右侧的存储器,其中DCode总线专门用于访问Flash,系统总线则用于访问其它存储器。

②由于DMA需要转运数据,所以DMA也有访问的主动权,DMA通过DMA总线可以访问右侧的存储器。

③DMA有若干个通道,CPU可以分别设置各个通道转运数据的源地址和目的地址,然后它们就可以脱离CPU独立工作。

④仲裁器用于决定哪个通道使用DMA总线,同一个DMA中的通道只能分时复用一条DMA总线,如果产生冲突,仲裁器会根据通道的优先级进行分配。总线矩阵中也有仲裁器,如果DMA和CPU都需要访问同一个目标,那么CPU的访问会被暂停,不过总线仲裁器会保证CPU得到一半的总线带宽,让CPU能够正常工作。

⑤CPU或DMA直接访问Flash的话,只可对Flash进行读操作;SRAM是运行内存,可以任意读写。

(3)DMA中有AHB从设备,这个是DMA自身的寄存器,它连接在AHB总线上,CPU可以通过系统总线访问AHB总线,以此对DMA进行配置。

(4)右侧的外设可以向DMA发送请求,也就是硬件触发DMA转运数据。(需要注意的是,如果使用硬件触发,那么配置外设时,需要打开相应的DMA通道,DMA的每个通道对应不同的硬件触发源,不可以随意匹配)

4、DMA基本结构

(1)DMA的数据转运方向可以是从外设到存储器,也可以是从存储器到外设,也可以是存储器到存储器。

(2)数据转运的起点和终点都有三个相关的参数:

①第一个参数是起始地址,该参数指向需要转运的第一个数据的地址(起点首地址)和负责接收数据的地址(终点首地址)。

②第二个参数是数据宽度,该参数指定一次转运多少位的数据。

③第三个参数是地址是否自增,它指定一次数据转运完成后,下一次转运需不需要将地址移动到下一个位置,具体移动步长由数据宽度决定。(一般寄存器方不需要地址自增,而存储器方需要)

(3)传输计数器(NDTR)是一个自减计数器,每次传输开始时,它的初始值指定数据转运次数。当计数器值为0时,起点/终点的“自增地址”会恢复为起始地址,为下一次数据转运做准备。

(4)自动重装器决定传输计数器值为0时是否将其恢复为初值,对应单次模式和循环模式:

①单次模式:转运完成后,传输计数器的值不会自动重置,DMA暂时停止工作。

②循环模式:一轮转运结束后,传输计数器的值自动置为初值,DMA马上开始下一轮转运。

(5)DMA有软件触发和硬件触发两种触发方式,M2M=1时,DMA为软件触发,M2M=0时,DMA为硬件触发。

①软件触发不是CPU调用一次函数,传输才进行一次,CPU配置好参数并使能DMA通道后,DMA会依托总线全速连续搬运数据,直至传输计数器减为0自动停止,全程无需CPU反复干预。存储器与存储器间的数据转运通常选用软件触发;软件触发没有停止触发的条件,若开启循环模式,会造成无限次非法传输。

②硬件触发由硬件外设向DMA发出数据传输请求,每发出一次DMA请求,DMA进行一次数据传输。存储器与外设间的数据转运通常选用硬件触发,并搭配循环模式使用(这个不是绝对的,也有很多搭配单次模式使用的情况,下图所示的是搭配循环模式使用的描述);硬件触发源可以选择ADC、串口、定时器等。

③一次触发会完成一次数据单元的完整搬运,搬运完成后,传输计数器自减1。

(6)当传输计数器等于0,且没有配置为自动重装(循环模式)时,无论是否有触发信号,DMA都不会处理转运,这时需要调用DMA_Cmd函数关闭DMA,然后给传输计数器写一个大于0的值,再调用DMA_Cmd函数使能DMA,才能开始下一次转运(在开始下次转运前,CPU可以选择是否重新配置起始地址等参数,再使能DMA通道)。

5、DMA硬件触发源与通道的对应关系

(1)DMA使用硬件触发时,需要将请求DMA的外设通往DMA的通道打开,比如ADC1需要给DMA发送请求,那么配置ADC1时需要打开ADC1和DMA间的通道。

(2)DMA的每个通道对应不同的硬件触发源,不可以随意匹配,下图所示的是STM32F103C8T6的DMA1请求映像。

6、数据宽度与对齐

(1)下表是STM32 DMA数据位宽转换表,定义了跨位宽DMA拷贝时内存的数据排布方式(比如把uint8_t数组搬运到uint16_t数组),当外设位宽和内存位宽不一致时,可以提前知道字节映射顺序(表格里所有跨位宽传输都遵循小端存储规则),不用手动移位拼接。

(2)该表适用于源地址和目标地址都开启自增的情况。

二、DMA的运用示例

1、存储器至存储器的数据转运

(1)见下图,本例将SRAM中的数组DataA转运到SRAM中的另一个数组DataB中:

①起点的起始地址为DataA数组的首地址,终点的起始地址为DataB数组的首地址;转运数据宽度为8位,每次转运正好转运一个数组元素;转运过程中起点和终点的地址都需自增。

②方向为DataA的数据转运至DataB中。

③Data数组有7个元素,传输计数器的初值为7,不需要自动重装(简单的值拷贝只要做一次就够了)。

④存储器与存储器间的数据转运使用软件触发。

(2)按照下图所示接好线路,并将OLED显示屏的工程文件夹作为模板复制一份使用

 (3)在stm32f10x_dma.h文件中有DMA相关的函数。

[1] DMA_DeInit函数:将指定DMA通道的所有寄存器复位为默认值(缺省值)。

函数原型:void DMA_DeInit(DMA_Channel_TypeDef* DMA_Channelx);

参数解释:

        DMA_Channel_TypeDef* DMA_Channelx:指定要进行复位的DMA通道

返回值:无返回值

[2] DMA_Init函数:根据DMA_InitTypeDef结构体中指定的参数(外设/内存地址、传输方向、数据大小、缓冲区长度、传输模式、优先级等),初始化指定的DMA通道。

函数原型:void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);

参数解释:

        DMA_Channel_TypeDef* DMA_Channelx:指定要初始化的DMA通道

        DMA_InitTypeDef* DMA_InitStruct:包含了DMA通道所有配置参数的结构体

返回值:无返回值

[3] DMA_StructInit函数:将DMA配置结构体DMA_InitTypeDef的所有成员填充为安全、确定的默认值。

函数原型:void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);

参数解释:

        DMA_InitTypeDef* DMA_InitStruct:包含了DMA通道所有配置参数的结构体

返回值:无返回值

[4] DMA_Cmd函数:使能或禁用指定的DMA通道。

函数原型:void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);

参数解释:

        DMA_Channel_TypeDef* DMA_Channelx:指定要操作的DMA通道

        FunctionalState NewState:指定DMA通道的新状态,ENABLE表示使能DMA通道,DISABLE表示禁用DMA通道

返回值:无返回值

[5] DMA_ITConfig函数:使能或禁用指定DMA通道的中断源。

函数原型:void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);

参数解释:

        DMA_Channel_TypeDef* DMA_Channelx:指定要配置中断的DMA通道

        uint32_t DMA_IT:指定要使能或禁用的中断源类型(传输完成中断、传输半满中断、传输错误中断)

        FunctionalState NewState:指定中断源的新状态,ENABLE表示使能,DISABLE表示禁用

返回值:无返回值

[6] DMA_SetCurrDataCounter函数:设置指定DMA通道的当前数据传输计数器的值。

函数原型:void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);

参数解释:

        DMA_Channel_TypeDef* DMA_Channelx:指定要设置的DMA通道

        uint16_t DataNumber:本次DMA传输的数据单元数量

返回值:无返回值

[7] DMA_GetCurrDataCounter函数:读取指定DMA通道当前传输计数器的值,即获取本次DMA传输中尚未完成的数据单元数量(剩余待传输数据量)。

函数原型:uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);

参数解释:

        DMA_Channel_TypeDef* DMA_Channelx:指定要读取传输计数器的DMA通道

返回值:当前DMA通道剩余待传输的数据单元数量

[8] DMA_GetFlagStatus函数:检查指定DMA通道的某个状态标志位是否被置位,即判断对应的DMA事件(如传输完成、半传输完成、传输错误等)是否已经发生。

函数原型:FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);

参数解释:

        uint32_t DMAy_FLAG:指定要检查的DMA标志位类型

返回值:SET表示指定的标志位被置位,RESET表示指定的标志位未被置位

[9] DMA_ClearFlag函数:清除指定DMA通道的一个或多个待处理状态标志位。

函数原型:void DMA_ClearFlag(uint32_t DMAy_FLAG);

参数解释:

        uint32_t DMAy_FLAG:指定要清除的DMA标志位类型

返回值:无返回值

[10] DMA_GetITStatus函数:检查指定DMA通道的特定中断是否已经发生且被使能,即判断是否有有效的中断请求正在等待处理。

函数原型:ITStatus DMA_GetITStatus(uint32_t DMAy_IT);

参数解释:

        uint32_t DMAy_IT:指定要检查的DMA中断源类型

返回值:SET表示指定的中断源已发生且已被使能——存在有效的中断请求,RESET表示指定的中断源未发生,或虽已发生但中断未被使能

[11] DMA_ClearITPendingBit函数:清除指定DMA通道的中断挂起标志位。

函数原型:void DMA_ClearITPendingBit(uint32_t DMAy_IT);

参数解释:

        uint32_t DMAy_IT:指定要清除的DMA中断挂起位类型

返回值:无返回值

(4)在项目的System组中添加MyDMA.h文件和MyDMA.c文件,用于封装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

uint16_t MyDMA_Size;    //记录传入的Size值(一轮转运的数据个数/传输计数器初值)

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;  //记录传入的Size值(一轮转运的数据个数/传输计数器初值)
	
	//开启DMA的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	//初始化传输参数
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;  //起点的起始地址(起点的基地址)
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  //每次转运1个字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;  //起点的地址自增
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;      //终点的起始地址(终点的基地址)
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;          //每次转运1个字节
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;          //终点的地址自增
	DMA_InitStructure.DMA_BufferSize = Size;           //传输计数器初值
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;      //正常模式(计数器自减为0,一轮转运结束)
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设站点(DMA_PeripheralBaseAddr)作为数据源(方向参数)
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;        //存储器到存储器
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  //指定优先级(本例对优先级无要求)
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	//DMA1_Channel1:选择DMA1的通道1进行AddrA->AddrB的数据转运
	
	//暂时不使能DMA,需要数据转运时调用MyDMA_Transfer函数即可
	//DMA_Cmd(DMA1_Channel1, ENABLE);
}

void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);  //失能DMA(修改传输计数器前需要关闭DMA)
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);  //修改传输计数器的值(置为初值)
	DMA_Cmd(DMA1_Channel1, ENABLE);   //使能DMA
	//传输计数器的值被置为初值,由于是软件触发模式,DMA直接开始一轮数据转运
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);   //等待一轮转运完成,传输计数器自减为0后标志位会被置为1
	DMA_ClearFlag(DMA1_FLAG_TC1);     //清空标志位,为下一轮转运做准备
}

(5)在main.c文件中粘贴以下代码,然后进行编译,将程序下载到开发板中,观察OLED屏的显示。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "MyDMA.h"
#include "Delay.h"

//DataA中的数据需要转运到DataB中
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);
	//将DMA的通道1配置为用于实现DataA->DataB的数据转运,一共转运4个数据)
	
	OLED_ShowString(1, 1, "DataA:");
	OLED_ShowString(3, 1, "DataB:");
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);  //显示数组DataA的地址
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);  //显示数组DataB的地址
	//STM32中的地址都是32位的
	//OLED_ShowHexNum不能接收地址类型数据,需要强制类型转换
	
	while(1)
	{
		/*改变DataA数组元素的值*/
		DataA[0] ++;
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		
		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();    //启用DMA进行一轮数据转运(本例中和值拷贝的结果一样)
		
		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);
	}
}

2、ADC至存储器的数据转运

(1)ADC收到触发信号后,会对7个通道上的模拟电压按次序进行轮流转换,在每一个通道转换完成后,需要DMA进行一次数据转运,将ADC中数据寄存器的内容转运到SARM中的ADValue数组,数组中每一个元素各自对应一个通道的电压值。

①起点的起始地址是ADC中数据寄存器的地址,转运过程中地址不需要自增;终点的起始地址是ADValue数组的首地址,转运过程中地址需要自增。

②转运数据宽度为16位,只需转运一次就能转走ADC数据寄存器的16位数据。

③ADC一共转换7个通道的模拟量,传输计数器的初值应为7。

④如果ADC采取单次扫描模式,那么DMA的传输计数器不使用自动重装(即单次模式),在下次触发ADC转换前,先给DMA计数器重新赋初值;如果ADC采取连续扫描模式,那么DMA的传输计数器就要使用自动重装(即循环模式),ADC启动下一轮转换时,DMA将自动进行下一轮的转运。(本例的代码按照ADC单次扫描模式和DMA单次模式进行配置)

⑤DMA的触发源要选择ADC的硬件触发,ADC每完成一个通道的转换,就会产生一个DMA请求,DMA每收到一次请求即进行一次数据转运。

(2)按照下图所示接好线路,并将AD多通道的工程文件夹作为模板复制一份使用

(3)修改AD.h文件和AD.c文件:

①AD.h文件:

#ifndef __AD_H
#define __AD_H

void AD_Init(void);
void AD_GetValue(void);

extern uint16_t AD_Value[4];

#endif

②AD.c文件:

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];  //存放ADC1中4个通道的转换结果

void AD_Init(void)
{
	//开启ADC、DMA和GPIO的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	//配置ADCCLK的分频器
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);  //ADCCLK = 72MHz / 6 = 12MHz
	
	//配置PA0~PA3为模拟输入模式
	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);
	
	//配置多路开关,将4个通道接入规则组
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);  //通道0接入规则组
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);  //通道1接入规则组
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);  //通道2接入规则组
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);  //通道3接入规则组
	
	//配置AD转换器
	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 = DISABLE;    //单次转换
	//ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;   可以使用连续转换模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;           //扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 4;                //序列中的通道数为4
	ADC_Init(ADC1, &ADC_InitStructure);
	
	//初始化DMA传输参数
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;   //起点的起始地址是ADC1的数据寄存器
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  //每次转运2个字节(半字,STM32中1个字32位)
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  //起点(寄存器)的地址不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;        //终点的起始地址是AD_Value数组首地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;          //每次转运2个字节(半字,STM32中1个字32位)
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;           //终点(存储器)的地址自增
	DMA_InitStructure.DMA_BufferSize = 4;               //传输计数器初值(ADC1配置了4个通道,一共需要转运4个数据)
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;       //正常模式(计数器自减为0,一轮转运结束)
	//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;   ADC连续转换模式需要配合DMA循环模式
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //外设站点(DMA_PeripheralBaseAddr)作为数据源(方向参数)
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  //指定优先级(本例对优先级无要求)
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	//DMA1_Channel1:ADC1的硬件触发接在DMA1的通道1上,只能选择DMA的通道1
	
	//打开ADC1和DMA间的通道,允许ADC1向DMA发送请求
	ADC_DMACmd(ADC1, ENABLE);
	
	//使能DMA
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	//开关控制(开启ADC)和校准
	ADC_Cmd(ADC1, ENABLE);
	ADC_ResetCalibration(ADC1);  //复位校准
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);  //等待复位校准完成(复位完成后该位自动置为0)
	ADC_StartCalibration(ADC1);  //开始校准
	while(ADC_GetCalibrationStatus(ADC1) == SET);       //等待校准完成(校准完成后该位自动置为0)

    //ADC_SoftwareStartConvCmd(ADC1, ENABLE); ADC连续转换模式下可以在初始化时就触发ADC,让ADC不停地进行转换
}

void AD_GetValue(void)  //获取一次ADC1中4个通道的转换结果并存放在AD_Value数组中
{
	//DMA循环模式下AD_Value数组中的值本来就不断更新,可以不需要AD_GetValue函数
	//推荐使用ADC连续转换模式配合DMA循环模式,节省软件资源
	
	DMA_Cmd(DMA1_Channel1, DISABLE);  //失能DMA(修改传输计数器前需要关闭DMA)
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);  //修改传输计数器的值(置为初值)
	DMA_Cmd(DMA1_Channel1, ENABLE);   //使能DMA
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);    //软件触发ADC进行转换
	//扫描模式会将4个通道逐个转换一遍,每转换一个通道,数据寄存器更新一次,然后向DMA发送请求
	//DMA将数据寄存器的数据转到AD_Value数组,ADC继续转换下一个通道,DMA传输计数器值-1
	//DMA传输计数器值为0,说明4个通道的数据全部更新到AD_Value数组中,一轮转运完成
	
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);   //等待一轮转运完成,传输计数器自减为0后标志位会被置为1
	DMA_ClearFlag(DMA1_FLAG_TC1);     //清空标志位,为下一轮转运做准备
}

(4)在main.c文件中粘贴以下代码,然后进行编译,将程序下载到开发板中,根据主函数中的注释进行调试。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "AD.h"
#include "Delay.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)
	{
		AD_GetValue();  //获取一次ADC1中4个通道的转换结果并存放在AD_Value数组中
		
		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);  //更新显示的时间间隔
	}
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐