🚀write in front🚀
🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝💬本系列哔哩哔哩江科大STM32的视频为主以及自己的总结梳理📚
🚀Projeet source code🚀
💾工程代码放在了本人的Gitee仓库:iPickCan (iPickCan) - Gitee.com
引用:
STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili
Keil5 MDK版 下载与安装教程(STM32单片机编程软件)_mdk528-CSDN博客
STM32之Keil5 MDK的安装与下载_keil5下载程序到单片机stm32-CSDN博客
0. 江协科技/江科大-STM32入门教程-各章节详细笔记-查阅传送门-STM32标准库开发_江协科技stm32笔记-CSDN博客
【STM32】江科大STM32学习笔记汇总(已完结)_stm32江科大笔记-CSDN博客
江科大STM32学习笔记(上)_stm32博客-CSDN博客
STM32学习笔记一(基于标准库学习)_电平输出推免-CSDN博客
STM32 MCU学习资源-CSDN博客
stm32学习笔记-作者: Vera工程师养成记
stem32江科大自学笔记-CSDN博客
术语:
英文缩写 | 描述 |
GPIO:General Purpose Input Onuput | 通用输入输出 |
AFIO:Alternate Function Input Output | 复用输入输出 |
AO:Analog Output | 模拟输出 |
DO:Digital Output | 数字输出 |
内部时钟源 CK_INT:Clock Internal | 内部时钟源 |
外部时钟源 ETR:External clock | 时钟源 External clock |
外部时钟源 ETR:External clock mode 1 | 外部时钟源 Extern Input pin 时钟模式1 |
外部时钟源 ETR:External clock mode 2 | 外部时钟源 Extern Trigger 时钟模式2 |
外部时钟源 ITRx:Internal trigger inputs | 外部时钟源,ITRx (Internal trigger inputs)内部触发输入 |
外部时钟源 TIx:external input pin | 外部时钟源 TIx (external input pin)外部输入引脚 |
CCR:Capture/Comapre Register | 捕获/比较寄存器 |
OC:Output Compare | 输出比较 |
IC:Input Capture | 输入捕获 |
TI1FP1:TI1 Filter Polarity 1 | Extern Input 1 Filter Polarity 1,外部输入1滤波极性1 |
TI1FP2:TI1 Filter Polarity 2 | Extern Input 1 Filter Polarity 2,外部输入1滤波极性2 |
DMA:Direct Memory Access | 直接存储器存取 |
正文:
0. 概述
从 2024/06/12 定下计划开始学习下江协科技STM32课程,接下来将会按照哔站上江协科技STM32的教学视频来学习入门STM32 开发,本文是视频教程 P2 STM32简介一讲的笔记。
引用:
什么是DMA以及DMA的意义-CSDN博客
一,什么是DMA?
- 1.1 DMA(Direct Memory Access,直接存储器访问)提供在外设与内存、存储器和存储器之间的高速数据传输使用。它允许不同速度的硬件装置来沟通,而不需要依赖于CPU,在这个时间中,CPU对于内存的工作来说就无法使用。
- 1.2 DMA的意义
代替 CPU 搬运数据,为 CPU 减负。
1. 数据搬运的工作比较耗时间;
2. 数据搬运工作时效要求高(有数据来就要搬走);
3. 没啥技术含量(CPU 节约出来的时间可以处理更重要的事)。
1.🚢第一个代码:DMA数据转运
连线图:
扩展知识
先扩展一下知识点:
通常我们定义的普通变量是存储在SRAM区,此时该变量的地址是以20开头的。
而在变量前面加上const,将变量变成常量,只能读不能写,而我们上节说过Flash区的数据就是只读不写,所以加了const关键字的变量是存储在Flash区的,此时它的地址是以08开头。加了const关键字的变量只能在定义的时候给一个值,后面想要给它赋值的话程序就会报错,因为它已经变成了常量,不能被修改。
对于变量或者常量来说,它们的地址是由编译器决定的,不同的程序地址可能不一样,是不固定的。但是外设寄存器的地址是固定的,是以40开头的,在手册里都能查得到。
在程序里也可以用结构体很方便地访问寄存器,比如要访问ADC1的DR寄存器,就可以写ADC1->DR,这样就可以访问ADC的DR寄存器了。
我们可以显示一下这个ADC1的DR寄存器的地址,
结果是4001 244C
我们在手册中查到的ADC1的起始地址是4001 2400,
然后查到DR寄存器偏移是4C,
所以ADC1的DR地址就是4001 244C。
所以如果想算某个寄存器的地址,就可以查手册计算一下。
首先查一下这个寄存器所在外设的起始地址,然后再在外设的寄存器器总表里查一下偏移,起始地址加偏移,就是这个寄存器的实际地址。
我们再来研究一下ADC1—>DR这个东西是如何知道ADC1_DR寄存器的地址,这种结构体的方式又是如何访问到寄存器的?
我们可以在ADC1处右键跳转到定义,可以看 ADC1就是这个东西
左边是一个强制类型转换,把ADC1_BASE转换为了ADC_TypeDef类型的指针。
ADC1_BASE就是ADC1的基地址,基地址是起始地址的意思,也就是我们刚才查表看到的4001 2400。
在APB2PERIPH_BASE右键再转到定义,APB2外设基地址就是外设基地址+0x10000。
再转到定义,外设基地址就是0x4000 0000,可以看到上面还有SRAM基地址,是2000,flash基地址是0800,和我们上面讲的都是一致的。
这里回过来看,
外设基地址0x4000 0000+0x10000=4001 0000,是APB2外设基地址;
APB2外设基地址4001 0000+0x2400=4001 2400,就得到了ADC1的基地址,也就是手册表里写那样。
现在基地址有了,但是基地址+偏移才是寄存器的实际地址,在这里,它使用了一个非常巧妙的方法来实现这个偏移:就是使用结构体来实现,我们跳转到结构体的定
可以看到,这里是依次定义的各种寄存器,
这个结构体成员的顺序,和手册上这个寄存器实际存放的顺序是一一对应的
所以说如果我们定义一个ADC结构体的指针,并且指针的地址就是这个外设的起始地址。那这个结构体的每个成员就会正好映射实际的每个寄存器
如果指定这个结构体的起始地址就是ADC1外设寄存器的起始地址,那么这个结构体的内存和外设寄存器的内存就会完美重合,再访问结构体的某个成员,就相当于是访问这个外设的某个寄存器。
这就是STM32中使用结构体来访问寄存器的流程。
那么回头看看ADC1—>DR,现在就应该明白它是什么意思了吧!
ADC1是结构体指针,指向的是ADC1外设的起始地址,访问结构体成员就相当于是加一个地址偏移,起始地址加偏移就是指定的寄存器。
STM32这个库函数把访问一个寄存器做的还是非常麻烦的,其实如果想简单点的话,直接用指针访问某个物理地址就行了,比如:
这样写也是可以访问ADC1的DR寄存器的,和这个结构体访问的效果是一模一样的。
到这里有关存储器地址、常量和变量、结构体访问寄存器这些知识点就讲完了。
接下来我们回到正题,看一下DMA的配置。
DMA的配置步骤
DMA初始化的步骤我们看这个基本结构图:
第一步,RCC开启DMA的时钟。
第二步,初始化DMA,就可以直接调用DMA_Init,初始化结构图中各个参数了,包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源。当然还有一个通道优先级,这里没画出来。这所有的参数通过一个结构体就可以配置好了。
第三步,开关控制,DMA_Cmd给指定的通道使能就完成了。如果选择的是硬件触发,不要忘了在对应的外设调用一下XXX_DMACmd开启一下触发信号的输出。如果需要DMA的中断,那就调用DMA_ITConfig开启中断输出,再在NVIC里配置相应的中断通道,然后写中断函数就行了。
这里结构图没有画中断的部分.
最后在运行的过程中,如果转运完成传输计数器清零。这时再想给传输计数器赋值的话,就DMA失能,写传输计数器,DMA使能这样就行了,这就是DMA的编程思路。
DMA的库函数
打开dma.h拖到最后。
DMA_DeInit
恢复缺省配置。
DMA初始化和DMA结构体初始化函数
DMA_Cmd
DMA_ITConfig
DMA中断输出使能
DMA_SetCurrDataCounter
DMA设置当前数据寄存器,这个函数就是给传输计数器写数据的。
DMA_GetCurrDataCounte
MA获取当前数据寄存器,这个函数就是返回传输计数器的值。如果想看看还剩多少数据没有转运,就可以调用这个函数获取一下传输计数器。
四个获取标志位状态函数
获取标志位状态、清除标志位、获取中断状态、清除中断挂起位。
代码实现
首先我们定义一下DMA转运的源端数组和目的数组,我们目前总共有四个数据,当然实际情况可能会有成千上万个数据,这样才能发挥出DMA转运的优势。然后第二个目的数组,全给0。
接下来我们的任务就是初始化DMA,然后让DMA把这里DataA的数据转运到DataB里面去。
MyDMA.c
第一步,开启时钟
DMA是AHB总线的设备,所以要用AHB开启时钟的函数。
第一个参数,对于互联型设备,这个参数可以是下面这些值的组合
对于其它设备,这个参数是这下面的组合
互联型是STM32F105/107的型号,我们的芯片是F103,所以我们在下面这个参数表里选,选择DMA1的参数就行了。
第二个参数enable开启DMA1的时钟。
第二步,初始化DMA
然后接下来DMA的初始化,初始化结构体的成员比较多
前面六个成员:
- 外设站点的起始地址、
- 数据宽度、
- 是否自增、
- 存储器站点的起始地址、
- 数据宽度、
- 是否自增。
之后是
- 传输方向DIR、
- 缓冲区大小(其实就是传输计数器)、
- 传输模式(其实就是是否使用自动重装)、
- M2M(选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发)、
- 最后priority优先级这个按照参数要求给一个优先级就行了。
MA_Init函数的第一个参数是DMAy_Channelx,其中y可以是1或2,用来选择是哪个DMA,x可以是1到7对应DMA1,或者可以是1到5对应DMA2,可以选择是哪一个通道。
所以这里第一个参数既选择了是哪个DMA,也选择了是DMA的哪个通道。
我们选择的是DMA1,y就改成1,x选择通道,这里因为是存储器到存储器的转运用的是软件触发,所以通道可以任意选择,就给通道1。
然后我们挨个看一下结构体的成员取值。
第一个成员是外设站点的起始地址,对于SRAM的数据,它的地址是编译器分配的,并不是固定的。所以我们一般不会写绝对地址,而是通过数组名来获取地址。那这里我们就把这个地址提取成初始化函数的参数,这样在初始化的时候,想转运哪个数组,就把哪个数组的地址传进来就行了
第二个成员:数据宽度,取值:字节,半字,字
这里我们是以字节的方式传输。
第三个成员:地址是否自增,跳转定义看一下。取值的第一个自增enable就是自增,第二个自增disable就是不自增。根据上节的分析,这种数组之间的转运地址肯定是要自增的。所以选第一个。
第四个成员是存储器站点的基地址,我们也把它提取成参数、
第五个成员是数据宽度,也选择字节。
第六个成员是存储器站点地址是否自增,我们选enable。
这样外设站点和存储器站点的参数就配置好了。
第七个成员是方向,是指定外设站点是源端还是目的地,参数取如下
这里有两个参数,第一个是外设站点,作为DST,即 destination目的地。外设站点作为目的地,其实就是传输方向是存储器到外设站点这样来传输的。第二个是外设站点作为SRC,即 source源头,也就是外设站点到存储器站点的传输方向。那我们打算是把DataA放在外设站点,DataB放在存储器站点,所以传输方向就是外设站点到存储器站点。所以这里选择第二个参数,外设站点作为数据源。
第八个成员是buffsize是以数据单元指定缓冲区的大小,就是说要传送几个数据单元,这个数据单元等于传输源端站点的Data size,说简单点就是buffer size就是传输计数器,指定传输几次。这个buffsize其实就是直接赋值给传输计数器的寄存器。它的取值是0到65535。我们把这个buffer size也提取到函数的参数。然后把它赋值给Buffersize成员
这样传输次数就完成了。
第九个成员指定传输计数器是否要自动重装,注意:循环模式(也就是自动重装)不能应用在存储器到存储器的情况下。也就是我们上节说的自动重装和软件触发不能同时使用。如果同时使用DMA就会连续触发,永远也不会停下来。这个成员的取值:
这里有两个参数,第一个是循环模式,就是传输计数器自动重装。第二个是正常模式,就是传输计数器,不自动重装,自减到0后停下来。这里我们转运数组是存储器到存储器的传输,转运一次停下来就行了。所以选择第二个正常模式。
第十个成员M2M,就是DMA是否应用于存储器到存储器的转运,存储器到存储器的模式,就是软件触发,取值:
enable就是使用软件触发,第二个disable就是不使用软件触发,也就是使用硬件触发,我们转运数组,所以选择第一个使用软件触发。
最后一个成员priority是指定通道的软件优先级。这里有四个优先级:
第一个是very high非常高,第二个是high高,第三个是medium中等,第四个是low低。如果有多个通道的话,可以指定一下,确保紧急的转运有更高的优先级。目前我们就一个通道,那优先级就随便,可以选择一个中等优先级
到这里,DMA的参数就配置完成了。
那到目前为止,DMA还暂时不会工作。
DMA转运有三个条件:
- 🤠第一个条件:传输计数器大于零。
- 🤠第二个条件:触发源有触发信号。
- 🤠第三个条件:DMA使能
三个条件缺一不可
目前如果传一个大于零的数给size的话,第一个条件满足。触发源为软件触发,所以一直都有触发信号,第二个条件满足。最后一个条件DMA还没有使能,第三个条件不满足。
第三步,开关控制
所以到目前为止,DMA还不会工作。如果想在初始化之后就立刻工作的话。可以在这最后加上DMA_Cmd,第一个参数DMA1_Channel1,第二个enable。使能DMA之后,三个条件满足DMA就会进行数据转运了。
转运一次传输计数器自减一次,当传输计数减到零之后,转运完成。之后第一个条件就不满足了,转运停止,这样就完成了一次数组之间的数据转运。
现在我们是初始化之后,立刻就进行转运,并且转运一次之后,DMA就停止了。如果DataA的数据又变化了,我们想再转运一次,那该怎么办?
启动DMA转运的函数
这时我们就需要给传输计数器重新赋值了。我们可以在初始化之后再写个函数,调用一次这个函数就再次启动一次DMA转运。在里面我们需要重新给传输计数器赋值,传输计数器赋值必须要先给DMA失能。
然后就可以给传输计数器赋值了,我们需要用到这个函数
第一个参数是DMAy_Channelx,选择DMA和通道,第二个参数是指定要给传输计数器写入的值,这里我们需要获取一下初始化时的size参数。但是它俩不在一个函数,不能直接传递过来,所以我们可以在这上面定义一个全局变量,然后初始化的时候,把size往这个全局变量也存一份。
之后在这个函数里就可以使用全局变量MyDMA_Size了,这样就可以重新给传输计数器赋值了。
最后再次使能DMA,就会再次开始转运。
然后我们先在上面写Disable,不让DMA初始化之后就立刻进行转运,而是等调用Transfer函数之后,再进行转运。
在转运开始之后,我们还需要做一个工作,就是等待转运完成。
我们需要用到这个函数查看一下标志位:
总共就是四种标志位,所有的通道都是这四种标志位
这里我们需要检查DMA1通道1转运完成的标志位,所以选择这个:
转运完成之后,标志位置1,所以我们需要加一个while循环等待这个标志位,如果没有完成,就一直循环等待,这样就实现了等待转运完成的效果了。标志位置1之后,不要忘了清除标志位,这个标志位是需要手动清除的,清除要用到这个函数参数和上面的一样:
到这里,我们这个函数就全部写完了
实验1源码
MyDMA.c
#include "stm32f10x.h" // Device header
#include "MyDMA.h"
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t ArrayA, uint32_t ArrayB, uint16_t Size)
{
MyDMA_Size = Size;
//RCC AHB外设DMA1时钟使能
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//DMA初始化
DMA_InitTypeDef DMA_InitStruct;
//DMA_StructInit(&DMA_InitStruct);
//DMA通道的配置
DMA_InitStruct.DMA_PeripheralBaseAddr = ArrayA; //DMA外设起始地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //DMA转运长度,字节/半字/字
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //DMA外设地址是否自增
DMA_InitStruct.DMA_MemoryBaseAddr = ArrayB; //存储器起始地址
//DMA_InitStruct.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte; //存储器DMA转运长度,字节/半字/字
//DMA_InitStruct.DMA_MemoryInc = DMA_PeripheralInc_Enable; //存储器地址是否自增
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_BufferSize = Size; //DMA计数器
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; //DMA方向,从外设到存储器/从存储器到外设
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable; //DMA M2M存储器到存储器转运,软件触发(不需要硬件触发源)
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; //DMA非循环模式,DMA单次执行非自动装载
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; //DMA通道的的优先级
DMA_Init(DMA1_Channel1, &DMA_InitStruct); //使用DMA1 channel_1
//使能DMA
DMA_Cmd(DMA1_Channel1, DISABLE);
}
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); //去使能DMA
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); //设置DMA计数器
DMA_Cmd(DMA1_Channel1, ENABLE); //使能DMA
//等待DMA完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1); //清除DMA完成标记
}
MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif
Main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
extern uint16_t Num;
uint8_t ArrayA[4];
uint8_t ArrayB[4];
int main(int argc, char *argv[])
{ uint16_t ADValue;
float voltage;
OLED_Init();
MyDMA_Init((uint32_t)ArrayA, (uint32_t)ArrayB, 4);
OLED_ShowString(1, 1, "ArryA");
OLED_ShowHexNum(1,7,(uint32_t)ArrayA, 8);
OLED_ShowString(3, 1, "ArryB");
OLED_ShowHexNum(3,7,(uint32_t)ArrayB, 8);
while(1)
{
ArrayA[0] += 1;
ArrayA[1] += 1;
ArrayA[2] += 1;
ArrayA[3] += 1;
MyDMA_Transfer();
OLED_ShowHexNum(2, 1, ArrayA[0], 2);
OLED_ShowHexNum(2, 4, ArrayA[1], 2);
OLED_ShowHexNum(2, 7, ArrayA[2], 2);
OLED_ShowHexNum(2, 10, ArrayA[3], 2);
Delay_ms(1000);
OLED_ShowHexNum(4, 1, ArrayB[0], 2);
OLED_ShowHexNum(4, 4, ArrayB[1], 2);
OLED_ShowHexNum(4, 7, ArrayB[2], 2);
OLED_ShowHexNum(4, 10, ArrayB[3], 2);
Delay_ms(1000);
}
return 1;
}
实验1实验结果
实验1问题记录
🙂🙂DMA的外设地址增长方向,外设地址是否自增,参数宏定义和DMA存储设备地址增长方向,存储设备地址是否自增的宏定义不是同一个。
使用错误的话,DMA会工作异常。
查看ad.h宏定义,也可以看到实际的定义值不一样
🙂🙂 DMA使用之前不要忘记初始化DMA
2.🚢第二个代码:DMA+AD多通道
接线图:
这个和上节AD多通道的接线是一样的。
我们是在上节AD.c的基础上修改的。
上节ADC的配置步骤:
前面说的DMA的配置步骤:
把ADC和DMA给配合起来,我们要将ADC配置成连续扫描+DMA循环转运的模式。
配合起来的流程就看这个图
ADC和DMA配合起来的配置步骤
第一步,开启RCC时钟,开启ADC1、GPIOA和DMA1的时钟,另外这里ADC CLK的分频器也需要配置一下;
第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式。
第三步,配置ADC多路开关,把通道接入到规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜列在菜单里
第四步,配置ADC转换器,在库函数里是用结构体来配置的,要使能连续转换模式,每转换一次规则组序列后立刻开始下一次转换。
第五步,配置DMA,就可以直接调用DMA_Init,所有的参数通过一个结构体就可以配置好了,其中,模式要选择循环模式,与ADC的连续转换一致。
第六步,就是开关控制,调用一下ADC_Cmd和DMA_Cmd的函数开启ADC和DMA,但是不要忘了ADC1触发DMA1的信号使能,就是这里:
这里有三个硬件触发源,具体使用哪个,取决于把哪个的DMA输出给开启了,要调用ADC_DMACmd这个函数开启ADC1作为硬件触发源。
第七步,ADC校准,这样可以减小误差。
第八步,配置ADC触发,软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作,这里要用到ADC_SoftwareStartConvCmd这个函数,调用一下就能软件触发转换了,它控制AD的启动和结束,对应结构图中的START这里:
代码实现
第一步,开启RCC时钟
第二步,配置GPIO
第三步,配置多路开关
首先我们要扫描PA0到PA3这四个通道,所以点菜菜单放在这里
这样菜单就点好了,菜单上的1到4号的空位我填上了0到3这四个通道。当然这个通道和次序可以任意修改,这样最终结果存放的顺序也会相应变化。
第四步,配置ADC转换器
点菜完成之后继续往下看ADC扫描模式,这个参数要改成enable,告诉厨师,我们点了多个菜,不要指盯第一个菜看。然后number of channel改成4,告诉厨师,我点的是四个菜,看前四个位置就可以了。
连续模式可以是连续扫描,也可以是单次扫描,这里我们用连续扫描模式,即Enable
到这里ADC扫描模式就配置完成了之后,我们来配置下DMA。
第五步,配置DMA
可以把DMA想象成服务员,ADC这个厨师把菜做好了,DMA这个服务员要尽快把菜端出来,防止被覆盖。
DMA的第一个参数,外设基地址,这里是端菜的源头,厨师把菜做好,就放在ADC_DR寄存器里,所以端菜的源头地址就填ADC_DR的地址。之前我们也算过ADC1的DR寄存器地址就是0x4001 244C,所以可以直接这样来填,不过我们一般都不自己算,因为库函数已经帮我们算好了,所以这里可以这样写 (uint32_t)&ADC1->DR。这样得到的结果,其实就是0x4001 244C,那这样外设地址就填好了。
数据宽度,我们想要DR寄存器低十六位的数据,所以数据宽度就是Halfworld以半字十六位来转运(高16位是ADC2的,所以不用管)
外设地址是否自增这个成员给disable,不自增,始终转运同一个位置的数据。
接下来存储器站点,存储器基地址,也就是端菜的目的地。我们想要把数据存在SRAM数组里,所以我们先在前面定义一个数组,
然后赋值给这个成员就可以了
数据宽度也是半字。
地址是否自增给enable存储储器的地址是自增的,每转运一次挪一个坑。
到这里DMA的源端和目的地的参数就配置好了。
传输方向外设站点是源。
传输数量给4个,因为有4个ADC通道,所以传输4次。
传输模式这个可以给正常的单次模式,也可以给自动重装的循环模式。这里我们配置成循环模式。
然后M2M要给disable不使用软件触发,我们需要硬件触发,触发源为ADC1。厨师每个菜做好了,叫我一下,我再去端菜,这样才是合适的时机。
最后所有的参数都配置到DMA1的通道1里面去。这里通道就不能任意选择了。
这里要上节讲过的DMA1请求映像那个框图,ADC1的硬件触发是只接在了DMA1的通道1上。
第六步,就是开关控制
接着DMA_Cmd可以直接使能
这时DMA转运的三个条件:
🤠第一个传输计数器不为零,满足。
🤠第三个DMA使能,满足,
🤠但是第二个触发源有信号,目前是不满足的,因为这里是硬件,触发ADC还没启动,不会有触发信号。
所以这里DMA使能之后不会立刻工作。
最后在ADC使能之前,还有一个事情需要做,就是开启ADC到DMA的输出。这个我们上一节说过。这里有三个硬件触发源,具体使用哪个,取决于把哪个的DMA输出给开启了。
这里我们在adc.h里面找这个函数,
这个函数就是用来开启DMA触发信号的。
⚠️⚠️⚠️注意:DMA一定要在ADC使能之前开启,否则可能会出错。
第七步,ADC校准
到目前为止,ADC和DMA配合工作的配置就完成了。
第八步,配置ADC触发
最后再触发ADC,ADC得手动开启才能自动触发DMA,,所以是软件触发。放在初始化的最后一行
当ADC触发之后,ADC连续转换,DMA循环转运,两者一直在工作。始终把最新的转换结果刷新到SRAM数组里。当我们想要数据的时候,随时去数组里取就行了。
实验2源码
AD.c
#include "stm32f10x.h" // Device header
#include "AD.h"
uint16_t MyADValue[4];
void AD_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //RCC APB2 GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //RCC APB2 ADC1时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //RCC AHB DMA1时钟
//RCC_ADC Clock预分频值
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //RCC_ADC_Clock=72Mhz/6=12Mhz
//GPIOA_Pin0配置为模拟输入
//GPIOA_Pin0作为ADC1的输入通道1,2,3,4
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_AIN;
gpioInitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
//ADC模拟输入多路开关
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_Init初始化
ADC_InitTypeDef ADC_InitStruct;
ADC_StructInit(&ADC_InitStruct);
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //非外部触发,软件触发方式
ADC_InitStruct.ADC_ScanConvMode = ENABLE; //ADC使用扫描模式,配合DMA自动转运
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; //ADC使用连续转换模式
ADC_InitStruct.ADC_NbrOfChannel = 4; //ADC规则组里扫描数目
ADC_Init(ADC1, &ADC_InitStruct);
//DMA初始化
DMA_InitTypeDef DMA_InitStruct;
DMA_StructInit(&DMA_InitStruct);
//DMA通道的配置
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //DMA外设起始地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //DMA转运长度,字节/半字/字
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //DMA外设地址是否自增,不自增
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)MyADValue; //存储器起始地址
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器DMA转运长度,字节/半字/字
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址是否自增
DMA_InitStruct.DMA_BufferSize = 4; //DMA计数器
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; //DMA方向,从外设到存储器/从存储器到外设
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; //M2M=0,DMA硬件触发源
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; //DMA循环模式,自动重装
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; //DMA通道的的优先级
DMA_Init(DMA1_Channel1, &DMA_InitStruct); //使用DMA1 channel_1
//使能DMA
DMA_Cmd(DMA1_Channel1, ENABLE);
//必须先使能DMA请求,再使能DMA开关,否则可能会出错
//ADC使能DMA请求
ADC_DMACmd(ADC1, ENABLE);
//ADC开关
ADC_Cmd(ADC1, ENABLE);
//ADC校准
ADC_ResetCalibration(ADC1); //软件置标志位
while(ADC_GetResetCalibrationStatus(ADC1) == SET); //当校准完成之后,硬件自动清除标志位
ADC_StartCalibration(ADC1); //软件置标志位
while(ADC_GetCalibrationStatus(ADC1) == SET); //当校准完成之后,硬件自动清除标志位
//软件ADC触发转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
uint16_t GetValue(uint8_t ADC_Channel)
{
return 0;
}
AD.h
#ifndef __AD_H__
#define __AD_H__
extern uint16_t MyADValue[4];
void AD_Init(void);
uint16_t GetValue(uint8_t ADC_Channel);
#endif
Main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
extern uint16_t Num;
uint8_t ArrayA[4];
uint8_t ArrayB[4];
int main(int argc, char *argv[])
{ uint16_t ADValue;
float voltage;
OLED_Init();
//MyDMA_Init((uint32_t)ArrayA, (uint32_t)ArrayB, 4);
AD_Init();
OLED_ShowString(1, 1, "AD1:");
OLED_ShowString(2, 1, "AD2:");
OLED_ShowString(3, 1, "AD3:");
OLED_ShowString(4, 1, "AD4:");
while(1)
{
OLED_ShowNum(1, 5, MyADValue[0], 4);
OLED_ShowNum(2, 5, MyADValue[1], 4);
OLED_ShowNum(3, 5, MyADValue[2], 4);
OLED_ShowNum(4, 5, MyADValue[3], 4);
Delay_ms(1000);
}
return 1;
}