news 2026/9/9 15:49:55

STM32F1+HAL库自制简易示波器与信号发生器,Proteus仿真全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F1+HAL库自制简易示波器与信号发生器,Proteus仿真全流程

简介:基于STM32F1 HAL库的示波器与信号发生器完整工程,面向嵌入式学习者和电子爱好者,可在Proteus仿真环境中完成电路验证与波形测量。资源共2000个文件,以C语言源码为主(1396个.c与523个.h),涵盖HAL驱动、信号采集、数据处理和波形生成逻辑;另含cpp、Python辅助脚本及说明文档,压缩包约89MB。项目通过ADC采集模拟信号,DAC生成正弦波、方波等波形,并利用Proteus完成软硬件协同仿真,涉及定时器、串口、中断等外设的HAL库调用方式,适合想深入理解STM32F1底层编程与仿真调试的开发者。已有1052人学习下载,工程结构清晰、模块划分合理,Source目录、头文件和脚本分工明确,可直接导入工程编译学习;对课程设计、毕业设计或电子竞赛准备也有较高参考价值,能帮助读者从代码到仿真完整跑通一套信号处理流程。 做过类似项目的人应该都有同感:入门 STM32 后第一个想做的东西,往往不是流水灯,而是一台能“看”波形的工具。市面上随便一台入门示波器都要几百上千,而手里这颗 STM32F1 芯片本身就集成了 ADC、DAC、定时器这些外设,完全有能力做成一个“示波器 + 信号发生器”的双功能调试工具。关键是,还有 Proteus 仿真可以把整套逻辑先在电脑上跑通,不需要立刻花钱买器件。

这篇文章我就完整复盘一下,如何基于 STM32F1 + HAL 库做一个带有采样显示功能的简易示波器,同时用片内 DAC 做一路信号发生器,并解决 Proteus 仿真过程中的几个典型坑。适合正在学 HAL 库、想彻底搞懂 ADC 与 DMA 协同工作的同学,也适合想低成本搭一个调试辅助工具、但又不想一开始就碰硬件的朋友。

1. 为什么要在 F1 上同时做“测”与“发”:项目的价值拆解

1.1 一个工具解决两个调试痛点

嵌入式调试时最常见的两个场景:一是想确认某路 PWM 波形频率对不对、串口波特率是否准确、I2C 的 SCL/SDA 是否在正常跳变,手头却没有示波器;二是调模拟电路时需要稳定的正弦波、三角波或方波输入,但只有简陋的 PWM 输出,连个频率都调不准。

把示波器和信号发生器合在一个 STM32F1 工程里,等于给桌面加了一个最小化的“信号测试台”。示波器部分用 ADC 连续采样外部信号,信号发生器部分用 DAC 或 PWM 输出可调波形,两者共用一套按键菜单和 OLED 显示屏。做这个项目的过程中,ADC、DMA、定时器触发、DDS 原理、波形显示这些知识都会得到一次完整的串联,比我当初照着开发板例程逐个跑一遍有用得多。

1.2 为什么用 HAL 库而不是寄存器或标准库

用 HAL 库做这个项目,最大的优势是 CubeMX 已经把时钟树、外设初始化和引脚复用关系处理好了,我只需要关注核心逻辑本身。比如要配置“定时器触发 ADC”,在寄存器版里需要同时翻参考手册查 TRGO、EXTI、DMA 请求映射三张表,而在 CubeMX 里只需要在 ADC 配置页选择触发源,再在 DMA 设置里勾选循环模式即可。

当然,HAL 库也有自己的脾气,比如回调函数的名字容易记混、某些外设初始化顺序有讲究。但这些恰恰是这个项目最好的学习素材——你用 HAL 库,但仍然需要理解底层为什么要这么配。

2. 先定硬件骨架:DAC 的有无决定了方案分叉

2.1 F103 系列外设差异:不要默认所有 F1 都有 DAC

很多同学一上来就直接打开 CubeMX,选中 STM32F103C8T6 开发板,结果在配置信号发生器时才发现:F103C8T6 根本没有 DAC 外设。这是一个非常容易被忽略的选型坑。

F103 系列根据封装和容量,并不是所有型号都带 DAC。以最常用的几个型号为例:

型号封装FlashDAC备注
STM32F103C8T6LQFP4864KB蓝色板最常见
STM32F103RCT6LQFP64256KB引脚多,带 DAC
STM32F103ZET6LQFP144512KB资源最全
STM32F103VET6LQFP100512KB性价比不错

如果你坚持用 C8T6,信号发生器部分就要走另一条路线:用定时器 PWM + RC 低通滤波模拟 DAC。这个方案不是不行,但波形质量、频率范围都很受限。我在实际做这个项目时选的是带 DAC 的型号做完整功能验证,另外单独写了一份兼容 C8T6 的 PWM 版本代码。Proteus 仿真阶段,直接用带 DAC 的型号最省事。

2.2 CubeMX 工程基础配置

不管最终用哪个型号,CubeMX 里的关键配置项是一致的:

  • 时钟源:外部 HSE 8MHz,系统时钟倍频到 72MHz(实测外部晶振更准;仿真时如果要省去晶振电路,可临时改成 HSI,后面会详细说)。
  • ADC1:单通道,12 位右对齐,扫描模式关闭,连续转换关闭,外部触发选择 TIM2 TRGO,目的是用定时器精确控制每个采样点的间隔。
  • DMA:ADC1 的请求映射到 DMA1 通道 1,方向外设到内存,循环模式,数据宽度半字。
  • TIM2:配置为内部时钟,PSC 和 ARR 根据期望采样率计算,TRGO 选择 Update Event。
  • OLED:我用的 SPI 接口 SSD1306,刷新比 I2C 快很多。
  • 按键:两个 GPIO 输入上拉,一个用来切换菜单,一个用来调参数。

配置完成后,生成的工程骨架能直接编译通过,但这只是开始,真正的核心逻辑在下面的代码组织里。

3. 示波器侧的核心:定时器触发 ADC 与 DMA 双缓冲

3.1 为什么不能直接在 main 循环里轮询 ADC

很多人接触 HAL 库的 ADC 例程时,习惯用HAL_ADC_PollForConversion()这种轮询方式。在普通电压测量场景下没有问题,但示波器采样不能这么干。

原因有两个:一是轮询间隔受 CPU 其他代码影响,采样点之间的时间间隔不均匀,画出来的波形横向比例不对,频率根本测不准;二是轮询模式需要 CPU 全程参与,采样 128 个点期间干不了别的,OLED 刷新和按键扫描就被卡死。

正确方案是让定时器给出精确的采样节拍,ADC 在每个节拍上升沿自动启动一次转换,DMA 把转换结果搬运到内存缓冲区。CPU 只在缓冲区装满一半或全部装满时介入一次,把数据拿去绘图。这就是“定时器触发 ADC + DMA 传输”的组合逻辑。

3.2 采样率怎么算:PSC 和 ARR 的数学关系

TIM2 挂在 APB1 定时器时钟上,在 72MHz 主频下,APB1 定时器时钟也是 72MHz(需要 CubeMX 里时钟树正确配置)。当 TIM2 的预分频 PSC 为 71、自动重装值 ARR 为 9 时:

采样频率 fs = 72MHz / (PSC + 1) / (ARR + 1) = 72MHz / 72 / 10 = 100kHz

也就是每 10 微秒采一个点。这个频率下观察 1kHz 左右的方波或正弦波非常合适,一个周期能采到 100 个点,波形还原度很高。

ADC 本身的转换速度也要注意。F103 的 ADC 时钟建议在 14MHz 以下,12 位采样约需要 14 个 ADC 时钟周期,算下来单次转换时间大约为 1.14 微秒。这意味着理论上最高能跑到 800kHz 级别的采样率,但实际应用中我很少超过 200kHz,因为波形显示还要兼顾刷新速度,点太密了 OLED 上也看不出细节。

3.3 F1 上的“伪双缓冲”:由两个 DMA 中断拼出来

F4 系列有真正的 DMA 双缓冲模式,但 F1 的 DMA 没有这个功能。我在做的时候通过 DMA 的半传输中断和全传输中断实现了一套等效的乒乓机制。

具体思路:申请一个长度为 256 的uint16_t数组adc_buf[256],DMA 连续搬运。当搬运完前 128 个点,硬件进入半传输中断;搬完后 128 个点,进入全传输中断。第一次中断处理adc_buf[0]adc_buf[127],第二次中断处理adc_buf[128]adc_buf[255],交替进行。主循环只需要读取一个标志位,就知道新一帧波形数据是否准备好了。

volatile uint8_t frame_ready = 0; volatile uint16_t *active_frame; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { active_frame = &adc_buf[0]; frame_ready = 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { active_frame = &adc_buf[128]; frame_ready = 1; } }

启动采集只需一条核心函数:

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 256);

从这之后,CPU 基本就解放了。我踩过的一个坑是:回调函数里不要做耗时操作,尤其不要在中断里直接刷 OLED。哪怕OLED_DrawLine只画 128 个点,对两个 8MHz 外围时钟的 SSD1306 来说也是个不小的负担,容易导致下一帧 DMA 数据还没搬完就被新中断打断,最终波形出现分段错位。正确做法是中断里只置标志位,主循环里拷贝一份帧数据后再绘制。

3.4 采样率上限评估:F103 示波器能测什么信号

F103 的 ADC 最高采样率在 1MHz 左右,考虑 Nyquist 采样定理,理论上可测最高频率是 500kHz。但实际显示效果要达到“能看”,每个周期至少要采 10 到 20 个点,所以实际可用测量范围一般在 50kHz 以内。

这个范围能干什么?测一测 1kHz 的音频信号、看一下串口 TX 的波形时序、检查 PWM 输出占空比、观察 RC 充放电曲线,都是够用的。拿它去测射频或高速 SPI 时钟,那不是这个方案的任务。

4. OLED 波形绘制与交互:UI 不豪华但必须顺手

4.1 波形怎么映射到 128x64 屏幕

OLED 屏分辨率是 128x64,我分配的波形显示区是横向 128 个点、纵向 48 个像素点(预留了顶部菜单栏、底部状态栏)。ADC 值是 0 到 4095 的 12 位数字,需要映射到 0 到 47 的屏幕坐标。

最简单的做法是把屏幕横向的 128 个像素点当作时间轴,从当前帧数据中等间隔抽取 128 个数据点。每个点对应的屏幕纵坐标由 ADC 值得出:

uint8_t y = WAVE_Y + WAVE_H - 1 - (uint32_t)adc_val * WAVE_H / 4096;

注意 F103 的 ADC 参考电压默认是 3.3V,所以 0 对应屏幕最底部,4095 对应屏幕最顶部。如果供电电压不是标准的 3.3V,或者 VDDA 引脚上有纹波,测出的波形底部会抖动,这是硬件层面需要关注的事情。

相邻两个屏幕点之间画一条直线,就能把离散的采样点连成一条连续曲线。库函数或者自己写一个简单的 Bresenham 画线算法都可以,128 个点逐点调用不会卡。

4.2 两个按键实现完整菜单

我用的交互方案是两颗按键:一颗“功能键”短按切换到下一层菜单项,一颗“调节键”长按或短按加减参数。菜单内容并不多,几个状态足够覆盖:

  • 示波器模式下可以调节垂直位置(在屏幕上整体上下平移波形)和时基(改变采样点数或触发模式)。
  • 信号发生器模式下可以调节波形类型、频率、幅度。
  • 状态栏实时显示当前 ADC 电压值、采样率、信号发生器输出频率。

实测下来,OLED 的刷新速率大概能到 15 到 20 帧每秒,视觉上已经比较流畅了。如果刷新率掉到 5 帧以下,优先检查 I2C OLED 的时钟频率是不是默认的 100kHz——改用 SPI 屏或把 I2C 时钟调到 400kHz 会有明显改善。

4.3 软件触发:把波形稳定在屏幕上

自由运行模式下,如果没有触发机制,屏幕上看到的波形会是随机的、来回滚动的状态。做示波器一定要想办法引入触发,哪怕是最简单的软件触发电平。

我的简化触发逻辑是:当检测到当前采样点数值大于阈值、前一个采样点数值小于阈值时,认为出现了一个上升沿,以这个点为波形窗口的起点。这样波形就能基本稳定地停在屏幕上。阈值默认取 2048,也就是 ADC 量程的一半,通过按键可以上下调节。这个方法不能和硬件示波器的触发模块比,但对于观察周期性信号已经足够用了。

5. 信号发生器:DDS 波形表的相位累加器玩法

5.1 为什么用 DDS 而不是查表 + 延时

信号发生器的核心指标是频率可调。如果用“查一个正弦表,然后每过一个固定延时输出一个点”的方式,频率精确度和调节分辨率都会受限于延时精度,而且主循环一旦被其他任务打断,波形就会失真。

DDS(直接数字频率合成)是更优雅的方案。它维护一个 32 位的相位累加器,每个 DAC 更新周期累加一次相位增量,然后从正弦表中取出对应的幅度值。输出频率和相位增量之间是一个纯粹的线性关系:

f_out = (phase_increment × DAC_update_rate) / 2^32

反过来,根据目标频率就能直接算出phase_increment

double target_freq = 1000.0; // 目标 1kHz double dac_rate = 100000.0; // DAC 更新率 100kHz uint32_t phase_increment = (uint32_t)(target_freq * 4294967296.0 / dac_rate);

这样调节频率只需要改一个 32 位变量,频率分辨率是整个相位累加器的精度,低到可以忽略不计。正弦表只需要 256 个点,远小于大数组的存储压力。

5.2 用定时器中断更新 DAC 输出

F1 系列带 DAC 的型号(比如 F103RCT6)可以直接用 DAC 外设输出波形,更新节拍由定时器中断控制。每次进入定时器中断,都执行一次相位累加、查表、写 DAC 寄存器。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { phase_acc += phase_increment; uint32_t idx = (phase_acc >> 24) & 0xFF; // 取高 8 位作为查表索引 int32_t sine = (int32_t)sine_table[idx] - 2048; int32_t out = 2048 + sine * amplitude / 4096; DAC1->DHR12R1 = (uint16_t)out; } }

一条核心经验:定时器中断里的计算量要尽量小,不要在中断里做浮点运算。上面代码里的乘除虽然是整数,但如果amplitude是变量,编译器也会生成较长的指令序列。实测 100kHz 中断频率下,这条中断函数耗时约占 CPU 的 20% 左右,还能接受。如果继续提高更新率到 500kHz 以上,就要考虑用 DMA 自动搬运波形表到 DAC,CPU 完全不参与,我后续做高速 DDS 时再单独写一篇聊这个。

5.3 正弦表怎么生成

正弦表直接用 C 语言在编译期生成,或者用 Python 脚本生成一个静态数组。我的表里存的是 0 到 4095 的整数,覆盖一个完整周期的正弦值,直流偏置是 2048,峰峰值最大 4096。如果目标波形是三角波或锯齿波,其实连表都不用查,直接对相位累加器的高 8 位做线性映射即可。方波更简单,看相位累加器最高位是 0 还是 1 来决定输出高或低电平。

5.4 F103C8T6 的替代方案:PWM + RC 低通

如果手里只有 C8T6,也别放弃。PWM 信号经过一个二阶 RC 低通滤波器,可以得到近似的直流电压,输出幅值由占空比决定。每更新一次 PWM 占空比,波形的输出值就变一次,从效果上看就是一路低速 DAC。

实际参数我用的是:PWM 频率 20kHz,RC 低通截止频率约 2kHz。合成 100Hz 正弦时波形平滑度不错,但合成 5kHz 以上信号时衰减严重、波形扭曲。这个方案的优点是电路极其简单,缺点也很明显:输出频率上限低、谐波大,不够“信号发生器”。如果只是给单片机初学者演示 DDS 原理,完全够用。

6. Proteus 仿真环节:能验证逻辑,但别把仿真当实物

6.1 建工程时需要立刻确认的仿真硬件配置

Proteus 从 8.6 左右开始支持 STM32F1 系列器件仿真,前提是安装对应的器件库。搭建仿真时,第一步就是把 CubeMX 生成的 HEX 文件加载到 Proteus 的芯片里,然后接好以下电路:

  • 8MHz 外部晶振,两端接 20pF 负载电容,接到 OSC_IN/OSC_OUT。
  • 复位引脚接一个 10k 上拉电阻到 3.3V,另外通过一个 100nF 电容到地。
  • VDDA、VSSA 引脚接到电源和地,不能悬空。
  • 模拟输入信号用 Proteus 的虚拟信号发生器接到 PA0,注意幅度不要超过 3.3V。

有一个非常容易卡住的点:CubeMX 里如果时钟树采用 HSE,Proteus 中必须把晶振电路接好,否则程序会一直卡在时钟初始化阶段,或者运行后时钟频率不对,外围设备全部乱套。如果不想在仿真里画晶振,把 CubeMX 时钟源改成 HSI,代码也能跑,但频率精度差一些。实测下来,仿真演示阶段用 HSI 最省事,实机调试再改回 HSE。

6.2 仿真中的 ADC + DMA 表现怎么这么慢

我在 Proteus 里运行这套代码时,波形刷新速度比实物慢很多,一度以为是 DMA 配置错误。后来反复查证发现,Proteus 的 STM32 仿真并不是周期精确的,对 ADC 转换、DMA 搬运这类操作的模拟速度远低于真实芯片,而且虚拟示波器刷新也拖慢了整体速度。

调试思路是:先用慢速采样模式验证数据是否在变化,比如把采样率降到 1kHz,直接在串口里打印 ADC 原始值,确认采集链路没问题;再逐步提高采样率,观察 DMA 半传输/全传输中断是否触发。把这两步跑通后,再连 OLED 显示波形,不要一上来就指望仿真画面像真实示波器那样流畅。

6.3 一个我反复遇到的 Proteus 启动问题

仿真首次启动时,如果 OLED 上完全没有内容、程序看起来像死掉一样,优先检查两件事:

第一,CubeMX 生成的工程里可能有HAL_ADCEx_Calibration_Start(),这是很正常的校准函数,但在 Proteus 仿真环境中有时会卡住或异常。我直接把这个调用注释掉了,实测不影响仿真功能,实机上也建议保留校准,但要做超时保护。

第二,主循环里如果加入了等待某个 GPIO 变高的逻辑(比如等按键),而仿真中没有主动操作按键,程序就会一直停在那里。排查方法是把按键初始化回调打印到串口,或用虚拟终端观察程序执行到哪一行。

6.4 仿真与实物的数据差异

Proteus 仿真最大的价值是验证逻辑链路:DDS 相位累加器的计算、DMA 回调标志位的切换、波形绘制函数的边界条件,这些逻辑问题在仿真里能提前暴露出来,省去实物调试时的反复烧录。

但仿真不会告诉你的是:实际 ADC 的参考电压会有纹波、采样点存在孔径抖动、OLED 的 SPI 时序在长线上会变形。所以我的建议是:先用 Proteus 跑通整个工程,验证按键交互和波形绘制逻辑之后,再上真板子做一轮实测。两个阶段各有不可替代的作用。

7. 一些真机上才会遇到的补充经验

当初做实物时,有个现象让我印象很深:OLED 显示的波形总会周期性抖动,仔细排查后不是采样问题,而是 DMA 中断回调里我顺手清除了一次标志位,但清零时机比 DMA 下一次写入早了一点,导致新数据覆盖了还没来得及绘制的旧数据。后来我把数据拷贝和标志位清空顺序调换,问题就消失了。类似这种“中断抢先”的细节,只靠仿真根本发现不了。

另外,给示波器输入信号时,一定要在 PA0 引脚和信号源之间加一个 1k 电阻做输入保护,同时并联一个 100nF 电容滤除高频噪声。千万不要直接把外部 12V 信号接进来,F103 的 ADC 引脚承受不了过压,一片芯片就这样烧掉过,教训挺深刻。

如果你打算把这个项目继续做成 PC 端上位机显示波形,思路也很清晰:STM32 侧用串口把 ADC 采样数据打包发送,上位机负责波形显示和控制参数下发。把信号发生器和示波器通过一个板级接口短路连接,还能输出一个固定频率的校准信号,用来验证整套测量链路是否正常。这个扩展方向以后再展开,先把这套仿真弄明白,已经够玩一阵子了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:49:08

提示系统A/B测试实战:从实验设计到平台落地的架构指南

好几年前做传统后端功能实验,我觉得A/B测试是个很成熟的事:配置开关、分流、埋点、看显著性,一套流程走下来没什么悬念。直到我开始做提示系统(Prompt System)的A/B测试,才意识到自己之前对“实验”的理解有…

作者头像 李华
网站建设 2026/9/9 15:49:04

现在性价比高的AI写论文工具有哪些品牌?亲测后说说真心话

每到期末、毕业答辩、课题申报阶段,很多学生都会深陷论文难题:选题毫无头绪、大纲搭建逻辑混乱、正文撰写耗时长、参考文献格式出错、查重重复率偏高、AIGC检测告警、本校论文排版标准复杂。纯人工从零开始写论文,不仅需要耗费数周时间反复修…

作者头像 李华
网站建设 2026/9/9 15:48:29

四方向网格最短路径:DAG陷阱与两种解法

前阵子接到一个需求,要在网格上找一个最低成本路径,从左上角到右下角,每一步允许左、右、上、下移动,需求描述里写着“有向无环图中的最短路径”。我盯着这行字愣了一下:只要允许四个方向移动,每个格子和邻…

作者头像 李华
网站建设 2026/9/9 15:46:25

ponytail技能注入器:前端工程化中的契约式能力交付

1. 项目概述:这不是一个发型,而是一个被严重低估的前端工程化工具 最近在几个前端技术群和 GitHub Trending 页面上反复刷到 ponytail 这个词——它既不是 TikTok 上新出的编发教程,也不是某位设计师的个人品牌,而是一个真实存在…

作者头像 李华
网站建设 2026/9/9 15:46:10

Hy4 Preview 实测:预约、免费时间与游戏合集全解析

最近社区里讨论度最高的,大概就是 Hy4 preview 这个话题了。官方那句“Hows everyone liking the Hy4 preview so far? We put together a little Hy4 game collection for you…”,像个钩子一样,直接把我的期待值拉满。我第一时间…

作者头像 李华