资讯动态

STM32F407 DDS函数发生器:相位累加与DAC定时器触发详解

发布时间:2026/10/19 14:58:51 来源:尧图企业网站定制
简介这份资源面向学习STM32嵌入式开发与信号发生技术的初学者及电子爱好者提供一套基于STM32F407的DDS函数发生器完整工程。它通过软件配合DAC的方式可生成任意频率、任意占空比、0至3.3V幅值可调的正弦波、方波与三角波可直接配套正点原子探索者STM32F407ZGT6开发板运行无需改动代码适合用于课程设计、实验教学与自学研究。压缩包共307个文件约10.96MB以c源文件、h头文件、o与crf编译中间文件、d依赖文件为主另含uvprojx工程文件、hex烧录文件及bat批处理脚本覆盖核心代码、固件库、硬件驱动与用户程序等模块。目前已有376人学习下载。读者可从中获得完整的DDS波形生成算法实现、DAC与定时器配置思路、工程目录组织方式及可直接编译下载的Keil工程便于快速上手并在此基础上扩展波形参数与功能。1. 为什么DDS能凭空造出任意波形从相位累加说起很多人第一次看到STM32F407配合DAC输出正弦波、方波、三角波甚至任意自定义波形时会觉得这件事有点玄学——一块MCU一个12位DAC凭什么能连续吐出频率精确、相位连续的模拟信号更反直觉的是DDS直接数字频率合成的核心运算其实只有两个动作相位累加和查表。没有振荡器没有模拟锁相环纯数字逻辑就能把频率分辨率做到毫赫兹级别。这背后的机制值得搞懂因为它决定了你能输出多高的频率、多纯的频谱、多灵活地切换波形。本文围绕基于STM32F407的DDS函数发生器软件配合DAC实现这份资源把相位累加器、频率控制字、采样率与奈奎斯特限制、DAC触发时序、定时器联动这些底层机制拆开讲透让你从能跑走到知道为什么能跑。2. 相位累加器DDS的心脏为什么是一个加法器2.1 从角度到地址的映射逻辑DDS最核心的部件是一个N位相位累加器。每个采样时钟周期它把频率控制字FTWFrequency Tuning Word加到当前相位值上累加结果的高位作为查表地址低位被丢弃。这个丢弃不是浪费而是频率分辨率的来源。设系统时钟为 $f_{clk}$相位累加器位宽为 $N$频率控制字为 $K$则输出频率为$$f_{out} \frac{K \cdot f_{clk}}{2^N}$$这个公式说明两件事第一输出频率由$K$线性控制改$K$就是改频率第二频率分辨率是 $f_{clk}/2^N$。如果$N32$$f_{clk}168\text{MHz}$分辨率约0.039Hz。这就是为什么DDS能做到极细的频率步进——它本质上是把频率这个连续量量化成了整数$K$。常见的实现思路是用uint32_t变量存相位每次DAC采样前执行phase ftw;然后取phase 24作为0~255的查表索引。这样表长256相位精度8位剩下的24位用于频率控制。表越长谐波失真越小但RAM占用越大。STM32F407有192KB SRAM放一张1024点的12位正弦表2KB绰绰有余。2.2 频率控制字怎么算一个容易翻车的除法假设你要输出1kHz正弦波DAC采样率设为1MHz查表长度1024相位累加器32位。那么每个采样点相位增量应该是// 目标频率 1kHz采样率 1MHz表长 1024累加器 32 位 // 每个采样周期相位增加 1/1000 圈即 2^32 / 1000 uint32_t ftw (uint32_t)((uint64_t)target_freq * (1ULL 32) / sample_rate); // ftw 1000 * 4294967296 / 1000000 4294967这里必须用64位中间变量否则1000 * 2^32会溢出32位。这是血泪经验很多人直接写(uint32_t)(freq * 4294967296 / sample_rate)结果频率完全不对查半天以为是定时器配置错了。参数说明target_freq是目标频率sample_rate是DAC更新率由定时器触发频率决定1ULL 32是$2^{32}$。算出来的ftw就是每次累加的步长。改频率时只改这个值相位连续不会产生切换噪声。2.3 相位截断与杂散为什么表不是越长越好相位累加器32位但查表只用高10位1024点这意味着低22位被截断。截断会引入相位误差表现为输出频谱上的杂散分量。理论上每截断1位杂散改善约6dB。但表长增加会带来两个代价一是查表时间变长限制最高输出频率二是DAC建立时间不变的情况下采样率上不去。我一般会这么理解表长1024、相位累加器32位是一个甜点。再往上杂散改善有限但代码复杂度和内存占用上升。如果要做频谱要求极高的场景应该换外部DAC或专用DDS芯片而不是在MCU内部死磕。3. 定时器触发DAC采样率到底由谁决定3.1 DAC的三种触发方式与选择依据STM32F407的DAC支持三种触发源软件触发、外部中断触发、定时器触发。软件触发适合低速、非实时场景定时器触发才是DDS的正确打开方式。原因很简单DDS要求严格的等间隔采样采样间隔抖动会直接调制到输出频谱上产生边带。常见的实现思路是用TIM2或TIM6作为触发源配置为更新事件输出连接到DAC的TRGO。DAC设置为定时器触发模式每次触发从DHR寄存器搬数据到DOR完成一次转换。这样采样率完全由定时器频率决定不受主循环影响。// TIM6 配置为 1MHz 更新频率TRGO 输出更新事件 // APB1 定时器时钟 84MHz预分频 0自动重载 83 TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler 0; tim.TIM_Period 83; // 84MHz / (831) 1MHz tim.TIM_ClockDivision 0; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, tim); // 主输出使能TRGO 选择更新事件 TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_Cmd(TIM6, ENABLE); // DAC 触发源选择 TIM6 TRGO DAC_InitTypeDef dac; dac.DAC_Trigger DAC_Trigger_T6_TRGO; dac.DAC_WaveGeneration DAC_WaveGeneration_None; dac.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, dac); DAC_Cmd(DAC_Channel_1, ENABLE);参数说明TIM_Period决定采样率84MHz/(831)1MHz。DAC_Trigger_T6_TRGO把DAC和TIM6绑定。DAC_OutputBuffer_Enable开启输出缓冲驱动能力更强但会引入少量失调。如果后级是高阻输入可以关掉缓冲降低噪声。3.2 采样率与输出频率的奈奎斯特约束DDS输出频率不能超过采样率的一半这是奈奎斯特限制。但实际工程中输出频率超过采样率1/4时重建滤波器的设计就会变得困难。比如采样率1MHz输出250kHz以上时镜像频率落在250kHz~750kHz之间需要陡峭的低通滤波器才能滤除。翻车场景有人设采样率100kHz想输出40kHz正弦波结果示波器上看到的是混叠后的低频信号。原因就是40kHz超过了奈奎斯特频率50kHz的80%镜像分量落在60kHz和主信号一起被DAC输出后级没有滤波器就直接看到了拍频。解决方法是要么提高采样率要么加重建滤波器。STM32F407的DAC最高更新率约1MSPS12位实际可用到500kSPS左右。输出音频范围20Hz~20kHz时采样率设200kHz以上奈奎斯特余量充足后级加一个简单的RC低通就能得到干净波形。3.3 DMA搬运让CPU从查表循环里解放出来如果每次DAC转换都由CPU中断响应并写DHR采样率一高CPU就全耗在中断里了。正确做法是用DMA把查表结果自动搬到DAC的DHR寄存器。DMA请求由TIM6的更新事件触发形成定时器→DMA→DAC的硬件链路CPU只需要在初始化时配置好之后完全不参与。// DMA2 Stream5 Channel7 对应 DAC1 DMA_InitTypeDef dma; dma.DMA_Channel DMA_Channel_7; dma.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; dma.DMA_Memory0BaseAddr (uint32_t)wave_table; dma.DMA_DIR DMA_DIR_MemoryToPeripheral; dma.DMA_BufferSize TABLE_SIZE; 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(DMA2_Stream5, dma); DMA_Cmd(DMA2_Stream5, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE);参数说明DMA_Mode_Circular让DMA在搬完整个表后自动回到开头形成连续输出。DMA_PeripheralDataSize_HalfWord对应12位DAC的右对齐数据。DMA_MemoryInc_Enable让内存地址递增逐个取表项。这样配置后DAC输出就是查表内容的循环播放频率由定时器决定波形由表内容决定。4. 波形表生成从正弦到任意波形的实现细节4.1 正弦表的定点化与对称压缩生成正弦表最直接的方法是sin(2*pi*i/N) * 2047 2048存为uint16_t。但这样一张1024点的表占2KB。如果RAM紧张可以利用正弦的对称性只存1/4周期查表时根据象限做符号和地址变换。常见的实现思路是存0~π/2的256点查表时用相位高2位判断象限低8位查表然后做加减和取反。// 1/4 周期正弦表256 点12 位无符号 const uint16_t sin_quarter[256] { 2048, 2098, 2148, ... }; // 根据 10 位相位索引取正弦值 uint16_t dds_sin(uint16_t phase) { uint16_t idx phase 0x3FF; // 10 位相位 uint16_t quad idx 8; // 高 2 位象限 uint16_t addr idx 0xFF; // 低 8 位地址 uint16_t val; switch (quad) { case 0: val sin_quarter[addr]; break; case 1: val sin_quarter[255 - addr]; break; case 2: val 4095 - sin_quarter[addr]; break; default: val 4095 - sin_quarter[255 - addr]; break; } return val; }逻辑说明象限0直接查表象限1反向查表象限2取补象限3反向取补。这样256个点就能还原1024点正弦内存省了75%。代价是每次查表多几个判断但在168MHz的F407上这点开销可以忽略。4.2 方波、三角波、锯齿波的表生成策略方波不需要查表直接根据相位最高位输出0或4095即可。三角波可以用相位的高位做线性映射。锯齿波更简单相位高位直接就是输出值。但为了统一架构通常还是生成表用同一套DMA搬运逻辑。# 生成 1024 点 12 位波形表输出 C 数组 import math N 1024 for name, func in [ (sin, lambda i: int(2047 * math.sin(2*math.pi*i/N) 2048)), (tri, lambda i: int(4095 * (2*abs(2*i/N - 1) - 1) if i N/2 else 4095 * (2*abs(2*i/N - 1) - 1))), (saw, lambda i: int(4095 * i / N)), ]: print(fconst uint16_t {name}_table[{N}] {{) for i in range(N): print(f{func(i)},, end\n if (i1) % 16 0 else ) print(};)参数说明正弦表用2047幅度加2048偏置落在0~4095。三角波用绝对值函数生成对称三角。锯齿波线性递增。生成后直接复制到C文件用const修饰放到Flash不占RAM。4.3 任意波形的上位机下发与存储如果要输出任意波形需要上位机把波形数据通过串口或USB下发到MCU存入一个缓冲区然后DMA从这个缓冲区搬运。STM32F407有192KB SRAM可以存96K个12位采样点按1MHz采样率算能存96ms的波形。如果要做循环输出缓冲区大小就是波形周期长度。常见的实现思路是上位机把波形归一化到0~4095按二进制下发MCU收到后存入uint16_t wave_buf[]然后重新配置DMA的DMA_Memory0BaseAddr和DMA_BufferSize启动传输。注意DMA传输期间不能改缓冲区内容否则输出会撕裂。要改波形先停DMA改完再启动。5. 避坑与排查DDS调试中最容易翻车的五个点5.1 输出频率总是差一点检查时钟树和定时器分频现象设1kHz输出实测998Hz或1002Hz。原因通常是系统时钟不是预期的168MHz或者APB1定时器时钟倍频没算对。STM32F407的APB1时钟最高42MHz但定时器时钟会倍频到84MHz。如果按42MHz算分频实际输出就是两倍频。解决用RCC_GetClocksFreq()打印实际时钟确认TIM6挂载总线的时钟频率。然后按TIM_Period timer_clk / sample_rate - 1重新计算。不要凭经验猜。5.2 波形顶部削平DAC输出缓冲与负载不匹配现象正弦波顶部和底部被削平看起来像方波。原因可能是DAC输出缓冲驱动能力不足或者后级负载太重。F407的DAC输出缓冲在负载电阻小于5kΩ时就会明显失真。解决要么关掉输出缓冲DAC_OutputBuffer_Disable但这样输出阻抗高需要后级高阻输入要么加一级运放跟随。实测接示波器1MΩ输入时开缓冲没问题接600Ω负载时必须加缓冲。5.3 DMA搬运一半就停缓冲区大小和DMA配置不匹配现象波形输出一小段后变成直流或噪声。原因通常是DMA的BufferSize设成了字节数而不是半字数。DAC数据是16位BufferSize应该等于表长点数而不是表长×2。解决确认DMA_PeripheralDataSize和DMA_MemoryDataSize都是HalfWordBufferSize等于TABLE_SIZE。如果表是uint16_t数组BufferSize sizeof(table)/sizeof(table[0])。5.4 切换频率时出现咔哒声相位没有保持连续现象改变频率控制字时输出波形出现跳变或咔哒声。原因是直接改了ftw但相位累加器被重置了。DDS的优势就是相位连续改频率不应该影响相位。解决只改ftw变量不要动phase累加器。如果用了DMAftw的更新可以在主循环里做DMA搬运的是查表结果相位累加在中断或主循环里算。注意ftw更新时最好原子操作避免中断里读到一半旧一半新。5.5 高频输出时幅度衰减DAC建立时间和采样保持效应现象输出频率升高后波形幅度变小正弦波变成三角波。原因是DAC的建立时间有限采样保持电路的孔径效应导致高频衰减。F407的DAC建立时间约1μs输出100kHz以上时每个采样点还没稳定就被下一个触发更新了。解决降低采样率到DAC能稳定的范围或者加采样保持电路。更根本的办法是换高速DAC。在F407上实用输出频率上限约100kHz再高就需要外部DAC芯片。6. 设计取舍为什么软件DDS在F407上是一个合理选择软件DDS配合DAC的方案本质上是用时间换硬件成本。专用DDS芯片如AD9833能做到几十MHz输出但频率控制字和波形切换不够灵活而且需要额外SPI接口。F407的软件DDS把灵活性拉满任意波形、任意频率、相位连续、频率分辨率到毫赫兹代价是输出频率上限受限于DAC和CPU。这个取舍的关键在于你的应用到底需要多高的频率。音频范围20Hz~20kHz内F407的软件DDS绰绰有余而且可以做到多通道、任意波形、实时调频调幅。如果要做射频或超声软件DDS就不合适了应该用专用芯片或FPGA。另一个取舍是查表精度和内存的平衡。1024点表、32位相位累加器、12位DAC这套组合在F407上跑1MHz采样率时CPU占用率不到5%DMA搬运CPU只算相位。如果换成2048点表内存翻倍杂散改善约6dB但采样率上限会降到800kHz左右。我一般会先跑1024点用示波器FFT看杂散如果满足要求就不加表长。验证方法很简单输出1kHz正弦用示波器FFT看谐波和杂散。如果二次谐波低于-60dBc说明表长和DAC线性度都够用。如果杂散明显先查电源和地平面再查DAC参考电压是否干净。很多时候问题不在DDS算法而在模拟部分。从那以后我每次调DDS都强制走一遍先确认时钟树再确认定时器分频然后看DMA配置最后用FFT验频谱。这四步走完基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑