STM32简易示波器设计

AI助手已提取文章相关产品:

STM32F103C6 示波器:基于Keil5的单片机源码设计与实现

在电子开发的世界里,能实时“看见”信号往往是调试成败的关键。可真正的数字示波器动辄上千元,体积庞大,对于学生、维修人员或DIY爱好者来说,既不经济也不便携。有没有可能用一块十几块钱的STM32“蓝丸”板子,搭出一个能看波形的小型示波器?答案是肯定的——而且还能跑在Keil MDK-5环境下,代码清晰、结构完整,拿来就能改。

这正是我们今天要深入探讨的项目: 基于STM32F103C6的简易示波器系统 。它不是玩具,而是一个从模拟输入到ADC采样、定时触发、DMA传输,再到OLED显示的完整闭环。虽然性能无法媲美专业设备,但在音频范围内的信号观测、教学实验和快速验证场景中,已经足够实用。

整个系统的灵魂在于如何高效地采集数据而不拖累CPU。如果每采一个点都靠软件轮询或中断,不仅精度差,还容易丢失数据。所以,我们必须把硬件外设的能力发挥到极致——让定时器自动触发ADC,再由DMA悄悄搬走结果,主程序只负责最后的“画图”。这种软硬协同的设计思路,才是嵌入式系统真正的魅力所在。

先来看核心部分:ADC配置。STM32F103C6内置的是12位逐次逼近型ADC,参考电压通常为3.3V,理论分辨率为 $ \frac{3.3}{4096} \approx 0.8\,\text{mV} $,最大采样率可达1MSPS(受限于ADC时钟不超过14MHz)。但实际使用中,为了兼顾精度和稳定性,我们会适当延长采样时间,并借助DMA实现连续采集。

下面这段初始化代码就体现了典型的工程实践:

// adc.c
#include "adc.h"

uint16_t ADC_Buffer[100]; // 只采100个点,够用就好

void ADC1_DMA_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // PA0作为模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // DMA1 Channel1 绑定 ADC1_DR
    DMA_DeInit(DMA1_Channel1);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_Buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = 100;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;        // 循环模式,关键!
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1);
    DMA_Cmd(DMA1_Channel1, ENABLE);

    // ADC独立模式,单通道连续转换
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);

    ADC_DMACmd(ADC1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);

    // 校准很重要,尤其在电源波动时
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

这里有几个关键点值得强调:

  • DMA循环模式(Circular Mode) :一旦开启,ADC每完成一次转换,DMA就会自动将结果写入缓冲区并指针递增,满了回到开头。这意味着我们不需要反复启动ADC或处理中断,数据流天然稳定。
  • 长采样时间(239.5周期) :虽然牺牲了一些速度,但对于阻抗较高的外部信号源,更长的采样时间可以减少误差,提升有效位数(ENOB)。
  • ADC校准步骤不可省略 :特别是在冷启动或电压变化后,执行一次复位+启动校准能显著改善线性度。

当然,光有ADC还不够。采样必须是等时间间隔的,否则还原出来的波形就是扭曲的。很多人第一反应是用 while 循环加 Delay_us() ,但这在多任务或中断环境下完全不可控。正确的做法是让 定时器来驱动ADC

以TIM2为例,将其配置为更新事件触发源(TRGO),连接到ADC的外部触发输入。这样每次定时器溢出,就会产生一个脉冲,自动启动一次ADC转换。整个过程无需CPU干预,时间精度仅取决于定时器时钟。

void TIM2_ConfigForADC(void) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 72MHz / (71+1) = 1MHz → 每1μs计数一次
    // ARR=99 → 溢出周期100μs → 触发频率10kHz
    TIM_TimeBaseStructure.TIM_Period = 99;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 关键:将更新事件映射到TRGO引脚
    TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);

    TIM_Cmd(TIM2, ENABLE);
}

这个配置实现了10ksps的采样率,适合观察音频信号(20Hz~20kHz)。如果你需要更高采样率,比如100ksps,可以把 TIM_Period 改为9,其他不变。但要注意,STM32F103的ADC在14MHz时钟下,最短总转换时间为1.17μs左右(采样时间+12.5周期),所以极限大约在800ksps以内,且随着采样率提高,精度会下降。

数据有了,接下来就是“可视化”。受限于STM32F103C6的资源(仅20KB SRAM),我们不可能接大屏,但一块常见的0.96英寸SSD1306 OLED就刚刚好。它通过I2C通信,只需要两根线(SCL/SDA),分辨率128×64,足够绘制一条清晰的波形曲线。

OLED驱动本身并不复杂,难点在于 绘图效率与刷新节奏的平衡 。频繁清屏和刷新会导致屏幕闪烁,用户体验很差。因此,合理的做法是:

  1. 检测到一组新数据(如100个点)已就绪;
  2. 将ADC值映射到屏幕Y轴(注意翻转,因为屏幕坐标原点在左上角);
  3. 逐点绘制或连成折线;
  4. 一次性刷新显存。

下面是简化后的绘图函数:

void OLED_DrawWaveform(uint16_t *data, uint16_t len) {
    int i;
    uint8_t y;
    const uint8_t x_max = 128;

    OLED_Clear();  // 视情况可改为局部擦除

    for(i = 0; i < len && i < x_max; i++) {
        // 映射0~4095到63~0(Y轴向下为正)
        y = (uint8_t)(63 - (data[i] * 63 / 4095));
        if(y > 63) y = 63;

        OLED_SetPixel(i, y);
    }

    OLED_Refresh();  // 必须调用,否则无显示
}

虽然这只是一个个像素点,但已经能直观反映信号形态。进一步优化的话,可以用Bresenham算法连接相邻点形成折线,视觉效果更连贯。

整个系统的运行流程非常清晰:

  • 上电后依次初始化GPIO、ADC+DMA、定时器、I2C和OLED;
  • ADC进入连续转换模式,由TIM2定时触发;
  • DMA自动填充 ADC_Buffer
  • 主循环中判断是否采集完一轮(可通过设置标志位或检查DMA半传输/全传输中断);
  • 若有新数据,则调用绘图函数刷新屏幕;
  • 延时几毫秒控制帧率(如30fps),避免过度刷新。

当然,实际搭建过程中总会遇到各种问题。比如刚上电时波形抖动严重,可能是电源噪声或接地不良;或者高频信号出现混叠,那就得加个RC低通滤波器做抗混叠处理。还有输入电压超限的问题——PA0只能承受0~3.3V,若要测更高电压,必须加分压电阻网络(如10k+10k实现2:1分压)。

另一个常见问题是 触发不稳定 。目前系统处于自由运行模式(Free Run),即不断采集并刷新,缺乏同步机制。改进方向可以是加入边沿触发逻辑:比较最近两个采样点的变化趋势,当满足上升沿或下降沿条件时才开始显示一帧波形。这虽然增加了软件复杂度,但能让波形“稳定下来”,更接近真实示波器的体验。

从工程角度看,这个设计在资源利用上做了不少权衡:

  • 采样深度限制 :SRAM只有20KB,假设每个采样点占2字节,最多缓存约10k点。对于瞬态信号记录显然不够,但用于实时显示绰绰有余。
  • 双通道扩展可行性 :理论上可通过扫描模式采集多个通道,但会降低等效采样率。若追求同步双通道,建议使用双ADC或外部多路复用器。
  • 未来升级路径 :加入FFT模块可实现频谱分析;通过串口上传数据到PC端进行存储和分析;甚至结合RTOS实现多任务调度。

Keil MDK-5在这个项目中的优势也十分明显。标准外设库(StdPeriph)虽然老旧,但结构清晰,易于理解底层寄存器操作。配合Keil强大的调试功能(如变量监控、内存查看、逻辑分析仪支持),开发者能快速定位问题。更重要的是, .uvprojx 工程文件便于团队协作和版本管理,适合教学和二次开发。

最终成品的成本几乎可以忽略不计:STM32F103C6最小系统板约10元,OLED屏15元,加上几颗电阻电容,整套不到30元。但它带来的价值远不止于此——它是理解嵌入式系统中 模拟前端、实时采集、DMA传输和图形输出 之间协作关系的最佳实践案例。

对于高校电子类课程而言,这样的项目比单纯的“点亮LED”更有吸引力。学生不仅能学到ADC、定时器、I2C等外设的配置方法,更能体会到“系统思维”的重要性:任何一个模块都不能孤立存在,必须考虑时序、带宽、资源占用和交互逻辑。

而对于电子爱好者来说,这是迈向自制测试仪器的第一步。你可以在此基础上增加触控菜单、自动量程切换、峰值检测等功能,甚至把它封装成一个真正可用的便携工具。

这种高度集成、软硬协同的设计理念,正在成为现代智能设备的主流趋势。无论是智能手表的心率监测,还是物联网节点的传感器采集,其底层逻辑都与此类似。掌握这样一个小而完整的系统,意味着你已经跨过了嵌入式开发的入门门槛。

这也正是开源硬件的魅力所在:用最低的成本,打开最大的可能性。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值