STM32示波器完整方案:信号链路、ADC采样与上位机显示的工程实践 简介这是一套基于STM32F103ZET6微控制器的完整示波器设计项目面向嵌入式学习者和从事信号采集应用开发的工程师可系统掌握从硬件搭建到软件实现的全流程。芯片基于ARM Cortex-M3内核集成ADC、DMA、定时器等高精度外设为波形采集与处理提供了可靠基础。压缩包约19.27MB包含PCB布局图、电路原理图、上位机测试程序、设计文档和解释文档等其中原理图详述信号路径与电源分布PCB图展示元件布局与走线便于直接参考或二次开发。源码覆盖采样、滤波、存储、触发等核心算法上位机测试程序支持串口通信与波形显示设计文档还说明了技术选型和调试思路解释文档则对硬件原理、软件架构、上位机交互机制进行细化说明可帮助初学者降低复现门槛。目前已有1388人学习下载适合希望深入理解STM32外设配置、模拟前端设计以及软硬件联合调试的开发者有助于快速复现并迁移至实际测量项目中。1. 一套能跑的STM32示波器比你想的更接近产品级手头有一套基于STM32F103ZET6的示波器完整工程包含原理图、PCB、下位机固件、上位机程序和设计文档不是那种只有个ADC采样然后串口打印原始值的教学demo而是把触发、存储、上位机波形显示、交互协议都串起来了。对于做嵌入式、仪器仪表、数据采集的工程师来说这类项目的价值在于能直接看到一条真实的信号链路——从模拟前端衰减和偏置到ADC采样率与分辨率的权衡再到DMA乒乓缓冲和上位机重绘策略。初学者可以用它理解示波器的工作原理5年以上的工程师则能从中拆出不少值得借鉴的工程细节比如触发条件的判定时机、数据包协议怎么设计才能兼顾速度和可靠性。这套资料在实际开发中的参考价值远大于“跑通一个FFT”那种碎片化实验。2. 信号链路拆解从模拟前端到ADC采样率与分辨率的工程取舍把示波器拆开看信号链路由模拟前端、ADC采集、触发判定、数据处理和显示交互五个环节组成。STM32F103ZET6内置的12位ADC最高采样率约1MHz实际受分频和转换周期影响这个指标决定了它定位在低频段适合音频、传感器、PWM、串口波形这类信号的观测而不是射频或高速数字总线。在动手移植或复刻这套设计前先要把链路中各模块的作用和取舍想清楚否则代码调通了也未必看懂为什么这样设计。2.1 模拟前端为什么不能直接把信号送进PA0引脚2.1.1 阻抗匹配与输入保护直接测一个0到3.3V以内的直流信号确实可以省略前端但实际使用场景往往面对的是带有直流偏置的交流信号、高压信号或工业现场的噪声信号。这套设计的原理图里模拟前端包含衰减电阻网络、运放跟随器和钳位保护二极管。衰减网络的作用是把大电压按比例降到ADC可接受的0到3.3V范围运放跟随器提供高输入阻抗避免探头负载效应拉低被测信号。钳位二极管则把意外的高压限制在电源轨内保护STM32的ADC引脚不被击穿。2.1.2 偏置与单电源供电的配合F103的ADC只能采正电压所以单电源供电时前端需要把双极性信号抬升到1.65V中心电平附近。常见做法是用运放搭建加法器电路把信号叠加一个Vref/2的直流偏置再用RC低通滤波去掉高频噪声。实际调试中可以看到运放选型直接决定信号完整性——比如LM358这类通用运放的带宽只有1MHz左右增益带宽积(Gain Bandwidth Product, GBW)不够时10kHz以上的信号幅值就会明显衰减换用OP07或AD8605这类精密运放带宽和噪声特性会好很多。参考这套工程时关键是理解原理图上每一级RC网络的截止频率设计而不是只照抄参数。2.2 ADC采样架构单次转换、连续转换与扫描模式的差异2.2.1 采样率与分辨率怎么平衡STM32F103的ADC是逐次逼近型(SAR)12位分辨率时钟最高14MHz最短采样周期1.5个ADC时钟单次转换最快约1μs。理论上1Msps的采样率不算高但在F103这个级别里已经是极限。实际使用中不会真的跑满因为还有触发判断和DMA搬运的开销。F103有规则组和注入组两套通道规则组适合连续采样多个通道注入组适合在特定时机插入采样。这套示波器工程用的是规则组扫描模式DMA循环搬运ADC自动按顺序转换配置好的通道整个过程不占用CPU。2.2.2 DMA乒乓缓冲的必要性如果每次采样都靠中断读数据CPU会被频繁打断数据连贯性也差。这套工程采用DMA双缓冲机制即乒乓缓冲。第一块缓冲区存满后DMA自动切换写到第二块同时触发中断让CPU处理第一块数据。这样采样过程不丢点CPU有足够时间去处理触发逻辑和把数据打包发送。F103的DMA1有7个通道ADC1的DMA请求映射在DMA1_Channel1配置时要特别注意外设地址固定为ADC1的数据寄存器地址而存储器地址要按半字16位递增因为12位ADC数据存在ADC_DR寄存器的低12位。2.3 触发机制示波器与普通数据采集器最本质的区别市面上的USB数据采集卡很多但示波器必须有触发功能。没有触发的话波形在屏幕上会乱跑根本看不出稳定形状。这套工程实现的是上升沿触发和下降沿触发两种模式核心思路是在ADC采样的数据流里每到一个新采样点就和预设的触发电平比较找到跨越阈值的那个点作为波形显示起点。2.3.1 软件触发的实现逻辑软件触发的代码逻辑很清晰本质上是一个状态机等待触发条件满足然后开始记录固定长度的波形数据。伪代码结构如下参考工程中trigger.c的逻辑uint8_t trigger_wait_for_edge(uint16_t *buf, uint16_t level, uint8_t edge) { static uint8_t armed 0; // 是否处于等待触发状态 uint16_t prev buf[0]; // 上一个采样值 for (uint16_t i 1; i SAMPLE_POINTS; i) { if (edge EDGE_RISING) { if (prev level buf[i] level) { armed 1; // 上升沿越过触发电平 } } else if (edge EDGE_FALLING) { if (prev level buf[i] level) { armed 1; // 下降沿越过触发电平 } } prev buf[i]; if (armed) { // 计算触发点在缓冲区中的位置 return i - PRE_TRIGGER_POINTS; } } return 0; // 未找到触发点 }2.3.2 触发位置偏移量的工程意义PRE_TRIGGER_POINTS是预触发深度表示波形显示窗口中触发点左侧保留的采样点数。为什么需要这个偏移量因为示波器不仅要看触发后的波形还要观察触发前的信号状态比如一个脉冲的上升沿之前是否有毛刺或预振荡。这种设计直接影响波形的可读性也是这套工程在思路上比那些只做“滚动显示”的伪示波器高一个档次的地方。3. 从原理图到布局PCB设计中容易被忽略的信号完整性问题拿到这套设计资料很多人会优先看代码但硬件方面也很关键。原理图给出了完整的电路连接关系PCB图则展示了元器件布局和走线。F103虽然是100脚LQFP封装的MCU但AD转换对布局布线还是很敏感的。3.1 地平面完整性与模拟地数字地的分割策略3.1.1 为什么不能简单地把地一刀切STM32的数据手册里通常建议在PCB Layout时区分模拟地和数字地但很多工程师在处理这个分割时容易走极端——直接把地平面切掉一大块。正确的做法是在PCB布局时首先确保地平面连续然后通过单点连接或磁珠连接的方式把模拟地与数字地串起来。如果地平面被强行切断信号回流的路径被迫绕行反而会产生更大的环路面积引入额外的电磁辐射和噪声。这套工程的PCB图里模拟部分集中在板子一角ADC的参考电压引脚附近专门放了一颗钽电容和一颗陶瓷电容并联形成低阻抗的去耦网络。数字部分的地电流不穿过模拟区域这是判断一块示波器板子设计好坏的最快方法。3.1.2 电源走线宽度与去耦电容的放置F103运行时电流不大但ADC采样瞬间会有电流尖峰。板子上3.3V电源线至少要走25mil以上每颗芯片的电源引脚旁边都要有100nF的陶瓷电容而且电容要尽量靠近电源引脚放置走线先经过电容再到芯片电源纹波噪声能明显改善。参考这套PCB时可以重点看这些细节是否到位顺便作为自己画板时的评价标准。3.2 上位机通信接口的硬件选型3.2.1 UART与USB方案的选择逻辑示波器与上位机通信最简单的方案是UART转USB例如使用CH340或CP2102但更稳定的是F103内部USB外设直接通过D和D-引脚连接电脑。两者区别在于UART转USB方案会受限于CDC驱动的兼容性问题而F103的USB外设如果使用ST官方库或CubeMX生成的USB CDC类则可以直接虚拟成串口无需额外驱动注意部分精简版Windows系统需要安装驱动。这套工程在设计上把USB和UART都做出来了通过跳线或拨码开关切换。实际使用中USB CDC的传输速度远快于UART在115200波特率下UART传一屏波形数据需要几十毫秒而USB Full Speed的12Mbps带宽能把这个时延压缩到几毫秒。调试低速信号时用UART足够观察音频信号或PWM波形时建议切换到USB模式。3.3 硬件调试要点上电后先看这几个关键节点拿到板子第一件事不是下载程序而是测量几个关键节点的电压和波形测试点预期结果测试方法VDD引脚3.3V±2%万用表直流档模拟参考电压VREF3.3V万用表应该接近VDD晶振两端8MHz正弦波示波器探头X10档NRST引脚3.3V高电平万用表复位按键按下时变0PA0输入前端1.65V中心偏置无输入信号时测运放输出如果以上节点异常优先检查焊接、电源短路和晶振配置。F103的晶振需要两个20pF左右的负载电容电容值不对会导致起振失败或频率偏移另外当时钟源配置成HSE时程序里必须和硬件实际晶振频率保持一致否则串口波特率会算错。4. 固件工程实战基于标准库的ADC采集、DMA搬运与波形数据打包上送搞懂了硬件接下来看固件。这套工程基于STM32标准外设库Standard Peripheral Library编写比HAL库更直接地暴露寄存器层面的操作逻辑适合做底层学习的素材。工程结构分成几个模块ADC配置、DMA配置、触发判定、波形数据缓冲区管理、串口/USB通信。4.1 采样参数的初始化ADC1DMA1定时器触发4.1.1 定时器触发采样为什么优于连续采样前面提到过用DMA循环搬运ADC数据但ADC的启动方式还有讲究。如果ADC跑在自由连续模式采样间隔取决于ADC转换时间但这个时间会受到CPU访问总线的影响导致采样点时间不均匀波形显示起来会有抖动。更稳定的是用定时器触发ADC启动比如TIM2的更新事件作为ADC触发源这样采样率就是定时器的更新频率严格等间隔。void ADC_Config(void) { ADC_InitTypeDef ADC_InitStruct; DMA_InitTypeDef DMA_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; // 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_TIM1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // ADC1 配置12位分辨率扫描模式数据右对齐 ADC_InitStruct.ADC_Mode ADC_Mode_Independent; ADC_InitStruct.ADC_ScanConvMode ENABLE; // 扫描模式 ADC_InitStruct.ADC_ContinuousConvMode DISABLE; // 定时器触发非连续 ADC_InitStruct.ADC_ExternalTrigConv ADC_ExternalTrigConv_T1_CC1; // TIM1捕获比较1触发 ADC_InitStruct.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStruct.ADC_NbrOfChannel 1; // 单通道采集 ADC_Init(ADC1, ADC_InitStruct); // 采样周期尽量短1.5周期提高采样率 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 校准ADCF103要求上电后必须执行校准 ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }这段配置里三个关键点解释一下一是ADC_ExternalTrigConv选择TIM1的CC1事件作为触发源确保采样时刻精确由定时器控制二是ADC_SampleTime_1Cycles5把采样周期压到最短换来最高可达1Msps的采样率三是ADC_NbrOfChannel设为1因为单通道采集的信号源是经过前端处理后的波形。实际使用中如果要多通道切换这里要改为对应通道数并配合扫描模式使用。4.1.2 DMA缓冲区的循环写入#define WAVE_SAMPLE_POINTS 1024 // 一屏波形数据点数DMA缓冲区大小 uint16_t adc_buffer[WAVE_SAMPLE_POINTS]; void DMA_Config(void) { DMA_InitTypeDef DMA_InitStruct; // DMA1通道1对应ADC1 DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; // 外设地址固定为ADC数据寄存器 DMA_InitStruct.DMA_MemoryBaseAddr (uint32_t)adc_buffer; // 内存地址 DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralSRC; // 外设作为源 DMA_InitStruct.DMA_BufferSize WAVE_SAMPLE_POINTS; // 缓存区大小 DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不递增 DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; // 16位 DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode DMA_Mode_Circular; // 循环模式不间断采集 DMA_InitStruct.DMA_Priority DMA_Priority_High; DMA_InitStruct.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStruct); DMA_Cmd(DMA1_Channel1, ENABLE); }DMA_Mode_Circular是这套设计最核心的设置它让DMA不停地把ADC转换结果搬运到adc_buffer数组里无需CPU干预。当缓冲区写满后DMA自动回到起点继续覆盖写入。这样做的目的是保证任何时刻内存里都保存着“最近N个采样点”的数据为触发判定提供滚动的时间窗口。DMA传输完成中断DMA_IT_TC在这里没有立即传输半字导致丢数的问题因为循环模式下中断会周期性触发每个周期处理一帧数据配合第3章讲的乒乓双缓冲思路用软件去模拟乒乓逻辑即可。4.2 波形数据帧协议给上位机一个明确的解析规则4.2.1 帧结构定义下位机采集到一屏数据后需要通过串口或USB发送给上位机显示。通信协议设计得好不好直接决定上位机解析的效率和容错性。这套工程采用的帧结构参考如下typedef struct { uint8_t frame_header; // 帧头固定为0xAA uint8_t cmd_id; // 命令字0x01表示波形数据 uint16_t data_len; // 数据长度单位字节 uint16_t sample_rate; // 实际采样率单位Hz uint8_t trigger_mode; // 触发模式0上升沿 1下降沿 2自由运行 uint16_t voltage_level; // 触发电平换算后的原始ADC值 uint16_t wave_data[1024]; // 波形数据数组 uint16_t crc16; // 对整帧做CRC16校验 } WaveDataFrame;4.2.2 为什么要在帧头之外加CRC串口通信环境下数据在传输过程中可能因为电磁干扰丢字节、错字节。如果只靠帧头定位那么数据错位后整帧解析全部错误。CRC16校验的作用是让上位机在接收到一帧怀疑数据时主动丢弃而不是显示一条脏波形。F103上做CRC16计算开销很小但可以显著提升系统的可靠性。上位机收到0xAA后开始组装数据包直到收满整形长度然后计算CRC校验通过才送显示。4.3 采样率动态调整的实现思路固定采样率很简单但示波器要有自适应能力。这套工程采用变采样率设计的逻辑是通过修改定时器TIM1的PSC和ARR值调整触发频率从而改变ADC的采样率。例如采样率从1MHz降到10kHz只需要把ARR从71改到719972MHz时钟下。当观测低频信号时使用低采样率ADC数据变化平缓能够更好地观察整个波形周期。上位机也能根据收到的采样率字段自动调整时间轴标尺不用人为换算。5. 上位机通信、显示与实时性的工程化处理下位机把数据送上来上位机如何显示是另一大难题。PC端测试程序常见的实现方案是MFC绘图控件或C# WinForm 双缓冲绘图这套工程里提供的上位机测试程序兼容了常见的串口读取、波形绘制和参数设置三大功能。5.1 串口数据接收与波形图绘制的同步机制5.1.1 高频刷新下的界面卡顿规避上位机绘制波形时如果直接把1024个点依次画在窗口上会有明显的闪烁感。原因是GDI绘图在单缓冲模式下会不断刷新整个控件背景造成视觉上的撕裂和闪烁。解决方案是双缓冲绘图先在内存中创建一个与控件尺寸一致的位图将波形画在位图上再一次性地把位图复制到屏幕。// C# 伪代码双缓冲绘制波形 private void DrawWaveform(Graphics g, ushort[] data) { // 创建内存画布避免屏幕闪烁 Bitmap bmp new Bitmap(wavePanel.Width, wavePanel.Height); Graphics memG Graphics.FromImage(bmp); memG.Clear(Color.Black); // 坐标映射ADC原始值0~4095映射到窗口高度 float xStep (float)wavePanel.Width / data.Length; PointF[] points new PointF[data.Length]; for (int i 0; i data.Length; i) { float y wavePanel.Height - (data[i] / 4095.0f) * wavePanel.Height; points[i] new PointF(i * xStep, y); } Pen pen new Pen(Color.Yellow, 1.0f); memG.DrawLines(pen, points); // 一次性复制到屏幕消除闪烁 g.DrawImage(bmp, 0, 0); memG.Dispose(); bmp.Dispose(); }这段代码的逻辑是先将波形数据线性映射到控件坐标空间然后用DrawLines一次性绘制整条折线再整体上屏绘制效率远高于逐点画点。注意事项是端口接收数据时不要直接在UI线程里处理否则接收大量数据时界面会假死正确的做法是数据接收事件中把包放入队列UI定时器定期从队列取出最新帧刷新显示。5.1.2 采样率、触发电平的联动交互上位机界面上的“采样率”下拉框、“触发电平”滑动条和“触发模式”单选框在改动后应通过串口向下位机下发配置帧。配置帧格式可以复用4.2的帧结构把cmd_id改成0x02data_len设为固定值用data字段携带参数。下位机收到后解析立即更新定时器分频系数和触发比较阈值从而实现“调整参数后波形马上变化”的实时体验。6. 一套自检与验证路径校准、工程固件移植与常见坑项目资料里附带设计文档和解释文档文档覆盖目标、技术选型、硬件设计原理和软件架构有对整体理解有帮助。但光读文档不够还要动手验证。6.1 用方波验证你示波器的时基是否准确把STM32的某个GPIO配置成PWM输出产生1kHz的方波接入示波器的输入通道。然后在上位机中读取波形观察一个完整周期的采样点数。如果采样率设置为10kHz那么一个1kHz方波周期里应该有10个采样点如果实际看到20个点说明实际采样率是设定值的两倍那么定时器预分频配置或ADC触发源有误。这种方法排查的思路是逆向验算不依赖额外仪器就能定位问题。6.2 接入信号发生器验证幅值精度如果有信号发生器输入一个1kHz、1Vpp的正弦波在0到3.3V电压范围内壳设置偏置到1.65V这样信号应在ADC量程范围内。然后用上位机读取波形计算波峰波谷对应的ADC差值换算成实际电压和信号发生器的设定值对比。12位ADC的理想量化误差约0.8mV如果实际的误差超过50mV则要考虑前端运放偏置是否准确、参考电压是否有纹波。6.3 常见工程问题排查提示示波器项目下的“波形乱跳”和“数据错乱”多数不是代码算法问题而是采样率配置和触发条件设置不当。F103的ADC校准是上电后必须做的步骤。漏掉ADC_ResetCalibration和ADC_StartCalibration也会导致采样值整体偏大或出现非线性误差。另一个需要留意的是ADC引脚输入阻抗与前端电路输出阻抗的关系如果前端运放输出阻抗过高例如超过10kΩADC内部的采样电容会在采样瞬间拉低信号电压尤其在使用1.5周期的最短采样时间时更明显输出波形出现轻微阶梯失真。排查方法是把采样时间调到239.5周期如果失真消失就说明问题出在前端驱动能力上而不是ADC硬件本身。6.4 把这套代码迁移到其他F103型号时要注意什么很多型号的F103如C8T6、RCT6引脚数比ZET6少外设通道映射也有差异。在迁移这套工程时重点检查ADC_Channel_0是否对应目标型号的PA0引脚以及TIM1的某个通道是否支持外部触发虽然F103的TIM1基本都带触发输出功能但引脚映射到封装上时可能被复用为其他功能。建议在CubeMX里新建工程按这套设计文档的配置重新生成初始化代码再移植主逻辑比直接改原工程省很多查错时间。这套工程里对调试验证最有帮助的反而是设计文档里对技术选型的说明比如为什么采样深度总是设置成2的幂次方1024/2048/4096因为DMA缓冲区大小和FFT变换都要求数据长度是2的幂方便做索引和计算。理解这一层之后自己扩展FFT频谱显示功能时就知道该按什么规则来设计缓冲区了。本文还有配套的精品资源点击获取