
简介基于STM32F407的DDS函数发生器项目是一套通过软件配合DAC实现波形输出的完整工程代码面向电子工程学生、嵌入式开发者及信号处理爱好者。支持任意频率、占空比和0~3.3V幅值调节的正弦波、方波、三角波可直接在正点原子探索者F407ZGT6开发板上运行无需改动任何代码适合学习单片机外设编程与DDS原理。压缩包共307个文件约10.96MB除核心C语言源码和头文件外还包含Keil工程文件uvprojx/uvoptx、编译中间产物crf/o/axf、链接脚本、启动文件以及HARDWARE、SYSTEM、FWLIB等目录便于查看配置和重新编译。已有374人学习下载。项目提供了从定时器、PWM到DAC输出的完整实现思路结合查表法或快速傅里叶变换生成波形并配有keilkilll.bat清理脚本和readme说明能让读者快速理解并二次开发属于自己的函数发生器。 做函数发生器不少人第一反应是买 AD9850、AD9834 这类专用 DDS 芯片。但如果你手边已经有一块 STM32F407 开发板而需求又集中在实验信号源、传感器激励、音频测试这些中低频应用那么完全可以用 MCU 内置的 DAC 配合软件 DDS 自己造一台。这个方案能出正弦波、三角波、锯齿波配合引脚翻转还能出像模像样的方波频率范围从 0.1Hz 一路推到几十 kHz。我先后做了两个硬件版本中间踩了波形台阶、直流偏置、滤波干扰这些坑下面就从选型、原理、电路、代码到调试完整复盘一遍。1. 先搞明白DDS 原理与 F407 的硬件底子1.1 单片机的 DAC 能干什么STM32F407 内置两个 12 位 DAC 通道理论最高转换速率约 1MSPS。所谓“软件配合 DAC 实现 DDS”本质就是让 DAC 按固定节奏把一张波形表里的电压值逐个翻译成模拟信号表里存的是波形一个周期的离散点DAC 把每个点变成对应电压只要点与点之间的时间间隔够短人眼和示波器看到的就是一条连续曲线。F407 的 DAC 在实际使用中有两个容易被忽略的约束第一输出范围是 0~3.3V 单极性想要正负对称波形必须外接运放做电平搬移第二DAC 本身带输出缓冲但驱动能力有限直接把信号引到长线或低阻抗负载上波形就会出现明显塌陷。所以真正的硬件设计重心不在 MCU 内部而在 DAC 后面那一小段模拟电路上。1.2 软件 DDS 的三板斧相位累加、查表、定时输出软件 DDS 的核心思想可以用一句话概括用一个 32 位计数器模拟信号相位每来一个采样时钟就往这个计数器里加一个固定数值计数器的最高几位拿去查波形表查到的值送给 DAC 输出。这个“计数器”就是相位累加器Phase Accumulator加的那个固定值叫频率控制字Frequency Tuning Word。相位累加器是 32 位的波形表通常取 256、512 或 1024 个点。每个采样时钟相位累加器加一次相当于波形指针在表上前进了一步走得快输出频率就高走得慢输出频率就低。配合定时器固定采样节奏天然就能实现稳定频率输出。频率控制字的计算公式为FTW f_out × 2^32 / f_sample其中 f_out 是目标输出频率f_sample 是 DAC 的采样率。举个例子采样率 1MHz想输出 10kHz 正弦波那么 FTW 10000 × 4294967296 / 1000000约等于 42949673。这个数值算出来后每次定时器中断里执行一次“相位累加 查表 写 DAC”10kHz 正弦波就出来了。2. 硬件电路设计把 DAC 的信号变得能见人2.1 运放缓冲别让输出阻抗拖垮波形DAC 输出信号后接的第一级放大电路首要任务是“把信号变成低阻抗源”。我用的方案是双电源供电的运放做成同相缓冲器再在前级做电平搬移。具体做法DAC 输出 0~3.3V 单极性信号用运放做减法器减去 1.65V 参考电压同时把幅度放大到需要的范围。这样 0~3.3V 就被映射成约 -1.65V~1.65V 的对称信号。运放选型上建议注意三点一是选低压摆率足够高的型号至少要能跟上你想要的最高输出频率二是偏置电流要小避免给前级电路引入额外直流误差三是供电电压余量要足够别让信号削顶。我用过 OPA2350 和 OPA2277前者速度更快适合几十 kHz 正弦波输出后者精度更好但带宽低一些做低频信号源更合适。如果只输出几 kHz 以内的波形用 NE5532 这类通用运放也完全够用成本还低。2.2 滤波与幅度调节简单但别省DDS 输出天然带一个特点阶梯状。因为波形表是离散点DAC 输出的是“点与点之间保持电压”的保持型信号除非表足够大、采样率足够高否则高频段一定会看到台阶同时还会在某些频率点出现镜频噪声。后级加低通滤波器几乎是必须的。我用的是一阶 RC 低通加运放缓冲的组合RC 截止频率约 100kHz。这个方案对 20kHz 以内的信号基本无损对 100kHz 以上的高频杂散有抑制作用。如果你想覆盖更高频率建议把滤波器做成两档切换低频档用 10kHz 截止高频档用 300kHz 截止否则输出 80kHz 信号时幅度会被滤波器吃掉不少。幅度调节方面最简单的做法是在运放反馈电阻上加一个多圈电位器做成可调增益。想要程控幅度可以把反馈电阻换成数字电位器或者用压控增益放大器。对于普通实验用途手调电位器最实用也最不容易引入额外噪声。2.3 波形表点数和采样率怎么定整套方案的输出指标由两个参数共同决定波形表点数和采样率。表越大波形越细腻采样率越高能输出的最高频率越高。但这两者都受硬件限制。实用配置参考如下波形表点数采样率最高可用频率波形质量256 点1MHz约 20kHz低频目测无台阶高频轻微锯齿512 点1MHz约 40kHz20kHz 以内波形质量较好1024 点1MHz约 80kHz50kHz 以内波形细腻Flash 占用稍大512 点500kHz约 20kHz存储和 CPU 开销低中断压力小“最高可用频率”通常按采样率除以表点数的 5~10 倍来估算。比如 512 点、1MHz 采样每个正弦周期只覆盖约 77 个采样点1MHz/13kHz超过这个频率后波形开始失真到 50kHz 时每周期只剩 20 个点形状就比较难看了。所以别指望一套参数通吃整个频率段需要根据目标频率段微调。3. 软件实现CubeMX 配置到 DDS 核心代码3.1 CubeMX 里把 DAC、DMA、定时器串起来软件部分我全程用的 HAL 库CubeMX 配置起来比较快。关键配置有几个DAC 选择 12 位右对齐开启输出缓冲触发方式选择 TIM6 触发DMA 方式可以走循环模式也可以走定时器中断手动更新两种场景不同后面会细说。CubeMX 里的操作路径大致是配置 DAC 通道为 Output ModeTrigger 选 Timer 6 Trigger Out event再给 TIM6 设置合适的预分频和自动重载值使更新事件频率等于目标采样率如果需要 DMA 自动输出波形就在 DMA Settings 里加一条 DAC1_CH1方向 Memory-To-Peripheral模式 Circular每次传输 16 位半字。TIM6 是基本定时器没有外部引脚纯粹适合做内部触发源。采样率的计算要特别留意时钟树F407 主频 168MHz 时APB1 定时器时钟是 84MHz而不是 APB1 外设时钟的 42MHz。初次配置时很容易在这里算错导致输出频率整体差一倍。3.2 核心 DDS 代码与波形表生成波形表可以在 PC 上先用 Python 生成再复制成 C 数组。生成 512 点正弦表的脚本如下import math N 512 print(const uint16_t sin_table[%d] { % N) for i in range(N): v 2047 int(2047 * math.sin(2 * math.pi * i / N)) print(0x%03X, % v, end) if i % 12 11: print() print(};)注意这里把 12 位 DAC 的满量程 0~4095 映射成以 2048 为中心的正弦波既不触发负电压也留了一定余量避免输出削波。三角波、锯齿波表的生成逻辑同理只需要替换幅度计算函数。DDS 核心代码用定时器中断实现#define PHASE_BITS 32 #define TABLE_BITS 9 /* 512 点 */ #define TABLE_LEN (1 TABLE_BITS) static const uint16_t sin_table[TABLE_LEN] { /* 脚本生成 */ }; volatile uint32_t phase_acc 0; volatile uint32_t phase_inc 0; void dds_set_freq(uint32_t f_out, uint32_t fs) { phase_inc (uint32_t)(((uint64_t)f_out PHASE_BITS) / fs); } void TIM6_IRQHandler(void) { phase_acc phase_inc; uint32_t idx phase_acc (PHASE_BITS - TABLE_BITS); DAC-DHR12R1 sin_table[idx]; __HAL_TIM_CLEAR_IT(htim6, TIM_IT_UPDATE); }这里直接操作 DAC-DHR12R1 寄存器比调用 HAL_DAC_SetValue 更快中断里少一层函数调用开销。相位累加器是软件模拟的所以频率可以无限细分理论频率分辨率约为 Fs / 2^32在 1MHz 采样率下约 0.00023Hz完全够用。3.3 频率字计算与不同输出方式对比上节代码里的 dds_set_freq 就是频率字计算入口。以采样率 1MHz、输出 10kHz 为例FTW 10000 × 4294967296 / 1000000 ≈ 42949673这个数值直接写进 phase_inc每次中断相位累加后相位的高 9 位正好对应 512 点表的索引查表得到 12 位 DAC 值写入寄存器完成一次输出。整个过程大约消耗几十个 CPU 周期1MHz 中断频率下 F407 的 168MHz 主频依然有大量余量。如果你不需要随时改频率只是想固定输出某几个频率可以用 DMA 循环模式输出静态表。CubeMX 里配置好 TIM6 触发和 DAC1 DMA 循环后调用 HAL_DAC_Start_DMA把波形表地址传给 DMADAC 就会自动按采样节奏把整张表循环输出。这种方式的优点是完全不占 CPU缺点是输出频率由“采样率 ÷ 表点数”决定改为其他频率需要重新生成一张表无法快速连续调频。两种方式各有用武之地固定频率、长期运行场景用 DMA扫频、交互式调频场景用定时器中断方式。4. 功能扩展调频、扫频与控制4.1 扫频怎么做扫频函数发生器的经典用途之一是测幅频特性。软件 DDS 做扫频非常直接在定时器中断外部周期更新 phase_inc 即可。最简单的方式for (uint32_t f 100; f 20000; f 100) { dds_set_freq(f, FS); HAL_Delay(10); }每 10ms 步进一次 100Hz从 100Hz 扫到 20kHz 耗时约 2 秒。对数扫频更符合电路幅频特性测试习惯可以按固定倍率递增next_freq start_freq * ratio。f_out 连续变化时只要 dds_set_freq 更新 phase_inc 的瞬间不重置 phase_acc波形相位就是连续的不会出现突兀的跳变。4.2 切换波形如何保持相位连续切换波形时最容易犯的错是重新初始化 phase_acc 为 0。这样做会让输出信号在切换瞬间跳变一大截后级功放或被测设备可能因此出现脉冲干扰。正确做法是相位累加器完全不复位只切换波形表指针const uint16_t *cur_table sin_table; void switch_wave(const uint16_t *new_table) { cur_table new_table; /* phase_acc 不动 */ }同一个相位值在两张表里可能代表不同电压但只要两张表都以同样相位起点存储切换瞬间虽然会有电压跳变但相位关系连续听感和示波器观察都自然很多。方波不建议走 DAC 输出。DAC 做方波有两个问题一是输出带宽不够上升沿会被拉成斜坡二是 DAC 值从 0 跳到满量程要经过 dac 输出缓冲建立时间明显。方波直接用某个定时器的 PWM 输出引脚翻转占空比和频率都能精确控制效果比 DAC 好一个数量级。函数发生器的“方波输出”接口可以和 DAC 波形输出共用一个 BNC 端子内部用继电器或模拟开关切换。4.3 人机交互按键、编码器、屏幕控制端我建议优先用旋转编码器加按键操作逻辑简单直接。选频率、选波形、选幅度三个状态分别映射到编码器旋钮和按键按下事件。加上一块 SPI 接口的 TFT 小屏显示当前参数整台仪器就基本成型了。F407 驱动 ILI9341 这类屏幕可以用 SPI 也可以用 FSMC 并口FSMC 刷新速度快得多但需要占用大量引脚适合正式做仪器面板。早期调试阶段用串口打印当前频率和波形类型就够了别过早把精力花在界面上。屏幕和输入这套东西可以后续慢慢叠DDS 核心逻辑稳定才是关键。如果要做成独立仪器外置 DAC 并非必要但电源设计必须认真对待数字电源和模拟电源分开走线DAC 参考电压 VREF 建议单独用基准源或者至少加强滤波否则输出波形上会看到明显的开关噪声毛刺。5. 实测与排雷这些锅我替你背过了5.1 波形质量与性能实测我用 512 点正弦表、采样率 1MHz 做了实测1kHz 时示波器看到的正弦波几乎完美谐波肉眼不可见10kHz 时波形边缘依然圆滑FFT 显示二次谐波约 -40dB到 30kHz 时能看到轻微台阶达到 50kHz 后台阶明显但用于测频响这类对波形纯度要求不高的场景仍然可用。这个结果说明一个事实软件 DDS 的上限取决于采样率和波形表点数而不是 CPU 算力。F407 跑到 1MSPS 时中断处理开销只有约 5% CPU剩余资源完全可以支撑屏幕刷新、编码器扫描和参数计算。5.2 常见问题速查表现象可能原因解决方法输出频率整体偏高一倍APB1 定时器时钟按 42MHz 配置确认时钟树APB1 Timer Clock 应为 84MHz波形明显呈台阶状波形表点数太少或采样率过低增大表到 512 点或提高触发频率高频幅度衰减严重后级 RC 滤波截止频率过低提高滤波截止频率或做分档滤波输出带直流偏置DAC 单极性输出未做电平搬移增加减法电路或隔直电容波形上叠加周期性毛刺数字电源噪声耦合到模拟部分模拟电源单独走线VREF 加强滤波DMA 方式切换频率后波形乱跳DMA 循环缓冲区被中途改写待 DMA 半传输完成再改表或用双缓冲5.3 两个容易被忽略的细节第一个是定时器优先级。TIM6 中断要设置为足够高的优先级尤其是在跑屏幕刷新和编码器扫描的情况下。如果中断被其他任务长时间打断采样节奏就不均匀波形上会看到随机抖动频率越高越明显。第二个是 DAC 输出缓冲问题。CubeMX 里 DAC Output Buffer 默认开启好处是输出阻抗低坏处是驱动能力有限。实测用输出缓冲直连 1kΩ 负载时波形顶部会有轻微变形换成 10kΩ 以上负载后正常。如果你要驱动更低阻抗的负载必须在外部加运放缓冲。做这台函数发生器给我最大的体会是硬件连线只要不犯原则性错误系统的天花板多半在软件设计和细节处理上。先把采样率、表点数、滤波器这三件事算清楚DDS 就成功了一半。剩下的一半就是在调试中慢慢把噪声、偏置、毛刺一个个磨平这也是这种小工具项目最让人上瘾的地方。本文还有配套的精品资源点击获取