用一片STM32搞定正弦波信号源:从原理到实战的完整设计
你有没有遇到过这样的场景?做模电实验时需要一个低频正弦信号,手头却只有笨重昂贵的函数发生器;或者开发便携设备时想集成一个可调信号输出功能,但外置DAC方案又太占空间、功耗太高?
其实,这些问题都可以用一片STM32来解决。今天我们就来拆解一个经典但极具实用价值的设计——基于STM32内置DAC的正弦波发生器。它不依赖任何专用芯片,仅靠MCU本身的外设组合,就能输出稳定、频率可调的模拟信号。
这不仅是一个“能跑就行”的Demo,而是一套真正可用于教学、原型验证甚至小型仪器开发的工程级实现方案。更重要的是,整个过程将带你深入理解DAC、定时器与DMA之间如何协同工作,掌握嵌入式系统中“软硬结合”的核心思维。
为什么选STM32来做波形发生器?
在开始动手前,先回答一个问题:我们为什么不用现成的信号源模块,非要自己搞一套?
关键在于三个字:可控性。
市面上大多数函数信号发生器虽然功能齐全,但封闭性强,无法灵活定制输出特性。而使用STM32这类高性能MCU构建信号源,优势非常明显:
- 高度集成:无需额外DAC芯片,节省PCB面积和BOM成本;
- 完全可编程:频率、幅度、波形类型全由软件控制;
- 低延迟响应:硬件触发链确保时间精度,避免CPU调度抖动;
- 易于扩展:支持多通道同步、任意波形生成(AWG)、远程调控等高级功能。
特别是像STM32F4、STM32G4或H7系列,本身就集成了12位DAC、高精度定时器和多通道DMA控制器,天生就是为实时信号处理准备的平台。
比如我们常用的STM32F407VG,就具备:
- 双通道12位DAC
- 多个通用/高级定时器(TIM6/TIM7可作DAC触发源)
- 支持循环模式的DMA传输
- 最高主频168MHz,轻松应对音频范围内的信号合成
这些资源组合起来,完全可以替代传统中低端信号源的功能。
DAC不是简单数模转换,而是模拟输出的核心引擎
很多人对DAC的理解还停留在“把数字变成电压”这个层面,但在实际工程中,它的用法远比想象复杂。
STM32 DAC的关键能力你知道几个?
首先明确一点:STM32的DAC是电压输出型,典型参考电压为3.3V,12位分辨率意味着它可以产生 $ 2^{12} = 4096 $ 个离散电平,最小步进约0.8mV($3.3V / 4095$)。这对于大多数非精密测量应用已经足够。
输出公式如下:
$$
V_{out} = \frac{DIN}{4095} \times V_{REF}
$$
其中DIN是你写入DAC寄存器的值,范围0~4095。
但真正决定性能的,不是分辨率,而是更新机制。
软件触发 vs 硬件触发:差别有多大?
如果你只是通过CPU不断写寄存器来更新DAC输出,那结果会怎样?
答案是:波形严重失真,频率不可控。
因为每次HAL_DAC_SetValue()都要经历函数调用、中断上下文切换、总线访问等一系列开销,导致采样间隔极不稳定。这种“软件延时+轮询”的方式根本达不到奈奎斯特采样定理的要求。
正确的做法是让硬件自动驱动DAC更新,也就是使用定时器触发模式。
具体路径是:
定时器溢出 → 发出TRGO信号 → 触发DAC转换启动
这样一来,每个采样点的时间间隔完全由定时器决定,误差仅取决于晶振稳定性,通常可以做到微秒级精度。
定时器不只是计时工具,更是波形节奏的指挥官
如果说DAC负责“发声”,那么定时器就是掌控节拍的鼓手。
在本设计中,我们选用TIM6作为主控定时器,原因很简单:它是专为DAC服务设计的——支持直接连接至DAC的外部触发输入端。
如何计算采样率?
假设系统时钟来自APB1总线,频率为84MHz。我们希望得到100kHz的采样率,该如何配置?
关键参数有两个:
- 预分频器 PSC:用于降低计数频率
- 自动重载值 ARR:决定计数周期
计算公式为:
$$
f_s = \frac{f_{clk}}{(PSC + 1) \times (ARR + 1)}
$$
举个例子:
// 想要 fs = 100kHz // f_clk = 84MHz // 设 PSC = 83 → 分频后为 1MHz // 则 ARR = 9 → 周期为 1μs → fs = 100kHz配置代码如下:
htim6.Instance = TIM6; htim6.Init.Prescaler = 83; // 84MHz / 84 = 1MHz htim6.Init.Period = 9; // 1MHz / 10 = 100kHz HAL_TIM_Base_Init(&htim6); // 启用主模式,选择更新事件作为触发输出 TIM6->CR2 |= TIM_CR2_MMS_1; // MMS = 010: Update Event as TRGO一旦使能,TIM6每10μs就会发出一次TRGO脉冲,精准唤醒DAC进行下一次转换。
DMA才是真正的“幕后英雄”:零CPU干预的数据流管道
到这里你可能会问:谁来给DAC提供数据?难道还要在中断里一个个塞进去?
当然不是。如果靠中断喂数据,不仅占用CPU,还会引入中断延迟,破坏实时性。
正确姿势是启用DMA(直接内存访问),建立一条从内存到DAC的“高速公路”。
工作流程一目了然:
- 在RAM中预存一张正弦查找表(Sine LUT)
- 配置DMA通道,源地址指向LUT首址,目标地址为DAC的数据保持寄存器(DHR)
- 设置为内存到外设、半字宽度、循环模式
- 启动后,DMA自动按节拍搬运数据,全程无需CPU插手
每当DAC完成一次转换并准备好接收新数据时,它会向DMA控制器发起请求(DMA Request),后者立即从LUT中取出下一个值送入DAC。
这就形成了一个闭环流水线:
定时器触发 → DAC请求数据 → DMA传输 → 输出新电压 → 等待下次触发
整个过程流畅无缝,CPU利用率接近于零。
正弦波是怎么“造”出来的?从查表到相位累加
现在所有硬件链路都打通了,最后一步就是生成波形本身。
最基础的方法是查表法(Look-Up Table, LUT)。
查表法的本质:用离散点逼近连续曲线
我们预先计算好一个周期内的N个正弦采样点,存储在一个数组中。DAC依次读取这些点,就能还原出近似的正弦波。
例如,创建一个128点的LUT:
#define LUT_SIZE 128 uint16_t sine_lut[LUT_SIZE]; void generate_sine_lut(void) { for (int i = 0; i < LUT_SIZE; i++) { float angle = 2 * M_PI * i / LUT_SIZE; sine_lut[i] = (uint16_t)(2047 + 2047 * sinf(angle)); // 映射到 0~4095 } }这里做了两个关键处理:
- 使用2047作为偏置,使输出围绕1.65V(中点)振荡
- 幅值也为2047,保证不超出0~4095的有效范围
然后启动DMA传输:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_lut, LUT_SIZE, DAC_ALIGN_12B_R);只要定时器持续触发,DAC就会循环播放这张表,输出稳定的正弦波。
输出频率怎么算?
非常简单:
$$
f_{out} = \frac{f_s}{N}
$$
比如采样率 $ f_s = 100kHz $,LUT长度 $ N = 128 $,则输出频率为:
$$
f_{out} = \frac{100000}{128} \approx 781.25\,\text{Hz}
$$
想要更精细调频?试试DDS思想中的相位累加器
查表法简单有效,但有个问题:频率调节粒度太粗。
你想调个800Hz?不好意思,要么改采样率,要么换表,都不方便。
怎么办?引入相位累加器(Phase Accumulator),这是DDS(Direct Digital Synthesis)技术的核心思想。
原理很简单:
传统的查表是以整数步长遍历LUT,比如每次+1。而相位累加器使用一个固定小数增量,累计到整数后再取模访问LUT。
这样即使步长小于1,也能缓慢推进,实现亚赫兹级分辨率。
示例代码:
uint32_t phase_accumulator = 0; uint32_t phase_step = (uint32_t)((100.0 / 100000.0) * (1 << 20)); // 100Hz @ 100kHz fs while (1) { uint16_t index = (phase_accumulator >> 20) % LUT_SIZE; HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, sine_lut[index]); phase_accumulator += phase_step; // 等待下一个定时器触发(可通过中断或事件标志同步) }虽然这段代码仍需CPU参与,但它展示了如何通过数学方法突破查表法的频率限制。
更进一步的做法是结合双缓冲DMA + 定时器中断动态更新指针,实现完全硬件化的高分辨率DDS引擎。
实际输出什么样?别忘了这几点关键优化
你以为代码跑通就万事大吉了?现实远比理想复杂。
1. 输出是“楼梯状”的,必须滤波!
DAC输出本质上是阶梯波(staircase waveform),含有大量高频谐波成分。直接接负载会导致严重失真。
解决办法:加一级RC低通滤波器,截止频率略高于目标信号频率即可。
例如输出1kHz正弦波,可用:
- R = 1kΩ
- C = 100nF
→ 截止频率 $ f_c = \frac{1}{2\pi RC} \approx 1.6kHz $
简单两级RC滤波就能显著改善波形质量。
2. 参考电压要稳,否则一切白搭
很多开发者忽略VREF引脚,直接用VDD供电。但VDD可能随负载波动,导致DAC输出漂移。
建议:
- 使用独立LDO供电
- 或接入精密基准源(如REF3030、TL431)
- 至少在VREF+引脚加0.1μF陶瓷电容去耦
3. 电源噪声不能忽视
DAC对电源噪声极其敏感。务必在DAC_AVDD和DAC_VSS之间放置0.1μF + 10μF的去耦电容组合,并尽量靠近封装引脚。
同时避免高速数字信号线从DAC附近穿过,防止串扰。
完整系统架构一览
最终系统的信号流如下:
[STM32F407] │ ├─ [System Clock] → [TIM6] → TRGO ─┐ │ ↓ ├─ [Sine LUT in SRAM] → [DMA1_Stream5] → [DAC_DHR1] │ ↓ └──────────────────────────────→ [Analog Out] → [RC LPF] → [Load]用户可通过按键、ADC旋钮或串口指令动态调整:
- 定时器ARR值 → 改变采样率 → 调整输出频率
- 相位步长 → 实现精细调频
- 替换LUT内容 → 切换为三角波、方波、自定义波形
整个系统模块化清晰,移植性强,适用于多种STM32平台。
还能怎么升级?这些方向值得探索
这套基础架构看似简单,实则潜力巨大。以下是几个可行的拓展方向:
- 双通道同步输出:利用双DAC通道,生成同频同相或差分信号
- 幅度调节:在LUT生成时乘以增益系数,或配合外部PGA(可编程增益放大器)
- 任意波形发生器(AWG):通过上位机下载自定义波形数据到Flash
- 闭环校准:加入ADC反馈,自动补偿非线性与温漂
- USB虚拟仪器:搭配USB CDC或Waveform类协议,变身PC外设
甚至可以做成一款迷你版的开源信号发生器,配上OLED屏和编码器旋钮,彻底摆脱对大型仪器的依赖。
写在最后:这不是玩具,是真正的工程实践
这个项目表面上是在“生成一个正弦波”,实际上涵盖了嵌入式开发的多个核心技术点:
- 外设联动机制(Timer → DAC → DMA)
- 实时性保障策略
- 数字信号处理基础(采样定理、DDS)
- 模拟电路设计意识(滤波、去耦、参考源)
它既适合高校电子类课程作为综合实验项目,也足以支撑工程师快速搭建原型系统。
更重要的是,当你亲手调试出第一段平滑的正弦曲线时,那种“软硬协同”的成就感,远超任何理论讲解。
如果你正在寻找一个既能练手又能落地的STM32实战案例,不妨就从这个波形发生器开始。所需材料不过一块开发板、几个电阻电容,回报却是对嵌入式系统本质更深的理解。
如果你在实现过程中遇到DAC噪声大、频率跳变等问题,欢迎留言交流。也可以分享你的改进方案,比如加入了SPI外置高精度DAC,或是实现了触摸屏交互界面。我们一起把这件小事,做到极致。