1. DMA初始化结构体详解
标准库函数对每个外设都建立了一个初始化结构体xxx_InitTypeDef(xxx为外设名称),结构体成员用于设置外设工作参数, 并由标准库函数xxx_Init()调用这些设定参数进入设置外设相应的寄存器,达到配置外设工作环境的目的。
结构体xxx_InitTypeDef和库函数xxx_Init配合使用是标准库精髓所在,理解了结构体xxx_InitTypeDef每个成员意义基本上就可以对该外设运用自如。 结构体xxx_InitTypeDef定义在stm32f10x_xxx.h(后面xxx为外设名称)文件中,库函数xxx_Init定义在stm32f10x_xxx.c文件中, 编程时我们可以结合这两个文件内注释使用。
DMA_ InitTypeDef初始化结构体
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; // 外设地址
uint32_t DMA_MemoryBaseAddr; // 存储器地址
uint32_t DMA_DIR; // 传输方向
uint32_t DMA_BufferSize; // 传输数目
uint32_t DMA_PeripheralInc; // 外设地址增量模式
uint32_t DMA_MemoryInc; // 存储器地址增量模式
uint32_t DMA_PeripheralDataSize; // 外设数据宽度
uint32_t DMA_MemoryDataSize; // 存储器数据宽度
uint32_t DMA_Mode; // 模式选择
uint32_t DMA_Priority; // 通道优先级
uint32_t DMA_M2M; // 存储器到存储器模式
} DMA_InitTypeDef;
1) DMA_PeripheralBaseAddr: 外设地址,设定DMA_CPAR寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储器地址。
2) DMA_Memory0BaseAddr: 存储器地址,设定DMA_CMAR寄存器值;一般设置为我们自定义存储区的首地址。
3) DMA_DIR: 传输方向选择,可选外设到存储器、存储器到外设。它设定DMA_CCR寄存器的DIR[1:0]位的值。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。
4) DMA_BufferSize: 设定待传输数据数目,初始化设定DMA_CNDTR寄存器的值,单位是字节数,最大值为2^16。
5) DMA_PeripheralInc: 如果配置为DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它设定DMA_CCR寄存器的PINC位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。
6) DMA_MemoryInc: 如果配置为DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它设定DMA_CCR寄存器的MINC位的值;自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。
7) DMA_PeripheralDataSize: 外设数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定DMA_CCR寄存器的PSIZE[1:0]位的值。
8) DMA_MemoryDataSize: 存储器数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定DMA_CCR寄存器的MSIZE[1:0]位的值。当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小。
9) DMA_Mode: DMA传输模式选择,可选一次传输或者循环传输,它设定DMA_CCR寄存器的CIRC位的值。例程我们的ADC采集是持续循环进行的,所以使用循环传输模式。
10) DMA_Priority: 软件设置通道的优先级,有4个可选优先级分别为非常高、高、中和低,它设定DMA_CCR寄存器的PL[1:0]位的值。DMA通道优先级只有在多个DMA通道同时使用时才有意义,如果是单个通道,优先级可以随便设置。
11) DMA_M2M: 存储器到存储器模式,使用存储器到存储器时用到,设定DMA_CCR的位14 MEN2MEN即可启动存储器到存储器模式。
2. DMA数据转运
初始化的步骤,我们还是看一下PPT的这个基本结构图。
- 第一步,RCC开启DMA的时钟
- 第二步,直接调用DMA_Init,初始化这里各个参数,包括外设和存储器站点的起始地址,数据宽度,地址是否自增、方向、传输计数器、是否需要自动重装,选择触发源、通道优先级,那这所有的参数,通过一个结构体,就可以配置好了。
- 第三步,就可以进行开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。
- 在这里,如果你选择的是硬件触发不要忘了在对应的外设调用一下XXX_DMACmd,开启一下触发信号的输出;如果你需要DMA的中断,那就调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,然后写中断函数就行了。中断的配置各个外设都一样,上面的结构图暂时没有画中断的部分。
- 第四步,在运行的过程中,如果转运完成,传输计数器清0了。这时想再给传输计数器赋值的话,就DMA失能、写传输计数器、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);//设置当前计数器,给传输计数器写数据的
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);
初始化第一步,RCC开启DMA的时钟
注意:这里开启DMA时钟的时候,根据型号不同开启时钟参数也不同
字节,uint8_t
半字,uint16_t
字,uint32_t
第一个是外设站点为DST,目的地,传输方向是:存储器站点到外设站点
第二个是外设站点为SRC,源端,传输方向是: 外设站点到存储器站点
打算把DataA放在外设站点,把DataB放在存储器站点,传输方向是: 外设站点到存储器站点
第一个是循环模式,就是传输计数器自动重装。
第二个是正常模式,就是传输计数器不自动重装,自减到0后停下来。
这里我们转运数组是,存储器到存储器的传输,所以选正常模式。因为循环模式不能应用于存储器到存储器的情况,因为自动重装和软件触发不能同时使用,如果同时使用,DMA就会连续触发,永远也不会停下来。
第一个M2M_Enable,就是使用软件触发。
第二个M2M_Disable, 就是不使用软件触发,也就是使用硬件触发。
MyDMA.c
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size; //定义全局变量,用于Init函数的Size,供Transfer函数使用
/**
*函数:DMA初始化
*参数:AddrA 源数组的首地址
*参数:AddrB 目的数组的首地址
*参数:size转运的数据大小
*/
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size) //对于SRAM的数组,它的地址是编译器分配的,并不是固定的,是通过数组名来获取地址,这里把这个地址提取成初始化函数的参数。这样在初始化的时候,你相转运那个数组,就把哪个数组的地址传进来就行。
{
MyDMA_Size = Size; //初始化时,把size往全局变量也存一份
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启时钟,DMA是AHB总线的设备
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; //存储器站点的起始地址。 给定形参AddrB
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器站点的数据宽度。 以字节的方式传输
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器站点的地址是否自增。 地址自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向。 SRC外设站点为数据源,外设站点到存储器站点
DMA_InitStructure.DMA_BufferSize = Size; //缓存区大小,其实就是配置传输计数器,指定传输几次
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式,其实是,选择是否使用自动重装。 这里我们转运数组是,存储器到存储器的传输,所以选正常模式。循环模式不能应用于存储器到存储器的情况,因为自动重装和软件触发不能同时使用,如果同时使用,DMA就会连续触发,永远也不会停下来。所以DMA_M2M_Enable和DMA_Mode_Circular不能同时配置。
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //选择是否,其实是选择软件触发还是硬件触发,这里是存储器到存储器转运,对应使用软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //指定通道的软件优先级,选择中等优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //初始化 ,把结构体指定的参数,配置到DMA1的通道1
//转运有三个条件,第一个传输计数器大于0。第二个触发源有触发信号。第三个DMA使能
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA使能 这里不给使能,初始化后不会立刻工作,等后续调用Transfer函数,再进行转运
}void MyDMA_Transfer(void) //DMA传输函数,调用一次函数,就再次启动DMA
{
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA先失能,在写入传输计数器之前,需要DMA暂停工作
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); //写入传输计数器,指定将要转运的次数
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,开始工作
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //等待DMA工作完成
DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志
}
MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_Hvoid MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);#endif
Main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"//uint8_t aa = 0x66;
//OLED_ShowHexNum(1, 1, (uint32_t)&aa,8); //显示地址的话,必须强制类型转换为数字才可以,把地址当成一个数字显示,不然编译会报警告,显示指针跨级了
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04}; //定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0}; //定义测试数组DataB,为数据目的地
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); //DMA初始化,把源数组和目的数组的地址传入
/*显示静态字符串*/
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] ++;
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
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); //显示数组DataB
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000); //延时1s,观察转运前的现象
MyDMA_Transfer(); //使用DMA转运数组,从DataA转运到DataB
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
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); //显示数组DataB
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000); //延时1s,观察转运后的现象
}
}
效果:
可以看到临时变量的地址是存在SRAM中的。且DataA数据成功转运到DataB数组中
3.DMA+AD多通道
和AD多通道接法一样。PA0接一个电位器,PA1~PA3接三个传感器模块的AO输出
用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运。
ADC单次扫描+DMA单次转运
AD+DMA.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
//设置ADC时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频
//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);
//规则组通道配置,配置4个通道到顾泽序列组中
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //通道0,放在序列1,每个通道和引脚关系是一一对应的,引脚定义表中规定死的。 开始点菜操作
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //通道1,放在序列2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //通道2,放在序列3
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //通道3,放在序列4
//ADC初始化
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //选择独立模式,单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换,每转换一次后立刻开始下一次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,多通道
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure);
//DMA初始化
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,源头地址,ADC->DR存的是数据寄存器的内容,这里参数是数据寄存器的地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,半字,对应16位的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,端菜的目的地 。给定存放AD转换结果的全局数组AD_Value,把数据AD_Value作为目的地
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,半字,对应16位的ADC数据寄存器
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,由外设到存储器
DMA_InitStructure.DMA_BufferSize = 4; //4个ADC通道,需要传输4次,DMA转运的第一个条件满足
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式,即选择使用自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发到存储器,使用硬件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //ADC1的硬件触发只接在了DMA1的通道1上,所以这里通道必须要使用DMA的通道1,其他的通道都不行
//DMA和ADC使能
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,DMA转运的第三条件满足//但是DMA转运的第二个条件,触发源有信号,目前是不满足的,因为这里是硬件触发,ADC还没启动,不会有触发信号,所以这里DMA使能之后,不会立刻工作。
//上图中有三个硬件触发源,具体使用哪个,取决于把哪个的DMA输出给开启了,ADC_DMACmd函数就是用来开启ADC到DMA的传输的,定时器也是类似的
ADC_DMACmd(ADC1, ENABLE); //开启DMA触发信号的 开启ADC到DMA输出
ADC_Cmd(ADC1, ENABLE); //ADC使能
//ADC校准 ,固定流程,内部有电路会自动执行校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
}/**
* 函 数:获取AD转换的值
* 参 数:无
* 返 回 值:无
*/
void AD_GetValue(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA先失能,在写入传输计数器之前,需要DMA暂停工作
DMA_SetCurrDataCounter(DMA1_Channel1, 4); //写入传输计数器,指定将要转运的次数
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,开始工作
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次,等待ADC和DMA转换完成while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //因为转运总是在转换之后的,等待DMA工作完成即可,等待ADC转换完成的代码,就不需要了
DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志
}
AD+DMA.h
#ifndef __AD_H
#define __AD_Hextern 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); //AD_Value[0]是通道0的转换结果
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);
}
}
这样当我们调用一下AD_GetValue函数,ADC开始转换,连续扫描四个通道DMA也同步进行转运。ADC转换结果依次存放在这上面的AD_Value数组里。
在主循环里,先调用AD_GetValue,无参无返回值,之后数据就直接跑到AD_Value数组里了。AD_Value[0]是第一个序列通道0的转换结果。AD_Value[1]是通道1的转换结果,AD_Value[2]是通道2的转换结果,AD_Value[3]是通道3的转换结果。下载看一下,这里四个通道的转换结果就出来了。
这就是ADC单次扫描加DMA单次转运的模式,那我们还可以配置成ADC连续扫描加DMA循环转运的模式,这样代码就会更加方便。
ADC连续扫描+DMA循环转运。
AD+DMA.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
//设置ADC时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频
//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);
//规则组通道配置,配置4个通道到顾泽序列组中
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //通道0,放在序列1,每个通道和引脚关系是一一对应的,引脚定义表中规定死的。 开始点菜操作
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //通道1,放在序列2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //通道2,放在序列3
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //通道3,放在序列4
//ADC初始化
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //选择独立模式,单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换,每转换一次后立刻开始下一次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,多通道
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure);
//DMA初始化
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,源头地址,ADC->DR存的是数据寄存器的内容,这里参数是数据寄存器的地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,半字,对应16位的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,端菜的目的地 。给定存放AD转换结果的全局数组AD_Value,把数据AD_Value作为目的地
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,半字,对应16位的ADC数据寄存器
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,由外设到存储器
DMA_InitStructure.DMA_BufferSize = 4; //4个ADC通道,需要传输4次,DMA转运的第一个条件满足
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式,即选择使用自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发到存储器,使用硬件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //ADC1的硬件触发只接在了DMA1的通道1上,所以这里通道必须要使用DMA的通道1,其他的通道都不行
//DMA和ADC使能
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,DMA转运的第三条件满足//但是DMA转运的第二个条件,触发源有信号,目前是不满足的,因为这里是硬件触发,ADC还没启动,不会有触发信号,所以这里DMA使能之后,不会立刻工作。
//上图中有三个硬件触发源,具体使用哪个,取决于把哪个的DMA输出给开启了,ADC_DMACmd函数就是用来开启ADC到DMA的传输的,定时器也是类似的
ADC_DMACmd(ADC1, ENABLE); //开启DMA触发信号的 开启ADC到DMA输出
ADC_Cmd(ADC1, ENABLE); //ADC使能
//ADC校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
//ADC触发
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC之后,由于ADC处于连续转换模式,所以触发一次后ADC就可以一直连续工作,另外DMA循环转运,两者一直在工作,始终把最新的转换结果,刷新到SRAM数组里,我们想要数据的时候,随时去数组里取就行了,GetValue()获取ADC转换数据函数也不需要了
}
AD+DMA.h
#ifndef __AD_H
#define __AD_Hextern uint16_t AD_Value[4];
void AD_Init(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)
{
OLED_ShowNum(1, 5, AD_Value[0], 4); //AD_Value[0]是通道0的转换结果
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循环转运模式,主循环啥都不干,直接读取AD_Value数组,就能得到结果,可以看到此时硬件外设,已经实现了相互配合,和高度的自动化,各种操作都是硬件自己完成的,极大地减轻了软件负担,软件什么都不需要做,也不需要进行任何中断,硬件自动把活干完了。
另外还可以在AD_init函数中再加一个外设,比如定时器,ADC用单词扫描,再用定时器去定时触发,这样就是定时器触发ADC,ADC触发DMA,整个过程完全自动,不需要程序手动进行操作,节省软件资源。这就是STM32的硬件自动化一大特色,各个外设相互连接,互相交织,不再是传统这样一个CPU,单独控制多个独立的外设,这样的星型结构,而是外设之间互相连接,互相合作,形成一个网状结构,这样在完成某些简单且繁琐的工作的时候,就不需要CPU来同一调度了,可以直接通过外设之间的相互配合,自动完成这些繁琐的工作,不仅可以减轻CPU负担,还可以大大提高外设的性能。在我们之前的学习中,也经常遇到过这样的设计,比如定时器的输出可以通向adc dac或其他定时器。的触发源可以来自定时器或外部中断的触发源可以来自定时器串口等等。
还有一个存储器到外设的情况,看第5小节,比如串口发送一批数据。就可以使用进行存储器到外设的转运。
4. DMA存储器到存储器模式实验
本章只讲解存储器到存储器和存储器到外设这两种模式,其他功能模式在其他章节使用到的时候再讲。 存储器到存储器模式可以实现数据在两个内存的快速拷贝。我们先定义一个静态的源数据,存放在内部FLASH, 然后使用DMA传输把源数据拷贝到目标地址上(内部SRAM),最后对比源数据和目标地址的数据,看看是否传输准确 。
4.1. 硬件设计
DMA存储器到存储器实验不需要其他硬件要求,只用到RGB彩色灯用于指示程序状态。
4.2. 软件设计
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 这个实验代码比较简单,主要程序代码都在main.c文件中。
4.2.1. 编程要点
使能DMA时钟;
配置DMA数据参数;
使能DMA,进行传输;
等待传输完成,并对源数据和目标地址数据进行比较。
4.2.2. 代码分析
DMA宏定义及相关变量定义
代码清单 22‑1 DMA数据流和相关变量定义
// 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定
#define DMA_CHANNEL DMA1_Channel6
#define DMA_CLOCK RCC_AHBPeriph_DMA1// 传输完成标志
#define DMA_FLAG_TC DMA1_FLAG_TC6// 要发送的数据大小
#define BUFFER_SIZE 32/* 定义aSRC_Const_Buffer数组作为DMA传输数据源
* const关键字将aSRC_Const_Buffer数组变量定义为常量类型
* 表示数据存储在内部的FLASH中
*/
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]=
{
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80
};
/* 定义DMA传输目标存储器
* 存储在内部的SRAM中
*/
uint32_t aDST_Buffer[BUFFER_SIZE];
使用宏定义设置外设配置方便程序修改和升级。
存储器到存储器传输通道没有硬性规定,可以随意选择。
aSRC_Const_Buffer[BUFFER_SIZE]定义用来存放源数据,并且使用了const关键字修饰,即常量类型,使得变量是存储在内部flash空间上。
DMA数据配置
代码清单 22‑2 DMA传输参数配置
void DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;// 开启DMA时钟
RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE);// 源数据地址
DMA_InitStructure.DMA_PeripheralBaseAddr =
(uint32_t)aSRC_Const_Buffer;
// 目标地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aDST_Buffer;
// 方向:外设到存储器(这里的外设是内部的FLASH)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
// 传输大小
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
// 外设(内部的FLASH)地址递增
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
// 内存地址递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 外设数据单位
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_Word;
// 内存数据单位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
// DMA模式,一次或者循环模式
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
// 优先级:高
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
// 使能内存到内存的传输
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;// 配置DMA通道
DMA_Init(DMA_CHANNEL, &DMA_InitStructure);
// 使能DMA
DMA_Cmd(DMA_CHANNEL,ENABLE); //启动或者停止DMA数据传输
}
使用DMA_InitTypeDef结构体定义一个DMA初始化变量,这个结构体内容我们之前已经有详细讲解。
调用RCC_AHBPeriphClockCmd函数开启DMA时钟,使用DMA控制器之前必须开启对应的时钟。
源地址和目标地址使用之前定义的数组首地址,传输的数据量为宏BUFFER_SIZE决定,源和目标地址指针地址递增, 使用一次传输模式不能循环传输,因为只有一个DMA通道,优先级随便设置,最后调用DMA_Init函数完成DMA的初始化配置。
DMA_ClearFlag函数用于清除DMA标志位,代码用到传输完成标志位,使用之前先清除传输完成标志位以免产生不必要干扰。 DMA_ClearFlag函数需要1个形参,即事件标志位,可选有传输完成标志位、半传输标志位、FIFO错误标志位、传输错误标志位等等, 非常多,我们这里选择传输完成标志位,由宏DMA_FLAG_TC定义。
DMA_Cmd函数用于启动或者停止DMA数据传输,它接收两个参数,第一个是DMA通道,另外一个是开启ENABLE或者停止DISABLE。
存储器数据对比
代码清单 22‑3 源数据与目标地址数据对比
uint8_t Buffercmp(const uint32_t* pBuffer,
uint32_t* pBuffer1, uint16_t BufferLength)
{
/* 数据长度递减 */
while (BufferLength--) {
/* 判断两个数据源是否对应相等 */
if (*pBuffer != *pBuffer1) {
/* 对应数据源不相等马上退出函数,并返回0 */
return 0;
}
/* 递增两个数据源的地址指针 */
pBuffer++;
pBuffer1++;
}
/* 完成判断并且对应数据相对 */
return 1;
}
判断指定长度的两个数据源是否完全相等,如果完全相等返回1;只要其中一对数据不相等返回0。 它需要三个形参,前两个是两个数据源的地址,第三个是要比较数据长度。
主函数
代码清单 22‑4 存储器到存储器模式主函数
int main(void)
{
/* 定义存放比较结果变量 */
uint8_t TransferStatus;/* LED 端口初始化 */
LED_GPIO_Config();/* 设置RGB彩色灯为紫色 */
LED_PURPLE;/* 简单延时函数 */
Delay(0xFFFFFF);/* DMA传输配置 */
DMA_Config();/* 等待DMA传输完成 */
while (DMA_GetFlagStatus(DMA_FLAG_TC)==RESET)
{}
/* 比较源数据与传输后数据 */
TransferStatus=Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);/* 判断源数据与传输后数据比较结果*/
if (TransferStatus==0)
{
/* 源数据与传输后数据不相等时RGB彩色灯显示红色 */
LED_RED;
}
else
{
/* 源数据与传输后数据相等时RGB彩色灯显示蓝色 */
LED_BLUE;
}while (1)
{
}
}
首先定义一个变量用来保存存储器数据比较结果。
RGB彩色灯用来指示程序进程,使用之前需要初始化它,LED_GPIO_Config定义在bsp_led.c文件中。开始设置RGB彩色灯为紫色, LED_PURPLE是定义在bsp_led.h文件的一个宏定义。
Delay函数只是一个简单的延时函数。
调用DMA_Config函数完成DMA数据流配置并启动DMA数据传输。
DMA_GetFlagStatus函数获取DMA事件标志位的当前状态,这里获取DMA数据传输完成这个标志位,使用循环持续等待直到该标志位被置位, 即DMA传输完成这个事件发生,然后退出循环,运行之后程序。
确定DMA传输完成之后就可以调用Buffercmp函数比较源数据与DMA传输后目标地址的数据是否一一对应。TransferStatus保存比较结果, 如果为1表示两个数据源一一对应相等说明DMA传输成功;相反,如果为0表示两个数据源数据存在不等情况,说明DMA传输出错。
如果DMA传输成功设置RGB彩色灯为蓝色,如果DMA传输出错设置RGB彩色灯为红色。
4.3. 下载验证
确保开发板供电正常,编译程序并下载。观察RGB彩色灯变化情况。正常情况下RGB彩色灯先为紫色,然后变成蓝色。如果DMA传输出错才会为红色。
5. DMA存储器到外设模式实验
将内存中的数据发到串口助手工具,DMA只需传送数据到开发板的串口发送模块,开发板串口再发给串口助手工具,硬件自动实现
上个实验我们讲了DMA存储器到存储器模式,接下来我们再讲一个存储器到外设的实验。我们先定义一个数据变量, 存于SRAM中,然后通过DMA的方式传输到串口的数据寄存器,然后通过串口把这些数据发送到电脑的上位机显示出来。
5.1. 硬件设计
存储器到外设模式使用到USART1功能,具体电路设置参考USART章节,无需其他硬件设计。
5.2. 软件设计
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们编写两个串口驱动文件bsp_usart_dma.c和bsp_usart_dma.h,有关串口和DMA的宏定义以及驱动函数都在里边。
5.2.1. 编程要点
配置USART通信功能;
设置串口DMA工作参数;
使能DMA;
DMA传输同时CPU可以运行其他任务。
5.2.2. 代码分析
将内存中的数据发到串口助手工具,DMA只需传送数据到开发板的串口发送模块,开发板串口再发给串口助手工具,硬件自动实现,所以代码里是内存与开发板串口发送引脚之间传输的。
USART和DMA宏定义
代码清单 22‑5 USART和DMA相关宏定义
// 串口工作参数宏定义
#define DEBUG_USARTx USART1
#define DEBUG_USART_CLK RCC_APB2Periph_USART1
#define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_BAUDRATE 115200// USART GPIO 引脚宏定义
#define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA)
#define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10// 串口对应的DMA请求通道
#define USART_TX_DMA_CHANNEL DMA1_Channel4
// 外设寄存器地址
#define USART_DR_ADDRESS (USART1_BASE+0x04)
// 一次发送的数据量
#define SENDBUFF_SIZE 5000
使用宏定义设置外设配置方便程序修改和升级。
USART部分设置与USART章节内容相同,可以参考USART章节内容理解。串口的发送请求对应有固定的DMA通道, 这里的外设的地址即串口的数据寄存器,一次要发送的数据量可以自定义,配置SENDBUFF_SIZE这个宏即可。
串口DMA传输配置
代码清单 22‑6 USART1 发送请求DMA设置
void USARTx_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;// 开启DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 设置DMA源地址:串口数据寄存器地址*/
DMA_InitStructure.DMA_PeripheralBaseAddr = USART_DR_ADDRESS;
// 内存地址(要传输的变量的指针)
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;
// 方向:从内存到外设
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
// 传输大小
DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;
// 外设地址不增
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// 内存地址自增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 外设数据单位
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_Byte;
// 内存数据单位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
// DMA模式,一次模式或者循环模式
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
// 优先级:中
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
// 禁止内存到内存的传输
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
// 配置DMA通道
DMA_Init(USART_TX_DMA_CHANNEL, &DMA_InitStructure);
// 使能DMA
DMA_Cmd (USART_TX_DMA_CHANNEL,ENABLE);
}
首先定义一个DMA初始化变量,用来填充DMA的参数,然后使能DMA时钟。
因为数据是从存储器到串口,所以设置存储器为源地址,串口的数据寄存器为目标地址,要发送的数据有很多且都先存储在存储器中, 则存储器地址指针递增,串口数据寄存器只有一个,则外设地址地址不变,两边数据单位设置成一致,传输模式可选一次或者循环传输, 只有一个DMA请求,优先级随便设,最后调用DMA_Init函数把这些参数写到DMA的寄存器中,然后使能DMA开始传输.
主函数
代码清单 22‑7 存储器到外设模式主函数
int main(void)
{
uint16_t i;
/* 初始化USART */
USART_Config();/* 配置使用DMA模式 */
USARTx_DMA_Config();/* 配置RGB彩色灯 */
LED_GPIO_Config();printf("\r\n USART1 DMA TX 测试 \r\n");
/*填充将要发送的数据*/
for (i=0; i<SENDBUFF_SIZE; i++)
{
SendBuff[i] = 'P';
}/*为演示DMA持续运行而CPU还能处理其它事情,持续使用DMA发送数据,量非常大,
*长时间运行可能会导致电脑端串口调试助手会卡死,鼠标乱飞的情况,
*或把DMA配置中的循环模式改为单次模式*//* USART1 向 DMA发出TX请求 */
USART_DMACmd(USARTx, USART_DMAReq_Tx, ENABLE);/* 此时CPU是空闲的,可以干其他的事情 */
//例如同时控制LED
while (1)
{
LED1_TOGGLE
Delay(0xFFFFF);
}
}
USART_Config函数定义在bsp_usart_dma.c中,它完成USART初始化配置,包括GPIO初始化,USART通信参数设置等等,具体可参考USART章节讲解。
USARTx_DMA_Config函数也是定义在bsp_usart_dma.c中,之前我们已详细分析。
LED_GPIO_Config函数定义在bsp_led.c中,它完成RGB彩色灯初始化配置,具体可参考GPIO章节讲解。
使用for循环填充源数据,SendBuff[SENDBUFF_SIZE]是定义在bsp_usart_dma.c中的一个全局无符号8位整数数组,是DMA传输的源数据, 在USART_DMA_Config函数中已经被设置为存储器地址。
USART_DMACmd函数用于控制USART的DMA请求的启动和关闭。它接收三个参数,第一个参数用于设置串口外设,可以是USART1/2/3和UART4/5这5个参数可选, 第二个参数设置串口的具体DMA请求,有串口发送请求USART_DMAReq_Tx和接收请求USART_DMAReq_Rx可选, 第三个参数用于设置启动请求ENABLE或者关闭请求DISABLE。运行该函数后USART的DMA发送传输就开始了,根据配置存储器的数据会发送到串口。
DMA传输过程是不占用CPU资源的,可以一边传输一次运行其他任务。
5.3. 下载验证
保证开发板相关硬件连接正确,用USB线连接开发板的USB转串口和电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。 程序运行后在串口调试助手可接收到大量的数据,同时开发板上RGB彩色灯不断闪烁。
这里要注意为演示DMA持续运行并且CPU还能处理其它事情,持续使用DMA发送数据,量非常大,长时间运行可能会导致电脑端串口调试助手会卡死, 鼠标乱飞的情况,所以在测试时最好把串口调试助手的自动清除接收区数据功能勾选上或把DMA配置中的循环模式改为单次模式。
参考:
野火和江科大的教程