基于stm32f103的使用如果是f4或者h7之类的也是可以看看原理的。
这篇文章完成三个实验:
1 通过使用DMA完成存储器到存储器之间的数据移动
2 通过DMA转运ADC单次触发扫描模式转换后的数据
3 通过DMA转移ADC连续触发扫描模式转换后的数据
如果不想看的可以直接使用git把我的代码下载出来,里面工程挺全的,后期会慢慢的补注释之类的
如果不会使用git快速下载可以选择直接下载压缩包或者去看看git的使用
git的使用(下载及上传_gitcode怎么下载文件_是小刘不是刘的博客-CSDN博客
目录
一 理论知识
1 DMA描述
DMA就是一个数据搬运工,它不需要CPU的干预就能自动帮你搬运数据,也就不会占用你的时间,如果有很多数据要搬运,DMA就能直接帮你般,但是不用DMA就会CPU自己搬就好有很长的阻塞时间。
stm32有两个DMAstm32有两个DMA,每个外设都有单独的DMA通道,但是一个DMA下所有通道又是同用一个传输通道,所以并不能同时传输也是分先后的。但是也得分芯片型号,并不是所有型号都有两个DMA通道的好吧。
2 存储器映像
存储器的映射这里映射了32位的地址,总共是4G的地址,STM32内存一般才几十K,几百K,所以肯定能存进去,但是这也造成了大部分的地址都没存东西,这里看个大概位置就行。
3 DMA框图
这个其实算是整体的时钟图,但是也能很好的看出DMA的运行,下面分开讲
1 )首先是两个DMA
这里明确说明DMA有两个并且,DMA1有7通道,2有5个通道,然后还有个仲裁的存在,就是上面说过的,其实一个DMA共用一个传输通道,所以需要仲裁他们到底谁传。
2 )主动单元与被动单元
这里将上面这部分分别划分为了1234 其中1 2 3为主动单元,享有访问权和控制权4为被动单元,只能被访问。(首先是1 Dcode这个设备是专门用来访问Flash的 2为系统总线访问各个外设 3为DMA了,因为DMA要在各个存储器之间搬运数据,所以他也享有主动权。
3)DMA挂在在哪里
既然是个外设,那应该就有相应的寄存器,可以让处理器来操作他,可以看出来两个DMA都是直接挂在在了AHB总线上的,并且还有个以太网外设(因为以太网外设的DMA是单独的,所以并不算在我们这次要讲的DMA里面,但是他有自己的一条DMA通道。
4)DMA请求
DMA的开始指令有软件控制和硬件控制两种,其中如果只是存储器搬运到存储器那么可以软件控制,但是如果是存储器和外设,那么我们就要判断外设的数据是否处理完成,外设的处理完成之后我们才能去搬移他的数据。
4 DMA逻辑图
(江科大的PPT)
1 传输方向
可以从图上看出有三种模式
1) 外设寄存器到FLASH和SRAM(存储器
2) 存储器到外设寄存器
3 )存储器到存储器
2 外设寄存器可配置的参数
外设寄存器和存储器寄存器都有三个可以配置的模式:1 起始地址 2数据宽度(这个可以配置3个,8位(字节) 16位(半字) 32位(字) 3地址是否自增(就传输完成一个,地址+1 就和指针是差不多的
3 传输计数器和重载器
传输计数器:每传输一次计数器就会减1 自动重载,归0后是否重载计数器
4 选择器
M2M给1:软件触发 M2M给0:硬件触发
可以选择软件触发或者硬件触发:但是这个软件触发就是连续触发,一直触发将计数器剪到0,所以这个东西不能和自动重载一起开,不然会一直减,和硬件不一样,硬件是触发一次减一次。
5 DMA请求映像
这是DMA1的内部映像
1 是通道1的EN位 这个位控制了通道1的开关
2 是M2M位 如果M2M位1则为软件触发
在图中还有触发位的选择项。可以看看这个图,这个图看着比上面的舒服一点。
6 数据传输
这里是前面说的数据传输:说了有8 16 32位
如果源端数据宽度为8 目标数据宽度为8 传输数目为4 写B0到目标 收到的就为B0
如果是8位到16位 收到的就会是00B0.16到8就会只有B0(很明显,接收到的数据是以接收方的宽度来决定的)
二 代码部分
1 存储器传到存储器
1)存储地址测试
首先看一下前面说的存储器地址(首先你要知道一般的数据也就是变量会存在运行内存中,如果是不需要改变变量(静态常量)和函数及代码会存在程序存储器FLASH里面我们可以测试一下先,虽然后面我们是直接取地址,不需要知道数据的地址,但是可以先了解一下。
测试一下啊,定一个变量和常量测试一下地址
代码如下
const int num1=1;
int main(void)
{
int num=1;
Usart_Config();
TIM6_Init();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
while(1)
{
if(time==1000)
{
printf("num=%p num1=%p\r\n",&num,&num1);
time=0;
}
}
}
定义了一个常量num1 和一个变量num,我们打印他们的地址,可以看出num是在2000开头的RAM区,常量则是在0800开头的FLASH,和刚刚给出的图一致,一般flash都会比ram大很多倍,所以不需要改变的数据可以存到flash去,节省ram'的空间
2)DMA传输框图
通过DMA将一个数据传输到另外一个地方,每次地址给他配置为自加,传输一位DMA的计数器就会自减一次,所以可以根据自己需要传输的数据大致算一下位数。
3)DMA单次传输代码
首先先把初始化代码放在这
void MyDMA_Init(uint16_t size,uint32_t MAddr,uint32_t PAddr)
{
DMA_InitTypeDef DMA_InitStruct;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitStruct.DMA_BufferSize=size;
DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_M2M=DMA_M2M_Enable;
DMA_InitStruct.DMA_Mode=DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority=DMA_Priority_High;
DMA_InitStruct.DMA_MemoryBaseAddr=MAddr;
DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralBaseAddr=PAddr;
DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Enable;
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
DMA_Cmd(DMA1_Channel1,ENABLE);
}
1 首先还是熟悉的写一个初始化,首先还是声明一个结构体变量,声明到第一行
然后开启DMA1的时钟,前面说过DMA是挂载在AHB总线上的,1为互联型芯片的参数 2为其余的芯片 我们用的不是互联型的,所以这里去沾2 里面的DMA1
之后是往刚刚声明的结构体对象里面写参数
这里看着参数很多,其实可以对照前面说的流程,
2 DMA_BufferSize
配置传输计数器的大小BUFFerSize 说了是传一次减一个,所以这里你传几个数据你就写几,这里我们将参数作为了形参传进来,后期好调用。
3 DMA_DIR
配置方向DIR 前面说过需要配置是存储器到存储器还是存储器到外设,或者外设到存储器,这个参数的意思是,指定外设地址是要传输的源或者目标,这里有两个参数可以选,一个是是后缀SRC(soure源头)一个是DST(destination目的地),我这里将方向配置为了存储器到外设
3 DMA_M2M
这个说过是配置我们是软件触发或者硬件触发 这里选择enable就是软件触发了,前面说过选1就是软件触发嘛,这里看一看一下位,很明显enble是填入的1
4 DMA_Mode
模式,其实是配置前面所说的,重载器的模式,我们可以配置为重载或者不重载
因为是软件触发所以我们不能开重载(前面说了,这个东西和软件触发效果冲突,一个想尽快减完一个又想减完让你重新减)我们这里就选第二个正常模式了
5 DMA_Priority
优先级 :前面说过DMA有仲裁器,会判断优先级的,这里就是就算通道号在后面,你也可以给他高优先级
6 DMA_MemoryBaseAddr&DMA_PeripheralBaseAddr
存储器和外设地址:这里写形参的名字,方便等会传参进来
7 DMA_MemoryDataSize&DMA_PeripheralDataSize
这个是刚刚说的,数据传输的那点了,配置双方的字节大小,可以配置为8位16位32位,我配置为了8位
8 DMA_PeripheralInc&DMA_MemoryInc
是否自增,就是传输完一个数据后,地址会不会自己+1
这里我们配置为开启
9 初始化结构体和使能DMA
将刚刚配置的参数,通过Init函数写入到DMA的配置中,之后开启cmd开启MDA传输数据,,这里随便选DMA通道都行,因为是软件触发,所以每个通道都能支持。
10 主函数
uint8_t Data1[]={0x01,0x02,0x03,0x14};
uint8_t Data2[]={0,0,0,0};
int main(void)
{
Usart_Config();
MyDMA_Init(4,(uint32_t)Data2,(uint32_t)Data1);
printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data1[0],Data1[1],Data1[2],Data1[3]);
printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data2[0],Data2[1],Data2[2],Data2[3]);
while(1)
{
}
}
声明两个数组,Data1里面有数据,2里面没有数据
调用MyDMA_Init()里面写入参数,第一个为传输个数,第二个是存储器,第三个是外设,因为我们前面配置的为存储器到外设。
调用之后我们打印一下数组内容。
结果(很明显看出,即使这里是吧data1的数据搬移到了data2但是1里面数据依然在,所以说是数据搬移,其实是数据复制了)
4)DMA循环转移代码
首先我们上面说过,软件触发是不能配合重载的,所以转移一次之后就会停止,所以下一次就算数据变动了,他也不会管,这里我们就写个函数,手动来对他进行使能和计数器赋值。
代码如下
1 添加函数
void MyMDA_Transfer(void)
{
/*重置计数值*/
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,MyMDA_Size);
DMA_Cmd(DMA1_Channel1,ENABLE);
/*等待传输完成*/
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
2 改Init函数
添加全局变量MyMDA_size 方便将传入的形参保存,后面好重新给计数器赋值
之后失能DMA只进行初始化,后面我们手动使能
函数分析
重置计数器部分,这里前面说过,DMA在开启的时候是不能更改计数器的值的,所以我们先失能之后更改完值重新使能
等待传输完成部分:等待传输完成的标志位为1退出循环
这里一般看有没有标志位主要是看参考手册:这里说了标志位名字和是否需要手动清除,他说在ifcr的相应标志写1就能清楚
然后看了下中文参考手册,应该是翻译错了,因为的里面是1为清除
之后我们写主函数
volatile uint32_t time = 0; // ms 计时变量
uint8_t Data1[]={0x01,0x02,0x03,0x14};
uint8_t Data2[]={0,0,0,0};
int main(void)
{
Usart_Config();
TIM6_Init();
MyDMA_Init(4,(uint32_t)Data2,(uint32_t)Data1);
MyMDA_Transfer();
while(1)
{
if(time==1000)
{
Data1[0]++;
Data1[1]++;
Data1[2]++;
Data1[3]++;
MyMDA_Transfer();
printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data1[0],Data1[1],Data1[2],Data1[3]);
printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data2[0],Data2[1],Data2[2],Data2[3]);
time=0;
}
}
}
这里用了一个基本定时器以前写过,不知道的朋友们可以先去看一下。
stm32f103基本定时器的使用_stm32f103定时器时钟_是小刘不是刘的博客-CSDN博客
逻辑就是将数组1的值每秒+1,然后给数组2赋值,这样循环改变,结束之后重置定时器
效果如下:从这也可以看出来,处理这个数据是花了多少时间,1000进入定时器,这段时间只做了time里面的东西,所以处理一次大致是0.003秒的样子,结果也是符合的。
2 ADC扫描模式+DMA
这个是之前写过的一个ADC采集的后续了stm32f103 ADC采集_是小刘不是刘的博客-CSDN博客
因为之前说了扫描模式下循环采集需要ADC来实现,所以吧ADC的多通道扫描采集写到了这里来了,所以没有看ADC的朋友可以先去看看ADC是怎么使用的。
首先还是看一下图
ADC触发之后,信号给DMA,这时候DMA会帮助ADC进行数据转移,也就不会像之前说的那样,不管转换多少个通道的数据都会只保存最后一个转换数据了
1) DMA多通道单次触发
OK打开我们之前写好的ADC的代码,将我们写的DMA的初始化加进去
整体代码如下,后面开始解释
//adc.c文件
uint16_t addr[2]={0};
static void AD_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
ADC_InitTypeDef ADC_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_10MHz;
GPIO_Init(GPIOC,&GPIO_InitStruct);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_RegularChannelConfig(ADC1,ADC_Channel_11,1,ADC_SampleTime_1Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_12,2,ADC_SampleTime_1Cycles5);
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=2;
ADC_InitStruct.ADC_ScanConvMode=ENABLE;
ADC_Init(ADC1,&ADC_InitStruct);
ADC_DMACmd(ADC1,ENABLE);
ADC_Cmd(ADC1,ENABLE);
//校验
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1) == SET);
}
static void MyDMA_Init(void)
{
DMA_InitTypeDef DMA_InitStruct;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitStruct.DMA_BufferSize=2;
DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;
DMA_InitStruct.DMA_Mode=DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority=DMA_Priority_High;
DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)addr;
DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;
DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable;
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
DMA_Cmd(DMA1_Channel1,ENABLE);
}
void MyAD_Init(void)
{
AD_Init();
MyDMA_Init();
}
void AD_GetValue(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,2);
DMA_Cmd(DMA1_Channel1,ENABLE);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
/*等待传输完成*/
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
//adc.h文件
#ifndef __ADC_H
#define __ADC_H
#include "stm32f10x.h"
extern uint16_t addr[2];
void AD_GetValue(void);
void MyAD_Init(void);
#endif /*__ADC_H*/
这里首先是AD的配置
1 ADC开启两个通道
(和ADC章节我开的两个通道的引脚是一样的是一样的) 我使用的为PC1 PC2 所以通道是11和12,
2 开启2个通道序列
(就是前面说的序列)之后我们把扫描模式使能。
3 使能ADC_DMA的信号
4 配置DMA
1 更改计数器的值
2 关闭软件触发
3 填写存储器的地址,我们这里定义了一个数组,直接将数组名填上就行,然后强转为32位,
4 改为接受半字 16位,因为AD转换的数据是12位存在了16位的寄存器里面,所以我们这里也只接受16位就够了
5 填写外设地址 这里ADC1为一个结构体指针,所以还是需要取地址的
6 改为半字(转换出来就是16的寄存器在存
7 关闭DMA的地址自增,我们只需要从转换的地方一直获取数据就行,不需要他自增地址
5 启动部分
1 首先DMA的触发,因为这里我们还是单次触发,所以还是需要停一下触发之后重置计数值,然后启动
2 启动
3 等待DMA传输完成,因为DMA传输完成肯定在转换完成之后,所以只用判断DMA是否转换完成即可
6 主函数
int main(void)
{
Usart_Config();
MyAD_Init();
printf("addr[0]=%d",addr[0]);
while(1)
{
AD_GetValue();
printf("%d %d\r\n",addr[0],addr[1]);
Delay(5000000);
}
}
这就是很正常的测试是否有值了,大家可以自己发挥
结果(扭动可变电阻正常变化,第二个引脚直接测的3.3 所以基本是满量程
2) DMA连续转换+扫描模式
很简单,对比刚刚的单次采集更改如下
1 更改ADC触发模式
将ADC的单次触发改为连续触发
2 循环模式
这里开启循环模式,等于计数器减到0后,会自动帮你重载计数器
3 加入触发
在DMA最后加上触发即可
4 删除函数
删除我们单次触发写的AD_GetValue()即可,这里只需要触发一次就可,不需要我们来判断是否转换完成,他会循环刷新,轮不到我们判断的。
5 主函数
我们只需要做个初始化即可,这样转换值就已经在自动刷新和转运了。