ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MicroPython rp2.PIO 类详解:RP2040 可编程 I/O(PIO)接口的进阶用法

MicroPython rp2.PIO 类详解:RP2040 可编程 I/O(PIO)接口的进阶用法 MicroPython rp2.PIO 类详解RP2040 可编程 I/OPIO接口的进阶用法【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython本文基于 MicroPython 仓库中 rp2.PIO.rst 文档展开系统讲解rp2.PIO类的构造、GPIO 基址配置、程序内存管理、状态机获取与中断绑定等进阶 API并结合 rp2_pio.c 的 C 实现源码逐条印证其行为细节。读完本篇后你将能够理解PIO与StateMachine、asm_pio三者之间的分工关系并在多引脚域GPIO0/GPIO16、指令内存复用、SM 级中断等进阶场景下正确使用 PIO 接口。PIO 在 rp2 模块中的定位为什么需要进阶类RP2040 的 PIOProgrammable I/O是一套由硬件状态机组成的可编程 I/O 外设每个 PIO 实例包含一段共享的 32 个 16 位指令存储器以及 4 个相互独立、可并行运行的状态机State Machine。rp2模块提供了三层接口职责分工明确rp2.asm_pio装饰器用 Python 语法编写 PIO 汇编程序在函数定义时完成指令编码rp2.StateMachine日常使用的首选入口负责把程序加载进指令内存、配置状态机引脚与移位寄存器、管理 TX/RX FIFOrp2.PIO进阶接口直接操作某个 PIO 实例本身——管理指令内存、切换 GPIO 引脚域、获取裸状态机对象、绑定实例级中断。官方文档 rp2.PIO.rst 的开篇就明确了这一分层The preferred way to interact with PIO is usingrp2.StateMachine, the PIO class is for advanced use.PIO对象是单例式的每次rp2.PIO(id)构造返回的都是同一个 C 侧对象。这一点可以从源码 rp2_pio.c 得到确认——模块内维护了一个固定的对象数组static rp2_pio_obj_t rp2_pio_obj[] { { { rp2_pio_type }, pio0, PIO0_IRQ_0 }, { { rp2_pio_type }, pio1, PIO1_IRQ_0 }, #if NUM_PIOS 3 { { rp2_pio_type }, pio2, PIO2_IRQ_0 }, #endif }; // constructor(id) static mp_obj_t rp2_pio_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_arg_check_num(n_args, n_kw, 1, 1, false); int pio_id mp_obj_get_int(args[0]); if (!(0 pio_id pio_id MP_ARRAY_SIZE(rp2_pio_obj))) { mp_raise_ValueError(MP_ERROR_TEXT(invalid PIO)); } return MP_OBJ_FROM_PTR(rp2_pio_obj[pio_id]); }构造函数签名严格限定为一个且仅一个位置参数mp_arg_check_num(n_args, n_kw, 1, 1, false)因此传入多余参数会抛出ValueError这与文档中 Raises aValueErrorif any other argument is provided 的描述一致。值得注意的是源码中通过NUM_PIOS 3条件编译支持了第三个 PIO 实例——从源码结构看该数组是为 RP235x 系列3 个 PIO、共 12 个状态机预留的RP2040 上则只有 2 个实例。构造函数 PIO(id)import rp2 from rp2 import PIO pio0 PIO(0) # 第一个 PIO 实例 pio1 PIO(1) # 第二个 PIO 实例 print(pio0) # PIO(0) print(pio1) # PIO(1)RP2040 上实例编号为 0 和 1越界的id会抛出ValueError源码中错误消息为 invalid PIO。对象的打印格式由 rp2_pio.c 的rp2_pio_print实现输出PIO(索引)。两个实例在硬件上完全对称各自拥有 32 条指令的共享指令内存和 4 个状态机因此系统共有 8 个全局状态机编号 0–7PIO0 的 SM0–SM3 对应全局 0–3PIO1 的 SM0–SM3 对应全局 4–7。PIO.gpio_base([base])查询与设置 GPIO 引脚域base PIO(0).gpio_base() # 查询当前 GPIO 基址返回 Pin 对象 PIO(0).gpio_base(machine.Pin(16)) # 切换到 GPIO16 基址32~48 号引脚域 print(PIO(0).gpio_base()) # Pin(16)RP2040 的 32 个 GPIO 被划分为两个可寻址域GPIO0 域引脚 0–31与 GPIO16 域引脚 16–48其中 32–48 号引脚只能通过该域访问。每个 PIO 实例可以独立选择一个基址从而让该实例的in()/out()等指令去驱动不同引脚域上的引脚。文档中强调了两个关键约束参数必须是引脚对象或对应的引脚号整数且只能是 GPIO0 或 GPIO16设置 GPIO 基址必须在添加任何程序、创建任何状态机之前完成。源码 rp2_pio.c 实现了这两条约束#if PICO_PIO_USE_GPIO_BASE // PIO.gpio_base([base]) static mp_obj_t rp2_pio_gpio_base(size_t n_args, const mp_obj_t *args) { rp2_pio_obj_t *self MP_OBJ_TO_PTR(args[0]); if (n_args 1) { uint gpio_base mp_hal_get_pin_obj(args[1]); // Must be 0 for GPIOs 0 to 31 inclusive, or 16 for GPIOs 16 to 48 inclusive. if (!(gpio_base 0 || gpio_base 16)) { mp_raise_ValueError(MP_ERROR_TEXT(invalid GPIO base)); } if (pio_set_gpio_base(self-pio, gpio_base) ! PICO_OK) { mp_raise_OSError(MP_EINVAL); } } // Return current gpio_base value. return pio_get_gpio_base(self-pio) 0 ? (mp_obj_t)pin_GPIO0 : (mp_obj_t)pin_GPIO16; } #endif返回值为Pin(0)或Pin(16)对象分别表示当前基址是 GPIO0 还是 GPIO16。当基址为 GPIO16 时后续通过StateMachine.init()传入的out_base等引脚会做范围校验——见 rp2_pio.c 中asm_pio_get_pins的ValueError(..._base not within gpio_base range)检查即引脚号必须落在当前基址覆盖的区间内。典型应用场景RP2040 的 GPIO32–39 默认接内部闪存通常不可用而 GPIO40–47以及 16–31 中的部分引脚可以通过 GPIO16 基址被 PIO 访问。需要同时使用两组物理引脚做不同协议时可以把两个 PIO 实例分别绑定到不同基址上。PIO.add_program(program)向指令内存加载程序rp2.asm_pio() def blink(): set(pins, 1) set(pins, 0) prog blink # asm_pio 装饰器返回的是程序对象不是普通函数 pio PIO(0) pio.add_program(prog) # 占用 2 条指令内存返回 Noneadd_program把asm_pio生成的程序写入该 PIO 实例的共享指令内存。这里有一个容易误解的点每个 PIO 实例只有 32 条 16 位指令的存储空间且是实例内所有状态机共享的。多个状态机若要运行同一程序StateMachine.init会复用已加载的偏移rp2_pio.c 中先读取程序对象里保存的PROG_OFFSET_PIOx字段offset 0才真正加载而加载两个不同程序则分别占用内存。空间不足时抛出OSError(ENOMEM)源码实现见 rp2_pio.c// PIO.add_program(prog) static mp_obj_t rp2_pio_add_program(mp_obj_t self_in, mp_obj_t prog_in) { rp2_pio_obj_t *self MP_OBJ_TO_PTR(self_in); mp_obj_t *prog; mp_obj_get_array_fixed_n(prog_in, PROG_MAX_FIELDS, prog); mp_buffer_info_t bufinfo; mp_get_buffer_raise(prog[PROG_DATA], bufinfo, MP_BUFFER_READ); struct pio_program pio_program { bufinfo.buf, bufinfo.len / 2, -1 }; if (!pio_can_add_program(self-pio, pio_program)) { mp_raise_OSError(MP_ENOMEM); } uint offset rp2_pio_add_managed_program(self-pio, pio_program); // Store the program offset in the program object. prog[PROG_OFFSET_PIO0 pio_get_index(self-pio)] MP_OBJ_NEW_SMALL_INT(offset); return mp_const_none; }程序对象的内部结构asm_pio装饰器返回的程序实际上是一个 9 元数组其字段定义在 Python 侧 rp2.py 与 C 侧 rp2_pio.c 中一一对应索引名称含义0PROG_DATAarray(H)编码好的 16 位指令序列1PROG_OFFSET_PIO0该程序在 PIO0 指令内存中的偏移未加载时为 -12PROG_OFFSET_PIO1同上对应 PIO13PROG_OFFSET_PIO2同上对应 PIO2RP235x 预留4PROG_EXECCTRLwrap 位置、sideset 位宽等执行控制字5PROG_SHIFTCTRL移位方向、阈值、autopush/autopull、FIFO 连接等6PROG_OUT_PINSout_init初始化的引脚/引脚组7PROG_SET_PINSset_init初始化的引脚/引脚组8PROG_SIDESET_PINSsideset_init初始化的引脚/引脚组这个结构解释了为什么同一程序可以跨实例复用记录每个实例的加载偏移独立存放在各自字段中。asm_pio的编码实现双 pass 汇编、shiftctrl/execctrl寄存器的位域拼装可以在 rp2.py 的PIOASMEmit类中完整阅读。指令内存的记账机制除了 SDK 提供的pio_can_add_program空间检查MicroPython 还用一张位掩码跟踪由本模块管理的指令内存占用见 rp2_pio.c// These masks keep track of PIO instruction memory used by this module. static uint32_t rp2_pio_instruction_memory_usage_mask[NUM_PIOS];rp2_pio_add_managed_program/rp2_pio_remove_managed_program在调用 SDK 的pio_add_program/pio_remove_program的同时置位/清除对应区间的掩码位位宽 32 正好与 32 条指令一一对应。这套记账还有一个重要用途系统重置资源时如进入 USB 复合模式、端口 deinitrp2_pio_deinit 会调用rp2_pio_remove_all_managed_programs只清除本模块管理的程序源码注释明确说明其他子系统如 CYW43 无线驱动可能也在用 PIO它们的程序必须保持完好。也就是说 PIO 指令内存是与系统内其他 C 驱动共享的资源PIO对象层面通过记账避免误删他人程序。PIO.remove_program([program])释放指令内存pio.remove_program(prog) # 仅移除指定程序 pio.remove_program() # 移除本模块在 PIO0 上加载的所有程序行为要点与 rp2.PIO.rst 一致无参调用时移除全部程序实现上按offset0、length32构造一个覆盖整个指令内存的程序来删除见 rp2_pio.c重复移除一个已删除的程序不是错误移除时会把程序对象中对应的偏移字段重置为 -1因此之后再次add_program会重新分配位置若传入的程序从未加载偏移 0则抛出ValueError(prog not in instruction memory)。// PIO.remove_program([prog]) static mp_obj_t rp2_pio_remove_program(size_t n_args, const mp_obj_t *args) { rp2_pio_obj_t *self MP_OBJ_TO_PTR(args[0]); uint8_t length 32; // 默认整段 32 条指令 uint offset 0; if (n_args 1) { // ... 取程序数据与偏移 ... if (offset 0) { mp_raise_ValueError(MP_ERROR_TEXT(prog not in instruction memory)); } prog[PROG_OFFSET_PIO0 pio_get_index(self-pio)] MP_OBJ_NEW_SMALL_INT(-1); } struct pio_program pio_program { NULL, length, -1 }; rp2_pio_remove_managed_program(self-pio, pio_program, offset); return mp_const_none; }PIO.state_machine(id, [program, ...])获取并可选初始化状态机sm PIO(1).state_machine(3) # 返回全局编号 7 的状态机 print(sm) # StateMachine(7) sm2 PIO(0).state_machine(1, prog) # 获取并顺带 init等价于 StateMachine.init每个 PIO 实例有 4 个状态机id取 0–3越界抛ValueError返回的是与rp2.StateMachine(全局id)完全相同的对象rp2.PIO(1).state_machine(3)即rp2.StateMachine(7)打印为StateMachine(7)示例见 rp2.PIO.rst 中的交互示例若继续传入program及freq、in_base等参数则等价于调用 rp2.StateMachine.rst 中定义的StateMachine.init(...)完成初始化。源码 rp2_pio.c 展示了获取逻辑先根据pio_id * 4 sm_id定位全局状态机对象获取过程中会做 claim 检查防止与其他使用者冲突见下文资源占用一节再在有额外参数时转调rp2_state_machine_init_helper// PIO.state_machine(id, prog, freq-1, *, setNone) static mp_obj_t rp2_pio_state_machine(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { rp2_pio_obj_t *self MP_OBJ_TO_PTR(pos_args[0]); mp_int_t sm_id mp_obj_get_int(pos_args[1]); if (!(0 sm_id sm_id 4)) { mp_raise_ValueError(MP_ERROR_TEXT(invalid StateMachine)); } // Return the correct StateMachine object. const rp2_state_machine_obj_t *sm rp2_state_machine_get_object(pio_get_index(self-pio) * 4 sm_id); if (n_args 2 || kw_args-used 0) { rp2_state_machine_init_helper(sm, n_args - 2, pos_args 2, kw_args); } return MP_OBJ_FROM_PTR(sm); }通过PIO.state_machine()还是直接StateMachine(id)获取最终落到同一个静态对象数组 rp2_pio.c因此状态FIFO 内容、配置是共享的与PIO 单例同理。PIO.irq(handler, trigger, hard)PIO 实例级中断def on_sm_event(pio): print(SM event!) pio PIO(0) irq_obj pio.irq(on_sm_event, triggerrp2.PIO.IRQ_SM0 | rp2.PIO.IRQ_SM1, hardFalse)要点对照 rp2.PIO.rst 与 rp2_pio.cMicroPython 只使用每个 PIO 实例的 IRQ 0IRQ 1 不可用RP2040 每个 PIO 有两条硬件 IRQ 线inte0/ints0供 PIO0/PIO1 各自使用trigger默认值为 0xf00即四个IRQ_SMx常量的按位或对应inte0寄存器中 SM0–SM3 的位可以只订阅其中一部分返回的 IRQ 对象是 MicroPython 统一中断框架mp_irq_obj_t的成员支持.handler、.flags、.triggers属性hardTrue注册硬中断在调度器层面同步执行hardFalse为软中断通过事件循环派发该中断同时服务于 SM 级事件源码中 pio_irq0 处理函数会先按irq-trigger匹配 PIO 级 IRQ 对象再逐一遍历 4 个状态机各自的 IRQ 对象分别触发回调。static void pio_irq0(PIO pio) { uint32_t ints pio-ints0; // Acknowledge SM0-3 IRQs if they are enabled on this IRQ0. pio-irq ints 8; rp2_pio_irq_obj_t *irq MP_STATE_PORT(rp2_pio_irq_obj[pio_get_index(pio)]); if (irq ! NULL (ints irq-trigger)) { irq-flags ints irq-trigger; mp_irq_handler(irq-base); } // Call handler if it is registered, for StateMachine irqs. for (size_t i 0; i 4; i) { rp2_state_machine_irq_obj_t *irq MP_STATE_PORT(rp2_state_machine_irq_obj[pio_get_index(pio) * 4 i]); if (irq ! NULL ((ints (8 i)) irq-trigger)) { irq-flags 1; mp_irq_handler(irq-base); } } }中断资源被占用时的保护绑定 PIO IRQ 时实现会检查该硬件中断是否已被其他资源独占rp2_pio.cvoid rp2_pio_irq_set_exclusive_handler(PIO pio, uint irq) { irq_handler_t current irq_get_exclusive_handler(irq); if ((current current ! rp2_pio_get_irq_handler(pio)) || irq_has_shared_handler(irq)) { mp_raise_ValueError(MP_ERROR_TEXT(irq claimed by external resource)); } else if (!current) { irq_set_exclusive_handler(irq, rp2_pio_get_irq_handler(pio)); } }若某个 C 驱动或另一个线程/核上的使用者已经接管了 PIO0_IRQ_0则抛出ValueError: irq claimed by external resource避免了静默覆盖别人的中断处理器。常量PIN 初始化、移位方向、FIFO 连接与 IRQ 触发位PIO类上挂载的常量全部定义在 rp2_pio.c 的类字典中取值如下IN_LOW / IN_HIGH / OUT_LOW / OUT_HIGHPIO.IN_LOW # 0引脚配置为输入初值低 PIO.IN_HIGH # 1引脚配置为输入初值高 PIO.OUT_LOW # 2引脚配置为输出初值低 PIO.OUT_HIGH # 3引脚配置为输出初值高这组常量用于asm_pio装饰器的out_init、set_init、sideset_init参数在init时由 MicroPython 一次性完成目标引脚的方向与初始电平设置。从源码看rp2_pio.c 的asm_pio_get_pins/asm_pio_init_gpio每个 pin 值的最高位是方向1输出、次低位是电平初始化时会用位掩码批量写引脚值与方向并把每个受管引脚的gpio_set_function设为GPIO_FUNC_PIOx。若某类引脚用了多个则传元组例如out_init(rp2.PIO.OUT_LOW, rp2.PIO.OUT_LOW)。SHIFT_LEFT / SHIFT_RIGHTPIO.SHIFT_LEFT # 0 PIO.SHIFT_RIGHT # 1用于asm_pio的in_shiftdir/out_shiftdir或StateMachine.init的同名参数分别控制 ISR输入移位寄存器与 OSR输出移位寄存器的移位方向。默认均为左移若希望字节低位先出/先入常改为SHIFT_RIGHT。JOIN_NONE / JOIN_TX / JOIN_RXPIO.JOIN_NONE # 0TX、RX 各 4 字 PIO.JOIN_TX # 1合并为单方向 8 字 TX FIFO PIO.JOIN_RX # 2合并为单方向 8 字 RX FIFO用于asm_pio的fifo_join参数。默认每个状态机各有一个 4 字的 TX 和一个 4 字的 RX FIFOJOIN_TX/JOIN_RX会把两个 4 字 FIFO 合并成一个 8 字单向 FIFO用于吞吐更高或需要更深缓冲的单向数据流。该值最终写入shiftctrl寄存器的高 2 位——在 Python 侧 rp2.py 中可以看到拼装过程shiftctrl ( fifo_join 30 | (pull_thresh 0x1F) 25 | (push_thresh 0x1F) 20 | out_shiftdir 19 | in_shiftdir 18 | autopull 17 | autopush 16 )IRQ_SM0 ~ IRQ_SM3PIO.IRQ_SM0 # 0x100 PIO.IRQ_SM1 # 0x200 PIO.IRQ_SM2 # 0x400 PIO.IRQ_SM3 # 0x800用于PIO.irq的trigger参数。这四个位正好对应硬件inte0寄存器中SMx 事件的中断使能位默认trigger0xF00即全部四个 SM 的事件都参与触发。综合实战从程序编写到状态机运行下面把asm_pio、StateMachine与PIO进阶接口串起来全部取自仓库自带示例。示例一WS2812 灯带驱动pio_ws2812.pyimport array, time from machine import Pin import rp2 NUM_LEDS 8 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() sm rp2.StateMachine(0, ws2812, freq8_000_000, sideset_basePin(22)) sm.active(1)这里用到的都是PIO常量OUT_LOW决定侧置引脚数据脚初始为低SHIFT_LEFT配合 24 位阈值pull_thresh24autopullTrue实现OSR 攒够 3 字节24 bit自动从 TX FIFO 取下一个像素从而主循环只需不断sm.put(ar, 8)推送 RGB 字。示例二PIO 进阶接口 状态机中断pio_quadrature_encoder.py该示例演示了程序检测状态变化 → push 到 RX FIFO → 触发 IRQ的完整数据通路并使用了sm.irq最终挂到与PIO.irq同一条pio_irq0硬件中断上rp2.asm_pio() def pio_quadrature(in_initrp2.PIO.IN_LOW): wrap_target() label(again) in_(pins, 2) mov(x, isr) jmp(x_not_y, push_data) mov(isr, null) jmp(again) label(push_data) push() irq(block, rel(0)) mov(y, x) wrap() sm StateMachine(0, pio_quadrature, freq160000, in_basePin(2)) sm.irq(encoder_state_changed_irq_handler) sm.exec(set(y, 99)) # 保证首次比较一定不同 sm.active(1)注意其中两个PIO相关细节in_initrp2.PIO.IN_LOW——按前文所述init时 MicroPython 会把in_base起始的引脚方向/电平设好并切到 PIO 功能程序内的irq(block, rel(0))会置位相对 IRQ状态机内部 IRQ进而拉高ints0中该 SM 的位最终经由 pio_irq0 派发到sm.irq(...)注册的 Python 回调回调中用sm.rx_fifo()轮询 FIFO 深度再sm.get()取数。示例三手动管理指令内存与 GPIO 基址PIO进阶接口的典型用法是把资源生命周期完全掌握在自己手里import rp2 from machine import Pin from rp2 import PIO pio PIO(1) # 1) 若程序需要驱动 32 号及以上引脚先切换引脚域必须在加载程序前完成 pio.gpio_base(Pin(16)) # 2) 手动加载程序同一程序重复加载会复用记录、不重复占内存的是 StateMachine.init 路径 # 直接 add_program 相同程序对象则会再次占用空间注意 32 条上限 rp2.asm_pio() def prog_a(): set(pins, 1) set(pins, 0) pio.add_program(prog_a) # 占用 2 条 # ... 创建状态机、运行 ... # 3) 用完释放重复 remove 不是错误 pio.remove_program(prog_a) pio.remove_program() # 兜底清空本模块加载的全部程序 pio.gpio_base(Pin(0)) # 恢复默认引脚域使用PIO.add_program与StateMachine.init自动加载的区别在于前者返回 None、把偏移写回程序对象供后续state_machine(id, program)使用适合一个 PIO 实例上手动编排多个程序的偏移布局后者对已加载的相同程序会复用偏移rp2_pio.c。资源占用与边界行为结合 rp2_pio.c 源码还可以梳理几条文档未展开但实践中会遇到的边界行为状态机占用claimStateMachine(0)或PIO(0).state_machine(0)首次获取某状态机时会调用pio_sm_claim若该状态机已被外部 C 驱动 claim例如板载外设驱动则抛出ValueError(StateMachine claimed by external resource)rp2_pio.c。系统资源重置时rp2_state_machine_reset_all会解除本模块 claim 并关闭全部 SMrp2_pio.c。指令内存上限每个 PIO 实例 32 条指令add_program前用pio_can_add_program预检空间不足抛OSError(ENOMEM)配合 32 位记账掩码remove_program()无参调用可一次清空本模块占用。与板载驱动的共存rp2_pio_deinit的注释明确提到 CYW43 驱动等外部子系统可能使用 PIO 指令内存与状态机MicroPython 的清理逻辑只回收自己记账的部分rp2_pio.c。因此在启用了射频功能的板卡上PIO(0)/PIO(1)的可用指令空间可能与裸 RP2040 不同。中断独占PIO.irq(handler...)绑定回调时走独占中断检查见前文 irq claimed by external resourcehandlerNone或不再传 handler 的再次调用则只取回 IRQ 对象、不改变硬件使能。小结三层接口的协作关系层级对象职责关键 API程序rp2.asm_pio返回的程序对象16 位指令序列 引脚初值 移位/FIFO 默认配置rp2.asm_pio(...)、rp2.asm_pio_encode状态机rp2.StateMachine运行程序、引脚/移位配置、FIFO 数据通道init、active、put/get、rx_fifo/tx_fifo、irq、exec实例rp2.PIO指令内存管理、GPIO 引脚域、裸 SM 获取、实例级 IRQPIO(id)、gpio_base、add_program/remove_program、state_machine、irq日常开发中写rp2.asm_pio程序 StateMachine运行已经能覆盖绝大多数协议类外设WS2812、软 SPI/I2C、编码器、按键扫描等可参考 examples/rp2 目录下的示例当需要多个实例分工、跨越 GPIO16 引脚域、精细控制指令内存布局或绑定 SM 级中断时再下探到rp2.PIO这一层。更多参数细节可继续查阅 rp2 模块总览文档、rp2.StateMachine以及 C 侧实现 rp2_pio.c 与 Python 侧汇编器 rp2.py。【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表