目录
DMA简介
- DMA(Direct Memory Access)直接存储器存取
- DMA可以提供外设(外设寄存器,一般是外设的数据寄存器DR)和存储器(运行内存SRAM和程序存储器Flash)或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
- 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
- 每个通道都支持软件触发和特定的硬件触发。(如果DMA进行的是存储器到存储器的数据转运,比如想把Flash的一批数据转运到SRAM里,那就需要软件触发,使用软件触发之后DMA就会一股脑地把这批数据以最快的速度全部转运完成,这也是我们想要的结果。如果DMA进行外设到存储器的数据转运,就不能一股脑了因为外设的数据是有一定时机的,这时我们要用硬件触发,比如转运ADC的数据,那就得ADC每个通道AD转换完成后,硬件触发一次DMA之后再DMA转运,触发一次转运一次,这样数据才是正确的。)
- STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器映像
现在了解一下STM32都有哪些存储器,这些存储器又是安排在哪些地址上。我们知道,计算机系统的5大组成部分是运算器、控制器、存储器、输入设备和输出设备,其中运算器和控制器一般会合在一起叫CPU,所以计算机的核心关键部分就是CPU和存储器。存储器又有两个重要知识点,一个是存储器内容,另一个就是存储器的地址。那STM32也不例外。下表就是STM32中所有类型的存储器和被安排的地址:
表里存储器主要分为两大部分ROM(只读存储器,非易失性、掉电不丢失)和RAM(随机存储器,易失性、掉电丢失)。
其中ROM分为三块,第一块是程序存储器Flash,也就是主闪存,它的用途就是存储C语言编译后的程序代码,也就是我们下载程序的位置,运行程序一般也是从主闪存开始运行的,这一块存储器STM32分配的地址是0x0800 0000,起始地址也就是第一个字节的地址是0800然后剩余字节的地址依次增长,每个字节都分配一个独一无二的地址,最终终止地址取决于它的容量,编到哪里哪里就是终止地址。
下面两块存储器系统存储器和选项字节也是ROM的一种,实际上他们的存储介质也是Flash,只不过我们一般说Flash指的是主闪存Flash,而不是这两块区域。
DMA框图
- DMA总线用于访问各个存储器,内部的多个通道可以进行独立的数据转运。
- 仲裁器用于调度各个通道防止发生冲突。
- AHB从设备用于配置DMA参数。
- DMA请求用于硬件触发DMA的数据转运。
注意一下如果转运地址写了只读的Flash,转运会出错。
DMA基本结构
下面看看DMA具体怎么工作的:
总结DMA进行转运的几个条件:
第一,开关控制,DMA_Cmd必须使能。
第二,传输计数器必须大于0。
第三,触发源必须有触发信号。
触发一次转运一次,传输计数器自减一次,当传输计数器等于0且没有自动重装时,这时无论是否触发DMA都不会继续转运了,此时就需要DMA_Cmd給DISABLE,关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd给ENABLE,开启DMA,DMA才能继续工作。注意一下:写传输计数器时必须先关闭DMA,再进行,不能在DMA开启时写传输计数器,这是手册里的规定。
DMA请求
数据宽度与对齐
前文提到,数据转运的两个站点都有一个数据宽度的参数,如果数据宽度都一样那就是正常的一个个转运,如果数据宽度不一样那会怎么处理呢?下表说明了这个问题:
总结就是把小的数据转到大的里面,高位就会补0,如果把大的数据转运到小的里面,高位就会舍弃掉。
DMA应用
DMA数据转运
任务是将SRAM里的数组DataA转运到另一个数组DataB中。下面描述其配置过程:
首先外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数,外设地址显然应该填DataA数组的首地址,存储器地址给DataB数组的首地址。然后数据宽度,两个数组的类型但是uint8_t,所以选字节。之后地址是否自增,我们需要DataA[0]转运到DataB[0],DataA[1]转运到DataB[1],两个数组的位置一一对应,所以转运完DataA[0]和DataB[0]之后两个站点的地址都应该自增,都移动到下一个数据的位置。
方向参数显然是外设站点转运到存储器站点,如果想把DataB的数据转运到DataA,那可以把方向参数换过来,这样就是反相转运了。
传输计数器和是否要自动重装,在这里显然要转运7次,所以传输计数器给7,自动重装器暂时不需要。
之后触发选择部分这里我们使用软件触发,因为这是存储器到存储器的转运,不需要等待硬件时机。
最后调用DMA_Cmd使能DMA。
这里数据转运是一种复制转运,转运完成后DataA的数据并不会消失。
首先定义DMA转运的源端数组和目的数组:
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};
接下来我们的任务就是初始化DMA,让DMA把DataA里的数据转运到DataB。先建立DMA模块,DMA不涉及外围硬件电路,可以在System里添加。为了防止与库函数的DMA重复我们取名为MyDMA。在MyDMA.c的MyDMA_Init()函数初始化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);//使能
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);//中断输出使能
/*
* 设置当前DMAy通道传输中的数据单元数。给传输计数器写数据的
* 参数1 DMAy_Channelx:其中y可以是1或2来选择DMA
* DMA1的* x取值为1 ~ 7,DMA2的* x取值为1 ~ 5,表示选择DMA通道。
* 参数2 datannumber:当前DMAy通道中的数据单元数
* 转移
* 返回值:无
*/
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
/*
* 返回传输计数器的值
* 参数 DMAy_Channelx:其中y可以是1或2来选择DMA
* DMA1的* x取值为1 ~ 7,DMA2的* x取值为1 ~ 5,表示选择DMA通道。
* 返回值:当前DMAy通道中剩余待转移的数据单元的数量
*/
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);//清除中断挂起位
第一步,RCC开启时钟。DMA是AHB总线的设备,要用AHB外设时钟控制函数。
/* 第一步RCC开启DMA时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
第二步,DMA_Init初始化DMA。第一个参数选择哪个DMA以及DMA的哪个通道,当前是存储器到存储器通道可以任意选择,我们就选DMA1的通道1;第二个参数结构体,它成员比较多:
- DMA_PeripheralBaseAddr,外设站点的起始地址
- DMA_PeripheralDataSize,外设站点的数据宽度
- DMA_PeripheralInc,外设站点是否自增
- DMA_MemoryBaseAddr,存储器站点的起始地址
- DMA_MemoryDataSize,存储器站点的数据宽度
- DMA_MemoryInc,存储器站点是否自增
- DMA_DIR,传输方向(指定外设站点是源端还是目的地)
- DMA_BufferSize,缓冲器大小,其实就是传输计数器
- DMA_Mode,传输模式,其实就是选择是否自动重装,正常模式不自动重装
- DMA_M2M,选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发
- DMA_Priority,优先级,按照参数要求给一个优先级就行了
第一个外设站点的基地址,这里要写一个32位的地址,对于SRAM的数组它的地址是编译器分配的并不是固定的,所以我们一般不会写绝对地址,而是通过数组名来换取地址,这里我们就把地址提取成初始化函数的参数。
数据宽度为字节。
上文分析我们数组之间的转运地址需要自增。
存储器站点的三个参数类似。
对于传输方向,我们打算把DataA放在外设站点,DataB放在存储器站点,传输方向就是外设站点到存储器,所以外设站点作为数据源。
对于传输计数器,把BufferSize也提取到MyDMA_Init参数里。
对于传输模式,这里我们转运数组,是存储器到存储器的传输,转运一次停下来就行了,所以不用自动重装。
对于M2M,我们转运数组采用软件触发。
对于优先级,如果有多个通道可以指定一下确保紧急的转运有更高的优先级,目前一个通道,优先级就随意了,我们选一个中等。
/* 第二步调用DMA_Init初始化各个参数 */
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设站点的起始地址,数组A的首地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设站点的数据宽度,字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设站点是否自增,自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//存储器站点的起始地址,数组B的首地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器站点的数据宽度,字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点是否自增,自增
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的参数就配置完成了,目前DMA暂时还不会工作,回忆DMA转运三个条件:传输计数器大于0,触发源有触发信号,DMA使能。我们还未使能,我们不让DMA初始化之后立刻转运,而是调用MyDMA_Transfer再进行转运。
/* 第三步开关控制,不让DMA初始化之后立刻转运,而是调用MyDMA_Transfer再进行转运 */
DMA_Cmd(DMA1_Channel1,DISABLE);
如果DataA的数据又变化了,我们想再转运一次,那该怎么办呢?这时我们就需要给传输计数器重新赋值了,也就是再写一个MyDMA_Transfer函数。调用一次这个函数就再启动一次DMA转运。在里面我们需要给传输计数器重新赋值,也就是先DMA失能,然后给传输计数器赋值,再DMA使能。指定给传输计数器的值我们需要获取初始化的Size参数,在初始化代码里赋值给全局变量MyDMA_Size。转运开始之后,我们还需要一个工作,就是等待转运完成,检查DMA1通道1转换完成标志位,转运完成标志位置1,我们在while循环等待,最后不要忘了清除标志位。
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);//DMA失能
DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);//重新给传输计数器赋值
DMA_Cmd(DMA1_Channel1,ENABLE);//DMA重新使能
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待转运完成
DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
}
完整代码:
MyDMA.c:
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)
{
MyDMA_Size = Size;
/* 第一步RCC开启DMA时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
/* 第二步调用DMA_Init初始化各个参数 */
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设站点的起始地址,数组A的首地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设站点的数据宽度,字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设站点是否自增,自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//存储器站点的起始地址,数组B的首地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器站点的数据宽度,字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点是否自增,自增
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初始化之后立刻转运,而是调用MyDMA_Transfer再进行转运 */
DMA_Cmd(DMA1_Channel1,DISABLE);
}
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);//DMA失能
DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);//重新给传输计数器赋值
DMA_Cmd(DMA1_Channel1,ENABLE);//DMA重新使能
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待转运完成
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};
void Show(void)
{
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);
}
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);
Show();
while(1)
{
DataA[0]++;
DataA[1]++;
DataA[2]++;
DataA[3]++;
Show();
Delay_ms(1000);
MyDMA_Transfer();
Show();
Delay_ms(1000);
}
}
DMA+AD多通道(ADC单次扫描+DMA单次转运)
DMA配置如下:
- 外设地址:ADC_DR寄存器的地址。
- 存储器地址:在SRAM中定义一个数组ADValue,ADValue地址当做存储器地址。
- 数据宽度:因为ADC_DR和SRAM数组但是uint16_t的数据,所以数据宽度都是16位的半字传输。
- 地址是否自增:外设地址不自增,存储器地址自增。
- 传输方向:外设站点到存储器站点。
- 传输计数器:7个通道,所以计数7次。
- 计数器是否自动重装,这里可以看ADC配置,ADC如果是单次扫描那DMA传输计数器可以不自动重装,转换一轮就停止。如果ADC是连续扫描,那DMA就可以自动重装,在ADC启动下一轮转换时DMA也启动下一轮转运。
- 触发选择:ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发选择ADC的硬件触发。说明:ADC扫描模式在每个单独的通道转换完成后没有任何标志位也不会触发中断,所以程序不太好判断某一个通道转换完成的时机,但是应该会产生DMA请求,去触发DMA转运。
在AD多通道代码基础上,在AD.c加上DMA数据转运功能,在ADC使能之前进行DMA结构体初始化,这里使用ADC扫描模式+DMA数据转运,首先扫描PA0到PA3这四个通道,相当于点菜菜单这里点四个菜。通道0放在序列1,通道1放在序列2,通道2放在序列3,通道3放在序列4。
/* 第三步选择规则组输入通道 */
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扫描模式这个参数改成ENABLE,通道数目为4。连续模式这里可以是连续扫描也可以是单次扫描,我们都会演示,先用单次扫描。
/* 第四步配置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 = DISABLE;//连续转换模式(选择连续转换还是单次转换)
ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描转换模式(选择扫描模式还是非扫描模式)
ADC_InitStructure.ADC_NbrOfChannel = 4;//通道数目,指定在扫描模式下总共会用到几个通道
ADC_Init(ADC1,&ADC_InitStructure);
接下来配置DMA参数,外设源地址在ADC_DR寄存器,写(uint32_t)&ADC1->DR。数据宽度想要DR寄存器的低16位数据(高16位是ADC2的),所以选半字。
外设站点是源,传输数量给4,因为有四个ADC通道,所以传输4次,传输模式给正常的单次模式或者自动重装的循环模式。先给正常。M2M给DISABLE,不使用软件触发,我们需要硬件触发,触发源为ADC1。最后参数配置到DMA1的通道1,这里通道不能任意选择,ADC1的硬件触发只接在了DMA1的通道1上。接着DMA_Cmd可以直接使能,这时DMA转运的三个条件,传输计数器大于0满足,DMA使能满足,触发源有信号目前是不满足的,因为这里是硬件触发,ADC还没启动,不会有触发信号,所以这里DMA使能之后不会立刻工作,最后在ADC只能之前还有一个事情要做技术开启ADC到DMA的输出。
/* 第五步调用DMA_Init初始化各个参数 */
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址,ADC1_DR的地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度,DR寄存器低16位
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点是否自增,不自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址,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_Normal;//传输模式,其实就是选择是否自动重装,正常模式不自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级,按照参数要求给一个优先级就行了
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
/* 第六步开关控制使能DMA */
DMA_Cmd(DMA1_Channel1,ENABLE);
/* 开启ADCDMA触发信号 */
ADC_DMACmd(ADC1,ENABLE);
在AD_GetValue里,参数和返回值都不需要了,函数里面因为现在还是ADC还是单次模式,所以还需要软件触发一下ADC开始,其他的不需要了。因为DMA也是正常的单次模式,所以在触发ADC之前,需要再重新写入一下传输计数器的值,传输次数给4,最后等待AD转换和DMA转运完成,这里因为转运总是在转换完成之后的,所以我们可以只等待DMA转运完成。
void AD_GetValue(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);//DMA失能
DMA_SetCurrDataCounter(DMA1_Channel1,4);//重新给传输计数器赋值
DMA_Cmd(DMA1_Channel1,ENABLE);//DMA重新使能
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//触发转换
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待转运完成
DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
}
这样,当我们调用AD_GetValue函数ADC开始转换,连续扫描四个通道,DMA转运也同步进行,AD转换结果依次存放在上面的AD_Value数组里。
完整代码:
AD.c:
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
/* 第一步开启RCC开启时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/* RCC开启DMA时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置ADCCLK
/* 第二步配置GPIO */
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转换器 */
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 = DISABLE;//连续转换模式(选择连续转换还是单次转换)
ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描转换模式(选择扫描模式还是非扫描模式)
ADC_InitStructure.ADC_NbrOfChannel = 4;//通道数目,指定在扫描模式下总共会用到几个通道
ADC_Init(ADC1,&ADC_InitStructure);
/* 第五步调用DMA_Init初始化各个参数 */
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址,ADC1_DR的地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度,DR寄存器低16位
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点是否自增,不自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址,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_Normal;//传输模式,其实就是选择是否自动重装,正常模式不自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级,按照参数要求给一个优先级就行了
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
/* 第六步开关控制使能DMA */
DMA_Cmd(DMA1_Channel1,ENABLE);
/* 开启ADCDMA触发信号 */
ADC_DMACmd(ADC1,ENABLE);
/* 第七步开启ADC并进行校准 */
ADC_Cmd(ADC1,ENABLE);
ADC_ResetCalibration(ADC1);//复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);//等待返回复位校准的状态
ADC_StartCalibration(ADC1);//开启校准
while (ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
}
void AD_GetValue(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);//DMA失能
DMA_SetCurrDataCounter(DMA1_Channel1,4);//重新给传输计数器赋值
DMA_Cmd(DMA1_Channel1,ENABLE);//DMA重新使能
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//触发转换
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待转运完成
DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
}
AD.h:
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
void AD_GetValue(void);
#endif
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)
{
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(100);
}
}
ADC多次扫描+DMA循环转运
那我们还可以配置ADC多次扫描+DMA循环转运。这样更加方便。当ADC触发之后,ADC连续转换,DMA循环转运,两者一直在工作,始终把转换的最新结果刷新到SRAM数组里,当我们想要数据的时候直接去数组里取就行了。
完整代码:
AD.c:
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
/* 第一步开启RCC开启时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/* RCC开启DMA时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置ADCCLK
/* 第二步配置GPIO */
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转换器 */
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 = DISABLE;//连续转换模式(选择连续转换还是单次转换)
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//打开连续转换模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描转换模式(选择扫描模式还是非扫描模式)
ADC_InitStructure.ADC_NbrOfChannel = 4;//通道数目,指定在扫描模式下总共会用到几个通道
ADC_Init(ADC1,&ADC_InitStructure);
/* 第五步调用DMA_Init初始化各个参数 */
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址,ADC1_DR的地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度,DR寄存器低16位
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点是否自增,不自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址,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_Normal;//传输模式,其实就是选择是否自动重装,正常模式不自动重装
DMA_InitStructure.DMA_Mode = DMA_Mode_DMA_Mode_Circular;//打开DMA循环模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级,按照参数要求给一个优先级就行了
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
/* 第六步开关控制使能DMA */
DMA_Cmd(DMA1_Channel1,ENABLE);
/* 开启ADCDMA触发信号 */
ADC_DMACmd(ADC1,ENABLE);
/* 第七步开启ADC并进行校准 */
ADC_Cmd(ADC1,ENABLE);
ADC_ResetCalibration(ADC1);//复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);//等待返回复位校准的状态
ADC_StartCalibration(ADC1);//开启校准
while (ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//ADC触发直接放在初始化最后一行
}
//void AD_GetValue(void)
//{
// DMA_Cmd(DMA1_Channel1,DISABLE);//DMA失能
// DMA_SetCurrDataCounter(DMA1_Channel1,4);//重新给传输计数器赋值
// DMA_Cmd(DMA1_Channel1,ENABLE);//DMA重新使能
//
// //ADC_SoftwareStartConvCmd(ADC1,ENABLE);//触发转换
//
// while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待转运完成
// 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)
{
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(100);
}
}