基于树莓派Pico的简易示波器:低成本便携信号观测方案
2026/7/29 13:44:41 网站建设 项目流程

1. 项目概述与核心价值

最近在捣鼓一些嵌入式的小项目,调试I2C、SPI信号时,手头那台笨重的台式示波器搬来搬去实在不方便。于是萌生了一个想法:能不能用我抽屉里吃灰的树莓派Pico,自己动手做一个便携、够用的简易示波器?这个想法听起来有点“野”,但仔细一想,Pico那颗RP2040芯片,双核ARM Cortex-M0+,主频133MHz,内置ADC,还有可编程的PIO(可编程IO)这个“外挂”,硬件底子其实不差。更重要的是,它成本极低,体积小巧,如果能实现,那就是一个可以揣进口袋的调试利器。

这个“Pico示波器”项目,核心目标不是去挑战专业仪器的性能指标,而是解决一个非常具体的痛点:为电子爱好者、嵌入式开发者提供一个低成本、便携的实时信号观测工具。它适合用来观察数字信号的时序(比如调试UART、I2C的起始位)、测量PWM占空比、粗略查看模拟信号(如传感器输出)的波形和频率。你不需要用它去测几百MHz的高速信号,也不需要追求极高的垂直精度,但对于大多数单片机级别的数字电路调试和简单的模拟信号观察,它完全能胜任。

我花了大概两周时间,从硬件连接到软件编写,从底层采样到上位机显示,完整地走通了这个流程。最终实现了一个带宽约500kHz(理论值,实际受多重因素限制)、实时刷新的双通道示波器。下面,我就把这个从零搭建的过程、踩过的坑以及一些性能优化的心得,毫无保留地分享出来。

2. 硬件设计与核心思路拆解

2.1 为什么是树莓派Pico?

选择Pico作为核心,是基于几个非常现实的考量。首先是成本与普及度,Pico的价格让这个项目几乎没有门槛。其次是RP2040的独特架构,其内置的12位ADC虽然只有500kSPS(每秒采样点数)的吞吐率,且实际有效位数(ENOB)可能只有9位左右,但对于音频范围(20kHz以内)的信号观测已经足够。最关键的是它的PIO(可编程输入输出)DMA(直接内存访问)组合,这构成了我们实现实时、不间断采样的技术基石。

传统的单片机ADC采样,需要CPU参与每一次的“启动转换-读取数据-存储”循环,这会占用大量CPU时间,并且采样间隔不稳定。而我们的方案是:利用PIO编写一个精确的“采样时钟发生器”,触发ADC转换;转换完成后,通过DMA自动将数据搬运到指定的内存缓冲区;CPU仅在缓冲区满或需要处理数据时介入。这样,采样过程几乎完全由硬件自动完成,CPU被解放出来专注于波形处理、通信和显示,从而实现了更高的实时性和更稳定的采样率。

2.2 输入电路设计:安全第一,性能第二

直接把信号接到Pico的GPIO上是极其危险的做法。Pico的GPIO引脚耐受电压是3.3V,超过此电压很可能永久损坏芯片。因此,一个简单但必要的输入保护与调理电路是必须的。

我的设计思路是衰减、限幅、偏置。对于期望测量0-3.3V的信号,一个最简单的方案是使用电阻分压网络。例如,如果你想测量0-5V的信号,可以用一个10kΩ和20kΩ的电阻串联,信号从10kΩ电阻上端输入,从两个电阻中间点输出到Pico的ADC引脚,这样就能将5V衰减到约3.33V。但更通用和安全的做法是使用运放构建一个衰减器与加法器

这里我采用了单电源供电的轨到轨运放(如MCP6002)。电路核心功能有两个:

  1. 衰减与电平移位:通过电阻网络,将输入信号(比如±5V)衰减并抬升到0-3.3V范围内,确保信号始终落在ADC的量程内。
  2. 过压保护:在运放输入端和输出端到电源轨之间并联肖特基二极管(如BAT54S),利用其正向导通电压(约0.3V)将电压钳位在GND-0.3V 到 3.3V+0.3V 之间,为ADC引脚提供最后一道保护。

注意:这个输入电路直接决定了示波器的输入阻抗和带宽。使用运放缓冲可以轻松实现高输入阻抗(1MΩ以上),减少对被测电路的影响。但运放本身有增益带宽积(GBP)的限制,选择GBP较高的运放(如10MHz以上)对保证几百kHz的带宽至关重要。

2.3 双通道与触发电路构思

Pico有3个ADC引脚(GP26, GP27, GP28),我们至少可以实现双通道输入。在软件上,我们可以让两个通道交替采样,但这会使得每个通道的实际采样率减半。更高级的做法是利用RP2040的ADC有一个多路复用器,配合DMA在单个采样周期内快速切换通道,但这需要更精细的时序控制。

触发是示波器的灵魂功能,它让波形稳定显示。在硬件上,我们可以预留一个GPIO作为外部触发输入。在软件层面,实现触发逻辑:持续采样并将数据存入一个环形缓冲区,当检测到信号满足触发条件(如上升沿穿越某个电压阈值)时,记录下这个点,然后将触发点前后一段时间的数据发送给上位机显示,这样每次都能在波形的同一位置开始显示,图像就稳定了。

3. 固件开发:PIO与DMA的魔法

这是整个项目的软件核心,目标是在Pico上实现一个高效、稳定的数据采集系统。

3.1 PIO程序:精准的采样时钟

我们用一个PIO状态机来生成周期性的脉冲,这个脉冲的上升沿将作为ADC转换的启动信号。PIO的指令执行速度是系统时钟的一个分频,我们可以通过分频值精确控制采样间隔。

下面是一个简化的PIO汇编程序示例,它每隔N个时钟周期拉高一个引脚(作为采样触发信号)一个时钟周期:

.program adc_trigger .side_set 1 opt ; 声明side-set操作,用于控制输出引脚 .wrap_target pull noblock ; 从TX FIFO读取采样间隔计数值(如果FIFO空,则使用OSR旧值) mov x, osr ; 将计数值移动到X寄存器 set pins, 1 [1] ; side-set: 拉高触发引脚,并延迟1个周期 set pins, 0 ; side-set: 拉低触发引脚 label loop jmp x-- loop ; 循环X次,实现N个周期的等待 .wrap

在C语言主程序中,我们需要配置这个PIO状态机,并将计算好的分频值和循环次数推送到它的TX FIFO,以设定我们想要的采样率。例如,对于1MHz的采样率,每个采样周期需要133个系统时钟(133MHz / 1MHz = 133)。我们需要将这个值换算成PIO程序中的循环次数。

3.2 ADC与DMA配置:构建自动化的数据流水线

RP2040的ADC API使用起来相对简单,但关键在于将其与DMA联动。

  1. 初始化ADC:选择输入通道(GP26/27/28),设置ADC时钟分频(影响转换速度,通常设为最快)。
  2. 配置DMA通道:这是关键一步。我们需要配置一个DMA通道,其传输请求(DREQ)设置为DREQ_ADC。这意味着每当ADC完成一次转换,产生一个数据就绪信号,DMA就会自动执行一次传输,将ADC结果寄存器(adc_hw->fifo)中的数据,搬运到我们预先定义好的内存数组(采样缓冲区)中。
  3. 启动循环:将DMA配置为循环模式,当它把目标缓冲区填满后,会自动回到起点重新开始填充,并可以产生一个中断通知CPU。这样,我们就拥有了一个被ADC数据持续刷新的“环形缓冲区”。
// 简化的代码逻辑 uint16_t sample_buffer[BUFFER_SIZE]; dma_channel_config c = dma_channel_get_default_config(dma_chan); channel_config_set_transfer_data_size(&c, DMA_SIZE_16); // ADC结果是12位,用16位存储 channel_config_set_read_increment(&c, false); // 始终从ADC FIFO地址读 channel_config_set_write_increment(&c, true); // 写入到递增的内存地址 channel_config_set_dreq(&c, DREQ_ADC); // 数据请求来自ADC channel_config_set_ring(&c, true, RING_SIZE); // 可选:设置环形写入,方便管理 dma_channel_configure(dma_chan, &c, sample_buffer, // 写入地址 &adc_hw->fifo, // 读取地址(ADC FIFO) BUFFER_SIZE, // 传输次数 true // 立即启动 );

3.3 触发与数据封包逻辑

采样数据源源不断地流入环形缓冲区。我们需要一个“触发检测”线程(可以跑在第二个核心上)来监视这个缓冲区。它实时检查新到来的数据是否满足触发条件(例如,上一个点低于阈值,当前点高于阈值,即为上升沿触发)。一旦触发事件发生,就记录下触发点在缓冲区中的位置。

当上位机(比如PC)请求一帧数据时,固件程序就从触发点之前某个位置开始,提取一段固定长度的数据(比如1024个点),通过USB CDC(虚拟串口)或USB Bulk Transfer(更快)发送出去。为了减少通信开销和便于解析,我会将数据打包成带有帧头、通道信息、采样率、数据长度和校验和的小数据包。

4. 上位机软件:波形显示与交互

下位机负责“采”,上位机负责“显”和“控”。我选择了使用Python和PyQt5来开发上位机,因为快速原型开发能力强,库丰富。

4.1 通信协议解析

上位机通过USB串口与Pico通信。我定义了一个简单的二进制协议:

  • 命令帧:从上位机发送到下位机,用于控制采样率、触发模式、垂直灵敏度等。
  • 数据帧:从下位机发送到上位机,包含一帧波形数据。

数据帧的结构例如:[帧头0xAA 0x55] [通道号] [采样率低字节] [采样率高字节] [数据长度低字节] [数据长度高字节] [数据...] [校验和]。上位机需要按照这个格式解析字节流,还原出原始的ADC数值数组。

4.2 基于PyQtGraph的实时绘图

PyQtGraph是专门为科学计算和实时数据可视化设计的库,性能远超Matplotlib。它的核心是使用Qt的GraphicsView框架和OpenGL进行加速渲染。

基本流程如下:

  1. 创建绘图窗口和曲线对象:使用pg.PlotWidget创建画布,使用pg.PlotCurveItem创建曲线对象。将曲线对象的setData方法替换为自定义的、更高效的数据更新方法,或者直接更新其内部数据数组。
  2. 数据接收线程:开辟一个独立的QThread,专门负责从串口读取数据、解析协议。这个线程不能直接更新UI。
  3. 信号与槽更新:数据线程解析完一帧数据后,通过PyQt的信号(Signal)机制,将数据数组发送给主UI线程。主线程中的槽函数(Slot)接收到数据后,去更新曲线对象的数据,并调用update()方法请求重绘。
  4. 性能优化
    • 双缓冲/环形缓冲区:在内存中维护两个数据缓冲区,一个用于接收线程写入,一个用于绘图线程读取,通过指针交换避免锁竞争。
    • 降频显示:如果采样点太多(比如100k点),屏幕像素点有限,全部绘制反而卡顿。可以在绘图前对数据进行抽点(Decimation)或峰值检测(Peak Detection),只抽取关键点进行绘制,既能保留波形特征,又能极大提升渲染速度。
    • 关闭抗锯齿:实时绘图时,关闭曲线的抗锯齿可以显著提升性能。

4.3 基础示波器功能实现

在UI上,我模仿了传统示波器的布局:

  • 时基控制(Time/Div):改变水平方向每格代表的时间。上位机发送命令给Pico调整采样间隔,同时调整绘图时的X轴范围。
  • 垂直灵敏度控制(Volts/Div):改变垂直方向每格代表的电压值。这主要在上位机进行标定和缩放。需要知道ADC的参考电压(通常是3.3V)和输入电路的衰减比。例如,ADC满量程4095对应3.3V,如果垂直灵敏度设为1V/div,那么屏幕上8个格子(假设)对应8V,就需要将ADC数值(adc_value / 4095 * 3.3 * 衰减系数)进行换算。
  • 触发设置:UI上提供触发模式(自动、正常、单次)、触发边沿(上升、下降)、触发电平(一个可拖动的水平线)设置。这些参数被封装成命令帧发送给下位机。
  • 测量功能:实现电压(Vpp, Vavg)、频率、周期、占空比的自动测量。这完全在上位机通过分析接收到的波形数据数组来实现。例如,找出一段数据中的最大值和最小值相减得到Vpp;通过寻找过零点的时间间隔来计算频率。

5. 系统集成、测试与性能优化

5.1 整合与调试

将焊接好的输入调理板与Pico连接,上电,烧录固件,启动上位机软件。第一次连接往往不会一帆风顺。

首先确保USB通信正常,上位机能识别到串口并能打开。然后发送一个读取版本号的简单命令,测试命令-响应机制是否通畅。接着开始测试数据流:设置一个较低的采样率(如1kSPS),让Pico持续发送正弦波测试数据(可以用另一个GPIO生成PWM再经RC滤波产生),观察上位机是否能稳定绘制出正弦波。

5.2 实际性能测试与瓶颈分析

理论带宽500kHz,但实际能达到多少?我用一个信号发生器输出一个干净的正弦波,逐步增加频率,观察屏幕上波形的幅度衰减和形状失真。

  • 在100kHz以下:波形显示良好,幅度准确。
  • 达到300kHz:波形幅度开始有轻微衰减(-3dB左右),形状仍可辨认。
  • 超过500kHz:幅度衰减严重,波形严重失真,方波上升沿变得圆滑。

瓶颈主要来自以下几个方面

  1. ADC本身性能:RP2040的ADC在500kSPS时有效位数下降。
  2. 输入电路带宽:运放和PCB布局引入的寄生电容会形成低通滤波器,限制高频信号。
  3. 采样策略:如果是双通道交替采样,每个通道的有效采样率减半,根据奈奎斯特采样定理,能无失真重建的信号频率也减半。
  4. 软件开销:数据打包、USB传输、上位机解析和绘图,每一个环节都有延迟。特别是USB虚拟串口(CDC)模式,吞吐量和实时性有限。

5.3 优化手段实录

针对这些瓶颈,可以尝试以下优化:

  1. 提升USB传输效率

    • 放弃CDC,使用RP2040的USB Bulk Transfer端点。这需要在上位机安装特定的USB驱动(如libusb),但数据传输速率和实时性会大幅提升。
    • 增加数据压缩。对于变化缓慢的信号,可以使用差分编码或简单的游程编码,减少需要传输的数据量。
  2. 优化采样策略

    • 单次突发高速采样:对于重复性信号,可以利用Pico的264KB SRAM,设置一个巨大的缓冲区,以最高速采样一段很短的时间,然后慢慢上传。这可以捕捉瞬态信号。
    • 等效采样:对于周期性高频信号,如果触发非常稳定,可以采用多次触发、每次触发后延迟不同时间进行采样的方式,用多个周期拼凑出一个高密度采样的波形。这能突破ADC采样率的限制,测量比采样率高得多的信号频率,但只适用于稳定周期信号。
  3. 上位机绘图优化

    • 使用PyQtGraph的setData时,传入autoDownsample=True参数,让其自动进行优化抽点。
    • 对于固定网格的示波器界面,可以预计算好网格线和背景,避免每次重绘都重新计算。

6. 常见问题、排查与进阶玩法

6.1 问题速查表

问题现象可能原因排查步骤
上位机无连接USB驱动问题、线缆问题、固件未运行1. 检查设备管理器是否有未知设备或CDC串口。
2. 换USB线,换USB口。
3. 按住Pico的BOOTSEL按钮上电,看是否出现U盘,确认芯片正常。
能连接但无波形命令格式错误、采样未启动、触发条件不满足1. 用串口助手手动发送命令,看是否有回复。
2. 检查固件中ADC和DMA初始化是否成功。
3. 将触发模式设为“自动”(AUTO)。
波形严重噪声电源噪声、输入悬空、接地不良1. 给Pico的3.3V和GND之间并联一个10uF和0.1uF的电容。
2. 确保信号源和Pico共地。
3. 不测量时,将输入端子短接到地,看基线是否平稳。
测量电压不准ADC参考电压不准、输入电路分压比误差、软件标定错误1. 用万用表测量Pico的3.3V引脚实际电压,修正代码中的参考电压值。
2. 测量输入电路的实际分压比。
3. 输入一个已知的精确电压(如1.65V),查看ADC读数并计算标定系数。
高频信号幅值衰减输入电路带宽不足、采样率不足1. 检查运放型号的增益带宽积是否足够。
2. 尝试减少输入电路的串联电阻,或使用更小的滤波电容。
3. 确认当前设置的采样率是否满足奈奎斯特定理(大于信号频率2倍)。

6.2 从“能用”到“好用”的进阶技巧

  1. 软件触发与高级触发:除了基本的边沿触发,可以实现脉宽触发(捕捉特定宽度的脉冲)、欠幅触发(捕捉幅度不足的脉冲)等,这对分析数字通信总线异常非常有用。
  2. FFT频谱分析:利用上位机的计算能力(如NumPy的FFT函数),对采集到的时域波形做快速傅里叶变换,在另一个窗口显示频谱。这样你的简易示波器就升级成了频谱分析仪,可以分析信号的谐波成分。
  3. 协议解码:针对I2C、SPI、UART等常见协议,可以在上位机添加解码功能。波形显示区域上方,用不同颜色的标记条直接显示出解码出的数据字节、地址、起始/停止位等,极大提升调试效率。
  4. 持久化与分享:添加截图、保存波形数据(CSV格式)、加载历史波形等功能,方便记录和分享调试结果。

6.3 个人实操心得

这个项目最有意思的部分不是最终的结果,而是不断逼近性能极限和解决问题的过程。有几点深刻的体会:

首先,硬件是基础,软件是灵魂。一个糟糕的输入电路会让软件的所有努力白费。在焊接调理板时,一点点的接地环路或电源噪声都会被ADC灵敏地捕捉到。使用星型接地、给每个IC的电源引脚就近放置去耦电容,这些教科书上的原则在高速采样时至关重要。

其次,要善用数据手册和官方例程。RP2040的数据手册和Pico C/C++ SDK的示例代码是宝藏。DMA和PIO的配置看起来复杂,但SDK中的dmapio例子几乎提供了所有需要的模板。我的大部分时间其实是花在理解这些例程,并将它们组合起来。

最后,分层调试,步步为营。不要试图一口气写完所有代码。我的步骤是:先写一个最简单的ADC轮询采样,通过串口打印数据,验证硬件通路。然后加入PIO控制采样节奏。再引入DMA实现自动搬运。接着实现环形缓冲区和触发逻辑。最后才完善通信协议和上位机。每完成一步,都进行充分的测试,这样当问题出现时,你很容易定位到是哪个新引入的环节导致的。

这个用树莓派Pico制作的示波器,最终性能可能比不上几百元的二手货,但它在制作过程中带给你的对ADC、DMA、实时系统、数据可视化的理解,是任何现成仪器都无法给予的。它已经成功帮我调试了好几个ESP32和STM32项目中的传感器通信问题,作为一个随手可用的“信号侦探”,完全超出了我最初的预期。如果你手头也有块Pico在吃灰,强烈建议你尝试一下这个项目,它绝对能让你对嵌入式硬件和信号处理有一个全新的认识。

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

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

立即咨询