在以下代码中,我们通过 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_dma 和 buffer_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。
以上链式触发详细时序图如下所示:

在实际数据传输过程中:

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

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