刚拿到树莓派 Pico 的那几天,我干的第一件事就是翻出抽屉里的 10K 电位器,想做一个最常见的模拟量采集实验。过程比想象中磕绊:read_u16()读出来的值不是预想中的 0-4095,转速快一点数据像心电图一样跳,光看串口数字完全没法判断噪声到底多大。这篇我把从硬件接线、MicroPython 代码到 PC 端串口波形调试的完整链路摊开讲一遍,核心关键词就三个:Pico ADC、电位器采集、SerialPlot 波形。适合手头有 Pico 板子、想把模拟采集吃透的读者,也适合想快速搭一套波形观察环境来调试其它传感器的人。
1. Pico 的 ADC 到底有多少位:硬件规格与 MicroPython 返回值的关系
1.1 RP2040 集成的 SAR ADC 架构与可用通道
树莓派 Pico 用的芯片是 RP2040,内置 ADC 是一个 12 位逐次逼近型(SAR)ADC。逐次逼近的工作方式很像猜数字游戏:先把电压和参考电压的一半比较,判断高了还是低了,再在二分之一的区间内继续二分,循环 12 次,最终得到一个 0-4095 的整数编码。这种方式的好处是转换速度可以做得比较快,坏处是它需要“采样保持”,也就是在转换前把输入电压冻结在一个采样电容上,如果信号源内阻太大,电容充不满,测量就会偏低或者抖动。
规格表我整理了一份,方便对照:
| 参数 | 数值 |
|---|---|
| ADC 位数 | 12 位 |
| 分辨率 | 0-4095 |
| 参考电压 | 3.3V |
| 单个 LSB 电压 | 3.3 / 4096 ≈ 0.806mV |
| 可用外部引脚通道 | ADC0-GPIO26、ADC1-GPIO27、ADC2-GPIO28 |
| 内部通道 | ADC3 温度传感器、ADC4 接 AGND(校准用) |
| 最大采样率 | 约 500kSPS(数据手册标称) |
这里最容易被忽略的是:Pico 虽然名义上有 5 个 ADC 通道,但真正可以从引脚引出的模拟输入只有 GPIO26、27、28 三个。ADC3 是芯片内部的温度传感器,ADC4 在 Pico 开发板上已经固定接到了 AGND,用来做偏移误差校准。如果你在代码里写machine.ADC(3)或者machine.ADC(4),得到的数据并不是一个外部电压。所以设计电路时,默认你有且只有三个可用的模拟输入脚。
1.2read_u16()为什么返回 0-65535 而不是 0-4095
用 MicroPython 操作 Pico 的 ADC,最常见的困惑就是:
import machine adc = machine.ADC(26) print(adc.read_u16())输出范围是 0-65535,而不是 12 位 ADC 应该有的 0-4095。这不是板子坏了,也不是 MicroPython 把底层 ADC 换成了 16 位,而是 MicroPython 为了统一不同开发板 ADC 的返回语义,把底层结果扩大到了 16 位返回。你可以理解为:12 位原始结果左移 4 位变成了 16 位,4095 × 16 = 65520,接近 65535。
我在实际代码里通常这样处理:
raw_16 = adc.read_u16() raw_12 = raw_16 >> 4 # 恢复 12 位原始值 voltage = raw_16 * 3.3 / 65535 # 换算电压这样做的好处是:当你想看真实 ADC 分辨率时,用raw_12;当你想控制同样是 16 位语义的 PWM 时,直接用raw_16减少一次映射。两套量纲各留一份,后面写联动控制会非常顺手。
1.3 输入阻抗对采样精度的影响
SAR ADC 的一个隐蔽坑在于输入阻抗。ADC 采样时,内部采样电容需要在采样窗口内充到输入电压。如果信号源是高阻抗的,充电时间常数变大,采样电容最终充不满,读出来的值就会偏低。用 10K 电位器时,抽头位置的等效源阻抗大约等于上半段和下半段电阻的并联值。旋到中间时,10K 电位器等效输出阻抗约 2.5K,这个量级对 Pico 的 ADC 是完全友好的。但如果拿 1M 电位器来做这个实验,波形就会明显不稳定,数值不光偏低,还会伴随随机抖动。所以这个实验固定用 10K 电位器,不是为了偷懒,是为了让初学者避开一个可能根本意识不到的硬件坑。
2. 电路搭建:电位器和 LED 该接在哪,限流电阻怎么算
2.1 元器件清单和一个合理的接线表
这次的元器件非常简单,我列一下:
- 树莓派 Pico 开发板一块
- 10K 单圈旋转电位器一个
- 普通 5mm 红色 LED 一个
- 220Ω 限流电阻一个
- 面包板一块
- 杜邦线若干
接线表如下:
| 器件 | 引脚 | 接 Pico 引脚 |
|---|---|---|
| 电位器 固定端 1 | 任意一个外端 | 3V3(OUT) |
| 电位器 固定端 2 | 另一个外端 | GND |
| 电位器 滑动端 | 中间抽头 | GPIO26(ADC0) |
| LED 阳极 | 长脚 | GPIO15 |
| LED 阴极 | 短脚 | GND |
| 限流电阻 | 串联在 GPIO15 和 LED 阳极之间 | 任选一侧串入 |
为什么 LED 放在 GPIO15 而不是 GPIO25?GPIO25 是板载 LED,你当然也可以拿它来做亮度控制实验,但用 GPIO15 的好处是:你会被迫把“板载 LED”和“外接 LED”分开,代码和电路更接近真实项目的结构。另外 GPIO15 没有和 ADC 通道冲突,离 GPIO26 右侧引脚区也比较近,面包板上走线会很整齐。
2.2 LED 限流电阻的取值计算,不能拍脑袋
LED 不能直接接 3.3V,否则电流过大容易烧毁。红色 LED 正向压降通常在 1.8-2.1V 左右。如果用 220Ω 电阻串联进回路,那么电阻两端电压约为 3.3V - 2.0V = 1.3V,流过 LED 的电流约 1.3V / 220Ω ≈ 6mA。这个电流足够让普通 LED 清晰发光,而且对 Pico 的 GPIO 来说负载很轻。
如果你换成蓝色或者白色 LED,正向压降接近 3V,同样的 220Ω 电阻下,电流会小很多甚至接近熄灭。所以换 LED 颜色时,限流电阻也要跟着换,这是一个很容易被忽略的细节。
3. 代码分三步走:ADC 读取、PWM 调光、软件滤波
3.1 第一步:裸读 ADC,确认电位器输出是线性的
先用一段最基础的代码把数据读出来,不要急着接 LED,避免问题叠加。
import machine import utime adc = machine.ADC(26) while True: raw_16 = adc.read_u16() raw_12 = raw_16 >> 4 voltage = raw_16 * 3.3 / 65535 print(raw_16, raw_12, round(voltage, 4)) utime.sleep(0.1)用 Thonny 运行后旋转电位器,正常情况下应该看到三个数值跟随旋转角度平滑变化。如果旋到两端读数不是 0 和 65535,而是卡在某个小范围,极有可能是接线问题而不是 ADC 问题。我遇到过旋到底读数还剩一百多的情况,检查发现是接地端松了,抽头浮空,ADC 悬空输入在读取浮空电压。这里有个关键提示:ADC 输入脚不要悬空,不接传感器时至少用一个电阻拉到 GND 或 3.3V,否则读数会随机漂移。
3.2 第二步:用 PWM 让 LED 亮度跟随电位器
确认 ADC 读数正常后,加入 GPIO15 的 PWM 输出:
import machine import utime adc = machine.ADC(26) led = machine.PWM(machine.Pin(15)) led.freq(1000) led.duty_u16(0) while True: value = adc.read_u16() led.duty_u16(value) print(value) utime.sleep(0.05)这里直接使用read_u16()的值作为 PWM 占空比,是因为 MicroPython 的 PWM 设置接口duty_u16()同样接收 0-65535,两边的语义刚好对齐。省掉了一次比例换算,逻辑更简洁。
PWM 频率我选择 1000Hz。人眼对超过 100Hz 的亮度变化基本没有闪烁感,1000Hz 既能保证肉眼无闪烁,功耗也不会太高。如果你将来用同样的思路驱动电机,频率要重新按电机特性选,LED 不需要那么讲究。
3.3 第三步:加滑动平均滤波,解决波形毛刺
ADC 裸读数直接传给 LED,低速旋转时能明显看到亮度在某个角度区间轻微抖动。这不一定是电位器坏了,更多是环境噪声耦合到模拟引线上。最直接的解决办法是硬件加一个 RC 低通滤波,但为了先验证问题,我通常会先上软件滤波。
import machine import utime adc = machine.ADC(26) led = machine.PWM(machine.Pin(15)) led.freq(1000) buf_size = 10 buf = [] while True: value = adc.read_u16() buf.append(value) if len(buf) > buf_size: buf.pop(0) avg = sum(buf) // len(buf) led.duty_u16(avg) print(avg, value) utime.sleep(0.02)滑动平均的原理很简单:维护一个固定长度的队列,每次取队列平均值。这个滤波对白噪声型毛刺非常有效,但代价是延迟。buf_size为 10,采样间隔 20ms,那么滤波窗口约 200ms,也就是说快速旋转电位器时,LED 亮度会滞后约 0.2 秒。这个延迟对于调光灯效没问题,但如果你要做一个需要快速响应的控制系统,就要缩短窗口,比如 4-5 点,延迟降到 0.1 秒以内,噪声抑制能力稍微弱一些,但手感好很多。
如果遇到的是偶发强脉冲干扰,滑动平均反而不如中位滤波。中位滤波取队列里排序后的中间值,对尖峰更鲁棒,但计算量略大。Pico 跑这两种滤波都毫无压力,具体用哪种取决于你面对的噪声类型。
4. SerialPlot 串口波形:把 ADC 数值变成看得见的实时曲线
4.1 为什么需要波形调试,而不是只看串口数字
串口终端打印一堆数字,你很难直观感受“数值到底稳不稳”。人眼看一串数字跳动几百个 LSB,和看一条曲线上下波动,完全是两种体验。SerialPlot 是一个轻量的跨平台串口波形显示软件,它的工作方式是:读取串口发来的文本行,一行里如果是空格或逗号分隔的多个数值,就自动解析成多通道数据并画成实时曲线。
用它来观察 ADC 波形,比单纯打印数字高效得多。我还经常拿它来调试其它传感器,比如光敏电阻、热敏电阻,甚至是 IMU 的原始数据。搭一次环境,后面可以反复用。
4.2 配置文件格式与 MicroPython 代码的配合
SerialPlot 默认按文本模式解析数据。每行数据以换行符结束,一行里可以有多个数值,数值之间用空格、逗号或制表符分隔。SerialPlot 会根据一行里值的个数自动创建对应数量的曲线。
打开 SerialPlot 后,在右侧面板设置串口参数:选择 Pico 对应的 COM 端口,波特率选 115200,数据位 8,停止位 1,无校验。这里有个细节:Pico 通过 USB 连接电脑时,在电脑端枚举出来的是虚拟串口,波特率设置实际上不影响 USB CDC 传输,但为了匹配调试习惯,统一填 115200 没有坏处。
配套的 MicroPython 代码里,最关键的一条纪律是:主循环只输出固定格式的数据行,不要混入任何调试文本。比如上面带滑动平均的代码里:
print(avg, value)这一行输出两个空格分隔的数字,SerialPlot 就会识别为两通道数据。第一通道是滤波后的均值,第二通道是原始 ADC 读数。一屏里同时显示两条曲线,滤波效果一目了然。
如果你想看的是电压值而不是 ADC 整数,可以这样输出:
print(avg * 3.3 / 65535, value * 3.3 / 65535)然后在 SerialPlot 的坐标设置里把 Y 轴范围设成 0-3.3V。我在实际调试中更喜欢同时看原始 ADC 整数和电压值,因为原始整数能直接反映 LSB 抖动幅度,电压值则方便和其他仪器读数对照。
4.3 SerialPlot 不工作时的排查思路
连接 SerialPlot 常见的问题无非这几种:
- 端口被占用。如果 Thonny 或者其他串口工具还开着,COM 口被占用,SerialPlot 会打开失败。先关掉其它占端口的程序。
- 串口数据乱码或者曲线不更新。检查代码里是不是在某些分支打出了非数字内容。MicroPython 运行时报错信息也会输出到串口,一旦混入,SerialPlot 解析就会卡住。
- 换行符问题。Windows 下 SerialPlot 通常能自动识别
\n,但如果你在 Thonny 里看到输出正常而 SerialPlot 不显示,可以确认一下 Thonny 是否还连着该串口,同一时间只能有一个程序打开虚拟串口。
4.4 从波形能读出哪些信息
接上 SerialPlot 后,旋转电位器,你会看到类似“三角波”的曲线。缓慢旋转时,曲线平滑上升或下降;快速旋转时,曲线呈现陡峭台阶。如果波形上有大量高频毛刺,说明环境干扰比较明显;如果某几个角度反复出现跳变,可能是电位器碳膜磨损,属于机械噪声。
滤波后的波形和原始波形同时显示时,你能直观观察到滑动平均的延迟。两个波形之间横向错开的那段时间,就是滤波窗口带来的相位滞后。这个现象在数值终端里是看不出来的,但波形图上一眼就能看懂。
5. 实测中的坑与调试心得:从接线到波形显示的完整排错清单
5.1 电位器引脚顺序的方向坑
电位器三个引脚是一个圆弧排列,1、2、3,但不同厂家可能方向和定义不一样。你只要记住:中间抽头必须接 ADC,另外两个固定端头接电源和 GND。如果发现旋转方向是反的,交换接 3V3 和 GND 的两根线即可,抽头永远不要接电源或 GND,否则 ADC 输入会被钳位在固定电平,读数不随旋钮变化。
5.2 USB 供电带来的 50Hz 纹波
用电脑 USB 口供电时,波形上偶尔能看到周期性波浪纹,频率接近交流电的 50Hz 或 100Hz。这种情况源头通常在供电,而不是 ADC。可以尝试换一根短 USB 线,或者把 Pico 的 3V3 引出给传感器,但一定要和 Pico 共地。如果条件允许,用锂电池或者独立稳压电源供电,纹波会小很多。我实测下来,电脑前置 USB 口和机箱后置 USB 口的供电质量都有差异,换接口有时比改代码更有效。
5.3 PWM 亮度阶梯感和非线性映射
在低速旋转电位器时,我最早发现 LED 亮度有肉眼可见的阶梯感,中低亮度区间尤其明显。原因有两层:一是 ADC 低位抖动会让 PWM 占空比微微突变;二是人眼对低亮度的变化比对高亮度的变化更敏感,同样的占空比变化,在低亮度段被感知得更明显。
软件滑动平均能解决一部分抖动问题,但阶梯感仍然存在,这时候可以用非线性映射让亮度变化更自然:
duty = (value * value) // 65535 led.duty_u16(duty)平方曲线让低亮度段的占空比变化变缓,亮度过渡更平滑。我试过之后发现,电位器转到一半时,亮度明显低于线性映射,整体视觉更柔和。缺点是如果你想要“线性亮度可预测”的调光方式,非线性映射会引入额外的心理物理对应关系,需要按使用场景取舍。
5.4 滤波器延迟对响应速度的影响
这个项目里 LED 亮度对延迟不敏感,10 点滑动平均可以放心用。但如果你换到舵机或者电机控制,200ms 延迟会直接体现在手感上,整个系统会变得粘滞。控制器项目中优先用 3-5 点窗口,或者只在必要时开滤波。我在控制实验里通常先看原始波形,判断噪声量级,再决定滤波强度,而不是一上来就默认开启重滤波。
5.5 关于 MicroPython REPL 粘贴长代码的碎碎念
用 Thonny 往 Pico 的 REPL 里粘贴多行代码,如果之前已经运行过某个程序,很容易出现缩进错乱或者代码被拆成多段执行的情况,报各种莫名其妙的IndentationError。我的建议是:不要直接在 REPL 里粘贴长程序,而是把代码保存为main.py,然后在 Thonny 里运行文件。这样无论是代码保存、重启上电自动运行,还是排查语法错误,都更接近正式项目的流程。
踩过几次坑之后,我对这类“入门级”实验的看法是:电位器 + ADC + LED 看起来简单,但它是理解整套模拟量采集链路的最佳载体。硬件上涉及传感器输入、阻抗匹配、限流计算,代码上涉及数据转换、滤波、PWM 控制,调试上又涉及串口波形观察。这一圈走通之后,后面再接光敏电阻、热敏电阻、土壤湿度传感器,本质上都是同一个套路:把物理量变成电压,把电压变成 ADC 数字,把数字变成可读信息或控制量。做完这个实验我最大的体会是,不管采样后边的算法多复杂,先用 SerialPlot 看一遍原始波形永远是第一步,很多藏着的数据问题,光靠数字终端真的发现不了。