简介:这是一套基于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% | 万用表直流档 |
| 模拟参考电压VREF | 3.3V | 万用表,应该接近VDD |
| 晶振两端 | 8MHz正弦波 | 示波器探头X10档 |
| NRST引脚 | 3.3V(高电平) | 万用表,复位按键按下时变0 |
| PA0输入前端 | 1.65V中心偏置 | 无输入信号时测运放输出 |
如果以上节点异常,优先检查焊接、电源短路和晶振配置。F103的晶振需要两个20pF左右的负载电容,电容值不对会导致起振失败或频率偏移;另外当时钟源配置成HSE时,程序里必须和硬件实际晶振频率保持一致,否则串口波特率会算错。
4. 固件工程实战:基于标准库的ADC采集、DMA搬运与波形数据打包上送
搞懂了硬件,接下来看固件。这套工程基于STM32标准外设库(Standard Peripheral Library)编写,比HAL库更直接地暴露寄存器层面的操作逻辑,适合做底层学习的素材。工程结构分成几个模块:ADC配置、DMA配置、触发判定、波形数据缓冲区管理、串口/USB通信。
4.1 采样参数的初始化:ADC1+DMA1+定时器触发
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); // 校准ADC(F103要求上电后必须执行校准) 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改到7199(72MHz时钟下)。当观测低频信号时使用低采样率,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改成0x02,data_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频谱显示功能时就知道该按什么规则来设计缓冲区了。
本文还有配套的精品资源,点击获取