这次我们来看一个基于STM32的简易示波器项目。这不是一个商业产品,而是一个完整的毕业设计实践,从硬件选型、原理图绘制、PCB设计到嵌入式软件编写,最终实现一个能测量和显示波形的设备。对于电子、自动化或嵌入式方向的学生和爱好者来说,自己动手“搓”一个示波器,是深入理解模拟信号采集、数字信号处理和嵌入式系统开发的绝佳途径。
项目的核心在于将模拟世界的连续信号,通过STM32的ADC(模数转换器)转换为数字量,再经过一系列处理,最终在屏幕上还原出波形。整个过程涉及硬件电路设计、ADC采样、数据处理、显示驱动等多个环节。本文将带你完整复盘这个项目的关键点,并提供可参考的硬件原理图和核心代码,让你不仅能看懂,更能动手复现或改进。
如果你正为嵌入式课程设计、电子竞赛或毕业设计寻找一个有挑战性又有趣的项目,或者单纯想深入理解示波器的工作原理和STM32的高级应用,那么这篇文章值得你仔细阅读。我们将重点关注硬件电路如何搭建、代码如何组织、关键参数如何调试,以及最终能达到怎样的测量效果。
1. 核心能力速览
在深入细节之前,我们先通过一个表格快速了解这个自制STM32示波器的核心规格和特点。这有助于你判断它是否符合你的需求或学习目标。
| 能力项 | 说明 |
|---|---|
| 核心主控 | STM32F103C8T6(或其他STM32F1系列,具备ADC和足够速度) |
| 模拟带宽 | 理论最高约100kHz(受限于ADC采样率和前端电路) |
| 垂直分辨率 | 取决于ADC位数(通常为12位) |
| 采样率 | 最高可达1MHz(依赖STM32的ADC配置和DMA) |
| 显示方式 | TFT液晶屏(如ILI9341驱动)或OLED屏 |
| 输入耦合 | 直流耦合(可通过电路设计增加交流耦合选项) |
| 输入电压范围 | 通常为0-3.3V(需通过前端衰减/放大电路扩展) |
| 触发功能 | 支持边沿触发(上升沿/下降沿),软件实现 |
| 波形测量 | 可计算并显示频率、周期、峰值、平均值等 |
| 代码架构 | 基于HAL库或标准库,模块化设计 |
| 硬件扩展性 | 留有按键、编码器接口用于控制,可扩展SD卡存储 |
| 适合场景 | 学习、课程设计、毕业设计、低频信号观测、原理验证 |
重要提示:这是一个教学演示性质的简易示波器。其性能(如带宽、精度)无法与商用示波器相比,主要价值在于理解原理和实现过程。
2. 适用场景与使用边界
在开始动手之前,明确这个项目的适用场景和局限性至关重要。
这个项目适合谁?
- 嵌入式/电子信息工程专业学生:作为综合性的课程设计或毕业设计项目,能串联单片机、模拟电路、数字信号处理等多门课程知识。
- 电子爱好者与创客:希望深入理解信号采集与显示原理,并享受从零搭建一个测量工具的成就感。
- STM32学习者:已经掌握GPIO、定时器、中断等基础,想挑战ADC、DMA、LCD驱动等综合应用。
它能解决什么问题?
- 原理学习:直观展示ADC采样、波形重构、触发等示波器核心概念。
- 低频信号观测:可用于观察单片机产生的PWM波、音频信号、传感器输出等低频(百kHz以下)信号。
- 系统调试辅助:在开发其他STM32项目时,可以临时用它来观测某个引脚上的信号变化。
它不适合什么场景?
- 高精度测量:其ADC精度、前端电路线性度、时基稳定性均无法满足精密测量要求。
- 高频信号测量:受限于STM32主频和ADC性能,无法准确捕获MHz以上的信号。
- 高压或危险信号测量:自制电路缺乏必要的隔离和保护,严禁直接测量市电或高压电路,以防损坏设备或危及人身安全。
- 商业或工业用途:其稳定性和可靠性未经严格认证,仅限学习和实验。
安全与合规边界:
- 输入信号电压限制:务必确保输入信号电压在硬件电路设计的安全范围内(通常经过分压后不超过3.3V)。测量未知信号时,建议先使用万用表确认电压。
- 接地与共地问题:测量电路信号时,注意共地问题,避免形成地环路引入干扰或造成短路。
- 静电防护:在焊接和调试过程中,注意静电防护,避免损坏敏感的CMOS器件。
3. 环境准备与前置条件
要成功复现或借鉴这个项目,你需要准备以下软硬件环境。
硬件清单:
- 主控核心:STM32F103C8T6最小系统板(即常见的“蓝色小板”),成本低廉且资源足够。
- 显示模块:一块SPI接口的TFT液晶屏,如1.44寸或2.4寸的ILI9341驱动屏幕。OLED屏(如SSD1306)也可行,但显示波形细节能力较弱。
- 前端信号调理电路:这是关键。你需要搭建一个电路板,包含以下部分:
- 衰减网络:用于测量高于3.3V的信号(例如,用1:10的分压电阻)。
- 电压抬升电路:因为STM32的ADC只能测量0-3.3V的正电压,而信号常有负电压部分。通常使用运放(如LM358)搭建一个加法器,将信号整体抬升1.65V(Vref/2),使-1.65V~+1.65V的信号对应ADC的0~3.3V输入。
- 低通滤波:简单的RC滤波,用于抗混叠,滤除高于采样频率一半的信号成分。
- 输入接口:BNC接头或简单的探针接口。
- 控制部件:几个按键或一个旋转编码器,用于调节时基、电压档位、触发模式等。
- 电源:5V USB供电,经LDO(如AMS1117-3.3)转换为3.3V为系统供电。
- 焊接与调试工具:电烙铁、万用表、示波器( ironically,你可以先用别人的示波器来调试你自己的示波器)。
软件与环境:
- 集成开发环境(IDE):Keil MDK-ARM 或 STM32CubeIDE。前者在国内更普及,后者是ST官方免费工具且集成CubeMX。
- STM32CubeMX:强烈推荐使用。用于图形化配置引脚、时钟、ADC、DMA、SPI等外设,生成初始化代码,能极大减少底层配置工作量。
- 串口调试助手:如XCOM、SSCOM,用于调试阶段打印数据。
- 电路设计软件:立创EDA、Altium Designer或KiCad,用于绘制原理图和PCB。
4. 硬件电路设计详解
硬件是基础,一个稳定可靠的前端电路决定了示波器的性能下限。
4.1 核心思想:信号调理
STM32的ADC输入范围是0到Vref(通常接3.3V)。为了测量交流信号(包含负电压),我们需要一个“电平移位”电路,将信号整体向上平移。例如,要测量±1V的信号,我们将其加上1.65V的偏置,变成0.65V~2.65V的信号,这样就能被ADC正确采集。采集到的数字值在软件中再减去这个偏置,即可还原原始波形。
4.2 原理图关键部分
以下是核心调理电路的简化示意图和分析:
输入 | ├───[R1]───┐ | | [C1] [R2] (分压与隔直,可选) | | └───┬───────┘ | └───[运放同相输入端] | [偏置电压1.65V]──[R3]──┐ | | GND [运放输出]───→ ADC输入引脚 | | ... [反馈网络](这是一个概念图,实际运放电路需连接电源、配置增益等)
具体模块说明:
- 输入保护与衰减:在输入端串联一个电阻(如1kΩ)并配合TVS二极管,可以起到一定的限流和过压保护作用。如果需要测量更高电压,则需要精密电阻分压网络(如9:1衰减)。
- 交流耦合(可选):通过一个电容(C1)串联输入,可以隔离信号的直流分量,只测量交流部分。这通常通过一个开关或跳线来选择。
- 电平移位与缓冲:这是核心。使用一个单电源运放(如LMV358)构成同相加法器电路。一路输入是待测信号,另一路输入是由电阻分压产生的1.65V基准电压。运放的作用是提供高输入阻抗(减少对被测电路的影响)和低输出阻抗(驱动ADC)。
- 低通滤波:在运放输出端加入一个RC低通滤波器,其截止频率应略高于你期望测量的最高信号频率,以抑制高频噪声和防止混叠。
PCB设计注意事项:
- 模拟地与数字地:建议使用单点连接。将ADC、运放、基准电压源的地归为“模拟地”,STM32数字部分的地归为“数字地”,在电源入口处通过一个0欧电阻或磁珠连接。
- 电源去耦:在运放和STM32的电源引脚附近,务必放置一个0.1uF的陶瓷电容和一个10uF的钽电容,以滤除高频和低频噪声。
- 信号走线:模拟信号走线尽量短,远离数字信号线(如时钟、SPI线),避免平行走线。
5. 软件架构与代码解析
软件部分负责驱动ADC采样、处理数据、控制显示和用户交互。代码应采用模块化设计。
5.1 工程结构
一个清晰的工程目录有助于管理:
/Project ├── /Core │ ├── Src │ │ ├── main.c │ │ ├── adc.c │ │ ├── dma.c │ │ ├── lcd.c │ │ ├── trigger.c │ │ └── ui.c │ └── Inc (对应的头文件) ├── /Drivers (STM32 HAL库文件) ├── /Hardware (硬件驱动,如按键、编码器) └── /Utilities (通用工具函数)5.2 核心模块代码示例
1. ADC与DMA配置(使用CubeMX生成后补充)ADC配置为连续扫描模式,使用DMA将转换结果自动搬运到内存中的缓冲区。这是实现高速采样的关键。
// 在 main.c 或 adc.c 中 #define ADC_BUFFER_SIZE 1024 // 采样缓冲区大小 uint16_t adc_buffer[ADC_BUFFER_SIZE]; // DMA目标缓冲区 // 启动ADC和DMA的代码片段(通常在初始化函数中) HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);这段代码启动ADC1,并让DMA自动将转换结果填充到adc_buffer数组中。采样率由ADC的时钟分频和采样周期决定,可以在CubeMX中配置。
2. 触发处理触发功能决定波形何时开始显示,是示波器稳定显示波形的关键。一个简单的软件边沿触发实现如下:
// trigger.c typedef enum { TRIG_MODE_AUTO, TRIG_MODE_NORMAL, TRIG_MODE_SINGLE } TrigMode_t; typedef enum { TRIG_EDGE_RISING, TRIG_EDGE_FALLING } TrigEdge_t; uint8_t Trigger_Check(uint16_t *buffer, uint32_t size, uint16_t trig_level, TrigEdge_t edge) { static uint8_t last_state = 0; uint8_t current_state; for (uint32_t i = 1; i < size; i++) { current_state = (buffer[i] > trig_level) ? 1 : 0; if (edge == TRIG_EDGE_RISING) { if (last_state == 0 && current_state == 1) { // 找到上升沿,返回触发位置 return i; } } else { if (last_state == 1 && current_state == 0) { // 找到下降沿,返回触发位置 return i; } } last_state = current_state; } return 0; // 未找到触发点 }该函数遍历采样缓冲区,寻找超过预设触发电平(trig_level)的边沿。找到后,返回触发点在缓冲区中的位置,后续显示波形时就从这里开始。
3. 波形显示与UI在LCD上绘制波形,本质是将ADC缓冲区中的数据点用线段连接起来。
// lcd.c 或 ui.c void Draw_Waveform(uint16_t *data, uint32_t data_size, uint16_t trig_pos) { uint16_t prev_x, prev_y, curr_x, curr_y; uint16_t screen_height = LCD_GetHeight(); uint16_t screen_width = LCD_GetWidth(); uint16_t voltage_range = 4096; // 12位ADC满量程 float volts_per_div = 0.5; // 每格电压,可调 float time_per_div = 0.001; // 每格时间,可调 (1ms/div) // 清屏或清空波形区域 LCD_Clear(BLACK); // 绘制网格 Draw_Grid(); // 计算缩放和偏移 uint16_t vertical_scale = (screen_height / 8) / (volts_per_div * (voltage_range / 3.3)); uint16_t horizontal_scale = (screen_width / 10) / (time_per_div * (data_size / get_sample_rate())); uint16_t vertical_offset = screen_height / 2; // 零点在屏幕中央 // 从触发点开始绘制 uint32_t start_index = trig_pos; prev_x = 0; prev_y = vertical_offset - ((int16_t)data[start_index] - 2048) * vertical_scale / voltage_range; // 假设2048对应0V for (uint32_t i = 1; i < screen_width && (start_index + i) < data_size; i++) { curr_x = i; uint32_t data_index = start_index + i; // 将ADC值转换为屏幕Y坐标,注意减去直流偏置(2048) curr_y = vertical_offset - ((int16_t)data[data_index] - 2048) * vertical_scale / voltage_range; // 在两点之间画线 LCD_DrawLine(prev_x, prev_y, curr_x, curr_y, GREEN); prev_x = curr_x; prev_y = curr_y; } }这段代码是高度简化的示例。实际项目中,你需要管理时基(每格时间)、电压档位(每格电压)、波形在屏幕上的起始位置等。Draw_Grid()函数用于绘制背景网格。
4. 测量与计算在稳定显示波形后,可以计算一些基本参数:
// measurement.c float Calculate_Frequency(uint16_t *data, uint32_t size, float sample_rate) { uint32_t zero_crossings = 0; uint8_t last_state = (data[0] > 2048) ? 1 : 0; // 2048为中间值 for (uint32_t i = 1; i < size; i++) { uint8_t current_state = (data[i] > 2048) ? 1 : 0; if (last_state == 0 && current_state == 1) { zero_crossings++; } last_state = current_state; } if (zero_crossings < 2) return 0.0; // 频率 = (过零点次数 / 2) / (数据点数 / 采样率) return (zero_crossings / 2.0) / (size / sample_rate); } void Calculate_Vpp(uint16_t *data, uint32_t size, uint16_t *min, uint16_t *max) { *min = 0xFFFF; *max = 0; for (uint32_t i = 0; i < size; i++) { if (data[i] < *min) *min = data[i]; if (data[i] > *max) *max = data[i]; } }6. 系统集成与调试流程
将硬件和软件组合起来,并按步骤调试是成功的关键。
6.1 上电前检查
- 目视检查:检查PCB有无短路、断路、虚焊,特别是电源和地之间。
- 电源测试:不插主控,先上电,用万用表测量3.3V、1.65V基准电压等关键点是否正常。
6.2 分模块调试
- 显示模块:先编写一个简单的LCD测试程序,画点、线、矩形、显示文字,确保屏幕驱动正常。
- ADC静态测试:将ADC输入引脚通过一个电位器接到3.3V和GND之间,编写代码读取ADC值并通过串口打印。旋转电位器,观察打印值是否在0-4095间线性变化。
- 前端电路测试:使用信号发生器或另一块单片机产生一个已知的简单信号(如1kHz正弦波),用商用示波器测量前端电路输出端(即STM32 ADC引脚处)的波形,确保电平移位和幅度符合预期。
- ADC动态采样测试:将测试信号接入,用DMA连续采样,并通过串口将大量采样数据发送到电脑,用Python(Matplotlib)或串口绘图工具绘制波形,验证采样是否正常。
6.3 系统联调
- 基础波形显示:将采样数据直接映射到屏幕,先不加入触发。你应该能看到一个滚动的波形。
- 实现触发:加入触发功能,使波形稳定。
- 添加控制:通过按键或编码器,实现时基(秒/格)和电压档位(伏/格)的调节。这需要动态调整波形绘制的缩放系数。
- 添加测量:在波形稳定显示后,实现频率、峰峰值等参数的自动测量与屏幕显示。
7. 性能优化与提升方向
基础功能实现后,可以考虑以下优化:
提高采样率:
- 使用STM32的ADC双模式或交替模式。
- 优化时钟树,在允许范围内提高ADC时钟。
- 减少ADC采样周期。
- 使用定时器触发ADC,实现精确的等间隔采样。
改善显示效果:
- 实现波形余辉(模拟示波器效果)。
- 添加插值算法(如正弦插值)使波形更平滑。
- 实现双通道显示(需要两个ADC或一个ADC多路复用)。
增强测量功能:
- 上升时间、占空比测量。
- FFT频谱分析(需要移植DSP库)。
- 波形存储与回放(需要外接SD卡)。
改进用户体验:
- 设计更友好的菜单系统。
- 添加自动设置(AutoSet)功能。
- 支持光标测量。
8. 常见问题与排查方法
在制作过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 屏幕无显示 | 电源接反/未接、SPI线接错、复位或背光控制错误、初始化序列不对 | 1. 检查电源电压。 2. 用逻辑分析仪或示波器抓取SPI时序。 3. 核对LCD驱动芯片型号及初始化代码。 | 1. 确保接线正确。 2. 查阅LCD数据手册,逐条核对初始化命令。 3. 降低SPI时钟频率测试。 |
| ADC采样值不变或全为0 | ADC引脚配置错误、参考电压未接、DMA未正确配置、采样时间太短 | 1. 用万用表测量ADC引脚实际电压。 2. 检查CubeMX中ADC通道和DMA配置。 3. 改用轮询模式读取ADC,排除DMA问题。 | 1. 检查硬件连接和CubeMX配置。 2. 增加ADC采样周期。 3. 确保 HAL_ADC_Start_DMA在初始化后被调用。 |
| 波形严重失真 | 前端运放电路工作点不对、电源噪声大、输入信号超出范围、低通滤波截止频率过低 | 1. 用已知好的示波器测量前端电路各点波形。 2. 测量运放电源引脚上的纹波。 3. 输入一个直流电压,看ADC读数是否线性。 | 1. 调整运放偏置电路电阻。 2. 加强电源滤波,使用线性稳压电源。 3. 确保输入信号在调理电路允许范围内。 |
| 触发功能不稳定 | 触发电平设置不当、信号噪声大、触发判断算法有缺陷 | 1. 通过串口打印触发函数找到的触发点位置。 2. 观察信号在触发电平附近的噪声。 | 1. 实现触发滞后(Hysteresis),避免噪声引起误触发。 2. 对信号进行软件滤波后再进行触发判断。 |
| 测量频率误差大 | 采样率不准确、过零点检测受噪声影响、信号非周期性 | 1. 校准系统时钟(HSE)。 2. 用标准信号源(如函数发生器)输入固定频率信号测试。 3. 尝试更稳健的频率算法(如过零+周期测量)。 | 1. 使用更高精度的外部晶振。 2. 在频率计算前对数据进行平滑滤波。 3. 实现多周期平均测量。 |
| 系统运行一段时间后死机 | 堆栈溢出、中断冲突、DMA传输完成中断未处理、硬件不稳定 | 1. 检查.map文件,看堆栈设置是否足够。2. 检查中断优先级。 3. 在DMA传输完成中断和半传输中断中添加标志位和超时判断。 | 1. 增大堆栈大小。 2. 合理分配中断优先级,避免嵌套过深。 3. 添加看门狗(IWDG)防止程序跑飞。 |
9. 项目总结与资源获取
通过这个“手搓”示波器的项目,你能够将STM32的ADC、DMA、定时器、SPI等外设融会贯通,并深入理解信号调理、采样定理、触发显示等模拟与数字混合系统的核心概念。虽然最终成品的性能有限,但其学习价值远超一个简单的开发板实验。
最值得尝试的点:
- 完整的系统思维:从需求分析、硬件选型、电路设计、PCB绘制、软件架构到调试优化,体验一个完整电子产品的开发流程。
- 软硬件协同调试:学会使用万用表、示波器、逻辑分析仪等工具定位问题,理解软件逻辑如何受硬件制约。
- 解决真实问题:在调试过程中遇到的噪声、失真、不稳定等问题,都是理论学习中难以触及的实战经验。
最先应该验证的功能:建议按照“LCD显示 -> ADC静态读取 -> 前端电路验证 -> DMA连续采样 -> 基础绘图 -> 触发稳定 -> 参数测量 -> 交互控制”的顺序逐步推进。每完成一步,都进行充分测试。
最容易踩的坑:
- 前端电路设计:电平移位电路计算错误,导致信号超出ADC输入范围。
- 电源噪声:开关电源的噪声会耦合到模拟部分,严重影响小信号测量。
- 地线处理:模拟地和数字地混乱,引入干扰。
- 软件时序:ADC采样、DMA传输、数据处理、LCD刷新之间的时序冲突,导致显示卡顿或数据错乱。
资源获取与下一步:
- 原理图与代码:由于篇幅限制,完整的工程文件无法在文中展示。你可以在开源硬件平台(如GitHub、Gitee)上搜索“STM32 Oscilloscope”或“STM32示波器”,能找到许多优秀的开源参考项目。
- 深入学习:在完成基本功能后,可以挑战更高阶的功能,如FFT频谱显示、自动测量、二次开发接口(通过串口发送波形数据到PC)等。
- 工程化改进:考虑设计一个美观的外壳,将PCB、屏幕、按键集成在一起,制作成一个真正的便携设备。
动手实践是学习嵌入式系统最好的方式。这个项目就像一把钥匙,帮你打开通往更复杂嵌入式系统设计的大门。建议收藏本文,在实践的不同阶段回来查阅对应的章节,希望能为你排忧解难。