树莓派Pico ADC模拟量采集与SerialPlot波形调试实战
2026/9/11 10:43:44 网站建设 项目流程

刚拿到树莓派 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 看一遍原始波形永远是第一步,很多藏着的数据问题,光靠数字终端真的发现不了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询