news 2026/9/7 5:27:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于MicroPython的DMA链式触发与Scatter-Gather数据聚合实现

MicroPython 项目做久了,你会发现一个很拧巴的事实:MicroPython 本身擅长“粘合业务”,但底层数据搬运却交给 C 写的驱动、外部协议栈和中断服务函水;一旦你想在 Python 脚本里直接编排 DMA,语言层面的无力感会瞬间冒出来。我这次要拆的课题是“基于 MicroPython+DMA 链式触发的 Scatter-Gather 数据聚合实现”,关键词拆开看就是三件事:MicroPython 应用层、DMA 链式触发、Scatter-Gather 聚合。合在一起,它解决的是“多个不连续的缓冲区,如何不发中断、不拷内存,一路被 DMA 自动搬进对应外设或 DMA 接收描述符”的问题。很多人上来就盯寄存器,我建议你先想清楚边界:硬件 DMA 可以做到无 CPU 参与地把 A、B、C 三段不连续数据凑成一次完整报文发出去,但 MicroPython 的运行时并不直接给你暴露“缓冲区的物理地址”这种底层句柄。真正可复现的做法,是让 MicroPython 负责协议拼装、缓冲池分配和事件通知,把 DMA 通道编排、描述符链构建和完成中断收敛成极少数的原生接口。下面把我的工程判断和完整实现思路写出来,这套思路在 RP2040、ESP32-S3、以及支持链式描述符的 STM32 系列上都能落地,区别只在于谁帮你管理描述符。

1. 先把 MicroPython 到底能碰 DMA 的哪一层说清楚

1.1 纯 Python 读寄存器只是“看起来能碰硬件”

我在 RP2040 上试过直接用machine.mem32写 DMA 寄存器,启动一条内存到内存的搬运,确实可以跑。但一旦场景升级为“连续采样、多通道、每次传输长度不等”,纯 Python 方案立刻崩掉,因为 MicroPython 的运行时没有给你稳定拿到任意 Python 对象地址的接口。你用id(buffer)拿到的不是 buffer 数据区的起始地址,堆管理器内部的差异让你根本没法稳定构造 DMA 描述符;即便你用uctypes.addressof之类的技巧拿到一段地址,GC、内存碎片、对象头偏移都可能让这个地址在下一轮分配后变得不可依赖。更麻烦的是,链式触发本身是硬件层的“自动接力”,你在 MicroPython 脚本里用mem32配置链路,只是把每个字段当成一个普通数字写进去,一旦某个段配置写错,你没有足够快的错误反馈,现象往往是整条总线卡死或者外设收到半个包。

“能碰”和“能在工程里可靠地用”是两码事。MicroPython 适合做编排,不适合做需要纳秒级一致性的 DMA 描述符维护。

1.2 我的设计边界:固件层只留“最窄的缝”

所以我的做法是在固件层增加一个很小的本地扩展模块,它只提供三件事:注册缓冲区、构建 Scatter-Gather 链、启动链式触发。底层 DMA 通道的分配、描述符管理、中断收敛全部用 C 完成。MicroPython 脚本不直接见寄存器,它只见“buffer”和“事件”。这样做损失了一点“脚本里能读寄存器”的极客感,但换来的是可维护性和可复现性。

模块接口被刻意收窄之后,MicroPython 侧的数据聚合业务反而变得干净:

  • 负责把业务数据切成段(头部、变长负载、尾部校验);
  • 负责维护稳定、固定大小的缓冲池;
  • 负责监听 DMA 完成事件,决定什么时候再提交下一轮数据。

如果你是在做数据采集聚合,比如多个传感器通道需要被 DMA 采集到同一块逻辑连续区域,也可以用同一个模型:将实际物理内存分成若干不连续区域,但通过 Scatter-Gather 描述符把它们逻辑上串成一个完整采集帧。MicroPython 只操作这个“虚拟聚合帧”的分段列表。

1.3 分层带来的直接收益

数据不用在 Python 层做整包 memcpy,CPU 不会被每个段完成中断打断。DMA 链走到最后一段才触发一次 IRQ,MicroPython 调度器只被唤醒一次,聚合后的数据已经全部落在外设或内存指定位置。接下来 MicroPython 的 asyncio 调度只需要处理“哪个缓冲池块空闲了”这种低频事件。

2. 链式触发真正“链”的是什么:一次搬运的自我接力

2.1 普通 DMA 的触发模型对照

普通 DMA 是“单发”:软件写一次启动寄存器,外设 DMA 请求或定时器触发后,DMA 控制器把一段数据从源地址搬到目的地址,搬完产生中断。整个过程 CPU 可以不参与数据搬运,但之后 CPU 需要处理完成中断、重新配置下一次搬运的数据源和目标。

Scatter-Gather 要解决的问题是“我不想去拼连续缓冲”。举个最常见的例子:一帧协议报文由 12 字节头部、2KB 传感器数据和 4 字节 CRC 共同组成。三块数据分别位于不同 bytearray。传统做法是先把三块数据 memcpy 到一块显式预留的发送缓冲区,再启动一次 DMA 发送。这个 memcpy 在 MicroPython 里尤其昂贵——它不仅是 CPU 时间,还意味着你必须准备一块等于整包大小的“聚合缓冲区”,内存碎片和双缓冲开销都会成倍上升。

2.2 链式触发让多个 DMA 通道自动接力

Scatter-Gather 的硬件基础是“描述符链”,但描述符链要工作,必须解决“谁触发下一段”的问题。一次链式搬运是这样发生的:

  1. MicroPython 调用原生接口,注册多个段:段 0、段 1、段 2;
  2. 原生接口把段信息写进 DMA 描述符或通道配置;
  3. 段 0 的 DMA 搬运结束后,DMA 控制器根据链式触发字段自动启动段 1;
  4. 段 1 结束后自动启动段 2;
  5. 段 2 是链尾,只有它产生完成状态和中断。

在 RP2040 上,这种“自接力”由通道的 chain-to 字段实现;在 ESP32-S3 GDMA 上,描述符节点的 next 指针天然支持链式跳转;在支持 linked-list DMA 的 STM32 系列上,则要维护一个内存中的 DMA 描述符数组。无论具体硬件叫什么,本质上 DMA 控制器是在自己做调度,CPU 只负责提交一次首地址。

我把这种模型叫“硬件调度队列”。队列的好处显而易见:链路一旦启动,中间不会出现 Python 层能感知到的缝隙。对某些严格依赖时序的外设(比如 SPI 屏刷新、ADC 连续采样、I2S 音频流),中间没有 CPU 参与反而更容易满足时序要求。

2.3 为什么要强调“数据聚合”,而不是单纯“多段发送”

Scatter-Gather 更常被低估的价值是用来做采集端的聚合。假设你要持续采集 4 路模拟信号,每路采集结果要放进不同长度的缓冲区;如果 DMA 是按缓冲区分段触发的,那么每一次触发之间的“间隙”可能造成采集丢点。用链式 Scatter-Gather,你可以把多个不连续缓冲区编排成一条逻辑上连续的数据链路,只要中断管理得当,整个采集过程看起来就像一次很长的 DMA 传输。这是“聚合”在标题里的实际含义:它不只是发送报文时的拼接优化,更是接收侧的数据连续性方案。

3. 从零实现一个最小可复现的 SG 引擎:以 RP2040 为例

3.1 为什么先用 RP2040 验证

我最早是在 RP2040 上把整个链路跑通的,原因有三个:寄存器模型简单、MicroPython 移植版本活跃、pico-sdk 里 DMA API 层级清晰。更重要的是,RP2040 的 DMA 通道数量足够多,拿来实验链式触发时不用像某些 MCU 一样抢通道。

我不建议新手一上来就碰 STM32H7 的 linked-list DMA,虽然那套 LLM 描述符很强,但缓存一致性、描述符放在哪个内存区域、对齐要求等问题会把第一个 Demo 拖得很难受。RP2040 的 DMA 直连 SRAM,没有 cache 一致性问题,最适合先把 Scatter-Gather 的编排逻辑验证清楚。

3.2 原生扩展模块的最小接口

实际实现时,我加了一个原生模块dma_sg,接口收敛成下面几个方法:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:26:43

嵌入式设备Web服务器实战:用Mongoose快速搭建远程管理界面

简介:mongoose是一套用C语言编写的轻量级嵌入式Web服务器,专注解决物联网设备、智能家居等资源受限环境下的HTTP/HTTPS服务需求,采用事件驱动和非阻塞I/O模型,API简洁,适合各类嵌入式开发者集成使用。压缩包共包含53个…

作者头像 李华
网站建设 2026/9/7 5:26:27

用流水线思维学汇川:PLC入门、InoProShop实战与知识库搭建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:26:19

分布式Datalog引擎核心解析:增量查询如何取代全量重算

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:25:15

一行命令装好 AI 技能库:从入门到跑通第一个技能

一行命令装好 AI 技能库:从入门到跑通第一个技能 【免费下载链接】skills Skills Catalog for Codex 项目地址: https://gitcode.com/GitHub_Trending/skills4/skills 团队里有人调教好了一套「让 AI 处理某类任务」的流程,你却只能靠口口相传&am…

作者头像 李华
网站建设 2026/9/7 5:24:48

前馈层扩宽需谨慎:显存、延迟与表达能力的权衡

前馈层扩多宽,要同时权衡表达、显存与延迟 在量化交易相关的深度学习模型里,注意力机制往往被讨论得最多,但真正决定模型容量和资源消耗的,往往是前馈层。把前馈层从 2048 扩到 4096,模型表达能力大概率会上升&#xf…

作者头像 李华