
1. 项目概述1.1 为什么我一看到旋转编码器就先想消抖旋转编码器这个东西玩过树莓派 Pico、STM32、ESP32 的朋友应该都不陌生。不管是做音量旋钮、菜单选择、舵机微调还是 3D 打印机的调平旋钮它都能给你一种“物理世界和数字世界之间握着个旋钮”的手感。但真正把它接到单片机上一测你会发现一个特别尴尬的事实不消抖的编码器读数根本没法看。我最早拿 Pico 直接接 EC11 编码器用轮询方式读 A、B 两相电平结果旋一格有时候跳 3 个数有时候反向还会先正向跳一下再跳回来完全不是想象中“滴答一格”的清脆感。这个问题的本质不是编码器坏了而是机械触点在旋转瞬间发生的物理抖动。触点闭合和断开不是一次到位的而是高频弹跳几十到几百微秒。你没有做消抖处理的时候单片机看到的不是干净的“01”跳变而是一连串“0101010101”的毛刺。这不是某个特定品牌编码器的问题而是所有机械式旋转编码器的通病。也正因为如此“消抖”就成了旋转编码器项目落地前的第一道必答题。这道题不解决后面你接舵机也好、调参数也好、写菜单也好全都会被误触发给搅乱。本来想着省去按键消抖的学习环节结果旋转编码器的抖动比独立按键还让人头大。1.2 这套方案到底解决什么问题先说清楚我在这篇博文里讲的消抖方案是基于树莓派 Pico MicroPython 定时器这套组合实现的。它要解决的核心问题有四个机械抖动导致的重读、漏读、跳数。这是最直观的问题旋一格要能稳定识别成一格。主循环阻塞问题。很多初学教程里的消抖写法是用time.sleep(0.005)死等抖动过去。这在只有编码器一个外设的时候勉强能用但一旦你的主循环里还要刷 OLED、控制舵机 PWM、处理按键、跑 PID这种死等基本等于把程序卡死。检测实时性。轮询间隔太长会漏掉快速旋转时的脉冲间隔太短又会在主循环里占用过多 CPU。定时器扫一遍 A/B 相电平比在主循环里轮询及时得多。资源开销要小。Pico 的 RP2040 虽然有双核 133MHz但 MicroPython 毕竟是解释执行的主频资源不能乱造。用定时器回调的方式把检测逻辑从主循环里摘出去是性价比最高的做法。这套方案最后达到的效果是代码量不大不依赖任何外部硬件滤波电路纯软件定时器扫描实测下来旋一格稳定输出一个计数正反转判断可靠快速旋转也能跟手追得上。1.3 适合谁看需要什么基础如果你属于下面这几类人这篇博文对你应该有不少参考价值刚入手树莓派 Pico正在用 MicroPython 写外设驱动的入门玩家。被旋转编码器抖动折磨想找一个不阻塞主循环的消抖思路的嵌入式爱好者。要在 Pico 上做带旋钮交互的完整项目比如音量控制器、迷你调音台、菜单旋钮、怠速旋钮模拟器的开发者。之前用 Arduino 的rotary()库写过编码器换到 MicroPython 之后发现没有现成库可用、想搞清楚原理的人。硬件基础方面你只需要有最基础的电子常识知道面包板怎么插、杜邦线怎么接、Pico 的 GP 引脚在哪即可。软件方面只要会跑 MicroPython 的 hello world看得懂while True和if判断那下面的代码就足够看得懂。注意这篇实现用的是软件消抖而非硬件 RC 滤波。并不是说硬件滤波不好而是软件方案在成本、改版灵活度、参数可调性上都有优势尤其适合快速原型验证。等项目成熟了、要量产了再往 PCB 上加 RC 电路也不迟。2. 编码器工作原理与抖动成因拆解2.1 旋转编码器是靠什么“感觉”到你在转在动手写代码之前我强烈建议先把旋转编码器的工作原理弄明白。很多人写消抖代码时参数怎么调都不对本质原因就是没搞懂编码器输出的到底是什么波形。以最常见的机械增量式编码器 EC11 为例它内部有两个触点开关对应输出两个信号我们一般称它们为A 相和 B 相。当你旋转旋钮的时候这两个触点会按照旋转方向产生相位差 90 度的方波信号。也就是说顺时针旋转时A 相跳变领先B 相约 90 度。逆时针旋转时B 相跳变领先A 相约 90 度。单片机正是通过检测“谁先跳变”来判断你是往哪边转的。而一格完整的转动从某个定位档位转到下一个定位档位A、B 两相各会产生一次完整的“高→低→高”跳变也就是一个周期。这里有个非常重要的细节编码器并不是时刻都输出稳定的高或低电平。在没有任何外力旋转的时候A、B 两相的引脚状态由你外接的上拉或下拉电阻决定——EC11 这类机械编码器通常内部两对触点分别接公共端COMMON公共端接 GND 时A、B 需要外接上拉电阻静态为高电平公共端接 VCC 时则需要下拉电阻。我们在 Pico 上通常用内部上拉所以静态读到的是1, 1。当你转动旋钮、经过定位点时会听到清脆的“咔哒”一声这个“咔哒”对应的就是内部弹簧片在定位凹槽上弹跳滑动。而在弹跳的过程里触点的闭合状态是不稳定的。2.2 抖动到底长什么样我直接用逻辑分析仪抓过 EC11 旋转一格时 A 相的实际波形看完你就明白为什么简单的电平判断会出错。理想情况下A 相应该是一段干净的电平跳变高→低→高中间没有毛刺。但实际抓到的是这样从高跳到低的过程中不是一步到位而是在高和低之间来回反弹好几次持续几十微秒到一两百微秒然后才稳定在低电平。松开、转下一格的时候也会有类似的弹跳。而 B 相的抖动和 A 相往往是不同步的。这就导致一个非常头疼的情况单片机在主循环里读 A 相和 B 相的时候可能刚好读到了 A 相已经稳定、B 相还在弹跳的状态或者 A 相还在弹跳、B 相已经稳定的状态。于是程序误判方向、计数翻转、一次旋转被当成多次旋转全都来了。想起我之前用 STM32 做类似项目时有人在论坛里讨论过“旋转编码器要不要硬件消抖”的问题有工程师直接说“STM32 的定时器输入捕获配一个 10ms 的消抖时间软件滤波就够了”。我当时将信将疑直到自己在 Pico 上用 MicroPython 试过一轮才发现这个说法在逻辑上是对的但具体到代码怎么写坑还不少。2.3 标准消抖思路有哪些我为什么放弃了两三种聊到消抖方案行业内常用的有大致几种我筛掉了一部分最后才定下定时器扫描下面说说理由。第一种硬件 RC 滤波。在 A、B 两相上各加一个低通滤波器典型参数 10kΩ 电阻 100nF 电容把高频毛刺滤掉。这个方案在工业设备上很常见效果很好但它需要额外的电子元件、焊接或面包板接线而且参数一旦定死就不好改了。对快速原型验证来说不够灵活。第二种纯延时消抖。检测到某相跳变后立刻time.sleep_ms(5)或time.sleep_ms(10)等抖动结束后再稳定读一次。代码只有三五行特别简单。这个方案最大的问题我在开头说过time.sleep_ms会阻塞整个程序你的 OLED 不刷新了、舵机不动了、按键不响应了。项目只有一个旋钮的时候还好一旦有多个任务并行直接崩掉。第三种定时器中断 状态机。这是本博文推荐方案的核心。用 Pico 的硬件定时器每隔一小段时间比如 1ms~2ms自动触发一次回调在回调里读一次 A 相和 B 相电平。因为每次读取都隔开了抖动持续的时间窗口弹跳毛刺会被天然滤除掉——你在一个稳定采样周期里读到的是经过时间累积后趋于稳定的电平值。再加上状态机记录上一次的 A/B 组合和当前组合就能准确判断方向、累加计数。我在动手实现时还有一个小插曲一开始我也想过用 Pico 的PIO 状态机配合轮询去实现精准的编码器解码RP2040 的 PIO 做正交解码是硬件级的精度极高。但考虑到 MicroPython 环境下 PIO 的编程复杂度偏高而且这个博文面向的读者大部分是刚入门 MicroPython 的人用machine.Timer是更好的平衡——代码短、逻辑直观、性能足够。实操心得做这类信号处理什么时候“够用就好”比“性能拉满”更重要。定时器扫描虽然不如 PIO 正交解码那样能扛住极高的转速但人手指旋转编码器的速度用 1ms 采样周期追绰绰有余。3. 定时器扫描消抖方案的总体设计3.1 为什么定时器扫描能“顺手”消掉抖动你可能会有疑问定时器扫描不过就是每隔 1ms 去读一次电平而已它不会把抖动读进来吗答案是它确实还可能读到抖动但抖动概率被大幅降低了。原因有两层机械抖动本质上是小时间尺度内的高频弹跳。触点在几十到上百微秒内来回变化。如果你的采样间隔是 1ms那么很有可能会错过抖动持续期间内的那些快速翻转。换句话说采样间隔天然起到了低通滤波的效果。你可以结合边沿变化做二次确认。第一次检测到 A 相或 B 相电平变化时先记录这个变化然后在下一个采样周期1ms 后再读一次。如果第二次读到的电平跟第一次记录的一致说明已经不是瞬时的弹跳毛刺而是稳定的状态变化这时候才确定“真的转了一格”。这就好比你要确认对面那个人是在向你招手还是只是风把袖子吹起来了——你不会看一眼就下结论而是隔半秒再看一眼确认动作延续了才会回应他。定时器扫描在消抖中的角色就是那个“隔半秒再看一眼”的机制。3.2 总体结构主循环和定时器各干各的我们的程序整体上分成两层第一层是定时器回调层。machine.Timer设置为周期触发模式每 1ms 自动调用一次read_encoder()函数。这个函数负责读 A 相、读 B 相、组合成一个 2 位的状态值比如 A 作为 bit1B 作为 bit0、跟上一次状态做比较、如果发生了变化就执行消抖判定和方向判定、最后更新全局的计数器encoder_pos。第二层是主循环层。主循环里不需要再去管 A 相 B 相的电平变化它只干一件事定期读取encoder_pos这个全局变量的变化然后去执行真正的业务逻辑。比如显示在 OLED 上、控制舵机角度、调整一个参数值、打印串口日志等等。这种分层的好处非常明显主循环永远不会被编码器读取逻辑阻塞你可以在主循环里放心地做刷屏、延时、状态机切换编码器照样“后台”默默计数等你想起来看它的时候它已经把结果准备好了。3.3 选用 Micropython 的 machine.Timer 还是自己写死循环在 MicroPython 里实现“每隔一小段时间干一件事”方案不止一种用while True time.sleep_ms(1)做轮询。这个实现最简单但问题跟time.sleep_ms(5)一样主循环被占住了。而且 sleep 间隔抖动大不精准。用uasyncio异步任务。思路对但 Pico 的 MicroPython 固件里 uasyncio 对定时器精度和回调的兼容性做过调整新手用它还需要理解事件循环学习成本变高了。用machine.Timer。这是最贴合我们需求的方案。它的回调在后台运行不占用主循环精度在毫秒级完全够用代码还短。machine.Timer在 RP2040 上是通过硬件定时器触发的。MicroPython 固件会为它创建一个回调环境中断上下文里执行的操作要尽量短否则会影响其他中断的响应。我们在回调里只做“读两脚电平 比较 改一个整数变量”这个开销极小实测下来完全不影响系统整体运行。注意MicroPython 的 Timer 回调里不能做浮点运算、不能调用print这种带 I/O 的操作否则轻则影响实时性重则导致定时器回调延迟累积。把数据存到变量里到主循环里再统一打印和转发这是一个非常重要的习惯。3.4 状态机判断正反转的底层逻辑编码器方向判断的原理我用表格给你展示出来。假设 A 相对应二进制高位bit1B 对应低位bit0那么编码器在一个静止状态下A、B 读数为11二进制就是 0b11。当你顺时针旋转一格时电平变化顺序可能是11 - 01 - 00 - 10 - 11当你逆时针旋转一格时顺序是11 - 10 - 00 - 01 - 11每一次状态变化都可以根据“上一个状态”和“当前状态”的组合查表判断它是正转还是反转。这就是经典的正交解码状态表。我们可以用一张表来表示部分状态跃迁上一个状态 (A,B)当前状态 (A,B)旋转方向1101顺时针0100顺时针0010顺时针1011顺时针1110逆时针1000逆时针0001逆时针0111逆时针其余所有“跳变不连续”的状态组合都视为抖动或非法跃迁直接忽略不计。这种查表法不仅判断准确还能天然忽略抖动——因为抖动产生的小幅快速跳变通常不会按照上表那样有序次第出现而会突然从11跳到00或者01 - 10之类的不相邻状态这些都会被我们直接丢进“无效状态”的分支里。4. 硬件连接与基础代码实现4.1 硬件清单与接线图说明本次项目的硬件清单特别简单树莓派 Pico 一块RP2040 芯片。EC11 旋转编码器一个带定位感、带按键的那种常见型号即可。面包板一块 杜邦线若干。可选0.96 寸 OLED 显示屏一块I2C 接口用来可视化计数结果。接线关系如下照做即可跑通EC11 编码器引脚说明EC11 通常有 5 个引脚排成一排。一般是GND、A、B、SW、VCC不同厂家针脚顺序略有差异最好拿万用表蜂鸣档确认公共端位置。接线方式EC11 引脚接到 PicoA 相GP10B 相GP11公共端COMGNDSW按键GP12可选上拉电阻Pico 内部上拉代码里开启注意EC11 的 A 相和 B 相在旋转时公共端会在 A 或 B 之间切换闭合。我们这里公共端接 GNDA/B 通过 Pico 内部上拉默认读高。当内部触点闭合时对应引脚被拉到 GND读到低电平。如果你把公共端接到了 VCC则要改用下拉模式否则逻辑反了。这里有个小经验EC11 的引脚顺序不同批次真的不一样第一次用之前最好用万用表测试一下哪两个引脚是 A/B哪两个是内部触点。如果接线反了程序读出来方向也是反的——不用慌最后把代码里的方向判断反过来即可。4.2 消抖定时器的核心代码下面给出可以直接运行的完整 MicroPython 代码。我在关键行都写了注释方便你照着敲。from machine import Pin, Timer import time # 编码器 A、B 相位引脚开启内部上拉 pin_a Pin(10, Pin.IN, Pin.PULL_UP) pin_b Pin(11, Pin.IN, Pin.PULL_UP) # 全局计数器 encoder_pos 0 last_state 0 last_valid_state 0 # 状态表法方向判定 # 用 (A1) | B 编码当前状态查表得到方向1 顺时针-1 逆时针0 无效 DIR_TABLE { (0b11, 0b01): 1, (0b01, 0b00): 1, (0b00, 0b10): 1, (0b10, 0b11): 1, (0b11, 0b10): -1, (0b10, 0b00): -1, (0b00, 0b01): -1, (0b01, 0b11): -1, } def read_encoder(timer): global encoder_pos, last_state # 读取 A、B 并合成状态值 a pin_a.value() b pin_b.value() state (a 1) | b # 如果状态没有变化直接返回 if state last_state: return # 查表法判断方向 direction DIR_TABLE.get((last_state, state), 0) if direction ! 0: encoder_pos direction # 更新上一次状态 last_state state # 初始化定时器1ms 周期扫描 timer Timer() timer.init(period1, modeTimer.PERIODIC, callbackread_encoder) # 主循环读取并打印当前计数 while True: print(encoder_pos:, encoder_pos) time.sleep_ms(100)这段代码的逻辑非常直接read_encoder每 1ms 被调用一次。它读到的 A、B 电平组合成状态值(a1)|b。如果state和last_state一致说明没有变化忽略。如果变化了就在DIR_TABLE里查表看这次变化属于顺时针还是逆时针。encoder_pos全局变量实时累加主循环只管读取。4.3 为什么DIR_TABLE能过滤掉抖动你把抖动波形的状态变化列出来就明白了。一次真实的旋转状态有序跳变11 - 01 - 00 - 10 - 11。而抖动的表现往往是11 - 01 - 11 - 01 - 00。后面的11 - 01 - 11 - 01这些来回跳在查表里对应(0b01, 0b11)这个组合吗查表发现不在表中或者在某些抖动场景下它确实在表中但方向不一致。问题来了如果抖动恰好沿着合法的查表路径跳呢比如11 - 01 - 11这个来回(0b11, 0b01)是顺时针(0b01, 0b11)不在表中所以只计了 1 又忽略了一次。但最终机械触点稳定到了最终状态方向判断基本不受影响。为了更彻底地抑制抖动你可以在查表通过后再加上“连续两次确认”逻辑即在检测到状态变化后等下一次扫描1ms 后重新读一次同一状态如果两次一致才真正执行计数累加。这样能进一步降低抖动造成的误计数。我这里再给一个“连续确认”版本的代码片段供你对比def read_encoder_with_confirm(timer): global encoder_pos, last_state, last_valid_state a pin_a.value() b pin_b.value() state (a 1) | b if state last_state: return # 第一次记录变化 last_state state # 等待下一次扫描再确认一次其实定时器已经隔了1ms这里主要是逻辑上的确认 # 在MicroPython的定时器回调里我们无法直接“等” # 所以这里用的是一个轻量级标志位。 confirm_state last_valid_state if state confirm_state: direction DIR_TABLE.get((last_valid_state, state), 0) if direction ! 0: encoder_pos direction last_valid_state state这个写法实际是把“确认”和“计数”分开到了不同周期执行。不过坦白说对于 EC11 接 Pico 这种场景纯DIR_TABLE方案已经够用了连续确认代码更多是作为一种思路补充给你方便你在更恶劣的电磁环境或者转速更慢、更精密的场景下做增强。4.4 主循环里如何安全读取计数主循环读取encoder_pos时有一个“坑”MicroPython 里虽然全局变量的整型赋值在 GIL 保护下是原子的但print(encoder_pos)这类操作和定时器回调同时发生时你打印出来的值可能不是最新的。不过对大多数项目来说这不是问题反而让你看到的效果更“干净”——统计完的最终结果才是真实的。但有一种情况必须注意如果你要基于encoder_pos的变化去控制舵机或播放声音千万别直接读一次就完事应该记录上一次的值用差值去触发动作。比如last_display_pos 0 while True: if encoder_pos ! last_display_pos: delta encoder_pos - last_display_pos print(转动了, delta, 格) # 这里根据 delta 调整舵机角度或菜单项索引 last_display_pos encoder_pos time.sleep_ms(20)这样即使两次主循环间隔期间编码器被转动了多格你也能感知到完整的差值而不是只感知到“变化了”这个布尔事件。5. 定时器参数选择与性能调优5.1 定时器扫描周期应该设多少毫秒这是新手问得最多的问题“period1和period2有什么区别我是不是设得越小越好”答案是否定的。定时器回调当然越频繁采样越密漏检的概率越低。但回调函数本身是有执行开销的。MicroPython 是解释执行的每次回调都要执行 Python 级别的函数调用、位运算、字典查找开销远比 C 语言大。如果period0.1ms你的 Pico 会花大量 CPU 时间在编码器读取上反而拖累其他任务。根据我实测的经验浏览器、菜单导航这类普通旋钮操作1ms 完全够用手感顺滑不丢步。快速连旋、测试极限转速可以尝试 0.5ms500 微秒观察是否漏步。低速精调、需要严格控制误触发的场景可以放宽到 2ms但再宽就有明显反应迟钝感了。我默认推荐 1ms。这个数值是“性能”和“响应”都能兼顾的经验值。实操心得MicroPython 的Timer.init(period1)代表的是 1ms 吗在 RP2040 的 MicroPython 固件中period参数单位确实是毫秒但底层定时器精度受系统 tick 影响不是绝对精确的 1ms。好在我们的消抖逻辑对微小的周期抖动并不敏感所以这个问题可以忽略。5.2 快速旋转时会不会丢步这是很多人担心的问题我直接给结论在人手旋转编码器的正常速度范围内1ms 采样不会丢步。我们可以做个简单的数学估算。EC11 编码器一般转动一圈有 20 个定位格每格对应 4 次状态变化11-01-00-10-11。也就是说快速转动一整圈A、B 两相约产生 40 次状态变化。如果你 1 秒钟转 5 圈这已经是相当快的速度了那一秒内需要捕捉的状态变化大约是 200 次。而我们在 1ms 内采样一次一秒能采样 1000 次。采样能力远大于实际需求至少还有 5 倍的余量。但要注意一点一次状态变化后如果 1ms 内状态又变了一次中间那次变化可能被漏掉。不过这种情况只会在极高速旋转或者抖动特别剧烈时出现普通的手感操作完全不受影响。5.3 定时器回调代码的“轻量化”建议在 MicroPython 的定时器回调里尽量避免以下几种操作避免print串口输出是阻塞式的一个print可能耗时几十毫秒如果它出现在定时器回调里整个定时周期会被拉飞。避免浮点运算MicroPython 的浮点运算在 RP2040 上不慢但比整型运算仍然慢一个量级。方向判定和计数累加完全可以用整数实现。避免列表/字典动态分配我们在初始化时就把DIR_TABLE定义成了模块级别的字典常量回调里只做.get()查表不会产生新的对象分配。避免在回调里驱动 OLED 等慢速外设I2C 通信一个字节都要几十微秒一个屏幕刷新要几毫秒。这些事情全放到主循环。只要你遵守以上规则定时器回调的实际耗时通常在 50 微秒以内占一个 1ms 周期的 5%剩下的时间都能留给主循环去干正经事。5.4 如果转速极高可以考虑“边缘触发中断 定时器判定”混合模式刚才说过 PIO 正交解码是终极方案但如果你暂时不想上 PIO又想比纯定时器采样更快地捕捉跳变可以试试“外部中断 定时器消抖窗口”的混合模式让 A 相和 B 相都配置成上升沿/下降沿触发Pin.IRQ中断到来时先记录时间戳然后设置一个标志位定时器每 0.5ms 检查一次标志位若标志位置位且当前电平稳定后再更新计数中断函数本身不执行耗时逻辑。这个方案的优势是实时性比纯定时器扫描更高缺点是代码复杂度上去了而且要处理好“同一个物理事件触发了多次中断”的去重。对 Pico MicroPython 来说除非你做的是高速测量设备否则没有必要上这个方案。6. 常见问题与坑位实录6.1 方向反了怎么办这是最简单的问题。如果你接线时把 A、B 两相反了——或者 EC11 引脚定义和你手里的实际排布不同——程序会把顺时针识别成逆时针。解决方法有两种把pin_a和pin_b的赋值交换pin_a Pin(11, Pin.IN, Pin.PULL_UP) pin_b Pin(10, Pin.IN, Pin.PULL_UP)把DIR_TABLE中所有 1 改成 -1、-1 改成 1。个人更推荐交换pin_a和pin_b因为改代码语义更直接不容易引出一堆方向相关的 bug。6.2 计数乱跳、抖动过滤不掉如果你发现即使用了定时器方案偶尔还是出现“一次旋转 2/3”的情况先不要急着怀疑代码按优先级排查以下项目接线接触不良。面包板插孔氧化、杜邦线松动都会造成电平随机跳变。先重新插拔一次杜邦线或者用万用表测 A/B 和 GND 之间的通断。上拉电阻缺失或过弱。Pico 内部上拉是大约 50kΩ 左右阻抗偏高。噪声环境较差时建议在 A、B 到 VCC 之间各外接一个 10kΩ 电阻能显著提高抗干扰能力。引脚悬空。EC11 的公共端如果没接 GNDA、B 无法被拉到低电平逻辑会乱。确认公共端确实接的是 GND。供电不稳。Pico 如果用 USB 供电还拖了一堆外设3.3V 可能波动较大。给外设单独供电或加个 100nF 去耦电容会有帮助。6.3Timer回调没走、计数一直为 0出现这种情况九成是定时器没正确初始化。检查以下几处确认代码里调用了timer.init(period1, modeTimer.PERIODIC, callbackread_encoder)。确认read_encoder函数名拼写正确且是全局函数不是嵌套在其他函数内部的内层函数有的 MicroPython 固件对闭包回调支持有点奇怪保险起见都用模块级函数。确认last_state初始化和实际状态一致。如果上电后引脚没有稳定第一次回调里可能直接发生一次“状态跳变”但不影响后续计数。6.4 接 OLED 之后编码器反应变迟钝了如果主循环里跑着 OLED 刷新Encoder_pos要过一两秒才更新一次问题不在定时器而在主循环刷新频率太低。OLED 的 I2C 刷新一屏可能要 20~50ms如果你每次刷新都重新画一帧主循环自然很慢。解决方案也很简单把“读编码器”和“显示”解耦。主循环里只做轻量判断比如每 20ms 读一次encoder_pos如果变了再刷 OLED。没变化的时候OLED 不需要重画。6.5 MicroPython 定时器回调与主循环的“线程安全”MicroPython 的定时器回调运行在软件定时器上下文和主循环共享同一个解释器锁。严格来说如果你在回调里修改了一个列表、字典之类的可变对象主循环同时在读取可能出现不一致。但我们的方案里只改encoder_pos这个整数主循环里也只是读取整数不会有任何问题。一旦你以后要在这个基础上扩展比如把编码器计数存成一个数组或字典就要注意加锁或者用一个标志位来协调读写避免出现数据竞争。7. 扩展实战编码器控制舵机角度7.1 思路把旋转角度映射到舵机 PWM 占空比写到这里我不建议你停留在“打印计数”这一步。旋转编码器最有价值的落地场景就是作为人机交互输入去控制某个执行器。最常见也最容易出成就感的就是控制舵机角度。舵机的控制原理我们简单回顾一下Pico 通过 PWM 输出一个周期为 20ms50Hz的方波方波的占空比决定舵机轴的角度。常规舵机的脉冲宽度大概在 0.5ms~2.5ms 之间对应 0 度到 180 度。MicroPython 里配置 PWM 非常简单from machine import Pin, PWM servo PWM(Pin(15)) servo.freq(50) def set_servo_angle(angle): # 角度转换为占空比 us 500 (angle / 180) * 2000 # 500us ~ 2500us servo.duty_ns(int(us * 1000))7.2 把旋转编码器和舵机联动起来我们只要在上面的编码器方案基础上修改主循环逻辑每次encoder_pos发生变化时把计数值映射到一个 0~180 的角度值然后调用set_servo_angle()即可。这里有两个细节值得注意给角度限幅。编码器转到超出 0~180 的范围时应该把目标角度夹紧在边界防止 PWM 计算出超范围的占空比。不要把计数值直接当角度值。如果一个步进对应一度那旋转一圈变化 20你可能觉得太慢了。可以设一个比例系数比如每格 3 度旋转一圈变化 60 度手感更畅快。这个系数完全看你项目需要。来看看改完之后的完整主循环逻辑servo_angle 90 # 初始角度 ANGLE_STEP 3 while True: if encoder_pos ! last_display_pos: delta encoder_pos - last_display_pos last_display_pos encoder_pos # 调整角度并限幅 servo_angle delta * ANGLE_STEP if servo_angle 180: servo_angle 180 if servo_angle 0: servo_angle 0 set_servo_angle(servo_angle) print(当前角度:, servo_angle) time.sleep_ms(20)这段代码跑起来你会获得一个非常直觉的体验拧旋钮舵机跟着转右拧增加角度左拧减小角度。整个交互的实时性非常自然没有任何明显的延迟感。实操心得舵机启动瞬间电流可能较大用 Pico 的 3.3V 引脚直接给舵机供电转速高的时候会导致电压跌落Pico 直接重启不是没可能。建议给舵机用独立 5V 电源供电Pico 和舵机共地即可。这个坑我踩过一次之后凡是带舵机的项目电源方案永远优先考虑。7.3 再接一个 OLED可视化菜单选择器编码器另一个高频应用场景是菜单导航。你可以在 OLED 上显示一个菜单列表编码器负责上下移动高亮项内置按键SW 引脚负责确认。这里不放完整代码只说思路把encoder_pos对一个菜单项总数取模就能得到当前高亮项的索引每次编码器计数变化时刷新高亮行按 SW 键时进入对应的子菜单。这个交互模式在小型仪器、迷你调音台、自制 SDR 收音机里都非常常见。8. 我对这套方案的一些复盘这个项目做到最后说实话代码本身反而是最简单的一部分。真正值钱的是理清楚“为什么这么做”的那条思路线。我第一次调编码器的时候还在网上翻各类 STM32、Arduino 的消抖方案买 RC 滤波器、灰度光电、专用编码器解码芯片折腾一圈后回到纯粹的软件定时器扫描发现十分钟就搞定了。回头想不是硬件方案不好而是大多数时候我们做的原型项目“够用”比“极致”重要。Pico 的定时器 状态机查表就是典型的高性价比方案。在写这篇博文的过程中我又翻出了当年的逻辑分析仪重新抓了一次 EC11 的波形确认了 1ms 采样对机械抖动的抑制效果。看着屏幕上干净的状态跳变序列再回头看最初“乱七八糟”的毛刺波形那个对比本身就是“消抖”二字最好的注脚。最后再分享一个实用的小技巧调试编码器程序的时候别在回调里 print也别在主循环里 print 得太频繁。串口打印会拖慢一切让手感变得粘滞。正确做法是接一块 OLED或者只在计数变化超过一定阈值时才打印一行。等你真正把编码器接进完整项目里会感谢这个习惯。如果你已经用上了这个定时器方案建议试试把扫描周期从 1ms 改成 2ms感受下手感差异再决定哪个更适合你的项目。不同人旋旋钮的习惯不同这种微调没有绝对标准实测才是王道。