基于 MicroPython+DMA 链式触发的 Scatter-Gather 数据聚合实现

在以下代码中,我们通过 DMA 传输的机制(链式触发和写入触发寄存器)实现了将多个字符串拼接到一个内存缓冲区中的操作,即 Scatter-gather 数据聚合操作,将多个固定大小的内存块聚集到一个内存空间中。

以下代码可以在我们提供的资料包中 elegance-devkit v1\Demo\69 DMA_ChainTrigger 文件夹找到。

示例代码如下:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/8 上午11:04   
# @Author  : 李清水            
# @File    : main.py       
# @Description : DMA类实验,外设到内存传输,DMA从ADC外设的FIFO传输数据到内存

# ======================================== 导入相关模块 =========================================

# 导入时间相关模块
import time
# 导入 addressof 函数,用于获取数据的内存地址
from uctypes import addressof
# 导入DMA相关模块
from rp2 import DMA
# 导入数组相关模块
from array import array

# ======================================== 全局变量 ============================================

# 定义了要拼接的字符串列表
input = ["FreakStudio", ":", "test", " of the scatter", " gather", " process"]
# 创建了 64 字节的目标缓冲区(bytearray),用于存储拼接后的字符串
output = bytearray(64)

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

def gather_strings(string_list: list, buf: bytearray) -> None:
    """
​    使用DMA将字符串列表中的字符串拼接并存入目标缓冲区。
​    通过双DMA通道协作实现:
​    1. gather_dma通道:传输字符串元数据(长度+地址)
​    2. buffer_dma通道:实际数据搬运到目标缓冲区

​    Args:
​        string_list (list): 要拼接的字符串列表,元素应为MicroPython字符串类型
​        buf (bytearray): 目标缓冲区,需预分配足够空间

​    Returns:
​        None

​    Raises:
​        ValueError: 如果输入参数类型不符合要求
​        RuntimeError: 如果DMA传输过程中发生错误
​    """

​    ​# 声明两个DMA通道:gather_dma和buffer_dma
    # gather_dma DMA 通道,用来处理字符串长度和地址信息
    gather_dma = DMA()
    # buffer_dma DMA 通道,用来处理实际的数据复制
    buffer_dma = DMA()

    # 声明一个整型数组,用来存放字符串的长度和内存地址
    gather_list = array("I")

    # 数据按顺序存储为:字符串长度、字符串地址
    for s in string_list:
        # 添加字符串长度
        gather_list.append(len(s))
        # 添加字符串的内存地址
        gather_list.append(addressof(s))

    # 打印gather_list中内容
    # gather_list:  array('I', [11, 536906720, 1, 536904296, 4, 536904298, 15, 536906752, 7, 536904303, 8, 536904311])
    print("gather_list: ", gather_list)

    # 为了标记列表的结束(以0结尾),在列表末尾添加两个0
    # 实现空触发(Null Trigger),即向触发寄存器写入全0值的操作
    gather_list.append(0)
    gather_list.append(0)

    # 配置 gather_dma 传输控制寄存器
    # ring_size设置为3,表示8字节对齐;启用寄存器环形缓冲区
    gather_ctrl = gather_dma.pack_ctrl(ring_size=3, ring_sel=True)
    # 配置 gather_dma 通道,写入地址为TRANS_COUNT寄存器和READ_ADD_TRIG寄存器位于最后一个寄存器别名
    # 即 寄存器 14 和 15
    gather_dma.config(
        read=gather_list,                   # 读取gather_list数组
        write=buffer_dma.registers[14:16],  # 将数据写入buffer_dma的寄存器:
        count=2,                            # 传输两个字节
        ctrl=gather_ctrl                    # 传输控制寄存器的设置
    )

    # 打印 gather_dma 传输控制寄存器中的参数:
    # gather_ctrl: {'inc_read': 1, 'high_pri': 0, 'write_err': 0, 'ring_sel': 1,
    # 'enable': 1, 'treq_sel': 63, 'sniff_en': 0, 'irq_quiet': 1, 'read_err': 0,
    # 'chain_to': 0, 'busy': 0, 'inc_write': 1, 'ring_size': 3, 'bswap': 0, 'size': 2, 'ahb_err': 0}
    print("gather_ctrl:",gather_dma.unpack_ctrl(gather_ctrl))

    # 设置buffer_dma传输控制寄存器,单字节传输并配置链式传输
    # 当 buffer_dma 完成时会启动 gather_dma 进行下一个传输
    buffer_ctrl = buffer_dma.pack_ctrl(size=0, chain_to=gather_dma.channel)
    # 将目标缓冲区 buf 设为 buffer_dma 的写入目标,实际字符串数据会通过此 DMA 通道被写入 buf
    buffer_dma.config(write=buf, ctrl=buffer_ctrl)

    # 启动 gather_dma 的 DMA 传输
    gather_dma.active(1)

    # 计算 gather_list 的最后一个地址
    end_address = addressof(gather_list) + 4 * len(gather_list)

    # 不停地检查 gather_dma 是否完成了传输
    while gather_dma.read != end_address:
        pass

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试信息,表示开始主程序
print("FreakStudio: DMA Chaining Triggers Test")

# ========================================  主程序  ===========================================

# 打印目标缓冲区初始时数据
print(output)
# 使用DMA将字符串列表input中的字符串存入缓存数组output
gather_strings(input, output)
# 打印目标缓冲区最终数据
print(output)

这里,实现聚合操作的核心方法是 gather_strings 函数,我们需要向其传入两个参数:要拼接的字符串列表和用于存放拼接后结果的目标缓存区。

首先,我们创建了两个 DMA 通道对象 gather_dmabuffer_dma,分别用于处理不同的任务:

  • gather_dma​: 负责将每个字符串的长度和地址传递给 buffer_dma
  • buffer_dma​: 负责实际的数据传输(复制字符串的内容到目标缓冲区)。

接着,我们定义了一个 gather_list 整型数组,用于存储每个字符串的长度和内存地址,我们使用 addressof(s) 获取字符串 s 在内存中的地址。

然后,我们配置第一个 DMA 通道 (gather_dma),我们使用 gather_dma.pack_ctrl 语句创建了 DMA 控制块,配置了 DMA 的环形缓冲区模式 (ring mode),环形缓冲区的配置允许 gather_dma 在完成一次字符串长度和地址的传输后,地址自动回到最开始的位置。这里具体配置的解释如下:

  • ring_size=3​:表示使用 8 字节对齐(1<<3,即 2^3 = 8),这意味着当写入地址偏移超过 8 字节时,地址会自动回绕到原始的 8 字节边界;简单来说,就是每个字符串和对应地址均为 32 位数据,这一对数据总共占据了 8 字节的空间,设置 ​ring_size=3​ 是为了确保在处理每个字符串时,这两个数据项作为一个整体一起传输,每次传输的 8 字节会写入 ​buffer_dma​ 的两个寄存器:一个保存长度,另一个保存地址。
  • ring_sel=True​:启用了环形缓冲区模式,并且环形缓冲模式仅在写地址生效;每当 ​gather_dma​ 将一对数据(长度和地址)写入 ​buffer_dma​ 的寄存器后,写入地址会自动回到起点。这样确保每次写入都在相同的两个寄存器中进行,避免了写入地址的递增。

这里,我们使用如下语句查看 gather_dma 通道的控制寄存器的完整配置:

print("gather_ctrl:",gather_dma.unpack_ctrl(gather_ctrl))

1.png

可以看到,这里设置 inc_read=True 确保了 gather_dma 在每次传输完成后,读取地址会递增到下一个字符串对应的地址处;也就是说,当处理完一个字符串的长度和地址之后,读取操作会移动到下一个字符串的地址,而不会回到原来的位置inc_write=True 允许其写入操作在将字符串的内容传输到目标缓冲区时,写入地址会自动递增,这样每次传输的字符串都依次被放入目标缓冲区中,避免覆盖之前的数据,同时也需要注意到ring_size=3,写入操作会在超过 8 字节的地址后,会自动回绕到最初的地址。

接着,我们配置了 gather_dma 通道:

  • read=gather_list​: DMA 将从 gather_list 中读取长度和地址
  • write=buffer_dma.registers[14:16]​: 将这些信息写入 buffer_dma 的第 14 和 15 个寄存器(这些寄存器控制目标数据的传输),这里写入地址为 TRANS_COUNT 寄存器和 READ_ADD_TRIG 寄存器位于最后一个寄存器别名(将字符串列表中单个字符串长度和地址传入对应寄存器)

2.png

即控制 buffer_dma 通道传输次数和读取目标地址的触发寄存器,在写入寄存器后,也会启动buffer_dma通道开始字符串列表中单个字符串到目标缓冲区的数据传输

  • count=2​: 每次传输两个元素(长度和地址)
  • ctrl=gather_ctrl​: 配置控制块,设置环形缓冲区

类似地,我们配置第二个 DMA 通道 (buffer_dma),使用语句 buffer_dma.pack_ctrl(size=0, chain_to=gather_dma.channel) 配置了 DMA 控制块:

  • size=0​:表示以单字节为单位进行传输。
  • chain_to=gather_dma.channel​: 当 buffer_dma 完成一次数据传输后,会自动启动 gather_dma,让 gather_dma 提供下一个字符串的长度和地址,形成一个链式操作。

然后配置 buffer_dma 将数据写入目标缓冲区 buf,并使用之前创建的控制块 buffer_ctrl

以上链式触发详细时序图如下所示:

3.png

在实际数据传输过程中:

4.png

  1. 初始触发​:我们首先启动 gather_dma 通道,开始处理第一个字符串的元数据,gather_dmagather_list 读取 len1addr1,写入 buffer_dma 的寄存器,写入 READ_ADDR_TRIG 寄存器会触发 buffer_dma 启动。
  2. 元数据传递​:gather_dma 通道写入数据到 buffer_dma 通道后,触发其开始数据传输,buffer_dma 根据收到的地址和长度,从地址为 addr1 的源字符串中复制 len1 字节数据到目标缓冲区 buf
  3. ​链式触发:​buffer_dma 完成一次数据传输后,会自动触发 gather_dma 进行下一个字符串的长度和地址传输。这种传输是链式的,即 buffer_dma 完成一段字符串的传输后,gather_dma 会接着传输下一个字符串的长度和地址,并且通过 gather_dma.read 判断 gather_dma 是否完成了所有长度和地址的传输。
  4. ​循环处理:​上述过程一直循环,直到 gather_list 中的所有字符串都被处理完。

烧录代码,连接远程终端,输出如下:

5.png

3.4 DMA链式触发.gif

可以看到,当 buffer_dma 完成一个字符串的数据传输后,它会自动触发 gather_dma 继续读取下一个字符串的长度和地址,这避免了 CPU 介入每次传输,极大地提高了效率。

posted @ 2026-09-03 16:27  FreakStudio  阅读(8)  评论(0)    收藏  举报