当前位置: 首页 > article >正文

Diving into the STM32 HAL-----DMA笔记

        每个嵌入式应用程序都需要与外部世界交换数据或驱动外部外设。例如,我们的微控制器可以使用 UART 与 PCB 上的其他模块交换消息,或者可以使用可用的 SPI 接口之一将数据存储在外部闪存中。这涉及在内部 SRAM 或 flash memory 和外设之间传输给定数量的数据,并且需要一定数量的CPU周期才能完成传输。这会导致计算能力的损失(CPU 在传输过程中被占用),导致整体性能下降,最终导致重要的异步事件丢失。

        直接内存访问 (DMA) 控制器是一个专用的可编程硬件单元,允许 MCU 外设访问内部存储器,而无需 Cortex-M 内核的干预。CPU 完全摆脱了数据传输产生的开销(与 DMA 配置相关的开销除外),并且可以并行执行其他活动。DMA 设计为以两种方式工作(即,它允许数据从内存传输到外设,反之亦然),所有 STM32 微控制器都提供至少一个 DMA 控制器,但大多数微控制器都实现了两个独立的 DMA。

        DMA 是现代 MCU 的一项“高级”功能,新手用户往往认为它太复杂而无法使用。相反,DMA 的基础概念基本上很简单,一旦你理解了它们,它就会很容易使用。此外,好消息是 CubeHAL 旨在为给定外设抽象出大部分 DMA 配置步骤,让用户负责提供少量基本配置。

1、DMA 简介

        在我们分析 HAL_DMA 模块提供的功能之前,了解 DMA 控制器背后的一些基本概念很重要。接下来的段落试图总结在研究此外设时要牢记的最重要方面。多年来,STM32 MCU 中的 DMA 实现随着更强大和现代系列的出现而发展。最早也是更便宜的 STM32 系列提供最简单的 DMA 架构。随着更强大的 F2/F4/F7 系列的出现,意法半导体推出了更灵活的 DMA 架构,增加了通信通道的数量,同时提高了整体性能,这要归功于将 DMA 直接连接到某些外设的专用“总线桥”,避免了总线矩阵上的“流量”。

        最后,在最近的 STM32L4+/L5/Gx/H7 系列中,混合源和目标的自由度达到了一个新的维度:多亏了 DMA MUX 多路复用器,源和目标外设之间的互连不再是设计上的固定连接,并且可以互连链中的外设,其中数据仅通过 DMA 交换,而无需 CPU 干预。

        如果您在没有充分准备的情况下开始处理这三种 STM32 DMA 架构,您很可能会完全困惑,尤其是对 ST 术语。由于笔者不清楚的原因,ST 决定改变三种不同架构之间的词汇表,弄乱了术语和定义。不幸的是,这种术语更改也反映在 CubeHAL 中,让程序员认为 DMA 架构彼此之间差异很大。相反,除了灵活性的提高之外,这三种架构背后的概念(和技术细节)几乎相同。接下来的段落将尝试最小化细节级别,同时专注于重要的事情。但在深入研究非常具体的细节之前,先了解一下整体情况。

1.1、DMA 的需求和内部总线的作用

        为什么 DMA 是一个如此重要的功能?STM32 微控制器中的每个外设都需要与内部 Cortex-M 内核交换数据。其中一些将这些数据转换为电气 I/O 信号,以根据给定的通信协议将其交换到外部世界(例如,UART 或 SPI 接口就是这种情况)。其他的只是设计为在外设内存映射区域(从 0x4000 0000 到 0x5FFF FFFF)内访问它们的寄存器会导致其状态发生变化(例如,GPIOx->ODR 寄存器驱动连接到该端口的所有 I/O 的状态)。但是,请记住,从 CPU 的角度来看,这也意味着 MCU 内核和外设之间的内存传输。

        理论上,MCU 的设计可以使每个外设都有自己的存储区域(专用存储器),进而可以与 MCU 内核紧密耦合,以最大限度地降低与存储器传输相关的成本(这是一些配备非常昂贵的超级计算机的矢量处理器发生的情况)。然而,这使得 MCU 架构复杂化,需要更多的硅和更多消耗功率的“有源元件”。因此,所有嵌入式微控制器都使用内部 SRAM 内存的某些部分作为不同外设的临时区域存储。由用户决定为这些区域分配多少空间。例如,让我们考虑以下代码片段:

uint8_t buf[20];
...
HAL_UART_Receive(&huart1, buf, 20, HAL_MAX_DELAY);

·        这里我们将从 UART1 接口读取 20 个字节,因此我们在 SRAM 中分配一个相同大小的数组(临时存储)。HAL_UART_Receive() 函数将访问 huart1 二十次。Instance->DR 数据寄存器将字节从外设传输到内部存储器,此外,它还将轮询 UART RXNE 标志以检测新数据何时准备好传输。在这些操作中,CPU 将参与其中,即使它的作用“有限”地将数据从外设移动到 SRAM。请记住,在中断模式下使用 UART 不会改变情况。一旦 UART 生成中断以向 MCU 内核发出新数据到达的信号,则始终由 CPU 将此数据从 UART 数据寄存器逐字节“移动”到 SRAM。这就是为什么从性能的角度来看, 轮询和中断模式下的 UART管理没有区别的原因。

        虽然这种方法一方面简化了硬件的设计,但另一方面也带来了性能损失。Cortex-M 内核“负责”将数据从外设内存加载到 SRAM,这是一个阻塞操作,它不仅阻止 CPU 执行其他活动,而且还要求 CPU 等待“较慢”的单元完成其工作(一些 STM32 外设通过较慢的总线连接到 MCU 内核)。这就是为什么高性能微控制器提供专用于外设之间数据传输的硬件单元和集中式缓冲区存储(即 SRAM)的原因。

        在我们更深入地了解 DMA 细节之前,最好先概述一下数据从 外设 到 SRAM 存储器的传输过程中涉及的所有组件,反之亦然。我们已经看到了 STM32F072 MCU 的总线架构,这是最简单的 STM32 微控制器之一。下图再次显示了总线架构,它与其他性能更高的 STM32 系列有很大不同。

        该图向我们说明了一些重要的事情:

        Cortex-M 内核和 DMA1 控制器都通过一系列总线与其他 MCU 外设进行交互。注意 FLASH和SRAM 存储器是 MCU 内核之外的组件,因此它们需要通过总线互连相互交互。

        Cortex-M 内核和 DMA1 控制器都是主控器件。这意味着它们是唯一可以在总线上启动事务的单元。但是,必须控制对总线的访问,以便它们不能同时访问同一个从外设。

        BusMatrix 管理 Cortex-M 内核与 DMA1 控制器之间的访问仲裁。仲裁使用 Round Robin 算法来规则对总线的访问。BusMatrix 由两个主设备(CPU、DMA)和四个从设备(FLASH 接口、SRAM、AHB、AHB1 到高级外设总线 (APB) 桥接和 AHB2)组成。BusMatrix 还允许自动互连它们之间的多个外设。

        System 总线将 Cortex-M 内核连接到 BusMatrix。

        DMA 总线将 DMA 的高级高性能总线 (AHB) 主接口连接到 BusMatrix。

        AHB 到 APB 桥接器在 AHB 和 APB 总线之间提供完全同步连接,其中连接了大多数外围设备。

        上图中,DMA requests请求箭头,它从外设块(白色矩形)到 DMA1 控制器。它到底是干什么用的?在前面,我们已经看到 NVIC 控制器通知 Cortex-M 内核来自外设的异步中断请求 (IRQ)。当外设准备好执行某项操作时(例如,UART 已准备好接收数据或定时器溢出),它会置位专用的 IRQ。MCU 内核在给定的周期数后执行相应的 ISR,其中包含处理 IRQ 所需的代码。不要忘记外设是从属单元:它们不能独立访问总线。始终需要主设备来启动事务。但是,由于外设是从属单元(除了一些高性能外设,如 USB 和以太网,需要独立访问一些内存缓冲区,以避免流向通信介质的重要数据丢失。),如果我们使用 DMA 将数据从外设传输到内存,我们有办法通知它外设已准备好交换数据。这就是为什么从外设到 DMA 控制器可以使用专用数量的 DMA 请求线的原因。在下一段中看到它们是如何组织的以及我们如何对它们进行编程。

1.2、DMA 控制器

        在每个 STM32 MCU 中,DMA 控制器都是一个硬件单元。

        有两个主端口,分别命名为外设和内存端口,连接到高级高性能总线 (AHB),一个能够连接从外设,另一个能够连接内存控制器(SRAM、闪存、FSMC 等);在一些 DMA 控制器中,外设端口还能够连接内存控制器,允许内存到内存传输;在大多数 STM32 MCU 中,内存端口还能够连接外设控制器,允许外设到外设的传输;

        有一个从端口,连接到 AHB 总线,用于从另一个主站(即 CPU)对 DMA 控制器本身进行编程;

        具有许多独立的可编程通道,每个通道都通过设计连接或可连接到给定的外设请求线(UART_TX、TIM_UP 等);

        为通道定义不同的优先级(基于可编程或设计),以便仲裁对内存的访问,从而为更快和重要的外设提供更高的优先级;

        允许数据双向流动,即从存储器到外设和从外设到存储器:每个STM32 MCU根据其系列和销售类型提供可变数量的DMA和通道。

        这些特性在所有 STM32 微控制器中都广泛通用。但是,正如本节开头所说,DMA 控制器架构在第一个 STM32 系列和最新的 STM32 系列之间发生了变化。这就是我们要分别处理它们的原因。

1.2.1、F0/F1/F3/L0/L1/L4 MCU 中的 DMA 实现

        上图显示了 F0/F1/F3/L0/L1/L4 MCU 中 DMA1 控制器的表示。这些系列中的一些 MCU 提供第二个 DMA 控制器 DMA2。DMA1 控制器提供 7 个可配置通道,而 DMA2 只有 5 个。通道用于在4GB 地址空间中的两个内存区域之间交换数据。每个 channel 都绑定到一个给定的请求行request line,这会触发两个内存区域之间的传输。每个请求行都有可变数量的外设请求源连接到它: multiplexer多路选择器用于将给定的外设请求源绑定到 channel 请求行。然而,在芯片设计过程中,一个通道被绑定到一组固定的外设,并且同一通道中一次只有一个外设可以处于活动状态。例如,下表显示了通道如何绑定到 STM32F030 MCU 中的外设。每个请求行request line也可以由 “software” 触发。此功能用于执行内存到内存传输。

        每个通道都有一个优先级,允许管理对 AHB 总线的访问。在最旧的 MCU 中,如 STM32F1 MCU,优先级是固定的:通道 1 具有最大优先级,通道 7 具有最低优先级。在最近的 MCU 中,可以使用四级刻度来配置优先级。内部仲裁程序根据通道的优先级来规则来自通道的请求。如果两个请求行激活一个请求,并且它们的通道具有相同的优先级,则编号较小的通道将赢得争用。我们已经在前面图中看到了 STM32F072 的总线架构。下图显示了具有相同 DMA 实现(例如 STM32F1)的高性能 MCU 的总线架构。这两个系列的内部总线组织完全不同。可以看到另外两个名为 ICode 和 DCode 的总线。为什么会有这种差异?

        除了基于 Cortex-M0/0+ 内核的 STM32F0/G0/L0 外,大多数 STM32 MCU 共享相同的计算机架构。事实上,与其他基于 Harvard 架构的 Cortex-M 内核相比,它们是唯一基于 von Neumann 架构的 Cortex-M 内核。这两种架构之间的根本区别在于 Cortex-M0/0+ 内核使用一条公共总线访问闪存、SRAM 和外围设备,而其他 Cortex-M 内核有两条独立的总线线用于访问闪存(一条用于获取称为指令总线的指令,或简称 I-Bus 甚至 I-Code,另一条用于访问称为数据总线的 const 数据, 或简称 D-Bus 甚至 D-Code)和一条用于访问 SRAM 和外设的专用线路(也称为系统总线,或简称 S-Bus)。我们的应用有哪些优势?

        在 Cortex-M0/0+ 内核中,DMA 和 Cortex 内核使用 BusMatrix 争夺对存储器和外设的访问。假设 CPU 正在对其内部 registers (R0-R14) 中包含的数据执行数学运算。如果 DMA 正在将数据传输到 SRAM,则 BusMatrix 会仲裁从 Cortex 内核到闪存的访问,以加载要执行的下一条指令。因此,MCU 内核停滞不前,等待轮到它。在其他 Cortex-M 内核中,CPU 可以独立访问闪存,从而提高整体性能。这是一个根本的区别,证明了 STM32F0 MCU 的价格是合理的:它们不仅可以拥有更少的 SRAM 和闪存并以较低的频率运行,而且它们面临着更简单且本质上性能较低的架构。

        但是,需要注意的是,BusMatrix 实施调度策略以避免给定的主设备(Value Lines MCUs 中的 CPU 和 DMA,或Connectivity Lines MCUs 中的 CPU、DMA、以太网和 USB)停顿太长时间。每个 DMA 传输由四个阶段组成:采样和仲裁阶段、地址计算阶段、总线访问阶段和最终确认阶段(用于表示传输已完成)。每个阶段都占用一个周期,但总线访问阶段除外,该阶段可以持续更多的周期。但是,它的最大持续时间是固定的,并且 BusMatrix 保证在确认阶段结束时,将安排另一个 master 来访问总线。STM32F2/F4/F7 系列在访问从设备时允许更高级的并行性。最后,DMA 还可以在特定条件下执行外设到外设的传输。

1.2.2、F2/F4/F7 MCU 中的 DMA 实现

       

        STMF2/F4/F7 MCU 实现了更高级的 DMA 控制器,如上图所示。与旧款 STM32 MCU 中的 DMA 相比,它提供了更高的灵活性。然而,ST决定弄乱这些家系中与 DMA 相关的术语,再次改变对 G0/G4/L4+/L5/H7 家系的看法。因此,术语streams/流是指内存地址和外设地址之间的通信通道;意即streams 是通道的同义词;术语 channels 指的是请求行 (ST 官方文档中也称为 request streams)。

        在 F2/F4/F7 系列中,每个 DMA 实现 8 个不同的流。每个流专用于管理来自一个或多个外设的内存访问请求。每个流总共最多可以有 8 个通道(请求行)(一个流中只能有一个通道/请求同时处于活动状态),并且它有一个仲裁程序,用于处理 DMA 请求之间的优先级,用户可以在四个级别的基础上进行配置。每个流也可以由 “软件” 触发。此功能用于执行内存到内存的传输,但仅限于 DMA2。

        可以选择将 stream 配置为启用 4 字深度 32 位先进先出 (FIFO) 内存缓冲区。FIFO 用于在将来自源的数据传输到目标之前临时存储数据,尤其是两个源的速度不同。当两个端点的数据帧大小不同时, FIFO 缓冲区可以在执行传输时自动执行数据转换。支持的操作有: 8 位 / 16 位 → 32 位 / 16 位(数据打包);32 位 / 16 位 → 8 位 / 16 位(数据解包)。

        每个 STM32F2/F4/F7 MCU 提供两个 DMA 控制器,总共 16 个独立流。与其他 STM32 微控制器一样,在芯片设计过程中,一个通道绑定到一组固定的外设。下表显示了 STM32F401RE MCU 中的 DMA1 流/通道请求映射。

        STM32F2/F4/F7 MCU 嵌入了多主/多从架构,由以下部分组成:

        八个主设备:Cortex 内核 I-bus;Cortex 内核 D-bus;Cortex 内核 S-bus;DMA1 内存总线 ;DMA2 内存总线;DMA2 外设总线;以太网 DMA 总线(如果可用);USB 高速 DMA 总线(如果可用)

        八个从设备:内部闪存 I-Code 总线;内部闪存 D 代码总线;主内部 SRAM1;辅助内部 SRAM2(如果可用);辅助内部 SRAM3(如果可用);AHB1 外围设备,包括 AHB 到 APB 桥接器和 APB 外围设备 ;AHB2 外围设备;AHB3外设 (FMC)(如果可用)

        主设备和从设备通过多层 BusMatrix 连接,确保来自单独主设备的并发访问和高效操作,即使多个高速外设同时工作也是如此。此外,专用的 AHB 到 APB 桥接允许从主设备(以及 DMA)直接访问某些外设,以避免传递 BusMatrix。下图显示了 STM32F405/415 和 STM32F407/417 线路的这种架构。

        多层 Bus Matrix 允许不同的主设备同时执行数据传输,只要它们寻址不同的从设备模块(但对于给定的 DMA,一次只有“一个流”可以访问总线)。在 Cortex-M Harvard 架构和双 AHB 端口 DMA 之上,这种结构增强了数据传输并行性,从而有助于缩短执行时间,并优化 DMA 效率和功耗。

1.2.3、G0/G4/L4+/L5/H7 MCU 中的 DMA 实现

        STM32G0/G4/L4+/L5/H7 MCU 中的 DMA 控制器架构类似于 STM32F0/F1/F3/L0/L1/L4 系列中的 DMA 控制器架构,但通过 DMA 请求多路复用器 (DMA MUX) 单元进行了增强。DMA MUX 添加了一个完全可配置的路由,将任何 DMA 请求从 DMA 模式下的给定外设路由到两个 DMA 控制器的任何 DMA 通道。这意味着绑定到给定通道的 peripheral requests 不是由设计定义的,从而在设计阶段为程序员和硬件开发人员提供了最大的灵活性。DMA MUX 不会在外设发送的 DMA 请求和配置的 DMA 通道接收的 DMA 请求之间添加任何时钟周期。它具有使用专用输入同步 DMA 请求的功能。DMA MUX 还能够从自己的触发器输入或通过软件生成请求。

        上图显示了整体 DMA 架构。这是一个简化的图表,我们稍后将更好地指定。在图的右侧,您可以看到两个 DMA(DMA1、DMA2)。两个 DMA 单元的架构与前图中所示的相同。F0/F1/F3/L0/L1/L4 系列之间的主要区别在于,这里的 DMA 请求源不是直接连接到各个外设,而是来自 DMAMUX 模块。顾名思义,DMAMUX 是一个充当多路复用器的模块。DMAMUX 的输出是请求行,它将触发源地址和目标地址之间的数据传输。DMAMUX 的 input 由实际的外设请求行和一组不是来自外设的请求,以及一组用于同步数据传输的信号组成。

        上图是DMAMUX 架构更详细的说明。Request Multiplexer 模块为两个 DMA 中的每个通道都有专用的子单元。根据特定的系列,每个 DMA 单元提供 7 或 8 个通道。这意味着 Request Multiplexer 集成了 14 或 16 个专用于单个通道的设备。每个通道单元都有一组请求行。这些来自外设和 Request Generator 模块。每个通道都可以绑定到每个外设请求行,唯一的限制是一条请求行只能绑定到一个通道。

        Request Generator 单元可以被视为外围设备和 DMA 控制器之间的中介。它允许没有 DMA 功能的外设(如 RTC 警报或比较器)在事件上生成可编程数量的 DMA 请求。触发信号(主要来自 EXTI_LINES 和 LPTIM 定时器)、触发极性和请求数定义了发生器通道的行为。在 trigger event 接收到后,相应的 generator 通道开始在其 output 上生成 DMA 请求。每次连接的 DMA 控制器提供 DMAMUX 生成的请求时,内置 DMA 请求计数器(每个请求生成器通道一个计数器)都会递减。当它欠载时,请求生成器通道停止生成 DMA 请求,并且 DMA 请求计数器在下一个触发事件时自动重新加载到其编程值。

        为了执行外设到存储器或存储器到外设的传输,DMA 控制器通道每次都需要外设 DMA 请求行。每次发生请求时,DMA 通道都会从外设传输数据/向外设传输数据。此模式称为无条件请求转发。除了无条件请求转发之外,每个通道子模块中的同步单元还允许软件实现有条件请求转发:只有当检测到定义的条件时,路由才会有效地完成。DMA 传输可以与内部或外部信号同步。例如,用户软件可以使用同步单元来启动或调整数据传输吞吐量。DMA 请求可以通过以下方式之一转发:

        每次在 GPIO 引脚 (EXTI) 上检测到边沿时;

        响应来自计时器的周期性事件;

        响应来自外设的异步事件;

        响应来自另一个请求路由器的事件 (请求链接);

        除了 DMA 请求调节之外,同步单元还允许生成事件(DMAMUX_EVTm lines),这些事件可能被其他 DMAMUX 子块(例如请求生成器或另一个 DMAMUX 请求多路复用器通道)使用。

2、HAL_DMA 模块

        CubeHAL 旨在抽象出大部分底层硬件细节。所有与 DMA 操作相关的 HAL 函数都经过精心设计,以便它们接受 C 结构DMA_HandleTypeDef实例作为第一个参数。此结构与 CubeF2/F4/F7 HAL 和其他 CubeHAL 略有不同,因为 DMA 实现不同。因此,我们将单独显示它们。

2.1、F0/F1/F3/L0/L1/L4 HAL中的DMA_HandleTypeDef结构体

        DMA_HandleTypeDef用于配置给定的 DMA 通道,在 CubeF0/F1/F3/L1 HAL中按以下方式定义:

typedef struct {
	DMA_Channel_TypeDef *Instance; /* Register base address */
	DMA_InitTypeDef Init; /* DMA communication parameters */
	HAL_LockTypeDef Lock; /* DMA locking object */
	__IO HAL_DMA_StateTypeDef State; /* DMA transfer state */
	void *Parent; /* Parent object state */
	void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
	void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
	void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
	__IO uint32_t ErrorCode; /* DMA Error code */
} DMA_HandleTypeDef;

        Instance:指向我们将要使用的 DMA/通道对描述符的指针。例如,DMA1_Channel5 表示 DMA1 的第 5 个通道。在这些 STM32 系列中,通道在 MCU 设计过程中绑定到外设。

        Init:是 C 结构DMA_InitTypeDef的实例,用于配置 DMA/通道对。

        Parent:HAL 使用此指针来跟踪与当前 DMA/通道关联的外设处理程序。例如,如果我们在 DMA 模式下使用 UART,则此字段将指向 UART_HandleTypeDef 的实例。        

         XferCpltCallback、XferHalfCpltCallback、XferErrorCallback、XferAbortCallback:指向函数的指针,用作回调,用于向用户代码发出 DMA 传输已完成、半完成、发生错误或传输中止的信号。当 DMA 中断被触发时,HAL 会通过函数 HAL_DMA_IRQHandler() 自动调用它们。

        所有 DMA/Channel 配置活动都是通过使用 C 结构体DMA_InitTypeDef实例来执行的,该实例定义如下:

typedef struct {
	uint32_t Direction;
	uint32_t PeriphInc;
	uint32_t MemInc;
	uint32_t PeriphDataAlignment;
	uint32_t MemDataAlignment;
	uint32_t Mode;
	uint32_t Priority;
} DMA_InitTypeDef;

        Direction:它定义了 DMA 传输方向,并且可以采用下表中报告的值之一。

        PeriphInc:DMA 控制器有一个外设端口,用于指定内存传输中涉及的外设寄存器的地址(例如,对于 UART 接口,其数据寄存器 (DR) 的地址)。由于 DMA 内存传输通常涉及多个字节,因此可以对 DMA 进行编程,以自动增加传输的每个字节的外设寄存器。当执行 memory-to-memory 传输时,以及当外设是字节、半字和字可寻址时(如外部 SRAM 存储器)时,都是如此。在这种情况下,字段采用值 DMA_PINC_ENABLE,否则为 DMA_PINC_DISABLE。

        MemInc:该字段与 PeriphInc 字段含义相同,但涉及内存端口。它可以假设值 DMA_MINC_ENABLE 来表示指定的内存地址在传输每个字节后必须递增,或者DMA_MINC_DISABLE值在每次传输后保持不变。

        PeriphDataAlignment:外设和内存的传输数据大小可以通过此字段和下一个字段设置。它可以采用下表中的值。DMA 控制器旨在当源和目标数据大小不同时自动执行数据对齐(打包/解包)。

        MemDataAlignment:指定内存传输数据大小,并且可以采用下表中的值。

        Mode:STM32 MCU 中的 DMA 控制器有两种工作模式:DMA_NORMAL 和 DMA_CIRCULAR。在正常模式下,DMA 将指定数量的数据从源端口发送到目标端口并停止活动。必须再次重新准备它才能执行另一次传输。在循环模式下,在传输结束时,它会自动重置传输计数器,并从源缓冲区的第一个字节再次开始传输(即,它将源缓冲区视为环形缓冲区)。这种模式也称为连续模式,它是在某些外设(例如高速 SPI 设备)中实现高传输速度的唯一方法。

        Priority:DMA 控制器的一个重要功能是能够为每个通道分配优先级,以管理并发请求。此字段可以采用下表中的值。如果来自连接到具有相同优先级的通道的外设的并发请求,则编号较小的通道将首先触发。

        要根据 DMA_HandleTypeDef 和 DMA_ InitTypeDef 结构体中的指定参数初始化 DMA,请使用以下 HAL 函数:

HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);

2.2、G0/G4/L4+/L5/H7 HAL 中的 DMA 配置

        到目前为止,在 STM32G0/G4/L4+/L5/H7 MCU 中,DMA 是 F0/F1/F3/L0/L1/L4 系列中 DMA 的演变:DMAMUX 提供了将外设请求源与每个 DMA 通道混合的可能性。因此,STM32G0/G4/L4+/L5/H7 MCU 的 HAL_DMA API 几乎与现在看到的相似,除了 DMAMUX 配置部分。

        因此,从程序员的角度来看,DMA_HandleTypeDef 结构体不会添加相关字段来处理。在 STM32G0/G4/L4+/L5/H7 HAL 中其他字段由 HAL_DMA_Init() 例程自动填充。这大大简化了不同 STM32 系列之间的代码移植。相反,DMAMUX 的配置需要使用专用结构和函数进行。

        要将 DMA 通道绑定到来自 Request Generator TypeDef实例来配置 DMA 通道。接下来,我们必须使用以下结构配置 Request Generator 模块:模块的信号,我们必须执行两个不同的配置。首先,我们必须使用之前看到的 struct DMA_Handle

typedef struct {
	uint32_t SignalID; /*!< Specifies the ID of the signal used for DMAMUX request generator */
	uint32_t Polarity; /*!< Specifies the polarity of the signal on which the request is generated */
	uint32_t RequestNumber; /*!< Specifies the number of DMA request that will be generated after a signal event */
} HAL_DMA_MuxRequestGeneratorConfigTypeDef;

        SignalID:指定要用作 Request Generator 模块输入的信号的 ID。它可以采用下表中的值。

        极性:指定输入信号的极性,它可以采用下表中的值。

        RequestNumber:指定信号事件发生后将触发的请求数。这可以假定 1 到 32 之间的值。

        要使用上述设置配置请求生成器模块,我们需要使用以下函数:

HAL_DMAEx_ConfigMuxRequestGenerator(DMA_HandleTypeDef *hdma, HAL_DMA_MuxRequestGeneratorConfigTypeDef *pRequestGeneratorConfig);

        它接受指向与 DMA 通道对应的 DMA_HandleTypeDef 实例的指针,以绑定到 Request Generator 信号,并将实例绑定到 HAL_DMA_MuxRequestGeneratorConfigTypeDef 结构。

        DMAMUX 通过将给定通道与 sync 信号相关联来允许同步给定通道。这是通过使用 HAL_DMA_MuxSyncConfigTypeDef 结构来执行的,该结构的定义方式如下:

typedef struct {
	uint32_t SyncSignalID; /* Specifies the synchronization signal gating the DMA request \ in periodic mode. */
	uint32_t SyncPolarity; /* Specifies the polarity of the signal on which the DMA reques\ t is synchronized. */
	FunctionalState SyncEnable; /* Specifies if the synchronization shall be enabled or disable\ d. */
	FunctionalState EventEnable; /* Specifies if an event shall be generated once the RequestNum\ ber is reached.*/
	uint32_t RequestNumber; /* Specifies the number of DMA request that will be authorized \ after a sync event. */
} HAL_DMA_MuxSyncConfigTypeDef;

        SyncSignalID:指定用于启动 DMA 事务的同步信号,它可以采用下表中的一个值。

        SyncPolarity:指定同步 DMA 请求的信号的极性,它可以采用下表中的值。

        SyncEnable 和 EventEnable:指定是启用还是禁用同步,并且可以采用值 ENABLE 或 DISABLE。        

        RequestNumber:指定同步事件后将授权的 DMA 请求数。

        要配置通道同步,请使用以下 HAL 函数:

HAL_DMAEx_ConfigMuxSync(DMA_HandleTypeDef *hdma, HAL_DMA_MuxSyncConfigTypeDef *pSyncConfig);

它接受指向与 DMA 通道对应的 DMA_HandleTypeDef 实例的指针,以与外部源同步,并将实例绑定到 HAL_DMA_MuxSyncConfigTypeDef 结构体。

2.3、F2/F4/F7 HAL 中的 DMA_HandleTypeDef

        到目前为止,我们已经看到 STM32F2/F4/F7 MCU 中 DMA 采用的术语和组织略有不同。CubeHAL 反映了这些差异。结构体DMA_HandleTypeDef定义方式如下:

typedef struct {
	DMA_Stream_TypeDef *Instance; /* Register base address */
	DMA_InitTypeDef Init; /* DMA communication parameters */
	HAL_LockTypeDef Lock; /* DMA locking object */
	__IO HAL_DMA_StateTypeDef State; /* DMA transfer state */
	void *Parent; /* Parent object state */
	void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
	void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
	void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
	void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
	__IO uint32_t ErrorCode; /* DMA Error code */
	uint32_t StreamBaseAddress; /* DMA Stream Base Address */
	uint32_t StreamIndex; /*!< DMA Stream Index */
} DMA_HandleTypeDef;

        Instance:指向我们将要使用的流描述符的指针。例如,DMA1_Stream6 表示 DMA1 的第七个流。必须先将流绑定到通道,然后才能使用。这是通过 Init 字段实现的。在 MCU 设计期间,通道会绑定到外设。

        Init:是 C 结构DMA_InitTypeDef的实例,用于配置 DMA/通道/流三元组。

        Parent:HAL 使用此指针来跟踪与当前 DMA/通道关联的外设处理程序。例如,如果我们在 DMA 模式下使用 UART,则此字段将指向 UART_HandleTypeDef 的实例。

        XferCpltCallback、XferHalfCpltCallback、XferM1CpltCallback、XferErrorCallback:这些是指向用作回调的函数的指针,用于向用户代码发出信号,表明 DMA 传输已完成、半完成、多缓冲区传输中第一个缓冲区的传输已完成或发生错误。当 DMA 中断被触发时,HAL 会通过函数 HAL_DMA_IRQHandler() 自动调用它们。

        所有 DMA/Channel 配置活动都是通过使用 C 结构体DMA_InitTypeDef实例来执行的,该实例定义如下:

typedef struct {
	uint32_t Channel;
	uint32_t Direction;
	uint32_t PeriphInc;
	uint32_t MemInc;
	uint32_t PeriphDataAlignment;
	uint32_t MemDataAlignment;
	uint32_t Mode;
	uint32_t Priority;
	uint32_t FIFOMode;
	uint32_t FIFOThreshold;
	uint32_t MemBurst;
	uint32_t PeriphBurst;
} DMA_InitTypeDef;

        Channel:指定用于给定流的 DMA 通道。它可以采用 DMA_CHANNEL_0 的值,DMA_CHANNEL_1,最多 DMA_CHANNEL_7。在 MCU 设计期间,外设会绑定到流和通道。

         Direction:它定义 DMA 传输方向。

        PeriphInc:DMA 控制器有一个外设端口,用于指定内存传输中涉及的外设寄存器的地址(例如,对于 UART 接口,其数据寄存器 (DR) 的地址)。由于 DMA 内存传输通常涉及多个字节,因此可以对 DMA 进行编程,以自动增加传输的每个字节的外设寄存器。当执行 memory-to-memory 传输时,以及当外设是字节、半字和字可寻址时(就像外部 SRAM 存储器一样),都是如此。在这种情况下,字段采用值 DMA_PINC_ENABLE,否则为 DMA_PINC_DISABLE。

        MemInc:该字段与 PeriphInc 字段含义相同,但涉及内存端口。它可以假设值 DMA_MINC_ENABLE 以表示指定的内存地址必须在传输每个字节后递增,或者DMA_MINC_DISABLE值在每次传输后保持不变。

        PeriphDataAlignment:外设和内存的传输数据大小可以通过此字段和下一个字段完全编程。DMA 控制器旨在当源和目标数据大小不同时自动执行数据对齐(打包/解包)。

        MemDataAlignment:它指定内存传输数据大小。

        Mode:STM32 MCU 中的 DMA 控制器有两种工作模式:DMA_NORMAL 和 DMA_CIRCULAR。在正常模式下,DMA 将指定数量的数据从源端口发送到目标端口并停止活动。必须再次重新准备它才能执行另一次传输。在循环模式下,在传输结束时,它会自动重置传输计数器,并从源缓冲区的第一个字节再次开始传输(即,它将源缓冲区视为环形缓冲区)。这种模式也称为连续模式,它是在某些外设(例如快速 SPI 设备)中实现高传输速度的唯一方法。

        Priority:DMA 控制器的一个重要功能是能够为每个流分配优先级,以便管理并发请求。如果来自连接到具有相同优先级的流的外围设备的并发请求,则首先触发编号较小的流。

        FIFOMode:用于使用 DMA_FIFOMODE_ENABLE / DMA_FIFOMODE_DISABLE 宏启用/禁用 DMA FIFO 模式。在 STM32F2/F4/F7 MCU 中,每个流都有一个独立的 4 字 (4 * 32 位) FIFO。FIFO 用于在将来自源的数据传输到目标之前临时存储数据。禁用时,使用 Direct 模式(这是其他 STM32 MCU 中提供的“正常”模式)。

        FIFO 模式引入了几个优点:它减少了 SRAM 访问,因此为其他 masters 提供了更多时间来访问 Bus Matrix,而无需额外的并发;它允许软件进行突发事务,从而优化传输带宽;它允许打包/解包数据以适应源和目标数据宽度,而无需额外的 DMA 访问。

        如果启用了 DMA FIFO,则可以使用数据打包/解包和/或 Burst 模式。FIFO 根据阈值水平自动清空(即传输其存储的数据到目标)。此级别可通过软件在 1/4、1/2、3/4 或全尺寸之间进行配置。(当FIFO中存储了FIFO总容量的1/4、1/2、3/4 或全尺寸的数据时,触发传输。)

        FIFOThreshold:它指定 FIFO 阈值水平,并且可以采用下表的值。

        MemBurst:先使用循环调度策略对DMA 流的进行选择,DMA 流才可以通过 AHB 总线传输字节序列。这会 “减慢” 传输操作,对于某些高速外围设备,它可能是一个瓶颈。突发传输允许 DMA 流重复传输数据,而无需完成像在单独事务中传输每条数据所需的所有步骤。burst 模式与 FIFO 结合使用,FIFO 作为一个临时缓冲区,可以存储从源到目标传输过程中的数据,而 burst 模式则允许 DMA 控制器在不需要每次传输都重新配置的情况下连续传输多个数据块。MemBurst 表示DMA控制器连续传输的数据块的数量,它由字节、半字和字组成,具体取决于源配置(MemDataAlignment 字段的设置)。MemBurst 字段可以采用下表中的一个值。

PeriphBurst:此字段与前一个字段具有相同的含义,但它与外设内存传输有关。它可以采用下表中的值。

2.4、 如何在轮询模式下执行 DMA 传输

        配置 DMA 通道/流后,我们还必须做一些其他事情:在内存和外设端口上设置地址;指定要传输的数据量;设置DMA;在相应的外设上启用 DMA 模式。

        前三点的实现,HAL 使用

HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);

而第四点取决于外设,我们必须查阅我们的特定 MCU 数据表。但是,HAL 也实现了这一点(例如,在 DMA 模式下配置 UART 时,使用相应的 HAL_UART_Transmit_DMA() 函数)。

        接下来,我们将要做的是使用 DMA 模式通过 UART1 外设发送字符串。涉及的步骤是: UART1 是使用 HAL_UART 模块配置的;DMA1 通道配置为执行内存到外设的传输;相应的通道被设置,并且在 DMA 模式下启用 UART。

#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include "string.h"

extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_tx;
char msg[] = "Hello STM32 Lovers! This message is transferred in DMA Mode.\r\n";
	
void SystemClock_Config(void);	
	
int main(void) {
	/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
	HAL_Init();
	
	/* Configure the system clock */
	SystemClock_Config();
	
	/* Initialize all configured peripherals */
	MX_GPIO_Init();
	MX_USART1_UART_Init();
	MX_DMA_Init();
	
	/* USART1_TX Init */
	/* USART1 DMA Init */
	hdma_usart1_tx.Instance = DMA1_Channel4;
	hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
	hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
	hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
	hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_usart1_tx.Init.Mode = DMA_NORMAL;
	hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
	HAL_DMA_Init(&hdma_usart1_tx);
	
	HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)msg, (uint32_t)&huart1.Instance->DR, strlen(msg));
	//Enable UART in DMA mode
	huart1.Instance->CR3 |= USART_CR3_DMAT;
	//Wait for transfer complete
	HAL_DMA_PollForTransfer(&hdma_usart1_tx, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
	//Disable UART DMA mode
	huart1.Instance->CR3 &= ~USART_CR3_DMAT;
	//Turn LED ON
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
	
	/* Infinite loop */
	while (1);
}

        首先,我们将 DMA1_Channel4 配置为执行内存到外设的传输。由于 USART 外设的传输数据寄存器 (TDR) 只有一个字节宽,因此我们配置 DMA,使外设地址不会自动递增 (DMA_PINC_DISABLE),而我们希望源存储器地址在发送的每个字节 (DMA_MINC_ENABLE) 时自动递增。配置完成后,我们调用 HAL_DMA_Init() ,它根据 hdma_usart1_tx 中提供的信息执行 DMA 接口配置Init 结构。接下来,我们调用 HAL_DMA_Start()函数,该函数配置源内存地址(即 msg 数组的地址)、目标外设地址(即 USART1->TDR 寄存器的地址)以及我们将要传输的数据量。DMA 现在已经准备好了,我们通过设置 USART1 外设的相应位来开始传输。最后,请注意,函数 MX_DMA_Init()使用宏 __HAL_RCC_DMA1_CLK_ENABLE() 来启用 DMA1 控制器(请记住,几乎每个 STM32 内部模块都必须使用 __HAL_RCC_<PERIPHERAL>_CLK_ENABLE() 宏来启用)。

        由于我们不知道完成传输过程需要多长时间,因此我们使用以下函数:

HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma, uint32_t CompleteLevel, uint32_t Timeout);

它会自动等待完全传输完成。这种以 DMA 模式发送数据的方式在 ST 官方文档中称为 “轮询模式”。传输完成后,我们禁用 UART2 DMA 模式并打开 LED。

2.5、如何在中断模式下执行 DMA 传输

        从性能的角度来看,轮询模式下的 DMA 传输毫无意义,除非我们的代码不需要等待传输完成。如果我们的目标是提高整体性能,那么没有理由使用 DMA 控制器,然后消耗大量 CPU 周期等待传输完成。因此,最好的选择是准备 DMA,并让它在传输完成时通知我们。

        DMA 能够生成与通道活动相关的中断(例如,STM32F072 MCU 中的 DMA1 有一个用于通道 1 的 IRQ,一个用于通道 2 和 3,一个用于通道 4 到 7 的 IRQ)。此外,三个独立的使能位可用于在半传输、完全传输和传输错误时启用 IRQ。

        可以按照以下步骤在中断模式下启用 DMA:

        定义三个充当回调例程的函数,并将它们传递给DMA_HandleTypeDef处理程序中的函数指针 XferCpltCallback、XferHalfCpltCallback 和 XferErrorCallback(可以只定义我们感兴趣的函数,但将相应的指针设置为 NULL,否则可能会发生奇怪的错误);

        记下与正在使用的通道关联的 IRQ 的 ISR,并调用 HAL_DMA_IRQHandler() 将引用传递给 DMA_HandleTypeDef 处理程序;

        在 NVIC 控制器中启用相应的 IRQ;

        使用函数 HAL_DMA_Start_IT(),该函数会自动为您执行所有必要的设置步骤,并向其传递与 HAL_DMA_Start() 相同的参数。

        关于 XferCpltCallback、XferHalfCpltCallback 和 XferErrorCallback 回调,请务必注意:我们需要在使用 DMA 而没有 CubeHAL 中介的情况下设置它们。假设我们在 DMA 模式下使用 UART1。如果我们自己进行 DMA 管理,那么可以定义这些回调例程,并在每次发生传输时管理必要的 UART 中断相关配置。但是,如果我们使用的是 HAL_UART_Trasmit_DMA()/HAL_UART_Receive_DMA() 例程,则 HAL 已经正确定义了这些回调,我们不必更改它们。相反,例如,要捕获 UART 的 DMA 完成事件,我们需要定义函数 HAL_UART_RxCpltCallback()。

        以下示例说明如何在中断模式下进行 DMA 内存到外设的传输。

#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include "string.h"

extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_tx;
char msg[] = "Hello STM32 Lovers! This message is transferred in DMA Mode.\r\n";
	
void SystemClock_Config(void);	
void DMATransferComplete(DMA_HandleTypeDef *hdma);
	
int main(void) {
	/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
	HAL_Init();
	
	/* Configure the system clock */
	SystemClock_Config();
	
	/* Initialize all configured peripherals */
	MX_GPIO_Init();
	MX_USART1_UART_Init();
	MX_DMA_Init();
	
	/* USART1_TX Init */
	/* USART1 DMA Init */
	hdma_usart1_tx.Instance = DMA1_Channel4;
	hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
	hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
	hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
	hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_usart1_tx.Init.Mode = DMA_NORMAL;
	hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
	hdma_usart1_tx.XferCpltCallback = &DMATransferComplete;
	HAL_DMA_Init(&hdma_usart1_tx);
	
	/* DMA interrupt init */
	HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
	
	HAL_DMA_Start_IT(&hdma_usart1_tx, (uint32_t)msg, (uint32_t)&huart1.Instance->DR, strlen(msg));
	//Enable UART in DMA mode
	huart1.Instance->CR3 |= USART_CR3_DMAT;
	//Wait for transfer complete
	//HAL_DMA_PollForTransfer(&hdma_usart1_tx, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
	
	
	/* Infinite loop */
	while (1);
}

void DMATransferComplete(DMA_HandleTypeDef *hdma) {
	if (hdma->Instance == DMA1_Channel4) {
		//Disable UART DMA mode
		huart1.Instance->CR3 &= ~USART_CR3_DMAT;
		//Turn LED ON
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
	}
}

2.6、将 HAL_UART 模块与 DMA 模式传输一起使用

        在前面文章,我们省略了如何在 DMA 模式下使用 UART。我们已经在前面的段落中看到了如何做到这一点。但是,我们不得不使用一些 USART 寄存器才能在 DMA 模式下启用外设。        

        HAL_UART 模块旨在从所有底层硬件细节中抽象出来。使用它所需的步骤如下:

        配置硬连线到将要使用的 UART 的 DMA 通道/流;

        使用 __HAL_LINKDMA() 将 UART_HandleTypeDef 链接到 DMA_HandleTypeDef;

        启用与正在使用的通道/流相关的 DMA 中断,并从其 ISR 调用 HAL_DMA_IRQHandler() 例程;

        启用 UART 相关中断并从其 ISR 调用 HAL_UART_IRQHandler() 例程;(启用 UART 相关中断并调用 HAL_UART_IRQHandler() 例程非常重要,因为 HAL 的设计使得即使在 DMA 模式下驱动 UART 时,也可能引发与 UART 相关的错误(如奇偶校验错误、溢出错误等)。通过捕获错误情况,HAL 会暂停 DMA 传输,并调用相应的错误回调,以向应用层发出错误情况信号。)

        使用 HAL_UART_Transmit_DMA() 和 HAL_UART_Receive_DMA() 函数通过 UART 交换数据,并准备好收到 HAL_- UART_RxCpltCallback() 的传输完成通知。

        以下代码显示了如何在DMA 模式下从 UART1 接收三个字节:

#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"

extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
	
void SystemClock_Config(void);	
	
uint8_t dataArrived = 0;
uint8_t data[3];
	
int main(void) {
	/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
	HAL_Init();
	
	/* Configure the system clock */
	SystemClock_Config();
	
	/* Initialize all configured peripherals */
	MX_GPIO_Init();
	MX_USART1_UART_Init();
	MX_DMA_Init();
	
	/* USART1_RX Init */
	/* USART1 DMA Init */
	hdma_usart1_rx.Instance = DMA1_Channel5;
	hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
	hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
	hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
	hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_usart1_rx.Init.Mode = DMA_NORMAL;
	hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
	HAL_DMA_Init(&hdma_usart1_rx);
	
	/* DMA interrupt init */
	HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
	
	//Link the DMA descriptor to the UART1 handle
	__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
	
	/* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
	
	//Receive three bytes from UART1 in DMA mode
	HAL_UART_Receive_DMA(&huart1, (uint8_t *)&data, 3);	
	
	while(!dataArrived); //Wait for the arrival of data from UART
	
	/* Infinite loop */
	while (1);
}

        HAL_UART_RxCpltCallback() 到底在哪里调用?在前面的段落中,我们已经看到DMA_HandleTypeDef包含一个指针(名为 XferCpltCallback),该指针指向一个函数,当 DMA 传输完成时,该函数由 HAL_DMA_IRQHandler() 例程调用。但是,当我们为给定的外设(在本例中为 HAL_UART)使用 HAL 模块时,我们不需要提供自己的回调:它们由 HAL 在内部定义,HAL 使用它们来执行其活动。HAL 使我们能够定义相应的回调函数(HAL_UART_RxCpltCallback() 用于 DMA 模式下的 UART_RX 传输),这些函数将由 HAL 自动调用,如下图所示。此规则适用于所有 HAL 模块。

        如上所见,一旦掌握了 DMA 控制器的工作原理,使用这种传输模式的外设就很简单了。

2.7、使用 CubeHAL 对 DMAMUX 进行编程

        STM32G4 MCU,该MCU提供DMAMUX外设。这让我们有机会了解如何使用 CubeHAL 对 DMAMUX 进行编程,以及如何使用外部同步信号同步请求传输。我们这里要分析的例子和前面一个类似:USART2 在 DMA 模式下用来传输一堆字符。然而,这一次,传输与 EXTI13 线路同步,该中断线路连接到 PC13 GPIO,该线路连接到板中的 USER 按钮。

	/* USART2_TX Init */
	/* USART2 DMA Init */
	hdma_usart2_tx.Instance = DMA1_Channel7;
	hdma_usart2_tx.Init.Request = DMA_REQUEST_USART2_TX;
	hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
	hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;
	hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
	hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_usart2_tx.Init.Mode = DMA_CIRCULAR;
	hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;
	HAL_DMA_Init(&hdma_usart2_tx);
	
	pSyncConfig.SyncSignalID = HAL_DMAMUX1_SYNC_EXTI13;
	pSyncConfig.SyncPolarity = HAL_DMAMUX_SYNC_FALLING;
	pSyncConfig.SyncEnable = ENABLE;
	pSyncConfig.EventEnable = ENABLE;
	pSyncConfig.RequestNumber = strlen(msg);
	HAL_DMAEx_ConfigMuxSync(&hdma_usart2_tx, &pSyncConfig);
	
	__HAL_LINKDMA(&huart2,hdmatx,hdma_usart2_tx);
	
	/* DMA interrupt init */
	/* DMA1_Channel7_IRQn interrupt configuration */
	HAL_NVIC_SetPriority(DMA1_Channel7_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(DMA1_Channel7_IRQn);
	
	HAL_UART_Transmit_DMA(&huart2, (uint8_t*)msg, strlen(msg));
	
	/* Infinite loop */
	while (1);
}
	
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
	if(huart->Instance == USART2) {
		//Turn LED ON
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
	}
}

        USART2_TX 请求源绑定到 DMA1_Channel7,但由于 DMAMUX 的原因,它可以连接到任何 DMA 通道。这次 DMA 配置为在循环模式下工作,原因稍后会很清楚。通道绑定到 EXTI13 同步信号:这意味着每次 GPIO13 电平从上到下电平时,都会进行传输。最后,EXTI15_10_IRQn 和 DMA1_Channel7_IRQn IRQ 都被启用,并且 DMA 通过使用 HAL_UART_Transmit_DMA() 进行开启。

        上面的代码将导致每次按下 USER 按钮时都会产生 UART2_TX 请求信号,因为 DMA 是以循环模式配置的。相应的消息将打印在串行控制台上。请注意,DMA 目标地址是 1 字节的 USART2->TDR Trasmit 数据寄存器。若要允许 DMA 传输 msg 变量中包含的所有字符,请将 pSyncConfig.RequestNumber 参数设置为消息的长度。这将导致 DMA 将自动触发 strlen (msg) 次。但是,这意味着字符串不能超过 32 个字符。完成所有字符的传输后,将触发 DMA1_Channel7 中断。这将导致HAL_UART_TxCpltCallback() 被调用,反转LED 的状态。

2.8、HAL_DMA 和 HAL_DMA_Ex 模块中的其他功能

        HAL_DMA 模块提供了其他有助于使用 DMA 控制器的功能。

HAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma);

此函数将禁用 DMA 流/通道。如果在数据传输过程中禁用了流,则将传输当前数据,并且只有在此单个数据的传输完成后,该流才会被有效禁用。

        一些 STM32 MCU 可以执行多缓冲区 DMA 传输,这允许在传输过程中使用两个单独的缓冲区:当到达第一个缓冲区的末尾时,DMA 将自动从第一个缓冲区(名为 memory0)跳转到第二个缓冲区(名为 memory1)。当 DMA 在循环模式下工作时,这尤其有用。函数:

HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength);

用于设置多缓冲区 DMA 传输。它仅在 F2/F4/F7 HAL 中可用。相应的 HAL_DMAEx_MultiBufferStart_IT() 也可用,它也负责启用 DMA 中断。

        函数:

HAL_StatusTypeDef HAL_DMAEx_ChangeMemory(DMA_HandleTypeDef *hdma, uint32_t Address, HAL_DMA_MemoryTypeDef memory);

在多缓冲区 DMA 事务中动态更改 memory0 或 memory1 地址。

3、使用 CubeMX 配置 DMA 请求

        CubeMX 可以将设置通道/流请求所需的工作量降至最低。在 Pinout 部分启用外设后,进入 System view 部分并单击 DMA 按钮。此时将显示 DMA Mode and Configuration 窗格,如下图所示。

第一个与外设请求有关。例如,如果要在传输模式下启用 USART2 的 DMA 请求(以执行内存到外设的传输),请单击 Add 按钮,然后选择 USART2_TX 条目。CubeMX 将自动为您填写其余字段,选择正确的通道。然后,可以为请求分配优先级,并设置其他内容,如 DMA 模式、外设/内存增量、DMAMUX 相关设置等。同样,可以配置 DMA 通道/流来执行内存到内存的传输。CubeMX 将为 stm32XXxx_hal_msp.c 文件中使用的请求/通道自动生成正确的初始化代码。

4、正确分配 DMA 缓冲区

        查看本章中介绍的所有示例的源代码,会发现 DMA 缓冲区(即用于执行内存到外设和外设到内存传输的源和目标数组)始终在全局范围内分配。为什么要这样做?

        这是所有新手迟早都会犯的常见错误。当我们在本地范围(即在被调用例程的堆栈帧上)声明变量时,只要该堆栈帧处于活动状态,该变量就会“存活”。当被调用的函数退出时,已分配变量的堆栈区域将重新分配给其他用途(以存储下一个被调用函数的参数或其他局部变量)。如果我们使用局部变量作为 DMA 传输的缓冲区(即,我们将堆栈中内存位置的地址传递给 DMA 内存端口),那么 DMA 很可能会访问包含其他数据的内存区域,如果我们进行外设到内存的传输,则会损坏该内存区域。 除非我们确定该函数永远不会从堆栈中弹出(这可能是在 main() 函数中声明的变量的情况)。

        上图清楚地显示了本地分配的变量 (lbuf) 和在全局范围内分配的变量 (gbuf) 之间的差异。只要 func1() 在堆栈上,lbuf 就会处于活动状态。如果要避免在应用程序中使用全局变量,则可以通过将其声明为 static 来表示另一种解决方案。静态变量在 .data 区域(图中的全局数据区域)内自动分配,即使它们的 “可见性” 在本地范围内受到限制。

5、案例研究:DMA 内存到内存传输性能分析

        DMA 控制器还可用于执行内存到内存传输。例如,它可用于将大量数据从 flash memory 移动到 SRAM,或复制 SRAM 中的数组,或将内存区域归零。C 库通常提供一组函数来完成此任务。memcpy() 和 memset() 是最常见的。你可以找到几个测试,它们在 memcpy()/memset() 例程和 DMA 传输之间进行性能比较。这些测试中的大多数都声称 DMA 通常比 Cortex-M 内核慢得多。这是真的吗?答案是:视情况而定。那么,既然您已经有了这些例程,为什么还要使用 DMA?这些测试背后的故事要复杂得多,它涉及几个因素,如内存对齐、使用的 C 库和正确的 DMA 设置。让我们考虑以下测试应用程序(代码设计为在 STM32F4 MCU 上运行),分为几个阶段:

DMA_HandleTypeDef hdma_memtomem_dma2_stream0;
	
const uint8_t flashData[] = {0xe7, 0x49, 0x9b, 0xdb, 0x30, 0x5a, ...};
uint8_t sramData[1000];
	
int main(void) {
	HAL_Init();
	BSP_Init();
	
	hdma_memtomem_dma2_stream0.Instance = DMA2_Stream0;
	hdma_memtomem_dma2_stream0.Init.Channel = DMA_CHANNEL_0;
	hdma_memtomem_dma2_stream0.Init.Direction = DMA_MEMORY_TO_MEMORY;
	hdma_memtomem_dma2_stream0.Init.PeriphInc = DMA_PINC_ENABLE;
	hdma_memtomem_dma2_stream0.Init.MemInc = DMA_MINC_ENABLE;
	hdma_memtomem_dma2_stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
	hdma_memtomem_dma2_stream0.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_memtomem_dma2_stream0.Init.Mode = DMA_NORMAL;
	hdma_memtomem_dma2_stream0.Init.Priority = DMA_PRIORITY_LOW;
	hdma_memtomem_dma2_stream0.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
	hdma_memtomem_dma2_stream0.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
	hdma_memtomem_dma2_stream0.Init.MemBurst = DMA_MBURST_SINGLE;
	hdma_memtomem_dma2_stream0.Init.PeriphBurst = DMA_MBURST_SINGLE;
	HAL_DMA_Init(&hdma_memtomem_dma2_stream0);
	
	GPIOC->ODR = 0x100;
	HAL_DMA_Start(&hdma_memtomem_dma2_stream0, (uint32_t)&flashData, (uint32_t)sramData, 1000);
	HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
	GPIOC->ODR = 0x0;
	
	
	
	while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin));
	
	hdma_memtomem_dma2_stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
	hdma_memtomem_dma2_stream0.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
	
	HAL_DMA_Init(&hdma_memtomem_dma2_stream0);
	
	GPIOC->ODR = 0x100;
	HAL_DMA_Start(&hdma_memtomem_dma2_stream0, (uint32_t)&flashData, (uint32_t)sramData, 250);
	HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
	GPIOC->ODR = 0x0;
	
	HAL_Delay(1000); /* This is a really primitive form of debouncing */
	
	while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin));
	
	GPIOC->ODR = 0x100;
	memcpy(sramData, flashData, 1000);
	GPIOC->ODR = 0x0;

        这里我们有两个相当大的数组。其中之一 flashData 是通过 const 修饰符在闪存中分配的。我们想将其内容复制到 sramData 数组中,顾名思义,该数组存储在 SRAM 中,我们想使用 DMA 和 memcpy() 函数测试需要多长时间。

        首先,我们开始测试 DMA。hdma_memtomem_dma2_stream0 句柄用于配置 DMA2 stream0/channel0 以执行内存到内存的传输。在第一阶段,我们将 DMA 流配置为执行字节对齐的内存传输。DMA 配置完成后,我们开始传输。使用连接到 PC8 引脚的示波器,我们可以测量传输需要多长时间。按下 USER 按钮(连接到 PC13)会导致另一个测试阶段的开始。这次我们配置 DMA 以执行字对齐传输。最后,我们使用 memcpy() 测试复制数组需要多长时间。

        上表显示了每个 Nucleo 板获得的结果。让我们关注 NucleoF401RE 板。DMA M2M 字节对齐传输需要 ∼42 μS,而 DMA M2M 字对齐传输需要 ∼14 μS。这是一个很大的加速,它证明使用正确的 DMA 配置可以给我们带来最佳的传输性能,因为我们每次 DMA 传输都会一次移动 4 个字节。memcpy() 呢?从表中可以看出,这取决于所使用的 C 库。我们使用的 GCC 工具链提供了两个 C 运行时库:一个名为 newlib,另一个名为 newlib-nano。第一个是两者中最完整和速度优化的,而第二个是缩小版。newlib 库中的 memcpy() 旨在提供最快的复制速度,但代价是代码大小。它会自动检测字对齐传输,并且在进行字对齐 M2M 传输时等于 DMA。因此,在进行字节对齐的 M2M 传输时,它比 DMA 快得多,这就是为什么有人声称 memcpy() 总是比 DMA 快的原因。另一方面,Cortex-M 内核和 DMA 都需要使用相同的总线访问闪存和 SRAM 内存。因此,没有理由 MCU 内核应该比 DMA更快。(这里排除了 Cortex-M 内核和 SRAM 之间的一些“特权路径”。这就是内核耦合存储器 (CCM) 的作用。)

        当 DMA stream/channel 禁用内部 FIFO 缓冲器 (∼12 μS) 时,可实现最快的传输速度。需要注意的是,对于具有较小闪存的 STM32 MCU,newlib-nano 几乎是一个不可避免的选择,除非代码可以适应闪存空间。但同样,使用正确的 DMA 设置,我们可以实现与 newlib 库中可用的速度优化版本相同的性能。

        我们要分析的最后一件事是表中的最后一列。它显示了使用如下简单循环进行内存传输需要多长时间:

...
GPIOC->ODR = 0x100;
for(int i = 0; i < 1000; i++)
    sramData[i] = flashData[i];
GPIOC->ODR = 0x0;
...

在最大优化级别 (-O3) 下,它花费的时间与 memcpy() 完全相同。为什么会这样?

...
GPIOC->ODR = 0x100;
8001968: f44f 7380 mov.w r3, #256 ; 0x100
800196c: 6163 str r3, [r4, #20]
800196e: 4807 ldr r0, [pc, #28] ; (800198c <main+0x130>)
8001970: 4907 ldr r1, [pc, #28] ; (8001990 <main+0x134>)
8001972: f44f 727a mov.w r2, #1000 ; 0x3e8
8001976: f000 f92d bl 8001bd4 <memcpy>
for(int i = 0; i < 1000; i++)
    sramData[i] = flashData[i];
GPIOC->ODR = 0x0;
800197a: 6165 str r5, [r4, #20]
...

        查看上面生成的汇编代码,您可以看到编译器在调用 memcpy() 函数时自动转换循环。这清楚地解释了为什么他们有相同的表现。

        上表显示了另一个有趣的结果。对于STM32F152RE MCU,newlib 中的 memcpy() 始终比 DMA M2M 快两倍。不知道为什么会这样,但已经执行了几次测试,我可以确认结果。

        最后,此处未报告的其他测试表明,当数组具有 30-50 个以上的元素时,使用 DMA 进行 M2M 传输很方便,否则 DMA 设置成本超过与其使用相关的收益。但是,需要注意的是,使用 DMA M2M 传输的另一个优点是,当 DMA 执行传输时,CPU 可以自由完成其他任务,即使它对总线的访问会降低整体 DMA 性能。


http://www.kler.cn/a/388217.html

相关文章:

  • 【Linux】进程池实现指南:掌控并发编程的核心
  • C# 集合与泛型
  • ORA-01092 ORA-14695 ORA-38301
  • 客户手机号收集小程序有什么用
  • 测试实项中的偶必现难测bug--<pre>标签问题
  • ManiSkill学习笔记
  • 【科普小白】LLM大语言模型的基本原理
  • 《Linux运维总结:基于银河麒麟V10+ARM64架构CPU部署redis 6.2.14 TLS/SSL哨兵集群》
  • Ubuntu学习笔记 - Day3
  • excel常用技能
  • C++ | 表示移动函数move()的基本用法
  • 【Golang】Go语言教程
  • 【leetcode练习·二叉树】用「分解问题」思维解题 I
  • mysql 配置文件 my.cnf 增加 lower_case_table_names = 1 服务启动不了
  • 【前端】JavaScript 方法速查大全-DOM、BOM、时间、处理JS原生问题(三)
  • C++学习笔记----11、模块、头文件及各种主题(一)---- 模板概览与类模板(1)
  • python opencv灰度变换
  • Docker部署Oracle 11g
  • selinux与防火墙
  • 【1】虚拟机安装
  • 开源模型应用落地-glm模型小试-glm-4-9b-chat-vLLM集成(四)
  • 快速傅里叶变换(FFT)基础(附python实现)
  • Go语言异常处理
  • Windows配置NTP时间同步
  • Docker:镜像构建 DockerFile
  • Spring 配置绑定原理分析