1. 项目概述
这个基于STM32的OLED简易示波器项目,是我最近完成的一个嵌入式系统开发实践。它能够在128×64分辨率的OLED屏幕上实时显示输入信号的波形,并支持幅值、时基和基准点的调节。作为一个低成本、便携式的信号观测工具,它特别适合电子爱好者用于日常电路调试和信号分析。
核心功能包括:
- 实时波形显示(最高50kHz采样率)
- 可调节的电压档位(0.25V/格、0.5V/格、1V/格)
- 可切换的时基设置(0.25ms/格、0.5ms/格、1ms/格)
- 自动频率测量与采样率适配
- 清晰的栅格参考线
硬件配置相当精简,只需要:
- STM32F103ZET6开发板(其他F103系列也可)
- 0.96寸OLED显示屏(SSD1306驱动)
- 6根杜邦线
- 信号源(函数发生器或被测电路)
2. 硬件设计与连接
2.1 主控芯片选型
选择STM32F103ZET6主要基于以下考虑:
- 内置12位ADC,最高1MHz采样率
- 充足的SRAM(64KB)存储采样数据
- 丰富的定时器资源用于精确控制采样时序
- 广泛的社区支持和资料
提示:如果使用其他型号,需确认ADC性能和定时器配置是否满足需求。F103C8T6(蓝桥杯开发板常用型号)也可实现基本功能,但采样深度会受限于较小的RAM。
2.2 关键硬件连接
接线示意图:
code复制OLED STM32 信号源
SCL → PB6
SDA → PB7
VCC → 3.3V
GND → GND
信号 → PA1(ADC1_IN1)
特别注意:
- OLED必须使用3.3V供电,5V会损坏屏幕
- ADC输入引脚(PA1)需避免超过3.3V,必要时添加分压电路
- 对于高频信号(>10kHz),建议使用屏蔽线减少干扰
2.3 信号调理电路(可选)
虽然可以直接连接信号源,但增加以下电路能提升测量质量:
- 电压跟随器(提高输入阻抗)
- 反相保护二极管(防止负电压)
- RC低通滤波(抗混叠滤波)
一个简单的保护电路设计:
code复制信号输入 → 10kΩ电阻 → 1N4148二极管到GND
↓
100nF电容 → GND
↓
PA1(ADC输入)
3. 软件架构设计
3.1 主程序流程
c复制int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM3_Init();
// 外设初始化
OLED_Init();
HAL_TIM_Base_Start(&htim3);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, SAMPLES);
// 主循环
while (1) {
HandleKeys(); // 处理按键输入
OLED_main(); // 波形显示处理
DrawGrid(); // 绘制参考栅格
}
}
3.2 关键模块说明
3.2.1 ADC采样配置
- 使用DMA传输避免CPU干预
- 定时器触发确保采样间隔精确
- 双缓冲技术防止数据冲突
ADC初始化要点:
c复制hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE; // 由定时器触发
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
3.2.2 定时器配置
TIM3用于触发ADC采样:
c复制htim3.Instance = TIM3;
htim3.Init.Prescaler = 72-1; // 72MHz/72 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 20-1; // 50kHz (1MHz/20)
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim3);
注意:定时器周期值=(TIMxCLK/(PSC+1))/目标采样率-1
4. 核心算法实现
4.1 波形显示处理
OLED_main()函数的工作流程:
- 擦除上一帧波形(避免残影)
- 从DMA缓冲区复制稳定数据
- 根据当前幅值设置计算Y坐标
- 使用Bresenham算法绘制波形线
- 刷新OLED显存
关键代码段:
c复制for(int x = 0; x < 127; x++) {
uint16_t y1 = 63-(adc_clear[x]*63/4095*volt);
uint16_t y2 = 63-(adc_clear[x+1]*63/4095*volt);
DrawLine(x, y1, x+1, y2, 1); // 绘制当前波形
}
4.2 Bresenham画线算法
这个算法的高效实现是波形显示流畅的关键:
c复制void DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) {
int dx = abs(x2 - x1);
int dy = abs(y2 - y1);
int sx = (x1 < x2) ? 1 : -1;
int sy = (y1 < y2) ? 1 : -1;
int err = dx - dy;
while(1) {
OLED_DrawPoint(x1, y1, color);
if(x1==x2 && y1==y2) break;
int e2 = 2*err;
if(e2 > -dy) { err -= dy; x1 += sx; }
if(e2 < dx) { err += dx; y1 += sy; }
}
}
算法特点:
- 纯整数运算,无浮点数
- 每帧可绘制多条线段而不影响性能
- 支持任意方向线段绘制
4.3 动态采样率调整
自动适应信号频率的算法:
c复制void AutoAdjustSampleRate(float measured_freq) {
uint32_t target_rate;
if(measured_freq < 150) target_rate = 2000;
else if(measured_freq < 500) target_rate = 10000;
else if(measured_freq < 2000) target_rate = 50000;
else target_rate = 80000;
if(target_rate != (uint32_t)current_sample_rate) {
SetSampleRate(target_rate);
}
}
频率测量采用过零检测法:
c复制// 计算信号频率
for(int i = 1; i < len; i++) {
if(buffer[i-1] < trigger_level && buffer[i] >= trigger_level) {
zero_cross[zero_count++] = i;
if(zero_count >= 5) break;
}
}
...
*freq = current_sample_rate / avg_samples;
5. 关键问题与解决方案
5.1 波形抖动问题
现象:低频信号时波形左右移动不稳定
原因:采样率与信号频率不成整数倍关系,导致相位滑动
解决方案:
- 实时测量信号频率
- 动态调整采样率使fs=N×f
- 每个周期采集固定点数(如N=50)
优化后的效果:
- 1kHz以下信号稳定显示
- 波形不再左右漂移
- 测量精度提高
5.2 显示残影问题
现象:改变幅值时旧波形残留
原因:直接清屏导致闪烁,不清屏则残留
创新解决方案:
c复制// 精准擦除上一帧波形
for(int x = 0; x < 127; x++) {
uint16_t y1 = 63-(prev_buffer[x]*63/4095*prev_volt);
uint16_t y2 = 63-(prev_buffer[x+1]*63/4095*prev_volt);
DrawLine(x, y1, x+1, y2, 0); // 用黑色覆盖
}
5.3 栅格闪烁问题
现象:重绘栅格时屏幕闪烁
优化方案:
- 将实线栅格改为虚线
- 减少栅格重绘频率
- 采用点阵式绘制
改进后的栅格绘制:
c复制void DrawGrid() {
// 垂直虚线(每10像素一条)
for(x = 0; x < 128; x += 10) {
for(y = 0; y < 64; y += 4) {
OLED_DrawPoint(x, y, 1);
}
}
...
}
6. 性能优化技巧
6.1 存储优化
使用双缓冲技术避免数据竞争:
- DMA直接写入adc_buffer
- 处理时复制到adc_clear
- 使用标志位同步
c复制if(adc_ready_flag) {
memcpy(adc_clear, adc_buffer, 128*2);
adc_ready_flag = 0;
}
6.2 绘制优化
- 局部刷新代替全屏刷新
- 使用快速画点函数
- 减少不必要的重绘
6.3 采样率自适应
建立频率-采样率对应表:
| 信号频率范围 | 推荐采样率 | 每周期点数 |
|---|---|---|
| 1-150Hz | 2kHz | 20-200 |
| 150-500Hz | 10kHz | 20-66 |
| 500Hz-2kHz | 50kHz | 25-100 |
| >2kHz | 80kHz | 40 |
7. 扩展功能实现
7.1 触发功能
添加边沿触发代码:
c复制// 等待上升沿
while(ADC_value < trigger_level);
while(ADC_value >= trigger_level);
// 开始采集
7.2 测量功能
可扩展的测量参数:
- Vpp(峰峰值)
- Vavg(平均值)
- 频率
- 占空比
7.3 持久化设置
使用Flash存储用户偏好:
c复制// 保存设置
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data);
// 读取设置
data = *(__IO uint16_t*)addr;
8. 实际测试结果
测试条件:
- 信号源:FY6900函数发生器
- 测试频率:10Hz-10kHz
- 输入幅度:0.1V-3V
性能指标:
| 参数 | 测量值 |
|---|---|
| 最大采样率 | 80kHz |
| 电压精度 | ±0.05V |
| 频率测量误差 | <1%(>100Hz) |
| 显示延迟 | <50ms |
波形显示效果:
- 正弦波:光滑连续
- 方波:上升沿清晰
- 三角波:线性度良好
9. 开发经验分享
9.1 调试技巧
-
使用串口打印关键变量
c复制printf("Freq: %.1fHz, Sample: %lu\n", freq, current_sample_rate); -
利用LED指示程序状态
c复制
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); -
分段测试各功能模块
9.2 常见问题排查
-
无波形显示
- 检查ADC引脚连接
- 确认DMA配置正确
- 测试ADC原始值是否正常
-
波形失真
- 检查信号幅度是否超限
- 确认采样率足够高
- 检查定时器配置
-
屏幕闪烁
- 优化栅格绘制频率
- 使用局部刷新
- 检查OLED初始化参数
9.3 性能提升建议
- 使用硬件SPI驱动OLED(提升刷新率)
- 添加FPU支持(加速浮点运算)
- 实现多级缓存(提升高频信号表现)
- 优化Bresenham算法(汇编级优化)
10. 项目总结与展望
这个STM32 OLED示波器项目实现了一个功能完整、成本低廉的便携式测量工具。通过这个项目,我深入掌握了以下技术:
- STM32的ADC与DMA配合使用
- 定时器精确控制采样时序
- 嵌入式图形显示优化技巧
- 信号处理基础算法
未来可能的改进方向:
- 增加FFT频谱显示功能
- 实现触摸屏控制
- 添加SD卡存储功能
- 设计专用PCB提升稳定性
这个项目的全部代码和原理图已开源,希望能为嵌入式开发者提供一个有价值的参考案例。在实际应用中,它已经帮助我快速调试了多个电路项目,显著提高了工作效率。