资讯动态

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

发布时间:2026/9/9 15:49:59 来源:尧图企业网站定制
简介基于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有引脚多带 DACSTM32F103ZET6LQFP144512KB有资源最全STM32F103VET6LQFP100512KB有性价比不错如果你坚持用 C8T6信号发生器部分就要走另一条路线用定时器 PWM RC 低通滤波模拟 DAC。这个方案不是不行但波形质量、频率范围都很受限。我在实际做这个项目时选的是带 DAC 的型号做完整功能验证另外单独写了一份兼容 C8T6 的 PWM 版本代码。Proteus 仿真阶段直接用带 DAC 的型号最省事。2.2 CubeMX 工程基础配置不管最终用哪个型号CubeMX 里的关键配置项是一致的时钟源外部 HSE 8MHz系统时钟倍频到 72MHz实测外部晶振更准仿真时如果要省去晶振电路可临时改成 HSI后面会详细说。ADC1单通道12 位右对齐扫描模式关闭连续转换关闭外部触发选择 TIM2 TRGO目的是用定时器精确控制每个采样点的间隔。DMAADC1 的请求映射到 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_incrementdouble 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 自动搬运波形表到 DACCPU 完全不参与我后续做高速 DDS 时再单独写一篇聊这个。5.3 正弦表怎么生成正弦表直接用 C 语言在编译期生成或者用 Python 脚本生成一个静态数组。我的表里存的是 0 到 4095 的整数覆盖一个完整周期的正弦值直流偏置是 2048峰峰值最大 4096。如果目标波形是三角波或锯齿波其实连表都不用查直接对相位累加器的高 8 位做线性映射即可。方波更简单看相位累加器最高位是 0 还是 1 来决定输出高或低电平。5.4 F103C8T6 的替代方案PWM RC 低通如果手里只有 C8T6也别放弃。PWM 信号经过一个二阶 RC 低通滤波器可以得到近似的直流电压输出幅值由占空比决定。每更新一次 PWM 占空比波形的输出值就变一次从效果上看就是一路低速 DAC。实际参数我用的是PWM 频率 20kHzRC 低通截止频率约 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 里如果时钟树采用 HSEProteus 中必须把晶振电路接好否则程序会一直卡在时钟初始化阶段或者运行后时钟频率不对外围设备全部乱套。如果不想在仿真里画晶振把 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 采样数据打包发送上位机负责波形显示和控制参数下发。把信号发生器和示波器通过一个板级接口短路连接还能输出一个固定频率的校准信号用来验证整套测量链路是否正常。这个扩展方向以后再展开先把这套仿真弄明白已经够玩一阵子了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价