从零搭建一个波形发生器:新手也能看懂的实战指南
你有没有试过在调试电路时,突然发现缺一个信号源?比如想测一测放大器的频率响应,或者验证一下滤波器的效果——结果手边连个像样的正弦波都出不来?
别急。今天我们就来亲手做一个能用的波形发生器,不靠昂贵设备,也不拼焊接手艺。整个项目成本不到50元,核心就是一块常见的开发板(比如STM32或Arduino)加上几个外围元件。重点是:你能真正搞明白每一步背后的原理。
这不仅是个“做出来就能玩”的小玩意儿,更是理解嵌入式系统、模拟电路和数字信号处理之间如何协作的绝佳入口。
为什么自己做波形发生器?
市面上当然有专业的函数发生器,精度高、功能全。但它们有两个问题:贵,而且像个黑盒子。
而我们自己做的这个,优势非常实在:
- 便宜:主控+DAC+按键+屏幕,材料费基本控制在50元以内;
- 透明:每一行代码、每一个电阻都有意义,你知道信号是怎么“生”出来的;
- 可扩展:今天能输出正弦波,明天加个串口指令就能远程调频,后天还能接WiFi做成网络化测试工具;
- 教学价值拉满:涉及MCU定时器、DAC转换、查找表设计、低通滤波……全是电子工程师的核心技能点。
说白了,这不是为了替代专业仪器,而是为了让你真正掌握底层逻辑。
波形是怎么“造”出来的?
先抛开硬件细节,我们从最本质的问题开始:怎么让一个数字芯片输出连续变化的电压?
答案很简单:把波形切成很多小段,一段一段地“拼”出来。
以正弦波为例。我们知道它的数学表达式是 $ V(t) = A \cdot \sin(2\pi f t) $。如果我们每隔一小段时间取一个值,把这些值存进数组里,然后让MCU按顺序把这些数值送给DAC输出,再稍微平滑一下——就成了!
这个过程可以拆成三步走:
- 建模:预先计算好一个周期内的采样点,存在数组中(也就是“查找表”);
- 输出:用定时器触发中断,每次从中断里读一个点,写给DAC;
- 还原:通过滤波器把阶梯状的输出变得光滑,逼近理想波形。
听起来是不是有点像“动画帧播放”?没错,这就是数字世界生成模拟信号的基本思路。
DAC:连接数字与模拟的桥梁
没有DAC,一切免谈。它是整个系统的“出口”,负责把二进制数变成真实电压。
常见实现方式
方案一:外接专用DAC芯片(推荐入门)
比如ADI家的AD5663、MCP4725这类I²C/SPI接口的DAC模块,价格不贵,使用简单,精度也不错。
优点非常明显:
- 输出稳定,噪声低;
- 支持12~16位分辨率,电压步进精细;
- 自带参考电压输入,动态范围可控。
举个例子:用12位DAC配合3.3V基准,最小电压步进只有约0.8mV(3.3 / 4096),足够做出肉眼看不出“台阶”的正弦波。
方案二:PWM + RC滤波(低成本备选)
如果你手上没有DAC芯片,也可以利用MCU自带的PWM通道“凑合”一下。
原理也很直观:PWM占空比越高,经过RC滤波后的平均电压就越高。公式就是:
$$
V_{out} = V_{cc} \times DutyCycle
$$
比如5V系统下设置50%占空比,理论上能得到2.5V直流电平。
但这方法有个硬伤:带宽太窄。一旦你要输出几百Hz以上的正弦波,就会发现波形严重失真、拖尾明显。因为它本质上还是靠“平均”来模拟电压,响应速度受限于RC时间常数。
所以结论很明确:
✅ 适合输出慢变信号、直流偏置、低频三角波
❌ 不适合高频、高保真音频或精密测试场景
关键组件参数怎么选?
别被数据手册吓到,其实只需要关注几个核心指标:
| 参数 | 我们关心什么? | 推荐值 |
|---|---|---|
| 分辨率 | 越高越平滑 | 至少12位(如MCP4725) |
| 接口类型 | 是否方便与MCU通信 | SPI > I²C > 并行(引脚多) |
| 建立时间 | 输出稳定要多久 | <10μs为佳 |
| 参考电压 | 决定最大输出幅度 | 外部精准基准更稳 |
举个实际例子:STM32F1系列自带的12位DAC,虽然省了外设,但共用VDD作为参考电压,电源一抖,输出就飘。相比之下,外接DAC配LM4040这类基准源,稳定性好太多。
微控制器干了啥?不只是“发数据”
很多人以为MCU在这里只是个“搬运工”:把数据从数组搬到DAC。其实它的工作远不止如此。
它至少要完成以下任务:
- 初始化SPI/I²C、配置定时器;
- 管理波形查找表(LUT);
- 控制定时中断节奏,确保输出频率准确;
- 响应用户操作(按键、旋钮、串口命令);
- 动态调整波形类型、频率、幅值等参数。
其中最关键的,就是如何精确控制输出频率。
如何精准调频?相位累加法了解一下
假设你有一个256点的正弦查找表。如果每次都按顺序一个接一个读,那输出频率取决于你读的速度——也就是定时器中断间隔。
但如果我想输出不是整数倍的频率呢?比如不是1kHz,而是1.237kHz?这时候就得上“相位累加器”了。
相位累加法(Phase Accumulator)
这是一种高效又节省资源的频率合成技术,广泛用于DDS(直接数字频率合成)系统中。
它的核心思想是:
不是逐点跳,而是“跳着走”,每次前进N个点。N越大,一圈跑得越快,频率越高。
具体实现如下:
#define TABLE_SIZE 256 const uint16_t sine_lut[TABLE_SIZE] = { 2048, 2105, 2162, 2219, 2275, 2331, 2386, 2440, // ... 中间省略 ... }; volatile uint32_t phase = 0; // 32位相位寄存器 volatile uint32_t phase_step = 655; // 步长(决定频率)在定时器中断中更新:
void TIM3_IRQHandler(void) { if (TIM3->SR & TIM_SR_UIF) { TIM3->SR &= ~TIM_SR_UIF; // 取高16位作为索引(保留8位小数精度) uint16_t index = (phase >> 8) % TABLE_SIZE; write_dac(sine_lut[index]); phase += phase_step; // 相位累加 } }这里用了个技巧:phase是32位变量,phase_step是定点小数。即使步长不是整数,也能实现极细粒度的频率调节。
比如:
- 每次中断间隔10μs,
-phase_step = 655对应约1kHz;
-phase_step = 1310就接近2kHz;
- 最小分辨率可达毫赫兹级!
这种方法不需要浮点运算,效率极高,非常适合资源有限的单片机系统。
查找表怎么生成?别手敲!
你肯定不想手动算256个正弦值吧?我们可以用Python快速生成:
import numpy as np TABLE_SIZE = 256 BITS = 12 MAX_VAL = (1 << BITS) - 1 CENTER = MAX_VAL // 2 AMPLITUDE = 1000 # 生成正弦查找表 sine_table = [int(CENTER + AMPLITUDE * np.sin(2 * np.pi * i / TABLE_SIZE)) for i in range(TABLE_SIZE)] # 输出C语言格式 print("const uint16_t sine_lut[{}] = {{".format(TABLE_SIZE)) for i, val in enumerate(sine_table): if i % 8 == 0: print(" ", end="") print("{:4d}".format(val), end="") if i != len(sine_table) - 1: print(", ", end="") if (i+1) % 8 == 0: print() print("\n};")运行一下,直接复制进你的工程就行。同理,方波、三角波、锯齿波都可以这样生成。
硬件怎么搭?一张图说清楚
[按键/编码器] [OLED屏] ↓ ↑ ┌──────────────────────┐ │ STM32 / MCU │ └──────────────────────┘ ↓ (SPI/I²C) ┌─────────┐ │ DAC │→ [RC滤波] → [运放缓冲] → 输出端子 └─────────┘几点关键说明:
- RC滤波必不可少:哪怕用了DAC,输出仍是阶梯状。建议一级RC(1.6kHz)起步,追求质量可上Sallen-Key有源滤波;
- 加个电压跟随器:用LM358或TLV2462做缓冲,降低输出阻抗,防止接负载后波形塌陷;
- 电源要干净:模拟部分最好单独用LDO供电,避免数字噪声串扰;
- 地线布局讲究:数字地和模拟地单点连接,否则容易引入哼声或毛刺。
遇到问题怎么办?这些坑我都踩过
1. 波形看起来像楼梯?
✅ 解决方案:
- 增大查找表尺寸(从256点升到1024);
- 提高DAC更新率(缩短定时器周期);
- 加二级有源低通滤波器。
2. 频率不准,尤其低频漂移?
✅ 检查:
- 是否用了内部RC振荡器?换成外部晶振;
- 定时器是否被其他高优先级中断打断?
3. 输出幅度不稳定?
✅ 可能原因:
- VREF没用独立稳压源;
- 电源纹波大;
- 负载能力不足,未加运放驱动。
4. 想要双路输出不同相位?
✅ 办法:
- 用双通道DAC(如AD5663);
- 分别维护两个phase变量,设置不同初始值即可实现移相;
- 改变phase_step还能做到扫频或调制。
还能怎么升级?让它变得更聪明
基础版搞定之后,完全可以继续拓展:
- 加入OLED屏:实时显示当前波形、频率、幅值;
- 旋转编码器调节:顺时针调频,按下切换波形;
- 串口控制:PC下发指令,实现自动化测试;
- EEPROM记忆设置:掉电不丢配置;
- 内置扫频模式:自动从100Hz扫到10kHz,用于Bode图测量;
- 支持任意波形上传:通过USB加载自定义波形数据。
甚至可以用ESP32版本,加上WiFi,做成手机APP控制的无线信号源。
写在最后:动手才是最好的学习
你看完这篇文章,可能觉得:“哦,原来就这么回事。”
但只有当你真正:
- 打开Keil写第一行write_dac()函数,
- 在示波器上看到第一个颤巍巍的正弦波,
- 调整phase_step看着频率慢慢上升,
那一刻你才会体会到:我不是在用工具,我是在创造工具。
这种掌控感,是任何现成设备都无法给予的。
这个项目看似简单,但它串联起了嵌入式编程、信号处理、模拟电路三大领域。掌握了它,你就已经站在了通往任意波形发生器(AWG)、锁相环(PLL)、软件无线电(SDR)的大门口。
所以别犹豫了——找块开发板,焊几个电阻,点亮你的第一个波形吧。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。