一、简介
上节课我们讲解了DMA的主要结构以及实现数据转运的基本原理,在本节课我们主要学习代码部分,使用DMA实现数据转运以及结合ADC实现多通道的使用。
上节课的地址:初学者STM32---DMA数据储存-CSDN博客
二、转运数组数据

cpp
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
cpp
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA ; //起始地址(外设)
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //起始地址(存储器)
DMA_InitStructure.DMA_MemoryDataSize= DMA_MemoryDataSize_Byte; //数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //是否自增
首先就是打开DMA的总线,通过查找可以知道DMA是AHB总线的设备,所以是AHB时钟。
初始化外设与存储器的三个参数:起始地址、数据宽度、是否自增
外设的起始置:地址为AddrA,这个地址是我设置初始数据地址,这是一个地址后面要用
数据宽度:字节传输uint8_t
是否自增:是(传输一个数值,地址++,因为这里是传输数组,所以要自增)
寄存器起始地址:AddrB,这个地址是我设置的,后面会传入地址
数据宽度:字节传输uint8_t
是否自增:是(传输一个后挪一个坑位防止数据覆盖)
cpp
DMA_InitStructure.DMA_DIR= DMA_DIR_PeripheralSRC; //传输方向
DMA_InitStructure.DMA_BufferSize = Size;//缓冲区大小
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式(是否重装)
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器(硬件触发或者软件触发)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
DMA_Cmd(DMA1_Channel1,ENABLE);
传输方向:外设为资源(也就是从外设传输到储存器)
缓冲区大小BufferSize:Size(这个是我在函数定义中定义的大小,后面通过调用传入数值)
是否重装:否(这里是因为采用了软件触发的方式,不能用自动重装)
触发方式:软件触发。
优先级:中(这个程序只有一个数据转运,没有先后之分,这里就用中等)
随后将结构体配置传进DMA_Init中(DMA1_Channel1按照上图要求填写)
最后开启DMA_Cmd。
cpp
uint8_t DataA[4] ={0x01,0x02,0x03,0x04};
uint8_t DataB[4] ={0,0,0,0};
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
因为我们地址没有数值,所以定义了DataA数组和DataB数组,我们尝试通过数据转运将DataA里面的数据传入DataB。
所以将这DataA和DataB的地址以及传输的次数(BufferSize)传进MyDMA_Init初始化函数里面。
虽然DataA和DataB是数组名,是一个地址,但是也需要强制转换成32位的地址,这样就能够与DMA所写入的地址保持一致了。
cpp
void DMA_Tranfer()
{
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);
}
最后,为了能够让改变DataA数据也能够转运到DataB当中,我加入了转换函数,其原理就是先关闭DMA转运,再重新放入转运的次数和通道再开启DMA
然后就是转运标志位必须要转运完成后进行清除才能进行下一次的转运。
最后程序能够实现DataA转运到DataB当中,且DataA在改变数据的同时也能转运到DataB。
三、DMA对ADC多通道转运
在ADC的扫描模式当中,由于转换过程中会因为数据无法及时挪走而导致数据被覆盖,所以我们引入DMA在扫描每一次都进行触发转运,这样就能够实现每一个数据都能够得到转换。

上图为主要思路。
连线图

电位器、光敏传感器、热敏传感器、反射式红外传感器 分别接入PA0,PA1,PA2,PA3这三个接口
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); //开启RCC时钟ADC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIO时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //72Mhz/6=12Mhz
GPIO_InitTypeDef GPIO_InitStructure; //通过结构体配置GPIO模式
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);
首先就是正常的配置RCC时钟,GPIO配置。。
然后进行ADC通道的选择,四个通道,四个序列,采样时时间不变
cpp
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_NbrOfChannel = 4; //扫描4个
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //持续转换模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式
ADC_Init(ADC1,&ADC_InitStructure);
ADC结构体配置
模式:选择独立模式,即单独使用ADC1
数据对齐:选择右对齐
外部触发:使用软件触发,不需要外部触发
连续转换:使能,每转换一次规则组序列后立刻开始下一次转换
扫描模式:使能,扫描规则组的序列,扫描数量由ADC_NbrOfChannel确定
通道数:4,扫描规则组的前4个通道
最后将结构体变量交给ADC_Init,配置ADC1
cpp
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结构体变量
外设基地址:(uint32_t)&ADC1->DR ,选择的是ADC1的地址
外设数据宽度:半字,对应16为的ADC数据寄存器
外设地址自增:失能,始终以ADC数据寄存器为源
存储器基地址:给定存放AD转换结果的全局数组AD_Value
存储器数据宽度:半字,与源数据宽度对应
存储器地址自增:使能,每次转运后,数组移到下一个位置
数据传输方向:选择由外设到存储器,ADC数据寄存器转到数组
转运的数据大小(转运次数):与ADC通道数一致为 4
模式:循环模式,与ADC的连续转换一致
存储器到存储器:失能,数据由ADC外设触发转运到存储器
优先级:选择中等
最后将结构体变量交给DMA_Init,配置DMA1的通道1
cpp
DMA_Cmd(DMA1_Channel1,ENABLE);
ADC_DMACmd(ADC1,ENABLE);
ADC_Cmd(ADC1,ENABLE);
ADC_ResetCalibration(ADC1); //复位校准
while(ADC_GetCalibrationStatus(ADC1) == SET); //返回复位校准状态
ADC_StartCalibration(ADC1); //开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
最后就是开关函数
打开DMA、ADC触发DMA、ADC三个使能开关。

我们看到框中的这个位置: 我们选择的是ADC1触发DMA,然后经过总裁器选择触发优先级(这里只有ADC1这一个触发),必须要开启这里一路才有效。
ADC_DMACmd(ADC1,ENABLE) 这个函数
接着校准ADC,这个是固定的操作
最后打开软件触发:软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
最后成功实现ADC扫描模式+DMA转运数据并显示出来
四、ADC+DMA完整代码
main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
AD_Init(); //AD转换初始化函数
OLED_Init(); //初始化OLED
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);
}
}
AD.c
cpp
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); //开启RCC时钟ADC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIO时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //72Mhz/6=12Mhz
GPIO_InitTypeDef GPIO_InitStructure; //通过结构体配置GPIO模式
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_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_NbrOfChannel = 4; //扫描4个
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //持续转换模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式
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_GetCalibrationStatus(ADC1) == SET); //返回复位校准状态
ADC_StartCalibration(ADC1); //开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
AD.h
cpp
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif