stm32学习笔记-8 DMA直接存储器读取

2023-11-01

8 DMA直接存储器读取

注:笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的Github仓库


8.1 DMA简介

DMA(Direct Memory Access)直接存储器存取 可以直接访问STM32内部的存储器,包括外设寄存器(一般指外设的数据寄存器DR,如ADC的数据寄存器、串口数据寄存器等)、运行内存SRAM(存储运行变量)、程序存储器FLASH(存储程序代码)等。DMA可以提供 外设寄存器和存储器 或者 存储器和存储器之间 的高速数据传输,无须CPU干预,节省了CPU的资源。翻译成人话就是,是一个数据转运小助手,主要用来协助CPU完成数据转运的工作。下面是stm32中DMA的一些配置:

  • stm32系列芯片共有12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)。
  • 每个通道都支持软件触发和特定的硬件触发。
  • 软件触发应用场景:数据源中的数据已确定。如将FLASH中的数据转运到SRAM中(存储器–>存储器),一次触发后会将数据以最快的速度全部转运完毕。
  • 硬件触发应用场景:数据源中的数据没有全部确定,需要在特定的时机转运数据。如将ADC数据转运到存储器(外设寄存器–>存储器),等对应ADC通道的数据转换完成后,才硬件触发一次DMA转运数据。
  • STM32F103C8T6型号的DMA资源:DMA1(7个通道)

下面介绍“存储器映像”。计算机有五大组成部分:运算器、控制器、存储器、输入设备、输出设备。计算机的核心关键部分就是CPU和存储器,上述的运算器和控制器会合在一起组成CPU,而存储器主要关心存储器的内容和地址。下图给出了stm32中都有哪些存储器,以及这些存储器的地址都是什么,即 “存储器映像”,下表则对下图进行了一个简单的总结:

图8-1 存储器映像——来自数据手册“4 存储器映像”
表8-1 stm32存储器映像——总结自“STM32F103x8B数据手册”
类型 起始地址 存储器 用途
ROM 0x0800 0000 程序存储器Flash 存储C语言编译后的程序代码
0x1FFF F000 系统存储器 存储BootLoader,用于串口下载
0x1FFF F800 选项字节 存储一些独立于程序代码的配置参数
RAM 0x2000 0000 运行内存SRAM 存储运行过程中的临时变量
0x4000 0000 外设寄存器 存储各个外设的配置参数
0xE000 0000 内核外设寄存器 存储内核各个外设的配置参数
  • 程序存储器FLASH:下载程序的位置,程序一般也是从主闪存里开始运行。若某变量地址为0x0800_xxxx,那么它就是属于主闪存的数据。
  • 系统存储器:存储BootLoader程序(俗称“刷机”),芯片出厂时自动写入,一般不允许修改。
  • 选项字节:存储的主要是FLASH的读保护、写保护、看门狗等配置。下载程序可以不刷新选项字节的内容,从而保持相应配置不变。
  • 运行内存SRAM:在程序中定义变量、数组、结构体的地方,类似于电脑的内存条。
  • 外设寄存器:初始化各种外设的过程中,最终所读写的寄存器就属于这个区域。
  • 内核外设寄存器:就是NVIC和SysTick。由于不是同一个厂家设计,所以专门留出来内核外设的地址空间,和其他外设的地址空间不一样。

注:由于stm32是32位的系统,所以寻址空间最大可达4GB(每个地址都代表1Byte),而stm32的存储器硬件最多也就是KB级别的,所以实际上4GB的寻址空间使用率远远低于1%。
注:上表中前三者存储介质都是FLASH,但是一般说“FLASH”就是代指“主闪存FLASH”,而不是另外两块区域。
注:上表中后三者存储介质也是SRAM,但是一般将“SRAM”就是代指“运行内存”,“外设寄存器”就直接叫“寄存器”。

图8-2 DMA电路框图

将从以下加粗的四大部分介绍DMA的电路结构。

  • 总线矩阵: 为了高效有条理的访问存储器,设置了总线矩阵。左端是主动单元,拥有存储器的访问权;右端是被动单元,它们的存储器只能被左端的主动单元读写。
  • 总线矩阵内部的仲裁器:如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。但是总线仲裁器仍然会保证CPU得到一半的总线带宽,以确保CPU正常工作。
  • 主动单元:
  1. Cortex-M3核心(左上角):包含了CPU和内核外设。剩下的所有东西都可以看成是存储器,比如Flash是主闪存、SRAM是运行内存、各种外设寄存器也都可以看成是一种SRAM存储器。
  2. ICode总线:指令总线。加载程序指令。
  3. DCode总线:数据总线,专门用来访问Flash。
  4. 系统总线:是访问其他东西的。
  5. DMA总线:用于访问各个存储器,包括DMA1总线(7个通道)、DMA2总线(5通道)、以太网外设的私有DMA总线。由于DMA要转运数据,所以DMA也有访问的主动权
  6. DMA1、DMA2:各个通道可以分别设置转运数据的源地址和目的地址,所以各个通道可以独立的进行数据转运
  • 仲裁器:调度各个通道,防止产生冲突。虽然多个通道可以独立地转运数据,但是DMA总线只有一条,所以所有的通道都只能 分时复用 这一条DMA总线,若通道间产生冲突,就会由仲裁器根据通道的优先级决定使用顺序。
  • AHB从设备:用于配置DMA参数,也就是DMA自身的寄存器。DMA的外设配置寄存器直接连接在了被动单元侧的AHB总线上。所以DMA既是总线矩阵上的主动单元,可以读写各种寄存器;同时也是AHB总线上的被动单元。CPU配置DMA的线路:“系统”总线–>总线矩阵–>AHB总线–>DMA中的AHB从设备。
  • 被动单元:
  1. Flash:主闪存,只读存储器。若直接通过总线访问(无论是CPU还是DMA),都只能读取数据而不能写入。若DMA的目的地址为FLASH区域,那么转运就会出错。要想对Flash写入,可以通过“Flash接口控制器”。
  2. SRAM:运行内存,通过总线可以任意读写。
  3. 各种外设寄存器(右侧两个方框):需要对比参考手册中的描述,这些寄存器的类型可能为 只读/只写/读写。不过日常主要使用的数据寄存器,都是可以正常读写的。
  • DMA请求: 用于硬件触发DMA的数据转运。“请求”就是“触发”的意思,此线路右侧的触发源是各个外设,所以这个“DMA请求”就是 DMA的硬件触发源,如ADC转换完成、串口接收到数据等信号。

从上面DMA的电路介绍中,不难看出寄存器是一种特殊的存储器,寄存器的两大作用:

  1. 存储数据。被CPU或DMA等读写,就像读写运行内存一样。
  2. 控制电路。寄存器的每一位都接了一个导线,可以用于控制外设电路的状态,比如置引脚的高低电平、导通或断开开关、切换数据选择器,或者多位结合起来当做计数器、数据寄存器等。

所以寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。既然外设相当于寄存器,寄存器又是存储器,那么使用DMA转运数据,本质上就是从某个地址取数据,再放到另一个地址去。

图8-3 DMA基本结构

前面的“DMA电路框图”只是一个笼统的结构图,没有展现出对于DMA内部的执行细节。而上图“DMA基本结构”则可用于代码编写时的思路参考,以实现控制DMA工作。

  • 外设寄存器站点、存储器站点(Flash和SRAM):数据转运的两大站点。“外设站点”的参数不一定是外设,“存储器站点”的参数也不一定是存储器。这两个站点的名字只是图一乐,真正表示的意思就是发送端和接收端的参数。可以看到图中由 三类数据转运线路。注意Falsh一般只读,所以不存在 “SRAM到Flash”或“Falsh到Flash”的线路。

注:虽然名字“图一乐”,但在stm32手册中,“存储器”一般特指“Flash”和“SRAM”;“外设”一般特指“外设寄存器”。

下面的7个参数都属于DMA的初始化结构体:

  1. 方向:指明“外设寄存器站点”是发送端还是接收端。
  • 站点参数:
  1. 起始地址:配合“方向”的设置,两个起始地址指明了发送端地址、接收端地址。
  2. 数据宽度:指定一次转运的数据宽度,可选字节Byte(8位)、半字HalfWord(16位)、字Word(32位)。比如ADC转换的数据位宽是16位,就需要选择数据宽度为“半字”。
  3. 地址是否自增:一次转运完成后,下一次转运是否地址自增。如ADC扫描模式使用DMA进行数据转运,数据源是ADC_DR寄存器,显然不需要地址自增;而数据目的地是存储器,就需要地址自增,以防止数据覆盖。
  1. 传输计数器:是一种自减计数器,指定总共需要转运几次。转运结束后,之前自增的地址也会恢复成起始地址,以方便新一轮的转换。参考手册规定,写“传输计数器”时必须使用“开关控制”关闭DMA。
  2. 自动重装器:传输计数器自减到0后,是否重装到最初的值继续转运,也就决定了两种转运模式:单次模式(不重装)、循环模式(重装)。例如想转运一个数组,就是单次模式;如果想转运ADC扫描模式+连续转换,此时DMA就需要循环模式。
  3. M2M(Memory to Memory):DMA的触发源选择器,设置为1选择软件触发、设置为0选择硬件触发。
  • DMA的软件触发(连续触发):和之前不同,DMA的软件触发无需手动触发。选择软件触发(M2M=1)后,若使能DMA,那么DMA会自动以最快的速度连续触发,尽快完成本轮转换。软件触发不能和循环模式同时使用,防止DMA停不下来。软件触发常用于存储器到存储器的转运。
  • DMA的硬件触发(单次触发、使用更多):硬件触发源可以选择ADC、串口、定时器等,由相应的外设库函数使能对应的硬件触发源。一般都是与外设有关的转运使用硬件触发,这些转运都需要特定的时机,如ADC转换完成、串口收到数据、定时时间到等。
  • 开关控制:即DMA_Cmd函数,用于使能DMA。

总结:DMA转运的必须条件:开关控制使能、传输计数器大于零、有触发源。

下面再看两个细节和两个例子

细节1:DMA请求
首先进一步介绍“DMA基本结构”中的硬件触发源——“DMA请求”:

图8-4 DMA1请求映像

上图是DMA1的请求映像,所以有7个通道,每个通道都有一个数据选择器,可以选择硬件触发/软件触发,

  • EN位:其实就是DMA的开关控制。
  • 软件触发需要M2M位置1,硬件触发则需要M2M位置0。
  • 硬件触发源:每个通道的硬件触发源各有不同。如ADC1触发必须选择通道1、TIM2更新时间触发必须选择通道2…… 通道的选择由相应的外设库函数决定,如ADC库函数ADC_DMACmd用于开启通道1、TIM库函数TIM_DMACmd可以开启通道2(TIM2_UP)。理论上可以同时开启同一通道的多个触发源,但一般只开启一个。
  • 软件触发通道可以任意选择。

细节2:数据宽度与对齐
前面提到数据发送端和接收端都可以设置数据宽度,若数据宽度设置相同显然就是正常转运,那如果发送端和接收端设置不同的数据宽度,会发生什么情况呢?见下图:

图8-5 数据宽度与对齐说明

基本原则就是:

  • 源端宽度<目的宽度:在目的宽度的高位补零。
  • 源端宽度>目的宽度:按照目的宽度,只保留源端的低位,多余的高位全部舍弃。

上面的过程类似于uint8_tuint16_tuint32_t之间的相互赋值,不够就补零,超了就舍弃高位。

例1:DMA数据转运
任务是将SRAM数组DataA转运到另一个SRAM数组DataB(存储器到存储器)。

图8-6 “数据转运+DMA”示意图

下面给出各参数的配置说明:

  • 两个站点的参数:外设地址->DataA数组首地址、存储器地址->DataB数组首地址;数据宽度都是8位;为保证数据的一一对应,“外设”和“存储器”都设置为地址自增。
  • “方向”参数:默认是“外设”–>“存储器”,当然也可以将方向反过来。
  • 传输计数器:数组大小为7,所以计数器为7。
  • 自动重装:不需要。
  • 触发源:软件触发。由于是“存储器->存储器”的触发,所以不需要等待转运时机。
  • 开关控制:最后调用DMA_Cmd开启DMA转运。

注:上述为“复制转运”,转运完成后DataA数据不会消失。

例2:ADC扫描模式+DMA
期望将外设ADC多通道(扫描模式)的数据,依次搬运到SRAM中的ADValue数组中(外设到存储器)。
注意下图左侧给出了ADC的扫描模式示意图,ADC每触发一次,7个通道依次进行ADC数据转换,每个通道转换完成时,都会将转换结果放到ADC_DR数据寄存器中,也就是ADC的所有通道共用一个ADC数据寄存器。所以每个通道转换完成后,都需要DMA立即转运一次,防止新来的通道数据将之前的通道数据覆盖掉。

图8-7 “ADC扫描模式+DMA”示意图

下面给出各参数的配置说明:

  • 起始地址:“外设站点”地址设置为ADC_DR寄存器的地址;“存储器站点”地址为ADValue(SRAM)的首地址。
  • 数据宽度:“外设站点”和“存储器站点”都设置为16位(HalfWord)。
  • 地址自增:“外设站点”不自增,“存储器站点”自增。
  • 方向:“外设”站点为发送端。
  • 传输计数器:按照ADC需要扫描的通道数来,即为7。
  • 计数器是否自动重装:ADC单次扫描,可以不自动重装;ADC连续扫描,DMA就使用自动重装,此时ADC启动下一轮转换,DMA同时也启动下一轮转运,可以实现同步工作。
  • 触发选择:选择硬件触发——ADC单通道转换完成。虽然前面说过,只有当所有通道都转换完成后,才会触发转换完成EOC标志,其余时间没有任何中断/标志,但实际上,硬件中保留了单个通道针对DMA的请求(虽然参考手册只字不提)。

一般来说,DMA最常见的用途就是配合ADC的扫描模式。因为ADC的扫描模式有数据覆盖的特征,或者说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使得ADC和DMA成为了最常见的伙伴。ADC对DMA的需求非常强烈,其他外设使用DMA可以提高效率,属于是锦上添花的操作,不使用DMA顶多只是损失一些性能;但是ADC的扫描模式如果不使用DMA,功能都会受到很大的限制。所以ADC和DMA的结合最为常见

更多关于DMA的详细内容可以查阅参考手册“2 存储器和总线构架”、“10 DMA控制器(DMA)”。

8.2 实验:DMA数据转运-存储器到存储器

需求:使用DMA,进行存储器到存储器的数据转运。

图8-8 “DMA数据转运”-接线图
图8-9 “DMA数据转运”-代码调用(非库函数)

代码展示:
- main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "DMA_User.h"
#include "Delay.h"

uint8_t DataA[] = {0x11,0x22,0x33,0x44};//源端数组
uint8_t DataB[] = {0x00,0x00,0x00,0x00};//目的端数组

    
int main(void){
    //OLED显示屏初始化
    OLED_Init();
    OLED_ShowString(1,1,"DataA:");
    OLED_ShowHexNum(1,7,(uint32_t)DataA,8);
    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_ShowString(3,1,"DataB:");
    OLED_ShowHexNum(3,7,(uint32_t)DataB,8);
    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);
    
//    //验证存储器映像-注意srm32中地址都是32位的
//    uint8_t aa = 0x66;//存储在运行内存SRAM中
//    const uint8_t bb = 0x55;//存储在Flash中
//    OLED_ShowHexNum(1,1,aa,2);
//    OLED_ShowHexNum(1,4,(uint32_t)&aa,8);//SRAM地址0x2000开头
//    OLED_ShowHexNum(2,1,bb,2);
//    OLED_ShowHexNum(2,4,(uint32_t)&bb,8);//Flash地址0x0800开头
//    OLED_ShowHexNum(3,1,(uint32_t)&ADC1->DR,8);//ADC外设寄存器地址
    
    //DMA 初始化
    DMA_User_Init((uint32_t)&DataA, (uint32_t)&DataB, 4);
    
    while(1){
        //改变数据并显示
        DataA[0]++;
        DataA[1]++;
        DataA[2]++;
        DataA[3]++;
        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);
        Delay_ms(1000);
        
        //转运数据并显示
        DMA_User_Transfer();
        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);
        Delay_ms(1000);
    };
}

- DMA_User.h

#ifndef __DMA_USER_H
#define __DMA_USER_H

void DMA_User_Init(uint32_t AddrA, uint32_t AddrB, uint16_t BuffSize);
void DMA_User_Transfer(void);

#endif

- DMA_User.c

#include "stm32f10x.h"                  // Device header

uint16_t DMA_User_BuffSize;//传输计数器

//DMA初始化-DMA1_Channel1-从AddrA到AddrB转运,不使能
void DMA_User_Init(uint32_t AddrA, uint32_t AddrB, uint16_t BuffSize){
    
    DMA_User_BuffSize = BuffSize;
    
    //1.开启RCC
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    //2.初始化DMA
    DMA_InitTypeDef DMA_InitStructure;
    DMA_StructInit(&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;
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_BufferSize         = BuffSize;//传输计数器
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;//方向:外设作为源端
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Enable;//软件触发
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Normal;//是否使用自动重装
    DMA_InitStructure.DMA_Priority           = DMA_Priority_Medium;//DMA多通道才会用到
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    //3.开关控制使能
    DMA_Cmd(DMA1_Channel1, DISABLE);
    //4.开启硬件触发源(按需求选做)
}

//手动触发一次DMA的转运
void DMA_User_Transfer(void){
    //触发转运
    DMA_Cmd(DMA1_Channel1, DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel1,DMA_User_BuffSize);
    DMA_Cmd(DMA1_Channel1, ENABLE);
    //判断转运完成,并清除相应标志位
    while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
    DMA_ClearFlag(DMA1_FLAG_TC1);
    DMA_Cmd(DMA1_Channel1, DISABLE);
}

编程感想:

  1. DMA不涉及外围电路,所以直接添加在System文件夹中,注意库函数中已经包含了DMA.hDMA.c,所以不要重名。
  2. DMA初始化参数。外设的三个参数和存储器的三个参数名称居然不一样!!!!!!淦。
  3. 变量地址。对一个变量取地址之后,会存放在一个指针变量里,要想调用OLED显示屏函数,还要进行强制类型转换。若不加强制类型转换,就是指针跨级赋值,编译会报错。注意变量的地址都是由编译器决定的,所以并不固定。
  4. const变量。stm32f103c8t6中拥有64KB的Flash、20KB的SRAM,所以有一些不需要更改但又很大的数组,就可以定义为const常量(如字模库等),存储在Flash中,减小SRAM空间占用(防止变成“栈溢出工程师”,哈哈哈哈)。
  5. 访问外设寄存器。可以用结构体方便的访问外设的寄存器,比如ADC的数据寄存器就是ADC1->DR
  6. 库函数中对于地址的定义。介绍一个比较巧妙的点。在代码中,右键ADC1->DR中的“ADC1”,跳转到地址定义。可以看到其定义了一个基地址,并将其强制转换成一个结构体指针(ADC_TypeDef *)。右键跳转进这个结构体,可以发现里面的变量定义顺序与参考手册“11.12.15 ADC寄存器地址映像”中的顺序完全一致,于是便巧妙地利用结构体的顺序定义出“偏移量”。当然上述这一套看起来可能比较麻烦,也可以采用下面这个方法:
#define ADC1_DR (uint32_t *)0x4001244C //定义变量的寄存器地址
*ADC1_DR//然后就可以直接在函数中获取到ADC1的数据寄存器的值了

8.3 实验:DMA+AD多通道-外设到存储器

需求:用ADC的扫描模式完成多通道采集,然后使用DMA完成外设到存储器的数据转运。(与上一节“ADC多通道”现象一致)

图8-10 “DMA+AD多通道”-接线图
图8-11 “DMA+AD多通道”-代码调用(非库函数)

代码展示:
- main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "ADC_User.h"
    
int main(void){
    uint16_t ADC_value[4] = {0,0,0,0};
    //OLED显示屏初始化
    OLED_Init();
    OLED_ShowString(1,1,"C1:+0.00V");
    OLED_ShowString(2,1,"C2:+0.00V");
    OLED_ShowString(3,1,"C3:+0.00V");
    OLED_ShowString(4,1,"C4:+0.00V");
    
    //ADC扫描模式初始化
    ADC_User_InitMuti((uint32_t)&ADC_value,4);
    ADC_User_Start();
    
    while(1){    
        OLED_ShowFloat(1,4,(float)ADC_value[0]*3.3/4095,1,2);
        OLED_ShowFloat(2,4,(float)ADC_value[1]*3.3/4095,1,2);
        OLED_ShowFloat(3,4,(float)ADC_value[2]*3.3/4095,1,2);
        OLED_ShowFloat(4,4,(float)ADC_value[3]*3.3/4095,1,2);
    };
}

- ADC_User.c新增函数

//ADC多通道初始化-ADC1的通道0~3-PA0~PA3共四个通道
void ADC_User_InitMuti(uint32_t AddrB, uint16_t BuffSize){
    //1.开启外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);//6分频使得ADC时钟为12MHz
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    //2.配置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);
    //3.配置ADC多路开关,选择通道进入规则组
    ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_1Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_1Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_1Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_1Cycles5);
    //4.配置ADC转换器
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;//数据右对齐
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;//不使用外部触发(软件触发)
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;//独立模式
    ADC_InitStructure.ADC_NbrOfChannel       = BuffSize;//通道总数(非扫描模式,此参数不起作用)
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//连续转换
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;//扫描模式
    ADC_Init(ADC1, &ADC_InitStructure);
    //5.配置ADC开关控制
    ADC_Cmd(ADC1, ENABLE);
    //6.进行ADC校准
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1)==SET);
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1)==SET);
    //7.开启ADC1硬件触发源
    ADC_DMACmd(ADC1, ENABLE);
    //8.配置DMA
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryBaseAddr     = AddrB;//(uint32_t)ADC_Value;
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_BufferSize         = BuffSize;//传输计数器
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;//方向:外设作为源端
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;//硬件触发
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;//是否使用自动重装
    DMA_InitStructure.DMA_Priority           = DMA_Priority_Medium;//DMA多通道才会用到
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    //9.DMA开关控制使能
    DMA_Cmd(DMA1_Channel1, ENABLE);

}
//记得在头文件中声明

编程感想:

  1. 硬件自动化。传统上CPU控制外设的思路是,由CPU控制所有外设的运行或停止。但通过上面的编程可以发现,当所有的外设初始化完成后,就不需要额外的软件资源,外设之间完全可以相互配合实现自动化工作,这是stm32的一大特色。这样不仅可以减轻CPU负担,还可以大大提升外设的性能。
  2. 卡了很久的Bug,读取到的ADC转换数据都很小。传感器要将AO模拟输出接到GPIO上,而不是DO数字输出!
  3. 卡了很久的Bug,读取到的数据都一样,且变化也相同。DMA的M2M参数选择为“硬件触发”。
  4. 卡了很久的Bug,读取的ADC通道数和预期的错位(比如应该是通道1的电位器数据,却出现在了通道4上)。这是因为在DMA初始化之前就对ADC进行了软件触发。所以结论就是,不管ADC、DMA的初始化的顺序如何,最关键的一步 “ADC软件触发”一定要等ADC、DMA初始化都完成后再进行,也就是一定要放在最后!!
  5. 关于连续触发。编程时有两种思路,一种是直接ADC+DMA初始化完成,给一个ADC的软件触发信号,就可以直接啥都不用管直接在while中显示;另一种是每次进行ADC转换之前,都要进行一次软件触发。值得注意的是,这两种思路和DMA的配置没有任何关系!毕竟ADC的通道转换完成信号是DMA的硬件触发源,所以DMA要设置成自动重装、硬件触发模式就OK。ADC也必须是扫描模式(毕竟多通道),但至于是否 ADC连续转换 就可以看个人的喜好了。
  6. 关于代码架构。本来是想在ADC和DMA两个文件中,分别进行ADC和DMA的初始化。但其实想想,ADC的多通道模式一般就是搭配DMA,而DAM自己再进行一个专门的ADC初始化函数就显得多余,所以直接将DMA的初始化放在ADC多通道模式的初始化函数当中,那是一点问题都没有。而且还能使代码调用关系更加简洁明了。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

stm32学习笔记-8 DMA直接存储器读取 的相关文章

  • STM32F207 I2C 测试失败

    我正在使用 STM32F207 微控制器在 STM3220G EVAL 板上学习嵌入式开发 我尝试通过连接同一芯片上的两个 I2C2 和 I2C3 模块并发送 接收字符来测试 I2C 接口 这是我当前编写的代码 使用 mdk arm 5 i
  • 普冉32位单片机 PY32C642,M0+内核,1.7 V ~ 5.5 V宽工作电压

    PY32C642 单片机采用高性能的 32 位 ARM Cortex M0 内核 宽电压工作范围 嵌入 24Kbytes Flash 和 3 Kbytes SRAM 存储器 最高工作频率 24 MHz 包含多种不同封装类型产品 工作温度范围
  • Jmeter 性能压测-常遇问题与解决技巧

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 2k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • 小学二三年级入门信奥赛,如何从Scratch进入C++的学习

    小学生几年级适宜开始学习C 这是讨论的比较热烈 也是比较热门的话题 小学生适宜几年级开始学C 小学生适宜几年级开始学C CSDN博客 simple happiness 信息学规划 北京二年级学生图形化过二级想往信奥靠拢如何准备 信息学规划
  • 我当年自学黑客(网络安全)的一些心得!(内附学习笔记)

    前 言 写这篇教程的初衷是很多朋友都想了解如何入门 转行网络安全 实现自己的 黑客梦 文章的宗旨是 1 指出一些自学的误区 2 提供客观可行的学习表 3 推荐我认为适合小白学习的资源 大佬绕道哈 文末有福利 一 自学网络安全学习的误区和陷阱
  • 学习STM32正点原子好吗?

    今日话题 学习STM32正点原子好吗 正点原子的教程内容简单明了 代码也清晰直接 使初学者能够轻松理解其功能和使用方法 尤其对于需要快速完成大学作业等任务的大学生来说 可以直接借鉴并稍作修改 便可满足需求 正点原子提供的资料通俗易懂 适合用
  • 【CTF必看】从零开始的CTF学习路线(超详细),让你从小白进阶成大神!

    最近很多朋友在后台私信我 问应该怎么入门CTF 个人认为入门CTF之前大家应该先了解到底 什么是CTF 而你 学CTF的目的又到底是什么 其次便是最好具备相应的编程能力 若是完全不具备这些能力极有可能直接被劝退 毕竟比赛的时候动不动写个脚本
  • 小白也能学会的创建Git仓库实操

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 2k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • SRC漏洞挖掘经验+技巧篇

    一 漏洞挖掘的前期 信息收集 虽然是前期 但是却是我认为最重要的一部分 很多人挖洞的时候说不知道如何入手 其实挖洞就是信息收集 常规owasp top 10 逻辑漏洞 重要的可能就是思路猥琐一点 这些漏洞的测试方法本身不是特别复杂 一般混迹
  • 库函数点亮Led

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 一 pandas是什么 二 使用步骤 1 引入库 2 读入数据 总结 前言 提示 这里可以添加本文要记录的大概内容 例如 随着人工智能的不断发展 机器学习这门
  • 网络安全(黑客)自学

    1 网络安全是什么 网络安全可以基于攻击和防御视角来分类 我们经常听到的 红队 渗透测试 等就是研究攻击技术 而 蓝队 安全运营 安全运维 则研究防御技术 2 网络安全市场 一 是市场需求量高 二 则是发展相对成熟入门比较容易 3 所需要的
  • 跨平台UI自动化框架:Airtest,游戏开发和应用测试的利器

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 3k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • 特殊寄存器

    特殊寄存器 文章目录 前言 一 背景 二 2 1 2 2 总结 前言 前期疑问 STM32特殊寄存器到底是什么 特殊寄存器怎么查看和调试代码 本文目标 记录和理解特殊寄存器 一 背景 最近在看ucosIII文章是 里面提到特殊寄存器 这就进
  • 【GRNN-RBFNN-ILC算法】【轨迹跟踪】基于神经网络的迭代学习控制用于未知SISO非线性系统的轨迹跟踪(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 2 1 第1部分 2 2 第2部分
  • 为什么这么多人自学黑客,但没过多久就放弃了(掌握正确的网络安全学习路线很重要)

    网络安全是一个 不断发展和演变 的领域 以下是一个 网络安全学习路线规划 旨在帮助初学者快速入门和提高自己的技能 基础知识 网络安全的 基础知识 包括 网络结构 操作系统 编程语言 等方面的知识 学习这些基础知识对理解网络安全的原理和技术至
  • 【js学习之路】遍历数组api之 `filter `和 `map`的区别

    一 前言 数组是我们在项目中经常使用的数据类型 今天我们主要简述作用于遍历数组的api filter 和 map 的区别 二 filter和map的共同点 首先 我们主要阐述一下 filter 和 map 的共同点 api的参数都是回调函数
  • STM32 Nucleo 上的上升沿中断多次触发

    我正在使用 STM32 NUCLEO F401RE 微控制器板 我有一个扬声器 经过编程 当向上 向下推操纵杆时 可以按设定的量改变频率 我的问题是 有时 通常 当向上 向下推动操纵杆时 频率会增加 减少多次 这意味着 ISR 正在执行多次
  • 通过JTAG恢复STM32 MCU磨掉的标记

    我有一块可能带有 STM32 MCU 的板 我想为该板制作定制固件 因为库存板有很多问题 不幸的是 电路板制造商很友善地磨掉了所有标记 有没有办法通过 jtag 获取设备 系列 ID 并将其交叉引用到型号 我能找到的一切都是关于获取芯片的唯
  • STM32内部时钟

    我对 STM32F7 设备 意法半导体的 Cortex M7 微控制器 上的时钟系统感到困惑 参考手册没有充分阐明这些时钟之间的差异 SYSCLK HCLK FCLK 参考手册中阅读章节 gt RCC 为 Cortex 系统定时器 SysT
  • PWM DMA 到整个 GPIO

    我有一个 STM32F4 我想对一个已与掩码进行 或 运算的 GPIO 端口进行 PWM 处理 所以 也许我们想要 PWM0b00100010一段时间为 200khz 但随后 10khz 后 我们现在想要 PWM0b00010001 然后

随机推荐

  • Redux的基本使用方法(计数器)

    Redux就像Vuex一样哈哈 可以让组件之间的数据传递变得方便 传统的数据传递非常的麻烦 如果最上层组件要将数据传递给最下层的组件需要逐级传递 组件的耦合度会变得非常的高 要是其中一个组件出现问题的话会导致整个项目出现问题 而Redux是
  • window下搭建zookeeper

    下载zookeeper的压缩包 官网连接 Apache ZooKeeper 我自己用的 apache zookeeper zip 互联网文档类资源 CSDN下载 下载后解压 进到bin目录启动zkServer cmd 参考链接 Zookee
  • Simon IELTS: Speaking

    文章目录 Speaking Lesson 1 General Advice Overall Tips Aims of the course Speaking Lesson 2 Part 1 Introduction to Part 1 Ti
  • [疯狂Java]泛型:泛型的底层原理(类型擦除、原生类型、编译前检查)

    1
  • Vue父子组件通信之父组件主动获取子组件的数据和方法(二)

    父组件主动获取子组件的数据和方法 操作步骤 1 调用子组件的时候定义一个ref
  • ReID行人重识别(训练+检测,附代码),可做图像检索,陌生人检索等项目

    利用ReID和目标检测对视频进行检测 可以对视频中的人进行重识别 支持更换数据集可以做车辆重识别等 可应用于图像 视频检索 行人跟踪等 在以前学习ReID的时候 是跟着下面视频学习的 该论文和代码也可以参考GitHub michuanhao
  • webpack中的loader的配置

    安装babel npm install save dev babel loader babel core 此处如果要用lastest 需要 npm install save dev babel preset latest 配置babel如下
  • Qt--动态链接库的创建和使用

    写在前面 在Qt的实际开发中 免不了使用和创建动态链接库 因此熟悉Qt中动态链接库的创建和使用对后续的库开发或使用是非常用必要的 在之前的文章https blog csdn net SNAKEpc12138 article details
  • Mask Rcnn目标分割-训练数据集-balloon/coco

    本文介绍了Mask Rcnn目标分割算法如何训练自己数据集 对训练所需的文件以及训练代码进行详细的说明 官方提供了coco数据集和balloon数据集两种 可以基于以上数据集进行训练 也可以基于自己采集的数据集进行训练 下面将对两种不同方式
  • VC编程实现IE7 IE8 IE9自动完成口令获取

    都是网上的代码 自己组合起来的 刚开始报很多错误 花了很多时间终于搞定了 环境 VC6 0 SDK SDK必须 简单说明 IE将网站的URL保存于历史文件中 将自动完成的密码保存于注册表中的以下位置 HKEY CURRENT USER So
  • Java基础之 Math UUID Random 随机数

    学习 Demo Math类 random方法获取随机数 UUID类 randomUUID方法生成UUID Random类 实例化一个Random对象创建一个随机数生成器 码上行动 代码如下 示例 import java util Rando
  • Rich Bowen: 无论你在创造什么,最终交付的是信任。

    早在开源被我们称之为开源 Rich Bowen 就已经参与其中 作为 Apache 软件基金会的成员 Rich 目前担任董事会成员 会议副总裁 此外 他还是亚马逊云科技的开源策略师 这些多重角色赋予了他对开源的更广泛和深刻的理解 在他于 2
  • 使用REST JSON XML和JAX-RS构建微服务,大数据(一)

    RESTful API 的基本原理 众所周知 数据库 网站以及业务应用之间都要进行数据交换 这就出现标准的数据格式 传输协议或Web服务 常见的数据格式XML JSON 常见的传输协议 SAOP REST等 开发人员通常都需要为一个应用写A
  • 2022年高教社杯全国大学生数学建模竞赛-【赛题解析篇】D题:气象报文信息卫星通信传输

    解题思路已更新 解题思路参考数模群内大佬提供资料 完整论文已补充 比赛规则及比赛指导大家可移步2022年高教社杯全国大学生数学建模竞赛 比赛规则篇 比赛规则及比赛指导 因为数模国赛期间比较敏感 相关附件数据无法上传 需要赛题及附件数据的可在
  • linux(Debian11)休眠锁屏后无法唤醒

    原文 Debian10 intel核显使用xfce锁屏会黑屏无法唤醒解决方案 Daniel Luo 博客园 intel核显笔记本安装了debian10 使用的是xfce的桌面环境 但是xfce有个bug就是xfce锁屏的时候会出现屏幕黑屏无
  • Linux 下运行.NET 6 7 8 程序遇到的两个问题

    一 lib64 libstdc so 6 version GLIBCXX 3 4 21 not found 的解决办法 1 下载 libstdc so 6 0 21 文件 注意区分x84 64和aarch64架构 下载对应的版本 2 把 l
  • 浙江工商大学python试卷_Python 100 One by One

    一 本课程共48个学时 共有63个教学微视频 3学分 二 教学计划如下 章节 课时安排 第一章 Python环境与操作 3 第二章 数据与表达3 第三章 基本语句应用 3 第四章 字符串3 第五章 组合数据类型 6 第六章 输入与输出6 第
  • Python prometheus_client使用方式

    背景说明 服务部署在阿里云的K8s上 配置了基于Prometheus的Grafana监控 原本用的是自定义的Metrics接口统计 上报一些字段 后面发现Prometheus自带的监控非常全面好用 适合直接抓取统计 所以做了一些改变 Pyt
  • 如何用MATLAB读取csv文件

    如何使用Matlab读取csv文件 在Matlab中 有专门读取csv文件的函数 csvread 在Matlab的帮助文档中 有对这个函数的详细解释 csvread 函数有三种使用方法 1 M csvread filename 2 M cs
  • stm32学习笔记-8 DMA直接存储器读取

    8 DMA直接存储器读取 文章目录 8 DMA直接存储器读取 8 1 DMA简介 8 2 实验 DMA数据转运 存储器到存储器 8 3 实验 DMA AD多通道 外设到存储器 注 笔记主要参考B站 江科大自化协 教学视频 STM32入门教程