- 嵌入式
- 语言运行时
- 编程语言
- 解释器
- 编译器
- 物联网
- 系统编程
【免费下载链接】micropython
MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems
本指南以官方文档 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_base | in()指令使用的第一个引脚 | 见下方注意 |
out_base | out()指令使用的第一个引脚 | 与out_init结合初始化 |
set_base | set()指令使用的第一个引脚 | 最多 5 个 |
jmp_pin | jmp(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_LEFT或PIO.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_level与pio_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=True与pull_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 编程要点速查
- 资源有限:RP2040 共 8 个状态机、每个 PIO 32 条指令;程序相同会自动复用指令内存。
- 频率下限:125 MHz 系统时钟下
freq最小 1908 Hz,更慢需降低系统时钟。 - 引脚归属:
in_base的引脚需手动配置;jmp_pin默认自动配置为输入;out/set/sideset引脚由程序内*_init与构造函数基址共同初始化。 - FIFO 通信:
put()/get()默认阻塞,批量数据可用 array/bytearray;读写前用tx_fifo()/rx_fifo()查询水位。 - 调试利器:
exec()可单步注入指令,restart()可干净地重置状态机。 - 高性能搬运:利用 buffer protocol 将 StateMachine 直接交给
rp2.DMA(),可让 FIFO 数据搬运完全脱离 CPU。
- 嵌入式
- 语言运行时
- 编程语言
- 解释器
- 编译器
- 物联网
- 系统编程
【免费下载链接】micropython
MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems
相关推荐
MicroPython rp2.PIO 类详解:RP2040 可编程 I/O(PIO)接口的进阶用法
MicroPython rp2.PIO 类详解:RP2040 可编程 I/O(PIO)接口的进阶用法 本文基于 MicroPython 仓库中 rp2.PIO.
嵌入式语言运行时编程语言解释器编译器物联网系统编程MicroPython rp2 模块完全指南:RP2040/RP2350 的 PIO、状态机与 DMA 编程
MicroPython rp2 模块完全指南:RP2040/RP2350 的 PIO、状态机与 DMA 编程 rp2 模块是 MicroPython 针对树莓派
嵌入式语言运行时编程语言解释器编译器物联网系统编程MicroPython RP2 可编程 IO(PIO)完全指南:状态机、PIO 汇编指令与实战
MicroPython RP2 可编程 IO(PIO)完全指南:状态机、PIO 汇编指令与实战 本指南基于当前仓库中的 docs/rp2/tutorial/pi
嵌入式语言运行时编程语言解释器编译器物联网系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考