MicroPython rp2.StateMachine 完全指南:掌控 RP2040 可编程 I/O(PIO)状态机
2026/9/20 23:12:15 网站建设 项目流程
  • 嵌入式
  • 语言运行时
  • 编程语言
  • 解释器
  • 编译器
  • 物联网
  • 系统编程

【免费下载链接】micropython

MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems

项目地址:https://gitcode.com/gh_mirrors/mi/micropython
点击查看免费下载

本指南以官方文档 rp2.StateMachine 为骨架,系统讲解 MicroPython 中rp2.StateMachine类的全部 API——从构造函数、init()的十余个配置参数,到exec()、FIFO 读写、IRQ 中断与缓冲区协议,并结合仓库内 rp2_pio.c 的源码实现与 examples/rp2 中的真实示例,帮助你理解状态机在底层如何被配置和驱动,并能够独立编写、装载、运行 PIO 程序,完成 UART、WS2812 灯带、正交编码器等高速外设时序任务。

一、PIO 与 StateMachine 类概览

RP2040 芯片内置两个完全相同的 PIO(Programmable I/O,可编程 I/O)外设,每个 PIO 实例拥有 4 个状态机(State Machine),因此整颗芯片共有8 个状态机,编号 0~7。状态机是 PIO 的核心执行单元:它以极低的延迟执行一段专用汇编程序,从而在不占用 CPU 的情况下产生或采样精确的时序信号。

在 MicroPython 中,rp2.StateMachine类就是访问这套可编程 I/O 接口的入口(详见 rp2.StateMachine.rst)。与之配套的是rp2.asm_pio()装饰器——用 Python 语法编写 PIO 汇编程序,再用StateMachine装载运行。汇编相关函数与全部 PIO 指令语法记录在 rp2.rst 的 "PIO assembly language instructions" 一节。

从源码结构看,StateMachine的实现在 rp2_pio.c:其中rp2_state_machine_obj_t结构体保存了 PIO 指针、IRQ 编号、状态机编号(0-3)与全局 ID(rp2_pio.c L53-L59);全局对象数组按pio0的 SM0-3、pio1的 SM0-3 顺序静态注册(rp2_pio.c L561-L576),这解释了为什么状态机 ID 0-7 会跨两个 PIO 实例分布。当某个状态机被外部资源(如 CYW43 WiFi 驱动)占用时,rp2_state_machine_get_object()会抛出ValueError: StateMachine claimed by external resource(rp2_pio.c L585-L591)。

二、构造函数:获取并初始化状态机

StateMachine(id, [program, ...])

id为状态机编号,取值范围0~7。构造时可以只传id获取对象,稍后通过init()配置;也可以直接传入program(由@rp2.asm_pio()装饰的函数)及init()支持的全部参数,一次性完成初始化。源码中rp2_state_machine_make_new在检测到额外参数时会直接调用rp2_state_machine_init_helper(rp2_pio.c L762-L778),因此两种写法等价:

# 方式一:先构造,后初始化 sm = rp2.StateMachine(0) sm.init(ws2812, freq=8_000_000, sideset_base=Pin(22)) # 方式二:构造时直接初始化 sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(22))

三、init():状态机的核心配置

init()方法签名如下,是使用 StateMachine 时最重要的 API:

StateMachine.init(program, freq=-1, *, in_base=None, out_base=None, set_base=None, jmp_pin=None, sideset_base=None, in_shiftdir=None, out_shiftdir=None, push_thresh=None, pull_thresh=None)

3.1 指令装载与复用(program)

program会被加入当前 PIO 实例的指令存储器。RP2040 每个 PIO 只有32 条指令的存储空间,MicroPython 对此做了自动去重:如果指令存储器中已经存在该程序,则复用其偏移量(offset)以节省指令空间

底层对应rp2_pio_add_managed_program(),它调用 SDK 的pio_add_program()并通过rp2_pio_instruction_memory_usage_mask位图跟踪已占用槽位(rp2_pio.c L137-L142);当程序已在内存中时,init()直接读取之前保存的 offset(rp2_pio.c L645-L651)。@asm_pio()生成的程序对象正是以 "program 数据 + offset 数组 + 配置字" 的多字段数组形式存在,字段布局定义在 rp2.py 的常量。

3.2 频率与时钟分频(freq)

  • freq为状态机运行频率(Hz),默认值为系统时钟频率,即不进行分频。
  • 时钟分频器按system clock frequency / freq计算,因此存在舍入误差
  • 最小分频为系统时钟的 1/65536。在默认 125 MHz 系统时钟下,freq的最小值为1908Hz;若要跑更慢的频率,需要先用machine.freq()降低系统时钟。

源码中freq的三种取值路径(rp2_pio.c L653-L672):

  • freq < 0(默认 -1):分频设为 1,即全速运行;
  • freq == 0:特殊值,将分频寄存器置 0(配合手动控制场景);
  • 正常正值:div = clk_sys * 256 / freq,其中整数部分clkdiv_int与小数部分clkdiv_frac(1/256 精度)分别写入硬件寄存器;越界时抛出ValueError: freq out of range

3.3 引脚基址参数

参数作用说明
in_basein()指令使用的第一个引脚见下方注意
out_baseout()指令使用的第一个引脚out_init结合初始化
set_baseset()指令使用的第一个引脚最多 5 个
jmp_pinjmp(pin, ...)指令使用的引脚默认配置为输入
sideset_base侧置(side-set)输出使用的第一个引脚sideset_init结合

重要注意in_base对应的引脚必须手动配置为输入(或其他模式),PIO 才能读到期望的信号——它们可以是输入引脚、输出引脚,或连接到其他外设。jmp_pin同样可以手动配置,但默认会被设为输入引脚

源码实现细节(rp2_pio.c L687-L756):out/set/sideset的引脚会同时检查程序内的*_init配置(asm_pio_get_pins解析单引脚或引脚元组,方向与初值编码为pindirs/pinvals),最终通过asm_pio_init_gpio()一次性设置引脚方向、初值并切换为GPIO_FUNC_PIO0/1复用功能。在 RP2350 上,若jmp_pin仍处于隔离(isolation)状态,代码还会自动调用pio_gpio_init()使其可被 PIO 读取(rp2_pio.c L742-L753)。

3.4 移位方向与阈值参数

  • in_shiftdir:ISR(输入移位寄存器)的移位方向,取PIO.SHIFT_LEFTPIO.SHIFT_RIGHT
  • out_shiftdir:OSR(输出移位寄存器)的移位方向,取值同上。
  • push_thresh:触发自动推送(auto-push)或条件重推(conditional re-push)的位数阈值
  • pull_thresh:触发自动拉取(auto-pull)或条件重拉(conditional re-pull)的位数阈值

这些参数覆盖@rp2.asm_pio()中设置的默认值。底层由asm_pio_override_shiftctrl()直接改写硬件SHIFTCTRL寄存器的对应位段(rp2_pio.c L231-L235),可见 MicroPython 将这些配置映射到了芯片原生的移位控制字。

四、运行控制:active() 与 restart()

4.1 active([value])

获取或设置状态机是否正在运行:

>>> sm.active() True >>> sm.active(0) False

不带参数时返回布尔值表示当前运行状态;传入参数则先设置再返回。源码直接读写 PIO 的CTRL控制寄存器(rp2_pio.c L786-L792)。在 pio_uart_tx.py 示例中,创建 8 个 UART TX 状态机后依次调用sm.active(1)启动(pio_uart_tx.py L35-L36)。

4.2 restart()

重启状态机并跳转到程序起点。该方法通过 RP2040 的SM_RESTART寄存器清除状态机的内部状态,包括:

  • 输入/输出移位计数器;
  • 输入移位寄存器(ISR)的内容;
  • 延迟计数器;
  • 等待 IRQ(waiting-on-IRQ)状态;
  • 通过StateMachine.exec()执行的、已停顿的指令。

源码中restart()pio_sm_restart()之外还执行一条pio_encode_jmp(initial_pc),将程序计数器显式跳回该状态机装载程序的起始偏移(rp2_pio.c L795-L801)。

五、单指令注入:exec(instr)

exec()用于单步执行一条 PIO 指令,常用于调试或在不重启整个状态机的情况下注入行为:

  • instr字符串,则通过asm_pio_encode从字符串编码为机器码:
sm.exec("set(0, 1)")
  • instr整数,则视为已编码的 PIO 机器码指令直接执行:
sm.exec(rp2.asm_pio_encode("out(y, 8)", 0))

从源码看,字符串形式会动态导入rp2模块并调用asm_pio_encode,同时自动读取该状态机当前的 sideset 配置(sideset 数量与可选位),保证编码与现有程序一致(rp2_pio.c L805-L822)。asm_pio_encode的 Python 实现在 rp2.py L276-L298,注意其jmp不被支持(设置为None),且要求恰好产生 1 条指令,否则抛出PIOASMError

六、FIFO 数据传输:put()、get() 与队列查询

状态机通过 TX/RX FIFO 与 CPU 交换数据。默认每个方向有4 个 32 位深度的 FIFO 字(可通过@asm_pio(fifo_join=...)合并为单一 8 字方向)。

6.1 get(buf=None, shift=0)

从 RX FIFO 拉取一个 32 位字:

  • FIFO 为空时阻塞等待,直到状态机推入数据;
  • 返回值先右移shift位:返回值为word >> shift
data = sm.get() # 阻塞读取一个字 data = sm.get(shift=8) # 读取并右移 8 位

源码实现(rp2_pio.c L826-L874):等待期间会调用mp_event_handle_nowait()保持事件响应;若传入缓冲区(buf参数),则可一次批量读取多个字,支持bytearray以及b/h/i类型码,get会按元素大小反复填充直到缓冲区写满。

6.2 put(value, shift=0)

向 TX FIFO 推入数据:

  • value可以是整数、类型为B/H/I的 array,或bytearray
  • 方法会阻塞直到所有字写入 FIFO:若 FIFO 已满或变满,会等待状态机拉取足够的字以完成写入;
  • 每个字先左移shift位:状态机实际收到word << shift
sm.put(0x12345678) # 推入单个字 sm.put(ar, 8) # 批量推入数组,整体左移 8 位

在 pio_ws2812.py 中,LED 的 24 位 RGB 数据正是通过sm.put(ar, 8)shift=8批量写入,配合autopull=True, pull_thresh=24自动补给 OSR(pio_ws2812.py L53)。UART 示例则用sm.put(ord(c))逐字符推入(pio_uart_tx.py L40-L42)。

6.3 rx_fifo() 与 tx_fifo()

返回对应 FIFO 中当前的字数,0表示空:

if sm.rx_fifo(): data = sm.get() # 非阻塞读取前先查询 if sm.tx_fifo() < 4: sm.put(word) # 确认有空间再写入

rx_fifo()适合在调用阻塞的get()前检查是否有数据等待;tx_fifo()则用于判断是否还有空间推入新字。两者分别映射到 SDK 的pio_sm_get_rx_fifo_levelpio_sm_get_tx_fifo_level(rp2_pio.c L918-L930)。

七、中断:irq(handler=None, trigger=0|1, hard=False)

irq()返回当前 StateMachine 的 IRQ 对象,并可选地完成配置。当状态机程序执行irq()指令(例如irq(rel(0)))时触发,IRQ 0-3 对处理器可见,4-7 仅用于状态机间通信(详见 rp2.rst 的 irq 指令说明)。

def on_sm_irq(irq): print("state machine raised IRQ, flags:", irq.flags()) sm.irq(handler=on_sm_irq, trigger=1, hard=False)

底层实现:pio_irq0()中断服务函数在读取INTS0后,会依次为 PIO 级 IRQ 与 4 个状态机 IRQ 分发事件(rp2_pio.c L77-L98);StateMachine.irq()通过写INTE0寄存器的8 + sm位使能/禁用对应状态机中断(rp2_pio.c L990-L999),trigger默认值1表示监听本状态机的 IRQ 标志位。若 PIO 的 IRQ 已被 CYW43 等外部资源独占,会抛出ValueError: irq claimed by external resource(rp2_pio.c L125-L134)。

八、Buffer 协议:直连 FIFO 与 DMA 搬运

StateMachine类支持buffer protocol(缓冲区协议),允许直接访问每个状态机的发送与接收 FIFO。其核心用途是:将 StateMachine 对象直接作为rp2.DMA()通道的 read/write 参数,让 DMA 直接读写 FIFO,实现无 CPU 参与的批量数据搬运。

rp2_state_machine_get_buffer的实现非常直接(rp2_pio.c L932-L947):写入方向返回pio->txf[sm]的地址,读取方向返回pio->rxf[sm]的地址,缓冲区长度固定为 4 字节(一个 32 位字),类型码为'I'。这意味着 StateMachine 在 DMA 眼中就是一个固定的 32 位寄存器映射端口,DMA 搬运的单位即 FIFO 字。

九、实战示例组合

以下结合仓库示例展示完整工作流。

9.1 WS2812 LED 灯带驱动(pio_ws2812.py)

import array, time from machine import Pin import rp2 NUM_LEDS = 8 @rp2.asm_pio( sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24, ) def ws2812(): T1 = 2 T2 = 5 T3 = 3 wrap_target() label("bitloop") out(x, 1) .side(0) [T3 - 1] jmp(not_x, "do_zero") .side(1) [T1 - 1] jmp("bitloop") .side(1) [T2 - 1] label("do_zero") nop() .side(0) [T2 - 1] wrap() sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(22)) sm.active(1) ar = array.array("I", [0 for _ in range(NUM_LEDS)]) for i in range(4 * NUM_LEDS): for j in range(NUM_LEDS): r = j * 100 // (NUM_LEDS - 1) b = 100 - j * 100 // (NUM_LEDS - 1) if j != i % NUM_LEDS: r >>= 3 b >>= 3 ar[j] = r << 16 | b sm.put(ar, 8) time.sleep_ms(50)

关键点:freq=8_000_000设定位元速率;autopull=Truepull_thresh=24让 OSR 自动从 TX FIFO 补给 24 位颜色数据;sm.put(ar, 8)shift=8将数据对齐到 FIFO 字的高 24 位。

9.2 多路 UART 发送(pio_uart_tx.py)

该示例在引脚 10~17 上同时创建8 个 UART TX,展示了多个状态机的并行复用:

from machine import Pin from rp2 import PIO, StateMachine, asm_pio UART_BAUD = 115200 PIN_BASE = 10 NUM_UARTS = 8 @asm_pio(sideset_init=PIO.OUT_HIGH, out_init=PIO.OUT_HIGH, out_shiftdir=PIO.SHIFT_RIGHT) def uart_tx(): pull() set(x, 7) .side(0) [7] label("bitloop") out(pins, 1) [6] jmp(x_dec, "bitloop") nop() .side(1) [6] uarts = [] for i in range(NUM_UARTS): sm = StateMachine( i, uart_tx, freq=8 * UART_BAUD, sideset_base=Pin(PIN_BASE + i), out_base=Pin(PIN_BASE + i), ) sm.active(1) uarts.append(sm) def pio_uart_print(sm, s): for c in s: sm.put(ord(c)) for i, u in enumerate(uarts): pio_uart_print(u, "Hello from UART {}!\n".format(i))

要点:freq=8 * UART_BAUD因为每个数据位由 8 个时钟周期产生(1 起始位 + 8 数据位 + 停止位,每指令周期耗时对齐);out_shiftdir=PIO.SHIFT_RIGHT保证 LSB 先发。

仓库中 tests/target_wiring/rp2.py 还提供了硬件联动的测试场景,examples/rp2目录下另有 pio_uart_rx.py、pio_quadrature_encoder.py、pio_pwm.py、pio_1hz.py 等示例,分别覆盖接收、编码器、PWM 与精确计时等典型 PIO 应用,可作为进阶参考。

十、总结:StateMachine 编程要点速查

  1. 资源有限:RP2040 共 8 个状态机、每个 PIO 32 条指令;程序相同会自动复用指令内存。
  2. 频率下限:125 MHz 系统时钟下freq最小 1908 Hz,更慢需降低系统时钟。
  3. 引脚归属in_base的引脚需手动配置;jmp_pin默认自动配置为输入;out/set/sideset引脚由程序内*_init与构造函数基址共同初始化。
  4. FIFO 通信put()/get()默认阻塞,批量数据可用 array/bytearray;读写前用tx_fifo()/rx_fifo()查询水位。
  5. 调试利器exec()可单步注入指令,restart()可干净地重置状态机。
  6. 高性能搬运:利用 buffer protocol 将 StateMachine 直接交给rp2.DMA(),可让 FIFO 数据搬运完全脱离 CPU。
  • 嵌入式
  • 语言运行时
  • 编程语言
  • 解释器
  • 编译器
  • 物联网
  • 系统编程

【免费下载链接】micropython

MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems

项目地址:https://gitcode.com/gh_mirrors/mi/micropython
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询