STM32F103(二十一)DMA(超详细的~)
Posted 独独白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32F103(二十一)DMA(超详细的~)相关的知识,希望对你有一定的参考价值。
学习板:STM32F103ZET6
一、DMA
1、简介
DMA,全称为:Direct Memory Access,即直接存储器访问,DMA 传输将数据从一个地址空间复制到另外一个地址空间。这个从一个地址空间到另外一个地址空间可以是:外设到存储器、存储器到存储器、存储器到外设。
通过DMA,实现一个地址空间到另一个地址空间的高速数据传输,无需CPU控制传输,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,大大提高了CPU的效率。
2、STM32的DMA数量
STM32有两个DMA控制器,其中DMA1有7个通道;DMA2有5个通道,且DMA2只存在于大容量32中。
DMA1的通道:
DMA2的通道:
二、DMA相关寄存器
1、DMA中断状态寄存器(DMA_ISR)
该寄存器分别控制1~7DMA通道,其中DMA2只有五个通道,此时位20 ~位27无效。
寄存器操作:
DMA1->ISR=
DMA2->ISR=
详细总结一下位0~位3,其它的类似。
位0:
中断标志位,即在通道x产生了TE、HT或TC事件时该位硬件置1。
TE:传输错误中断事件
HT:传输过半中断事件
TC:传输完成中断事件
如使能HT中断后,如果传输过半,则会发生HT事件,本寄存器位0(通道1)或位4(通道2)或位8(通道3)或位12(通道4)或位16(通道5)或位20(通道6)或位24(通道7)硬件置1,表示发生中断事件。
位1~位3:
位1:传输完成标志
位2:半传输标志
位3:传输错误标志
这3位是传输状态标志位,如传输完成,则位1置1(通道1而言,其他通道类似);传输过半,则位2硬件置1;传输错误则位3硬件置1.
这些位都需要软件清零的。操作是:在IFCR寄存器对应位写1,清除ISR寄存器的状态标志。
2、DMA中断标志清除寄存器(DMA_IFCR)
该寄存器也是被分为1~7通道配置。与前面的ISR寄存器一一对应。
以通道1为例,说明。其他通道类似。
位0:清除GIF全局中断标志:TEIF、HTIF和TCIF
位1:清除传输完成标志
位2:清除传输过半标志
位3:清除传输错误标志
3、DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)+寄存器使用代码
位0:
软件置1开启通道。
该寄存器怎么使用呢?
可以看到前面俩个寄存器在DMA_TypeDef
结构体中
可以看到DMA1、DMA2被定义为DMA_TypeDef
类型,故之前总结的俩个寄存器使用方法:
DMA1->ISR=
DMA2->ISR=
DMA1->IFCR=
DMA2->IFCR=
所以CCR寄存器的用法:
DMA1_Channel1->CCR|=1;//开启DMA1通道1
位1~位3:
位1允许传输完成中断、位2允许半传输中断、位3允许传输错误中断。
代码:
DMA1_Channel1->CCR|=1<<1;//允许传输完成中断
DMA1_Channel1->CCR|=1<<2;//允许半传输中断
DMA1_Channel1->CCR|=1<<2;//允许传输错误中断
// DMA1_Channel1->CCR|=0x111<<1;//同时设置
位4:
设置传输方向,是从外设传输到储存器还是从储存器传输到外设(储存器传输到储存器需另外设置)。
位5:
设置循环模式
在设置DMA时需要设置可编程数据量,即需要进行多少次传输,存储在CNTTRx寄存器中,每进行一次数据传输,该寄存器减1 。当递减到0时说明所有数据都传输完成。如果没有设置循环模式,此时会终止数据传输;如果设置了循环模式,CNTTRx寄存器会自动重装载之前设置的初值,重新开始传送数据,重新递减到0,这样循环传输数据。
位6~位7:
设置外设地址增量和储存器地址增量。
首先每次数据传输都要明确源地址和目标地址,外设地址储存在CPARx寄存器中,储存器地址储存在CMARx寄存器中。
如果没有地址增量,则固定地址的储存空间数据进行传输。对于源地址,如果地址空间的数据不更新,则每次传送数据是一致的;对于目标地址,每次传输的数据会覆盖上一次传输的数据。
如果只设置了源地址增量,则每传输一次数据,源地址会自增一次,而目标地址不变,所以传输的数据会在目标地址中一直被覆盖。
如果源地址和目标地址都设置了增量,每传输一次数据,源地址和目标地址都会自增。
位8~位11:
设置数据宽度,即设置源地址储存数据的数据宽度和目标地址储存数据的数据宽度。一般情况下俩个数据宽度保持一致就好。当然可以源地址32位、目标地址8位这种设置也可以,具体操作可以参考中文手册:
位13~位12:
DMA中有一个仲裁器用来判断个通道优先级的。首先DMA1优先级高于DMA2;然后就是位13:12设置的优先级了,高优先级先执行;最后就是通道编号了,如果两个通道的位13:12设置的通道优先级一样,则通道编号小的优先级高。
如:
DMA1通道1优先级:中
DMA1通道2优先级:中
DMA1通道3优先级:高
DMA2通道1优先级:中
DMA2通道2优先级:中
DMA2通道3优先级:高
优先级顺序:DMA1通道3 > MA1通道1 > MA1通道2 > DMA2通道3 > DMA2通道1 > DMA2通道2
位14:
设置从储存器到储存器模式
4、DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)
设置传输所有数据需要多少次。之前CCRx寄存器中总结循环模式时总结过。
5、DMA通道x外设地址寄存器(DMA_CPARx)(x = 1…7)
储存外设地址
6、DMA通道x存储器地址寄存器(DMA_CMARx)(x = 1…7)
储存储存器的地址
三、DMA相关库函数
1、取消初始化函数DMA_DeInit()
参数: DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5
操作:
①关闭通道
②CCR寄存器每一位都置0
③CNDTR寄存器置0,传输数设置为0
④CPAR、CMAR寄存器清零,清空源地址和目标地址
⑤重置通道的三大中断(完成、过半、错误中断),即清除中断标志位
2、DMA初始化函数DMA_Init()
参数:
参数1:
DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5
参数2:DMA_InitTypeDef* DMA_InitStruct
详细总结一下这个结构体:
①DMA_PeripheralBaseAddr
外设基地址
②DMA_MemoryBaseAddr
储存器基地址
③DMA_DIR
方向,是从外设到储存器还是存储器到外设
选择时:
DMA_DIR_PeripheralDST
外设作为目标,即从储存器到外设
DMA_DIR_PeripheralSRC
外设作为源,即从外设到储存器
如果方向为外设到储存器,则DMA_PeripheralBaseAddr
外设基地址为源地址,DMA_MemoryBaseAddr
储存器基地址为目标地址;如果方向为存储器到外设,则DMA_PeripheralBaseAddr
外设基地址为目标地址地址,DMA_MemoryBaseAddr
储存器基地址为源地址;
④DMA_BufferSize
通道的缓冲区大小,最大为35535 表示传输多少次
⑤DMA_PeripheralInc
外设基地址是否递增
⑥DMA_MemoryInc
储存器基地址是否递增
⑦DMA_PeripheralDataSize
外设数据的大小。有字节(8位)、半字(16位)、字(32位)
⑧DMA_MemoryDataSize
储存器储存数据大小。有字节(8位)、半字(16位)、字(32位)
⑨DMA_Mode
设置模式,是否为循环模式
⑩DMA_Priority
设置通道优先级,分为最高、高、中、低四个等级
⑪DMA_M2M
设置是否为储存器到储存器
3、结构体初始化函数DMA_StructInit()
参数:
参数为DMA_Init()函数传递的那个结构体
操作:
①DMA_PeripheralBaseAddr
外设基地址设置为0
②DMA_MemoryBaseAddr
储存器基地址设置为0
③DMA_DIR
方向,设置为从外设到储存器
④DMA_BufferSize
通道的缓冲区大小,设置为0
⑤DMA_PeripheralInc
外设基地址是否递增,设置为不递增
⑥DMA_MemoryInc
储存器基地址是否递增,设置为不递增
⑦DMA_PeripheralDataSize
外设数据的大小。有字节(8位)、半字(16位)、字(32位),设置为字节(8位)
⑧DMA_MemoryDataSize
储存器储存数据大小。有字节(8位)、半字(16位)、字(32位),设置为字节(8位)
⑨DMA_Mode
设置模式,是否为循环模式,设置为不循环
⑩DMA_Priority
设置通道优先级,分为最高、高、中、低四个等级,设置为低优先级
⑪DMA_M2M
设置是否为储存器到储存器,设置为不启用
4、DMA使能函数DMA_Cmd( )
参数:
参数1:DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5
参数2:DISABLE、ENABLE
操作:
对CCR寄存器位0操作
5、DMA中断配置函数DMA_ITConfig()
参数:
参数1:DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5
参数2:DMA_IT
表示哪种中断:
DMA_IT_TC //传输完成中断
DMA_IT_HT //传输过半中断
DMA_IT_TE //传输错误中断
参数3:DISABLE、ENABLE
6、设置传输数据量函数DMA_SetCurrDataCounter()
参数:
参数1:DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5
参数2:0~0xffff之间的数,最多为65535,即循环一次(如果设置了循环模式)传输次数最多为65535
操作:
对CNDTR寄存器写操作
7、获取剩余传输次数函数DMA_GetCurrDataCounter()
参数:
参数1:DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5
操作:
对CNDTR寄存器读操作
由于CNDTR寄存器装载初值后,每传输一次会自减一次,故返回的CNDTR的值是现在剩余的数,即还可以传输多少次。
8、获取中断状态函数DMA_GetFlagStatus()
参数:
DMA1和DMA2各通道的全局中断(即完成、过半、错误任意几个中断都行)、完成中断、过半中断、错误中断。
返回:
返回SET表示发生参数类型的中断,返回RESET表示未发生中断。
9、清除中断标志函数DMA_ClearFlag()
参数:
操作:
对IFCR对应位写1清除ISR寄存器对应位。
10、获取中断状态函数DMA_GetITStatus()
与DMA_GetFlagStatus()
函数基本一样
11、获取中断状态函数DMA_ClearITPendingBit()
与DMA_ClearFlag
基本一样
四、DMA的编程顺序
1、使能DMA时钟
使用RCC_AHBPeriphClockCmd()函数
2、初始化 DMA
使用DMA_Init()函数
3、使能外设
4、使能DMA
5、状态查询
使用DMA_GetFlagStatus()查询中断
使用DMA_GetCurrDataCounter()查询还剩余多少带传输数据
五、例题
1、题
利用板子上的ADC采集光敏传感器的电压等效值(即将0~3.3V的电压值映射到0 ~ 4095的这个u16数字),利用DMA储存在一个u16数组中。存储完成后,在串口中输出数组中的值。
2、分析
ADC采集光敏传感器的实验之前博客ADC相关的几个实验—内部温度传感器、内部参照电压、光敏电阻总结过,这部分不在详述。
将数据传输到数组中,所以ADC需要使能DMA。并且之前总结光敏传感器时说到过,本板的光敏传感器连接ADC3通道6,而根据本博第一部分:STM32的DMA数量这一目中的表格可得ADC3用到DMA2通道5 。
3、代码
ADC采集光敏传感器代码的代码上一博客总结过了,唯一不同的是需要使能ADC的DMA,ADC的DMA使能在主函数中进行。
直接附ADC采集光敏传感器代码:
adc.h代码:
#ifndef _ADC_
#define _ADC_
#include "stm32f10x.h"
void adc_Init(void);
#endif
adc.c代码:
#include "adc.h"
#include "stm32f10x.h"
void adc_Init(void)
{
ADC_InitTypeDef ADC_InitStruct;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3|RCC_APB2Periph_GPIOF, ENABLE);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOF, &GPIO_InitStructure);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_DeInit(ADC3);
ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
ADC_InitStruct.ADC_NbrOfChannel=6;
ADC_InitStruct.ADC_ScanConvMode=DISABLE;
ADC_Init(ADC3, &ADC_InitStruct);
ADC_Cmd(ADC3,ENABLE);
ADC_ResetCalibration(ADC3);
while(ADC_GetResetCalibrationStatus(ADC3)) //等待复位校准结束
{
}
ADC_StartCalibration(ADC3);
while(ADC_GetCalibrationStatus(ADC3))//等待校准结束
{
}
ADC_RegularChannelConfig(ADC3, ADC_Channel_6, 1, ADC_SampleTime_28Cycles5);//规则通道设置
}
dma.h代码:
#ifndef _DMA_
#define _DMA_
void dma_Init();
#endif
在DMA中,外设作为源,数组作为目标,外设是ADC采集到的数据,存储在ADC3的DR寄存器中,该寄存器在单ADC模式下是低16位有效,故外设数据大小为16位,外设地址就是(u32)&ADC3->DR 。储存器为数组,可以为16位数组,地址为数组名。
考虑到要知道是否传输完成,故需要设置传输完成中断。
dma.c代码如下:
#include "dma.h"
#include "stm32f10x.h"
extern u16 arr[200];
void dma_Init()
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2,ENABLE);
DMA_InitStructure.DMA_BufferSize=200;
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr=(u32)arr;
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;
DMA_InitStructure.DMA_PeripheralBaseAddr=(u32)&ADC3->DR;
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_Priority=DMA_Priority_High;
DMA_Init(DMA2_Channel5,&DMA_InitStructure);
DMA_ITConfig(DMA2_Channel5,DMA_IT_TC,ENABLE);
}
main.c代码:
#include "stm32f10x.h"
#include "adc.h"
#include "usart.h"
#include "dma.h"
u16 arr[200];
u16 i=0;
int main(void)
{
adc_Init();
dma_Init();
uart_init(115200);
while(1)
{
ADC_SoftwareStartConvCmd(ADC3,ENABLE);//软件触发、并开启转换ADC
while(!ADC_GetFlagStatus(ADC3,ADC_FLAG_EOC))
{
//转换过程中执行空语句,等待转换完成
}
ADC_DMACmd(ADC3,ENABLE);
DMA_Cmd(DMA2_Channel5,ENABLE);
DMA_Cmd(DMA2_Channel5,DISABLE);
if((DMA_GetITStatus(DMA2_IT_TC5)!=RESET))
{
DMA_ClearFlag(DMA2_FLAG_TC5);
break;
}
}
for(i=0;i<200;i++)
printf("a[%d]=%d\\r\\n",i,arr[i]);
while(1)
{
}
}
主函数的第一个while语句中,使能软件触发ADC,使ADC开始转换并等待转换完成。
接下来三句代码很重要,首先使能ADC的DMA、DMA使能;然后失能DMA,这里必须使能再失能,否则无法传输。
当全部完成,发生中断,跳出第一个while语句,并输出数组值。
4、效果
实验室光照下:
手电筒照亮光敏传感器:
六、强调几点
1、DMA中断配置函数DMA_ITConfig()
注意它的有效性说明 ,参数应该是上面的三个,而不是下面的这堆!
2、DMA清除中断函数:DMA_ClearFlag()和DMA_ClearITPendingBit()
注意参数:
虽然根据名字能看出是DMA几的通道几的哪个中断类型,而且IT和FLAG的值是相同的,但是不要混用,即DMA_ClearFlag()用DMA1_FLAG_TC1;DMA_ClearITPendingBit()函数用DMA1_IT_TC1;
3、DMA_BufferSize
DMA_BufferSize在DMA_Init()函数中,打开函数体:
可见它是对CNDTR寄存器的操作,即DMA_BufferSize表示传输多少次 。
4、传输时源、目标的字节大小
字节大小尽量与有效位数保持一致,有时候需要单片机自动类型转换,如32位转16位,一定要保证转换时数据不丢失。
如本例中ADC采集的DR寄存器为32位寄存器,但是只有低16位有效,故可以把“源”或者说外设的数据大小设置为DMA_PeripheralDataSize_HalfWord
16位的数。
储存器数据大小要设置对,如本例中,传递过来的数是16位的数,也把存储器数据大小设置为16位:DMA_MemoryDataSize_HalfWord
,但是数组改为u32位,就会出错,因为设置与实际存储情况不符合。把数组改为u32后:
出现上述情况的原因是u32位的数组储存区域,每一个地址单元会储存2次传递的数值,导致a[100]~a[199]没有值。
把储存器的类型也改为32位:
发现又好了:
总而言之,端到端,每一端各自设置的数据大小要与实际寄存器、存储器数据相同。可以一端是16位,另一端是32位,但必须保证16位这一端的寄存器或存储器的实际类型就是16位;32位这一端的寄存器或储存器实际类型就是32位。
5、使能与失能DMA
因为多次传输时DMA不能传输一次就中断,可以理解为每使能一次DMA,传输一次,失能后再使能,循环下去,完成DMA多次传输。
以上是关于STM32F103(二十一)DMA(超详细的~)的主要内容,如果未能解决你的问题,请参考以下文章
STM32F103(二十七)超长篇解读STM32访问外部flash
STM32F103(二十七)超长篇解读STM32访问外部flash
STM32F103(二十七)超长篇解读STM32访问外部flash