
简介串口数字示波器Serial Digital Scope是一款面向单片机开发者、电子爱好者与嵌入式初学者的上位机调试工具核心用途是将单片机经串口发送至上位机的数据实时绘制成波形解决原始数据无法直观观察的问题。软件最多支持四个通道同时显示具备波形缩放、自动跟踪等交互能力并能一键导出CSV格式便于对采集数据进行后续分析或记录。资源包共10个文件主要包含可直接运行的exe程序、C/H源码文件、TXT说明文档以及DAT配置文件压缩包仅796KB小巧轻便适合快速部署到调试环境中。目前已有444人学习下载受到单片机调试场景的认可。包内源码与可执行程序并存既能直接使用V2版本进行多通道波形观察也可通过阅读C/H源码理解串口协议解析与波形绘制原理便于二次开发和功能裁剪配合说明文档和配置文件还能帮助用户快速掌握四通道设置、缩放与自动跟踪等操作适用于嵌入式课程设计、传感器数据采集和设备调试等多种场景。 最近整理移动硬盘的时候翻到一个Serial_Digital_Scope V2.zip时间戳显示是两年前打包的。解压看了一圈代码觉得这套小型串口示波器项目的设计思路和踩坑过程还是挺值得写的。Serial_Digital_Scope 说白了就是一台基于串口传输的简易数字示波器单片机负责 ADC 采样把波形数据打包后通过 USB 转串口发给 PC再在上位机里还原成实时波形。它不是为了替代专业示波器而是解决日常调试里“手边没有示波器但需要看传感器输出、PWM 波形、串口电平变化”这类问题。这篇文章会把 V2 版本从硬件选型、固件数据流、上位机绘制到实际调试中踩过的坑完整拆开讲。如果你打算自己动手做一台低成本示波器或者只是对串口协议设计、ADC 采样调度和实时绘图感兴趣这篇应该能帮你少走不少弯路。1. 这个 zip 里到底装了什么以及为什么要做一台串口示波器1.1 从文件命名看项目定位Serial_Digital_Scope这个名字实际透露出几个关键信息采用串口Serial作为传输通道采集的信号属于数字域采样得到的离散波形Digital最终目标是把信号可视化Scope。V2 代表这是第二个迭代版本不是推倒重来而是在 V1 的基础上解决采样率不稳、丢帧、波形抖动这些明显问题。解压后内容和我预期的差不多Serial_Digital_Scope_V2/ ├── firmware/ │ ├── Core/ │ ├── Drivers/ │ └── Project/ ├── host_software/ │ ├── serial_scope.py │ ├── scope_ui.py │ └── requirements.txt └── docs/ ├── protocol_v2.md └── calibration.md固件部分是一个基于 STM32 的标准工程上位机则用 Python 写的。V1 时代的上位机界面非常简陋只有一条实时滚动曲线几乎没有缩放和触发功能。V2 花了大量精力重写了帧协议和数据缓存逻辑这是和 V1 最大的区别。1.2 什么场景真正用得上它有人可能会问几十块钱的 USB 逻辑分析仪不是更省事吗确实逻辑分析仪擅长看数字电平时序但它很难真实反映模拟信号的幅值变化。Serial_Digital_Scope 的定位正好在“纯逻辑分析仪”和“专业台式示波器”之间适合这几类场景调试 NTC 热敏电阻或光敏电阻的缓慢变化信号观察 PWM 输出是否真的达到了预期占空比和频率读取加速度计、气压计等传感器通过 SPI/I2C 输出的模拟波形分析音频信号经过放大电路后的削波情况验证电源轨上电瞬间的纹波是否在允许范围内。这些场景不需要 500MHz 带宽也不需要 1Gpts 的存储深度只要能把波形稳定显示出来能测量大致幅值和频率就够了。串口示波器够用而且元器件成本很低维修替换也方便。2. 硬件方案取舍V2 在采样链路上的关键改动2.1 主控选型与 ADC 采样能力V1 主控用的是 STM32F103C8T6V2 依然保留了这颗芯片原因很直接便宜、资料多、ADC 速度够用。STM32F103 的 ADC 是 12 位理论上最高采样率 1Msps对于大多数传感器信号和低频 PWM 检测完全足够。不过“理论最高采样率”和“稳定持续采样率”是两码事。如果让 ADC 连续最高速采样然后把每个 16 位结果都通过串口发出去串口根本来不及传。V2 妥协后的做法是单次触发模式以 1Msps 采样采满一帧深度比如 1024 点后暂停再整帧发送连续显示模式把采样率降到 50kSps 到 200kSps 区间数据边采边发。ADC 的参考电压也做了改动。V1 直接使用 MCU 的 3.3V VDD 作为参考源导致 USB 供电波动直接影响测量精度。V2 在模拟电源处增加了一颗低噪声 LDO并用陶瓷电容和磁珠做了隔离ADC 的参考电压从 VDD 改为外部基准电压。虽然绝对精度还是达不到实验室级别但长期稳定度明显提升5V 电源电压波动时波形基线不会再跟着漂。2.2 前端信号调理电路的设计单片机的 ADC 输入范围一般只有 03.3V而需要测量的信号往往可能是双极性例如音频信号和交流信号或者超过 3.3V。V1 的输入保护做得很弱一不下心烧过单片机引脚V2 专门设计了前端调理电路。基本的信号链路是这样的输入端子 → 分压电阻 → 电压跟随器 → 钳位二极管 → ADC引脚分压电阻用来衰减高压信号V2 使用 10kΩ 和 10kΩ 分压把 06.6V 范围折半映射到 03.3V。分压会提高信号源阻抗所以后面必须接一级高输入阻抗的电压跟随器。V2 用了 OPA340 这类单电源轨到轨运放供电电压 3.3V输入阻抗在 MΩ 级别对被测电路的影响极小。钳位环节是 V1 的教训。原来只加了一个 3.3V 稳压管结果静电或反接时还在损坏引脚。V2 改用两个快恢复二极管分别钳到 GND 和 3.3V再串联 1kΩ 限流电阻。这个方案虽然会增加一点点输入阻抗但保护效果比稳压管好得多。2.3 串口传输瓶颈决定了整体架构很多第一次做串口示波器的人会把注意力全放在 ADC 采样率上结果做完才发现串口才是最大的瓶颈。以常用的 921600 波特率为例理论有效数据率大约 92KB/s也就是每秒最多传输九万字节左右。如果每帧包含 1024 个采样点每个采样点用 2 字节16 位表示再加上帧头、通道号、 CRC 等开销一帧差不多 2050 字节。在 921600 波特率下一帧传输就要 22 毫秒左右。也就是说理想状态下每秒也只能刷新约 45 帧。如果还要显示多通道帧率只会更低。V2 为此专门设计了两种工作模式避免让“采样率”和“传输率”互相打架。单次触发模式下MCU 先把一段波形完整采进 SRAM再慢慢上传连续显示模式则主动把采样率降低到传输能力允许的范围。这样既能看突发信号细节也能观察实时变化趋势不会一上来就把数据链路堵死。3. 固件端的数据流设计从 DMA 到串口打包3.1 定时器触发 DMA 采样要让波形显示稳定ADC 采样间隔必须严格均匀。如果直接在 while 循环里调用HAL_ADC_Start阻塞采集采样点之间的时间间隔会被中断、串口发送等逻辑扰乱画出来的波形会明显失真。V2 的固件采用“定时器触发 ADC DMA 搬运”的结构。STM32 的高级定时器 TIM1 产生固定频率的更新事件作为 ADC 的外部触发源ADC 每次触发采样一次转换完成后由 DMA 把结果自动搬进内存缓冲区全程不需要 CPU 介入。核心初始化逻辑类似static void MX_ADC1_Init(void) { hadc1.Instance ADC1; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T1_TRGO; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; if (HAL_ADC_Init(hadc1) ! HAL_OK) Error_Handler(); }这里必须注意ContinuousConvMode设为 DISABLE否则定时器触发就失去意义了。每次 DMA 传输达到设定长度后触发转换完成中断在中断里切换缓冲区或启动新一轮采集。3.2 双缓冲与乒乓切换如果只用一块固定缓冲区那么 MCU 在发送这块数据期间ADC 还在继续往同一块内存里写数据就会产生覆盖。V2 采用经典的双缓冲Ping-Pong Buffer两块缓冲区轮流工作#define SAMPLE_BUF_SIZE 4096 __attribute__((aligned(4))) uint16_t adc_buf_a[SAMPLE_BUF_SIZE]; __attribute__((aligned(4))) uint16_t adc_buf_b[SAMPLE_BUF_SIZE]; volatile uint8_t current_buf 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { if (current_buf 0) { // 通知发送线程处理 buf_a 数据继续采 buf_b process_and_send(adc_buf_a, SAMPLE_BUF_SIZE); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf_b, SAMPLE_BUF_SIZE); current_buf 1; } else { process_and_send(adc_buf_b, SAMPLE_BUF_SIZE); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf_a, SAMPLE_BUF_SIZE); current_buf 0; } } }这里的process_and_send要尽可能短。如果在中断里做大量打包和串口发送采样就会被打断所以 V2 在实际实现中只把缓冲区标记为“待发送”真正的发送流程放在主循环里处理。对实时性要求更高的场景可以考虑用 RTOS 或者更细粒度的临界区保护。3.3 帧协议怎么保证上位机能正确解码串口是字节流本身就很容易发生错位。如果上位机只是简单从第一个字节开始按顺序解析一旦一个字节丢失或者多发之后所有波形数据都会错乱。V2 采用的帧格式如下typedef struct __attribute__((packed)) { uint8_t header; // 固定 0xA5 uint8_t channel; // 通道编号 uint8_t mode; // 0连续, 1单次 uint8_t point_num; // 采样点数高字节 uint8_t point_num_low; uint8_t sample_rate; // 采样率档位索引 uint8_t reserved; uint8_t crc8; // 帧头校验 uint16_t data[]; // 有效采样数据 } ScopeFrame;上位机不能只依赖帧头来找边界因为数据区里也可能出现 0xA5。V2 的做法是“帧头 长度 CRC8”三重校验先扫描到帧头 0xA5再根据接下来的长度字段计算整帧长度最后做 CRC 校验全部通过才认为这帧有效。如果 CRC 不对就从下一个字节重新开始搜索帧头直到自动恢复同步。3.4 采样率与波特率的换算关系固件设计里有一个很关键的换算公式。假设波特率是 B帧开销是 8 字节每采样点用 number_of_bytes 字节表示那么理论最大持续采样率大约为持续采样率 ≈ (B / 10) / (number_of_bytes 8 / points_per_frame)这里除以 10是因为 UART 每一字节实际传输 10 bit起始位 8 数据位 停止位。以 921600 波特率、每帧 4096 点、每点 2 字节为例有效传输 92160 Byte/s ≈ 46080 采样点/s所以连续显示模式下V2 默认把采样率设置在 50kSps 以内留出一点余量给操作系统调度和 USB 转换器延迟。如果强行设到 200kSps上位机接收数据的速度跟不上很快就会出现丢帧。V2 在采样率档位设计上做了几个固定档位1kSps、10kSps、50kSps、100kSps、200kSps、1MSps。前几个用于连续显示最后两个仅用于单次触发。这样用户不用手动输入频率减少配置错误。4. 上位机波形显示串口数据到屏幕像素的漫漫长路4.1 技术栈选型为什么用 Python上位机最初考虑过 C 加 Qt但 V2 最终还是用了 Python。原因很简单快速迭代。串口示波器本来就是个辅助调试工具不值得花大量时间在 C 的界面和编译环境上。Python 生态里有pyserial做串口通信pyqtgraph做高性能绘图配合 PyQt5 可以在一周内完成界面和逻辑。依赖列表很小pyserial3.5 PyQt55.15.9 pyqtgraph0.13.3 numpy1.24.3pyqtgraph天生基于 numpy 数组绘制性能远好于 matplotlib刷新几千个点不会有明显卡顿。4.2 串口线程与界面线程的协作模式上位机最容易犯的错误是在主界面线程里去读串口一旦串口数据堆积界面就会卡死。V2 的做法是单独开一个 QThread 负责串口读取和帧解析解析完成的波形数据通过 Qt Signal 发给主窗口。大致代码结构如下class SerialReader(QThread): frame_ready pyqtSignal(object) def __init__(self, port, baudrate): super().__init__() self.serial serial.Serial(port, baudrate, timeout0.05) self.running True def run(self): buffer bytearray() while self.running: buffer.extend(self.serial.read(512)) frames self.parse_buffer(buffer) for frame in frames: self.frame_ready.emit(frame) def parse_buffer(self, buffer): # 查找帧头、解析长度、CRC校验返回完整帧列表 pass把串口读取放到独立线程后即便串口短时间内涌入大量数据界面线程依然能保持响应。timeout0.05很重要它避免线程无限期阻塞在read()上保证退出程序时线程能及时收到停止信号。4.3 绘制优化和触发逻辑pyqtgraph 画波形本身不算难真正麻烦的是数据刷新策略。V1 每收到一帧就立刻setData当串口数据量大时界面会明显掉帧。V2 改成定时刷新比如每 30 毫秒更新一次画面这个间隔内如果收到多帧数据就只取最新一帧来显示。这样人眼看起来是实时波形CPU 占用却低很多。触发逻辑在 V2 里也做了重做。原来只是简单地从第一个点开始画波形在屏幕上会左右乱跑。V2 增加了一个上升沿触发检测当检测到波形从低于阈值变为高于阈值时从该点开始截取一帧数据。这样波形的起始相位基本固定主观视觉上稳定很多。def find_trigger(data, threshold): for i in range(len(data) - 1): if data[i] threshold and data[i 1] threshold: return i return 0触发在软件层实现不可避免有一点延迟但用于低频信号足够。如果以后要做 MHz 级信号触发就必须靠硬件比较器了。5. 实际调试中的典型故障与排查思路5.1 波形上叠加了规律毛刺V2 刚调试时波形正常但每几百毫秒会周期性叠加一个毛刺。一开始怀疑是电源噪声后来用示波器量测模拟前端发现毛刺周期和串口发送中断的周期完全吻合。问题出在串口发送时的 DMA 和 ADC 采样共用总线串口占用总线的时间会对 ADC 转换造成微小干扰。解决办法有两个第一是提高串口 DMA 优先级让发送过程尽快结束第二是在模拟前端增加一个 RC 滤波截止频率设置到几十 kHz把高频毛刺滤掉。V2 最终两个方案都用上了毛刺幅度从几十毫伏降到了几个毫伏以内。这类问题在原理图阶段很难预料必须通过实测波形反推。所以串口示波器这种工具虽然精度不高但用来排查自身硬件干扰反而特别方便。5.2 长时间采集时丢帧连续跑几分钟后上位机的波形会出现断层或明显跳变。排查链路是这样的先确认串口硬件换了一条短 USB 线问题依旧增加上位机读取缓冲大小没有明显改善把上位机串口超时调小到 0.01 秒问题改善了一些最终定位到 USB 转串口芯片的驱动延迟。很多 USB 转 UART 芯片默认会启用接收缓冲USB 端点每 1ms 或 2ms 才提交一次数据导致实际数据到达上位机时是一坨一坨的。Windows 下可以通过修改设备管理器里的“延迟计时器”来降低Linux 下则可以用setserial设置 low_latency 模式。setserial /dev/ttyUSB0 low_latency设置之后长时间采集的丢帧现象基本消失。这提醒我串口示波器的稳定性不只是 MCU 固件的问题PC 端驱动风格也会极大影响表现。5.3 刷新率上不去的真正瓶颈早期 V2 上位机无论怎么优化波形刷新率一直卡在十几帧。我一度认为是 pyqtgraph 绘制能力不足后来用cProfile跑了一遍发现主要耗时在帧解析里的 numpy 数组拼接操作。原来的代码每收到一帧就做np.append到大数组反复分配内存开销巨大。改成预先分配一块固定大小的 numpy 数组每次用切片覆盖写入效率立刻提升两三倍。绘制层面反而非常轻松。经验就是优先优化数据处理路径而不是一上来就怀疑绘图引擎。5.4 测量电压不准的校准方法ADC 的 12 位读数并不能直接换算成真实电压因为分压电阻有误差参考电压也不是绝对精确。V2 的标定很简单给输入端分别加 0V、1.0V、3.3V 三组已知电压记录对应的 ADC 读数然后做线性拟合得到实际换算系数。voltage (adc_code * slope) offset这个系数被写进上位机的配置文件里不用每次都改固件。实测下来输入 1V 时误差能控制在 10mV 以内对于观察波形幅值已经够用。标定还有一个额外作用能间接检查整个模拟前端的线性度。如果三点拟合出来的残差很大说明前端电路或参考电压有问题这时候修硬件比调软件更有效。6. 如果继续做 V3我会优先改哪几处6.1 值得加入的触发和存储深度V2 的软件触发虽然够用但触发点之后的数据仍然是有限的。如果能把采样深度提高到 64K 点以上配合硬件比较器触发就能捕捉到偶发的异常毛刺。这可能是 V3 最值得做的一点。同时 ADC 采样率上限 1Msps 受限于 MCU 主频真要提升带宽就得换更高性能的芯片。6.2 离开串口后的替代方案串口始终有波特率的天花板。V3 可以考虑直接用 MCU 的 USB 高速模式或者通过 WiFi 传给 PC传输带宽能提升一个量级。另外给 MCU 直接接一块小尺寸 TFT 屏让示波器作为便携设备独立工作也是一条好玩且实用的路线。不过如果只是日常调试用当前 V2 这套架构其实已经覆盖了 90% 以上的需求。我最后想说的一点是这类项目最有价值的往往不是最终跑通的波形而是中间那次“串口数据对不上”“波形为什么有毛刺”“刷新率上不去”的排查过程。把这些经验记录下来比单纯上传一个 zip 要对后来者有用得多。本文还有配套的精品资源点击获取