免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

树莓派Pico ADC采集与PWM控制:从电位器到SerialPlot可视化实战

树莓派Pico ADC采集与PWM控制:从电位器到SerialPlot可视化实战 1. 项目概述与硬件准备1.1 这个项目到底在做什么手头有一块树莓派 Pico 开发板想玩点模拟量采集这应该是很多嵌入式初学者绕不开的一关。Pico 作为一块功能极其丰富但价格相当友好的开发板官方资料里最容易被忽略却又最实用的功能之一就是 ADC模数转换。这次的项目核心很简单通过 Pico 的 ADC 接口读取一个模拟电位器旋钮变阻器的电压值把电压值映射为 LED 的亮度变化同时将采集到的数据通过 USB 串口发送到上位机用 SerialPlot 这个免费工具把波形画出来实时观察 ADC 采样的动态响应。这个看似“三合一”的小项目其实把嵌入式开发里几个最基础也最重要的环节全串起来了模拟信号采集、数字量转换、PWM 输出控制、串口通信、上位机数据可视化。把这套流程跑通后面再去做传感器采集、PID 控制调试、信号分析基本就会顺手很多。项目适合谁如果你是刚接触微控制器、想理解 ADC 原理和实际用法的新手或者已经玩过 GPIO 点灯、想进一步提升的老手又或者正在做某个需要采集模拟信号并观察波形的实际项目但不知道从哪里下手这套流程都能给你一个可以直接复现的完整方案。1.2 硬件清单与接线方法实物接线非常简单任何一个入门的玩家手头基本都有这些零件硬件型号/规格数量主控板Raspberry Pi PicoRP20401电位器10KΩ 旋转式也可以用滑动变阻器1LED普通 5mm 红/绿/蓝均可1限流电阻220Ω1面包板通用 830 孔1杜邦线公对公/公对母若干若干数据线Micro USB注意要支持数据传输1接线之前先说清楚 Pico 的 ADC 引脚资源。RP2040 芯片内部集成了一共 5 路 ADC 输入其中 3 路被引出到排针上分别是 GPIO26ADC0、GPIO27ADC1、GPIO28ADC2还有一路是 GPIO29ADC3它连接到了芯片内部的温度传感器。换句话说实际可用的外部模拟输入就是前三个引脚。官方 ADC 的分辨率是 12 位但 MicroPython 封装后返回的是 16 位精度的数值0~65535这个细节后面会详细说到。具体的接线方案电位器的 1 脚接 Pico 的 3.3V 输出物理引脚 36电位器的 3 脚接 Pico 的 GND物理引脚 38电位器的 2 脚中间抽头接 Pico 的 GPIO26物理引脚 31LED 正极通过 220Ω 限流电阻接 GPIO16物理引脚 21LED 负极接 GND物理引脚 23这里有个非常关键的细节Adafruit 版本的 Pico 板卡上引脚丝印会有不同但标准 Pico 的排针布局是一致的。接线前最好用万用表确认一下电位器中间脚到底是不是活动的那个触点如果接反了中间抽头和固定端代码还是能跑但你拧旋钮的时候会发现 ADC 读数不变那就是接错位置了。提示Pico 的 ADC 输入范围是 0~3.3V不要直接给超过 3.3V 的电压否则有可能烧毁引脚。电位器接法刚好是一个可调分压电路输出的电压范围天然不会超过 VCC所以这个用法是绝对安全的。1.3 开发环境搭建与 MicroPython 固件刷写既然要用 Pico 做开发绕不开的就是选哪个开发框架。选项主要有三个官方 C SDK、Arduino 环境、MicroPython。对于 ADC 采集和串口通信这类功能三个方案都能做但 MicroPython 优势在于代码量少、实时修改方便、不需要交叉编译器非常适合快速验证想法和做调试。C SDK 性能更强、实时性更稳适合产品化开发。Arduino 则介于两者之间。这里我选择 MicroPython因为它在上位机联调场景下的开发效率是最高的。刷固件流程不复杂按住 Pico 板子上的 BOOTSEL 按钮不放用 Micro USB 数据线连接 Pico 到电脑此时电脑会出现一个名为 RPI-RP2 的移动磁盘从树莓派官网或 MicroPython 官网下载最新的 .uf2 固件文件推荐 rp2-pico-w 或 rp2-pico根据板型选择将 .uf2 文件直接拖入 RPI-RP2 磁盘中拖入后磁盘自动消失板子自动重启此时固件已刷写完成刷完固件以后如果你用的是 Windows 系统很有可能会遇到一个设备识别问题——设备管理器里看到的是未知设备或没有正确识别出串口。这个几乎可以肯定是 USB 串口驱动的问题。Pico 的 MicroPython 固件默认使用 CDC 协议在 Windows 上它会被识别成一个虚拟串口通常不需要额外装驱动就能被识别成 “USB Serial Device”但如果你的系统是老版本 Windows 或者存在冲突可能需要检查一下 CH340 芯片驱动。这里插一句很多 USB 转 TTL 模块用的都是 CH340 芯片网上搜索“CH340串口驱动”能轻松找到官方驱动包。如果 Pico 直接连接后没识别出串口先检查驱动如果识别出的 COM 口带黄色感叹号就是驱动冲突手动更新驱动即可。接下来连接好板卡之后我比较推荐使用 Thonny 作为编辑器。它集成了解释器、文件管理、串口终端三个核心功能在“工具-选项-解释器”里选择 MicroPython (Raspberry Pi Pico)端口会自动列出。或者用 VS Code 加 MicroPico 插件也可以但 Thonny 对新手的友好程度是无可替代的。2. ADC 采集原理解析与核心代码实现2.1 电位器分压与 ADC 采样背后的数学逻辑很多人会把 ADC 当作一个无需理解内部原理的“黑盒子”觉得只要调用库函数就可以了。但你要是不知道它在算什么就很容易在某些场景下犯错。ADC 的本质是把连续的模拟电压离散成数字量。Pico 的 RP2040 芯片内部 ADC 参考资料明确说明输入电压范围为 0~3.3V分辨率为 12 位也就是输出 0~4095 的整数值。MicroPython 库函数read_u16()返回的不是 12 位而是将其左移到了 16 位变为 0~65535。写成公式就是ADC 原始值 / 65535 输入电压 / 3.3V所以当你读取到一个值raw时对应的实际电压电压 (V) raw * 3.3 / 65535这一点是很多新手容易踩坑的地方如果你以为返回值是 12 位的直接把 4095 作为分母算出来的结果偏差会非常大。同时官方文档里也指出RP2040 ADC 的参考电压其实精确值是 3.3V但如果你需要更高精度的测量最好用万用表实测一下板子的 3.3V 引脚电压再代入运算。我实测过几块 Pico3.3V 引脚实际输出多在 3.28V 左右虽然误差不大但在精密测量场景下这个因素必须考虑进去。再说回电位器。电位器的本质是一个可调电阻分压器一端接 3.3V、另一端接地、中间抽头输出电压。当旋钮转到最左端时中间抽头相当于接到 GND 上电压趋近 0V转到最右端时相当于直接接到 3.3V电压趋近满量程。中间位置则按照线性或对数规律变化。所以 ADC 读到的数值本质上就是旋钮角度的映射。这里再延展一个实际项目里常遇到的问题ADC 采样值的噪声。由于电磁干扰和电源纹波的影响ADC 读数并不稳定尤其在旋钮转到中间位置时常常能看到采样值在几个甚至几十个 LSB 之间跳动。解决方式一般有两种硬件上在 ADC 引脚与 GND 之间并联一个 100nF 的电容做低通滤波软件上做多次采样取平均或滑动滤波。对于这个项目软件滤波已经足够后面代码里会体现。2.2 核心代码ADC 读取、电压换算与软件滤波下面给出完整的 MicroPython 代码。这段代码把 LED 亮度控制、电位器读取、串口输出全部整合到一起逻辑非常清晰每一段注释在做什么、为什么这么做都写出来了。from machine import Pin, ADC, PWM import time import math # ------------------- 引脚初始化 ------------------- # 电位器信号线接 GPIO26对应 ADC 通道 0 adc_pin ADC(Pin(26)) # LED 接 GPIO16必须使用 PWM 模式才能实现亮度渐变 led_pin PWM(Pin(16)) led_pin.freq(1000) # PWM 频率 1kHz人眼感受不到闪烁 # ------------------- 滤波相关变量 ------------------- # 滑动窗口滤波窗口大小可以调节窗口越大越平滑但响应变慢 FILTER_WINDOW 10 adc_history [0] * FILTER_WINDOW history_index 0 def moving_average_filter(raw_value): 滑动平均滤波保存最近 N 次采样值并求均值 global history_index adc_history[history_index] raw_value history_index (history_index 1) % FILTER_WINDOW return sum(adc_history) // FILTER_WINDOW # ------------------- 主循环 ------------------- while True: # 1. 采集原始 ADC 值16位范围 0~65535 raw_u16 adc_pin.read_u16() # 2. 软件滤波降低噪声影响 filtered_u16 moving_average_filter(raw_u16) # 3. 计算实际电压值保留三位小数 voltage filtered_u16 * 3.3 / 65535 # 4. 将 ADC 值映射到 LED 亮度PWM 也是 0~65535 范围恰好直接映射 led_pin.duty_u16(filtered_u16) # 5. 通过串口输出数据SerialPlot 用逗号分隔多个通道 # 这里输出两个通道原始 ADC 值和电压值 print(f{filtered_u16},{voltage:.3f}) # 采样间隔50ms即约 20Hz 的采样率 time.sleep_ms(50)代码不长但有几个细节必须重点说明。第一read_u16()返回的是 0~65535这刚好和 PWM 的duty_u16()输入范围完全一致所以可以直接把 ADC 读数赋值给 LED 的 PWM 占空比。这是一个非常巧妙的间接映射你旋动电位器LED 亮度就跟着线性变化。这里 PWM 频率我设置为 1kHz没有选更高的 5kHz 或者 10kHz是因为 1kHz 在不做缓冲驱动的情况下对普通 LED 来说效果已经够了而且更低的频率在视觉上更容易察觉到亮度变化的过程。如果你用示波器观察 PWM 波形1kHz 也能看到明显的方波周期。第二滤波窗口大小的选择。10 次滑动平均对于 50ms 采样周期来说滤波器滞后大约 500ms在视觉上看到 LED 亮度变化时会感觉“柔顺”了一些但旋转速度快的时候会有一种“拖尾”阻尼感。如果你希望实时性更高窗口减到 3 或 5如果希望曲线更平滑可以加到 20 甚至 50。这里没有标准答案完全看你想要的效果。第三串口输出的数据格式。SerialPlot 能自动识别逗号分隔的数值流每一行数据对应一次采样周期。如果数据只有一列SerialPlot 默认单通道如果有多个通道用逗号分隔即可。同时在打印时把电压值格式化成三位小数避免超长数字占用带宽。2.3 把代码跑起来Thonny 运行与常见卡点直接在 Thonny 里打开刚才的代码文件点击工具栏上的绿色运行按钮程序就会在 Pico 上执行。如果一切正常你会看到 Thonny 的 Shell 窗口不断滚动输出类似这样的内容28672,1.445 28750,1.449 28690,1.446旋动电位器数字会同步变化。如果数字完全不动或者只在 0 和 65535 两个极值附近跳变有几种可能电位器接线接错中间抽头没有连到 GPIO26 —— 用万用表量一下引脚电压电位器本身损坏或接触不良 —— 换一个试试Pico 引脚损坏 —— 换 GPIO27 或 GPIO28 重新接线还有一点值得注意当你用 Thonny 点击“停止”按钮后Pico 上运行的 MicroPython 脚本会被中断但如果你拔掉 USB 线再重新插上程序并不会自动运行。要让代码在板子上电后自动运行需要把文件保存为main.py。Thonny 里你可以用“文件 - 另存为”然后选择“MicroPython 设备”把文件名改为main.py即可。这样 Pico 一上电就会自动执行这段代码。3. LED 控制与 PWM 渐变实现细节3.1 为什么用 PWM 而不是直接高低电平很多新手在控制 LED 亮度时想到的第一个方案是模拟调整电压但实际上微控制器无法直接输出一个“可变的电压”来驱动 LEDPWM脉宽调制就是解决办法。PWM 技术通过固定频率、改变高电平在一个周期内所占的比例即占空比来等效改变输出的平均功率。LED 对这种快速开关的光信号存在视觉暂留效应只要频率足够高人眼感知到的就是亮度的变化。用生活化类比来解释你按电灯开关按得足够快眼睛看起来灯的亮度就会介于亮和灭之间——PWM 就是把这个操作自动化了以几千赫兹的频率快速切换电路的通断从而等效控制亮度。如果不用 PWM 而直接输出高电平LED 除了“全亮”就是“全灭”根本没法体现电位器旋转过程中的连续变化。所以 PWM 是这个项目的必然选择。3.2 非对称映射从 ADC 到 PWM 的三种映射策略前面代码里我把filtered_u16直接赋值给duty_u16这是一种线性映射电位器最小 - LED 全灭电位器最大 - LED 全亮。看起来没问题但实际体验中可能会发现一个问题旋钮在刚开始旋转时LED 的亮度变化非常明显但在高亮度区间变化反而感知不强。原因是人眼对亮度的感知不是线性的而是接近对数关系。所以如果想让亮度变化在视觉上更“均匀”可以考虑用指数映射。代码改为# 将 0~65535 映射到 0~1再取平方 normalized filtered_u16 / 65535.0 brightness int((normalized ** 2) * 65535) led_pin.duty_u16(brightness)这样做的效果是低亮度区间变化更细腻高亮度区间变化更平缓人眼感知就更均匀。类似的做法在手机屏幕的亮度调节中也广泛使用。第三种映射是加入死区。当电位器旋转到接近两端极限时ADC 值可能会有微小波动比如 65535 会跳到 65400虽然肉眼难察但在某些精确控制场景下这个抖动会造成输出不稳定。可以加一个边界裁剪if filtered_u16 500: filtered_u16 0 elif filtered_u16 65000: filtered_u16 65535这三种映射策略各有适用场景你甚至可以做成按键切换模式这样同一个项目就能演示多种思路在写博客或者做分享时内容也会丰富很多。3.3 PWM 频率选择的实际意义代码里我用了 1000Hz 作为 PWM 频率但这个选择并不随意。PWM 频率如果太低比如 50HzLED 会明显闪烁因为人类能感知到约 60Hz 以下的光闪烁如果太高比如 1MHz虽然灯不闪了但对于 MCU 来说意味着更高的中断负载且 LED 驱动电路如果有三极管/MOSFET的开关损耗也会增加。1000Hz 是 LED 控制常见的安全区间既不闪烁也没必要往高了调。如果你用的不是 LED 而是直流电机PWM 频率可能需要提升到 10kHz 以上因为电机线圈的感抗会平滑电流低频率会造成明显的转速脉动和噪声。不同负载对 PWM 频率的要求差别很大这一点是实际项目中设计者必须考虑的。4. SerialPlot 串口波形采集实战4.1 为什么串口数据要用上位机可视化有时候你觉得代码逻辑明明是对的但看 Shell 里那一串数字也看不出所以然。频率、范围、趋势、异常抖动这些特征在波形图上一眼就能看出来在纯数字列表里却要盯着看好久。对于传感器数据观测、控制参数整定、信号质量评估这类任务串口波形工具几乎是刚需。SerialPlot 就是一款免费开源的串口数据可视化工具支持多通道实时曲线、数据导出、实时缩放完全满足嵌入式开发中的日常调试需求。类似的工具还有 Arduino 自带的 Serial Plotter、Processing、Python Matplotlib 动态绘图等但 SerialPlot 胜在开箱即用、交互流畅、不需要写任何前端代码。我推荐的组合是Pico 串口输出数据 SerialPlot 实时绘制这套方案在我做传感器调试的时候帮了大忙。4.2 SerialPlot 安装与连接配置到 SourceForge 搜索 SerialPlot 即可下载安装包Windows 版本是免安装的便携版解压后直接运行 exe。打开之后界面比较简洁主要分三个区域连接设置区端口、波特率、数据曲线区、命令发送区。连接配置步骤在 Thonny 中停止当前脚本或者断开 Thonny 与 Pico 的连接因为 Thonny 会占用串口SerialPlot 就抢不到了打开 SerialPlot在端口下拉框里选择 Pico 对应的 COM 口波特率选 115200点击 Connect 按钮这里有个操作顺序的坑如果你在 Thonny 里运行着脚本串口被 Thonny 占用SerialPlot 是连不上的。所以要么用 Thonny 运行后立刻断开 Thonny 的串口连接菜单“运行 - 停止/重启后端进程”要么更优雅的方案是用一个独立文件管理器直接把 main.py 烧进 Pico 里的方法运行这样脚本在 Pico 上独立运行串口资源就不会被 Thonny 抢占。接好之后SerialPlot 会自动识别数据流。默认情况下它会尝试识别一到两个通道由于我们代码里输出的数据格式是数值,数值SerialPlot 会自动识别为两个通道。曲线区会出现两条实时变化的曲线其中一条是原始 ADC 值0~65535另一条是电压值0~3.3。两条曲线看起来形状完全一样只是量程不同。你旋转电位器两条曲线会同步起伏LED 亮度也同步变化整套系统就完整跑通了。SerialPlot 中还可以配置各个通道的颜色、数据点样式、坐标轴范围。默认的自适应坐标轴比较复杂有时候曲线会和上下边界贴合可以手动把纵轴范围调到 0~70000这样曲线就始终在中间位置浮动视觉上更清晰。4.3 采样率、串口波特率与数据吞吐量分析任何串口调试都必须考虑一个底线问题你的波特率能支撑多高的采样率115200 波特率默认 8N1 格式8 数据位、无校验、1 停止位每个字节实际传输需要 10 个 bit。所以有效传输速度是 11520 字节/秒。在 50ms 采样周期下每秒输出 20 次每个数据行大约 10 到 12 个字符数值加逗号加换行每秒的数据量大约在 240 字节左右波特率完全够用。但如果你把采样周期降到 1ms即 1kHz 采样率每秒 1000 行、每行 12 字节就是 12KB/s已经接近 115200 波特率的传输极限。这时候必须提高波特率到 921600或者减少输出通道数量。这里再说一个容易被忽略的问题Pico 的 MicroPython 通过 USB CDC 虚拟串口通信时底层枚举的串口速率设置看起来是 115200但实际传输速度上限由 USB 协议决定通常可以达到 1Mbps 以上。这意味着你可以用更高的波特率比如 921600而不必担心 USB 串口芯片的瓶颈。但如果你用的是外部 USB 转 TTL 模块连接 UART 引脚那么必须严格按照芯片的波特率能力来。4.4 数据可视化进阶多通道对比与动态交互SerialPlot 最有用的功能之一是它能同时绘制多个通道。这意味着你不只可以观察电位器原始数据还可以同时观察滤波前和滤波后的数据对比噪声抑制效果或者同时观察 ADC 数据和 PWM 占空比输出验证整个链路的一致性。把滤波前后的数据一起发出来很简单代码里多加一个变量print(f{raw_u16},{filtered_u16},{voltage:.3f})波形图上就会出现三个通道你能清晰地看到原始数据的毛刺被滤波后抹平的效果。数据可视化不仅能让你“看到”数据更能帮你快速验证算法是否正确。给不懂代码的人展示成果时波形图也远比数字列表更有说服力。再进阶一步SerialPlot 支持把数据保存为 CSV 文件在菜单的 Logging 选项里可以设置保存路径。这样你在调试中采集到的数据可以离线导入 Python 或者 MATLAB 进一步分析。比如你正在采集一个传感器的响应曲线先采集几百个点再离线画出曲线、分析线性度整个过程非常顺手。5. 常见问题、坑点排查与扩展方向5.1 实践中最容易踩的 5 个坑第一串口端口被占用。程序在 Thonny 里运行后SerialPlot 连不上端口或者反过来 Thonny 连不上。这是新手遇到最多的问题。解决思路其实很简单串口同一时刻只能被一个程序占用。调试完代码以后如果想要长期运行并配合 SerialPlot 使用就把代码保存为main.py然后完全断开 Thonny 的连接甚至关掉 Thonny再打开 SerialPlot 连接。第二ADC 数值跳变剧烈。这个问题常见于使用了较长的杜邦线连接电位器或者电位器本身质量一般。解决方法是软件滤波代码里已经实现或者在硬件上并联一个 0.1uF 电容在 GPIO26 与 GND 之间形成一阶 RC 低通滤波。第三LED 不亮或亮度不变。先用万用表测量 GPIO16 引脚在程序运行时的电压旋动电位器看电压是否在 0~3.3V 之间变化。如果电压不变说明代码里 PWM 赋值没有生效多半是引脚号填错。第四串口乱码或数据显示不完整。检查 Thonny 的串口设置和 SerialPlot 的波特率是否一致。理论上来讲 MicroPython 的 print 输出默认走 USB CDC波特率设置对 USB 虚拟串口几乎没有影响但如果诡异问题不断统一设置成 115200 通常能解决。第五电位器旋钮转到底时读数到不了 65535。这是正常现象因为电位器自身的引脚电阻和接触电阻导致电压无法完全达到 3.3V。读数可能在 65000 左右不影响功能。5.2 参数调优经验与“手感”调整这个看似简单的项目其实有很多可以琢磨的调节空间。采样周期、滑动滤波窗口大小、PWM 频率这三个参数的组合直接影响系统表现。如果你希望系统响应更迅速将time.sleep_ms(50)调小到 10ms同时把滤波窗口减到 3如果你希望显示更平滑把采样周期保持 50ms、滤波窗口加到 30这时曲线会非常顺滑几乎没有毛刺。我实际调过几组组合这里给一个参考经验表需求场景采样周期滤波窗口PWM 频率体验效果快速响应试验10ms3100HzLED 跟随旋钮几乎无延迟波形毛刺较多演示效果50ms101kHzLED 顺滑适中波形干净极致平滑100ms505kHz旋钮转动时 LED 有明显阻尼感波形非常圆润这三组方案我都实跑过没有哪组是绝对正确的关键是你想让这个系统呈现出什么样的“手感”。5.3 这个方案还能往哪些方向扩展这套 ADC 采集 串口可视化的基础框架可以横向迁移到非常多的实际场景中。简单列几个我想到的扩展方向。方向一换成其他模拟传感器。比如光敏电阻接在分压电路里、NTC 热敏电阻、麦克风模块输出等只要输出的是 0~3.3V 的模拟电压就能直接接到 GPIO26~28 上复用这套采集代码。方向二加入多通道采集。Pico 有 3 路 ADC插上两个电位器和一路光敏电阻代码里声明三个 ADC 对象在循环里依次读取并打印SerialPlot 上就会显示三条曲线。这可以用于对比不同传感器对同一物理量的响应差异。方向三结合 PID 控制。如果你把电位器当作目标值输入、把某个执行器的当前状态当作反馈值ADC 采集到的数据就是 PID 控制器的输入信号。配合 SerialPlot 观察调节曲线这是很多做平衡车、无人机、温控项目的玩家都在用的调试方式。方向四数据记录与离线分析。SerialPlot 可以保存 CSV 文件拿到数据后可以用 Python 的 matplotlib 画出更专业的图表也可以计算均值、方差、线性度等指标用数据来验证你的硬件表现。方向五扩展到其他 MCU 平台。这套逻辑不仅适用于 PicoSTM32 的 HAL 库 ADC 采集、ESP32 的 analogRead、Arduino 的 analogRead都遵循相同的原理读取原始值 - 换算实际电压 - 滤波 - 串口输出。换平台时只需要替换 API 调用整体架构完全一致。5.4 一点额外的经验心得最后分享一个我在实际调试中的小体会把 ADC 项目的成败标准定义为“能不能看到波形”其实是个很好的心态。如果 SerialPlot 上曲线不平滑你就去调滤波参数如果 LED 亮度变化不自然你就去改 PWM 映射曲线如果数据有周期性干扰你就要找干扰源是不是来自电源纹波。这种“问题驱动”的调试方式能把很多教科书上抽象的概念比如采样率、量化误差、滤波、占空比变成你亲手解决过的实际问题。很多工程上的直觉都是在这样的循环里练出来的。这个项目虽然小但它几乎包含了一个完整的“信号采集-处理-输出-反馈”链路的所有环节值得耐心地做一遍再按自己的需求改一遍你会收获很多程序之外的东西。
返回列表