1. 项目概述
去年在调试一个电机控制项目时,我经常需要观察PWM信号的波形质量。虽然手边有专业示波器,但每次都要拖着笨重的设备来回跑实在麻烦。于是萌生了自己做一个便携式示波器的想法,最终用STM32F103C8T6(蓝色药丸开发板)配合0.96寸OLED,实现了一个采样率100KHz、带宽20KHz的简易示波器。这个巴掌大的设备现在成了我调试数字电路的随身利器,今天就把完整实现过程分享给大家。
这个示波器核心功能包括:
- 单通道模拟信号采集(通过STM32内置ADC)
- 实时波形显示(OLED 128x64分辨率)
- 基础测量功能(峰峰值、频率估算)
- 触发电平调节
- 时基缩放控制
整套方案成本不到50元,代码量约800行,特别适合用来监测传感器输出、检查数字信号质量等日常调试场景。下面我会从硬件选型到软件优化,详细拆解每个环节的实现要点。
2. 硬件设计与关键元件选型
2.1 主控芯片选择
为什么选择STM32F103C8T6?
- ADC性能:12位分辨率,1μs转换时间,支持最高1MHz采样率(实际稳定工作在500KHz以下)
- 性价比:10元左右的售价,自带硬件SPI接口驱动OLED
- 生态完善:标准库和HAL库支持完善,便于快速开发
注意:STM32F1系列的ADC在超过500KHz采样时线性度会下降,建议工作在设计采样率的2倍以上(本项目设计100KHz,实际ADC时钟配置为250KHz)
2.2 显示模块对比
测试过三种常见显示屏:
| 类型 | 分辨率 | 刷新率 | 功耗 | 价格 | 最终选择 |
|---|---|---|---|---|---|
| LCD1602 | 16x2字符 | 2Hz | 5mA | 15元 | × |
| OLED 0.96" | 128x64 | 50Hz | 10mA | 18元 | √ |
| TFT 1.44" | 128x128 | 30Hz | 80mA | 35元 | × |
选择SSD1306驱动的OLED主要考虑:
- 自发光特性,比LCD更适合波形显示
- 高对比度(关闭像素完全不耗电)
- 支持硬件SPI,刷新率可达50FPS
2.3 模拟前端设计
简易分压电路实现0-3.3V输入范围:
code复制信号输入 → 10kΩ电阻 → STM32 ADC引脚
↓
10kΩ电阻 → GND
↓
100nF电容(去抖)
关键参数计算:
- 输入阻抗:10kΩ//10kΩ = 5kΩ
- -3dB带宽:1/(2πRC) = 1/(23.145000*0.0000001) ≈ 318Hz
- 实际测试可稳定测量20KHz信号(受ADC采样率限制)
3. 软件架构与核心算法
3.1 系统工作流程
c复制void main() {
hardware_init(); // 初始化ADC、SPI、GPIO等
oled_init(); // 初始化显示屏
while(1) {
trigger_check(); // 检查触发条件
adc_sample(); // 采集256个点
waveform_process(); // 计算峰峰值、频率
oled_refresh(); // 刷新显示
key_scan(); // 处理按键调整
}
}
3.2 ADC采样优化
实现100KHz采样的关键技巧:
- 使用定时器触发ADC采样(TIM2触发ADC1)
- DMA传输避免CPU干预
- 双缓冲机制:当DMA填满缓冲A时自动切换至缓冲B,同时处理A中的数据
配置代码示例:
c复制// TIM2配置 10us周期(100KHz)
TIM_TimeBaseInitTypeDef TIM_InitStruct;
TIM_InitStruct.TIM_Period = 72 - 1; // 72MHz/72 = 1MHz
TIM_InitStruct.TIM_Prescaler = 1;
TIM_TimeBaseInit(TIM2, &TIM_InitStruct);
// ADC1 DMA配置
DMA_InitStructure.DMA_BufferSize = 256;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&adc_buffer;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
3.3 波形显示算法
将ADC值映射到OLED纵坐标:
c复制// 假设ADC值范围0-4095,屏幕高度64像素
y = 63 - (adc_value * 64 / 4096);
// 优化:消除浮点运算
y = 63 - (adc_value >> 6); // 4096/64=64=2^6
动态时基调整实现:
c复制// 时基变量time_base表示每像素点对应的采样间隔
void zoom_in() {
if(time_base > 1) time_base /= 2;
}
void zoom_out() {
if(time_base < 64) time_base *= 2;
}
// 显示时跳过部分采样点
for(int i=0; i<128; i++) {
draw_pixel(i, y_buffer[i * time_base]);
}
4. 关键问题与解决方案
4.1 触发抖动问题
现象:波形在触发点附近出现横向抖动
解决方法:
- 采用迟滞比较器算法:
c复制// 当信号从下向上穿越触发电平时触发
if(prev_sample < trigger_level && current_sample >= trigger_level) {
start_display();
}
- 增加触发滤波计数器,连续3次满足条件才触发
4.2 频率测量误差
实测发现对于1KHz方波,测量结果在980-1020Hz之间波动
优化方案:
- 过零检测+周期平均法
- 对连续8个周期求平均
- 启用定时器输入捕获功能辅助测量
改进后误差<±5Hz(@1KHz)
4.3 显示闪烁优化
原始方案直接刷新整个屏幕会导致明显闪烁
改进措施:
- 局部刷新:只更新波形区域(第10-60行)
- 垂直同步:在屏幕回扫期间更新显存
- 使用硬件SPI(实测比I2C快5倍)
刷新率从15FPS提升到45FPS
5. 实测性能数据
测试条件:信号发生器输出正弦波,探头直接连接开发板
| 信号频率 | 采样率 | 显示效果 | 测量误差 |
|---|---|---|---|
| 100Hz | 100KHz | 稳定 | <1% |
| 1KHz | 100KHz | 稳定 | <0.5% |
| 10KHz | 100KHz | 轻微锯齿 | <2% |
| 20KHz | 100KHz | 明显失真 | <5% |
功耗测试:
- 静态电流:12mA(3.3V供电)
- 最大工作电流:25mA(持续刷新时)
6. 进阶改进方向
-
双通道支持:利用STM32的多个ADC实现
- 需要修改DMA为双缓冲模式
- 显示时用不同颜色区分通道
-
FFT频谱显示:添加arm_math库实现
c复制arm_rfft_fast_instance_f32 fft; arm_rfft_fast_init_f32(&fft, 256); arm_rfft_fast_f32(&fft, adc_buffer, fft_output, 0); -
持久化设置:利用Flash最后一页保存配置
c复制FLASH_ErasePage(0x0801F000); FLASH_ProgramHalfWord(addr, value); -
锂电池供电:添加TP4056充电电路
- 需增加电压检测功能
- 低功耗模式可延长续航
这个项目最让我惊喜的是STM32内置ADC的实际表现——虽然规格书上写着12位分辨率,但通过软件校准和过采样,实际能获得14位有效分辨率。具体做法是对同一信号连续采样16次取平均,这样可以将量化噪声降低4倍(√16),相当于增加2位分辨率。