
1. 内容整体设计与思路拆解1.1 为什么单片机也需要“任务调度器”先聊个挺常见的场景你在树莓派 Pico 上用 MicroPython 写程序今天想让 LED 每秒闪一下明天想加个按键扫描后天又想驱动舵机转个角度。一开始每个功能单独写还能应付但一旦放一起跑问题就来了——while True里塞了一堆轮询代码某个传感器读取稍微慢一点LED 闪烁的节奏就乱了按键响应也跟着迟钝。这就是典型的裸机编程痛点没有分时机制多个任务互相抢 CPU。很多人第一反应是上 RTOS实时操作系统但 Pico 跑 MicroPython 本身资源就有限引入 FreeRTOS 类的调度器太重而且用 MicroPython 写 RTOS 应用也别扭。更实际的做法是利用芯片自带的硬件定时器做一个轻量级任务调度器让不同任务按自己的时间片轮流执行。这个思路其实不新鲜。STM32、ESP32 上的定时器中断做任务切片、用systick做系统心跳都是嵌入式开发里的老手艺。Pico 用的 RP2040 芯片内部也有完善的定时器资源MicroPython 固件把它们封装成了machine.Timer模块。我们要做的就是把这套定时器机制好好利用起来。1.2 这套方案能解决什么问题用定时器做任务调度核心理念就一句话把“主动轮询”变成“定时通知”。具体来说有三个明显好处多任务并行不互相干扰。LED 控制、按键扫描、舵机驱动、传感器读取各分配一个时间片谁也不会堵住谁。实时响应性有保障。哪怕主循环里在处理耗时逻辑定时器中断照样能准时触发让关键任务优先执行。代码组织更清晰。每个任务是独立的回调函数逻辑解耦后期加功能、删功能都很方便。适合谁来参考如果你用 Pico 做智能小车、桌面小机械臂、环境监测站这类项目代码开始变得“一团乱麻”或者你想搞懂单片机多任务的基本原理这篇文章就是给你准备的。我会从最基础的定时器 API 讲起一步步搭出一个可复用的调度器模块全程用 MicroPython 代码演示。2. 核心细节解析与实操要点2.1 Pico 的定时器硬件到底怎么回事在动手写代码之前得先搞清楚 Pico 上定时器的工作方式。RP2040 芯片内部有 8 个Timer实例在 MicroPython 里你可以通过machine.Timer()任意创建它们底层映射到不同的硬件定时器通道。这里有个关键点MicroPython 的 Timer 不是直接映射到某个固定外设而是由固件动态分配。你写timer machine.Timer()的时候固件会自动给你分配一个空闲的定时器通道。对于大多数应用场景不需要也不应该去试图指定具体通道内部资源管理远比我们手动干预更可靠。定时器的周期单位是微秒μs但实际精度受限于 12MHz 的参考时钟。MicroPython 内部会把这个值换算成计数周期误差通常在微秒级别。如果你做的是高精度 PWM 输出比如舵机控制这点误差基本无感但如果要做高精度频率测量那就得考虑别的手段了。我用一张表说明一下定时器的基本配置参数参数含义典型值mode工作模式Pico 上常用Timer.PERIODIC周期触发或Timer.ONE_SHOT单次触发Timer.PERIODICperiod触发周期单位微秒5000表示 5mscallback触发时的回调函数函数签名必须是callback(t)t是定时器对象def tick(t): ...freq频率模式单位 Hz与 period 二选一不常用2.2 一个定时器回调函数就够了天真了明确一个容易踩坑的点定时器回调函数不是越多越好而是越多越容易出问题。machine.Timer的回调本质上是硬件中断处理程序ISR。虽然 MicroPython 帮我们屏蔽了很多底层细节但中断处理是有代价的——进入和退出中断有上下文切换开销回调函数执行期间其他定时器中断可能被延迟。极端情况下多个定时器同时触发中断嵌套会导致优先级混乱程序表现“时好时坏”。所以设计任务调度器的第一原则定时器回调函数要短小精悍。我见过不少新手直接把耗时操作比如 I2C 读传感器丢进回调里结果 I2C 通信还没完成下一个定时器中断就来了数据错乱甚至死机。正确的姿势是让定时器只负责发一个“该干活了”的信号真正的任务逻辑放到主循环里处理。这就是所谓“前后台系统”的思想——定时器中断是前台主循环是后台。我在第三节会详细展开这种实现方式。2.3 真正需要的核心 API 和它的初始化流程machine.Timer模块本身很简单核心也就 4 个操作from machine import Timer # 创建定时器实例此时还没有启动 timer Timer() # 初始化定时器配置为周期模式5ms 触发一次回调 timer.init(modeTimer.PERIODIC, period5000, callbacktick_callback) # 停止定时器 timer.deinit() # 重新配置 timer.init(modeTimer.PERIODIC, period10000, callbackanother_callback)这套 API 很直观但有两个容易忽略的细节细节一deinit之后要重新init而不是直接改参数。有些固件版本支持直接改但保险做法永远是先deinit再init避免底层状态机出错。细节二回调函数必须能接收一个参数就是定时器对象本身。如果你写def tick():而不接收参数MicroPython 会直接给你报 TypeError。另外我需要提醒一下machine.Timer是 MicroPython 通用 API不同开发板支持程度不同。比如 ESP32 上你可能要面对Timer(0)或Timer(1)的选择而 Pico 上直接Timer()就行这是 RP2040 芯片和 ESP32 内部资源数量不同导致的。如果你在其他板子上复现代码建议先查一下该平台的 Timer 文档不要盲目照搬。3. 实操过程与核心环节实现3.1 第一步先实现一个“不阻塞”的闪烁 LED先做一个最基础的小实验验证整体流程。假设我们要让板载 LED 以 1Hz 频率闪烁同时主循环里还要做点别的事情比如打印计数器。from machine import Pin, Timer import time led Pin(25, Pin.OUT) counter 0 def led_tick(t): global counter led.toggle() counter 1 print(LED tick, counter) timer Timer() timer.init(modeTimer.PERIODIC, period500, callbackled_tick) while True: # 主循环里可以做点不紧急的事情 # 这里故意死等一下模拟耗时操作 for _ in range(10000): pass # 注意这段死等不会阻塞 LED 闪烁运行这段代码你会看到 LED 稳定闪烁主循环里的“耗时操作”并没有影响闪烁频率。这就是定时器中断跑在“另一个线程”上的效果。这里有个很重要的实验结论print放在回调里其实不太合适因为打印会占用串口输出耗时较长。如果多个定时器都在回调里print中断处理时间会显著拉长定时精度会受到影响。建议实际项目中把打印挪到主循环调试信息由队列暂存。我给你演示一个改进方案。3.2 第二步用软件标志位实现完整的“轮询通知”模型单纯用 LED 开关验证太小儿科了我们来看实际应用场景假设我们要调度 3 个任务任务 A每 10ms 读一次 ADC电位器电压值任务 B每 50ms 控制舵机到目标角度任务 C每 200ms 在 OLED 上刷新一次数据。如果直接在while True里用time.sleep_ms()轮询任务 C 刷屏的时候任务 A 可能已经错过了该采样的时间点。用定时器中断就能解决。from machine import Pin, Timer, ADC import time # 任务标志位 flag_task_a False flag_task_b False flag_task_c False # 定时器回调函数只置标志位不执行任务本身 def timer_tick(t): global flag_task_a, flag_task_b, flag_task_c # 用计数器实现不同任务的定时周期 tick_count t.counter() if tick_count % 10 0: # 假设基准定时器是 1ms flag_task_a True if tick_count % 50 0: flag_task_b True if tick_count % 200 0: flag_task_c True # 1ms 定时器作为系统心跳 sys_timer Timer() sys_timer.init(modeTimer.PERIODIC, period1, callbacktimer_tick) def task_a(): # 读取 ADC执行快速采样任务 adc_val adc.read_u16() # 存储或处理 adc_val pass def task_b(): # 更新舵机角度执行中等周期任务 pass def task_c(): # 刷新 OLED 显示执行慢速任务 pass while True: if flag_task_a: flag_task_a False task_a() if flag_task_b: flag_task_b False task_b() if flag_task_c: flag_task_c False task_c() # 其他非关键任务这个方法好理解但在 MicroPython 下有个隐患——创建一个 1ms 周期的定时器回调实际上会非常频繁地进入中断。实测下来RP2040 上 MicroPython 处理 1ms 级软定时器中断已经接近固件性能极限如果再叠加其他处理容易丢失心跳。我的经验是基准心跳频率设置在 5ms 或 10ms 即可。像 10ms 的心跳能满足绝大多数任务需求100Hz 的采样率对 ADC 读电位器、按键扫描这类任务完全够用而且中断频率低了 10 倍整体稳定性显著提升。这个取舍在实验中反复验证过太激进的心跳周期是系统不稳定的常见原因之一。3.3 第三步真正的任务调度器架构节点链表版前面的标志位方案只能处理固定数量的任务而且周期要写死在回调函数里加任务就得改回调逻辑。我们来升级一下做一个基于节点链表的常驻调度器注册任务和注销任务都变成 API 调用想加几个任务加几个周期随时改。重点看一下调度器的核心结构。我用一个列表来存储所有注册的任务每个任务是一个字典或者你用类也可以from machine import Timer class SimpleScheduler: def __init__(self, tick_ms5): tick_ms 是心跳周期默认 5ms self.tick_ms tick_ms self.tasks [] # 任务列表 [(period, current, callback)] self.timer Timer() self.timer.init(modeTimer.PERIODIC, periodself.tick_ms * 1000, # MicroPython 用微秒 callbackself._tick) def _tick(self, t): 定时器中断回调只做计数检查不执行任务逻辑 for task in self.tasks: task[1] self.tick_ms if task[1] task[0]: task[1] 0 # 注意这里不调用 callback只记录待执行 task[2] True def register(self, period_ms, callback): 注册一个周期任务period_ms 为任务周期 if period_ms % self.tick_ms ! 0: raise ValueError(period_ms 必须是 tick_ms 的整数倍) self.tasks.append([period_ms, 0, callback, False]) def run_main_loop(self): 主循环执行到期的任务 while True: for task in self.tasks: if task[3]: task[3] False task[2]()这个版本还有个小问题回调里直接调用了任务函数而任务函数本身耗时不可控。如果一个任务函数执行时间超过 heartbeat 周期下一次中断来了它还没干完就会出麻烦。我后来在实践中把“待执行标志位”机制加了回去。完整版如下from machine import Timer class SimpleScheduler: def __init__(self, tick_ms5): self.tick_ms tick_ms self.tasks [] self.timer Timer() self.timer.init(modeTimer.PERIODIC, periodself.tick_ms * 1000, callbackself._tick) def _tick(self, t): for task in self.tasks: task[counter] self.tick_ms if task[counter] task[period]: task[counter] 0 task[pending] True def register(self, period_ms, callback): if period_ms % self.tick_ms ! 0: raise ValueError(period_ms 必须是 tick_ms 的整数倍) self.tasks.append({ period: period_ms, counter: 0, callback: callback, pending: False }) def run_main_loop(self): while True: for task in self.tasks: if task[pending]: task[pending] False task[callback]()注意看这里的关键改动回调_tick里只做累加计数和置标志位不执行任何任务函数真正执行任务函数是在主循环里完成的。这样即使某个任务函数执行时间比较长比如 LCD 刷新花了几十毫秒定时器中断也不会被阻塞其他任务只是延迟执行不会丢失。用法很简单sched SimpleScheduler(tick_ms5) def led_task(): led.toggle() def read_adc_task(): adc_val adc.read_u16() # 处理数据 sched.register(500, led_task) # LED 每 500ms 翻转一次 sched.register(20, read_adc_task) # ADC 每 20ms 采样一次 sched.run_main_loop()3.4 第四步在项目中实际接线与运行舵机示例这套调度器在真实项目里怎么用以树莓派 Pico 控制舵机为例演示假设我们有一个 SG90 舵机要让它每隔 500ms 在 0° 和 90° 之间来回转动。硬件连接很简单舵机信号线通常橙色接 Pico 的 GP15舵机电源线红色接 5V外部供电更稳妥舵机地线棕色/黑色接 Pico 的 GND软件主程序from machine import Pin, PWM, Timer import math # 舵机 PWM 初始化 servo PWM(Pin(15)) servo.freq(50) # 50Hz即 20ms 周期这是舵机标准频率 def set_servo_angle(angle): 角度转占空比0° 对应 0.5ms180° 对应 2.5ms duty int(65535 * (0.025 angle / 180 * 0.1)) servo.duty_u16(duty) flag False counter 0 def servo_task(): global flag, counter flag not flag if flag: set_servo_angle(0) else: set_servo_angle(90) counter 1 print(Servo cycle:, counter) # 使用调度器 sched SimpleScheduler(tick_ms5) sched.register(500, servo_task) sched.run_main_loop()舵机的 0° 到 180° 对应的脉冲宽度计算实际上取决于具体舵机型号。SG90 比较标准0.5ms~2.5msPWM 周期 20ms。换算成占空比就是 0.5/20 2.5%2.5/20 12.5%。代码里0.025和0.1这两个常数就是这么来的公式是duty_ratio 0.025 angle / 180 * 0.1。如果换上其他型号的舵机这个范围可能不同要自己实测调整。这个例子的核心展示是任务调度器接管了周期控制主循环不再需要任何sleep舵机任务只要配置好注册进去就能自动按时执行。而且因为定时器是硬件中断驱动哪怕主循环里出现了短暂的耗时操作舵机任务到期也能照常执行。3.5 更高级用法队列化的任务结果处理在项目里跑了一阵子你会发现一个更隐蔽的问题有些任务本身执行很快但它产生的结果下游任务需要等待或缓存。比如读取温湿度传感器 DHT11单个任务读取需要几百毫秒DHT11 协议要求间隔大于 1 秒如果直接阻塞在主循环里其他任务就遭殃。我的做法是把这类慢速任务独立成周期任务但结果用队列或全局变量传给显示任务。调度器只保证“该读的时候读”不需要保证“读取和显示在同一个时间片”。# 共享数据区 sensor_data { temp: 0.0, humidity: 0.0, last_read_ms: 0 } def read_sensor_task(): global sensor_data # 假设这里读取 DHT11耗时几百毫秒 # temp, humidity dht.read() sensor_data[temp] 25.3 sensor_data[humidity] 60.1 def display_task(): # 从数据区取最新值刷新 OLED temp sensor_data[temp] humidity sensor_data[humidity] # oled.text(fT:{temp:.1f} H:{humidity:.1f}, 0, 0) # oled.show() sched SimpleScheduler(tick_ms10) sched.register(2000, read_sensor_task) # DHT11 每 2 秒读一次 sched.register(100, display_task) # OLED 每 100ms 刷新一次 sched.run_main_loop()这种模式下慢任务和快任务之间的耦合只剩一个共享数据结构调度器完全不关心它们谁先谁后。这也是任务调度器的真正价值——把周期性执行的问题全部抽象掉,让你可以专注于写任务的业务逻辑。4. 常见问题与排查技巧实录4.1 踩坑记录定时器回调里放 print 导致的问题有段时间我在回调函数里去print调试信息结果发现程序运行一段时间后就变得很卡LED 闪烁频率参差不齐。排查了很久最后发现问题出在print上串口输出是阻塞式的如果 USB 转串口接收端没有及时读取缓冲区满了之后print会一直等待中断处理时间被拉长到几十毫秒甚至更久。从那以后我给自己定了一条规矩生产环境代码回调函数里绝不打印。调试信息统一放到主循环或者用标志位由主循环打印。4.2 定时器回调里调用 read_u16 会怎样还有一个坑是直接在回调里读取 ADC。ADC 读取本身不慢几微秒到几十微秒但如果你做多次平均滤波就要循环读很多次时间会翻倍。一旦回调耗时超过心跳周期下一个心跳就被延迟时间基准就乱了。更隐蔽的问题Pico 的 ADC 和某些硬件外设共用引脚或者与 DMA、PWM 冲突回调里操作外设出错时很难定位。我的建议是回调里绝对不要碰外设一律主循环处理这样出问题也容易排查。4.3 常见问题速查表现象可能原因解决方案LED 闪烁频率不准period单位搞错MicroPython 的 Timer period 单位是微秒不是毫秒多个任务中的某些任务长期不执行回调被某一个长耗时任务阻塞确认回调里没有耗时操作长任务放到主循环一段时间后死机中断回调节拍堆积内存增长检查回调里是否有变量未释放检查是否用global正确声明舵机抖动严重电源不足或 PWM 频率设置错误舵机外部供电确认 50Hz 标准频率定时器回调收不到回调函数签名错误回调必须定义为def func(t)接收一个定时器参数period_ms % tick_ms报错注册的任务周期不是心跳整数倍调整任务周期或者减少心跳周期程序运行后重启电感负载舵机/电机瞬时电流过大外部电源方案Pico USB 供电电流不够4.4 调试技巧用逻辑分析仪确认时间基准有条件的同学推荐用逻辑分析仪抓一下 GPIO 波形直观看看任务执行是不是真的周期性。方法非常简单在某个任务回调里翻转一个空闲 GPIO然后在逻辑分析仪上测量这个 GPIO 的周期变化。debug_pin Pin(10, Pin.OUT) def debug_task(): debug_pin.toggle() sched.register(1000, debug_task)如果波形稳定、周期严格 1000ms说明调度器没问题如果波形抖动厉害大概率是某个任务执行时间太长或者系统内存紧张导致 GC 频繁运行。这个手段在排查“任务偶尔卡一下”这类问题时非常有效。另外一个比较隐蔽的问题是 MicroPython 的内存碎片化。频繁创建和销毁对象比如在回调里创建字符串会导致堆碎片化时间久了 GC 暂停时间越来越长任务执行变得不稳定。实践中的做法是回调里不创建任何新对象尽量用预分配好的全局变量存储中间结果。你可以在调试时打印一下gc.mem_free()观察内存变化趋势如果持续下降就要注意回收策略了。5. 这套调度器还能怎么扩展5.1 给调度器加一个“删除任务”接口任务调度器做到这基本功能都有了。不过实际项目里还有个高频需求动态停止和注销任务。比如智能小车碰到障碍物后超声波测距任务要暂停但 LED 和电机控制继续。以前我采用的方式是直接操作任务列表但这里有个细节要注意不要在回调里修改任务列表。因为_tick遍历列表时如果同时增删元素列表长度变化会导致漏掉或重复执行。正确做法是用一个“待删除队列”在主循环里真正执行删除操作。class SimpleScheduler: def __init__(self, tick_ms5): # ... 省略 ... self._deleted [] def delete(self, task_name): 标记任务待删除 self._deleted.append(task_name) def _tick(self, t): # ... 原有逻辑 ... pass def run_main_loop(self): while True: # 先处理待删除任务 if self._deleted: for name in self._deleted: for i, task in enumerate(self.tasks): if task[name] name: self.tasks.pop(i) break self._deleted.clear() # 再执行任务 for task in self.tasks: if task[pending]: task[pending] False task[callback]()5.2 任务优先级支持另一个实用的升级是引入优先级。简单用“先注册先执行”的队列在某些场景下不够好用——比如事件类任务希望尽快响应而周期类任务可以稍等一下。优先级实现不需要搞复杂。给每个任务加一个priority_level字段主循环每次遍历时先执行优先级高的任务再执行低的。也可以维护两个列表高优先级列表和普通列表遍历的时候先查高优先级列表。无论哪种方式关键在于保证高优先级任务不会被低优先级任务长期阻塞。5.3 回调池与中断安全还有个小技巧现在很多 MicroPython 固件支持micropython.schedule()函数它可以把一个函数调度到“稍后”执行并且保证不会打断正在运行的 MicroPython 代码。如果你在中断回调里有很耗时的逻辑要处理可以考虑用micropython.schedule(callback, None)把任务丢给主循环执行。import micropython def _tick(self, t): for task in self.tasks: task[counter] self.tick_ms if task[counter] task[period]: task[counter] 0 micropython.schedule(self._run_task, task[callback]) def _run_task(self, cb): cb()这样做的最大优势是中断回调几乎瞬间返回绝不会阻塞其他定时器。缺点是调度器内部多了一层微任务队列极端情况下潜在的任务执行延迟会稍微变大但对于大多数传感器读取、LED 控制这类任务完全无感。我在几个实际项目里就是这么用的稳定性提升很明显。6. 从 Pico 迁移到其他平台时要注意什么6.1 定时器 API 差异对照这套调度器不仅能在 Pico 上跑稍微改改也能适配 ESP32、STM32 等支持 MicroPython 的板子。但不同平台的定时器 API 有一些差异我整理了一张对照表方便参考平台创建定时器周期单位回调签名注意点Pico (RP2040)Timer()自由分配微秒def cb(t)只有 8 个硬件定时器ESP32Timer(0)/Timer(1)指定编号微秒def cb(t)只有 2 个定时器STM32Timer(id)指定编号微秒def cb(t)具体依赖芯片系列改动的核心就一个地方创建定时器传入的参数。Pico 上Timer()不传参就行ESP32 上必须传编号STM32 要根据你的开发板选对应的定时器编号。其余的 API 和调用模式基本通用因为底层都是 MicroPython 抽象出来的machine.Timer。6.2 周期单位换算小心得无论什么平台MicroPython 的 Timerperiod参数单位都是微秒。这个单位很容易和time.sleep_ms搞混。我专门给自己写了个换算口诀ms × 1000 μs比如要 10ms 周期period10000。有一种情况要特别当心timer 的freq参数。如果你用timer.init(freq50)表示 50Hz那周期就是 1/50 秒 20ms要注意period和freq是二选一的两者都传会导致参数错误或者最后一个参数覆盖前一个。6.3 为什么 Pico 上有时Timer不够用Pico 的 RP2040 有 8 个硬件定时器看起来不少但如果你同时用了PWM 输出每个 PWM 通道底层也会占用定时器资源或者用了MicroPython 的time.ticks_us()高精度计时实际可用的 Timer 实例会变少。这个问题在开发大型项目时尤其明显4 路舵机 PWM 2 个周期任务 1 个心跳定时器可能就把资源吃光了。解决方案有两个一是尽量在 PWM 输出时复用同一个定时器的不同通道Pico 的 PWM 通道共享同一个 time base二是把多个周期任务合并到一个定时器回调里就是我们前面调的调度器方案减少直接创建 Timer 实例的数量。7. 常见问题再补几条亲测有效的经验7.1 定时器回调中的global陷阱刚接触 MicroPython 的朋友经常在这里翻车。回调函数内部要修改变量时必须显式加global声明否则 Python 会把变量当局部变量的新变量值根本改不进去。counter 0 def cb(t): global counter # 这行必须加 counter 1忘了global时MicroPython 不一定报错但计数器永远不变排查起来非常懵。这是一个很隐蔽的逻辑错误需要特别注意。7.2 避免用time.sleep配合定时器我在调试时还遇到过一种用法有人在定时器回调里用time.sleep_ms(1)来微调时序结果整个系统直接崩溃。原因很简单——回调本身就在中断里你在中断里 sleep等于把中断整个卡住了其他定时器没法触发系统就死给你看。矫枉过正的做法是回调里禁止一切可能阻塞的调用包括sleep、I2C 读取、网络请求。这些操作有专门的异步库或者可以放主循环里做。这是我踩过的最惨的一个坑分享出来希望大家别重蹈覆辙。7.3 任务函数异常会卡死整个调度器调度器的主循环是按顺序执行任务的。某个任务函数一旦抛出未捕获的异常主循环就会中断后续所有任务跟着停摆。而定时器中断还在继续置标志位你会看到的现象就是“程序假死”。解决办法是在主循环里对每个任务函数做异常保护加一层try...exceptdef run_main_loop(self): while True: for task in self.tasks: if task[pending]: task[pending] False try: task[callback]() except Exception as e: # 打印错误但不中断调度器 print(Task error:, e)我实测过大大小小很多项目这层 guard 几乎每次都能救场。传感器偶尔读不出来、网络偶发超时、显示内容格式问题——有了这层保护哪怕某个任务崩了其他任务还能继续运行系统不会彻底罢工。顺便说一句deleted列表的处理也建议放在遍历执行之前否则删除操作发生在遍历过程中同样会出问题。7.4 关于任务执行时间过长的最后提醒任务调度器不是万能的。如果某个任务函数本身执行就要 500ms而你给它设的周期是 100ms那不管怎么调度这个任务也跑不过来了——它在主循环里就会把其他任务的时间全吃掉。这种情况就要考虑把任务拆分或者降低任务频率或者换用真正的异步机制。对于 MicroPython 来说超过几十毫秒的任务逻辑我建议优先使用asyncio模块去做并发而不是依赖定时器调度。定时器调度器擅长的场景是大量短小周期任务让它们互不干扰地按时执行asyncio擅长的场景是 I/O 密集的异步等待。两者结合使用威力更大。8. 一个真实项目的完整组合示例最后再分享一个我实际做过的桌面小气象站项目把上面的知识点串起来。功能是每 5 秒读一次 DHT22 温湿度传感器每 500ms 刷一次 OLED 屏幕显示温湿度每 1 秒读一次空气质量传感器并通过蜂鸣器做高温报警。from machine import Pin, I2C, Timer, PWM import time, gc class SimpleScheduler: # ... 前面定义的完整版本 ... pass # 传感器/外设初始化略 ... sched SimpleScheduler(tick_ms10) # 共享数据区 env_data { temp: 0.0, humidity: 0.0, pm25: 0, alert: False } def read_dht22(): # dht22.measure() env_data[temp] dht22.temperature() env_data[humidity] dht22.humidity() if env_data[temp] 35: env_data[alert] True else: env_data[alert] False def read_air_sensor(): # pm25_val air_sensor.read() env_data[pm25] pm25_val def refresh_display(): # oled 刷新逻辑 # oled.text(fT:{env_data[temp]:.1f}C, 0, 0) # oled.text(fH:{env_data[humidity]:.1f}%, 0, 16) # oled.show() pass def buzzer_task(): if env_data[alert]: # 触发蜂鸣器报警 pass sched.register(5000, read_dht22) sched.register(1000, read_air_sensor) sched.register(500, refresh_display) sched.register(200, buzzer_task) sched.run_main_loop()整体跑下来非常稳定。OLED 刷新 500ms 一次显示不卡顿DHT22 读取 5 秒一次传感器数据准确蜂鸣器报警任务 200ms 检查一次响应时间小于 200ms足够及时。我大概连续运行了 48 小时没有死机也没有数据错乱稳定性完全可以接受。这个项目的核心体验在于代码结构变得特别清爽加功能就是在sched.register里多一行的事不需要去改主循环逻辑也不需要担心新任务影响旧任务。这也是为什么我强烈建议大家在 Pico 项目里尽早引入一个轻量调度器。我在实际使用中的另一个体会是任务调度器不是替代while True而是让while True变得简单。它把“什么时候该做什么”这个复杂的时序问题简化成了“每个任务自己定闹钟”的模式减少了心智负担也让代码能从几千行一直加到几万行不出乱子。这也是多年嵌入式开发里最值钱的一课。