RP2040 的 DMA 是图灵完备的(2023)
摘要
Cornell ECE4760 课程项目证明了 RP2040 微控制器的 DMA 子系统是图灵完备的,完全通过 DMA 通道操作构建了一个可运行的取指-执行 CPU,无需涉及 ARM 核心。
<p><a href="https://lobste.rs/s/jdhrhp/rp2040_dma_is_turing_complete_2023">评论</a></p>
查看缓存全文
缓存时间: 2026/06/05 02:15
# ECE4760 rp2040 DMA 计算机
来源:https://people.ece.cornell.edu/land/courses/ece4760/RP2040/C_SDK_DMA_machine/DMA_machine_rp2040.html
康奈尔大学 ECE4760 直接内存访问计算机 RP2040
**RP2040 上的 DMA**
DMA 使用独立于 CPU 的内存控制器来加速内存位置之间或外设与内存之间的数据传输。RP2040 拥有 12 个 DMA 通道,在许多情况下无需影响 CPU 性能,每秒可传输超过 100 MB 的聚合数据。设置 DMA 传输有大量可用选项。你可以将 DMA 通道控制器视为一个独立的、可编程的处理器,其主要职责就是移动数据。
rp2040 上的内存以总线矩阵形式组织,每个 ARM 核心和 DMA 系统各有独立的内存总线控制主设备,多个内存总线从设备可由主设备访问。每个总线从设备可在每个机器周期内被访问。
在此,我们使用 DMA 子系统构建一个完整的计算系统,独立于主 ARM CPU 运行。该 DMA 计算机利用了内存拷贝能力、传输触发操作以及自修改代码。*代码*由存储在数组中的一系列 DMA 块描述符组成。所实现的操作是[图灵完备](https://en.wikipedia.org/wiki/Counter_machine)的。每秒可获取/执行约 800 万个 DMA 块。
使用纯内存移动来构建通用 CPU 是有历史先例的。2013 年,Stephen Dolan 发表了 [x86 mov 是图灵完备的](https://people.ece.cornell.edu/land/courses/ece4760/RP2040/C_SDK_DMA_machine/mov.pdf),描述了一种单指令机器的示例。论文 [Run-DMA](https://www.usenix.org/system/files/conference/woot15/woot15-paper-rushanan.pdf) 由 Michael Rushanan 和 Stephen Checkoway 撰写,展示了如何利用某版本(Raspberry Pi 2)的 ARM DMA 实现这一目标。RP2040 上的 DMA 系统具有更多传输触发功能,构建起来稍微容易一些。Joseph Primmer 和我使用 Microchip PIC32 DMA 系统构建了一个 DMA 处理器,加法和分支必须基于查找表实现。详见 [DMA 奇异机器](https://people.ece.cornell.edu/land/courses/ece4760/PIC32/index_DMA_weird_machine.html)。
**DMA CPU**
DMA 计算机是一个取指-执行 CPU,其中取指功能由一个 DMA 通道完成,该通道将 DMA 控制块镜像从 RAM 加载到另一个(执行)DMA 通道。所加载的"程序"由一系列精心构造的 DMA 控制块组成,这些控制块共同构成一台通用计算机。
通过使用 DMA1 块在 DMA1 控制块镜像被传输到硬件 DMA1 控制寄存器之前修改数组中后续的 DMA1 控制块镜像,我们可以执行加法、自增、条件跳转、与/或/非逻辑运算以及其他所需操作。
DMA 子系统中的几个传输触发动作使设计变得更加容易。这些功能包括"通道嗅探器"中的加法器,以及所有 SFR 上的原子 SET/CLEAR/XOR 写入功能。
基本的取指/执行机器使用通道 DMA0 的读地址作为程序计数器。每次取指后,读地址指向下一个块位置。DMA0 从 RAM 数组中读取下一个块,将其复制到 DMA1 通道硬件控制寄存器,然后链式启动新加载的 DMA1 通道。DMA1 通道执行所指定的数据移动,然后链式启动 DMA2。DMA2 将 DMA0 的 write\_address 重置为指向 DMA1 控制寄存器。
程序分支通过使用 DMA1 将新的 DMA0 读地址加载到 DMA0 控制寄存器来实现。
编写 DMA 块程序非常类似于为一台只有单个累加器寄存器且只支持内存到累加器操作的奇特汇编语言机器编程。下图试图概括这一切"疯狂"之处。黑色箭头表示数据流,蓝色箭头表示通道之间的链式关系。控制块数组只是一个整数数组,由 DMA0 每次以四个为一组进行读取。
除直接复制操作外,RP2040 DMA 系统中还有一些[传输触发操作](https://en.wikipedia.org/wiki/Transport_triggered_architecture),作为读取或写入特定地址的副作用而发生:
- 写入与每个*特殊功能寄存器*(SFR)关联的某些影子寄存器会对 SFR 中的位进行清零、置位或异或操作。DMA 嗅探器数据寄存器是一个可以这样使用的 SFR,这允许通过向寄存器传输数据来对每个 SFR 执行通用逻辑操作。每个外设控制寄存器(端口、定时器、DMA,*所有一切*)都是一个带有三个影子寄存器的 SFR!
- 对于两个逻辑位 A 和 B:
- 先加载 B,再使用 A 作为掩码进行 SET 操作,实现 A OR B
- 先加载 B,再使用 NOT(A) 作为掩码进行 CLR 操作,实现 A AND B
- 先加载 B,再使用 A 进行 XOR 操作,实现 A XOR B
- DMA 嗅探器系统本身支持在通道传输数据时即时计算 CRC32。嗅探器还可以在传输数据时进行累加运算。32 位加法能力使其他操作更易于实现。
- 从嗅探器数据寄存器中读出的数据可以被逻辑取反或位反转。
- 任何 DMA 通道传输都可以进行字节交换。
- 当然,写入某些 SFR 可能具有系统功能。例如,写入 I/O 端口会设置输出引脚的值,或者设置中断标志会触发 CPU 中断。
编程过程必须将这些不寻常的原语操作映射为熟悉的数学和逻辑操作,以及某种形式的条件分支或跳转。嗅探器加法操作和按位 SFR 操作意味着我们可以直接实现这些功能。*但请记住,每个基本操作只是一次数据移动。*
对于*加法*,一系列 DMA 块可以是:
1. 将一个操作数移动到 sniffer\_data 寄存器
2. 启用嗅探器,将另一个操作数移动到位桶(丢弃)(这执行了加法)
3. 将 sniffer\_data 寄存器移动到结果地址
对于*左移*,只需将一个变量与自身相加(乘以二)。
对于*逻辑*操作(OR、AND、XOR 等):
1. 将一个操作数移动到 sniffer\_data 寄存器
2. 将另一个操作数移动到嗅探器的 SET、CLR 或 XOR 写地址(例如 `DMA_SNIFF_DATA_CLR`)
3. 将 sniffer\_data 寄存器移动到结果地址
对于 (A-B) 的*减法*,必须显式计算 B 的二进制补码取反:
1. 将 B 移动到 sniffer\_data 寄存器
2. 将 0xFFFFFFFF 移动到 XOR 写地址(`DMA_SNIFF_DATA_XOR`)以反转所有位
3. 启用嗅探器,将 1 移动到位桶(丢弃)(这实现了二进制补码中的加 1)
4. 启用嗅探器,将操作数 A 移动到位桶(丢弃)(这执行了加法)
5. 将 sniffer\_data 寄存器移动到结果地址
对于*右移*,过程更加繁琐。右移是位反转的左移:
1. 将变量移动到 sniffer\_data 寄存器
2. 将 sniff\_rev\_mask 移动到 `DMA_SNIFF_CTRL_SET`,这将使从 sniff\_data 寄存器写出时对字中的位进行反转排序
3. 将 temp\_register 移动到 sniff\_data(位顺序已反转)
4. 将 temp\_register 移回 sniff\_data
5. 启用嗅探器,将 temp\_register 移动到位桶(丢弃)(将其翻倍;即左移)
6. 将 sniff\_data 寄存器移动到结果地址(位顺序已反转,从而恢复正确顺序)
7. 将 sniff\_rev\_mask 移动到 `DMA_SNIFF_CTRL_CLR`,关闭位反转选项
*无条件跳转*很简单,只需一步:将跳转目标地址移动到 DMA0 硬件读地址控制字。
最难处理的操作是*条件跳转*。每个跳转条件(例如,负数时跳转)必须转换为绝对地址,并且所有数据情况(例如正数、零、负数)都*必须跳转*!这是因为设置条件跳转的最后一步是将数据推送到 DMA0 硬件。这个奇怪的约束意味着跳转条件需要转换为表示块地址的小整数。
下面概述*变量为负数时跳转*的方案:
1. 开启 *DMA 字节交换*,将待测试的变量移动到 sniffer\_data 寄存器。这会将符号位移动到第 7 位。只要寄存器绝对值小于 pow(2,28),第 4-6 位将与符号位相同。
2. 将 0xFFFFFFeF 移动到 CLR 写地址(`DMA_SNIFF_DATA_CLR`)以隔离第 4 位(或第 4 到 7 位中的任意位)。若选择第 4 位,结果对于正数(或零)为 0,对于负数为 16。
3. 将正数输入时跳转的目标地址 ADDR 移动到位桶(丢弃),同时启用嗅探器。结果将是 ADDR 或 ADDR+16 之一,其中 16 是程序数组中一个块的大小。这两个地址各自可包含一个跳转到程序中任意位置的无条件跳转指令。
4. 将 sniff\_data 寄存器移动到 DMA0 硬件读地址控制字,以强制实际跳转到两个位置之一。
**使用 DMAasm 简化编程**
通过构建 DMA 控制结构来编写程序会变得非常繁琐。一种解决方案是将一个或多个通道定义打包成宏,使用更好的表示法和更直观的名称。下表是我目前整理的内容,最新程序将包含相应实现。
下表中的每个宏都解析为一个或多个顺序 DMA 控制结构,使 DMAcpu 看起来像 1980 年代风格的单累加器、内存到 ALU 架构。DMA 嗅探器寄存器的传输触发功能构成了 ALU 的大部分。大多数 DMAasm 语句执行时间约为 125 纳秒(每秒 800 万次),但向量加法或乘法可能慢得多,大约每微秒 100 次加法。因此乘以 256 需要超过 2 微秒。右移超过一位使用相同的加法机制,可能更慢。
| DMAcpu 宏 | 用途与功能 |
|---|---|
| `nop()` | 你猜对了!跳过一个取指/执行周期。 |
| `gpio_out(gpio_num, state)` | 设置或清除由 *gpio_num* 指定的一个 GPIO 引脚。*state* 是一个求值为有效控制字的变量,例如 `int pin_hi = 0x3300; int pin_lo = 0x3200;` |
| `load_sniff(var_name)` | 将一个 32 位值从内存移动到 sniff\_data 寄存器 |
| `store_sniff(var_name)` | 将一个 32 位值从 sniff\_data 存储回内存 |
| `store_sniff_bitrev(var_name)` | sniff\_data 寄存器在存储时具有位反转数值的能力,这偶尔很有用,例如用于右移函数 |
| `move(source, destination, count, width, source_inc, dest_inc)` | 将 *count* 个位宽为 *width* 的数据项从 *source* 移动到 *destination*。*width* 必须为 `DMA_SIZE_32`、`DMA_SIZE_16` 或 `DMA_SIZE_8`。*source\_inc* 必须为 *var\_read* 或 *array\_read*。*dest\_inc* 必须为 *var\_write* 或 *array\_write*。移动数组时,*source* 应为 `array_name[0]` |
| `move_bswap(source, destination, count, width)` | 将 *count* 个位宽为 *width* 的数据项从 *source* 移动到 *destination*,并反转字节顺序。width 必须为 `DMA_SIZE_32`、`DMA_SIZE_16` 或 `DMA_SIZE_8`。移动数组时,*source* 应为 `array_name[0]` |
| `set_sniff_CRC32()` | 将嗅探器使能功能设置为对通过其传输的数据执行 CRC32,使用当前 sniffer\_data 值作为种子 |
| `set_sniff_add()` | 将嗅探器使能功能设置为对通过其传输的数据执行累加运算 |
| `crc32_sniff(var_name, count)` | 执行实际的 CRC32 操作,从 *var\_name* 中取 *count* 个数据项(嗅探器必须设置为 CRC32) |
| `add_sniff(var_name, count, source_inc)` | 执行实际的累加运算,从 *var\_name* 中取 *count* 个数据项。这是一个向量加法(嗅探器必须设置为 add)。如果变量是数组,使用 `array_name[0]` 作为源。*source\_inc* 必须为 *var\_read* 或 *array\_read* |
| `neg_sniff()` | 对 sniff\_data 取二进制补码 |
| `inv_sniff()` | 对 sniff\_data 的每个位取逻辑非 |
| `clr_sniff(mask)` `set_sniff(mask)` `xor_sniff(mask)` | 低级 SFR 直接写入函数,*mask* 指示要更改哪些位 |
| `or_sniff(var_name)` | 将 *var\_name* 逻辑或入嗅探器 |
| `and_sniff(var_name)` | 将 *var\_name* 逻辑与入嗅探器 |
| `shift_left_sniff()` | 左移一位 |
| `logical_shift_right_sniff()` `logical_shift_right4_sniff()` `logical_shift_right8_sniff()` | 逻辑右移 1、4 或 8 位 |
| `arith_shift_right_sniff()` | 算术右移一位 |
| `mult_sniff(constant)` `mult_sniff_var(var_name)` | 乘以一个字面量 *constant*。仅乘以相对较小的正整数常量,例如 4 或 20,而不是 1000000。----- 乘以一个变量,但请记住这是通过重复加法完成的,对于较大的数字会更慢。 |
| `jump(block_addr)` | 无条件跳转到 *block\_addr*,该地址必须是一个赋值了地址的 `int*` 变量,通常使用类似 `label(extra_pulse_addr)` 的方式,其中 label 宏设置后续块的地址 |
| `jump_neg(block_addr_neg, block_addr_not_neg)` | 条件跳转:根据输入为负数或正数/零跳转到两个不同位置。参数地址必须是带有块地址的 `int*` 变量 |
| `pacer(dreq)` | 执行节拍器。等待下一个 DREQ 事件。如果事件是 DMA 时钟滴答,则定时器必须为 `DREQ_DMA_TIMER0`、`DREQ_DMA_TIMER1`、`DREQ_DMA_TIMER2` 或 `DREQ_DMA_TIMER3` 之一,且定时器必须已设置好。 |
| `label(label_name)` | 将 *label\_name* 变量赋值为当前块的地址。用于指定跳转和函数地址。该变量必须为 `int*` |
| `jump_link(function_addr)` `jump_return()` | 跳转到指定地址,并将返回地址存储在链接寄存器中。从函数返回到链接寄存器中的地址。 |
| `ptr_to_value_sniff()` | 该宏用于对 sniff\_data 寄存器中包含的指针进行解引用,并将值返回到 sniff\_data。用于将数组地址转换为数据。 |
---
*以下程序按博客风格的倒序时间顺序排列,最新内容在最上面。*
以下程序列表按时间顺序排列:
1. 用于验证基本执行模型的测试程序,测试 GPIO 输出、加法、OR 操作、条件分支和无条件跳转。(2022年12月23日)
2. 直接数字合成用于测试定时器调节的执行速度、SPI 输出,以及结合 DMA 通道字节交换功能和 CLR 掩码,将 32 位累加器的最高字节隔离出来用作正弦表的索引。插入正弦表指针需要自修改代码。性能足以用于音频合成速率。(2022年12月28日)
3. 更新的测试程序,实现了加法、减法、左移、右移,以及几种不同的条件跳转生成方式。(2023年1月2日)
4. 重构和通用化版本。清理了 DMA 通道依赖关系以兼容其他软件(例如 VGA 生成)。取指/执行架构与 DMA 程序定义分离。(2023年1月3日)
5. 使用 DMAcpu 机器读取 ROSC 随机位,将其移入嗅探器,然后使用结果通过嗅探器硬件计算 CRC32 值,并将其输出到 SPI 通道以产生音频白噪声。(2023年1月7日)
6. 使用单极点 IIR 对白噪声发生器进行低通滤波,主要是为了验证 DMAcpu 能否完成该算术运算。一个 38 步程序约需 4 微秒计算一个低通采样值。(2023年1月11日)
7. 将 DMAcpu 与 VGA 生成合并。由于两者都大量使用 DMA 系统,需要进行测试……
相似文章
在我们的自定义CPU上运行Doom并走红
作者描述了在逻辑门级别设计自定义CPU、集成带有缓存的DDR3内存,并成功在FPGA上运行Doom的经历,该经历随后走红网络。
将1980年代的内存映射与Raspi Pico进行比较
比较1989年摩托罗拉68000内存映射设计与现代树莓派Pico的设计,突出整数地址分配的奢侈性以及内存规模相似性(192KB对比260KB),同时讨论了架构差异和可行性。
@pupposandro: https://x.com/pupposandro/status/2054241934164492328
该文章宣布了 llama.cpp 对 AMD Strix Halo 集成 GPU (iGPU) 上的 DFlash 和 PFlash 投机解码的支持,并展示了使用 ROCm 时推理性能的显著提升。
用RP2350监视Z80
一篇博客文章,探讨如何使用带PIO的Raspberry Pi Pico RP2350来监视Z80微处理器的地址和数据总线,包括时序考虑和时钟速度限制。
2026年中ROCm状态 [D]
作者询问2026年中AMD的ROCm生态系统在AI训练领域的当前可行性,将其与NVIDIA的CUDA进行比较,并询问它是否已达到PyTorch的“开箱即用”阶段。