树莓派Pico MicroPython中断全解析:GPIO、定时器与UART实战

发布时间:2026/9/9 8:28:43
树莓派Pico MicroPython中断全解析:GPIO、定时器与UART实战 1. 写在前面的实话Pico 的中断资源比你想的复杂一点树莓派 Pico 这块板子因为价格便宜、双核 Cortex-M0、MicroPython 固件又成熟几乎成了入门嵌入式和高性能小项目的首选。但等你真正开始做稍微复杂一点的东西——比如按键消抖、编码器计数、串口不定长接收、定时采样——就会发现一个绕不开的问题MicroPython 下到底能用哪些中断哪些又是看似支持、实际用起来特别别扭甚至根本不行的我在 Pico 上折腾了大半年从最简单的 GPIO 中断到 UART DMA 接收踩过不少坑。今天这篇就一次性把 MicroPython 在 Pico 上的中断支持边界讲清楚先给出结论GPIO 外部中断、定时器中断、以及基于回调机制的 UART/I2C/SPI 事件在 MicroPython 里都能用但像 STM32 那种 EXTI 通道映射、DMA 中断、USB 中断、PWM 中断这类底层资源MicroPython 官方固件基本没有直接暴露你只能在 C 固件层面自己扩展或者用轮询和软定时器变通。适合刚入门的朋友建立正确预期也适合已经踩过坑的开发者对照排查。2. 中断基础Cortex-M0 的中断模型与 MicroPython 的封装逻辑2.1 RP2040 中断控制器到底长什么样Pico 使用的 RP2040 芯片基于两个 Cortex-M0 核心内部嵌套向量中断控制器NVIC最多支持 32 个外部中断线。这些中断线被分配到各个外设GPIO、UART、I2C、SPI、PWM、ADC、DMA、USB 控制器、定时器等等。Cortex-M0 的中断优先级只有 4 级0~3这一点和 M3/M4 的 16 级甚至更多级不同意味着你在设计实时性要求较高的系统时优先级只有很粗的粒度。MicroPython 的运行机制是固件底层用 C 语言实现了外设驱动同时托管了一个 Python 解释器。当中断发生时C 语言的中断服务函数ISR会先执行它会设置事件标志然后通过 MicroPython 的调度机制在 Python 虚拟机空闲时去执行你用machine模块注册的回调函数callback。换句话说Python 层面的回调并不是真正的硬实时中断而是“延迟处理”的软中断。这一点是你理解所有可用和不可用中断的钥匙因为很多“不能用”并不是硬件不支持而是 MicroPython 的封装层没有把相应中断路由给你。2.2 MicroPython 如何暴露中断能力MicroPython 提供了三种与中断相关的常见接口machine.Pin.irq(handler, trigger)GPIO 引脚中断支持上升沿、下降沿、高电平、低电平触发实际上 Pico 的 MicroPython 只支持边沿触发电平触发在部分固件版本上有 bug。machine.Timer(period, mode, callback)定时器中断定时器周期性地触发回调。machine.UART.irq(trigger, handler)UART 中断可以设置为收到字符时触发回调UART.RX_ANY主要用于接收中断。此外I2C和SPI也提供了irq方法但实际用处有限因为 MicroPython 的 I2C/SPI 驱动默认是主机模式同步操作中断只能起到提示作用。DMA 通道、USB、PWM 这些则没有开放给 Python 层的irq方法所以你没法直接写PWM.irq这样的代码。注意MicroPython 的中断回调必须在主线程虚拟机空闲时执行。如果在回调里调用time.sleep()、print()、或者大量内存分配轻则回调丢失重则导致系统崩溃或死机。2.3 能用、不能用、能变通使用的完整总览我把个人实测过的中断能力整理成一张速查表方便你大致规划项目中断类型MicroPython 支持情况实际体验替代/变通方案GPIO 外部中断边沿支持好用但回调时机有延迟无直接用定时器中断软/硬支持很稳定精度约 1ms 级无直接用UART 接收中断支持RX_ANY能实现不定长接收但需要配合缓冲区也可以轮询any()I2C 事件中断接口存在实际较少用主机模式下意义不大轮询SPI 事件中断接口存在实际较少用同上轮询ADC 中断不支持没有ADC.irq方法定时器采样PWM 中断/比较匹配中断不支持官方 API 没暴露定时器模拟DMA 中断不支持固件层面有但 Python 层不可用自己写 C 扩展或用 UART RX_ANYUSB 中断不支持与 VCP/键盘等协议栈绑定使用 USB 端口轮询EXTI类似 STM32 的独立外部中断线部分支持Pico 的每个引脚都能作为外部中断属于 GPIO 中断直接用 Pin.irq这张表基本就是我在实际项目里踩过的边界。你会发现真正不可用的主要集中在外设级中断尤其是 DMA 和比较类中断。下面逐一展开讲。3. 能用GPIO 外部中断项目里最常用也最容易被坑3.1 引脚中断的两种方式轮询 vs 中断回调在写代码之前你首先要明确一个问题什么时候用中断什么时候用轮询。很多初学者一开始就追求“中断”但其实对于 Pico 这种微控制器GPIO 扫描频率并不高如果你只是检测一个按钮200ms 轮询完全够用代码还更简单。只有遇到编码器脉冲计数、低频传感器脉冲捕捉、或者需要一边处理主逻辑一边等待外部信号的场景才值得考虑中断。Pico 的每个 GPIO 引脚在硬件上都支持边沿检测MicroPython 把这一能力封装成Pin.irq()。语法如下from machine import Pin led Pin(25, Pin.OUT) btn Pin(14, Pin.IN, Pin.PULL_UP) def btn_handler(pin): led.toggle() btn.irq(handlerbtn_handler, triggerPin.IRQ_FALLING)这段代码实现的是按钮按下时由于接了上拉按下为低电平触发下降沿翻转板载 LED。看起来很简单但实际运行时你会遇到几个问题按键抖动导致一次按下多次触发回调中断回调里有led.toggle()虽然运行很快但如果主程序正阻塞在一个长时间计算中回调会被延迟到阻塞结束才执行。3.2 一个更完整的按键中断示例含消抖和计数我在实际项目里推荐把按键中断设计成“只记录事件”的模式不要让回调里做太多事。比如下面这个 20ms 消抖 计数器示例from machine import Pin, Timer import time key_count 0 _last_time time.ticks_ms() _last_key_state 1 def key_isr(pin): global key_count, _last_time, _last_key_state now time.ticks_ms() # 简单的软件消抖小于 20ms 的跳变忽略 if time.ticks_diff(now, _last_time) 20: return _last_time now # 读取当前电平因为是在中断里 read速度极快也安全 state pin.value() # 检测到下降沿按下 if state 0 and _last_key_state 1: key_count 1 _last_key_state state btn Pin(14, Pin.IN, Pin.PULL_UP) btn.irq(handlerkey_isr, triggerPin.IRQ_FALLING | Pin.IRQ_RISING) # 主循环里读取 key_count 并且定时打印 while True: time.sleep(0.1) print(count , key_count)这个例子里我同时触发了上升沿和下降沿然后在回调内判断电平状态。这样做的好处是你能检测到完整的按键动作按下和释放。但要注意这是以牺牲一定性能为代价的因为每个边沿都会触发一次中断而 MicroPython 回调本身有开销如果你接入的是高频信号比如每分钟上千个脉冲这种写法容易崩溃。3.3 必须遵守的回调“黄金三原则”在 MicroPython 的中断回调里你要严格遵守以下三条否则程序会出各种诡异问题不要做内存分配。避免在回调里创建列表、字典、字符串哪怕是很小的hello也可能触发垃圾回收导致中断处理超时。不要调用会阻塞的函数。time.sleep()、print()、socket这类操作在回调里是禁忌轻则丢失中断事件重则引发RuntimeError: schedule queue full。回调函数要尽量短小。最好只做置标志、计个数、存数值这几个操作把复杂逻辑放到主循环里去处理。我见过有人抱怨“Pico 的中断太垃圾了居然会死机”最后发现是他在中断回调里print()调试把缓冲区打满导致固件崩溃。这不是 Pico 的锅是你违反了规则。4. 能用定时器中断精准节拍与后台采样的利器4.1 machine.Timer 的基本用法Pico 的 MicroPython 定时器分为两种machine.Timer和machine.RTC相关的 alarm。实际使用中我们最常用的是Timer它底层使用了 RP2040 的硬件定时器但 MicroPython 封装成了 4 个软定时器通道ID 0~3。你可以这么理解无论你用 1 个还是 4 个定时器MicroPython 都会通过软件管理调度底层用同一个硬件定时器产生 tick然后分发到不同的回调。基本用法from machine import Timer def tick(timer): print(tick) tim Timer() tim.init(period500, modeTimer.PERIODIC, callbacktick)period单位是毫秒mode可以选Timer.PERIODIC周期执行或Timer.ONE_SHOT单次执行。更多时候我喜欢把它用来做周期性采样比如每 10ms 读取一次传感器from machine import Timer, ADC adc ADC(26) sample_value 0 def sample(timer): global sample_value sample_value adc.read_u16() tim Timer() tim.init(period10, modeTimer.PERIODIC, callbacksample) while True: # 主循环可以干其他事sample_value 永远保存着最近一次采样值 pass4.2 定时器中断的精度与注意事项实测下来MicroPython 定时器回调的周期精度大约在 1ms 级别但并不是严格的晶振精度。原因有两个一是 Python 解释器执行回调本身耗时如果回调内容较长会导致下一次触发延迟二是 MicroPython 中断调度不是抢占式的主循环中的长时间计算会阻塞回调执行。举个例子我试过用Timer产生 100Hz 的方波去驱动步进电机结果发现频率不稳定用示波器看 jitter 大概有几百微秒。后来改用 C 语言写固件扩展才解决。因此如果你需要微秒级精度的波形或脉冲MicroPython 的定时器中断真的不适合你应该考虑用PWM或者直接写 C。定时器回调里同样不能做print()和内存分配。如果你需要调试建议设置一个全局标志在主循环中根据标志来决定是否打印flag False def timer_cb(timer): global flag flag True tim Timer(period100, modeTimer.PERIODIC, callbacktimer_cb) while True: if flag: flag False print(100ms passed)4.3 用软定时器实现延时任务替代复杂状态机定时器中断一个很巧妙的应用是状态机调度。比如你想控制一个设备分为“启动-运行-停止”三个阶段每阶段不同时长你可以在主循环里写一个不阻塞的调度器也可以直接用多个定时器。不过大部分场景用一个定时器产生心跳然后在主循环里累加计数更灵活。我的经验是如果任务数量不多定时器回调 全局标志是最简单的方案如果任务很多建议用简单的状态机链表而不是在回调里做分支逻辑。5. 能用UART / I2C / SPI 外设中断与 DMA 的现状5.1 UART 接收中断如何实现不定长串口数据很多从 STM32 转过来的朋友第一反应是找“串口空闲中断”或“DMA 接收完成中断”。很遗憾MicroPython 在 Pico 上并没有直接开放这俩。但别急MicroPython 的UART类提供了一个irq方法其中UART.RX_ANY可以在 RX 引脚有任何数据到来时触发回调。我们可以利用它实现不定长接收from machine import UART, Pin uart UART(0, baudrate115200, txPin(0), rxPin(1)) rx_data b def uart_cb(uart): global rx_data while uart.any(): rx_data uart.read(1) uart.irq(triggerUART.RX_ANY, handleruart_cb)看起来挺美好但请注意回调里使用rx_data b...会创建新字节对象属于内存分配在数据量大时容易让调度器崩溃。而且回调中while uart.any()可能长时间占用虚拟机影响其他中断。比较稳妥的做法是回调里只设置一个标志位然后在主循环里读取数据rx_flag False def uart_cb(uart): global rx_flag rx_flag True uart.irq(triggerUART.RX_ANY, handleruart_cb) while True: if rx_flag: rx_flag False # 在主循环读取所有数据 while uart.any(): data uart.read(uart.any()) # 处理 data这样虽然会有几毫秒延迟但安全稳定。也不存在“空闲中断”的概念所以判断一帧数据是否结束需要你自己设定超时机制比如相邻字节间隔超过 10ms 就算一帧结束。5.2 I2C/SPI 中断支持吗实际意义不大machine.I2C和machine.SPI也有irq()方法文档里说是用于事件回调比如 I2C 的地址匹配、SPI 的传输完成。但它们是主机模式下的阻塞操作你调用read()或write()时CPU 会一直等待传输完成这时候中断回调没必要触发。所以在我的实际项目中I2C 和 SPI 传感器都直接用轮询完全不需要配置中断。如果你真的需要“总线上有数据时立刻处理”这种从机模式的唤醒机制那 MicroPython 就力不从心了必须自己写 C 固件。5.3 DMA 中断为什么没有开放以及有没有绕路RP2040 的 DMA 控制器有丰富的完成中断但 MicroPython 固件并没有向 Python 层暴露 DMA 相关的类或方法。网上有人通过ctypes直接操作寄存器理论上可以实现 DMA 中断但这样做既复杂又危险不利于维护。如果你的项目确实需要高速、不占用 CPU 的数据搬运比如连续 ADC 采集、高速 UART 收发我建议直接转向 C 语言固件比如用 Pico SDK 编写自定义 C 模块再在 MicroPython 中调用。这才是正路。6. 不能用EXTI、PWM 中断、USB 中断等几个容易踩的坑6.1 为什么没有 STM32 风格的 EXTI 通道玩过 STM32 的人都知道 EXTI 支持 16 条外部中断线可以单独配置每个 GPIO 口。Pico 上并不存在这个“复用”概念因为它的每个 GPIO 都独立连接到 NVIC 的同一个中断源——IO_IRQ_BANK0。MicroPython 将这一机制封装成了Pin.irq所以你并不需要像 STM32 那样去映射 EXTI 线。换句话说“不能用”其实是不需要这个机制功能已经通过Pin.irq完全实现了。唯一遗憾是电平触发高/低电平在部分固件上不稳定我建议老实用边沿触发。6.2 PWM 中断注定和 MicroPython 无缘在 RP2040 上PWM 外设的计数器可以产生比较匹配中断用来做精确的频率输出或者占空比动态调整。但是 MicroPython 的machine.PWM类只有duty_u16()等方法并没有注册中断回调的irq()。所以你想在 PWM 计数溢出时去执行一段 Python 代码目前标准固件做不到。不过如果你只是想输出特定频率、特定占空比的波形直接用 PWM 就够了不需要中断。如果一个任务必须依赖 PWM 中断来做比如精确脉冲计数可以考虑两个方案一是用定时器 GPIO 引脚翻转模拟二是为此专门写一个 C 扩展模块。后者在 MicroPython 的官方文档里有详细说明。6.3 USB 中断、ADC 中断等特殊外设中断的处理USB 中断MicroPython 将 USB 协议栈与 REPL/文件系统绑定你并不能注册一个usb_irq回调。如果你想在 USB 收到数据时立刻处理通常在主循环中轮询sys.stdin或usb_cdc接口的数据。ADC 中断RP2040 的 ADC 本身支持转换完成中断但 MicroPython 不暴露。处理高速 ADC 采样的通常做法是在定时器中断里调用read_u16()采样率上限大约到 1kHz 左右再高就会失真。RTC/闹钟中断machine.RTC有wakeup()的报警功能但需要配合lightsleep()使用并不像定时器那样全程跑在后台。7. 常见问题与排查技巧实录7.1 中断回调里做了“坏事”系统表现为哪些症状我把常见的错误操作和对应的症状列成一个表你可以拿来对照回调内的操作可能出现的症状print()太频繁回调丢失、串口输出乱码、抛RuntimeError创建list/dict/bytearray内存碎片导致崩溃、死机time.sleep()阻塞调度器其他中断全部延迟无限循环如while True整个固件卡死只能复位回调函数耗时超长连续触发时丢失事件定时器周期不准访问未上锁的共享变量主循环和回调同时读写数据不一致7.2 中断不触发的三大原因第一引脚模式不对。比如你希望按键下降沿触发结果没有配置上拉电阻引脚悬空电平不稳定导致中断不触发或乱触发。建议先接上拉/下拉并且用示波器或万用表确认初始电平。第二回调注册错误。Pin.irq()的handler必须是可调用对象并且不能带参数函数会自动接收 pin 对象。如果你写成btn.irq(handlerbtn_handler(), ...)就出错了。第三和外设共享了同一个引脚。Pico 很多引脚有默认功能比如 GPIO0/1 是 UART0 TX/RX如果初始化了 UART再对该引脚设置Pin.irq可能冲突。我的经验是专用功能和外设先初始化再配置中断并且尽量少复用特殊功能引脚。7.3 中断与线程_thread的配合MicroPython 支持_thread创建多个线程其中线程 0主线程负责运行 REPL 和调度。官方限制中断回调只能在主线程的 Python 虚拟机中调度执行其他线程不能注册回调。所以你想在一个独立线程里等待 GPIO 中断是做不到的。实际做法是中断回调设置全局标志主线程读取标志后分发任务再用_thread去执行耗时操作这样最稳。7.4 关于中断优先级和嵌套中断RP2040 的 NVIC 支持多级中断但 MicroPython 固件在进入 Python 回调之前往往只允许简单的调度并没有实现高优先级中断抢占低优先级 Python 回调的机制。也就是说如果你在 GPIO 回调还没执行完时来了一个 UART 中断UART 中断的 Python 回调不会立刻执行它会被放到调度队列的尾部。因此在设计系统时不要期望多个 Python 中断能严格抢占执行。如果需要严格抢占建议在 C 层编写中断服务函数只放那些必须立刻响应的逻辑。8. 最后分享几个调整中断性能的小技巧说几个我在实际项目里反复验证过的方法能明显提升你在这块板子上写中断代码的稳定性。第一个尽可能在中断里只写“置一个整数标记”然后主循环检查标记。这种方式最安全也最好调试。主循环轮询频率如果是time.sleep(0.001)响应时间基本能控制在 1ms 内远高于多数传感器和按钮的需求。第二个如果你需要同时处理多个传感器边沿但担心多个Pin.irq回调互相影响可以考虑用单一定时器中断统一采样引脚状态并自己判断边沿。代码如下from machine import Pin, Timer pins [Pin(10, Pin.IN, Pin.PULL_UP), Pin(11, Pin.IN, Pin.PULL_UP)] prev [1, 1] def poll(timer): global prev for i, p in enumerate(pins): val p.value() if prev[i] 1 and val 0: print(fPin {i} falling edge) prev[i] val tim Timer(period1, modeTimer.PERIODIC, callbackpoll)这样本质上是用 1ms 定时器替代 GPIO 中断牺牲一点实时性换来统一的处理和逻辑避免多个回调打架。第三个技巧保住你的系统时钟。如果主循环有重计算比如一个while循环要计算 50ms那么定时器回调会被延迟。我习惯在主循环的关键计算前先判断一下ticks_diff手动处理那些需要最终保证的节拍任务比如电机控制而不是完全依赖回调。第四个技巧如果 MicroPython 的性能实在不够可以混合开发。把频繁中断的部分用 Pico SDK 写成 C 模块输出为一个简单的irq_flag()函数剩下的业务逻辑用 MicroPython 写。这听起来复杂但其实官方有详细的 build 文档比你去绕各种寄存器稳定得多。树莓派 Pico 的 MicroPython 中断支持说强不强说弱也不弱。它给了你 GPIO 和定时器这两个最常用的入口足够应付 90% 的教育项目和智能家居小项目。真正的瓶颈在于外设级的 DMA、USB、PWM 比较中断不能被直接使用这也是所有高层语言固件共通的取舍。如果你只是想在 Pico 上快速验证一个想法用好共有的中断资源就够了如果你的项目硬需求超出了这些边界我的建议很直接要么换 C要么换 ESP32那边 MicroPython 开放的中断资源更多不要硬撑着在 Python 里抠寄存器否则你会被各种调度问题折磨到怀疑人生。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询