简介这份资源面向学习STM32嵌入式开发与信号发生技术的初学者及工程师提供一套基于STM32F407的DDS函数发生器完整工程。它通过软件配合DAC的方式可生成任意频率、任意占空比、0至3.3V幅值可调的正弦波、方波与三角波直接适配正点原子探索者STM32F407ZGT6开发板无需改动代码即可编译运行适合课程设计、电子竞赛与自学练手。压缩包共307个文件约10.96MB以C源码、头文件、目标文件与编译中间文件为主另含Keil工程文件、固件库、硬件驱动及说明文档覆盖核心算法、外设配置与工程构建各环节。目前已有376人学习下载。读者可借此掌握DDS查表与定时器触发DAC的波形合成思路理解频率、占空比与幅值的软件调节方法并参考完整目录结构快速上手调试与二次开发。1. 从零搭建一台 DDS 函数发生器为什么我选 STM32F407 加软件 DAC手头没有专用 DDS 芯片又需要一台能输出正弦、方波、三角波频率和幅度都能调的任意波形发生器怎么办我一开始也纠结过买现成模块但市面上一两百块的信号发生器要么频率上不去要么波形失真严重调个参数还得翻半天菜单。后来干脆用 STM32F407 自己搭一台——核心思路很直接用定时器触发 DAC把预先算好的波形采样表按节奏“喂”给 DAC 输出频率靠改定时器周期来调波形靠换采样表来切。这就是软件 DDS 函数发生器的基本盘。这套方案适合谁适合已经会点 STM32 基础、想搞明白 DDS 到底怎么在单片机里跑起来的嵌入式开发者也适合手头有 F407 开发板、想拿它当低成本信号源用的硬件工程师。它不需要额外买 DDS 专用芯片一颗 F407 加个运放滤波就能出波形成本低、可控性强参数全在代码里想改就改。但前提是你得接受一个现实软件 DDS 的极限频率和波形质量跟 DAC 分辨率、定时器速度、CPU 主频直接挂钩不是随便写两行就能出干净波形的。2. 软件 DDS 的核心原理采样表、定时器与 DAC 怎么配合2.1 DDS 的数学本质相位累加与查表输出DDS 的全称是直接数字频率合成核心就两个东西一个相位累加器一张波形查找表。相位累加器每个时钟周期加一个频率控制字累加结果的高位拿来当查表地址查出来的值送给 DAC 转换。输出频率的公式是f_out (f_clk × FREQ_WORD) / 2^N其中 f_clk 是采样时钟频率FREQ_WORD 是频率控制字N 是相位累加器位宽。比如采样时钟 168MHz累加器 32 位想要输出 1kHz那 FREQ_WORD 就是 (1kHz × 2^32) / 168MHz ≈ 25565。这个公式决定了频率分辨率——N 越大能分辨的频率间隔越细。在 STM32F407 上我一般用 TIM2 或 TIM6 来产生这个采样时钟。TIM6 挂在 APB1 上时钟 84MHz做 DAC 触发源很合适。DAC 配置成定时器触发模式每次 TIM6 溢出就触发一次 DAC 转换DAC 从内存里取一个采样值输出。整个过程不需要 CPU 干预DMA 把采样表搬到 DAC 数据寄存器就行。2.2 波形采样表的生成点数、位宽与谐波的关系采样表就是波形一个周期内的离散采样值数组。点数越多波形越细腻但占用内存也越大而且在高频输出时可能来不及搬完。我一般用 256 点或 512 点DAC 是 12 位的所以每个采样值范围 0~4095。生成正弦表最直接的办法是用 Python 或 C 算好再贴进代码import math POINTS 256 AMPLITUDE 2047 # 12位DAC中点2048幅度留一点余量 OFFSET 2048 sine_table [] for i in range(POINTS): value int(OFFSET AMPLITUDE * math.sin(2 * math.pi * i / POINTS)) sine_table.append(value) # 输出成C数组格式 print(const uint16_t sine_table[%d] { % POINTS) for i in range(0, POINTS, 8): print( , .join(str(v) for v in sine_table[i:i8]) ,) print(};)这段脚本生成 256 点正弦表幅度 2047偏置 2048保证输出在 0~4095 之间不溢出。方波表更简单前半周期给 4095后半周期给 0。三角波就是线性上升再线性下降。注意方波如果直接跳变高频时会有严重的吉布斯现象实际输出会振铃后面滤波电路要处理好。2.3 定时器触发与 DMA 搬运的配置逻辑TIM6 配置成向上计数ARR 决定采样率。比如 ARR 99时钟 84MHz那更新频率就是 84MHz / 100 840kHz也就是每秒触发 84 万次 DAC 转换。如果采样表 256 点输出频率就是 840kHz / 256 ≈ 3.28kHz。想输出更低频率就加大 ARR想输出更高频率就减小 ARR 或减少采样点数。DMA 配置成循环模式源地址是采样表数组目标地址是 DAC 的 DHR12R1 寄存器每次 TIM6 触发请求搬一个半字。这样 DAC 就能连续不断地输出波形CPU 完全不用管。关键代码如下// DAC 初始化 DAC_InitTypeDef dac; DAC_StructInit(dac); dac.DAC_Trigger DAC_Trigger_T6_TRGO; // TIM6触发 dac.DAC_WaveGeneration DAC_WaveGeneration_None; dac.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, dac); DAC_Cmd(DAC_Channel_1, ENABLE); // DMA 配置 DMA_InitTypeDef dma; DMA_StructInit(dma); dma.DMA_Channel DMA_Channel_7; dma.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; dma.DMA_Memory0BaseAddr (uint32_t)sine_table; dma.DMA_DIR DMA_DIR_MemoryToPeripheral; dma.DMA_BufferSize 256; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Stream5, dma); DMA_Cmd(DMA1_Stream5, ENABLE); // TIM6 配置 TIM_TimeBaseInitTypeDef tim; TIM_TimeBaseStructInit(tim); tim.TIM_Prescaler 0; tim.TIM_Period 99; // 采样率 84MHz/100 840kHz tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, tim); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_Cmd(TIM6, ENABLE);这里有几个参数要盯住DMA_BufferSize 必须和采样表点数一致DMA_MemoryInc 要开否则每次搬的都是同一个值。DAC 输出缓冲建议开驱动能力强一些但输出范围会受限大概 0.2V 到 VDDA-0.2V想要满摆幅就得加运放。3. 从采样表到实际波形频率、幅度与波形切换的代码实现3.1 频率调节改 ARR 还是改频率控制字前面说了两种调频思路一种是改定时器 ARR直接改采样率另一种是保持采样率不变改相位累加器的步进值。第一种简单粗暴但频率分辨率受限于 ARR 的整数粒度低频时 ARR 很大调一档跳变很小高频时 ARR 很小调一档跳变很大。第二种更接近真正的 DDS频率分辨率由累加器位宽决定可以做得很细。我一般用第一种做快速验证用第二种做精细调频。改 ARR 的代码很简单void DDS_SetFreqByARR(uint32_t freq) { // 采样率 84MHz / (ARR1) // 输出频率 采样率 / 采样点数 // 所以 ARR 84MHz / (freq * 采样点数) - 1 uint32_t arr 84000000 / (freq * SAMPLE_POINTS) - 1; TIM6-ARR arr; TIM6-EGR TIM_EGR_UG; // 立即更新 }注意 ARR 最小不能小于 1否则定时器不工作。另外改 ARR 时最好先关定时器再改改完再开避免中间出现异常周期。实际调试时我发现如果频率设得太高ARR 算出来是 0 或者负数波形就没了这时候得加个下限保护。3.2 幅度调节DAC 参考电压与数字增益的取舍调幅度有两种办法一是改 DAC 参考电压但 F407 的 VREF 一般直接接 VDDA不好动态调二是改采样表的数值范围相当于数字增益。我一般用第二种把采样表的值乘以一个系数再输出。void DDS_SetAmplitude(uint16_t table[], uint16_t points, float gain) { for (int i 0; i points; i) { int32_t val (int32_t)((table[i] - 2048) * gain) 2048; if (val 4095) val 4095; if (val 0) val 0; output_table[i] (uint16_t)val; } }这里 gain 范围 0~1超过 1 会削顶。注意这个操作会修改 DMA 正在搬运的缓冲区如果直接改原表输出会瞬间跳变。稳妥做法是准备两个缓冲区改完再切换 DMA 地址或者先停 DMA 再改。3.3 波形切换正弦、方波、三角波的查表实现波形切换就是换一张采样表。我一般把几种波形表都放在 Flash 里切换时改 DMA 的源地址就行const uint16_t *wave_tables[] {sine_table, square_table, triangle_table}; const uint16_t wave_points[] {256, 256, 256}; void DDS_SwitchWave(uint8_t type) { DMA_Cmd(DMA1_Stream5, DISABLE); while (DMA_GetCmdStatus(DMA1_Stream5) ! DISABLE); DMA1_Stream5-M0AR (uint32_t)wave_tables[type]; DMA1_Stream5-NDTR wave_points[type]; DMA_Cmd(DMA1_Stream5, ENABLE); }方波表就是前 128 个点给 4095后 128 个点给 0。三角波表是 0 到 4095 线性上升再线性下降。注意方波在高频时因为采样点少边沿会变得很斜而且会有过冲这是正常现象加个低通滤波会好很多。4. 避坑与排查软件 DDS 最容易翻车的五个地方4.1 波形频率对不上差一个固定倍数现象设了 1kHz示波器测出来是 2kHz 或者 500Hz。原因采样点数、定时器时钟、ARR 三者关系算错了。常见错误是把 APB1 定时器时钟当成 42MHz实际上 F407 的 APB1 定时器时钟是 84MHzAPB1 预分频不为 1 时定时器时钟是 APB1 时钟的 2 倍。解决先确认 SystemCoreClock 和 APB1 定时器时钟用示波器测 TIM6 的触发频率再反推采样率。公式里的每个量都要实测验证别信手册上的标称值。4.2 DAC 输出一直是中点电压没有波形现象DAC 引脚电压固定在 1.65V 左右示波器看不到任何变化。原因DMA 没启动、DAC 触发源选错、或者 DMA 请求没使能。F407 的 DAC 有独立的 DMA 请求光配置 DMA 流还不够还得调 DAC_DMACmd(DAC_Channel_1, ENABLE)。解决检查三个地方——DAC 触发源是不是 TIM6 TRGO、DMA 流有没有使能、DAC 的 DMA 请求有没有开。用调试器看 DMA 的 NDTR 寄存器如果一直不变说明根本没搬数据。4.3 高频输出时波形严重失真幅度也掉现象低频时波形干净频率一上去就变成三角波甚至正弦波幅度也明显下降。原因DAC 输出缓冲的压摆率有限而且采样点数在高频时太少一个周期只有几个点自然出不来好波形。另外 DMA 搬运速度也可能成为瓶颈。解决降低采样点数到 64 或 32提高采样率让一个周期至少有 8 个点以上。如果还不行就得换专用 DDS 芯片或者用 FPGA 做。软件 DDS 的极限大概在几十 kHz 到一百多 kHz再高就不现实了。4.4 改频率时波形会闪一下或者卡死现象调频率的瞬间输出中断或者直接没波形了。原因改 ARR 时定时器还在跑中间可能产生一个超长或超短的周期导致 DMA 请求异常。另外如果 ARR 算出来是 0定时器直接停摆。解决改 ARR 前先关定时器改完再开。加个下限保护ARR 最小给 1。如果用的是相位累加器方案改频率控制字是原子操作不会有这个问题。4.5 输出有高频噪声底噪很大现象波形上叠加了明显的毛刺FFT 看有很多高频谐波。原因DAC 输出是阶梯波本身就含大量高频谐波。另外电源噪声、地线干扰也会耦合进来。解决DAC 输出后加一个截止频率略高于目标频率的低通滤波器二阶或三阶巴特沃斯就行。电源加 LC 滤波模拟地和数字地单点连接。示波器探头要用短地线否则测到的噪声比实际大很多。5. 进阶技巧用相位累加器做精细调频与扫频输出前面用的改 ARR 方案频率分辨率受限于整数 ARR低频时还行高频时调一档跳几百 Hz做扫频输出会很难看。真正好用的方案是相位累加器保持采样率不变每个采样周期累加一个频率控制字用累加器的高位查表。这样频率分辨率只取决于累加器位宽32 位累加器在 840kHz 采样率下分辨率是 840000/2^32 ≈ 0.0002Hz完全够用。实现上DAC 还是由 TIM6 触发但 DMA 搬的不再是固定采样表而是一个实时计算的缓冲区。可以在 TIM6 中断里做相位累加和查表但中断频率太高 CPU 扛不住。更好的办法是用 DMA 双缓冲半满和全满中断里各算一半CPU 压力小很多。#define PHASE_BITS 32 #define TABLE_BITS 8 #define TABLE_SIZE (1 TABLE_BITS) volatile uint32_t phase_acc 0; uint32_t freq_word 0; uint16_t dds_buffer[2][DMA_BUF_SIZE]; void DDS_SetFreqWord(float freq) { // freq_word freq * 2^32 / 采样率 freq_word (uint32_t)(freq * 4294967296.0f / sample_rate); } void DMA_HalfComplete_Handler(void) { uint32_t local_phase phase_acc; for (int i 0; i DMA_BUF_SIZE / 2; i) { uint16_t idx (local_phase (PHASE_BITS - TABLE_BITS)) (TABLE_SIZE - 1); dds_buffer[0][i] sine_table[idx]; local_phase freq_word; } phase_acc local_phase; }这段代码在 DMA 半满中断里填充前半缓冲区全满中断填后半DMA 循环搬运输出就是连续波形。频率控制字一变下一个采样周期就生效没有闪断。扫频输出就是每隔一段时间改一次 freq_word线性扫、对数扫都行。验证方法很简单设一个 1kHz 正弦用示波器测周期是不是 1ms设 10kHz看波形还像不像正弦设扫频听蜂鸣器或者看频谱仪频率是不是线性变化。我一般还会用 FFT 看一下谐波分布确认低通滤波器有没有起作用。从那以后我每次调 DDS 都先把采样率、点数、频率三者的关系在纸上算一遍再用示波器实测验证绝不凭感觉改参数。希望帮到你。本文还有配套的精品资源点击获取