树莓派Pico ADC采集实战:从电位器到MicroPython滤波与SerialPlot可视化

树莓派Pico ADC采集实战:从电位器到MicroPython滤波与SerialPlot可视化 1. 为什么选 Pico 做 ADC 采集性价比与学习曲线最近在玩树莓派 Pico顺手把一个模拟电位器接上去做了 ADC 采集配合 LED 亮度控制和 SerialPlot 串口波形可视化整套流程走下来很顺在这里把完整的实战过程记录下来。如果你正在学习嵌入式开发或者想搞懂模拟信号到数字量之间的转换过程这篇文章应该能帮你把整个链路打通。先说为什么选 Pico 而不是 Arduino 或者 STM32。Pico 用的是 RP2040 芯片片上集成了 3 路 12 位 ADC 输入采样率能到 500kSPS对这个场景来说绰绰有余。更关键的是Pico 支持 MicroPython 和 C/C 两套开发方式我的建议是新手先用 MicroPython 打逻辑后续再切到 C 做底层优化这样学习曲线最平滑。STM32 的 HAL 库虽然功能强但光初始化配置就能劝退一批新手Arduino 的 ADC 大多数板子是 10 位的同样的电位器Pico 能给出 0 到 4095 的分辨率Arduino Uno 只能给到 0 到 1023精度差了四倍。Pico 的 ADC 引脚分布在 GP26、GP27、GP28 三个通用引脚上分别对应 ADC0、ADC1、ADC2另外还有一个芯片内部温度传感器也接在 ADC 通道上。这意味着如果你想做多路模拟信号采集只需要多接几个电位器或者传感器每个模拟量占一个 ADC 通道互不干扰。顺带提一句如果你是做音频采样或者波形分析的项目Pico 的 ADC 可以配合 DMA 实现高频连续采样性能完全扛得住。不过那是进阶玩法这篇我们先聚焦在最基础的电位器采集上把 ADC 的工作原理和工程实现走通再说。2. 硬件连接与分压原理电位器为什么这样接2.1 接线前的关键认知ADC 不能直接测电压很多新手拿到电位器就直接往 Pico 上一怼三个引脚随便接结果读出来的数据要么一直是 0要么飘到 4095 然后卡住不动。原因很简单ADC 的输入引脚虽然有高阻特性但它要求输入电压严格落在 0V 到参考电压VREF之间超出这个范围轻则削波失真重则损坏引脚内部的保护二极管。Pico 的 ADC 参考电压默认是 3.3V所以你的模拟输入信号绝对不能超过 3.3V。这一点务必牢记很多开发板烧 ADC 引脚就是在这里翻车的。电位器作为一个可变电阻分压器正好可以提供 0 到 VCC 之间任意比例的电压值所以我们把电位器的两端分别接到 3.3V 和 GND中间抽头通常标记为 Wiper接到 Pico 的 ADC 引脚。2.2 完整接线清单我用的是 10K 旋转电位器B10K 型这一型号是线性锥度的输出与旋转角度成线性关系适合做精准的模拟量调节。接线方式电位器 1 脚一个固定端接 Pico 的 3V3物理 Pin 36电位器 2 脚中间抽头接 Pico 的 GP26 / ADC0物理 Pin 31电位器 3 脚另一个固定端接 Pico 的 GND物理 Pin 38电阻 220ΩGP15物理 Pin 20串联至 LED 阳极LED 阴极接 GND注意LED 和 ADC 这两条链路独立供电、独立回流不要共用一根杜邦线。我在第一次搭建时偷懒把 LED 的 GND 和电位器的 GND 走同一根面包板总线结果 LED 亮灭瞬间 ADC 读数出现 ±5mV 左右的波动虽然不影响大局但确实能观察到串口数据上的轻微毛刺。最好是各自独立接一条线到 Pico 的 GND 引脚完事后你会发现波形干净很多。顺便说说 LED 为什么要在 GP15 上控制纯属个人习惯。GP25 是板载 LED但我想保留它做系统状态指示所以外接一个 LED 用来做模拟量的可视化输出。GP15 离 GND 比较近接线顺手就选它了。2.3 电位器分压的数学关系B10K 电位器内部等效为一个 10KΩ 电阻加上一个可滑动触点。当旋钮转到某个角度 θ 时中间抽头输出的电压满足V_out VCC × (R_bottom / (R_top R_bottom))其中 R_top 是抽头到 3.3V 端子的电阻R_bottom 是抽头到 GND 端子的电阻两者之和恒等于电位器的标称阻值 10KΩ。旋转角度从 0° 到 300°机械转角V_out 从 0V 线性变化到 3.3V。Pico 的 ADC 将这个电压映射为 12 位数字量ADC_Value (V_out / 3.3V) × 4095所以当你把旋钮拧到中间位置时理论读数约等于 2047 或 2048这是一个验证 ADC 是否正常的快速自检方法。3. MicroPython 代码实现从原始读到滤波再到 PWM 控制3.1 读取原始 ADC 值的二种写法MicroPython 的 machine 模块提供了 ADC 类初始化方式有两种我分别说明。第一种通过构造器传入引脚对象from machine import Pin, ADC adc_pin machine.Pin(26) adc ADC(adc_pin) value adc.read_u16() print(value)第二种直接通过引脚编号构造adc ADC(26) value adc.read_u16()注意一个很多新手踩过的坑read_u16() 返回的是一个 16 位无符号整数范围是 0 到 65535而 RP2040 的 ADC 硬件是 12 位的0 到 4095。MicroPython 底层帮你做了左移对齐把 12 位的数据左移了 4 位凑成一个 16 位值。所以你在 read_u16() 结果上右移 4 位才能拿到真实的 12 位 ADC 原始数据。这一点容易引发混乱。如果你想要真正反映硬件精度的原始值建议用 read_u16() 4 的写法。后续的数值映射需要基于 0 到 4095 的范围来算不然你按 65535 去做归一化中间多出来的精度纯粹是 MicroPython 刻意填充的没有实际意义。3.2 滑动平均滤波让数据稳定下来直接从 ADC 读出来的数值是有噪声的。电位器内部的碳膜电阻本身存在热噪声加上面包板上的电磁干扰原始读数即使你旋钮不动跳变幅度也能有几十个单位。几十个单位是什么概念4096 的分辨率下几十个单位相当于 0.05V 左右的波动用来做 LED 亮度控制可能不明显但如果你要做精度要求高的 PID 控制或者传感器测量这部分噪声就是不可接受的。我采用了滑动平均滤波维护一个固定长度的缓冲区每次读到一个新值就把它加入缓冲区同时弹出最早的一个值然后对整个缓冲区求平均。实现如下class MovingAverageFilter: def __init__(self, size10): self.size size self.buffer [] def add(self, value): self.buffer.append(value) if len(self.buffer) self.size: self.buffer.pop(0) return sum(self.buffer) / len(self.buffer) def reset(self): self.buffer.clear()这里有几个设计细节值得展开说。首先是窗口大小 size 的选取。窗口越大滤波结果越平滑但响应越迟钝。我实测下来 10 个采样点的窗口在响应速度和稳定性之间比较平衡旋钮快速旋转时波形延迟大约一个采样周期约 50ms肉眼几乎感知不到但如果你把它加到 50转动电位器时 LED 亮度变化会有明显的拖尾感这就不太舒服了。其次是 pop(0) 操作的时间复杂度。MicroPython 的列表对象对头部弹出操作是 O(n) 的因为要移动后面所有的元素。如果你追求极致效率比如做高频采样可以用 collections.deque 代替。不过在我这个场景下采样率最高也就 100Hz列表方式的性能完全够用。我测试了不加滤波和加滤波后的波形差异。用 SerialPlot 观看不加滤波时波形是一条在中心线上下跳动的粗线加了 10 点滑动平均后跳动幅度从 ±20 个单位缩减到 ±3 个单位左右波形变成一条平滑的细线。效果明显。3.3 PWM 控制 LED把数值变成亮度ADC 读到的 0 到 4095 数值本身没有意义需要映射到 0 到 65535 的范围来控制 Pico 的 PWM 模块。Pico 的 PWM 分辨率为 16 位所以这是一个数值空间转换的问题。from machine import Pin, PWM led PWM(Pin(15)) led.freq(1000) # 1kHz PWM 频率避免可见闪烁 pwm_value int((adc_value / 4095) * 65535) led.duty_u16(pwm_value)PWM 频率为什么选 1000Hz人眼对超过 100Hz 的光闪烁就感知不到了但用手机摄像头录像时低频 PWM 会导致录出来的画面有明暗条纹滚动这是摄像头采样频率与 PWM 频率的混叠效应。1000Hz 是业界常用的经验值既稳定又不会因为频率过高导致 MOSFET 开关损耗变大——虽然 Pico 引脚驱动 LED 时这个损耗几乎可以忽略但养成好习惯总没错。还有一个值得说的点是映射公式的极性。电位器旋到 0V 时LED 应该是熄灭的旋到 3.3V 时LED 应该最亮。这个关系是正比例映射。如果你想让行为反过来旋钮越大灯越暗把公式改成pwm_value int((1 - adc_value / 4095) * 65535)这类逻辑虽然简单但在实际项目里经常出bug需要在需求阶段就明确旋钮增大到底代表什么。3.4 完整程序骨架把上述内容整合起来得到一个完整的 MicroPython 脚本from machine import ADC, Pin, PWM import time class MovingAverageFilter: def __init__(self, size10): self.size size self.buffer [] def add(self, value): self.buffer.append(value) if len(self.buffer) self.size: self.buffer.pop(0) return sum(self.buffer) / len(self.buffer) adc ADC(26) led PWM(Pin(15)) led.freq(1000) filter MovingAverageFilter(size10) while True: raw_value adc.read_u16() 4 # 转为 12 位 filtered_value filter.add(raw_value) pwm_value int((filtered_value / 4095) * 65535) led.duty_u16(pwm_value) print(filtered_value) time.sleep(0.02)这段代码里 time.sleep(0.02) 最终决定了采样率约为 50Hz每秒钟向串口发送 50 个数据点。这个频率对电位器旋钮的调节力度来说足够了。如果你要做更精细的调节把 sleep 减到 0.005采样率就能到 200Hz——当然这受限于 MicroPython 解释器的执行速度纯 MicroPython 能稳定跑到 500Hz 已经是极限再高就得用 C SDK 了。实际运行中值得注意的一点不要在循环里同时 print 大量数据又做 PWM 更新。print 本身会阻塞串口发送如果发送速度跟不上你的设置频率缓冲区会堆积导致波形出现周期性断点。解决办法是降低打印频率比如每个循环都更新 PWM但每 2 次循环打印一次或者提高串口波特率Pico 默认可支持到 115200我这套逻辑在 115200 波特率下跑 50Hz 打印完全无压力。4. 滑条绘图用 SerialPlot 把波形搬上屏幕4.1 为什么不用 Arduino 串口绘图器Arduino IDE 自带的串口绘图器大家都用过功能简单粗暴把串口收到的数字解析成折线图。但它的局限也很明显只能显示一个串口数据流而且绘图刷新逻辑相对简单数据点一多就糊成一团。SerialPlot 是一个开源的跨平台串口数据可视化工具支持同时绘制多条曲线、自定义数据格式、打开/保存数据记录文件。它同时也是调试传感器的利器——不管你是做四旋翼姿态解算、心电图采集还是温度曲线观察它都能把串口数据实时渲染成波形。4.2 安装和首通配置SerialPlot 提供了 Windows 和 Linux 版本。下载后解压直接运行可执行文件不用安装。关键配置在 Port 选项里有三项必须和我们 MicroPython 代码中的设置保持一致否则收不到数据选择正确的 COM 口Windows 上在设备管理器中确认 Pico 的虚拟串口号Baud rate 必须设为 115200与 Thonny 或重映射固件中配置的波特率一致数据格式设置为 Text 模式、单个浮点数SerialPlot 默认接受 \n 结尾的 ASCII 数字行4.3 从看不到数据到看到波形的排查过程我在自己电脑上第一次打开 SerialPlot 时屏幕上什么都没有折腾了十几分钟才定位到问题。完整的排查过程分享一下你在遇到同样问题时可以照着查。第一步确认串口数据到底有没有从 Pico 发出来。用 Thonny 的终端窗口运行同一个程序如果 Thonny 里能看到数字滚动说明 Pico 和电脑之间的 USB 链路是好的。我的 Thonny 能正常打印说明问题出在 SerialPlot 这边。第二步检查 SerialPlot 的端口选择。插着 Pico 的情况下Port 下拉框里会出现一个新增的 COM 口比如 COM17。如果看不到这个口要么是 Pico 的 USB 线只有供电没有数据线这种情况很多换一条线试试要么是驱动程序没装好。Pico 自带的是 USB Mass Storage 和 CDC 虚拟串口功能Windows 10 以上版本无需额外驱动老旧操作系统需要装一个 WinUSB 驱动。第三步检查波特率。我这台电脑上曾经装过一个 USB 转 TTL 模块把那个模块的驱动和 Pico 的串口驱动搞混了导致 SerialPlot 打开的是错误的 COM 口波特率也不匹配。后来我直接在设备管理器里把带 CP210x 字样的虚拟串口禁用掉只保留 RP2040 USB问题才解决。第四步SerialPlot 的解析格式必须匹配。如果你在 MicroPython 里 print 的是 0 到 4095 的整数SerialPlot 默认能识别如果你改成 print 带小数点的浮点数也没有问题。关键是每行结尾必须是\n换行符不能是\r\n否则 SerialPlot 会把空行解析成 0 值的点波形底部会出现一串无意义的水平线。4.4 波形解读与参数验证跑通之后SerialPlot 上实时绘制出来的曲线就是 ADC 采集值的时序图。你可以做两个简单实验验证系统工作正常。第一个实验电位器旋钮保持不动观察波形线的粗细。如果线很细波动不超过 2-3 LSB说明滤波生效了如果线较粗且杂乱说明噪声较大需要检查电源是否干净或者滤波窗口是否开得足够大。第二个实验快速旋转电位器一圈观察波形的斜率。波形应当跟随旋钮角度平缓变化不会出现跳变到 4095 再掉回 0 的毛刺。如果出现跳变大概率是电位器内部的滑动触点接触不良这在便宜的碳膜电位器上很常见换个进口的导电塑料电位器就能解决。SerialPlot 的 Time range 参数决定了横轴显示时长。默认是 10 秒如果你在做慢速调节可能需要调整到 30 秒甚至更长观察曲线整体走势更方便。这个参数在界面的右侧面板里可以随时调整不影响采集过程。5. 数据处理进阶ADC 值到工程量的转换与标定5.1 电压值换算从 LSB 到真实世界ADC 给出的 12 位原始值本身没有物理意义你需要把它换算成实际电压值。公式如下V_sensed (raw_value / 4095) × 3.3V比如你读到 raw_value 2047那么 V_sensed (2047 / 4095) × 3.3 ≈ 1.65V。但这里有一个很多人容易忽略的问题Pico 的数据手册上标注的参考电压是 3.3V实际板子上给 ADC 供电的电压可能不是严格意义的 3.3V。我在万用表上测过自己这块板子的 3V3 引脚实际输出是 3.296V。差出来的 4mV 虽然看起来很小但在高精度测量的场景下会带来系统偏差。如果你在做正经的测量项目建议用万用表实测你手里那块板子的 3V3 引脚电压用实测值代替理论值 3.3 代入换算公式精度能提升肉眼可见。5.2 两点标定法消除器件误差电位器旋钮转到物理最低端时ADC 读数不一定是 0可能是 2 或 3转到最高端时ADC 读数可能只有 4090 而不是 4095。这是电位器端子残余电阻和 ADC 输入漏电流共同作用的结果。如果项目对精度有硬性要求可以采用两点标定法把电位器旋到最小记录 ADC 读数 low_raw 和对应实际电压 low_voltage用万用表测抽头对地电压把电位器旋到最大记录 ADC 读数 high_raw 和对应实际电压 high_voltage建立线性映射actual_voltage low_voltage (raw_value - low_raw) * (high_voltage - low_voltage) / (high_raw - low_raw)实测下来我用这种方法算出的电压值与万用表读数偏差在 3mV 以内远优于直接用理论公式的计算结果。这套方法不局限于电位器任何模拟传感器接入 ADC 后都可以先做一次线性标定再投入使用。5.3 把 ADC 值映射到 LED 亮度时的 Gamma 校正如果仔细观察你会发现直接用线性映射控制 LED 亮度时旋钮在低亮度区间的变化很迟钝旋钮转了三分之一LED 几乎没反应过了中间点之后亮度又急速爬升。这是因为人眼对亮度的感知不是线性的而是近似对数的关系。为了让 LED 亮度的变化在视觉上更均匀需要对 PWM 值做 Gamma 校正。标准 Gamma 值为 2.2校正公式linear pwm_value / 65535 gamma_corrected pow(linear, 2.2) pwm_value_corrected int(gamma_corrected * 65535)加入 Gamma 校正后旋钮从 0 开始旋转时LED 亮度变化柔和且线性视觉效果舒适很多。我强烈建议做 LED 调光应用时把这一步加上代码量只多三行但体验提升非常明显。实际上从农业生产到智能家居的调光面板几乎所有 LED 驱动器出厂固件里都内置了 Gamma 查找表就是这个原因。人眼适应亮度变化的能力很强但线性映射在低灰度级下会产生明显的跳阶感Gamma 校正能把 4096 个灰度级的人眼感知均匀化。6. 调试与排错手册从波形异常反推硬件故障6.1 波形卡在 0 或者 4095 的排查链路这是频率最高的故障现象。波形要么是一条贴近 0 的水平线要么是一条贴在 4095 顶部的直线。出现这种情况我建议按以下顺序排查。先量电位器两端电压用万用表测电位器 1 脚和 3 脚之间的电压如果是 0V说明 3.3V 电源有问题检查 Pico 的 3V3 引脚是否正常输出如果电压正常且接近 3.3V那问题出在抽头信号没有到达 ADC 引脚。再查接线把万用表打在通断档表笔一端接触电位器中间抽头另一端接触 Pico 的 GP26 引脚听蜂鸣声确认导线连通。面包板上的接触不良经常引发这种问题换个插孔往往能解决。最后检查引脚映射确认你没把电位器接到 GP27 或 GP28 上然后初始化代码里用的是ADC(26)。这两个不匹配的情况下读到的数据就会一直是 0 或者漂移不定。6.2 波形毛刺多且无规律的排查链路波形不是平滑线而是粗颗粒状的杂乱曲线波动范围超过 30 LSB。排查方向从电磁干扰和接地开始。把这些导致问题的源头都捋一遍面包板供电线过长且与杜邦线平行走线会产生耦合干扰Pico 用 USB 线供电时某些廉价 USB 充电器输出的纹波很大可达 100mV 以上直接污染了 3.3V 电源轨面包板的金属接插件氧化产生非线性接触电阻解决方法是从电脑的 USB 口直接取电电脑 USB 的纹波比充电器小两个数量级给 3.3V 和 GND 之间跨接一个 100uF 电解电容和 0.1uF 陶瓷电容做去耦线缆尽量用带屏蔽的双绞线信号线远离电源线。完成这三步后原始 ADC 读数的跳动能缩小到 ±3 LSB 以内效果很理想。6.3 LED 不随电位器变化而变化的排查链路如果你的代码逻辑没问题、ADC 也在正常读数但 LED 亮度就是恒定不变问题大概率出在 PWM 初始化和引脚映射上。检查要点如下PWM 初始化代码中PWM(Pin(15))里的引脚号必须和实际连接 LED 的 IO 口一致duty_u16 的输入范围是 0 到 65535如果你传入了 4095 以内的数值LED 只会在一段极小的亮度区间内变化看起来像不变化LED 的限流电阻选大一点220Ω 在 3.3V 下电流约为 (3.3-2.0)/220 ≈ 6mA亮度适中如果你用 1KΩ 电阻电流只有约 1.3mALED 亮度和不亮几乎无法区分之前有个朋友抄我的代码把 LED 接在 GP25 上板载 LED代码里却初始化的是Pin(15)搞了半天发现 GPIO 对不上号。GP25 这颗板载 LED 在原理图上标注为 LED但它内部是直接连到芯片的 GPIO 上的不能像普通 GPIO 一样掌控自由度。建议外接 LED 时优先选扩展出来的引脚避开板载 LED。6.4 SerialPlot 数据显示台阶状而非连续平滑曲线这个现象很有意思。波形不是连续的折线而是一格一格的台阶状。其实这预示着你的采样频率与电位器转动速度不匹配。当电位器快速转动时单次采样之间的模拟电压变化幅度足够大ADC 每次采集的值本身就分阶了当转动速度较慢时ADC 数值每次变化 1 或 2 个 LSB在较粗的 Y 轴刻度下看起来就是台阶。这不是故障而是数字化的必然结果。如果你希望台阶感更弱、波形更顺滑可以在滤波缓冲区上做一次线性插值或者提高采样率到 200Hz 以上。如果是 MicroPython 纯软件做高频采样遇到瓶颈可以考虑用 C 固件或直接用 Pico SDK 开发采样率能上万级提升。7. 一个完整的可视化实验波形采集 LED 亮度跟随结合前面的所有模块我写了一个稍复杂的演示程序让它既能在 SerialPlot 上画波形又能让 LED 亮度实时跟随电位器同时串口还能输出滤波前后的对比数据。from machine import ADC, Pin, PWM import time, math class MovingAverageFilter: def __init__(self, size10): self.size size self.buffer [] def add(self, value): self.buffer.append(value) if len(self.buffer) self.size: self.buffer.pop(0) return sum(self.buffer) / len(self.buffer) adc ADC(26) led PWM(Pin(15)) led.freq(1000) filter_raw MovingAverageFilter(size5) filter_smooth MovingAverageFilter(size20) while True: raw_value adc.read_u16() 4 # 原始值滤波保留快速响应 t filter_raw.add(raw_value) # 平滑滤波用于观察 smoothed filter_smooth.add(raw_value) # Gamma 校正后输出到 LED linear smoothed / 4095.0 gamma math.pow(linear, 2.2) led.duty_u16(int(gamma * 65535)) # 向串口输出两个值用于 SerialPlot 双通道显示 print({}, {}.format(t, smoothed)) time.sleep(0.01)这里使用了两组滤波器一组窗口很小的用于快速响应控制端一组窗口较大的用于平滑显示制造视觉上的柔和效果。你完全可以根据实际需求调整窗口大小。SerialPlot 那边需要在数据格式设置里选择 Two values per line, comma separated然后就能同时看到两条曲线一条带毛刺的原始滤波值线和一条非常平滑的处理后曲线。两条线的对比效果非常直观特别适合写教程时演示滤波器的效果。这个程序的采样率约为 100Hz每行输出两个数字数据量不大在 115200 波特率下传输不会丢帧。如果你要同时显示 4 个通道的数据也只需要在 print 语句里用逗号分割追加数字SerialPlot 支持的通道数足够多。8. 后续扩展方向从电位器到真实传感系统跑通这个项目后你的熟练度应该已经覆盖了模拟信号采集的全链路硬件接线、ADC 初始化、滤波处理、数值映射、可视化调试。下一步可以往这几个方向扩展。把电位器替换成其他模拟传感器比如光敏电阻或热敏电阻。原理一样它们都是将物理量变化转化为电阻值变化再用分压电路转成电压。接上之后用 SerialPlot 观察波形随光照或温度的变化一套代码就能走通。我试过用 BME280 数字温湿度传感器做对比但那走的是 I2C 总线不需要 ADC。真正能发挥 ADC 价值的是那些输出的模拟量的传感器比如老式模拟麦克风模块、压电震动传感器、FlexSensor 弯曲传感器等。它们的原始信号是连续变化的电压用 ADC 采集后你可以做各种后处理阈值判断、包络检测、FFT 频谱分析等。另外一个值得尝试的项目是用 Pico 采集心电信号ECG。虽然需要额外的仪表放大器做前端信号调理但 ADC 链路完全复用这篇文章的知识。采集到的 ECG 波形放到 SerialPlot 上看效果相当震撼也适合作为物联网健康监测项目的出发点。如果你对舵机控制感兴趣这个项目也能顺带玩起来。电位器调节出的 ADC 值映射到舵机的 PWM 脉宽通常 500us 到 2500us就能实现旋钮转动实时控制舵机角度的效果。上面说的 Gamma 校正思路在这里依然适用只不过 PWM 的调制对象从 LED 变成了舵机。至此Pico 的 ADC 采集从硬件连接到软件滤波、从波形可视化到 LED 调光的完整链路已经跑通了。这只是 Pico 模拟世界的起点接下来就看你想采集什么信号了。