
简介一份基于AD9910的DDS单频调制实现代码包面向嵌入式开发者、射频与信号处理方向的工程师以及需要快速上手ADI DDS芯片的学生。资源聚焦“单频调制”场景涵盖频率控制字计算、幅度调制参数配置、寄存器设置与SPI/I²C通信接口驱动等关键环节。包体共962个文件以C语言源码为主辅以头文件、汇编文件及工程配置文件并包含IAR/Keil等IDE的编译链接产物整体约21.85MB。已有1857人浏览学习适合对照中文数据手册进行实战验证。压缩包内提供完整的工程框架可直接编译调试帮助理解DDS相位累加器原理与查找表转换流程也可作为二次开发基础快速移植到其他AD9910项目。 AD9910 这芯片我在好几个项目里都用过不过之前一直是用它做扫频和 Profile 切换这类复杂玩法。这次有个哥们儿找过来说他的需求特别简单——只要上电以后输出一个固定频率的正弦波频率能写死相位噪声别太离谱就行。一开始我还觉得这有点浪费AD9910 这种 1 GSPS 的 DDS 芯片拿来做单频输出相当于开着超跑去菜市场买菜。但转念一想单频调制恰恰是理解 AD9910 最核心工作机制的切入点把这条路走通了后面做扫频、ASK/FSK 调制、Profile 切换才有基础。单频调制说人话就是让 DDS 芯片稳定输出一个指定频率的连续波。它不需要复杂的数字调制接口不需要 RAM 分段不需要 Profile 切换逻辑甚至连频率斜变都不用碰。真正要做的只有三件事算对频率转换字FTW、通过 SPI 把寄存器写进去、触发一次 IO_UPDATE 让配置生效。这篇文章就把这三件事从原理到寄存器再到代码一条线全给你捋清楚。不管你是第一次玩 DDS还是之前被满屏的寄存器配置吓退过照着这篇文章实操一遍AD9910 单频输出基本一遍过。1. 项目整体设计与思路拆解1.1 为什么选 AD9910 做单频调制现在的信号源方案其实不少。传统的 PLL 频率合成器能做单频但锁定时间、频率切换速度和相位连续性都差点意思。DDS 方案的优势在于频率分辨率极高AD9910 在 1 GHz 系统时钟下频率分辨率可以做到大约 0.23 Hz1 GHz / 2^32这是 PLL 很难达到的精度。另外一个关键点是 DDS 的相位连续特性哪怕你后面要切频波形也不会出现毛刺断裂这在某些测量仪器和高精度激励源场景里是刚需。但选 AD9910 也得客观说两句。这芯片是 BGA 封装手工焊接难度不低而且外围电路要求不算简单——供电要分模拟和数字参考时钟要干净输出端要做滤波和阻抗匹配。如果只是固定输出一个频率其实 AD9912 甚至 AD9834 这类低端 DDS 也够用。之所以还要选 AD9910多半是考虑到后续升级空间输出频率可以做到 400 MHz 以上支持线性扫频、支持多种调制模式一块板子能覆盖后面好几个项目的需求。我用 AD9910 做单频调制时硬件上就按官方数据手册的推荐电路来设计没有搞什么花活。参考时钟用了 20 MHz 的有源晶振通过内部 PLL 倍频到 1 GHz。输出端用了一个低通滤波器滤掉高频镜像分量再接 50 欧传输线。这套方案实测下来输出频谱干净杂散抑制能达到器件手册的典型值水平满足绝大多数应用场景的需求。1.2 单频调制的核心逻辑所谓单频调制本质就是往 AD9910 的寄存器里写一个固定值。AD9910 内部有一个 32 位的频率转换字寄存器也叫 FTW 寄存器。DDS 核心的工作原理就像在绕圈相位累加器每来一个系统时钟就累加一次 FTW 的值累加器溢出产生一个锯齿波相位序列然后通过查找表把相位映射成正弦波的幅度值。FTW 越大相位累加器跑得越快波形周期越短输出频率就越高。这个过程说起来像是绕圈实际就是离散采样和相位映射的数学关系。单频输出不需要动相位偏移寄存器也不需要动幅度寄存器最多为了信号干净把幅度设为最大值或者按自己需求衰减一点。整体流程就三步第一步对 AD9910 复位让芯片进入默认状态第二步配置必要的控制寄存器包括 PLL 倍频、参考时钟分频等第三步把计算好的 FTW 写入对应的频率寄存器触发 IO_UPDATE。这一步做完AD9910 的 OUTPUT 引脚就开始输出设定频率的正弦波了。1.3 硬件连接的工程考量在写代码之前得先把硬件连接搞清楚不然代码写得再好也是白搭。AD9910 是 SPI 从设备需要 MCU 或者 FPGA 通过四线 SPI 接口来控制。常用的是 SDIO 三线制也可以配成 SDIO/SDO 四线制这里用标准四线制最省心。CS 片选低有效SCLK 时钟上升沿采样数据SDIO 做 MOSISDO 做 MISO。IO_UPDATE 引脚在 SPI 写入完成后需要一个上升沿脉冲才能把串行缓冲区的数据真正锁存到寄存器组里。这里有个容易踩的坑很多人 SPI 写完之后忘了给 IO_UPDATE导致寄存器看起来写进去了读回来也对但芯片就是不工作。因为 AD9910 内部有两种寄存器一种是缓冲寄存器就是 SPI 直接写的那部分另一种是活动寄存器是真正参与工作的那部分。只有 IO_UPDATE 上升沿到来时缓冲寄存器的内容才会一次性拷贝到活动寄存器。这点在调试时务必要注意。还有一个容易被忽略的是 MASTER_RESET 引脚。芯片上电后建议先拉高再拉低实现一次可靠复位。这一步能避免很多莫名其妙的问题尤其是芯片从上一次配置状态直接进入工作状态导致的异常行为。2. 核心细节解析与实操要点2.1 频率转换字计算一点都不能马虎单频调制最核心的一件事就是根据目标频率算出 FTW然后写进寄存器。AD9910 的频率计算公式是FTW (F_out × 2^32) / SYSCLK其中 F_out 是目标输出频率SYSCLK 是 DDS 的系统时钟。需要注意的是这里 SYSCLK 不是外部参考时钟频率而是经过内部 PLL 倍频或者直通后的实际工作时钟。也可以说是 DAC 的采样率。比如外部参考时钟是 20 MHzPLL 倍频 50 倍那 SYSCLK 就是 1 GHz。算出来 FTW 一般是小数需要四舍五入取整。比如要输出 10 MHz 频率SYSCLK 是 1 GHz那 FTW (10 × 10^6 × 2^32) / (1 × 10^9) 42949672.96四舍五入取 42949673。这个数转成十六进制就是 0x028F5C29。这里有一个很关键的精度概念32 位的 FTW 意味着频率分辨率为 SYSCLK / 2^321 GHz 下大约是 0.2328 Hz。也就是说你设定 10 MHz实际输出可能是 10 MHz ± 0.12 Hz 左右这种误差在绝大多数情况下可以忽略不计。我在工程里通常直接写一个小函数来做这个转换把浮点运算封装好避免每次手动计算容易出错。公式虽简单但口算加十六进制转换连续写十几个频率配置的时候真的容易搞错。用代码算一劳永逸后面调试还能快速换频率测试。2.2 寄存器规划和配置顺序AD9910 的寄存器空间很大但单频调制只用得上几个CFR10x00、CFR20x01、CFR30x02、以及频率寄存器0x04 到 0x07 对应四个 Profile。如果只是固定频率输出使用 Profile0地址 0x04就够。CFR1 寄存器主要控制 DAC 相关参数包括是否使能 DAC 校准、功耗模式、输出电流设置等。单频调制场景默认值基本能用但要注意把 SDIO 输入输出模式配置位设对。如果用了四线制需要把 CFR1 的 bit15SDIO_INPUT_ONLY置 0。CFR2 寄存器里最重要的是 PLL 相关配置包括 PLL 使能位、倍频系数、参考时钟分频比。如果不用内部 PLL直接外部给 1 GHz 时钟那 CFR2 里的 PLL 使能位就要置 0。CFR3 主要控制一些辅助功能单频调制一般保持默认或者根据需要配置 REFCLK 倍频。寄存器写入顺序上有个小技巧先写 CFR1、CFR2、CFR3 这些控制寄存器最后写频率寄存器。然后把所有配置一次性通过 IO_UPDATE 锁存。虽然 AD9910 允许边写边更新但一次配置完再统一更新出错概率低而且排查问题的时候思路清爽。2.3 一个被大量忽略的细节——Profile 引脚和输出使能AD9910 有四个 Profile 引脚PS0-PS3用于选择当前使用哪一组 Profile 寄存器。单频调制时如果只用了 Profile0硬件上要把 PS0-PS3 全部拉低确保选中 Profile0。如果这些引脚悬空或者电平不确定芯片可能意外选中别的 Profile输出频率就会和你设的完全对不上。这个坑我在早期调试时踩过当时 PS 引脚悬空结果输出频率总是不对排查了半天最后用万用表量引脚电平才发现问题。另外需要注意 OUTPUT 引脚使能。AD9910 的 RF 输出有两路都是电流输出型需要外部接电阻到地或者经过变压器转换为电压信号。上电默认是使能输出的但如果你在调试过程中给 MASTER_RESET 拉高过有些配置会被复位输出状态也可能变化。所以代码里最好加上显式的输出使能配置别依赖默认状态。3. 实操过程与代码实现3.1 硬件环境和引脚定义我这里用 STM32 作为主控通过 SPI 接口控制 AD9910硬件连接如下STM32 AD9910 PB10 ----- SCLK PB11 ----- SDIO PB12 ----- SDO PB13 ----- CS PB14 ----- IO_UPDATE PB15 ----- MASTER_RESET需要注意的是AD9910 的 SPI 是模式 0CPOL0CPHA0也就是 SCLK 空闲为低电平数据在上升沿采样。如果之前写过其他 SPI 器件一定要确认这个模式模式不对写入的数据全是乱的而且串行通信出错时很难排查。另外 SDIO 是双向引脚STM32 这边要配置成开漏输出并外接上拉电阻或者在半双工模式下动态切换方向。外围电路方面参考时钟我用的 20 MHz 有源晶振REFCLK 引脚直接交流耦合接入。输出端每路接了一个 50 欧电阻到地再用 SMA 引出。实际应用中如果输出频率偏高需要在输出端加上对应的低通滤波网络滤除 DDS 固有的镜像频率和杂散分量。3.2 核心代码框架整个单频调制代码分三层底层 SPI 读写函数、AD9910 寄存器操作函数、上层频率配置函数。SPI 读写这块用的是 STM32 标准外设库代码结构清晰方便移植到其他平台// SPI 接口底层读写函数 uint8_t AD9910_SPI_ReadWriteByte(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI2, data); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI2); } // AD9910 单字节寄存器写操作 void AD9910_WriteReg(uint8_t reg_addr, uint8_t *data, uint8_t len) { uint8_t i; AD9910_CS_LOW(); // 指令字节bit70 表示写操作bit60 表示单字节模式低6位是寄存器地址 AD9910_SPI_ReadWriteByte(reg_addr 0x3F); // 写入数据高位在前 for (i 0; i len; i) { AD9910_SPI_ReadWriteByte(data[i]); } AD9910_CS_HIGH(); }写寄存器的时候有几个细节要注意。第一个是 AD9910 的寄存器地址是 6 位的而命令字的高两位用于指示读写方向和字节模式。写操作时最高位 BIT7 必须为 0读操作时 BIT7 必须为 1。第二个是每次 CS 拉低后只能写一个寄存器写完必须拉高 CS 释放总线。第三个是关键AD9910 的 SPI 是高位在前MSB First这和很多普通 SPI 器件一致但有些 DDS 芯片是低位在前如果是从其他 DDS 平台移植过来要特别小心。3.3 单频调制完整配置流程下面这段是单频调制的核心配置代码包含复位、PLL 配置、FTW 写入和 IO_UPDATE 触发。以输出 10 MHz 为例// 定义目标频率和系统时钟 #define SYSCLK 1000000000.0 // 1 GHz #define TARGET_FREQ 10000000.0 // 10 MHz // FTW 计算函数 uint32_t AD9910_ComputeFTW(double freq) { double ftw_d (freq * 4294967296.0) / SYSCLK; return (uint32_t)(ftw_d 0.5); // 四舍五入 } void AD9910_SingleTone_Init(void) { // 1. 复位 AD9910_RESET_HIGH(); delay_ms(10); AD9910_RESET_LOW(); delay_ms(10); // 2. 写 CFR2使能 PLL参考时钟20MHz倍频50倍得到1GHz uint8_t cfr2[4] {0x00, 0x62, 0x50, 0x00}; // 解析0x62 二进制约为 0110 0010 // VCO_SEL110 - 选择2.0-2.5GHz VCO范围后二分频 // PLL_EN1 - 使能PLL // N50 - 倍频系数为 20MHz * 50 1GHz AD9910_WriteReg(0x01, cfr2, 4); // 3. 写 CFR1使能DAC校准四线SPI模式其他默认 uint8_t cfr1[4] {0x00, 0x00, 0x00, 0x00}; // 注意bit15 SDIO_INPUT_ONLY0使用四线制 AD9910_WriteReg(0x00, cfr1, 4); // 4. 写 Profile0 频率寄存器 uint32_t ftw AD9910_ComputeFTW(TARGET_FREQ); uint8_t freq_reg[4]; freq_reg[0] (ftw 24) 0xFF; freq_reg[1] (ftw 16) 0xFF; freq_reg[2] (ftw 8) 0xFF; freq_reg[3] ftw 0xFF; AD9910_WriteReg(0x04, freq_reg, 4); // 5. 触发 IO_UPDATE让所有配置生效 AD9910_IO_UPDATE_PULSE(); }这段代码里最需要注意的是 CFR2 的配置值不同参考时钟频率和不同目标 SYSCLK 对应完全不同的 VCO_SEL 和倍频值不能照抄。AD9910 内部 PLL 的 VCO 分几个频段参考时钟过来先预分频再经过 PLL 倍频最后还要过 VCO 分频器才能得到 SYSCLK。要确保最终 SYSCLK 落在 400 MHz 到 1 GHz 之间。我实际用的 20 MHz 参考、50 倍频、VCO 工作在 2.4 GHz 然后二分频到 1.2 GHz再经过内部校准输出 1 GHz SYSCLK这个过程比较复杂调通以后我一般不去动它。如果要用 10 MHz 参考或者别的倍频建议直接参考数据手册里的配置例子别自己硬推。3.4 IO_UPDATE 脉冲的实现细节IO_UPDATE 是 AD9910 配置生效的开关这个脉冲的时序要求是最小脉冲宽度不小于一个 SYNC_CLK 周期。SYNC_CLK 一般是 SYSCLK 分频出来的常见的是 SYSCLK/4也就是 250 MHz 左右周期 4 ns。所以 IO_UPDATE 脉冲理论上拉高 5 ns 以上就够了但工程实现上我习惯给 100 ns 以上稳妥。void AD9910_IO_UPDATE_PULSE(void) { AD9910_IO_UPDATE_HIGH(); delay_us(1); // 拉高至少1us远大于最小要求 AD9910_IO_UPDATE_LOW(); delay_us(1); }还有一点IO_UPDATE 最好在 CS 拉高之后触发。因为 SPI 写入过程中如果 IO_UPDATE 来了可能写到一半的寄存器数据被部分锁存产生不可预期的状态。常见做法是一轮所有寄存器写完后CS 全部拉高最后统一发 IO_UPDATE 脉冲。4. 常见问题与排查技巧实录4.1 输出频率与预期不符问题出在哪这是单频调试中最常见的问题基本可以按下面顺序排查先确认 SPI 读写没毛病——回读寄存器确认写入的值确实进去了再确认 PLL 配置对不对——算一下 SYSCLK 到底是多少别以为写了 1 GHz 就真是 1 GHz然后看 Profile 引脚电平——是不是 PS0-PS3 恰好选中了一个你没写过的 Profile最后考虑输出端滤波网络是否引入谐振或者衰减。我见过一个情况是输出端变压器绕反了信号幅度小了一半频率倒是对的。如果寄存器回读没问题但频率差了一个固定倍数那基本可以断定是 SYSCLK 和你以为的不一样。比如你配了 PLL 但实际没使能SYSCLK 直接是参考时钟 20 MHz那输出频率就会比预期低 50 倍FTW 对应关系完全错乱。4.2 SPI 通信写不进去的应对方法SPI 写不进去的现象通常是回读全 0 或者全 F。先看硬件连接SDIO/SDO 是否接反CS 是否被外部拉死SCLK 极性相位和 AD9910 要求是否匹配。再用示波器抓 CS、SCLK、SDIO 波形确认时序逻辑没问题。软件层面检查一下 STM32 SPI 是否配置成了正确的模式数据宽度是不是 8 位高位在前还是低位在前NSS 是否用软件管理。很多人第一次调 AD9910 会忽略 SPI 的模式模式不对通信必然失败。另外注意 AD9910 的 SPI 最高速率一般在 40 MHz 左右STM32 跑低速 SPI 完全没问题。如果你用了很高的 SPI 时钟短线干扰可能导致通信不稳定这时把时钟分频调低一点试试往往就好了。4.3 信号质量差杂散太大怎么办单频输出频谱不干净通常是参考时钟质量不够导致相位噪声恶化或者输出端滤波不足导致镜像频率泄漏。DDS 有一个固有问题输出会在 SYSCLK 的整数倍附近产生镜像频谱。比如 SYSCLK 1 GHz 输出 10 MHz那 990 MHz、1010 MHz 这些位置会有杂散分量。这些分量必须靠外部低通滤波器滤除不然频谱仪上看就是一堆毛刺如果你后端电路带宽够大这些杂散会直接耦合进系统。解决思路很清晰第一提升参考时钟质量用低相位噪声的晶振或者用 OCXO 做高精度场景第二在输出端加 π 型低通滤波器截止频率选在目标频率的 1.2 到 1.5 倍第三保证 PC 板布局合理DDS 输出引脚到滤波器之间的走线尽量短走微带线或者共面波导避免阻抗突变产生反射。4.4 从单频扩展到其他模式的思路单频调制跑通了后面扩展扫频、FSK、ASK 就顺理成章。核心思想其实还是那些寄存器扫频要用 CFR1 里的扫频使能位配合扫频上升/下降速率和上下限寄存器FSK 就靠 PS 引脚切换不同 Profile 寄存器实现ASK 则需要用幅度可调功能或者 OSK 引脚。这些功能都是建立在单频调制的寄存器读写基础之上的底层 SPI 通信和 IO_UPDATE 时序完全一样。所以我的建议很明确单频调制真的别嫌低级把它彻底吃透寄存器配置、时序逻辑、硬件滤波都摸清楚了后面玩高级功能能省一大半的时间。我自己是把单频调制的代码整理成了一个模块后面做扫频和 Profile 切换就在这个模块基础上加功能复用率很高。本文还有配套的精品资源点击获取