ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从电位器到波形:树莓派Pico ADC采样与PWM控制实战

2026/9/12 12:01:37 拓冰建站 浏览量
从电位器到波形:树莓派Pico ADC采样与PWM控制实战 上周末我把一只 B10K 线性电位器接到树莓派 Pico 的 GPIO26 上同时用 PWM 口驱动一个 5mm 指示 LED然后打开 SerialPlot 盯着串口波形看了半个多小时。旁边同事问了一句这曲线有啥好看的我当时的回答是这条曲线背后其实是模拟电压 → 数字码值 → 软件滤波 → 输出控制 → 数据回传 → 上位机可视化这整条嵌入式数据链路的缩影。电位器只是引子真正值得写下来的是从硬件接线到代码再到波形验证的每一个细节以及踩过的那些坑。这篇博文就是这份实战记录的完整版。这套实验不需要昂贵的仪器一块 Pico、一只电位器、一颗 LED、几根杜邦线就够了。适合刚开始接触单片机的学生、想搞明白 ADC 采样原理的嵌入式爱好者也适合那些已经会点灯但想进一步把模拟世界接进代码里的朋友。文章里我会把 ADC 硬指标、硬件接线、MicroPython 固件、SerialPlot 配置、电压校准、PCB 布局注意事项全部串起来讲你可以照着一步步复现也可以只挑感兴趣的部分看。1. 从电位器到屏幕上那条曲线的完整链路1.1 这套实验到底在模拟什么真实场景先把目标说清楚。你旋转电位器实际上是在改变一个分压点的电压这个电压从 0V 到 3.3V 之间连续变化。Pico 的 ADC 把这段连续电压离散成数字量代码再根据数字量计算当前电压、控制 LED 亮度最后把采样值通过串口发给电脑。SerialPlot 收到数据后实时画出一条波形你就能在屏幕上看见自己手指转动电位器的过程。这个链路听起来简单但它覆盖了嵌入式开发里最常见也最重要的几件事模拟信号的采集、ADC 的量化和精度、PWM 对执行器的控制、串口协议的封装、上位机数据可视化。你之后做传感器读取、做舵机角度映射、做电压监测、甚至做数据记录仪本质上都是这条链路的变体。很多人在单独学 ADC 或者单独玩串口的时候都能跑通但把它们串在一起时才会发现各种问题比如数据跳动、波形毛刺、串口数据解析失败、LED 亮度变化不自然等等。1.2 适合谁来跑、跑完能留在手里的东西这个项目对初学者非常友好因为硬件只有三个元器件电位器、电阻、LED。软件方面也不需要配置复杂的 SDK用 MicroPython 写几十行代码就能跑起来。对有一点基础的人来说这篇文章里关于滤波方式选择、Gamma 校正、电压校准、硬件 UART 与 USB 虚拟串口取舍、PCB 布局的部分反而更值得细看。跑完整个项目你的收获不是简单的点了个灯而是理解了一个可以复用的调试方法无论以后做温湿度传感器、光敏电阻、还是电机电流采样用传感器输出 → ADC 采集 → 串口 → SerialPlot 画波形这条链路来验证硬件和算法永远比只靠串口打印几个数字高效得多。你可以直接观察到信号是否干净、滤波是否有效、校准是否准确。2. Pico 的 ADC 资源与硬件接线里的每一根线2.1 RP2040 ADC 的硬指标和引脚分配树莓派 Pico 用的是 RP2040 芯片内置一个 12 位的逐次逼近型SARADC理论采样率最高到 500ksps。这里要注意一个很容易被忽略的点虽然 ADC 是 12 位分辨率但 MicroPython 的read_u16()返回的是 16 位无符号整数也就是把 12 位结果左移了 4 位映射到 0~65535 范围。所以很多教程里你会看到ADC 值范围是 0~65535千万别和真正的 12 位范围搞混后面换算电压时要按 16 位来处理。RP2040 实际可用的外部 ADC 通道有三个GPIO26 对应 ADC0GPIO27 对应 ADC1GPIO28 对应 ADC2。另外还有一个内部 ADC4 通道连接到芯片内置的温度传感器本次实验用不到。输入电压范围是 0~3.3V如果接超过 3.3V 的电压会直接损坏芯片在搭电路之前一定要用万用表确认电位器抽头的电压范围。2.2 电位器分压阻值选型和接线顺序电位器推荐选 B10K也就是 10kΩ 线性锥度电位器这里的关键是线性B 型。如果误用 A 型对数锥度电位器你从波形上看到的就不是均匀变化的直线而是曲线虽然也能用但对理解 ADC 分压有干扰。接线非常简单但顺序有讲究。电位器有三个引脚通常把 1 脚接 Pico 的 3V33 脚接 GND中间抽头 2 脚接 GPIO26。这样旋到一端是 0V另一端是 3.3V中间连续变化。如果你反接也没关系只是旋钮方向和电压变化方向相反影响不大。但有一个细节值得注意电位器的输出阻抗会直接影响 ADC 采样稳定性。10kΩ 的阻抗不算太高但也不算低它对 ADC 内部采样电容的充电时间有一定影响。如果你发现读数在旋转停止后仍有缓慢跳动可以在 GPIO26 与 GND 之间并联一个 0.1uF 的陶瓷电容形成低通滤波能明显减少高频噪声耦合。2.3 LED 限流电阻计算与 PWM 引脚选择LED 不能直接接在 GPIO 和 GND 之间必须串联限流电阻。用欧姆定律算一下Pico 的 GPIO 输出高电平为 3.3V普通红色 LED 的正向压降约 2.0V如果目标电流是 5mA那么电阻值为(3.3 - 2.0) / 0.005 260Ω。取标准系列里的 330Ω 很合适如果手头只有 470Ω 也可以用亮度略低但更省电不会烧坏 LED。PWM 引脚我选了 GPIO16这个引脚在 Pico 上有 PWM 功能而且和 ADC 输入引脚 GPIO26 之间物理距离较远可以降低数字开关噪声对模拟采样的干扰。这是我个人焊接和布线时的习惯数字信号引脚尽量离模拟采样引脚远一点。PWM 频率设为 1000Hz 就足够人眼基本看不出闪烁同时又不会因为频率过高引入额外的电磁干扰。2.4 面包板和洞洞板上的电源与地线处理如果你是在面包板上插接要注意杜邦线接触不良的问题。电位器中间抽头那根线如果没插紧波形会出现大量毛刺这个问题很隐蔽因为从面包板上看线头似乎都进去了但实际内部金属弹片可能没夹住。我的习惯是每一根杜邦线插好后轻轻往外拔一下感觉到明显阻力才算接触到位。如果计划焊接到洞洞板有两点经验供参考。第一地线尽量短而粗模拟部分和数字部分的地先分别汇接再在电源入口处单点相连。第二给 3V3 和 GND 之间加一个 10uF 电解电容和 0.1uF 陶瓷电容并联去耦放在电位器附近而不是放在板子角落。Pico 通过 USB 供电时板上稳压器输出的 3.3V 并非绝对干净LED 每毫秒开关一次就会在电源线上产生微小纹波这个纹波会直接影响 ADC 的参考电压导致读数跳动。去耦电容的作用就是给这些高频纹波提供一条低阻抗泄放路径。3. MicroPython 固件采样、滤波、Gamma 映射一整套3.1 为什么拿 MicroPython 而不是 C SDK这个问题我在项目开始前也要纠结一下。C SDK 的性能更好能精确控制采样周期甚至用 DMA 把 ADC 数据直接搬进内存但代价是开发周期长、调试麻烦。对一个以搞懂原理和验证方案为目标的项目来说MicroPython 完全够用开发和调试速度会快一个量级。具体到代码层面MicroPython 的machine.ADC和machine.PWM把寄存器操作全部封装好了你只需要关心引脚号和取值范围。性能方面read_u16()一次调用大概耗时几十微秒到一百微秒对 50Hz 级别的电位器采样来说绰绰有余。如果你未来要做音频采样或者高速数据采集那时候再切换到 C SDK 也不迟因为实验过程中积累的对 ADC 特性的理解完全是可以迁移的。3.2 软件滤波滑动平均 vs 中值ADC 原始读取值通常不稳定我在实验里测到电位器保持静止时相邻两次读数的跳动量大约在正负 10 到 30 个 LSB以 16 位码值计算这种跳动在波形图上表现为一条带毛刺的曲线。要处理这种随机噪声最常用的是滑动平均滤波或中值滤波。滑动平均的思路是维护一个长度为 N 的缓冲区每次取平均作为输出。优点是平滑效果好缺点是有滞后旋钮快速转动时波形会显得钝。中值滤波则是取一组样本的中间值对尖峰脉冲噪声抑制效果非常好但计算量稍大。对于电位器这种信号我最终选择滑动平均窗口长度为 8。因为电位器接触噪声主要是随机小幅跳动滑动平均就够用了如果用了带脉冲干扰的环境我会改成先取 5 个样本中位数再对中位数做滑动平均的复合滤波方式不过这次实验没必要搞那么复杂。3.3 人眼不是线性的LED 亮度 Gamma 校正这一步是很多人会忽略的。如果不做任何处理直接用滤波后的 ADC 值作为 PWM 占空比你转动电位器时会发现前半段 LED 亮度变化非常快到达中间位置后亮度几乎看不出变化到后半段又突然变亮。这并不是 PWM 或者 ADC 出了问题而是人眼对亮度的感知近似于对数关系。简单说人眼对暗部亮度变化更敏感对亮部变化不敏感。如果用 0~65535 的数值直接驱动 PWM亮度并不会均匀地随着数值线性变化。解决方法是做 Gamma 校正把输入值做一次幂函数变换输出 输入^2.2。在单片机里实时算浮点幂函数不太划算更常见的做法是预生成一张 256 项查找表每次采样后取 ADC 值的高 8 位作为索引查表得到校正后的 PWM 占空比。这样既保证了视觉上的均匀过渡又不会拖慢采样循环。3.4 可复现的完整代码和部署方式下面这份代码是我在 Pico 上实测过的完整版本关键逻辑都加了注释from machine import ADC, PWM, Pin import time ADC_PIN 26 # GPIO26 ADC0 LED_PIN 16 # GPIO16 硬件PWM # 初始化 ADC 和 PWM adc ADC(ADC_PIN) led PWM(Pin(LED_PIN)) led.freq(1000) # 滑动平均滤波缓冲区 FILTER_SIZE 8 samples [0] * FILTER_SIZE idx 0 def read_adc_filtered(): global idx samples[idx] adc.read_u16() idx (idx 1) % FILTER_SIZE return sum(samples) // FILTER_SIZE # 预生成 Gamma 2.2 查找表 def build_gamma_lut(): lut [] for i in range(256): value int(((i / 255) ** 2.2) * 65535) lut.append(value) return lut LUT build_gamma_lut() # 让设备稳定一下避免启动时的乱码污染串口波形 time.sleep(1) while True: raw read_adc_filtered() voltage raw * 3.3 / 65535 # 亮度查表取16位ADC值的高8位作为索引 brightness_index raw 8 duty LUT[brightness_index] led.duty_u16(duty) # 串口输出三通道数据供 SerialPlot 绘图 print({},{:.3f},{}.format(raw, voltage, duty)) time.sleep_ms(20)如果你希望数据通过 GPIO0/GPIO1 的硬件 UART 发送而不是走 USB 虚拟串口可以这样初始化from machine import UART, Pin uart0 UART(0, baudrate115200, txPin(0), rxPin(1)) # 在 while 循环里使用: # uart0.write({},{:.3f},{}\n.format(raw, voltage, duty))部署时我建议把代码保存为main.py放到 Pico 里拔掉 USB 重新上电让它自动运行。这样就不会被 Thonny 的 REPL 占用串口。注意main.py开头我特意加了time.sleep(1)是为了等板子完全稳定后再开始打印避免启动时的一些非数据字符混进 SerialPlot 的数据流里。4. 串口输出协议与 SerialPlot 的波形可视化4.1 USB 虚拟串口和硬件 UART0 怎么选Pico 提供了两种串口通道。第一种是板载 USB 虚拟串口只需要一根 USB 线连接电脑在设备管理器里就能看到一个 COM 端口MicroPython 的 print 输出和 REPL 共用这个端口。第二种是硬件 UART0占用 GPIO0/GPIO1需要外接 USB-TTL 转接器才能连接到电脑常见的转接芯片有 CH340、FTDI 等。对本次实验来说USB 虚拟串口是首选因为它零额外硬件、零驱动负担直接就能在 SerialPlot 里看到数据。但有一个限制你不能同时用 Thonny 的 REPL 打开同一个 COM 口必须把 Pico 断电重启后直接跑main.py否则端口被占用SerialPlot 会打不开。硬件 UART0 的优势在于串口通道和调试 REPL 分离。你可以一边在 Thonny 里敲命令一边让程序从 GPIO0 往外发波形数据两边互不干扰。缺点是需要额外接线而且 CH340 这类芯片在某些系统上会有驱动问题。我遇到过 Win7 下 CH340 驱动装不上的情况Win10/11 一般插上就识别Ubuntu 下多数内核也自带驱动但个别发行版需要手动确认。我用这张表给两类用户一个快速判断依据对比项USB 虚拟串口硬件 UART0 CH340额外硬件无USB-TTL 转接器接线仅 USB 线GPIO0 TX 接 RX、GPIO1 RX 接 TX、共地与 REPL 关系共用不能同时占用独立可同时调试波特率影响不敏感虚拟串口两端必须一致常用 115200适合场景SerialPlot 快速验证对接外部 MCU、Unity、传感器模块4.2 数据帧设计分隔符、通道数和帧结束符SerialPlot 支持 ASCII 和二进制两种数据格式。对于 DIY 调试ASCII 格式最省事关键是设计好一帧数据的结构。我的帧格式非常简单一行为一帧每帧包含三个数字用逗号分隔最后以换行符结束。例如32768,1.650,32768第一个值是原始 ADC 码值第二个是换算后的电压第三个是 Gamma 校正后的 PWM 占空比。SerialPlot 在 ASCII 模式下会根据你指定的分隔符拆分成三列然后画出三条曲线。这样的设计有一个好处原始码值、物理量、控制量同时显示当你旋转电位器时可以直观对比三者的变化关系。有几个细节需要注意。第一一帧里不要混入任何字符串说明文字比如adc32768这种格式SerialPlot 解析字符串会失败或者在图上产生毛刺。第二帧结束符用\nSerialPlot 默认按行读取print 自带换行正好匹配。第三如果你要发小数控制好小数点位数我习惯保留三位既够用又不会让数据行太长影响传输效率。4.3 SerialPlot 关键配置步骤SerialPlot 是一款开源工具在 GitHub 和 SourceForge 上都可下载。安装后按以下步骤配置打开 SerialPlot在串口设置区选择正确的 COM 端口波特率选 115200数据位 8无校验停止位 1。确认 Pico 的main.py已经在运行否则点 Open 之后看不到数据流。把显示模式切到 ASCII设置分隔符为逗号,通道数设为 3。点 Open 打开串口再点 Start 开始采集这时绘图区应该出现三条实时曲线。如果波形没有出现先用串口调试助手看一眼原始数据是否正常排除接线和驱动问题。我一开始在 SerialPlot 里看不到波形折腾了半天最后发现是串口被 Thonny 占用了。所以再强调一次如果你用 Thonny 把main.py运行起来看起来代码在跑但 SerialPlot 是连不上那个端口的。正确做法是保存文件后断开 Thonny重新插拔 USB 让 Pico 独立运行再打开 SerialPlot。4.4 从波形上能读出哪些硬件状态波形画出来之后先别急着高兴多看几眼里面的细节。稳定状态下原始 ADC 码值曲线应该是一条比较平滑的直线波动幅度如果在正负几十个 LSB 之内属于正常范围。如果波动呈周期性大概率是电源噪声比如 USB 供电里 50Hz 工频纹波耦合进来了。如果用手指触碰电位器引脚或杜邦线波形会明显变乱这说明外界干扰已经进入了模拟输入端也反过来说明输入端加 0.1uF 电容的必要性。当你快速旋转电位器时波形会出现一个平滑的斜坡上升沿或下降沿的陡峭程度反映了你的采样率和滤波窗口的滞后。如果斜坡上有明显台阶那就是滑动平均窗口带来的量化效果属于正常现象如果出现不规则的尖峰毛刺一般是电位器内部触点接触不良或者接线松动。5. 电压校准与实测漂移原始码值不是最终答案5.1 16 位 ADC 码值到电压的换算公式MicroPython 的read_u16()返回 0~65535对应的输入电压是 0~3.3V换算公式是电压(V) 原始码值 / 65535 * 3.3这个公式看着简单但有两个隐含问题。第一个是理想换算默认 ADC 是完美线性的而实际芯片存在零偏误差和增益误差通常误差在几个 mV 到十几个 mV 级别对一般控制场景可以忽略但对高精度测量不行。第二个问题是 VREF 的实际值。RP2040 内部 ADC 的参考电压来自 3.3V 电源轨当 USB 供电波动时参考电压也会跟着波动这是系统误差的一部分。所以你会发现 Pico 测到的电压和万用表读数之间总是有一点差异这不奇怪。5.2 万用表对照和两点线性修正要让电压读数更准最简单有效的办法是两点校准。具体操作是把电位器旋到接近 0V 的位置记录万用表读数和 Pico 计算值再旋到接近 3.3V 的位置再记录一组数据。有了这两组数据就可以解出修正公式修正后电压 实测电压 * K B其中 K 是增益修正系数B 是零偏修正值。我实测的一组数据如下旋钮位置万用表读数 (V)Pico 计算值 (V)偏差 (V)接近 0%0.0120.008-0.004约 25%0.8230.8260.003约 50%1.6521.650-0.002约 75%2.4742.4790.005接近 100%3.2923.3010.009可以看到在中间区域误差很小但在接近满量程时偏差变大这是增益误差的典型表现。用两端点做一次线性修正之后全量程偏差基本能收敛到正负几个 mV 以内。如果要求更高可以做三点校准通过分段线性插值来逼近 ADC 的非线性曲线思路类似只是标定点更多。5.3 校准前后的 SerialPlot 波形对比把校准前后的电压曲线同时显示在 SerialPlot 里对比效果非常直观。校准前当电位器旋到最大位置时Pico 显示 3.301V而万用表读数是 3.292V差了 9mV。校准后两条曲线几乎重合。我建议你在代码里直接输出两个电压值一个是理想换算值一个是修正后的值。这样在 SerialPlot 上就能实时看到修正量的大小也更方便理解校准的意义。对初学者来说这一个对比能直观说明ADC 读到的数字不等于真实物理量这个重要概念。6. 采样周期、电源噪声与 PCB 布局的踩坑笔记6.1 MicroPython 实际采样周期到底有多快很多人的第一反应是能不能把采样周期降到 1ms让 SerialPlot 上的波形更细腻这里要说清楚 MicroPython 的瓶颈。adc.read_u16()本身很快但加上 print 格式化、字符串发送、滑动平均滤波和 PWM 更新之后单次循环的实际耗时可能达到 5ms 到 10ms。也就是说即使你把sleep_ms(20)改成 0实际采样率也很难稳定超过 100Hz 到 200Hz。我在调试时试过删掉所有 sleep结果 SerialPlot 上看到的曲线出现了明显的时间间隔不均匀这是因为每轮循环的耗时不完全固定print 偶尔会遇到 USB 调度延迟。对于电位器这种缓慢变化信号50Hz 到 100Hz 的刷新率完全足够没必要追求高采样率。如果你想做真正的高速采样比如采集音频或者振动信号唯一靠谱的路径是切换到 C SDK用 ADC 的 FIFO 和 DMA 功能。RP2040 的 ADC 最高支持 500ksps但前提是你不能用 MicroPython 一层层封装好的 API而要通过寄存器配置连续采样模式把数据通过 DMA 搬到内存之后再统一处理。这部分内容展开是另一篇文章这里只提醒大家先评估自己的信号频率不要盲目追求高速。6.2 ADC 值跳动的三个主要来源我在实验里遇到 ADC 值跳动排查下来主要有三个来源按出现频率排序如下。第一是电源噪声。USB 供电的 5V 经过 Pico 板载稳压器得到 3.3V这个 3.3V 同时给 ADC 提供参考电压和给 LED 的 PWM 供电。LED 以 1kHz 的频率开关时会在 3.3V 电源轨上产生周期性纹波ADC 的参考电压跟着波动采样值自然会跳动。解决方法是给 3.3V 和 GND 之间加去耦电容并且尽量让 PWM 走线和 ADC 输入走线分开。第二是输入引脚的电磁耦合。从电位器抽头到 GPIO26 的杜邦线如果太长又没有地线屏蔽就会像一根天线一样接收环境中的电磁噪声。缩短引线、把线束绞合、在 GPIO26 对地加 0.1uF 电容都能有效改善。第三是电位器自身的接触噪声。碳膜电位器在转动时触点与碳膜的接触电阻会发生变化产生随机尖峰。这种噪声即使你用再好的电源也滤不掉只能通过软件滤波解决。我前面选的滑动平均滤波对这类噪声有一定抑制如果尖峰特别严重换成中值滤波效果更好。6.3 ADC 前端 RC 滤波与三个 PCB 布局要点这次实验用的是面包板但我把洞洞板/PCB 上的布局经验也一起整理出来因为很多人在面包板上跑通后下一步就会想做一个正经的板子。首先ADC 输入前加一个简单的 RC 低通滤波器电阻取 100Ω 到 1kΩ电容取 0.1uF截止频率大约在 1.6kHz 到 16kHz 之间可以有效滤除高频噪声。但要注意电阻太大会降低 ADC 前端采样电容的充电速度反而影响精度所以这个电阻不能盲目加大。其次三条 PCB 布局要点对所有 ADC 电路都适用。第一模拟地和数字地要分区最后在电源入口处单点汇接不要让大电流的数字地回流穿过模拟采样的地平面。第二ADC 输入线尽量短而粗周围不要走高频数字信号线特别是 PWM 线和串口线。第三参考电压引脚附近一定要放置去耦电容通常是 1uF 和 0.1uF 并联并且电容要尽量靠近引脚远离电感性的过孔和长走线。这些经验并不只属于 RP2040任何内置 SAR ADC 的 MCU 都适用。你在 Pico 上验证过的电路思路过几天换到 STM32、ESP32 上依然能复用。我早年做 STM32 的 ADC 采集时就是因为没注意地平面分割模拟部分和数字部分的地在芯片下方交织导致采样值在电机启动瞬间出现明显跳动后来把所有地统一汇接到一个点才解决。6.4 下一步扩展舵机、Unity 串口与外部 ADC这个项目做完后自然的扩展方向有三个我根据自己的经验列出来。第一个是舵机控制。Pico 控制舵机需要 50Hz 的 PWM 信号脉宽一般在 0.5ms 到 2.5ms 之间。你只需要把 ADC 采集到的 0~65535 映射到对应脉宽的占空比就能用电位器实时控制舵机角度非常适合做云台或机械臂的手动控制。热搜里反复提到树莓派pico控制舵机其实基础就是这一套ADC 读角度、PWM 输出控制信号。第二个是 Unity 串口通信。SerialPlot 只是把数据画成曲线你完全可以用同样的串口数据格式在 Unity 里写一个串口读取脚本把 ADC 值映射成虚拟场景里物体的位置或旋转角度。这个玩法在很多交互项目里非常受欢迎数据链路完全不用改只需要在电脑端把 SerialPlot 换成自己的解析程序。第三个是外接更高精度的 ADC 芯片比如 ADS1115。Pico 片内 ADC 是 12 位在绝大多数控制场景下够用但如果你要做精密电压测量或传感器信号调理外接 16 位 ADC 是常见的升级路径。外接 ADC 通常需要 I2C 或 SPI 通信校准流程也会更复杂常见做法是三点甚至多点校准来修正非线性误差。我个人的看法是先把这个基础项目彻底吃透把示波器般的调试习惯养成再去碰舵机、Unity、外部 ADC 这些更复杂的玩法。毕竟无论是控制舵机还是驱动彩屏数据进 MCU 的第一站永远是 ADC 或数字接口而数据出 MCU 的最后一道关卡永远是稳定可靠的串口协议。Pico 这块小板子虽然便宜但该讲的硬件原理和软件工程思维一样都不少很适合作为嵌入式入门的第一块实验田。