
1. 为什么我会动手写这个基于定时器的调度器玩树莓派 Pico 有一段时间了用 MicroPython 写了挺多小项目环境监测、舵机控制、OLED 屏显、按键扫描。做着做着发现一个绕不开的问题——当你要同时处理传感器轮询、显示屏刷新、舵机输出的时候单线程顺序执行是真的不够用。你可能会说Pico 是双核 RP2040 啊开_thread跑多线程不就行了我一开始也是这么想的但实际用下来发现_thread有个硬伤MicroPython 的线程间通信和锁机制相对简陋两个线程同时操作 I2C 或者 GPIO 的时候很容易出现莫名其妙的数据错乱。而且_thread的调度时机不可控做实时性要求稍微高一点的任务就很吃力。那asyncio协程呢MicroPython 1.21 之后的版本内置了asyncio语法跟 CPython 版很像用async def定义协程、await挂起等待确实能从语法层面解决同时跑多个任务的问题。但asyncio本质上还是协作式调度——某个协程如果里面写了个阻塞的while循环整个事件循环就卡死了。再加上 NewPico 版本的固件对asyncio的支持还受限于uasyncio的兼容层部分第三方库比如 OLED 驱动还是传统的同步写法混在一起用经常踩坑。说白了我在 Pico 上真正需要的是一个简单直接、基于硬件的定时器多任务调度器由底层定时器中断来驱动调度到点了就切换任务不需要协程库、不需要线程锁、写起来还跟裸机思维一致。为了把这件事彻底搞定我整理了一套自己的实现方案今天这篇文章就把它完整地拆开讲透。这篇文章适合谁看只要你用的是树莓派 Pico / Pico W想用 MicroPython 同时跑多路传感器采集、多路输出控制或者单纯想搞懂定时器中断到底怎么驱动多任务都能从这篇文章里拿到能直接落地的代码和思路。2. 任务调度器整体架构与设计思路2.1 为什么是定时器而不是延时轮询或操作系统在讲调度器代码之前我想先把这个技术选型背后的道理说清楚因为很多新手就是在这里开始懵的。最直觉的多任务方案是什么就是主循环里写time.sleep(0.01)隔 10ms 检查一次 A 传感器再过 10ms 检查一次 B 传感器。这种延时轮询方案简单归简单问题在于 sleep 期间任务被冻结什么都没做CPU 在空转而且某个耗时操作比如 OLED 刷一帧 128*64 的画面会阻塞后面的任务。你想让舵机以 50Hz 的 PWM 频率快速换向同时又想让 DHT11 温湿度传感器每 2 秒采一次数据用 sleep 轮询写出来的代码要么舵机抖动要么湿度读数卡顿。那上 RTOS实时操作系统行不行Pico 移植 FreeRTOS 确实可行但在 MicroPython 生态里玩 RTOS 就很尴尬了——MicroPython 的接口本身就做得比较高层你跟 FreeRTOS 的任务句柄、信号量打交道等于抛弃掉 MicroPython 的便利性代码维护成本直接翻倍对很多硬件项目来说性价比不高。再说asyncio和_thread我在开头已经说了它们的局限一个怕阻塞、一个怕竞争。所以我的选择是用 Pico RP2040 内部硬件定时器产生周期中断在中断服务函数ISR里给任务打到点标志主循环根据标志去执行对应任务。这套思想在嵌入式里叫时间片轮询调度 中断驱动比裸奔延时强得多比上 RTOS 轻得多还跟 MicroPython 的代码风格完美兼容。2.2 调度器的核心组成模块我把这个任务调度器拆成了 4 个模块各司其职模块职责我选择的设计时间基准产生稳定、周期性的触发信号使用 RP2040 内部 Timer 的PERIODIC模式每 1ms 触发一次中断任务注册表存储所有待调度任务的信息用 Python 列表存储任务类实例每个实例包含回调函数、周期、状态等调度核心决定什么时刻该执行哪个任务在定时器 ISR 中递增 tick 计数并检查任务的到期状态设置对应任务的就绪标志任务执行体实际的业务代码用户自定义的回调函数在主循环里被调用在这个设计里中断函数只负责打标记不直接执行任务这是个红线级别的重要设计原则。原因后面花一大节讲这里先提一嘴。3. 关键核心定时器中断的初始化与配置详解3.1 RP2040 的定时器资源与 MicroPython 中的 Timer 类树莓派 Pico 搭载的 RP2040 芯片内部有 4 个通用定时器编号 0~3它们的基础时钟可以映射到不同的参考时钟源。不过在 MicroPython 的machine.Timer抽象层里我们不需要关心这么底层的映射关系——MicroPython 官方固件已经帮你把定时器接口封装好了。我这里贴一段最基础的定时器初始化代码from machine import Timer, Pin # 创建定时器对象选择定时器编号Pico上建议用 0 或 12/3 留给其他扩展功能后面有说明 timer Timer(0) # 以周期模式启动定时器周期 1ms timer.init(period1, modeTimer.PERIODIC, callbacktimer_callback)这段代码在 Pico 上非常常用但有几个细节值得展开。period单位是毫秒period1代表 1ms 产生一次中断。RP2040 的定时器精度在 MicroPython 封装下实际能做到微秒级但因为我们做的调度器是基于毫秒 tick 的就先用 1ms 作为时基。mode参数有两种Timer.ONE_SHOT单次触发和Timer.PERIODIC周期触发。做调度器当然用PERIODIC。callback传进去的是中断回调函数名。注意是函数引用不是函数调用也就是不能写callbacktimer_callback()这个括号一旦加上去了就相当于把返回值传给 callback程序跑起来你的回调函数根本不会被调用。3.2 定时器中断回调函数里能做什么不能做什么这是整个实现里最重要的知识点也是新手最容易犯错的地方。我直接用一句话概括中断回调函数里绝不允许放耗时操作和阻塞操作。为什么因为 MicroPython 的中断回调是在底层 C 代码的 IRQ 上下文里执行的你回调执行多久就相当于把整个处理器焊死在那里多久。期间其他所有中断包括 Pico 的 USB 通信中断都被挂起了表现出来就是你在回调里写个print()或者time.sleep_ms(10)主程序运行会明显卡顿甚至 USB REPL 都变得不响应。更隐蔽的问题是在中断回调里做内存分配。MicroPython 有一个著名的MemoryError问题——在中断上下文里垃圾回收器GC处于锁定状态malloc不被允许。如果你的回调里写了列表拼接、字符串格式化、甚至某些库内部会动态创建 Python 对象系统可能直接崩掉报的错可能是荒谬的MemoryError或Fatal error。我第一次做定时器调度时就栽在这里。我本来想让中断每 10ms 直接执行一次显示回调回调里调用了ssd1306的text()方法这个方法内部会创建字符串对象结果就是Pico 偶尔无规律死机Reset 之后又恢复了。后来查文档、调试很久才确认是中断分配内存导致的。所以我定下一条铁律中断 ISR 里只做三件事递增全局 tick 计数、比对任务表里的到期时间、设置就绪标志位。加起来就几条整型运算和列表遍历全部在中断里完成运行时间不超过几微秒。3.3 定时器初始化完整代码与应用示例直接上一段可以跑的基础定时器代码from machine import Timer import time # 全局 tick 计数器表示系统运行以来的毫秒数 tick_ms 0 def timer_callback(timer): global tick_ms tick_ms 1 timer Timer(0) timer.init(period1, modeTimer.PERIODIC, callbacktimer_callback) while True: print(tick , tick_ms) time.sleep_ms(100)这个代码每隔 100ms 打印一次当前的系统 tick你能看到 tick_ms 是稳定、单调递增的。它就是整个调度器的心跳。如果你发现 tick 突然跳变或者打印间隔不均匀先检查是不是你的主循环里有print操作因为 USB 通信本身也会占用 CPU 时间会让主循环变慢——但它不会中断定时器所以定时器 tick 依然是准确的。这也是定时器驱动调度比延时轮询更可靠的根本原因。4. 手写一个轻量级任务调度器从数据结构到调度算法4.1 任务表数据结构设计细节有了定时器这个心跳下一步就是把调度逻辑做出来。我先定义一张任务表用来记录每个任务的周期、回调函数、状态等元信息。我直接用一个 Python 类来管理任务项这样可读性最好class Task: def __init__(self, name, func, period_ms, enabledTrue): self.name name # 任务名称方便调试 self.func func # 任务执行函数不带参数 self.period_ms period_ms # 周期单位毫秒 self.next_run_ms 0 # 下次应该运行的时间点基于 tick self.enabled enabled # 是否启用 self.last_run_ms 0 # 上次实际运行的时间用于统计这里的关键字段是next_run_ms它决定了调度器怎么判断任务是否到期。我用的是绝对时间片比对法每次中断 tick 增加后拿当前 tick 跟每个任务的next_run_ms比较如果当前 tick next_run_ms说明这个任务该执行了就把它的就绪标记置为 True。为什么不用递减计数器而用绝对时间戳因为递减计数器每 tick 都要做一次减法多个任务时还要防止计数器值因为某次主循环耗时过长、中断累加导致溢出。绝对时间戳只要保证 int 不溢出即可MicroPython 的 int 是任意精度的完全不用担心。4.2 调度核心中断里只做标记主循环里执行任务下面是调度器的核心代码。我已经把中断设置标志 主循环执行任务这个模式拆成了一个完整的类class TaskScheduler: def __init__(self, time_base_ms1): self.time_base_ms time_base_ms self.tasks [] self.tick_ms 0 self._timer Timer(0) self._timer.init(periodtime_base_ms, modeTimer.PERIODIC, callbackself._on_tick) def _on_tick(self, timer): # 中断上下文只更新 tick 和就绪标志绝对不能做耗时操作 self.tick_ms self.time_base_ms for task in self.tasks: if task.enabled and self.tick_ms task.next_run_ms: task.next_run_ms self.tick_ms task.period_ms task.due True def add_task(self, name, func, period_ms, enabledTrue): task Task(name, func, period_ms, enabled) task.next_run_ms self.tick_ms period_ms # 错开起始时间避免所有任务扎堆执行 self.tasks.append(task) return task def run(self): # 主循环检查就绪标志并执行任务 while True: for task in self.tasks: if task.due: task.due False task.func() # 其他空闲时间可以干点别的事但不要阻塞太久写到这里你可能已经感受到了这种设计的好处主循环几乎永远是轻的那些task.func()执行再多耗时操作比如 OLED 刷屏、传感器延时采集都不会影响定时器 tick 的准确性最多是某个任务在这个周期里执行晚了但下一个周期的对时依然准确。add_task里把第一次执行时间next_run_ms设置为当前 tick 周期这样多个任务就不会在同一时刻一起执行避免了瞬间的 CPU 峰值还能让所有任务自动错峰。4.3 调度器的完整样例让两个 LED 以不同频率闪烁纸面讲再多不如一个能跑的例子。我写一个非常直观的验证程序板载的 GP16、GP17 两个 LED一个以 500ms 周期闪烁一个以 200ms 周期闪烁。运行后你会看到两个 LED 各自按节奏闪烁互不干扰from machine import Pin from scheduler import TaskScheduler # 上面那个类的文件 led_a Pin(16, Pin.OUT) led_b Pin(17, Pin.OUT) def blink_a(): led_a.toggle() def blink_b(): led_b.toggle() sched TaskScheduler() sched.add_task(LED_A_500ms, blink_a, 500) sched.add_task(LED_B_200ms, blink_b, 200) sched.run()实测效果非常好。500ms 任务的抖动范围基本控制在 1~3ms 以内这已经能满足绝大多数非严格实时场景的需求了。5. 调度器的高阶玩法给任务设置优先级和启用/暂停控制基础版的调度器跑通之后你可能会发现几个新的需求某个任务我暂时不想运行能不能随时停掉某个任务特别重要能不能优先执行我在实际项目中总结了一套控制接口非常实用。核心思想是在任务表里增加优先级和状态控制。5.1 优先级调度让关键任务插队因为 MicroPython 没有内置的优先级抢占调度我之前的设计本质上是时间片轮询Round-Robin——所有任务到期后按注册顺序轮流执行。这对大多数场景都够了但它有个问题如果 A 任务耗时 20ms、B 任务耗时 10ms两个任务同时到期B 任务可能因为排在 A 后面而被拖延。解决办法有两个思路我推荐的是给任务加优先级队列调度器的 run() 循环里不是因为到期就立刻执行而是把到期的任务放进一个待执行队列然后按优先级从高到低执行。def run(self): while True: # 从所有任务里挑出 due 且启用的按优先级排序执行 ready [t for t in self.tasks if t.due and t.enabled] if ready: ready.sort(keylambda t: t.priority, reverseTrue) # 数值越大优先级越高 for task in ready: task.due False task.func()注意这个 list 推导式是在主循环里做的不是中断里做的所以可以放心地创建临时列表。中断里我只做了task.due True的标记没有分配内存安全。这种到期标记 主循环排序的方案在嵌入式里叫非抢占式固定优先级调度比时间片轮询聪明之处在于它保证关键任务比如舵机换向永远优先执行而耗时任务比如 OLED 显示只要不拖太久就不会影响关键任务。5.2 动态启用/暂停随时改变任务运行状态另一个很实用的能力是暂停和恢复任务。比如你摁了一下按钮进入低功耗模式希望 DHT11 的采集任务暂停过几秒又需要它恢复。在 Task 类里增加两个方法def enable(self): self.enabled True self.next_run_ms 0 # 重新启用时立即触发一次 def disable(self): self.enabled False self.due False然后在调度器里加两个接口def pause_task(self, name): for t in self.tasks: if t.name name: t.disable() def resume_task(self, name): for t in self.tasks: if t.name name: t.enable()注意disable()里把due也清掉了否则任务被禁用的那一刻如果due已经是 True主循环可能在下一轮仍然执行它这就违背了暂停的语义。这种细节属于经验坑写代码的时候稍微想一想能省后面排查的好多时间。5.3 任务超时统计与 CPU 占用率估算还有一个高级技巧是我做数据采集项目时研究出来的用调度器统计每个任务的实际执行时长以及整体 CPU 占用率。思路非常简单我在Task里记录任务开始执行和结束执行的 tick 时间戳class Task: def __init__(self, ...): ... self.total_calls 0 self.total_exec_ticks 0 self.last_exec_ticks 0 # 在 run() 中包裹调用 def run(self): last_time self.tick_ms while True: now self.tick_ms idle_ms now - last_time ... for task in ready: start self.tick_ms task.func() task.total_exec_ticks self.tick_ms - start task.total_calls 1这样你就可以算出每个任务平均执行耗时还能通过统计主循环空闲 tick 数估算 CPU 占用率。比如你发现某个任务平均耗时 15ms而它的执行周期是 20ms那它已经占了 75% 的 CPU 时间你就该考虑优化逻辑或者把它拆成子任务了。6. 定时器与多任务结合的进阶实战舵机控制与多个输入并行6.1 实战场景描述前面讲了一大堆原理和封装现在来一个更贴近实际硬件的综合案例。这是我最近做的一个小型机械臂模拟项目Pico 同时控制两个 MG90S 舵机一个负责臂关节一个负责抓手另外还需要用一个按键切换抓手开合状态OLED 屏幕实时显示当前角度和状态。这个项目天然就是一个多任务场景任务 1每 500ms 读取按键输入任务 2每 20ms 更新舵机 PWM 脉宽因为舵机对更新频率有要求低于 40Hz 会出现抖动任务 3每 200ms 刷新 OLED 显示如果用裸循环你会发现 OLED 刷新的时候按键读取会被阻塞按键灵敏度变差舵机更新也可能被 OLED 占用 CPU 的几十毫秒影响导致舵机抖动。有了调度器事情就清晰多了。6.2 舵机 PWM 的底层细节与定时器任务配合MG90S 舵机需要的 PWM 信号是 50Hz周期 20ms脉宽 0.5ms 到 2.5ms对应 0 度到 180 度。在 Pico 上用 MicroPython 的 PWM 可以直接生成from machine import PWM, Pin servo_pwm PWM(Pin(15)) servo_pwm.freq(50) # 50Hz # 计算 1.5ms 脉宽对应的占空比 duty int(1.5 / 20 * 65535) servo_pwm.duty_u16(duty)这里的 65535 是因为duty_u16用的是 16 位无符号整型占空比 0~65535 对应 0~100%。这个转换公式很有用建议直接记下来。那舵机更新任务怎么跟调度器20ms 周期融合实际上舵机 PWM 信号是由machine.PWM模块的硬件产生的你只需要在任务里偶尔更新一次脉宽寄存器PWM 信号本身是独立的硬件输出不占 CPU。所以舵机的20ms 任务其实只需要每隔 20ms 把目标角度转换成脉宽值写进寄存器即可任务本身极其轻量。这也是为什么我用 PWM 硬件而不是自己用定时器软件生成 50Hz 方波——后者会无谓消耗 CPU。能用硬件外设解决的事情就不要用 CPU 软件去模拟这是嵌入式开发的重要原则也直接影响了调度器的负载。6.3 多任务协同的完整示例代码这是舵机控制与按键、显示任务的整合我把关键点都写清楚了from machine import Pin, PWM, I2C import ssd1306 from scheduler import TaskScheduler # 硬件初始化 servo PWM(Pin(15)) servo.freq(50) btn Pin(14, Pin.IN, Pin.PULL_UP) i2c I2C(0, sclPin(5), sdaPin(4), freq400000) oled ssd1306.SSD1306_I2C(128, 64, i2c) # 全局状态 current_angle 90 gripper_open False def angle_to_duty(angle): # 0度0.5ms脉宽180度2.5ms脉宽20ms周期 # 映射到 0~65535 的16位占空比 pulse_ms 0.5 angle / 180.0 * 2.0 return int(pulse_ms / 20.0 * 65535) def update_servo_job(): # 每20ms执行一次把目标角度写入党硬件负责实际波形 servo.duty_u16(angle_to_duty(current_angle)) def read_key_job(): # 每100ms扫一次按键做下降沿检测 global gripper_open, current_angle if btn.value() 0: # 简易去抖跳过前10ms time.sleep_ms(10) if btn.value() 0: gripper_open not gripper_open current_angle 180 if gripper_open else 90 while btn.value() 0: pass # 等待松开实际也要加超时保护 def display_job(): oled.fill(0) oled.text(ANGLE: {}.format(current_angle), 0, 10) oled.text(GRIP: {}.format(OPEN if gripper_open else CLOSE), 0, 30) oled.show() sched TaskScheduler() sched.add_task(servo, update_servo_job, 20, priority10) sched.add_task(key, read_key_job, 100, priority5) sched.add_task(display, display_job, 200, priority2) sched.run()这个例子演示了两个关键工程实践第一给不同任务分配不同优先级。舵机任务优先级最高10防止被显示任务拖后腿显示任务优先级最低2晚几十毫秒刷新毫不影响体验。第二任务函数内部可以做阻塞操作但要控制阻塞时间。比如按键去抖里的time.sleep_ms(10)放在 100ms 周期任务里是能接受的。但千万不要在中断回调里这么干这是前面反复强调的纪律。7. 常见问题的排查技巧与方法实操记录再好的代码也可能磕磕绊绊。这一节完全来自我这几个月的真实踩坑经历每条都对应一个真实问题我按现象 - 原因 - 解决 - 心得的结构来写。7.1 任务突然不再执行但板子没死机现象定时器还在跑主循环没卡死但某个任务就是再也不执行了。排查打印这个任务的next_run_ms和当前tick_ms发现next_run_ms已经落后当前 tick 好几秒了。这是因为任务回调函数内部执行时间超过了任务周期。比如一个任务周期是 50ms回调逻辑本身耗时 200ms那么下一次它到期时回调还没执行完等到due被标记后被主循环检查到回调又重新执行一遍——看起来是漏执行但更糟的是如果任务回调里用了非可重入操作久了就会出错。解决把执行周期加长或者把回调里的耗时操作拆分成更小的子步骤。同时我给调度器加了一个防积压机制在中断里标记due时如果发现当前 tick 已经远超任务的next_run_ms就不要再往后推了而是直接跳到下一个周期避免单个任务霸占系统。if task.enabled and self.tick_ms task.next_run_ms: # 如果积压过多只补偿一次周期不追赶 task.next_run_ms max(self.tick_ms task.period_ms, task.next_run_ms task.period_ms) task.due True7.2 舵机控制出现随机抖动现象舵机应该停在某个角度但每隔几秒会咔一下抖动再复位。排查用示波器或者 Pico 的第二块板子抓 PWM 波形检查发现正常情况下 PWM 脉宽很稳定抖动的时候脉宽突然跳变然后又变回来。追下去发现是因为我在某个周期任务里无意中修改了舵机current_angle的值而舵机更新任务和修改任务之间没有任何互斥。解决MicroPython 没有原生的互斥锁虽然可以用_thread里的 Lock 但调度器主循环不支持所以我的办法是凡是会被多个任务共享的变量统一放到一个地方修改。把current_angle的修改集中到read_key_job里舵机任务只读不改就杜绝了竞争。7.3 中断回调里调用 print 导致 USB 串口乱码现象用 Thonny 跑代码从定时器回调里打印 tick串口输出偶尔乱码甚至固件崩溃重启。原因MicroPython 的 print 在 USB 虚拟串口里会执行较长时间的 IO 操作而且需要申请缓冲区。在中断上下文里这就非常有风险。解决主循环里跑一个debug print 任务周期性地把需要打印的状态信息输出中断回调里就死都不碰 print。这也帮助我形成了一套习惯中断回调只写全局状态主循环负责消费和输出。7.4 多个定时器 id 选择不当现象代码里用了两个定时器一个做调度一个做单次延时跑起来后其中一个失效了。原因在我用的固件版本里Timer(2)和Timer(3)跟系统内部资源有冲突比如被用于 REPL 的底层任务或固件内部调度。查了 Pico 官方文档也看了 MicroPython 源码发现固件对 Timer id 的支持在不同版本略有差异。解决我最终只用Timer(0)做调度时间基准其他延时功能尽量用任务调度器本身解决或者用machine.time_pulse_us这类工具。如果你确实需要第 2 个定时器建议在Timer(1)上测试确认没问题再投入项目。7.5 MicroPython 定时器周期最小能达到多少现象我把调度器的time_base_ms设置为 0.1ms即 100 微秒结果 Pico 直接卡死。估算RP2040 官方保证的最小定时器中断周期实际上可以到微秒级10µs 也能跑但 MicroPython 的开销比 C 语言大得多。一次定时器中断从进入回调到退出MicroPython 解释器要执行 Python 字节码遍历任务列表、比较整型、置布尔标志。以我的经验在 MicroPython 里把中断周期设置到低于 500 微秒CPU 就基本被 IRQ 吃满了主循环几乎没时间跑任务。建议调度器的time_base_ms设置在 1~5ms 之间最合适。对绝大多数传感器DHT11、BME280和显示刷新需求60fps 的 OLED 是 16ms 一帧完全够用。如果你的项目要求微秒级精度那应该用 PIO、PWM 硬件或者直接切到 C/C SDK 去写。7.6 常见问题速查表现象可能原因解决方案任务漏执行回调耗时超过任务周期加大周期或拆分子步骤加防积压判断舵机随机抖动多任务共享变量无互斥集中修改共享状态或加锁USB 串口乱码/死机中断回调里 print 或申请内存中断只打标志主循环负责输出Timer(2)/(3) 不工作固件底层占用只用 Timer(0)/(1)或更新固件调度器 time_base 太短卡死MicroPython 中断开销大周期设为 1~5ms高精度需求用 C 或硬件8. 如果你需要更强实时性从调度器到双核与硬件定时器方案我的这套调度器方案适合绝大多数 MicroPython 的通用场景但如果你真的需要更高实时性比如做一个飞行器飞控、高速信号采集MicroPython 本身以及这套调度器可能都不够。这时候有几个升级路线我简单说一下方便你评估自己的项目边界。8.1 使用 Pico 双核把实时任务放到第二个核RP2040 是双核 Cortex-M0MicroPython 支持_thread但这不是我推荐的主要方案——虽然可以做到核间任务分配核0跑调度器主循环负责显示、按键等慢速任务核1跑一个高优先级 while 循环负责舵机控制和传感器读取。但这里的关键难点是核间通信。我实测过用_thread共享全局变量在大量读写时会出现数据不同步问题需要用 lock 来保护而 lock 本身又有性能开销。所以除非你非常需要双核并行否则对大部分项目单核调度器就够了。8.2 使用 PIO 或 DMA 卸载高频任务Pico 的 PIO可编程 IO模块可以用来生成高频 PWM、读取高速编码器完全不需要 CPU 参与。如果你有 10kHz 以上的高速输出需求建议把 PWM 的生成逻辑从调度器里剥离用 PIO 来实现。MicroPython 对 PIO 的支持还可以但毕竟 PIO 编程是一门独立的学问。8.3 终极选择切换到 C/C SDK如果发现调度器在 MicroPython 下无法满足实时要求比如需要 1kHz 的 PID 控制环那最稳妥的方案是切换到 C/C SDK。SDK 下面你可以直接用硬件定时器中断回调逻辑用 C 语言写开销小到可以忽略实时性会提升好几个数量级。但这就失去了 MicroPython 的开发效率属于两害相权取其轻。9. 基于调度器扩展出来的几个实用小项目最后分享几个我拿这套调度器做过的小项目都是能很快出成果的。它们模式几乎一样一个或者多个周期任务 一个显示/输出接口但背后的能力完全不同能帮你建立更强的信心。9.1 四通道温湿度记录仪我在 Pico 上接了 4 个 DHT11 传感器因为 DHT11 的采集时序要求比较苛刻起始信号 40bit 数据如果我只跑一个采集函数要串行等很久。用调度器把 4 个传感器的采集设成 4 个独立任务周期都是 2s但因为add_task时起始时间错开了 500ms所以 4 个传感器分时工作互不干扰每 2 秒就能记录一轮完整数据。在 OLED 上直接滚动显示温度曲线实测非常稳定。9.2 呼吸灯 按键调节亮度呼吸灯本质是周期性改变 PWM 占空比这个频率很快时用 PWM 自动更新其实不需要任务调度器但组合了读取电位器、显示状态、按键调模式之后一个调度器就能把逻辑统一起来。我用的呼吸周期是 2s占空比在 0~65535 之间三角波递增递减21ms 更新一次任务函数就是很干净的计算 写入完全不卡。9.3 串口 GPS LED 状态灯GPS 模块通常以 1Hz 频率输出 NMEA 句子我用调度器设置了一个 1s 周期的解析任务同时用一个 250ms 周期任务让 LED 闪烁表示系统活着另一个 5s 周期任务检查 GPS 信号质量如果信号弱就切换 LED 闪烁模式。这种多节律并行的代码用裸循环写会很头痛用调度器就像拼积木一样清楚。这三个项目都印证了一件事任务调度器的本质是时间维度上的模块化它让每个功能模块只管自己的周期逻辑不用关心其他模块的运行节奏这种解耦不仅让代码更好写也让项目更好扩展——想加一个任务注册进去就完了不动别的代码。我在实际使用中还有一个偷懒技巧凡是周期型的业务逻辑我都往调度器里注册凡是按键触发的一次性动作我单独写一个事件标志。这样系统里只有两种事件驱动方式思维负担小很多。这个习惯建议大家也尽快建立。