单片机开发---RP2040数据手册之PIO功能
手册第三章
3.1 概述(Overview)
RP2040 包含两个 PIO 子系统:PIO0 和 PIO1。每个 PIO 提供了 灵活的可编程状态机(state machines),可以用来实现几乎任何串行或并行通信协议,而不占用 CPU 资源。
每个 PIO 包含:
- 4 个独立的状态机(state machines)
- 32 条指令内存(instruction memory)
- FIFO 缓冲区(TX 和 RX)
- GPIO 映射逻辑
- IRQ 和 DMA 接口
这些状态机可以协同工作,实现如 WS2812、DVI、I2C、SPI、UART、Manchester 编码等协议,甚至是自定义协议。
3.2 程序员模型(Programmer’s Model)
3.2.1 PIO 程序(PIO Programs)
PIO 程序是用一种小型汇编语言编写的,最多支持 32 条指令。每条指令执行一个时钟周期,支持以下操作:
指令 | 功能 |
---|---|
jmp | 条件跳转 |
wait | 等待 GPIO 电平或边沿 |
in | 从 GPIO/FIFO/引脚读取数据 |
out | 向 GPIO/FIFO/引脚输出数据 |
push | 将数据推入 RX FIFO |
pull | 从 TX FIFO 拉取数据 |
mov | 在寄存器之间移动数据 |
irq | 触发中断 |
set | 设置 GPIO 状态 |
3.2.2 控制流程(Control Flow)
状态机可以:
- 独立运行
- 同步运行(使用 IRQ 或 FIFO)
- 通过 jmp 实现循环、分支
- 使用 wait 实现精确时序控制
3.2.3 寄存器(Registers)
每个状态机都拥有少量内部寄存器。这些寄存器用于保存输入或输出数据,以及临时值(例如循环计数器变量)
每个状态机有以下寄存器:
寄存器名 | 功能 |
---|---|
x , y | 通用 scratch 寄存器 |
isr | 输入移位寄存器 |
osr | 输出移位寄存器 |
pc | 程序计数器 |
shiftctr | 移位计数器 |
execctrl | 执行控制寄存器(如 autopull、autopush) |
pinctrl | 引脚控制寄存器(如引脚映射、方向) |
3.2.3.1 输出移位寄存器(OSR)
图 40. 输出移位寄存器(OSR)
输出移位寄存器(OSR)用于在 TX FIFO 与引脚(或其他目标,如暂存寄存器)之间缓存并移位输出数据。
- PULL 指令:从 TX FIFO 中取出 1 个 32 位字,并加载到 OSR。
- OUT 指令:将 OSR 中的数据逐位输出(每次 1~32 位),输出方向可配置为左移或右移。
- 数据回收:未使用的输出数据会被双向移位器回收。
- 自动重载:当 OSR 为空时,若启用了 autopull,状态机会自动从 TX FIFO 重新加载一个新的 32 位字。
- OSR 在数据移出过程中,空出的位会被自动填充为 0。
解释一下autopull功能
未启用 autopull 的版本(pull_example1)
program pull_example1
loop:out pins, 8 ; 输出 8 位到引脚pull ; 手动从 TX FIFO 拉取 32 位数据到 OSRout pins, 8 [1] ; 再次输出 8 位,延迟 1 个周期out pins, 8 [1] ; 输出第 3 个 8 位,延迟 1 个周期out pins, 8 ; 输出第 4 个 8 位jmp loop
autopull 简化版(pull_example2)
启用 autopull后,硬件会在 OSR 空时自动从 FIFO 重新加载数据。状态机若尝试从空 OSR 输出,会自动暂停。
优点:
- 无需手动使用 pull 指令;
- 吞吐量更高:若 FIFO 保持充足,可每个时钟周期输出 32 位。
简化后的程序如下,功能与上例完全一致:
program pull_example2
loop:out pins, 8 ; 每 2 个时钟周期输出 1 字节jmp loop
使用程序循环包装进一步优化(pull_example3)
启用程序包装后,可进一步简化代码,并实现每个系统时钟周期输出 1 字节:
program pull_example3
.wrap_targetout pins, 8 [1] ; 每周期输出 1 字节
.wrap
✅ 总结
版本 | 是否手动 pull | 是否使用 autopull | 是否使用程序包装 | 输出速率 |
---|---|---|---|---|
示例 1 | ✅ 是 | ❌ 否 | ❌ 否 | 每 2 周期 1 字节 |
示例 2 | ❌ 否 | ✅ 是 | ❌ 否 | 每 2 周期 1 字节 |
示例 3 | ❌ 否 | ✅ 是 | ✅ 是 | 每周期 1 字节 |
3.2.3.2 输入移位寄存器(ISR)
功能与OSR相反,从IO读取数据,写入数据缓冲区
图 41 输入移位寄存器(ISR)与数据 RXFIFO
移位寄存器:
- 每次通过引脚接收 1~32 位数据
- 当前内容向左或向右移位,为新数据腾出空间
- IN 指令每次将 1~32 位数据移入寄存器
- 寄存器装满后,其内容被写入 RX FIFO
- PUSH 指令将 ISR 内容写入 RX FIFO,随后 ISR 被清零
- 如果启用了自动推送(autopush),当移入数据达到设定的移位阈值后,状态机会在 IN 指令执行时自动推送 ISR 内容至 RX FIFO。
移位方向可通过处理器的配置寄存器进行设置。
对于 UART 等外设,由于线路顺序是 LSB 在前,因此必须从左端移入才能保证位序正确;然而,处理器可能期望最终字节右对齐。
为解决此问题,提供了特殊的 null 输入源,允许程序员在数据之后向 ISR 中移入若干位 0。
3.2.3.3. 移位计数器
状态机通过硬件计数器记录通过 OUT 指令从输出移位寄存器(OSR)移出以及通过 IN 指令移入输入移位寄存器(ISR)的总位数。这些信息始终由一对硬件计数器跟踪——输出移位计数器和输入移位计数器——每个计数器的值范围为 0 到 32(包含 32)。每次移位操作时,相关计数器会增加移位数,最大值为 32(等于移位寄存器的宽度)。状态机可以配置为在计数器达到可配置阈值时执行特定操作:
- 当移出一定数量的位后,OSR 可以自动重新填充。详见第 3.5.4 节。
- 当移入一定数量的位后,ISR 可以自动清空。详见第 3.5.4 节。
- PUSH 或 PULL 指令可以根据输入或输出移位计数器进行条件判断。
在 PIO 复位或 CTRL_SM_RESTART 信号断言时,输入移位计数器被清零(尚未移入任何位),输出移位计数器初始化为 32(没有剩余位需要移出;完全耗尽)。其他一些指令会影响移位计数器:
- 成功的 PULL 操作将输出移位计数器清零。
- 成功的 PUSH 操作将输入移位计数器清零。
- MOV OSR,…(即任何写入 OSR 的 MOV 指令)将输出移位计数器清零。
- MOV ISR,…(即任何写入 ISR 的 MOV 指令)将输入移位计数器清零。
- OUT ISR,count 设置输入移位计数器为 count。
3.2.3.4. 暂存寄存器
每个状态机有两个32位的内部暂存寄存器,称为 X 和 Y。
它们用作:
- IN/OUT/SET/MOV 指令的源/目标
- 分支条件的源
例如,假设我们想要为“1”数据位产生一个长脉冲,为“0”数据位产生一个短脉冲:
.program ws2812_led
public entry_point:pull ; 拉取操作set x, 23 ; 设置 x 为 23,循环 24 位
bitloop:set pins, 1 ; 将引脚驱动为高电平out y, 1 [5] ; 移出 1 位,并将其写入 yjmp !y skip ; 如果位为 0,则跳过额外的延迟nop [5] ; 无操作,延迟 5 个周期
skip:set pins, 0 [5] ; 将引脚驱动为低电平,延迟 5 个周期jmp x-- bitloop ; 如果 x 不为零,则跳回 bitloop,并递减 xjmp entry_point ; 跳转到入口点
在这里,X 用作循环计数器,Y 用作从 OSR 分支到单个位的临时变量。该程序可以用于驱动 WS2812 LED 接口,尽管更紧凑的实现是可能的(例如,仅用 3 条指令)。
MOV 指令允许使用暂存寄存器来保存/恢复移位寄存器,例如,如果您想重复移出相同的序列。
3.2.3.5. FIFO
每个状态机都有一对4字深度的FIFO,一个用于从系统到状态机(TX)的数据传输,另一个用于从状态机到系统(RX)。TX FIFO由系统总线主设备(如处理器或DMA控制器)写入,而RX FIFO由状态机写入。FIFO解耦了PIO状态机和系统总线的时序,使状态机能够在没有处理器干预的情况下运行更长时间。
FIFO还生成数据请求(DREQ)信号,允许系统DMA控制器根据RX FIFO中的数据存在或TX FIFO中新数据的空间来调整其读写操作。这使得处理器可以设置一个长事务,可能涉及许多千字节的数据,而无需进一步的处理器干预。
通常,状态机只在一个方向上传输数据。在这种情况下,SHIFTCTRL_FJOIN选项可以将两个FIFO合并为一个单向8字深度的FIFO。这对于DPI等高带宽接口非常有用。
3.2.4. 暂停
状态机可能会因为多种原因暂时暂停执行:
- WAIT 指令的条件尚未满足
- 当 TX FIFO 为空时的阻塞 PULL,或当 RX FIFO 已满时的阻塞 PUSH
- 设置了 IRQ 标志并等待其清除的 IRQ WAIT 指令
- 启用了自动拉取(autopull)时的 OUT 指令,并且 OSR 已经达到其移位阈值
- 启用了自动推送(autopush)时的 IN 指令,ISR 达到其移位阈值,并且 RX FIFO 已满
在这种情况下,程序计数器不会前进,状态机将在下一个周期继续执行该指令。如果指令指定在下一个指令开始之前有一些延迟周期,则这些延迟周期在暂停清除后才会计时。
3.2.5. 引脚映射
PIO 控制多达 32 个 GPIO 的输出电平和方向,并可以观察它们的输入电平。在每个系统时钟周期,每个状态机可以执行以下操作之一或两者:
- 通过 OUT 或 SET 指令更改某些 GPIO 的电平或方向,或通过 IN 指令读取某些 GPIO
- 通过边设操作更改某些 GPIO 的电平或方向
这些操作中的每一个都作用于四个连续的 GPIO 范围之一,每个范围的基地址和计数通过每个状态机的 PINCTRL 寄存器配置。对于 OUT、SET、IN 和边设操作,每个操作都有一个范围。每个范围可以覆盖给定 PIO 块(在 RP2040 上这是 30 个用户 GPIO)的任何 GPIO,并且这些范围可以重叠。
对于每个单独的 GPIO 输出(电平和方向),PIO 会考虑该周期内可能发生的所有 8 次写操作,并应用来自编号最高的状态机的写操作。如果同一个状态机同时对同一个 GPIO 执行 SET/OUT 和边设操作,则使用边设操作。如果没有状态机写入该 GPIO 输出,则其值不会从前一个周期改变。
通常,每个状态机的输出映射到一组不同的 GPIO,实现某些外设接口。
3.2.6. IRQ 标志
IRQ 标志是状态位,可以由状态机或系统设置或清除。总共有 8 个:所有 8 个对所有状态机都是可见的,并且较低的 4 个也可以通过 IRQ0_INTE 和 IRQ1_INTE 控制寄存器掩码到 PIO 的一个中断请求线。
它们有两个主要用途:
- 从状态机程序中断言系统级中断,并可选地等待中断被确认
- 在两个状态机之间同步执行
状态机通过 IRQ 和 WAIT 指令与这些标志进行交互。
3.2.7. 状态机之间的交互
指令存储器被实现为一个1写4读的寄存器文件,因此所有四个状态机可以在同一个周期内读取指令,而不会停顿。
应用多个状态机有三种方式:
- 在同一个程序中指向多个状态机
- 在不同的程序中指向多个状态机
- 使用多个状态机运行同一接口的不同部分,例如 UART 的发送(TX)和接收(RX)端,或 DPI 显示器上的时钟/同步和像素数据
状态机之间不能直接通信数据,但可以通过使用 IRQ 标志进行同步。总共有 8 个标志(其中较低的 4 个可以被掩码用作系统 IRQ),每个状态机可以使用 IRQ 指令设置或清除任何标志,并可以使用 WAIT IRQ 指令等待某个标志变高或变低。这允许状态机之间进行周期精确的同步。
3.3 PIO 汇编器(pioasm)
pioasm 是官方提供的 PIO 汇编器,支持以下语法:
3.3.1 指令
以下指令用于控制 PIO 程序的组装。
指令 | 描述 |
---|---|
.define (PUBLIC) <symbol> <value> | 定义一个名为 <symbol> 的整数符号,其值为 <value> 。如果 .define 出现在第一个程序之前,则为全局定义。 |
.program <name> | 以 <name> 名称开始一个新的程序。程序持续到另一个 .program 指令或源文件结束。 |
.origin <offset> | 可选指令,用于指定程序必须加载的 PIO 指令内存偏移量。通常用于必须在偏移量 0 处加载的程序。 |
.side-set <count> (opt) <pinds> | 指定要使用的边集位的数量。可选的 opt 参数指定 side_values ,pinds 参数指示边集值应用于 PINDIRs 而不是 PINs。 |
.wrap_target | 放置在指令之前,指定由于程序包装而继续执行的指令。只能在程序内使用一次。 |
.wrap | 放置在指令之后,指定在正常控制流之后程序将包装到 wrap_target 指令。只能在程序内使用一次。 |
.lang_opt <lang> <name> <option> | 为特定语言生成器指定与程序相关的选项。只在程序内有效。< |