MicroPython DMA链式触发,硬件实现Scatter-Gather数据聚合


在这里插入图片描述
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/HHX_01

前言

先问一个送命题:手上有六个字符串,想拼成一句完整的话,你打算怎么写?

正常人:for 循环加个加号,三分钟收工。
我:先造两个 DMA 通道,配一个环形缓冲,再给它们拴一根链子,让硬件自己干,CPU 全程在旁边喝茶。

对,我就是那种能把一秒钟的活干成一下午的人。
但你先别急着笑。这个实验跑通的那一刻,我甚至觉得 CPU 终于活成了我理想中的样子——上班不用干活,全靠下属自觉。

1. 这实验到底在干嘛

先把需求说成人话:散落在内存各个角落的字符串,要聚到一个缓冲区里,拼成一句完整的话。专业术语叫 Scatter-Gather,翻译过来就是四个字:化零为整。

这跟把散落一地的乐高零件收进一个盒子,本质上是同一件事。区别在于:乐高收错了顶多拼不出城堡,内存地址搞错了,轻则乱码,重则直接重启。两种我都见过,都很精彩。

1.1 为什么不让 CPU 亲自搬

理论上,这活 CPU 三行代码就能干完。但你想,CPU 什么身份?嵌入式世界的老板。

老板的职责是开会、拍板、应对突发状况——对应到单片机,就是跑主循环、处理中断、响应按键。你让老板亲自搬砖,一次搬六块,它累不累先不说,ADC、UART、定时器这些员工全在工位上干等,生产线直接停摆。

所以正确的职场逻辑是:老板把活儿派给中层(DMA),中层自己调度,老板只需要在验收的时候说一句"干得不错"。

1.2 双人组合:一个动嘴,一个动手

整个方案的核心是两个 DMA 通道,分工明确,谁也不越界:

  • gather_dma:负责念清单。每个字符串多长、住在哪个地址,它一项一项报。
  • buffer_dma:负责搬货。听到地址就去取货,搬到目标缓冲区放下。

一个动嘴,一个动手。这组合让我想起我和我同事的日常:他负责说"这活很简单",我负责加班。

2. 准备工作:内存里的通讯录

2.1 gather_list:登记个人信息

开工之前,先把六个字符串的"个人信息"登记到一张表里,也就是 gather_list 数组。登记格式很讲究:长度、地址、长度、地址……成对出现。

长度用 len(s),地址用 addressof(s)。注意,addressof 拿到的是字符串在内存里的门牌号,不是家门口的快递柜号。搞混了,DMA 会跑去邻居家搬货。

2.2 名单结尾的两个 0,是暗号

数组末尾追加了两个 0。这不是写嗨了顺手多打的,这叫空触发(Null Trigger)。

你可以把它理解为名单最后一行的"以上"。DMA 念到 0 就知道:名单念完了,收工。比领导的"我就简单说两句"可靠多了——它说停,是真停。

2.3 那两个神秘的寄存器

gather_dma 把数据写进哪?buffer_dma 的 14 号和 15 号寄存器。一个叫 TRANS_COUNT,管传输次数;一个叫 READ_ADDR_TRIG,管读取地址。

重点是这个 TRIG。往这个寄存器里写个非零值,buffer_dma 就自动开机干活。写地址等于按启动键,物理世界里按一下就行,这里写一下就启动,还挺讲道理。

3. 让 DMA 学会原地打转

接下来是最反直觉的部分:寄存器只有两个坑位,写一轮就得回到起点再写一轮。所以得给 DMA 开跑步机模式:环形缓冲(ring mode)。跑完一圈自动回到原点,永远在同一台跑步机上踩步。

3.1 ring_size=3:强迫症的胜利

ring_size=3 是什么意思?2 的 3 次方等于 8,也就是 8 字节对齐。传输粒度是 4 字节(size=2 表示 32 位),一次传两个(count=2),凑成一对正好 8 字节。

为什么非要是 8?因为每一对数据——长度占 4 字节,地址占 4 字节——加起来不多不少 8 字节,一对一个坑,整整齐齐。

我严重怀疑 DMA 硬件工程师当年也是强迫症:连对齐尺寸都算得让你挑不出毛病。

3.2 读地址前进,写地址原地踏步

ring_sel=True 的含义是:环形模式只对写地址生效。

读地址照常递增——念完一家换下一家;写地址原地打转——永远写进同一对寄存器。

一个负责前进,一个负责原地待命。这大概就是团队协作的真谛:有人往前冲,有人守在坑里,谁也别抢谁的活。

4. 链式触发:你干完,我上

现在到了全场最骚的操作:chain_to。

buffer_dma 的配置里写了 chain_to=gather_dma.channel。意思很直白:我干完这一单,自动把下一个同事喊醒,不用老板催。

这就叫链式触发。像接力赛,交接棒都不用喊,前一棒跑完,后一棒自己起跑。

4.1 完整流程,比流水线还流水线

把整个流程串起来看:

  1. CPU 把两个 DMA 都配置好,喊一声"开工"——gather_dma.active(1)
  2. gather_dma 从通讯录里念第一组:长度 + 地址。
  3. 数据写进 buffer_dma 的 14、15 号寄存器。写 READ_ADDR_TRIG 这一步等于按下启动键,buffer_dma 立刻开跑。
  4. buffer_dma 按地址取货,按长度搬运,货物落进目标缓冲区。
  5. 搬完,链子自动触发,gather_dma 接着念下一组。
  6. 如此循环,直到撞上名单末尾的两个 0。

全程 CPU 就干三件事:配置、喊开工、然后站在旁边等。这要是在公司,属于典型的"领导已经把活安排明白了"。

5. 代码,拿去烧

废话不多说,完整代码在这。环境是 MicroPython v1.23.0,板子树莓派 Pico(RP2040)。代码里注释写得比我文章还细,重点看 gather_strings 这个函数,核心就五步:登记、配控制块、配链式、开工、干等。

# 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
from uctypes import addressof
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()   # 用来处理字符串长度和地址信息
    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)

至于那个 while gather_dma.read != end_address: pass——对,空语句,纯干等。我称之为"老板式监控":不看过程,只看结果。

6. 跑起来看看

烧录、连串口、mpremote connect COM8,回车。屏幕上先滚出三样东西:

  • 初始的目标缓冲区:全是 0,像一张没写完的作业。
  • gather_list:一排长度和地址,数字大得像我银行卡余额的位数。
  • gather_ctrl:一长串配置,谁看谁头大。

最后一行是拼好的字符串:

FreakStudio: DMA Chaining Triggers Test
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
gather_list: array('I', [11, 536906720, 1, 536904296, 4, 536904298, 15, 536906752, 7, 536904303, 8, 536904311])
gather_ctrl: {'inc_read': 1, 'high_pri': 1, '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}
bytearray(b'FreakStudio:test of the scatter gather process\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

看到最后这行的瞬间,我差点从椅子上站起来。不是激动,是怕死机——毕竟我那个 while 循环还在那傻等着呢。

7. 图啥?图个安静

你可能会问:绕这么大一圈,就为了拼一个字符串?for 循环三行代码的事,你搁这儿表演杂技?

对,就是图个安静。

数据搬运这种"量大、无脑、重复"的活,交给 DMA 之后,CPU 就能腾出手来干正事——刷屏幕、解析协议、处理中断,样样都在排队等它。

说白了:重复劳动外包给硬件,CPU 只负责思考。这跟职场一个道理——琐事能甩就甩,精力留给能加薪的活。

最后提醒一句:别学我写 while 循环加 pass 干等。虽然它真能跑,但它也是真的傻。

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东离与糖宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值