从零开始造一个波形发生器:不只是“输出正弦波”那么简单
你有没有试过用示波器看自己写的代码?
听起来像玩笑,但其实——波形发生器就是让代码“发声”的第一站。
它不神秘,也不一定昂贵。哪怕是一块十几块钱的STM32最小系统板,加上几个被动元件,就能变成一台能产生正弦、方波、三角甚至自定义信号的“迷你函数发生器”。对于刚入门嵌入式或模拟电路的新手来说,这可能是最值得动手做的项目之一。
但问题来了:
为什么我DAC输出的是“台阶”,不是平滑的正弦?
为什么频率一高,波形就糊成一团?
为什么换了芯片,同样的代码却跑不动?
别急。今天我们不讲教科书式的模块堆砌,而是带你一步步拆解真实设计中的每一个决策点,从底层原理到PCB布线,告诉你一个真正可用的波形发生器是怎么“炼”出来的。
DAC不是“自动画曲线”的黑盒子
很多人以为,只要给DAC送一组数据,它就会乖乖输出对应的模拟电压。
但现实是:DAC只是个数字到电压的翻译官,不会帮你补全中间过程。
它到底在做什么?
想象你在画画——如果你只画了几个离散的点(比如每隔10°标一个正弦值),然后指望别人看出一条光滑曲线,那大概率会被说“这是折线图”。
DAC干的就是这事:你给它一个数字(比如2048),它输出对应电压(比如1.65V)。下一刻你再给2060,它跳到1.67V……这个过程是突变而非连续的。
所以最终输出长这样:
┌───┐ ┌───┐ ┌───┐ │ │ │ │ │ │ ────┘ └───────┘ └───────┘ └─────→ 时间这就是所谓的“阶梯波”——本质是采样+保持的结果。
数学上,它的频谱包含我们想要的基波,还有大量以采样频率为中心的镜像分量(如 $ f_s - f_0 $, $ f_s + f_0 $ 等)。如果不处理,这些高频噪声会严重污染信号质量。
✅关键认知刷新:
DAC本身并不生成“波形”,它只是按节拍播放“音符”。真正的波形重建,靠的是后续滤波和足够的采样密度。
怎么让MCU成为“节拍大师”?
如果说DAC是扬声器,那MCU就是指挥家——它决定什么时候发哪个音符。
最简单的玩法:查表+定时中断
假设我们要生成一个1kHz正弦波,用256个点来描述一个周期。那么每秒钟就得更新 256 × 1000 = 256,000 次DAC。
怎么实现?靠定时器。
#define TABLE_SIZE 256 uint16_t sine_table[TABLE_SIZE]; // 预计算正弦波数据(适配12位DAC) void Generate_Sine_Table(void) { for (int i = 0; i < TABLE_SIZE; ++i) { float angle = 2 * M_PI * i / TABLE_SIZE; sine_table[i] = (uint16_t)(2047 + 2047 * sin(angle)); } }然后设置一个定时器,每 1 / 256000 ≈ 3.9μs 触发一次中断,在中断里取出下一个表项送给DAC:
volatile uint16_t phase = 0; void TIM_IRQHandler(void) { HAL_TIM_IRQHandler(&htim); // 取出当前相位对应的数据并写入DAC HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, sine_table[phase]); phase = (phase + 1) % TABLE_SIZE; // 循环索引 }看起来很完美?但这里有三个隐藏陷阱。
⚠️ 坑点1:中断太频繁,CPU累瘫了
每秒25万次中断是什么概念?
在主频72MHz的STM32F4上,平均每次中断只有不到300个时钟周期来执行代码。而进入/退出中断、保存寄存器、调用HAL库函数……随便一算就超了。
结果就是:系统卡顿、其他任务无法响应,甚至定时精度下降导致频率漂移。
🔧解决办法:DMA救场
DMA(直接内存访问)可以在不需要CPU干预的情况下,自动把波表数据搬运到DAC的数据寄存器。配置一次后,它自己跑,CPU彻底解放。
// 启动DAC + DMA传输 HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_table, TABLE_SIZE, DAC_ALIGN_12B_R);从此以后,MCU可以去干别的事,比如刷新LCD、读按键、串口通信……完全不影响波形输出稳定性。
⚠️ 坑点2:频率调节太死板
上面的例子中,输出频率由两个因素决定:
$$
f_{out} = \frac{f_{update}}{N}
$$
其中 $ f_{update} $ 是DAC更新率,$ N $ 是每周期采样点数。
如果你想切到500Hz怎么办?重做一张表?改中断周期?都不够灵活。
🔧进阶方案:引入相位累加器(Phase Accumulator)
这是一种轻量级DDS(直接数字频率合成)思想:
volatile uint32_t phase_accum = 0; const uint32_t phase_step = 0x1000000 / 100; // 控制频率 void TIM_IRQHandler(void) { phase_accum += phase_step; uint16_t index = (phase_accum >> 24) & 0xFF; // 提取高位作为索引 HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, sine_table[index]); }通过调整phase_step,你可以实现亚赫兹级别的频率分辨率,比如 1.23Hz、15.7Hz……再也不用为非整数倍频率头疼。
而且切换波形也变得简单:换张表就行,节奏不变。
⚠️ 坑点3:RAM不够存多张波表?
想支持正弦、三角、锯齿、噪声、甚至心电图模拟?每张表256×2=512字节,五张才2.5KB——对现代MCU不算啥。但如果你用的是ATmega328P(Arduino Uno),只剩几百字节SRAM,就得精打细算。
🔧优化策略:
- 利用对称性:正弦波只需存1/4周期,其余靠软件翻转;
- 减少表长:128点也能凑合用,牺牲一点THD;
- 存储介质:把大表放在Flash里,运行时动态加载部分段落。
阶梯波怎么变“丝滑”?滤波器必须跟上
就算你的DAC更新得飞快,没有滤波器,输出依然是“锯齿感”十足的伪正弦。
为什么要滤波?
因为DAC输出本质上是一个“零阶保持”系统——每个样本持续一个采样周期。这种信号的频谱如下:
- 主峰在目标频率 $ f_0 $
- 强烈的镜像分量出现在 $ f_s - f_0 $, $ f_s + f_0 $, $ 2f_s - f_0 $ …
- 幅度随频率升高缓慢衰减(sinx/x 特性)
如果你的目标是输出20kHz音频信号,而采样率是256kHz,那么第一个镜像就在 236kHz 处。虽然不在听觉范围,但它会影响后级放大器稳定性,也可能耦合进其他通道。
🔊 实测案例:某学生项目中未加滤波,接耳机放大后听到高频“嘶嘶”声,根源正是这些镜像成分。
二阶Sallen-Key低通滤波器:性价比之选
推荐使用经典的Sallen-Key结构,因为它:
- 只需一个运放
- 易于调节Q值和截止频率
- 对元件误差容忍度较高
设计参数建议:
| 参数 | 推荐值 |
|---|---|
| 截止频率 $ f_c $ | ≥1.2×最大输出频率 |
| Q值 | 0.707(巴特沃斯响应,通带平坦) |
| 运放类型 | 轨到轨输入输出(RRIO),如 MCP6002、TLV2462 |
电路图示意:
Vin ──┬───R1───┬───C2─── Vout │ │ C1 [OPA] │ │ GND R2 │ GND典型取值($ f_c = 25kHz $):
- R1 = R2 = 10kΩ
- C1 = 1nF, C2 = 680pF
💡 小技巧:先仿真!用LTspice搭个模型验证频率响应,避免反复改板。
更进一步?试试多级滤波
单级二阶滤波滚降约40dB/十倍频程。如果要求更高抑制比(比如用于精密测试),可串联两级,构成四阶贝塞尔或切比雪夫滤波器。
但注意:级数越多,相位延迟越大,可能导致低频相位失真。权衡取舍很重要。
系统整合:不只是连几根线那么简单
你以为焊好就能用?真正的挑战往往藏在细节里。
典型架构长这样:
[按键/LCD] ←→ [MCU] → [DAC] → [滤波器] → [缓冲放大] → 输出 ↑ ↓ [用户输入] [DMA/定时器]各环节要点:
| 模块 | 关键考量 |
|---|---|
| 电源 | 数字与模拟部分共地但分开供电;所有IC旁路0.1μF陶瓷电容 |
| DAC选择 | 内置DAC省事,但外置(如DAC8560)精度更高(16位)、建立时间更快 |
| 输出驱动 | 加一级电压跟随器(运放缓冲),防止负载影响滤波器特性 |
| PCB布局 | 模拟走线远离时钟线和数字信号;地平面完整不分割 |
新手最容易犯的3个错误
忽略参考电压稳定性
DAC输出依赖 $ V_{ref} $。如果用MCU的3.3V供电当参考,而电源纹波大,输出就会抖动。
✅ 解法:使用专用基准源(如TL431或REF3033)。采样率低于奈奎斯特极限
想输出10kHz信号?至少要20kHz更新率,建议≥10倍即100kHz以上,否则阶梯明显。
✅ 解法:确保定时器或DMA能支撑所需速率。忘记直流偏置归零
很多DAC输出范围是0~Vref,而你需要±信号?必须加交流耦合电容,或在运放级做电平搬移。
✅ 解法:后级用反相加法器将中点抬至1.65V,或直接设计双电源供电。
它能做什么?远不止实验室玩具
一旦你掌握了这套方法论,扩展空间极大:
- 扫频仪雏形:让频率从1Hz扫到20kHz,配合ADC采集被测系统响应,就能画出Bode图。
- 教学演示平台:实时展示采样定理、混叠现象、滤波效果。
- 音频合成实验:加载不同波表,模拟乐器音色。
- 传感器激励源:为RTD、电容式传感器提供精确交流激励。
甚至有人把它做成“开源函数发生器”,配上OLED屏和编码器旋钮,成本不足百元,性能媲美千元设备。
写在最后:动手,是最好的学习方式
你看再多文档,不如真正烧录一次程序、看一眼示波器上的波形变化。
从第一个“咔咔响”的方波,到终于看到那个圆润的正弦曲线缓缓展开——那一刻你会明白:
电子工程的魅力,从来不在理论推导的尽头,而在你亲手点亮的那个小灯、发出的那一声“嘀”。
所以,别等了。
找块开发板,写个查找表,接上DAC,打开示波器……
去造一个属于你的波形发生器吧。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。