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)。电路核心功能有两个:
- 衰减与电平移位:通过电阻网络,将输入信号(比如±5V)衰减并抬升到0-3.3V范围内,确保信号始终落在ADC的量程内。
- 过压保护:在运放输入端和输出端到电源轨之间并联肖特基二极管(如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联动。
- 初始化ADC:选择输入通道(GP26/27/28),设置ADC时钟分频(影响转换速度,通常设为最快)。
- 配置DMA通道:这是关键一步。我们需要配置一个DMA通道,其传输请求(DREQ)设置为
DREQ_ADC。这意味着每当ADC完成一次转换,产生一个数据就绪信号,DMA就会自动执行一次传输,将ADC结果寄存器(adc_hw->fifo)中的数据,搬运到我们预先定义好的内存数组(采样缓冲区)中。 - 启动循环:将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进行加速渲染。
基本流程如下:
- 创建绘图窗口和曲线对象:使用
pg.PlotWidget创建画布,使用pg.PlotCurveItem创建曲线对象。将曲线对象的setData方法替换为自定义的、更高效的数据更新方法,或者直接更新其内部数据数组。 - 数据接收线程:开辟一个独立的QThread,专门负责从串口读取数据、解析协议。这个线程不能直接更新UI。
- 信号与槽更新:数据线程解析完一帧数据后,通过PyQt的信号(Signal)机制,将数据数组发送给主UI线程。主线程中的槽函数(Slot)接收到数据后,去更新曲线对象的数据,并调用
update()方法请求重绘。 - 性能优化:
- 双缓冲/环形缓冲区:在内存中维护两个数据缓冲区,一个用于接收线程写入,一个用于绘图线程读取,通过指针交换避免锁竞争。
- 降频显示:如果采样点太多(比如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:幅度衰减严重,波形严重失真,方波上升沿变得圆滑。
瓶颈主要来自以下几个方面:
- ADC本身性能:RP2040的ADC在500kSPS时有效位数下降。
- 输入电路带宽:运放和PCB布局引入的寄生电容会形成低通滤波器,限制高频信号。
- 采样策略:如果是双通道交替采样,每个通道的有效采样率减半,根据奈奎斯特采样定理,能无失真重建的信号频率也减半。
- 软件开销:数据打包、USB传输、上位机解析和绘图,每一个环节都有延迟。特别是USB虚拟串口(CDC)模式,吞吐量和实时性有限。
5.3 优化手段实录
针对这些瓶颈,可以尝试以下优化:
提升USB传输效率:
- 放弃CDC,使用RP2040的USB Bulk Transfer端点。这需要在上位机安装特定的USB驱动(如libusb),但数据传输速率和实时性会大幅提升。
- 增加数据压缩。对于变化缓慢的信号,可以使用差分编码或简单的游程编码,减少需要传输的数据量。
优化采样策略:
- 单次突发高速采样:对于重复性信号,可以利用Pico的264KB SRAM,设置一个巨大的缓冲区,以最高速采样一段很短的时间,然后慢慢上传。这可以捕捉瞬态信号。
- 等效采样:对于周期性高频信号,如果触发非常稳定,可以采用多次触发、每次触发后延迟不同时间进行采样的方式,用多个周期拼凑出一个高密度采样的波形。这能突破ADC采样率的限制,测量比采样率高得多的信号频率,但只适用于稳定周期信号。
上位机绘图优化:
- 使用PyQtGraph的
setData时,传入autoDownsample=True参数,让其自动进行优化抽点。 - 对于固定网格的示波器界面,可以预计算好网格线和背景,避免每次重绘都重新计算。
- 使用PyQtGraph的
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 从“能用”到“好用”的进阶技巧
- 软件触发与高级触发:除了基本的边沿触发,可以实现脉宽触发(捕捉特定宽度的脉冲)、欠幅触发(捕捉幅度不足的脉冲)等,这对分析数字通信总线异常非常有用。
- FFT频谱分析:利用上位机的计算能力(如NumPy的FFT函数),对采集到的时域波形做快速傅里叶变换,在另一个窗口显示频谱。这样你的简易示波器就升级成了频谱分析仪,可以分析信号的谐波成分。
- 协议解码:针对I2C、SPI、UART等常见协议,可以在上位机添加解码功能。波形显示区域上方,用不同颜色的标记条直接显示出解码出的数据字节、地址、起始/停止位等,极大提升调试效率。
- 持久化与分享:添加截图、保存波形数据(CSV格式)、加载历史波形等功能,方便记录和分享调试结果。
6.3 个人实操心得
这个项目最有意思的部分不是最终的结果,而是不断逼近性能极限和解决问题的过程。有几点深刻的体会:
首先,硬件是基础,软件是灵魂。一个糟糕的输入电路会让软件的所有努力白费。在焊接调理板时,一点点的接地环路或电源噪声都会被ADC灵敏地捕捉到。使用星型接地、给每个IC的电源引脚就近放置去耦电容,这些教科书上的原则在高速采样时至关重要。
其次,要善用数据手册和官方例程。RP2040的数据手册和Pico C/C++ SDK的示例代码是宝藏。DMA和PIO的配置看起来复杂,但SDK中的dma和pio例子几乎提供了所有需要的模板。我的大部分时间其实是花在理解这些例程,并将它们组合起来。
最后,分层调试,步步为营。不要试图一口气写完所有代码。我的步骤是:先写一个最简单的ADC轮询采样,通过串口打印数据,验证硬件通路。然后加入PIO控制采样节奏。再引入DMA实现自动搬运。接着实现环形缓冲区和触发逻辑。最后才完善通信协议和上位机。每完成一步,都进行充分的测试,这样当问题出现时,你很容易定位到是哪个新引入的环节导致的。
这个用树莓派Pico制作的示波器,最终性能可能比不上几百元的二手货,但它在制作过程中带给你的对ADC、DMA、实时系统、数据可视化的理解,是任何现成仪器都无法给予的。它已经成功帮我调试了好几个ESP32和STM32项目中的传感器通信问题,作为一个随手可用的“信号侦探”,完全超出了我最初的预期。如果你手头也有块Pico在吃灰,强烈建议你尝试一下这个项目,它绝对能让你对嵌入式硬件和信号处理有一个全新的认识。