完全指南:状态机、PIO 汇编指令与实战)
嵌入式语言运行时编程语言解释器编译器物联网系统编程【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址https://gitcode.com/gh_mirrors/mi/micropython点击查看免费下载本指南基于当前仓库中的 docs/rp2/tutorial/pio.rst 教程系统讲解 Raspberry Pi RP2040/RP2350 可编程 IOProgrammable Input Output, PIO子系统在 MicroPython 中的使用方法。你将掌握 PIO 状态机的硬件架构FIFO、指令内存、九条 PIO 汇编指令与修饰符、rp2.asm_pio与rp2.StateMachine的完整 API并通过 1Hz LED 闪烁、WS2812 灯带驱动、PWM 渐变三个仓库内真实示例学会编写周期精确的 PIO 程序并理解其底层原理。PIO 解决什么问题从位拆腾bit banging说起RP2xxx 系列微控制器RP2040、RP2350为 I2C、SPI、UART 等标准通信协议提供了硬件外设支持。但对于没有硬件支持的协议或需要自定义 IO 行为的场景PIO可编程输入输出就派上了用场。MicroPython 应用中常用到一种叫位拆腾bit banging的技术——通过快速开关引脚来传输数据。这种做法的代价是CPU 必须把大量时间花在反复置位/清零引脚上而不是执行其他业务逻辑导致整个流程变慢。PIO 的价值在于让位拆腾在后台进行状态机State Machine由独立的硬件时钟驱动与 CPU 并行运行CPU 可以同时执行主程序逻辑。因此 PIO 特别适合时序敏感、周期精确的场景如 WS2812NeoPixel灯带协议、DHT 传感器、软件 UART 等。PIO 状态机与 FIFOCPU 与硬件的解耦桥梁PIO 的核心执行单元是状态机State Machine。状态机通过先入先出FIFO缓冲区与其他实体传输数据这使得状态机和主处理器既能独立工作又能同步数据。FIFO 还可以与 DMA 联动在几乎不占用 CPU 的情况下传输大量数据关于 DMA 通道的配置可参考 docs/library/rp2.DMA.rst。在 MicroPython 中rp2.StateMachine类支持 buffer 协议可以直接作为rp2.DMA()通道的读写参数实现 PIO FIFO 与内存之间的直接传输这正是FIFO 可链接到 DMA的 API 级体现见 docs/library/rp2.StateMachine.rst 的 Buffer protocol 一节。硬件资源对比RP2040 与 RP2350原教程给出了两个芯片的 PIO 硬件资源差异整理如下特性RP2040RP2350PIO 块数量2 个3 个每块状态机数量4 个4 个状态机总数8 个编号 0–712 个编号 0–11每状态机 TX/RX FIFO各 4 字每字 32 位各 4 字可合并为 8 字单向 FIFO 提升带宽RX FIFO 特性普通 FIFO支持对其寄存器的随机读写访问兼容性—向后兼容 RP2040并带额外增强RP20402 个 PIO 块每块 4 个独立状态机共 8 个每个状态机的 TX 与 RX FIFO 各有 4 个字每字 32 位。RP23503 个 PIO 块每块 4 个独立状态机共 12 个。与 RP2040 类似每个状态机默认拥有 4 字 FIFO但具备额外功能每个状态机的 TX 或 RX FIFO 可合并成单个 8 字 FIFO以获得更高带宽对应asm_pio的fifo_join参数见下文RX FIFO 支持对其寄存器的随机读写访问。兼容性说明RP2350 的 PIO 向后兼容 RP2040并带一些额外增强。目前 MicroPython 对 RP2350 PIO 增强功能的原生支持还非常有限部分特性的工作已在进行中。因此教程主体以两者通用的基础功能为准。指令内存与通用指令格式每个 PIO 块拥有32 条指令的指令内存由其四个状态机共享对应源码中 ports/rp2/rp2_pio.c 用rp2_pio_instruction_memory_usage_mask[NUM_PIOS]跟踪每个 PIO 块指令内存占用。所有 PIO 指令遵循统一模式instruction .side(side_set_value) [delay_value]其中 side-set 修饰符.side(...)和延时[...]都是可选的指定后一条指令可同时完成多件事。这保证了 PIO 程序短小高效。九条核心 PIO 指令PIO 共有九条指令各自任务如下指令功能jmp()将控制流转移到程序的另一部分wait()暂停执行直到某个特定事件发生in_()将位从源暂存寄存器或一组引脚移入输入移位寄存器ISRout()将位从输出移位寄存器OSR移到目的端push()将 ISR 数据送入 RX FIFOpull()从 TX FIFO 取数据到 OSRmov()将数据从源移动到目的端irq()设置或清除一个 IRQ 标志set()将一个立即数写入目的端指令修饰符与伪指令.side()在指令执行开始时设置 side-set 引脚的电平值[]在指令执行完毕后延迟若干时钟周期等价于.delay(value)取值范围 0–31使用 side-set 引脚时最大值会减小。此外还有两个常用伪指令/操作nop()汇编为mov(y, y)无副作用常用于精确填充延时周期见pio_1hz示例word(instr, labelNone)在汇编输出中插入任意 16 位原始值label存在时将其值按位或进instr。汇编指令详解根据 docs/library/rp2.rst这些指令的完整形式与参数如下jmp(...)两种形式jmp(label)无条件跳转到 labeljmp(cond, label)条件跳转条件可为not_x/not_y寄存器为零、x_dec/y_dec寄存器非零且后自减、x_not_yX≠Y、pin输入引脚为高、not_osreOSR 未达到阈值即未空。wait(polarity, src, index)阻塞等待某引脚或 IRQ 线为高/低。polarity为 0/1 表示等待低/高src可为gpio绝对引脚、pin相对in_base的引脚、irqindex取值 0–31。in_(src, bit_count)从src移位进 ISRsrc可为pins/x/y/null/isr/osrbit_count为 1–32 位。out(dest, bit_count)从 OSR 移位出到destdest可为pins/x/y/pindirs/pc/isr/execbit_count为 1–32 位。push(...)将 ISR 推入 RX FIFO 并清零 ISR。形式有push()、push(block)、push(noblock)、push(iffull)、push(iffull, block)、push(iffull, noblock)。默认blockRX FIFO 满时阻塞iffull表示仅当输入移位计数达到阈值时才推。pull(...)从 TX FIFO 拉取到 OSR。形式同理有pull()、pull(block)、pull(noblock)、pull(ifempty)等默认blockTX FIFO 空时阻塞ifempty表示仅当输出移位计数达到阈值时才拉。mov(dest, src)dest可为pins/x/y/exec/pc/isr/osrsrc可为pins/x/y/null/status/isr/osr可选地包上invert()或reverse()修饰两者不能同时使用。irq(...)两种形式irq(index)index 为 0–7 或rel(0)–rel(7)irq(mode, index)mode为block阻塞直至标志被其他实体清除或clear清除标志rel()相对 IRQ 索引按模 4 加法叠加状态机 IDIRQ 0–3 对处理器可见4–7 仅用于状态机间通信。set(dest, data)dest可为pins/x/y/pindirsdata取值 0–31。汇编指令directiveswrap_target()指定程序回绕后继续执行的位置默认是 PIO 例程开头wrap()指定程序结束并回绕的位置若未指定则自动加在例程末尾回绕不消耗任何执行周期label(label)在当前位置定义标签供jmp()使用标签可为字符串或整数word()见上文直接输出一条 16 位原始指令字。实战示例用 PIO 让 LED 以 1Hz 闪烁并触发 IRQ原教程以仓库中的 examples/rp2/pio_1hz.py 为例演示 PIO 与状态机的最基本用法。完整代码与教程一致注释亦保留# Example using PIO to blink an LED and raise an IRQ at 1Hz. # Note: this does not work on Pico W because it uses Pin(25) for LED output. import time from machine import Pin import rp2 rp2.asm_pio(set_initrp2.PIO.OUT_LOW) def blink_1hz(): # Cycles: 1 1 6 32 * (30 1) 1000 irq(rel(0)) set(pins, 1) set(x, 31) [5] label(delay_high) nop() [29] jmp(x_dec, delay_high) # Cycles: 1 1 6 32 * (30 1) 1000 nop() set(pins, 0) set(x, 31) [5] label(delay_low) nop() [29] jmp(x_dec, delay_low) # Create the StateMachine with the blink_1hz program, outputting on Pin(25). sm rp2.StateMachine(0, blink_1hz, freq2000, set_basePin(25)) # Set the IRQ handler to print the millisecond timestamp. sm.irq(lambda p: print(time.ticks_ms())) # Start the StateMachine. sm.active(1)逐行拆解执行流程这段代码创建了一个rp2.StateMachine实例以2000Hz运行blink_1hz程序并连接到 25 号引脚。程序让该引脚上的 LED 以 1Hz 闪烁并在 LED 点亮时触发 IRQIRQ 回调lambda函数打印毫秒时间戳。blink_1hz是 PIO 汇编例程通过set_initrp2.PIO.OUT_LOW把所用引脚配置为输出且初始为低电平。各条指令的作用irq(rel(0))触发与状态机关联的 IRQset(pins, 1)点亮 LED引脚置高set(x, 31) [5]把 31 写入寄存器 X再延时 5 个周期nop() [29]空操作并延时共等待 30 个周期jmp(x_dec, delay_high)只要 X 非零就跳回delay_high标签并后自减 X。X 初始为 31因此会跳转 31 次nop() [29]总共执行 32 次注意每次循环jmp本身还占 1 个指令周期单独的nop()与 IRQ 触发消耗的 1 个周期对应保证 LED 亮、灭两半周期数一致set(pins, 0)熄灭 LED引脚置低随后再执行 32 次nop() [29]jmp(...)循环未指定wrap_target()与wrap()故采用默认值程序执行到底部后从顶部回绕回绕不消耗周期。周期计算验证注释中写得很清楚高电平半周期为1 1 6 32 * (30 1) 1000个状态机周期irq1 周期 set1 周期 set(x,31)[5]6 周期 32 轮nop()[29]的 30 周期与jmp的 1 周期低电平半周期同样为 1000 周期。整个例程恰好消耗2000 个状态机周期将状态机频率设为 2000Hz 后LED 即以 1Hz 闪烁。关于频率与时钟StateMachine.init()的freq参数决定状态机运行频率默认为系统时钟频率。时钟分频器按系统时钟频率 / freq计算因此存在轻微舍入误差最小分频器为系统时钟的 1/65536即在默认 125MHz 系统时钟下freq最小约为1908Hz更慢的频率需先用machine.freq()降低系统时钟。本例在 RP2040 与 RP2350使用状态机 ID 0上表现一致由于 RP2350 系统时钟更快可支持更高的freq值。实战延伸更多仓库内示例原教程之外仓库 examples/rp2/ 还提供了多个 PIO 实战程序可进一步印证指令与修饰符的用法WS2812 灯带驱动side-set autopullexamples/rp2/pio_ws2812.py 用 4 条指令驱动 WS2812 协议充分展示.side()修饰符与自动拉取autopull的威力rp2.asm_pio( sideset_initrp2.PIO.OUT_LOW, out_shiftdirrp2.PIO.SHIFT_LEFT, autopullTrue, pull_thresh24, ) 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()关键点sideset_initrp2.PIO.OUT_LOW把 side-set 引脚初始化为输出低autopullTrue与pull_thresh24让 OSR 在移出 24 位一个 LED 的 RGB 值后自动从 TX FIFO 拉取新数据out(x, 1)每次移出 1 位判断是1码还是0码配合.side()与延时精确构造 WS2812 要求的 800kHz 时序。主程序用sm.put(ar, 8)批量推送 32 位整数数组shift8实现字节对齐。软件 PWM 与 LED 渐变noblock exec 预装载examples/rp2/pio_pwm.py 用pull(noblock)、mov()、jmp(x_not_y, ...)实现软件 PWM并演示StateMachine.exec()预装载 ISR 的技巧asm_pio(sideset_initPIO.OUT_LOW) def pwm_prog(): pull(noblock) .side(0) mov(x, osr) # Keep most recent pull data stashed in X, for recycling by noblock mov(y, isr) # ISR must be preloaded with PWM count max label(pwmloop) jmp(x_not_y, skip) nop() .side(1) label(skip) jmp(y_dec, pwmloop)初始化时先用sm.put(max_count)写入数据、再用sm.exec(pull())和sm.exec(mov(isr, osr))把最大值预装载进 ISRpull(noblock)在 TX FIFO 为空时不阻塞而是复用 X 中缓存的旧值实现无抖动的占空比更新。运行效果是在 Pico 板载 LEDPin 25上平滑的呼吸灯效果。完整 APIasm_pio 与 StateMachinerp2.asm_pio(...)装饰器参数根据 docs/library/rp2.rstasm_pio的完整签名为asm_pio(*, out_initNone, set_initNone, sideset_initNone, side_pindirFalse, in_shiftdirPIO.SHIFT_LEFT, out_shiftdirPIO.SHIFT_LEFT, autopushFalse, autopullFalse, push_thresh32, pull_thresh32, fifo_joinPIO.JOIN_NONE)引脚初始状态参数取PIO.IN_LOW/PIO.IN_HIGH/PIO.OUT_LOW/PIO.OUT_HIGH多引脚时传元组如out_init(PIO.OUT_LOW, PIO.OUT_LOW)out_init配置out()指令所用引脚的初始状态set_init配置set()指令所用引脚最多 5 个sideset_init配置.side()修饰符所用引脚最多 5 个side_pindirTrue时.side()用于控制引脚方向而非电平值。移位相关参数可在StateMachine.init()中覆盖in_shiftdir/out_shiftdirISR/OSR 的默认移位方向PIO.SHIFT_LEFT或PIO.SHIFT_RIGHTpush_thresh/pull_thresh触发自动推/拉或条件推/拉的阈值位autopush/autopull是否启用自动推/拉fifo_join是否将 4 字 TX/RX FIFO 合并为单一方向 8 字 FIFO取PIO.JOIN_NONE/PIO.JOIN_RX/PIO.JOIN_TX这正是 RP2350 手册级 FIFO 合并能力在 MicroPython 层的映射。另有两个相关 APIasm_pio_encode(instr, sideset_count, sideset_optFalse)用于编码单条指令如rp2.asm_pio_encode(set(0, 1), 0)返回57345bootsel_button()用于读取 BOOTSEL 按键。汇编出错时抛出PIOASMError异常。rp2.StateMachine类构造StateMachine(id, program, ...)RP2040 上 id 为 0–7也可通过rp2.PIO(id).state_machine(sm_id)获取见 docs/library/rp2.PIO.rst。StateMachine.init()的关键参数freq状态机运行频率Hz默认系统时钟频率in_base/out_base/set_base/jmp_pin/sideset_base分别指定in()/out()/set()/jmp(pin,...)/side-set 使用的首引脚in_base引脚需手动配置方向jmp_pin默认配置为输入in_shiftdir/out_shiftdir/push_thresh/pull_thresh覆盖asm_pio中的同名默认值。常用方法active([value])获取/设置状态机运行状态restart()复位状态机内部状态移位计数、ISR、延时计数器、等待 IRQ 状态、被阻塞的exec()指令并跳回程序开头exec(instr)执行单条 PIO 指令支持字符串走asm_pio_encode或已编码整数get(bufNone, shift0)从 RX FIFO 取一个字空则阻塞返回word shiftput(value, shift0)向 TX FIFO 推入字满则阻塞支持整数、B/H/I类型数组与bytearray状态机收到word shiftrx_fifo()/tx_fifo()查询 FIFO 中的字数irq(handlerNone, trigger0|1, hardFalse)获取/配置状态机 IRQ。在 ports/rp2/rp2_pio.c 的源码实现中可以看到这些 API 的底层机制IRQ 由pio_irq0中断服务例程统一分发PIO0_IRQ_0/PIO1_IRQ_0RP2350 上另有pio2_irq0程序被pio_add_program()加载到共享指令内存并通过掩码跟踪占用asm_pio_init_gpio在初始化时用gpio_set_function(..., GPIO_FUNC_PIO0 pio_index)把引脚复用为 PIO 功能——这就是状态机接管引脚的硬件层面证据。兼容性注意事项原教程最后给出了 RP2040 与 RP2350 的兼容性要点使用本文所述基础指令的 PIO 程序在 RP2040 与 RP2350 之间完全兼容MicroPython 的rp2模块 APIrp2.asm_pio、StateMachine等完全一致RP2350 额外支持状态机 ID 0–11RP2350 还提供高级特性新指令、FIFO 模式、安全机制、GPIO 重定位但不在本入门教程范围内详见 RP2350 数据手册由于两颗芯片的默认 GPIO 复位状态存在差异个别程序可能需要进行小幅调整。从 docs/rp2/general.rst 可知RP2040 为 40nm 双核 Cortex-M0133MHz可超频至 400MHz、264KB SRAM、30 个 GPIO其中 4 个可作模拟输入、8 个 PIO 状态机RP2350 为双核 Cortex-M33 或 Hazard3 RISC-V最高 150MHz、520KB SRAMRP2350A 30 个 GPIO / RP2350B 48 个 GPIO共 12 个 PIO 状态机。这些差异也是编写 PIO 程序时评估资源上限的依据。小结通过本指南你可以理解 PIO 如何将位拆腾卸载到独立状态机掌握九条 PIO 指令、.side()/[]修饰符与wrap_target()/wrap()/label()/word()汇编指令使用rp2.asm_pio编写周期精确的程序并通过StateMachine的频率设置、FIFO 读写与 IRQ 回调与 CPU 交互同时了解 RP20408 个状态机与 RP235012 个状态机、FIFO 合并、RX 随机访问之间的资源差异与兼容性边界。建议下一步阅读 docs/library/rp2.rst、docs/library/rp2.StateMachine.rst 与 examples/rp2/ 下的全部示例并在真实板卡上逐一运行验证。赞分享嵌入式语言运行时编程语言解释器编译器物联网系统编程【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址https://gitcode.com/gh_mirrors/mi/micropython点击查看免费下载相关推荐MicroPython rp2 模块完全指南RP2040/RP2350 的 PIO、状态机与 DMA 编程MicroPython rp2 模块完全指南RP2040/RP2350 的 PIO、状态机与 DMA 编程 rp2 模块是 MicroPython 针对树莓派嵌入式语言运行时编程语言解释器编译器物联网系统编程MicroPython RP2 移植版完全指南构建、烧录与 PIO 编程实战MicroPython RP2 移植版完全指南构建、烧录与 PIO 编程实战 本指南围绕 MicroPython 官方仓库中的 RP2 移植版ports/r嵌入式语言运行时编程语言解释器编译器物联网系统编程MicroPython rp2.PIO 类详解RP2040 可编程 I/OPIO接口的进阶用法MicroPython rp2.PIO 类详解RP2040 可编程 I/OPIO接口的进阶用法 本文基于 MicroPython 仓库中 rp2.PIO.嵌入式语言运行时编程语言解释器编译器物联网系统编程上一篇Rustpad终极自托管实时协同代码编辑器完全指南下一篇Node-lru-cache与Redis对比何时使用内存缓存终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考