STM32快速复习(五)DMA直接存储器读取


前言

DMA(Direct Memory Access)直接存储器存取 :配合上一章ADC的转换器,可以达到ADC转换的数据利用DMA转运到其他地址。DMA配合其他外设效果为锦上添花,个人感觉配合ADC是不可或缺。

软件触发应用场景:数据源中的数据已确定。如将FLASH中的数据转运SRAM中(存储器–>存储器),一次触发后会将数据以最快的速度全部转运完毕。

硬件触发应用场景:数据源中的数据没有全部确定,需要在特定的时机转运数据。如将ADC数据转运到存储器(外设寄存器–>存储器),等对应ADC通道的数据转换完成后,才硬件触发一次DMA转运数据。

-以上针对复习。
软件触发:最快的速度干完,数量明确。
硬件触发:有一次请求触发一次,数量未知。


提示:以下是本篇文章正文内容,下面案例可供参考

一、 DMA简介

DMA(Direct Memory Access)直接存储器存取 可以直接访问 STM32 内部的存储器,包括外设寄存器(一般指外设的数据寄存器DR,如ADC的数据寄存器、串口数据寄存器等)、运行内存SRAM(存储运行变量)、程序存储器FLASH(存储程序代码)等。

DMA可以提供 外设寄存器和存储器 或者 存储器和存储器之间 的高速数据传输,无须CPU干预,节省了CPU的资源。
翻译成人话就是,是一个数据转运小助手,主要用来协助CPU完成数据转运的工作。

下面是stm32中DMA的一些配置:

stm32系列芯片共有12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)。
STM32F103C8T6型号的DMA资源:DMA1(7个通道)。

二、 存储器映像

下面介绍“存储器映像”。计算机有五大组成部分:运算器、控制器、存储器、输入设备、输出设备。计算机的核心关键部分就是CPU和存储器,上述的运算器和控制器会合在一起组成CPU,而存储器主要关心存储器的内容地址。下图给出了stm32中都有哪些存储器,以及这些存储器的地址都是什么,即 “存储器映像”,下表则对下图进行了一个简单的总结:
在这里插入图片描述
因为STM32有32位,最多4G空间,但是F103C8T6内存极小,一般KB。导致99%地址均为空白。
地址总结如下;
在这里插入图片描述
(1)程序存储器FLASH:下载程序的位置,程序一般也是从主闪存里开始运行。若某变量地址为0x0800_xxxx,那么它就是属于主闪存的数据。
(2)系统存储器:存储BootLoader程序(俗称“刷机”),芯片出厂时自动写入,一般不允许修改。
(3)选项字节:存储的主要是FLASH的读保护、写保护、看门狗等配置。下载程序可以不刷新选项字节的内容,从而保持相应配置不变。
(4)运行内存SRAM:在程序中定义变量、数组、结构体的地方,类似于电脑的内存条。
(5)外设寄存器:初始化各种外设的过程中,最终所读写的寄存器就属于这个区域。

DMA的硬件框图(仅了解)
在这里插入图片描述

下图,官方自带的,知道请求的信号来自于那一部分即可。原理简化如下
在这里插入图片描述
上图是DMA1的请求映像,所以有7个通道,每个通道都有一个数据选择器,可以选择硬件触发/软件触发:
1.EN位:其实就是DMA的开关控制。
2.软件触发需要M2M位置1,硬件触发则需要M2M位置0。
3.硬件触发源:每个通道的硬件触发源各有不同。如ADC1触发必须选择通道1、TIM2更新时间触发必须选择通道2…… 通道的选择由相应的外设库函数决定,如ADC库函数ADC_DMACmd用于开启通道1、TIM库函数TIM_DMACmd可以开启通道2(TIM2_UP)。理论上可以同时开启同一通道的多个触发源,但一般只开启一个。
4.软件触发通道可以任意选择。

原理简化如下
在这里插入图片描述
左右两个站点是互相传输数据的两方,一方的数据传输从起始地址开始以一次数据宽度的间隔将数据传输给另一方的起始地址数据宽度进行保存。传输完一次,是否地址自增换地址。
举例:物流,十个仓库的货发给另一个城市。
A城市的货物以一次一个大货车的速度给B城市第一个仓库。
AB城市的第一个仓库是双方起始地址,大货车载重是数据宽度。第一个仓库放满了换第二个仓库。所以地址需要自增。(如果A城市所有的货物都运往第一个仓库,那么A就无需地址自增)
传输计数器:大货车数量。软硬件触发(货物是否满足运输条件,全部满足就是赶紧发,没满足就等有满足的信号再发)
总结:物流
TIPS:数据宽度只有uint8_t、uint16_t、uint32_t;
如果传输数据宽度接受和发送无法不一样;
在这里插入图片描述
基本原则就是:
源端宽度<目的宽度:在目的宽度的高位补零。
源端宽度>目的宽度:按照目的宽度,只保留源端的低位,多余的高位全部舍弃。
上面的过程类似于uint8_t、uint16_t、uint32_t之间的相互赋值,不够就补零,超了就舍弃高位。

举例(常用)
在这里插入图片描述
例1:DMA数据转运
任务是将SRAM数组DataA转运到另一个SRAM数组DataB(存储器到存储器)。

下面给出各参数的配置说明:
1.两个站点的参数:外设地址->DataA数组首地址、存储器地址->DataB数组首地址;数据宽度都是8位;为保证数据的一一对应,“外设”和“存储器”都设置为地址自增。
2.“方向”参数:默认是“外设”–>“存储器”,当然也可以将方向反过来。
3.传输计数器:数组大小为7,所以计数器为7。
4.自动重装:不需要。
5.触发源:软件触发。由于是“存储器->存储器”的触发,所以不需要等待转运时机。
6.开关控制:最后调用DMA_Cmd开启DMA转运。
注:上述为“复制转运”,转运完成后DataA数据不会消失。
在这里插入图片描述
例2:ADC扫描模式+DMA
期望将外设ADC多通道(扫描模式)的数据,依次搬运到SRAM中的ADValue数组中(外设到存储器)。
注意下图左侧给出了ADC的扫描模式示意图,ADC每触发一次,7个通道依次进行ADC数据转换,每个通道转换完成时,都会将转换结果放到ADC_DR数据寄存器中,也就是ADC的所有通道共用一个ADC数据寄存器。所以每个通道转换完成后,都需要DMA立即转运一次,防止新来的通道数据将之前的通道数据覆盖掉。

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

一般来说,
DMA最常见的用途就是配合ADC的扫描模式。因为ADC的扫描模式有数据覆盖的特征,或者说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使得ADC和DMA成为了最常见的伙伴。ADC对DMA的需求非常强烈,其他外设使用DMA可以提高效率,属于是锦上添花的操作,不使用DMA顶多只是损失一些性能;但是ADC的扫描模式如果不使用DMA,功能都会受到很大的限制。所以ADC和DMA的结合最为常见。
更多关于DMA的详细内容可以查阅参考手册“2 存储器和总线构架”、“10 DMA控制器(DMA)”。

三、使用步骤

1.引入库

DMA_DeInit :将DMA恢复到默认配置。
DMA_Init 【必需】:DMA初始化。
DMA_StructInit :给DMA的初始化结构体赋一个初值。
DMA_Cmd 【必需】:DMA的开关控制。
DMA_ITConfig :DMA的中断输出使能,用于开启DMA中断。
DMA_SetCurrDataCounter :设置DMA计数器。
DMA_GetCurrDataCounter :获取当前DMA的计数器值。
DMA_GetFlagStatus :主函数,获取DMA的各种标志位。
DMA_ClearFlag :主函数,清除DMA各种标志位。
DMA_GetITStatus :中断函数,获取DMA的各种标志位。
DMA_ClearITPendingBit :中断函数,清除DMA的各种标志位。

2.读入数据

代码如下(示例):

- 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.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.h、DMA.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的数据寄存器的值了

这一章很简单,只是传输规则拗口复杂,看懂代码,敲一敲,通过编程去理解是最理想的状况。

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值