从零打造任意波形信号发生器:一个工程师的实战笔记
最近在调试一款超声驱动电路时,我遇到了一个棘手的问题——手头的标准函数发生器只能输出正弦、方波和三角波,而我的系统需要一段特定包络的脉冲串来模拟真实工作场景。于是,我决定不再依赖昂贵的专业设备,而是自己动手做一台真正意义上的任意波形信号发生器(AWG)。
这不仅是一次硬件搭建,更是一场对数字信号处理、嵌入式实时控制与模拟接口设计的综合实战。今天,我就以“过来人”的身份,带你一步步实现这个项目,不讲空话,只说干货。
为什么我们需要“任意”波形?
你可能用过那种带旋钮的函数发生器,调频率、换波形,看似方便,实则受限。它能生成的波形是预设好的,无法表达复杂的时间序列行为。比如:
- 超声探头所需的突发脉冲+衰减包络
- 电机驱动中的非对称PWM激励
- 雷达仿真里的线性调频(Chirp)信号
这些都超出了传统仪器的能力范围。而任意波形信号发生器的核心价值,就在于把波形变成数据——只要你能写成数组,它就能输出。
换句话说,AWG 的本质就是:
内存中存波形 → 定时送 DAC → 模拟输出 → 经滤波平滑
听起来简单?但要让它稳定、精确、低噪声地工作,背后涉及多个关键技术点的协同优化。下面我们就从最核心的三个模块讲起。
第一块拼图:DAC——让数字“活”起来
数模转换器不是“数转模”那么简单
很多人以为,只要给 DAC 写个值,就会立刻得到对应的电压。但实际上,DAC 的性能直接决定了 AWG 的上限。
我们常用的 DAC 分辨率有 8 位、12 位、16 位。以 STM32 内部的 12 位 DAC 为例,在 3.3V 参考电压下,最小步进为:
$$
\Delta V = \frac{3.3}{4095} \approx 0.8\,\text{mV}
$$
这意味着你能分辨出 0.8mV 的变化,听起来不错。但如果采样不够快或跳变不连续,输出的仍然是“楼梯状”的离散信号,远非平滑波形。
所以选 DAC 时要关注几个关键参数:
| 参数 | 影响 |
|---|---|
| 分辨率 | 决定动态范围和信噪比(SNR),越高越好 |
| 建立时间 | 输出稳定所需时间,限制最大更新率 |
| DNL/INL | 非线性误差,影响波形保真度 |
| 单调性 | 确保码值增加时电压不会下降 |
推荐器件:
-MCP4725:I²C 接口,12 位,适合低速应用
-AD5663:16 位轨到轨输出,内置参考源
-STM32F4 内部 DAC:免外接芯片,成本低,但精度一般
别再用 delay 控制频率了!
新手常写的代码长这样:
for (int i = 0; i < 256; ++i) { DAC_Set(sine_table[i]); delay_us(10); // 想通过延时控制频率? }这种做法问题很大:delay_us()受编译器优化、中断干扰影响极大,导致采样间隔不均,产生严重抖动(jitter),最终输出的波形频谱发散、杂散多。
真正的解决办法是:用硬件定时器触发 DAC 更新。
第二块拼图:DDS——软件定义频率的灵魂
相位累加器才是精髓
如果你只是查表播放固定波形,那叫“波形回放”。而 DDS 才是现代 AWG 的核心技术,它让你可以用软件动态调节频率,且切换无相位突变。
DDS 的核心结构其实很简单:
[频率控制字 FCW] ↓ +------------------+ | 相位累加器 | → [高位截取] → 查找表 → DAC +------------------+ ↑ 每周期加一次每经过一个时钟周期,相位寄存器就加上一个FCW,就像秒针匀速走动。然后我们取高几位作为查找表索引,读出对应幅度值送给 DAC。
输出频率公式为:
$$
f_{out} = \frac{\text{FCW} \times f_{clk}}{2^N}
$$
其中 $ N $ 是相位寄存器位宽(通常 32 位),$ f_{clk} $ 是系统主频。
举个例子:
假设使用 STM32F4(主频 168MHz),配置定时器每 1μs 触发一次 DAC 更新(即采样率 1MHz),相位寄存器 32 位,则最小频率步进可达:
$$
\Delta f = \frac{1 \times 10^6}{2^{32}} \approx 0.00023\,\text{Hz}
$$
也就是说,你可以精确生成 1.23456 Hz 的信号,这是普通函数发生器做不到的。
实现一个轻量级 DDS 引擎
#define TABLE_SIZE 1024 static uint16_t sine_lut[TABLE_SIZE]; static uint32_t phase_accum = 0; static uint32_t fcw = 0; // 初始化正弦查找表(预计算) void init_sine_table(void) { for (int i = 0; i < TABLE_SIZE; ++i) { double angle = 2.0 * M_PI * i / TABLE_SIZE; sine_lut[i] = (uint16_t)(2047 + 2047 * sin(angle)); } } // 设置目标频率(单位:Hz) void dds_set_frequency(float freq, float sample_rate) { fcw = (uint32_t)((freq * (1ULL << 32)) / sample_rate); } // 获取下一个采样点 uint16_t dds_next_sample(void) { phase_accum += fcw; uint16_t index = (phase_accum >> 22) & (TABLE_SIZE - 1); // 提取高10位 return sine_lut[index]; }注:这里右移 22 位是为了从 32 位相位中提取 10 位索引(支持 1024 点表)。若使用更大表格,可调整位数。
这个 DDS 引擎可以在每次定时器中断中调用,输出当前样本,完全不受 CPU 负载波动影响。
第三块拼图:MCU + 定时机制——构建零干预流水线
单片机不只是“控制器”,更是“数据流调度中心”
很多人低估了 MCU 在 AWG 中的作用。你以为它只是算算 DDS、写写 DAC?错。它的真正能力在于整合硬件资源,构建自动化数据通道。
理想状态下的 AWG 应该是这样的:
一旦启动,CPU 就可以去睡觉了
怎么做?靠的是三大神器组合拳:TIM + DAC + DMA
TIM(定时器):精准节拍发生器
设置一个周期性中断或事件触发信号,比如每 1μs 发一次 TRGO 脉冲。
DAC:支持外部触发模式
配置为“等待 TRGO 来了再更新”,而不是由 CPU 主动写入。
DMA:自动搬运数据
提前把波形数组放在内存里,让 DMA 自动按序送到 DAC 数据寄存器。
三者联动后,整个流程无需 CPU 参与,实现了真正的“后台静默运行”。
配置示例(基于 STM32 HAL 库)
void awg_init_hardware_dma(void) { TIM_HandleTypeDef htim3 = {0}; DAC_ChannelConfTypeDef dac_ch = {0}; // --- 配置 TIM3 作为触发源 --- __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance = TIM3; htim3.Init.Prescaler = 83; // 84MHz APB1 → 1MHz 计数频率 htim3.Init.Period = 999; // 1kHz ~ 1MHz 可调 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_Base_Start(&htim3); // 启用主模式更新触发(TRGO) htim3.TriggerOutputSource = TIM_TRGO_SOURCE_UPDATE; HAL_TIM_GenerateEvent(&htim3, TIM_EVENTSOURCE_UPDATE); // --- 配置 DAC --- hdac.Instance = DAC; HAL_DAC_Init(&hdac); dac_ch.DAC_Trigger = DAC_TRIGGER_T3_TRGO; // 关键!由 TIM3 触发 dac_ch.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; HAL_DAC_ConfigChannel(&hdac, &dac_ch, DAC_CHANNEL_1); // --- 启动 DMA 循环传输 --- HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_lut, TABLE_SIZE, DAC_ALIGN_12B_R); }这段代码执行完后,DAC 就会按照 TIM3 设定的节奏,自动从sine_lut数组中循环读取数据并输出,直到你手动停止。
构建完整系统:不只是“能出波形”
系统架构全景图
[用户输入] → [MCU] ↓ [DDS引擎 / 查表] ↓ [DMA] ←→ [DAC] → [LPF] → [OpAmp] → [输出] ↑ ↑ ↑ [定时器] [参考电压] [增益控制]别忘了,DAC 输出的是“阶梯波”,含有丰富的高频镜像成分(奈奎斯特频率以上的谐波),必须通过低通滤波器(LPF)滤除。
如何设计抗混叠滤波器?
原则很简单:截止频率略高于目标信号最高频率,衰减陡峭。
例如你要输出 100kHz 正弦波,建议使用5阶巴特沃斯低通滤波器,截止频率设为 120kHz 左右。这样既能保留基波,又能有效抑制 1MHz 附近的采样镜像。
运放部分建议使用低噪声、高带宽型号,如OPA2134或LMH6642,并加入50Ω 输出阻抗匹配,兼容示波器、频谱仪等标准负载。
常见坑点与避坑指南
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 波形毛刺多、底噪大 | 数字电源噪声串入模拟部分 | 模拟地与数字地单点连接,使用磁珠隔离 |
| 频率不准或漂移 | 使用内部 RC 振荡器 | 改用外部晶振,精度提升百倍 |
| 幅度随温度变化 | 参考电压不稳定 | 使用 TL431 或 REF3033 等精密基准源 |
| 输出无法带负载 | 运放驱动能力不足 | 加入缓冲器或选用电流反馈型运放 |
| 切换波形卡顿 | 全部加载到 RAM 导致内存溢出 | 外扩 SPI Flash 存储波形模板,按需加载 |
进阶玩法:不止于“发生器”
掌握了这套方法论,你可以轻松扩展功能:
- 双通道同步 AWG:两路 DAC 共享同一时钟,实现 I/Q 调制;
- 扫频信号生成:让 FCW 随时间线性增长,产生 Chirp 信号;
- 远程控制版:加上 ESP32-WiFi 模块,手机 APP 调参;
- 上位机图形化编辑:Python 编辑波形 → 下载到设备 → 实时播放;
- 闭环校准:接入 ADC 反馈,自动补偿 DAC 非线性。
甚至,你可以把它改造成:
- 简易音频播放器
- 函数发生器教学平台
- PWM 波形生成工具(用于电机控制)
- 脉冲激励源(用于压电陶瓷驱动)
最后一点思考
做这台 AWG 的过程,让我重新理解了“信号”的本质:
信号 = 时间 + 幅度的有序排列
无论是声音、振动、光强还是电压,它们都可以被数字化、存储、重组、再生。而我们手中的 MCU 和 DAC,正是打开这扇门的钥匙。
更重要的是,这个项目教会我一种思维方式:
不要满足于“调用库函数”,要去理解每一行代码背后的物理意义;不要迷信商业仪器,要学会拆解复杂系统的底层逻辑。
当你亲手做出第一段自定义波形,并在示波器上看到它完美呈现时,那种成就感,远胜于买一台万元设备。
如果你也在学习嵌入式、信号处理或自动化测试,不妨试试从做一个 AWG 开始。它不大,但五脏俱全;它不贵,却能带你走得很远。
如果你在实现过程中遇到具体问题——比如“DMA 不启动”、“波形不对称”、“滤波器震荡”——欢迎留言交流,我可以分享更多调试细节和实测波形截图。一起把这件事做得更扎实。