STM32

发布于:2025-07-25 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

#DMA直接存储器存取#

1.DMA简介

DMA数据转运有俩个要注意:

转运传输方向细写:

2.存储器/寄存器映像

2.1.计算机系统五大组成部分:

2.2寄存器/存储器映像

2.2.1存储器和寄存器区别

2.2.2大框架讲明白寄存器映像(通俗易懂版)

2.2.3大框架讲明白存储器映像(通俗易懂版) 

2.2.4存储器/寄存器映像联系

 3.寄存器映像作用:

4.存储器映像重要性

4.1.ROM细分:

4.2.RAM细分:

5.存储器图

5.1.核心概念:存储器图 = 单片机的“地址地图”

5.2.为什么存储器图对DMA和STM32编程如此重要?

5.3.STM32典型存储器图最重要的区域

5.4.DMA、ADC、SRAM在存储器图上的位置与交互

5.5.如何获取你芯片的精确存储器图?

6.DMA框图

6.1根据框图讲解

6.2.DMA1框图

7.DMA基本结构

7.1.外设和存储器俩个站点分别有三个参数控制转运方向

7.2.DMA结构解读

7.3.DMA进行转运的条件:

8.DMA请求

9.数据宽度与对齐

9.1.数据宽度

9.2.地址对齐 (Address Alignment):

9.3.关键原则:

10.数据转运+DMA

10.1配置要点:

10.2根据图片说参数

11.ADC扫描模式+DMA

11.1ADC扫描模式 (Scan Mode) 回顾:

11.2.DMA 如何完美解决:

11.3.工作流程详解:

11.4.根据图片说参数

12.DMA数据转运

12.1.初始化DMA

12.2.DMA库函数

12.3.DMA.c

12.4main.c

12.5数组A数据改变,数组B数据跟着改变

DMA.c(修改)

main.c

12.6.ADC单次扫描+DMA单次转运模式

AD_DMA.c

头文件修改

主函数修改

12.7ADC连续扫描+DMA循环转运模式


#DMA直接存储器存取#

1.DMA简介

  • DMADirect Memory Access)直接存储器存取

DMA可直接访问STM32内部存储器(运行内存SRAM,程序存储器Flash和寄存器等)

  • DMA可以提供外设(外设寄存器,一般是外设数据寄存器DR——ADC数据寄存器,串口数据寄存器)和存储器(运行内存SRAM和程序存储器Flash——存储变量数组和程序代码)或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源(数据转运助手)

DMA数据转运有俩个要注意:

①源 (Source): 数据从哪里来?可以是:

  • 外设的数据寄存器
  • 内存地址(SRAM中的变量、数组、缓冲区)。

②目标 (Destination): 数据到哪里去?同样可以是:

  • 外设的数据寄存器。
  • 内存地址。

转运传输方向细写:

外设到内存 (Peripheral-to-Memory): 最常见!如ADC采集数据到内存数组,串口接收数据到接收缓冲区。

内存到外设 (Memory-to-Peripheral): 也很常见!如内存数组发送到串口,内存中的波形数据发送到DAC。

内存到内存 (Memory-to-Memory): DMA可以直接在内存的两个区域之间搬数据(某些型号的DMA支持,如STM32F4/F7/H7)。非常高效!

外设到外设 (Peripheral-to-Peripheral): 相对少见,比如将一个定时器的捕获/比较寄存器值直接搬到另一个定时器的寄存器。需要特定外设组合支持。

  • 12个独立可配置的通道(数据转运路径): DMA17个通道), DMA25个通道)
  • 每个通道都支持软件触发和特定的硬件触发

若进行存储器到存储器的数据转运,比如将Flash的一些数据转运到SRAM中,需要软件触发(快速,同一时间量大)

若进行外设到存储器的数据转运,由于外设数据有一定时机,不可同一时间大量转运,需要硬件触发(比如转运AD数据)

  • STM32F103C8T6 DMA资源:DMA17个通道)

2.存储器/寄存器映像

2.1.计算机系统五大组成部分:

运算器+控制器)=(CPU),存储器输入设备输出设备

2.2寄存器/存储器映像

2.2.1存储器和寄存器区别
特性 寄存器 存储器 (主要指主存/RAM)
位置 CPU内部 CPU外部
速度 最快 (1-几个时钟周期) 慢得多 (几十到几百时钟周期)
容量 极小 (几十到几百字节) 极大 (GB级别)
主要用途 存储CPU当前操作数、中间结果、指令、地址、状态 存储运行的程序代码和大量数据
CPU操作 指令直接操作寄存器 数据需加载到寄存器才能被操作
成本/位 最高 较低
访问方式 通过寄存器名 通过内存地址

在计算机的存储层次结构中,寄存器位于最高层(最快、最小、最贵),紧接着是高速缓存(Cache),然后是主存储器(Main Memory/RAM),最后是辅助存储器(如硬盘、SSD、光盘等,最慢、最大、最便宜)。这个层级结构是为了在速度和容量/成本之间取得最佳平衡。

简单来说,寄存器是CPU的贴身工作空间,用于处理即时任务;存储器是CPU的大容量数据仓库,用于存放程序和数据。 两者协同工作,共同支撑计算机的运行。

2.2.2大框架讲明白寄存器映像(通俗易懂版)

①想象一个大楼:STM32芯片

整个STM32芯片就像一栋超级智能写字楼:

  • 楼层:内存(SRAM)、闪存(Flash)、各种外设(DMA、串口、ADC等)都在这栋楼里。
  • 房间号:每个房间有唯一门牌号(这就是内存地址)。

②关键角色:外设的「控制面板」(寄存器)

  • 每个外设(比如DMA、串口)都有一个自己的「控制面板」(就像奶茶店的点单台、银行的柜台)。
  • 面板上有很多按钮和指示灯(这些就是寄存器):
  • 按钮:你按一下,设备就执行操作(比如「启动DMA」按钮)。
  •  指示灯:亮起来告诉你设备状态(比如「DMA传输完成」指示灯)。

③寄存器映像:给每个「按钮/灯」分配门牌号!

寄存器映像的核心就一句话:

把DMA控制面板上的「启动按钮」、「状态指示灯」…… 每一个物理的按钮和灯,都分配一个这栋大楼里的「房间号」(内存地址)!

举个例子(简化版):

  • DMA启动按钮 👉 分配房间号: 0x40026008
  • DMA传输完成指示灯 👉 分配房间号: 0x40026000
  • 串口发送数据槽 👉 分配房间号: 0x40011004

💡 重点:这些地址是芯片设计时就定死的! 查手册就像查大楼的《房间号分配表》。

④ CPU/DMA 如何操作?「打电话」!

CPU 或 DMA 控制器想操作这些按钮/灯怎么办?它们不会亲自跑过去,而是:

1.「打电话」: 它们知道要操作的按钮/灯的房间号(地址)。

2.「发指令」:

  • 写操作(按按钮): 对着电话说:“往地址 0x40026008 写数字 1!” ➡️ 相当于按下了「DMA启动按钮」。
  • 读操作(看指示灯): 对着电话说:“看看地址 0x40026000 的值是多少?” ➡️ 如果返回值是 1,就说明「DMA传输完成指示灯」亮了!

⑤ 头文件——《通讯录》

手动记这些地址 (0x40026008, 0x40026000...) 太反人类了!所以ST提供了头文件 (如 stm32fxxx.h),它本质上是一个超全的《房间号通讯录》

⑥ 与 DMA 的关系

当你在配置DMA时:

  1. 告诉DMA「去哪取东西」: 你其实是在把串口数据槽的房间号 (USART_DATA_SLOT) 写到DMA自己的一个配置寄存器(比如叫 SOURCE_ADDR)里。你写的是地址 0x40011004。
  2. 告诉DMA「放东西到哪」: 你把内存中数组的地址 (如 0x2000C000) 写到DMA的另一个配置寄存器(比如叫 DEST_ADDR)里。
  3. 按下「启动按钮」: 你向地址 DMA_START_BUTTON (0x40026008) 写 1。

DMA控制器收到指令后:

  • 它知道要去 SOURCE_ADDR 里存的地址 (0x40011004) 取数据(从串口的数据槽拿)。
  • 它知道要把数据放到 DEST_ADDR 里存的地址 (0x2000C000) 存数据(放到内存数组里)。
  • 它知道要搬多少块(你设定的数量)。
  • 它就开始一趟趟跑了!跑完了,就把 DMA_DONE_LIGHT 地址 (0x40026000) 对应的灯点亮(状态寄存器置位)。

⑦总结「寄存器映像」

就是把STM32芯片里,所有硬件设备(DMA、串口等)的每一个控制开关(寄存器),都给它分配一个唯一的「房间号」(内存地址)。CPU或DMA想操作哪个开关,不用知道它在哪个车间、流水线上,直接对着这个开关的「房间号」发指令(读/写数据)就行了!头文件就是帮你记住这些房间号名字的《通讯录》。

2.2.3大框架讲明白存储器映像(通俗易懂版) 

①核心比喻:STM32 的「国土规划图」

想象 STM32 芯片内部是一片辽阔的国土,总地址空间从 0x0000 0000 到 0xFFFF FFFF (共 4GB)。

存储器映像就是这片国土的《详细规划地图》,它严格规定了:

从地址 A 到地址 B 的这片“土地”归谁用、用来做什么?是建“指令仓库”(Flash)、还是“临时仓库”(RAM)、或是“外设控制站”(Peripheral Registers)?

②存储器映像的核心:划分「功能区域」

STM32 (以 Cortex-M 为例) 的国土被规划成几个主要功能大区,每个大区占据连续的地址范围:

地址范围 (典型) 区域名称 用途描述 访问速度 类比
0x0000 0000 别名区 / 重映射起点 一个神奇的门! 芯片设计让它能“瞬间传送”到 FlashSRAM 或 System 的实际起点。配置选项决定它通向哪里 (通常默认通向 Flash)。 - 国土的“任意门”
0x0800 0000 Flash 存储器区 存放你编写的程序代码 (Instructions)、常量数据 (const) 和中断向量表。掉电不丢失。 ⚡ 快 国家图书馆 + 宪法档案馆 (永久存储核心知识)
0x2000 0000 SRAM 存储器区 存放程序运行时的变量、数组、堆栈(Stack)、堆(Heap) 等临时数据。掉电丢失。 ⚡⚡ 很快 中央物流仓库 + 临时工棚 (运行时的核心工作区)
0x4000 0000 外设寄存器区 (APB) 这里就是 寄存器映像的“家”! 所有低速外设 (USART2/3, I2C, SPI2, TIM2-7...) 的控制开关 (寄存器) 都映射在这片区域 (0x4xxxxxxx)。 🐢 较慢 低速工业区控制中心
0x5000 0000 外设寄存器区 (AHB) 所有高速外设/核心外设 (GPIO, DMA, CRC, RCC, FLASH 接口...) 的控制开关 (寄存器) 都映射在这片区域 (0x5xxxxxxx)。 ⚡ 快 高速核心控制中心
0xA000 0000 FSMC/FMC 区 (扩展) 用于连接外部存储器 (如 SRAM, NOR Flash, LCD 控制器)。地址由片选信号决定。 🚗 可变 对外贸易港口
0xE000 0000 Cortex-M 内核区 存放内核本身的私有外设寄存器 (如 NVIC 中断控制器, SCB 系统控制块, SysTick, MPU)。 ⚡ 快 中央政府直辖特区

③CPU / DMA 如何看待存储器映像?

对 CPU (Cortex-M 内核) 和 DMA 控制器来说:

  1. 统一编址: 它们眼里没有“这是 Flash”、“那是 RAM”、“那是串口寄存器”的概念。它们只认地址

  2. 发起访问:

    • 取指令: CPU 从 0x0800 0000 (假设 Flash 映射在别名区) 读 -> 硬件自动去 Flash 物理存储器 取指令。

    • 读写变量: CPU 写 0x2000 0000 -> 硬件自动去 SRAM 物理存储器 写入数据。

    • 控制外设: CPU 写 0x4001 100C (USART1->CR1) -> 硬件自动通过总线,将数据写入 USART1 外设内部的 CR1 物理寄存器电路

    • DMA 搬运: DMA 被告知源地址 0x2000 C000 (SRAM 数组),目标地址 0x4001 1004 (USART1->TDR) -> DMA 控制器分别向这两个地址发起读写操作,硬件自动路由到正确的物理位置 (SRAM 和 USART1 的寄存器)。

  3. 硬件路由: 芯片内部的 总线矩阵 (Bus Matrix) 和 存储器控制器 就像国土管理局 + 交通部,它们根据 CPU/DMA 发出的目标地址,自动判断这个地址属于哪个功能区:

    • 地址在 0x0800 0000 - 0x08xx xxxx? -> 路由到 Flash 控制器

    • 地址在 0x2000 0000 - 0x20xx xxxx? -> 路由到 SRAM 控制器

    • 地址在 0x4000 0000 - 0x5FFF FFFF? -> 路由到 AHB/APB 总线系统,再根据具体地址找到对应的外设寄存器。

 

2.2.4存储器/寄存器映像联系

关键点 :寄存器映像是存储器映像的一部分!

  • 之前讲的 USART1->TDR (0x40011004), DMA1->S0CR (0x40026010) 这些外设寄存器地址,就位于 0x4000 0000 - 0x5FFF FFFF 这片“外设控制站”大区里!它们是存储器映像中为“硬件开关”专门划出的地皮

  • 存储器映像 定义了 0x40000000 这块地是给外设寄存器用的。

  • 寄存器映像 定义了这块地里面,0x40011004 这个具体位置放的是 USART1 的 TDR 开关,0x40026010 放的是 DMA1 流 0 的 CR 开关。


 与寄存器映像的关系:国土规划 vs 门牌分配

  • 存储器映像 (Memory Map):

    • 宏观规划: 相当于国家颁布《国土功能区划法》。

    • 规定: 0x08000000 - 0x080FFFFF 这片地只能建图书馆 (Flash);0x20000000 - 0x2001FFFF 这片地只能建物流仓库 (SRAM);0x40000000 - 0x400FFFFF 这片地划为“低速工业区控制中心用地” (APB 外设)。

    • 回答: 某个大块地址范围是用来做什么的?

  • 寄存器映像 (Register Map):

    • 微观执行: 相当于在“工业区控制中心用地” (0x40000000 开始) 内部,给每一个工厂 (外设) 的每一个控制按钮 (寄存器) 分配具体的门牌号

    • 规定: “STM 电机厂” (USART1) 的“启动按钮” (CR1 寄存器的 UE 位) 放在 0x4001100C 这个门牌号;“转速表” (ISR 寄存器的 TXE 位) 放在 0x4001101C

    • 回答: 某个具体地址对应哪个外设的哪个开关?


总结:存储器映像 vs 寄存器映像

特性 存储器映像 (Memory Map) 寄存器映像 (Register Map)
层级 宏观顶层规划 微观底层实现
作用范围 整个 4GB CPU 地址空间 (0x00000000 - 0xFFFFFFFF) 仅针对 外设寄存器区 (主要是 0x4xxxxxxx 和 0x5xxxxxxx)
回答的问题 地址 X 属于哪个功能区? (Flash? RAM? 外设? 内核?) 地址 Y 对应哪个外设的哪个具体寄存器/寄存器位?
类比 国家国土功能区划图 (划分省、市、工业区、农业区、特区) 城市门牌号分配表 (XX路YY号是张三家的厨房电灯开关)
依赖关系 寄存器映像建立在存储器映像划定的“外设区”之上 是存储器映像在“外设区”内的具体细化
开发者关注点 链接脚本 (.ld), 变量地址,DMA 源/目标区域 外设驱动开发,操作 Peripheral->Register
手册位置 芯片参考手册 Memory Map 章节 芯片参考手册 Peripheral Registers 章节 (每个外设单独一节)

存储器映像 把 STM32 的 4GB 地址空间划分成几个功能大省 (Flash省、RAM省、外设省...)。寄存器映像 则是在 “外设省” 内部,给每个外设工厂的每个控制按钮钉上了精确的门牌号。CPU 和 DMA 通过地址这个“GPS坐标”访问全国,硬件会根据坐标自动导航到正确的省份、找到正确的工厂、按下正确的按钮。 

 3.寄存器映像作用:

1.统一访问方式: CPU 使用相同的 Load/Store 指令(在 C 语言中体现为指针操作或直接变量访问)来读写内存和操作外设寄存器,硬件设计变得统一和高效。

2.标准化编程: 开发者只需要知道寄存器的内存映射地址,就可以用 C 语言(通过指针)或汇编语言来读写它们,控制硬件行为。

3.地址唯一性: 确保每个寄存器都有一个唯一的地址,避免访问冲突。

4.存储器映像重要性

  1. 硬件的统一接口: CPU/DMA 只需发出地址和读写命令,复杂的“找谁干活”由硬件自动完成。

  2. 软件的可预测性: 开发者知道:

    • 我的代码肯定在 0x080x xxxx

    • 我的全局变量肯定在 0x200x xxxx

    • 我想控制串口1,就去 0x4001 1xxx 找它的寄存器。

  3. 编译器和链接器的基石: 它们根据存储器映像 (在链接脚本 .ld 文件中定义) 决定:

    • 代码段 (.text) 放到 Flash 区 (0x08000000)。

    • 已初始化数据 (.data) 从 Flash 复制到 SRAM (0x20000000)。

    • 未初始化数据/堆栈 (.bss.stack.heap) 在 SRAM 中分配空间 (0x20000000 + offset)。

  4. DMA 配置的依据: 配置 DMA 搬运时,源地址和目标地址必须准确指向存储器映像中有效的区域

    • 想从 ADC 读数?目标地址填 SRAM 区地址 (0x200xxxxx)。

    • 想用串口发送?源地址填 SRAM 区地址 (0x200xxxxx),目标地址填串口 TDR 的寄存器地址 (0x4001xxxx)。

    • 想内存复制?源和目标都填 SRAM 区地址 (如果 DMA 支持 Mem-to-Mem)。

存储器总共分成两大类:ROM和RAM。

ROM :只读存储器,非易失性、掉电不丢失存储器;

RAM:随机存储器,易失性、掉电丢失存储器。

4.1.ROM细分:

程序存储器flash(主闪存)。用途为存储C语言编译后的程序代码,也是下载程序的位置,运行程序一般从主闪存里开始运行。被分配的起始地址是0x0800 0000,其剩余字节的地址依次增长,每个字节都被分配特定地址。终止地址取决于其容量。

系统存储器。存储介质是Flash,起始地址0x1FFF F000。用途是存储BootLoader,用于串口下载。BootLoader程序存储的位置被分配到起始位置。BootLoader程序是芯片出厂自动写入的,一般不允许修改。

选项字节。存储介质是Flash,起始地址0x1FFF F800。用于存储一些独立于程序代码的配置参数。其位置在ROM区的最后面。下载程序可以不刷新选项字节的内容,因此选项字节的配置可以保持不变。选项字节主要存储flash的读保护,写保护和看门狗等配置。

系统存储器和选项字节这两块存储器的位置是在ROM区的最后面

4.2.RAM细分:

运行内存SRAM。起始地址0x2000 0000,用于存储运行过程中临时变量(定义变量、数组、结构体的地方)。

外设寄存器,起始地址0x4000 0000,用于存储各个外设配置参数(初始化各个外设最终读写的东西),外设寄存器也是存储器的一种。存储介质为SRAM。一般习惯运行内存叫SRAM,外设寄存器叫寄存器。

内核外设寄存器,起始地址0xE000 0000,用于存储内核各个外设的配置,参数内核外设是NVIC和SysTick。

5.存储器图

5.1.核心概念:存储器图 = 单片机的“地址地图”

想象单片机的整个可寻址空间是一个巨大的、连续的国家。这个国家被划分成不同的省份和城市,每个区域有明确的边界(地址范围)和特定的功能:

①地址 (Address): 每个可以访问的位置(字节、半字、字)都有一个唯一的数字编号。STM32通常是 32位 系统,因此寻址范围是32位范围,地址范围从 0x0000 0000 到 0xFFFF FFFF (最大4GB,STM32存储器是KB级别,图片中,灰色区域就是没用到)。

②存储器图 (Memory Map): 定义了在这4GB的地址空间中:

1)哪些地址范围 对应 什么类型的物理设备或功能 (如 Flash, SRAM, 外设寄存器, 系统组件)。

2)对这些地址进行 读/写操作 会产生什么效果(访问内存数据?配置外设?触发系统异常?)。


5.2.为什么存储器图对DMA和STM32编程如此重要?

①DMA配置源地址和目标地址: 当你配置DMA时,它们必须指向 有效的、存在的、且有正确访问权限 的地址。存储器图告诉你:

1)Flash 在哪个地址范围?(0x0800 0000 开始)

2)SRAM 在哪个地址范围?(0x2000 0000 开始)

3)某个外设(如ADC1)的控制寄存器、数据寄存器(如 ADC1->DR)在哪个地址范围?(0x4001 2000 到 0x4001 23FF 之类,具体查手册)

这些地址是 物理上真实存在 的硬件资源的入口点

②理解外设寄存器访问: 配置外设(如ADC、UART、TIMER)就是通过读写位于特定地址(由存储器图定义)的寄存器来实现的。例如,使能ADC的DMA请求,就是往ADC控制寄存器(ADCx->CR2)的某个位写1,这个寄存器在存储器图上有一个固定的地址。

③地址对齐要求: 访问不同宽度的数据(字节、半字、字)要求地址满足特定的对齐规则。存储器图本身不强制对齐,但访问不满足对齐规则的地址可能导致硬件错误或数据错误(具体行为取决于硬件)。

④链接脚本 (Linker Script): 编译器/链接器需要根据存储器图来决定把你的程序代码(.text)、常量数据(.rodata)、初始化变量(.data)、未初始化变量(.bss)放到哪个物理存储区域(通常是Flash和SRAM)。DMA操作的目标地址通常就是SRAM中的变量或数组地址。

5.3.STM32典型存储器图最重要的区域

1.片上 Flash 存储器 (Code Memory):

①地址范围: 通常是 0x0800 0000 到 0x080X XXXX (X取决于Flash大小)

②功能: 存储程序代码(.text)、常量数据(.rodata)、以及初始化的全局/静态变量(.data的初始值)。

③访问: 通过 ICode / DCode 总线由CPU读取执行指令。DMA 通常不能直接读写Flash(需要特殊操作如Flash控制器)。DMA的目标地址一般不指向Flash!

④别名: 地址 0x0000 0000 开始的一段空间是Flash的别名或者系统存储器,取决于BOOT引脚(Boot0和Boot1)。程序从0地址开始运行,因此要将需要执行地程序映射到0地址。若映射到Flash区,就是从Flash执行;若映射在系统存储器区,就从系统存储器运行BootLoader……

2.片上 SRAM 存储器 (Data Memory):

①地址范围: 通常是 0x2000 0000 到 0x200Y YYYY (Y取决于RAM大小)

②功能: 存储程序运行时的数据:

  • 栈 (Stack): 函数局部变量、函数调用返回地址等。
  • 堆 (Heap): 动态分配的内存 (malloc, new)。
  • 全局/静态变量 (.data 的运行时副本, .bss)。
  • 自己编写的DMA目标数组! uint16_t adc_results[10]; 这个数组就存放在SRAM的某个地址(比如 0x2000 0100)。

③访问: CPU通过系统总线读写。DMA的主要操作对象! DMA的源地址或目标地址经常是SRAM中的某个位置(变量、数组、缓冲区)。

④别名: 0x1000 0000 开始的区域是SRAM的别名。

3.片上外设寄存器 (Peripheral Registers):

①地址范围: 0x4000 0000 到 0x400F FFFF (APB1, APB2 外设), 0x5000 0000 到 0x500F FFFF (AHB1, AHB2 外设) 等

②功能: 这是配置和控制所有片上外设(GPIO, USART, SPI, I2C, ADC, TIMER, DMA控制器本身等) 的地方。每个外设都有一组连续的寄存器。

③访问: 通过 AHB / APB 总线进行读写。DMA操作的关键源地址或目标地址! 例如:

  • DMA源地址: 可能是 0x4001 2000 + 0x4C = 0x4001 204C (假设是 ADC1->DR 数据寄存器的地址)。
  • DMA目标地址: 可能是 0x4000 3800 + 0x04 = 0x4000 3804 (假设是 USART1->TDR 发送数据寄存器)。

④如何知道地址? 必须查阅芯片的 参考手册 或 头文件 。

头文件里定义了宏,如 #define ADC1 ((ADC_TypeDef *) ADC1_BASE)ADC1基地址,#define ADC1_BASE (APB2PERIPH_BASE + 0x2000),#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000),#define PERIPH_BASE 0x40000000UL。所以 &ADC1->DR 最终会被计算为 0x40000000 + 0x10000 + 0x2000 + 0x4C = 0x4001204C。

4.Cortex-M 内核外设 (Core Peripherals):

①地址范围: 0xE000 0000 到 0xE00F FFFF (系统控制块SCB, 嵌套向量中断控制器NVIC, SysTick定时器, 调试组件MPU/FPU等)。

②功能: 控制内核本身的行为(中断、异常、系统定时、内存保护等)。

③访问: 通过私有外设总线。DMA一般不直接操作这里。

5.4.DMA、ADC、SRAM在存储器图上的位置与交互

1.物理位置: Flash、SRAM、外设寄存器块、内核外设块都是物理上独立的硬件模块。

2.地址映射: 存储器图通过总线矩阵将这些物理模块映射到统一的4GB线性地址空间的不同区域。给一个地址,总线矩阵就知道该访问哪个物理设备。

3.DMA操作: DMA控制器也连接到总线矩阵。当DMA工作时:

  • 它根据你配置的源地址,通过总线矩阵找到对应的物理模块(如ADC的DR寄存器),从中读取数据。
  • 它根据你配置的目标地址,通过总线矩阵找到对应的物理模块(如SRAM的某个单元),将数据写入。

4.外设寄存器即地址: 配置外设(如设置ADC为扫描模式、使能DMA请求)就是CPU(或库函数)向外设寄存器块中特定地址(如 ADC1->CR2 对应的地址)写入特定的比特值。DMA操作外设寄存器(如读取 ADC1->DR)也是同理,只是由DMA硬件自动完成读操作。

5.你的变量在哪里? 当你定义 uint16_t adc_results[10];:

  • 这个数组的内容存储在 SRAM 的某个连续区域(比如从 0x2000 0100 到 0x2000 0113)。
  • 变量名 adc_results 在程序里代表的就是这个数组在 SRAM中的首地址 (0x2000 0100)。
  • 当你把 &adc_results[0] (即 0x2000 0100) 作为DMA的MemoryBaseAddr时,DMA就知道该把数据搬运到SRAM的这个地方。

5.5.如何获取你芯片的精确存储器图?

1.必须查阅官方文档:

芯片参考手册 (Reference Manual RMxxxx):

查找 “Memory Map” 或 “Memory Organization” 章节。

这里会提供最详细、最权威的地址范围划分,包括所有Flash、SRAM、外设寄存器块、引导存储器、别名区等的精确起始和结束地址。

2.数据手册 (Data Sheet DSxxxx): 会简要列出主要的存储器大小和地址范围(Flash大小,SRAM大小)。

3.头文件: 定义了所有外设寄存器结构体和基地址宏。通过查看这些宏定义,你可以知道具体外设寄存器的地址是如何计算出来的。

6.DMA框图

6.1根据框图讲解

内核包含CPU和内核外设,其他都可看成存储器。

框图可看成CPU和存储器俩个东西


Flash是主闪存,SRAM是运行内存,各个外设都可以看成是寄存器,也是一种SRAM存储器,因为寄存器是一种特殊的存储器。

一方面,CPU可以对寄存器进行读写,就像读写运行内存。另一方面,寄存器的每一位背后都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器或者多位结合起来,当做计数器,数据寄存器等。

因此,寄存器是连接软件和硬件的桥梁


由于外设=寄存器 寄存器=存储器,DMA进行数据转运就可以归纳为从一个地址取内容到另一个地址去。

由图看出,数据总线左边是主动单元(拥有存储器的访问权),右边是被动单元(存储器只可以被左边主动单元读写)

主动单元:内核有Dcode(专门访问Flash)和系统总线,可访问右边存储器。

DMA需要转运数据,因此也需要访问主动权。主动单元还包括DMA总线。在DMA1和DMA2里面可以看到DMA1有七个通道,,DMA2有五个通道,各个通道可以分别设置转移数据的原地址和目的地址。


仲裁器

①DMA内部:虽然多个通道可以独立转运数据,但是最终DMA总线只有一条。所以所有的通道都只能分时复用这一条DMA总线,如果产生了冲突,就会由仲裁器根据通道的优先级决定谁先用谁后用。

②总线矩阵内部:如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。但总线仲裁器仍然会保证CPU得到一半的总线带宽,使CPU也能正常的工作


AHB从设备:

DMA自身寄存器。连接在总线右边AHB总线上。DMA既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。(CPU可通过图中蓝线对DMA配置)


DMA请求(触发):

触发源为各个外设,因此,DMA触发为DMA硬件触发源。


总结:

DMA结构:

①访问各个存储器的DMA总线

②DMA内部通道可进行独立的数据转运

③仲裁器来调度各个通道,防止冲突产生

④AHM从设备来配置DMA参数

⑤DMA请求来触发DMA的数据转运

6.2.DMA1框图

 6.2.1关键部件解释 (F103 视角):

1.通道寄存器 (每个通道独立):

  • CPAR (DMA_CPARx): 外设地址寄存器。存放源地址或目标地址(取决于方向)。例如,&ADC1->DR。
  • CMAR (DMA_CMARx): 存储器地址寄存器。存放目标地址或源地址(取决于方向)。例如,你的数组地址 &adc_value[0]。
  • CNDTR (DMA_CNDTRx): 传输数量寄存器。初始化写入要传输的数据项数量。传输时递减,减到0时传输完成。
  • CCR (DMA_CCRx): 通道配置寄存器。包含:

①MEM2MEM: 存储器到存储器模式使能 (1=使能)。

②PL[1:0]: 通道优先级 (00=低, 01=中, 10=高, 11=最高)。

③MSIZE[1:0]: 存储器数据宽度 (00=8位, 01=16位, 10=32位)。

④PSIZE[1:0]: 外设数据宽度 (00=8位, 01=16位, 10=32位)。

⑤MINC: 存储器地址递增模式 (1=递增, 0=固定)。

⑥PINC: 外设地址递增模式 (1=递增, 0=固定)。外设寄存器地址通常固定!

⑦CIRC: 循环模式 (1=使能)。注意F103的循环模式:

      1)当CNDTR减到0时,如果CIRC=1,则自动将CNDTR重载为初始值。

      2)不会自动重置CPAR和CMAR地址! 如果需要地址重置(如ADC连续扫描),必须配合MINC=1并在初始时设置正确的CMAR。下一轮传输会从CMAR初始值开始(因为CNDTR重置了,MINC操作会从初始地址重新递增)。对于固定地址的外设端(PINC=0),没有问题。

  • DIR: 传输方向 (0=从外设读->写入存储器, 1=从存储器读->写入外设)。
  • TEIE: 传输错误中断使能。
  • HTIE: 半传输中断使能。
  • TCIE: 传输完成中断使能。
  • EN: 通道使能 (1=使能)。写1启动传输。

2.仲裁器: 决定当多个通道同时请求时,哪个通道优先被服务。

3.AHB 总线接口: DMA控制器通过AHB总线访问存储器和外设。

7.DMA基本结构

数据转运俩个站点:外设寄存器和Flash+SRAM存储器

在STM32手册里所说存储器,一般是特指Flash和SRAM,不包含外设寄存器。外设寄存器一般被称外设。即使寄存器也是存储器的一种,但STM32使用外设和存储器来区分。

转运方向:

①外设与存储器之间数据转运,有一个方向的参数可以控制目的方向。

②存储器之间数据转运,Flash→SARM和SARM→SARM,但flash只读,因此DMA不可以进行SARM→Flash和Flash之间的转移操作。


7.1.外设和存储器俩个站点分别有三个参数控制转运方向

①起始地址(外设端和存储器端),决定数据转运方向。

②数据宽度,指定一次转运要按多大的数据宽度来进行。它可以选择字节(Byte-8位)、半字(HalfWord-16位)和字(word-32位)。

③地址是否自增,指定一次转运完成后的下一次转运是否要把地址移动到下一个位置。比如ADC扫描模式,用DMA进行数据转运。外设地址是ADC_DR寄存器,寄存器地址是不用自增的,如果自增,下一次转运就会跑到别的寄存器。存储器地址需要,每转运一个数据后,就往后一个位置,以防下次再转,把上次数据覆盖。


如果进行存储器到存储器的数据转运,就需要把其中一个存储器的地址放在外设站点。


7.2.DMA结构解读

外设/存储器站点中,起始地址可写存储器地址/寄存器地址/Flash地址/SARM地址,它没有限制。

传输计数器NDT(自减计数器):指定转运的次数。自减到0,之前地址是否自增参数引起的自增地址会恢复到起始地址的位置,以便下一次的转运。

自动重装器,决定在输出计数器减到0后是否自动回复到最初值和转运模式。

传输模式:

  • 正常模式: 配置好数量NDT,启动一次,搬完NDT个数据就停止。
  • 循环模式: 搬完NDT个数据后,自动将源/目标地址重置回初始值,并重新开始传输。非常适合连续数据流 (如ADC连续扫描)。

DMA触发控制(硬件/软件触发):

触发决定DMA在何时进行转运。触发选择由M2M(memory to memory)参数决定。

M2M=1,软件触发(连续触发),应用于存储器之间转运。不可和循环模式同时使用,会死循环。

注意:如果在使用DMA正常模式(也就是传输计数器不自动重装)时,但是想在传输完成再开启DMA传输,一定要在传输完成中断里先失能DMA,然后设置传输计数器的值,再使能DMA,这样DMA就可以再次转运

7.3.DMA进行转运的条件:

①开关DAM_Cmd使能

②传输计数器NDT>0

③触发源有触发信号,转运一次,触发一次,NDT自减一次,当NDT=0且没有自动重装,DMA不会再次进行转运,需要DAM_Cmd关闭。

注意:
写传输计数器NDT时,必须先关闭DMA。

8.DMA请求

  • 是什么? 一个硬件信号,由外设(Peripheral)发出,通知DMA控制器:“我这里数据准备好要发送了(如UART TX空)” 或 “我这里数据准备好要接收了(如UART RX收到数据,ADC转换完成)”。
  • 谁产生? STM32的外设模块内部逻辑产生。例如:

①ADC: 转换完成事件 (EOC - End Of Conversion)

②UART: 发送寄存器空 (TXE - Transmit Data Register Empty),接收寄存器非空 (RXNE - Read Data Register Not Empty)

③SPI/I2C: 发送缓冲空 (TXE), 接收缓冲非空 (RXNE)

④TIMER: 更新事件 (Update Event), 捕获/比较事件 (Capture/Compare Event)

  • 如何连接? STM32内部有固定的或可配置的映射关系,将特定外设的特定事件连接到特定的DMA通道。
  • 配置:

①在外设侧:需要使能该外设的DMA功能。

②在DMA侧:需要配置该通道的请求源为对应的外设

  • 概念:外设事件(如ADC转换完成、UART TX空)触发硬件信号请求DMA传输。
  • 映射表 (必须查手册!): STM32F103xx Reference Manual (RM0008) Section 10.3.3: DMA requests 有完整表格。部分重要映射如下:
  • 软件触发通道可任意。

DMA1 通道

外设请求源 (可能多个)

常用外设

Channel 1

ADC1, TIM2_CH3, TIM4_CH1, SPI/I2S_Rx, USART3_Tx

ADC1

Channel 2

SPI1_Rx, USART1_Rx, TIM2_UP, TIM3_CH3, TIM1_CH1

Channel 3

SPI1_Tx, USART1_Tx, TIM3_CH4, TIM1_CH2, TIM2_CH2

Channel 4

USART2_Rx, SPI/I2S_Rx, TIM1_CH1, TIM1_CH4, TIM2_UP

Channel 5

USART2_Tx, I2C2_Tx, TIM1_UP, TIM1_CH3, TIM3_CH2

Channel 6

I2C1_Rx, TIM1_CH3, TIM3_CH1, TIM4_CH2

Channel 7

I2C1_Tx, TIM2_CH1, TIM2_CH2, TIM4_CH3

  • 重要结论:

①ADC1 的DMA请求只能映射到 DMA1 Channel 1!这是固定的。

②配置ADC1使用DMA时,必须使用DMA1 Channel 1。DMA输出决定对应的外设。

③其他外设(如USART1)可能有多个通道可选,需根据需求选择。

④7哥触发源进入仲裁器,经过优先级判断,最终产生内部DMA1请求。默认通道号越小,优先级越高。

9.数据宽度与对齐

①当源端和目标宽度都是八位时,转运第一步是在源端的0位置读数据B0,在目标的零位置写数据B0,把这个B0从左边挪到右边。之后的步骤相同,把Bn从左边挪到右边。

②当源端是8位,目标是16位,转运第一步是在源端读B0,在目标写00B0。之后读B100B1

如果目标数据宽度比源端数据宽度大,那就在目标数据前面多出来的空位补零……

如果目标数据宽度比源端数据宽度小,比如由十六位转到八位,就是读B1、B0只写入B0。读B3、B2只写入B2……把多出来的高位舍弃掉。

9.1.数据宽度

  • 定义:每次DMA传输操作移动多少位数据。
  • 选项:通常为 字节 (Byte, 8位)、半字 (HalfWord, 16位)、字 (Word, 32位)。
  • 配置点 (两个地方!):

①外设数据宽度 (Peripheral Data Width): 告诉DMA控制器,源/目标外设寄存器期望/提供的数据是多少位。例如,ADC数据寄存器 (DR) 通常是 16位 (半字)。UART数据寄存器 (DR) 通常是 8位 (字节)。

②存储器数据宽度 (Memory Data Width): 告诉DMA控制器,源/目标内存地址的数据是多少位。例如,你定义了一个 uint16_t adc_buf[10]; 数组,那么内存宽度应该是半字 (16位)。

  • 为什么重要? DMA控制器会根据你设置的宽度去访问外设寄存器和内存。设置错误会导致数据错位或只传输部分数据。

9.2.地址对齐 (Address Alignment):

  • 定义:源地址和目标地址必须符合其数据宽度的自然边界要求。
  • 规则:

①访问 字节 (8位) 数据:地址可以是任意地址 (0x00000000, 0x00000001, 0x00000002, ...)。

②访问 半字 (16位) 数据:地址最低位必须为0 (即地址是2的倍数:0x00000000, 0x00000002, 0x00000004, ...)。

③访问 字 (32位) 数据:地址最低两位必须为00 (即地址是4的倍数:0x00000000, 0x00000004, 0x00000008, ...)。

  • 为什么重要? 现代微控制器(包括STM32)的存储器系统(总线、缓存)通常要求对齐访问以获得最佳性能或避免硬件错误(总线错误)。非对齐访问有时能工作(取决于具体硬件和配置),但强烈建议遵循对齐规则,否则可能导致不可预知的行为(数据错误、效率低下、甚至硬件异常)。
  • 数组定义: 在C语言中定义存储DMA数据的数组时,使用编译器指令确保其地址对齐是良好实践(尤其是在使用字宽度时)。例如:
     
    // 使用 GCC/ARMCC 属性指定4字节对齐
    uint32_t buffer[100] __attribute__ ((aligned (4))); // 字传输
    uint16_t adc_results[8] __attribute__ ((aligned (2))); // 半字传输
    // 或者使用标准类型 (通常已自然对齐)
    uint32_t buffer[100]; // 通常就是4字节对齐的
    uint16_t adc_results[8]; // 通常就是2字节对齐的

9.3.关键原则:

  • 源端和目标端的数据宽度可以不同! DMA控制器会自动处理必要的打包(Packing)或解包(Unpacking)。例如:

①源(外设)宽度=半字(16位),目标(内存)宽度=字(32位):DMA会搬2次半字(2个请求),凑成一个字写入内存地址。

②源(内存)宽度=字(32位),目标(外设)宽度=字节(8位):DMA会从一个字中拆出4个字节,分4次写入目标外设。

  • 地址必须各自满足其数据宽度的对齐要求。 这是硬性规定

10.数据转运+DMA

这是DMA最基础的应用。

场景: 将内存数组 src_buffer 中的 100 个 32位 整数复制到另一个内存数组 dst_buffer。

10.1配置要点:

  1. 方向: 存储器到存储器
  2. 源地址: &src_buffer[0] (起始地址)。
  3. 目标地址: &dst_buffer[0]。
  4. 数据宽度: 字 (32位) (因为数组元素是 uint32_t)。
  5. 地址递增: 源地址 递增,目标地址 递增。
  6. 传输数量 (NDT): 100。
  7. 模式: 正常模式 (复制一次)。
  8. 请求: 存储器到存储器模式通常不需要外设请求! DMA通道一旦使能,会立即开始传输(或者根据触发源配置)。
  9. 中断 (可选): 使能传输完成中断 (TCI),在复制完成后通知CPU。

优势: CPU只需要几条指令配置DMA,然后就可以去干别的事情。100个字的复制由DMA硬件高效完成,不占用CPU时间。比CPU用 for 循环复制快得多。


10.2根据图片说参数

由DMA结构图可知重要的是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。

由图可知:

①DataA数组为外设地址首地址,DataB数组为存储器地址首地址,且数据宽度两个数组的类型都是uint8_t,因此数据宽度都是按8位的字节传输。

②想要效果是DataA[0]转到DataB[0],两个数组位置一一对应,以此类推,因此转运完DataA[0]和DataB[0]之后,两个站点的地址都是自增,自动移到下一个数据的位置,继续转运DataA[1]和DataB[1]……

猜想:

①若左边自增,右边不自增,则DabaB[0]=DataA[6],其他数字不变

②若左边不自增,右边自增,则DataB[0]=DataB[1]=……DataB[6]=DataA[0]

方向参数是外设→存储器

传输计数器不自动重装DNT7减到0后结束

触发选择是软件触发(存储器之间的转运)

11.ADC扫描模式+DMA

这是STM32中使用DMA最经典、最高效的场景之一。解决了传统轮询或中断方式在扫描多通道时的CPU开销和实时性问题。

11.1ADC扫描模式 (Scan Mode) 回顾:

  • ADC可以配置一个通道序列 (Sequence)。
  • 启动一次转换 (软件启动或外部触发),ADC会自动按顺序转换序列中的所有通道。
  • 每次转换完成一个通道,结果存储在同一个 ADC数据寄存器 (DR) 中。
  • 问题: 当扫描多个通道时,CPU如何及时读取DR寄存器中的结果而不丢失或覆盖?传统方法(轮询或中断)会导致CPU忙于读取数据或频繁被中断。

11.2.DMA 如何完美解决:

1.配置ADC扫描模式: 设置好要转换的通道序列 (长度 = N)。

2.配置DMA:

  • 通道: 选择映射到该ADC (如ADC1) 的DMA通道 (查手册)。
  • 方向: 外设到存储器 (ADC DR -> RAM)。
  • 源地址: &ADC1->DR (固定地址!)。
  • 目标地址: &adc_results[0] (指向你的数组首地址)。
  • 源数据宽度: 根据ADC分辨率 (通常12位) 和DR寄存器设计 (通常是16位右对齐),设置为半字 (16位)。
  • 目标数据宽度: 根据数组元素类型 (如 uint16_t),设置为半字 (16位)。
  • 源地址递增: 禁止 (因为ADC DR寄存器地址是固定的)。
  • 目标地址递增: 使能 (这样每个通道的结果会依次存放到数组的不同位置)。
  • 传输数量 (NDT): N (等于扫描序列的长度)。
  • 模式: 循环模式 (Circular Mode) (关键!)。
  • 外设请求: 使能ADC的DMA请求 (通常叫 DMA Continuous Requests 或类似)。
  • 中断 (可选): 使能 传输完成中断 (TCI) 或 半传输中断 (HTI)。TCI 发生在整个序列(N个通道)转换完成并传输到数组时。HTI 发生在传输了 N/2 个数据时(如果N是偶数)。

3.启动ADC: 启动ADC转换 (连续转换模式或单次转换后再次触发)。

11.3.工作流程详解:

1.ADC转换开始: 软件启动或外部触发启动ADC转换序列。

2.ADC转换通道1完成:

  • ADC将通道1结果写入 ADC1->DR 寄存器。
  • ADC硬件自动产生一个DMA请求!

3.DMA响应请求:

  • DMA控制器检测到ADC1的请求。
  • 从 &ADC1->DR 读取 16位数据 (通道1结果)。
  • 写入 到当前目标地址 &adc_results[0]。
  • 因为目标地址递增使能,目标地址 自动+2 (半字宽度) 指向 &adc_results[1]。
  • 传输计数器 NDT减1 (假设初始NDT=N)。

4.ADC继续转换通道2: ADC自动开始转换序列中的下一个通道。

5.ADC转换通道2完成:

  • 结果写入 ADC1->DR (覆盖了通道1的结果)。
  • 再次产生DMA请求!

6.DMA再次响应:

  • 从 &ADC1->DR 读取 (通道2结果)。
  • 写入 &adc_results[1]。
  • 目标地址 +2 -> &adc_results[2]。
  • NDT减1 -> N-2。

7. 重复步骤4-6 ... 直到转换完序列中所有N个通道。

8.最后一个通道转换完成并传输:

  • DMA将通道N结果写入 adc_results[N-1]。
  • 目标地址递增到 &adc_results[N] (但已超出数组范围,这个地址不会被使用)。
  • NDT减到0!

9.模式生效:

  • DMA控制器自动将传输计数器NDT重置为初始值N。
  • DMA控制器自动将目标地址重置回初始值 &adc_results[0]。
  • DMA通道保持使能状态!

10.ADC开始下一轮扫描 (如果是连续模式): ADC自动开始下一轮扫描序列的第一个通道转换。

11.流程回到步骤2: 新的通道1结果被写入 adc_results[0],覆盖掉上一轮的结果。如此往复循环。

12.中断 (如果使能):

  • 当NDT减到0(即一轮完整的N个通道转换结果都已传输)时,如果使能了传输完成中断(TCI),则触发DMA传输完成中断。
  • 在循环模式下,TCI中断标志着新的一轮完整数据已准备好! CPU可以在中断服务程序(ISR)中安全地读取 adc_results 数组,处理这N个通道的最新数据。

巨大优势:

  • 零CPU干预数据传输: CPU完全不需要在每次ADC转换完成时去读取DR寄存器。DMA在后台自动完成所有数据的搬运。
  • 保证数据完整性: DMA在ADC转换完成的瞬间就将数据移走,避免了数据被后续转换覆盖的风险。
  • 高效连续采样: 结合ADC连续转换模式和DMA循环模式,实现了硬件级的连续、高速、多通道数据采集流水线。CPU只需要在每轮采集完成(TCI中断)时处理一次完整的数据块即可。
  • 低延迟: DMA响应硬件请求非常快。
  • 低功耗: CPU在数据搬运期间可以睡眠(WFI指令),仅在需要处理数据时唤醒(DMA中断唤醒)。


11.4.根据图片说参数

由图可知:左边是ADC扫描模式且设置七个通道,触发一次后,七个通道依次进行AD转换,转换结果放在ADC_DR数据寄存器。

每个通道AD转换到ADC_DR数据立马数据转移到SRAM组,SRAM数组以防被覆盖,需要DMA数据存储和地址自增。

因此,DMA配置:

①外设地址写入ADC_DR寄存器的地址,存储器地址为在SRAM中的一个数组ADvalue。

②数据宽度,由ADvalue数组定义可知数据宽度为16位的半字传输。

③地址是否自增,外设地址不自增(ADC_DR寄存器数据每转换一次被覆盖),存储器地址自增(排列存储ADC_DR中每一次的数据)。

④传输方向:外设站点→存储器站点。

⑤传输计数器7(通道有七个)

⑥计数器是否自动重装,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止。如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。

⑦触发选择,ADC_DR的值在ADC单个通道转换完成之后才有效,所以DMA转运的时机需要和ADC单个通道转换完成同步,因此DMA的触发是硬件触发。

注意:ADC扫描模式在每个单独的通道转换完成后,没有任何标志位也不会触发中断。因此不好判断某一个通道转换完成的时间。虽然单个通道转换完成后,不产生任何标志位和中断,但是它会产生DMA请求去触发DMA转运。


12.DMA数据转运

12.1.初始化DMA

    ①RCC开启DMA时钟
    ②调用DMA_Init初始化里面的参数(外设和存储器站点三个参数,传输方向,传输计数器,是否需要自动重装,选择触发源M2M,仲裁器的优先级)
    ③开关控制DMA_Cmd给指定通道使能即可


注意:
    ①若选择硬件触发,需要在对应的外设调用函数xxx_DMACmd,开启触发信号输出;
    ②若需要DMA中断,需要调用DMA_ITConflg,开启中断输出,再在NVIC配置相应终端通道,后中断函数配置即可
    ③在运行过程中,若转运完成,传输计数器清零,再想给传输计数器赋值,需要DMA失能,写传输计数器,DMA使能即可


12.2.DMA库函数

    1.恢复缺省配置
    void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);
    
    2.初始化
    void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
    
    3.结构体初始化
    void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);
    
    4.DMA使能
    void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
    
    5.中断输出使能
    void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
    
    6.设置当前数据寄存器(给传输计数器写数据)
    void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 
    
    7.DMA获取当前数据寄存器(返回传输计数器的值)
    uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
    
    8.获取标志位状态
    FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
    
    10.清楚标志位
    void DMA_ClearFlag(uint32_t DMAy_FLAG);
    
    11.获取中断状态
    ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
    
    12.清除中断挂起位
    void DMA_ClearITPendingBit(uint32_t DMAy_IT);

12.3.DMA.c

#include "stm32f10x.h"                  // Device header
void DMA111_init(uint32_t AddrA,uint32_t AddrB,uint16_t size)
{
	//①开启时钟,DMA是AHB总线设备,因此用AHB开启时钟设备;函数解释第一段是互联型设备(STM32F105/107)第二段是其他设备
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//②调用DMA_Init初始化里面的参数(外设和存储器站点三个参数,传输方向,传输计数器,是否需要自动重装,选择触发源M2M,仲裁器的优先级)
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_BufferSize=size;//缓冲区大小(传输计数器)
	/*
	以数据单元指定缓存区大小-传输几个数据单元(等于传输源端站点的DataSize)
	BufferSize为传输计数器,指定传输几次,在DMA_Init函数其参数直接赋值给传输计数器
	数值0~65535
	*/
	
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
	/*
	#define DMA_DIR_PeripheralDST 外设站点作为目的地destination             ((uint32_t)0x00000010)
  #define DMA_DIR_PeripheralSRC 外设站点作为源头Source             ((uint32_t)0x00000000)

	*/
	
	DMA_InitStructure.DMA_M2M=DMA_M2M_Enable ;//选择是否存储器到存储器(硬件触发/软件触发)
	
	DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//存储器站点起始地址
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte ;//存储器站点数据宽度
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable ;//存储器站点是否自增
	
	DMA_InitStructure.DMA_Mode=DMA_Mode_Normal ;//传输模式(是走使用自动重装)
	
	DMA_InitStructure.DMA_PeripheralBaseAddr=AddrA;//外设站点起始地址(对于SRAM数组,地址是编译器分配,并不固定,一般不屑绝对地址,而是通过数组名来获取地址,因此可以将地址提取成初始化函数参数)
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;//外设站点数据宽度,字节传输
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//外设站点是否自增
	
	DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级(若有多个通道,就要确定优先级)
	
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);//选择DMA1通道1(由于选择存储器到存储器转运,软件触发,通道任意)

	//③开关控制DMA_Cmd给指定通道使能即可
	DMA_Cmd(DMA1_Channel1,ENABLE);

}

12.4main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "DMA111.h"
//定义DMA转运源端数组和目的数组
uint8_t DataA[]={0x01,0x02,0x03,0x04};
uint8_t DataB[]={0,0,0,0};
int main(void)
{	
	
  OLED_Init();
	
		OLED_ShowHexNum(1,1,DataA[0],2);
		OLED_ShowHexNum(1,4,DataA[1],2);
		OLED_ShowHexNum(1,7,DataA[2],2);
		OLED_ShowHexNum(1,10,DataA[3],2);
	                  
		OLED_ShowHexNum(2,1,DataB[0],2);
		OLED_ShowHexNum(2,4,DataB[1],2);
		OLED_ShowHexNum(2,7,DataB[2],2);
		OLED_ShowHexNum(2,10,DataB[3],2);
	
	
	
	while(1)
	{
		DMA111_init((uint32_t)DataA,(uint32_t)DataB,4);
        //数组名为地址,不需要取地址,强制转换成uint32_t类型
		OLED_ShowHexNum(3,1,DataA[0],2);
		OLED_ShowHexNum(3,4,DataA[1],2);
		OLED_ShowHexNum(3,7,DataA[2],2);
		OLED_ShowHexNum(3,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);
	}
}

#显示结果是数组A只能保持不变,数组B转移数组A#

12.5数组A数据改变,数组B数据跟着改变

DMA.c(修改)

①DMA初始化失能

②增加循环之后才能添加的转运函数和标志位获取与清除函数

#include "stm32f10x.h"                  // Device header

uint16_t DMA_tranfer_size;

void DMA111_init(uint32_t AddrA,uint32_t AddrB,uint16_t size)
{
	DMA_tranfer_size=size;

	
	//①开启时钟,DMA是AHB总线设备,因此用AHB开启时钟设备;函数解释第一段是互联型设备(STM32F105/107)第二段是其他设备
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//②调用DMA_Init初始化里面的参数(外设和存储器站点三个参数,传输方向,传输计数器,是否需要自动重装,选择触发源M2M,仲裁器的优先级)
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_BufferSize=size;//缓冲区大小(传输计数器)
	/*
	以数据单元指定缓存区大小-传输几个数据单元(等于传输源端站点的DataSize)
	BufferSize为传输计数器,指定传输几次,在DMA_Init函数其参数直接赋值给传输计数器
	数值0~65535
	*/
	
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
	/*
	#define DMA_DIR_PeripheralDST 外设站点作为目的地destination             ((uint32_t)0x00000010)
  #define DMA_DIR_PeripheralSRC 外设站点作为源头Source             ((uint32_t)0x00000000)

	*/
	
	DMA_InitStructure.DMA_M2M=DMA_M2M_Enable ;//选择是否存储器到存储器(硬件触发/软件触发)
	
	DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//存储器站点起始地址
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte ;//存储器站点数据宽度
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable ;//存储器站点是否自增
	
	DMA_InitStructure.DMA_Mode=DMA_Mode_Normal ;//传输模式(是否使用自动重装)
	
	DMA_InitStructure.DMA_PeripheralBaseAddr=AddrA;//外设站点起始地址(对于SRAM数组,地址是编译器分配,并不固定,一般不屑绝对地址,而是通过数组名来获取地址,因此可以将地址提取成初始化函数参数)
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;//外设站点数据宽度,字节传输
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//外设站点是否自增
	
	DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级(若有多个通道,就要确定优先级)
	
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);//选择DMA1通道1(由于选择存储器到存储器转运,软件触发,通道任意)

	//③开关控制DMA_Cmd给指定通道使能即可
	DMA_Cmd(DMA1_Channel1,DISABLE);
	//DMA初始化后不立即开始转运

}

void DMA111_Transfr(void)
{
	//调用函数,可再次启动一次DMA转运
	//需要给传输计数器重新赋值,必须先失能DMA,再改变数据,再DMA使能
	DMA_Cmd(DMA1_Channel1,DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,DMA_tranfer_size);
	DMA_Cmd(DMA1_Channel1,ENABLE);

	//转运之后,需要等待转运完成,转运完成标志位置1
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
	/*
	*     @arg DMA1_FLAG_GL1: DMA1 Channel1 global flag.全局标志位
  *     @arg DMA1_FLAG_TC1: DMA1 Channel1 transfer complete flag.转运完成标志位
  *     @arg DMA1_FLAG_HT1: DMA1 Channel1 half transfer flag.转运过半标志位
  *     @arg DMA1_FLAG_TE1: DMA1 Channel1 transfer error flag.转运错误标志位
	*/
	
	//清除标志位
	DMA_ClearFlag(DMA1_FLAG_TC1);
}
main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "DMA111.h"
//定义DMA转运源端数组和目的数组
uint8_t DataA[]={0x01,0x02,0x03,0x04};
uint8_t DataB[]={0,0,0,0};
int main(void)
{	
	
  OLED_Init();
	OLED_ShowString(1,1,"DataA");
	OLED_ShowString(3,1,"DataB");
	DMA111_init((uint32_t)DataA,(uint32_t)DataB,4);
	//数组名为地址,不需要取地址,强制转换成uint32_t类型

	
	//分别显示地址
	OLED_ShowHexNum(1,8,(uint32_t)DataA,8);
	OLED_ShowHexNum(3,8,(uint32_t)DataB,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_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);
	
	
	
	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_s(1);
		
		DMA111_Transfr();//函数执行完成后,转运结束,再次显示最终数组结果
	}
}

12.6.ADC单次扫描+DMA单次转运模式

AD_DMA.c

①DMA和ADC初始化结合,修改通道 

②AD_GetValue函数修改

③数组AD_Value[4]全局变量

#include "stm32f10x.h"                  // Device header
/*
ADC扫描模式和DMA数据转运
①扫描PA0到PA3四个通道
*/

uint16_t AD_Value[4];
void AD_init(void)
{
	//①开启RCC时钟,包括ADC和GPIO时钟,配置RCC内部时钟的ADC_CLK
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//6分频 72/6=12MHz
	
	//②配置GPIO口,将其配置成模拟输入模式,PA0
	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);
-----------------------------------------------------------------------------------------	
			//菜单 序号1-4.填写通道0-3
			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);
-----------------------------------------------------------------------------------------
	//③配置多路开关,将左边通道接入右边规则组列表

	//非扫描模式,指定通道0(PA0),放在第一个序列,采样时间根据需求决定(快转换小参数,稳定转换大参数)
	//若想在其他序列填写其他通道,复制函数即可,每隔通道也可以设置不同的采样时间
	
	//④配置AD转换器和AD数据寄存器,用结构体配置参数,包括单次/连续转换,(非)扫描,通道数量,触发源和数据对齐模式
	//用结构体初始化ADC
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_ContinuousConvMode=DISABLE;//单次转换
	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=4;//看前四个位置
	        ADC_InitStructure.ADC_ScanConvMode=ENABLE;//扫描模式    

-----------------------------------------------------------------------------------------
	ADC_Init(ADC1,&ADC_InitStructure);
	
	
	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	
	//①开启时钟,DMA是AHB总线设备,因此用AHB开启时钟设备;函数解释第一段是互联型设备(STM32F105/107)第二段是其他设备
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//想象成服务员,ADC将菜做好放在ADC_DR寄存器,DMA尽快把才端出来,防止被覆盖
	//②调用DMA_Init初始化里面的参数(外设和存储器站点三个参数,传输方向,传输计数器,是否需要自动重装,选择触发源M2M,仲裁器的优先级)
	DMA_InitTypeDef DMA_InitStructure;
-----------------------------------------------------------------------------------------
	DMA_InitStructure.DMA_BufferSize=4;//缓冲区大小(传输计数器)
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
	DMA_InitStructure.DMA_M2M=DMA_M2M_Disable ;//选择是否存储器到存储器(硬件触发/软件触发),选择硬件触发,ADC做好菜,DMA端一个菜,有时机的
	
	DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//存储器站点起始地址,将数据存在SRAM数组
	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=(uint32_t)&ADC1->DR;//外设站点起始地址(对于SRAM数组,地址是编译器分配,并不固定,一般不屑绝对地址,而是通过数组名来获取地址,因此可以将地址提取成初始化函数参数)
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//外设站点数据宽度,字节传输,DR寄存器低16位数据
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//外设站点是否自增,不自增,始终转运同一个位置
	
	DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级(若有多个通道,就要确定优先级)
	
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);//选择DMA1通道1(ADC1的触发源出发只能是到通道1上)
-----------------------------------------------------------------------------------------
	//③开关控制DMA_Cmd给指定通道使能即可
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	//由于DMA三个转运条件:DMA使能,NDT>0,触发源有信号
	//此时触发源有信号不满足,因为硬件触发,ADC未启动,因此,需要开启ADC到DMA的输出
	//开启DMA触发信号
	ADC_DMACmd(ADC1,ENABLE);
----------------------------------------------------------------------------------------
	//配置ADC电源
	ADC_Cmd(ADC1,ENABLE);
	
	//⑥ADC校准
	//复位校准
	ADC_ResetCalibration(ADC1);//开始复位校准置1
	//获取复位校准状态
	while(ADC_GetResetCalibrationStatus(ADC1)== SET );//若为1,一直复位校准;若为0,复位校准完成跳出循环
	//set通常表示 “设置” 或 “使能” 状态。在逻辑上,它的值一般被定义为 1,用来表示某个功能或标志被激活、启用或处于高电平状态。
	//reset通常表示 “复位” 或 “清除” 状态。在逻辑上,它的值一般被定义为 0,用来表示某个功能或标志被禁用、清除或处于低电平状态。
	//开始校准
  ADC_StartCalibration(ADC1);
	//获取开始校准状态
	while(ADC_GetCalibrationStatus(ADC1)== SET );//等待校准标志位完成
}

-----------------------------------------------------------------------------------------
//启动转换,获取结果
//因为是扫描模式,dma把数据放内存(数组)里了,所以不需要返回值和参数
void ADC_Getvalue(void)
{
	//因为DMA是正常的单次模式,因此再触发ADC之前,需要重新写入传输计数器
	//需要给传输计数器重新赋值,必须先失能DMA,再改变数据,再DMA使能
	DMA_Cmd(DMA1_Channel1,DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,4);
	DMA_Cmd(DMA1_Channel1,ENABLE);

	
	//由于ADC单次模式,需要软件触发ADC开始
	//硬件触发是触发DMA转运,这里软件触发是启动ADC,软件触发相当于触发厨师做菜,硬件触发相当于触发服务员端菜
	//①ADC软件开始转换控制(用于软件触发),给SWSTART位置1,开始转换
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
	//等待DMA转运和ADC转换完成
	//由于转运总是在转换之后(端菜在做菜之后)

	//转运之后,需要等待转运完成,转运完成标志位置1
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);
	
	//等待ADC转换完成的代码不需要
	//因为数据已经通过DMA转运了,就算不判断ADC是否转运完成,ADC还是会一直采样一直转换,和EOC标志位是否被清0没有关系,所以就不用管EOC标志位是否被置1
  //这里是ADC先转换,完成之后给DMA发送请求,之后DMA接受数据,所以只用等DMA完成之后就行了。
  //AD每次转化完成都会产生一个DMA请求,因为是硬件触发,ADC和DMA时机都是准确的
	
	/*
		总的来说,当调用此函数,
		ADC开始转换,连续扫描四个通道,DMA也同步开始转运
		ADC转换结果,依次放在AD_Value数组
		*/
}



头文件修改
#ifndef __AD_D_
#define __AD_H_
void AD_init(void);
void ADC_Getvalue(void);
extern uint16_t AD_Value[4];

#endif
主函数修改
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0,AD1,AD2,AD3;

int main(void)
{	
	
  OLED_Init();
	AD_init();
	OLED_ShowString(1,1,"AD0:");
	OLED_ShowString(2,1,"AD1:");
	OLED_ShowString(3,1,"AD2:");
	OLED_ShowString(4,1,"AD3:");
	while(1)
	{
		ADC_Getvalue();
		
		OLED_ShowNum(1,5,AD_Value[0],4);
		OLED_ShowNum(2,5,AD_Value[1],4);
		OLED_ShowNum(3,5,AD_Value[2],4);
		OLED_ShowNum(4,5,AD_Value[3],4);
		Delay_ms(100);
	}
}

12.7ADC连续扫描+DMA循环转运模式

①    ADC_InitStructure.ADC_ContinuousConvMode=ENABLE;ADC连续模式打开
②    DMA_InitStructure.DMA_Mode=DMA_Mode_Circular ;//传输模式(是走使用自动重装)DMA循环模式打开

③    ADC_SoftwareStartConvCmd(ADC1, ENABLE);将ADC触发放在初始化最后一行

④主循环直接读取AD_Valuie数组,就能得到结果

ADC触发后,ADC连续转换,DMA循环转运,始终将最新结果放在SRAM数组内,想要获取数据,直接从数组里取即可


此外,还可以再加一个外设,比如定时器。

ADC用单次扫描,再用定时器去定时触发。实现定时器触发ADC,ADC触发DMA,整个过程完全自动,不需要程序手动进行操作,节省软件资源。


网站公告

今日签到

点亮在社区的每一天
去签到