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

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

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

示例代码如下:

Python 复制代码
# 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 通道的控制寄存器的完整配置:

Python 复制代码
print("gather_ctrl:",gather_dma.unpack_ctrl(gather_ctrl))

可以看到,这里设置 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 寄存器位于最后一个寄存器别名(将字符串列表中单个字符串长度和地址传入对应寄存器)**

即控制 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

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

在实际数据传输过程中:

  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 中的所有字符串都被处理完。

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

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

相关推荐
Freak嵌入式1 小时前
MicroPython ADC 采样拖慢主循环?试试 DMA + 乒乓缓冲,全程解放 CPU
嵌入式
Freak嵌入式2 小时前
从代码到项目:MicroPython+MCP4725 自定义类实现信号发生器功能的完整流程
嵌入式
与你4033 小时前
STM32F103_AB_OTA_从零复现教程
嵌入式
Freak嵌入式3 小时前
一文搞懂 MCP4725 波形发生器:MicroPython 代码实现、定时器驱动与调试流程全梳理
嵌入式
慧都小项3 小时前
汽车工具链重复测试太耗人?用 TestComplete 自动跑
功能测试·回归·汽车·嵌入式·软件工程·测试覆盖率
Discipline~Hai4 小时前
Linux网络编程02-TCP协议
linux·网络·tcp/ip·嵌入式·linux网络编程
Jason_zhao_MR5 小时前
电能质量监测为什么需要PTP精确时钟同步
linux·单片机·嵌入式硬件·嵌入式
Freak嵌入式6 小时前
全网最详细|树莓派 Pico ADC 软件控制全解析:从machine.ADC API 到定时温度采集实战(含 ISR 避坑指南)
嵌入式
嵌入式阿蔡9 小时前
存储_14:嵌入式存储测试岗面试——可靠性/兼容/车规面试题与项目话术
单片机·嵌入式硬件·面试·嵌入式·存储