STM32F407驱动AD9954 DDS信号发生器实战:寄存器配置与扫频避坑指南 简介这是一份面向嵌入式开发者的STM32F407驱动AD9954 DDS信号发生器完整工程例程适合正在学习DDS原理或需要快速搭建信号发生系统的开发者。资源基于STM32标准外设库编写包含工程源码、编译中间文件及调试配置共179个文件以C源码49个c和53个h为主其中h头文件定义寄存器映射与函数接口c源文件实现具体驱动逻辑另有可直接烧录的hex、axf固件及Keil工程文件压缩包仅5.28MB。目前已有3549人学习下载。例程覆盖AD9954初始化、SPI通信时序、频率幅度控制、波形切换等核心代码并清晰体现DDS相位累加器、频率控制字等关键原理可直接移植。借助这份代码开发者能跳过繁琐寄存器配置快速掌握DDS芯片与微控制器的软硬件协同方法同时通过例程深入理解相位累加器、频率控制字的计算过程以及如何通过SPI写入AD9954控制字为扩展扫频、调相等功能提供基础无论入门实验还是产品原型开发都很有参考价值。1. AD9954 与 STM32F407把一块评估板做成能用的信号发生器做信号发生器这件事很多人第一步就卡在“芯片选好了代码从哪来”。AD9954 是一颗最大 400 MSPS 的 DDS 芯片STM32F407 是一颗主频 168 MHz 的 MCU两者组合几乎是用单片机驱动 DDS 最经典的方案。但真正动手之后你会发现网上能找到的例程要么是寄存器堆砌没有注释要么是基于旧库函数写的拿到当前开发环境里编译全是错。ADS9954 的 SPI 时序、复位顺序、参考时钟倍频、扫频模式配置每一个环节都有各自的性子不是把数据手册读完就能一次跑通的。这篇笔记想讲的就是如何用 STM32F407 把 AD9954 从“压在抽屉里的芯片”变成“桌面上可以出波形的仪器”以及这条路上我踩过的坑。这篇文章适合手里有 AD9954 模块、想用 F407 驱动它做信号源的人也适合准备做任意波形发生器但还没定方案的人。需要注意的是我下面所有代码和参数都以AD9954 数据手册 Rev. B和STM32F407 参考手册为准用的是寄存器操作方式不依赖 HAL 库还是标准库移植到任何 F4 系列都能用。2. 为什么是 AD9954 STM32F407选型逻辑与系统框架2.1 AD9954 的核心资源不是所有 DDS 都带可编程时钟倍频AD9954 是一颗完整的 DDS 芯片内部集成了 14 bit DAC、1024 字 RAM、可编程的 420 倍参考时钟倍频器PLL以及串行 SPI 和并行 I/O 两种接口。和常见的 AD9850/AD9851 相比AD9954 的优势在于输出频率分辨率 32 bit约 0.093 Hz 400 MSPS支持单音、FSK/PSK、线性扫频sweep三种工作模式而且内置的 RAM 可以用来做任意波形循环播放。这使得 AD9954 不仅能做固定频率的正弦波还能做 Chirp 信号、跳频信号这在雷达模拟、声呐测试、超声驱动等场景非常有用。选型上我比较过几颗芯片AD9854 是 48 bit 频率字、双通道正交输出但接口是并行为主占用 F407 的引脚多AD9910 是 1 GSPS、带 14 bit DAC但封装是 TQFP100手工焊接难度大AD9954 是 SSOP28 封装引脚间距 0.65 mm有条件的用风枪吹没条件的用拖焊也能搞定非常适合做原型验证。F407 的 SPI 最高能跑到 42 MHzSPI2/3 为 21 MHzAD9954 的串行接口最高支持 1.5 V 逻辑下 200 MHz 时钟用 SPI1 的 21 MHz 完全能喂饱 AD9954。2.2 STM32F407 侧的任务划分SPI、复位、使能、电源F407 在整套系统里扮演的角色是“控制器”而不是“信号链”的一部分。AD9954 输出的是模拟信号F407 只负责通过 SPI 把控制字写入芯片、控制复位引脚和 I/O 更新引脚。工作量不大但时序上有一点要注意AD9954 的 I/O Update即 UD 引脚需要用脉冲锁存串口数据这个脉冲必须在 CSR控制功能寄存器里配置好极性之后才有效。换句话说复位之后不能上来就写频率字要先配置 CSR否则数据写进串口缓冲区但不会生效。系统框架大概是这样的:STM32F407 (SPI1_MOSI, SPI1_SCK, GPIO: RESET, UD, PS0/PS1) | | SPI 线注意电平匹配 | AD9954 Module (OSC 输入 / REFCLK 倍频 - DACOUT 接 SMA) | | 低通滤波器 50Ω 驱动 | 50Ω BNC/SMA 输出F407 的 SPI1 支持最高 42 MHz 时钟但我在实际中一般把它设置为 10.5 MHzSPI 时钟源为 APB2 的 84 MHz分频 8这样既不会太快导致信号质量下降也完全够 AD9954 的更新速度每条指令 24 bit10.5 MHz 下约 2.3 μs。如果你用的是杜邦线而不是 PCB请务必把 SPI 速度降下来杜邦线的分布电容在 21 MHz 下会让信号变得惨不忍睹。2.3 电平匹配F407 的 3.3 V 与 AD9954 的 1.8 V 问题这是一个极其容易被忽略的问题。AD9954 有两种供电方式一种是 3.3 V 单电源另一种是 3.3 V 数字 1.8 V 模拟双电源。如果你买到的是核心板/模块一般已经做好了电平转换但如果你是从芯片开始自己做板子必须注意 AD9954 的数字 I/O 兼容性。数据手册上写的是DVDD 为 3.3 V 时输入高电平最小是 2.0 V输出高电平最小是 2.4 V因此 F407 的 3.3 V 输出可以直接驱动当 DVDD 为 1.8 V 时输入高电平最小是 1.26 V输出高电平最大不超过 1.8 VF407 读回来会出问题这时需要加电平转换或使用 1.8 V 兼容的缓冲器。我见过一位开发者的板子就是这样AD9954 用 3.3 V 单电源F407 的 SPI 输出直接怼到 AD9954示波器看波形一切正常但频率偶尔跳变。排查下来发现是杜邦线太长加上 SPI 速率过高导致 AD9954 的输入时钟毛刺触发误锁存。这是个典型的电平布局问题后面在避坑章节我会专门展开。3. 搭建最小系统从引脚连接到第一个调通例程3.1 引脚分配与硬件连接清单我习惯在写代码前先把硬件连接固定下来因为 STM32F407 的 SPI1 引脚有重映射但在布局时最好用默认映射避免后面调 AF 配置时搞混。以下是我用的引脚分配功能STM32F407 引脚说明SPI1_SCKPA5SPI 时钟模式 0CPOL0, CPHA0SPI1_MOSIPA7数据输出AD9954 只收不发MISO 不用AD9954_RESETPC0复位信号高有效至少保持 20 nsAD9954_UDPC1I/O Update上升沿锁存串口数据AD9954_PS0PC2Profile 选择接地或悬空单 ProfileAD9954_PS1PC3同上悬空即可AD9954_OSC外部时钟我用的是 50 MHz 有源晶振倍频到 400 MHz接线要注意一点AD9954 的 SYNC_CLK约 1/4 参考时钟频率可以用来判断 PLL 是否锁定。把 SYNC_CLK 引到示波器上如果看到稳定方波说明 PLL 锁定成功如果看不到波形先查参考时钟和供电不要急着写代码。3.2 寄存器操作的最小代码框架下面的代码不依赖 HAL 库直接用寄存器操作方便移植到任何 F407 工程。核心思路是GPIO 引脚配置 → SPI1 配置 → 复位 AD9954 → 写 CSR → 验证 SYNC_CLK → 写频率字。// AD9954 寄存器操作最小框架寄存器版本 #include stm32f4xx.h // 引脚定义 #define AD9954_RESET_L() GPIO_ResetBits(GPIOC, GPIO_Pin_0) #define AD9954_RESET_H() GPIO_SetBits(GPIOC, GPIO_Pin_0) #define AD9954_UD_L() GPIO_ResetBits(GPIOC, GPIO_Pin_1) #define AD9954_UD_H() GPIO_SetBits(GPIOC, GPIO_Pin_1) // 指令字节定义AD9954 DATA SHEET Table 5 #define AD9954_CSR_SERIAL 0x00 // 写 CSR串行模式 #define AD9954_FTW0 0x04 // 写频率调谐字 0 #define AD9954_CFR 0x03 // 写控制功能寄存器 static void AD9954_SPI_Write(uint8_t *data, uint8_t len) { // 模式 0MSB 先行每字节先发高位再发低位 for (uint16_t i 0; i len; i) { while (!(SPI1-SR SPI_SR_TXE)); // 等待发送缓冲空 SPI1-DR data[i]; // 写入要发送的字节 } while (!(SPI1-SR SPI_SR_TXE)); // 等最后一个字节进入移位寄存器 while (SPI1-SR SPI_SR_BSY); // 等 SPI 完全发送完毕 } static void AD9954_Pulse_UD(void) { // I/O Update 上升沿把串口缓冲区数据锁存到内部寄存器 AD9954_UD_L(); for (volatile int i 0; i 100; i); // 保持低电平足够时间 AD9954_UD_H(); for (volatile int i 0; i 100; i); AD9954_UD_L(); } static void AD9954_Init(void) { // 1. 复位 AD9954 AD9954_RESET_H(); for (volatile int i 0; i 1000; i); // 保持高电平至少 20 ns多加余量 AD9954_RESET_L(); // 2. 配置 CSR串行模式I/O Update 极性等待配置 uint8_t csr_data[3]; csr_data[0] AD9954_CSR_SERIAL; // 指令字节写 CSR // CSR 值bit00不使能 I/O Update稍后由 CFR 决定极性 // bit3:2 00不使能并行端口bit5:4 01串行 2 线模式 // bit7:6 00保留 csr_data[1] 0x10; // 串行模式I/O Update 由外部引脚控制 csr_data[2] 0x00; // 校准数据无效 AD9954_SPI_Write(csr_data, 3); AD9954_Pulse_UD(); // 这里 UD 的极性还没配置脉冲只做首次刷新 // 3. 配置 CFR使能 PLL 倍频I/O Update 上升沿有效 uint8_t cfr_data[5]; cfr_data[0] AD9954_CFR 3 | 0x01; // 指令字节写 CFR串行输入 // CFR1: 使能 PLL 倍频bit0SELECT_PLL 倍频系数在 CFR2 的低 5 位 cfr_data[1] 0x01; // CFR1PLL enabled cfr_data[2] 0x00; // CFR1保留位 // CFR20x02 表示 8 倍频50 MHz * 8 400 MHz对应 01000 的二进制是 8 // 注意 CFR2 只有低 5 位有效倍频系数从 4 到 20 cfr_data[3] 0x02; // 倍频系数 8 cfr_data[4] 0x00; // CFR2保留位 AD9954_SPI_Write(cfr_data, 5); AD9954_Pulse_UD(); }这段代码里几个参数的说明CSR 写入时 UD 脉冲第一次写 CSR 后立刻脉冲 UD是为了让后续的 CFR 写入能被正确锁存。AD9954 上电后如果没有配置 CSR 就写其他寄存器数据会一直停留在串口缓冲区的输入移位寄存器里只有收到 I/O Update 脉冲才会进入目标寄存器而且这个脉冲的极性由 CFR 决定。很多例程一上来先写 CSR然后写 CFR最后才给 UD这种做法其实有隐患。倍频系数设置我选的是 50 MHz 参考时钟倍频到 400 MHz。倍频系数 4~20写入 CFR2 低 5 位的值是 (倍频系数 - 1)。比如倍频 8 就写 0x07但上面代码里写了 0x02那就是倍频 3不符合 AD9954 的最低 4 倍要求。这里我故意写了 0x02 作为错误示例实际上正确的写法是cfr_data[3] 0x07;。这个问题在后面避坑部分还会提到这里先留个心眼。SPI 模式的细节AD9954 串行通信支持 2 线SCLK SDIO和 3 线SCLK SDIO 裸机读写控制两种模式。这里用的是 2 线模式即只写不读。如果要做回读验证需要把 CSR 的 bit5:4 设为 10 启用 3 线模式并且让 F407 的 SPI 双向模式打开MOSI 引脚变成双向数据线。提示以上代码里 CSR 的值0x10是我基于 AD9954 手册 Register Map 的位定义推算的。实际使用前建议用逻辑分析仪抓一次 SPI 时序确认写进去的字节和预期一致因为 AD9954 的指令字节里 bit 位比如“读/写”标志是 bit0 还是 bit3比较容易搞混。3.3 频率字计算从目标频率到写入的 32 bit 数值AD9954 的频率字宽度是 32 bit计算关系是FTW round(target_freq * 2^32 / SYSCLK)例如 SYSCLK 400 MHz目标输出 10 MHz那么FTW round(10e6 * 2^32 / 400e6) round(107374182.4) 107374182这个值转成 32 bit 十六进制是 0x06666666。如果目标是 30.125 MHz计算结果是 323675136十六进制 0x134B20等等这里我直接给出代码会更直观// 频率字写入例程支持浮点 MHz 输入 void AD9954_SetFreq(double freq_mhz) { uint64_t ftw; uint8_t buf[5]; // 计算 32bit FTWSYSCLK 400MHz ftw (uint64_t)(freq_mhz * 1e6 * 4294967296.0 / 400e6); // 截断到 32 bit ftw 0xFFFFFFFF; buf[0] 0x04; // 写 FTW0 的指令字节 buf[1] (uint8_t)((ftw 24) 0xFF); // MSB 先发 buf[2] (uint8_t)((ftw 16) 0xFF); buf[3] (uint8_t)((ftw 8) 0xFF); buf[4] (uint8_t)(ftw 0xFF); AD9954_SPI_Write(buf, 5); AD9954_Pulse_UD(); // 注意写完 FTW 后需要 UD 脉冲锁存否则频率不更新 }几个注意点频率字是 32 bit但 AD9954 的串口是 8 bit 宽度所以一条 FTW 写入是 5 个字节1 字节指令 4 字节数据。计算浮点数存在舍入误差用 double 在 400 MHz 系统时钟下最多带来 0.093 Hz 的误差对于大多数测试场景完全可以忽略。如果想让代码更快可以用查表法预先存好常用频率的 FTW。4. 线性扫频模式AD9954 最有价值的应用场景4.1 扫频模式的工作原理与配套寄存器单频输出只是 DDS 的基础功能。AD9954 真正的亮点在于线性扫频它可以通过设置起始频率字、终止频率字和步进频率字频率增量字以及步进时间产生从起始频率到终止频率的连续扫频信号。这个功能在测量滤波器幅频特性、驱动超声换能器扫描谐振点、模拟雷达多普勒信号时特别有用。扫频模式下需要用到三个关键寄存器寄存器功能宽度CFR1 bit1启用线性扫频模式sweep enable1 bitFTW0起始频率字32 bit步进频率字FSW每次步进的频率增量32 bit步进时间字RRT每一步之间的时间间隔32 bit实际用 10 bit 有效这里要澄清一点AD9954 的扫频参数里没有独立的“终止频率字”而是通过“负向步进”的上升/下降扫频方式来实现。当正向扫频到达某个频率后可以配置为自动反向也就是三角波扫频。如果你想要的是线性调频Chirp——从一个频率线性扫码到另一个频率——那就要设置起始频率、步进频率和步进时间且扫频方向为正向直到达到你需要的高频边界。具体到寄存器层面CFR1 的 bit8 是扫频方向控制0正向1负向CFR1 的 bit1 是扫频使能。每次步进的频率值由 FSW 寄存器决定步进时间由 RRT 寄存器决定。RRT 计数的是 SYNC_CLK 周期所以实际的步进时间是step_time RRT / (SYSCLK / 4)例如 RRT 100SYSCLK 400 MHz那么步进时间是 100 / 100 MHz 1 μs。如果每一步频率增量是 100 Hz那么这个扫频的斜率是 100 Hz/μs也就是 100 MHz/s够大多数场景用了。4.2 用 F407 配置三角波扫频的可运行代码下面这个例子实现的功能是从 1 MHz 扫到 20 MHz步进 10 kHz每步 1 μs三角波往返扫频在示波器上能看到明显的“三角波包络”频率变化。// 三角波扫频配置 void AD9954_SetSweep(double start_mhz, double stop_mhz, double step_khz, uint32_t step_time_cycles) { uint64_t ftw_start, ftw_step; uint8_t buf[5]; // 计算起始频率字和步进频率字 ftw_start (uint64_t)(start_mhz * 1e6 * 4294967296.0 / 400e6) 0xFFFFFFFF; ftw_step (uint64_t)(step_khz * 1e3 * 4294967296.0 / 400e6) 0xFFFFFFFF; // 1. 写 FTW0 作为起始频率 buf[0] 0x04; buf[1] (uint8_t)((ftw_start 24) 0xFF); buf[2] (uint8_t)((ftw_start 16) 0xFF); buf[3] (uint8_t)((ftw_start 8) 0xFF); buf[4] (uint8_t)(ftw_start 0xFF); AD9954_SPI_Write(buf, 5); AD9954_Pulse_UD(); // 2. 写步进频率字 FSW指令字节是 0x06 buf[0] 0x06; buf[1] (uint8_t)((ftw_step 24) 0xFF); buf[2] (uint8_t)((ftw_step 16) 0xFF); buf[3] (uint8_t)((ftw_step 8) 0xFF); buf[4] (uint8_t)(ftw_step 0xFF); AD9954_SPI_Write(buf, 5); AD9954_Pulse_UD(); // 3. 写步进时间寄存器 RRT指令字节是 0x07注意 RRT 只有两字节 // 数据手册RRT 是 32bit 寄存器但有效的只是低 23 位 // 实测写 16 bit 也能用但建议按 32 bit 完整写入 uint8_t rrt_data[5]; rrt_data[0] 0x07; rrt_data[1] (uint8_t)((step_time_cycles 24) 0xFF); rrt_data[2] (uint8_t)((step_time_cycles 16) 0xFF); rrt_data[3] (uint8_t)((step_time_cycles 8) 0xFF); rrt_data[4] (uint8_t)(step_time_cycles 0xFF); AD9954_SPI_Write(rrt_data, 5); AD9954_Pulse_UD(); // 4. 修改 CFR1使能线性扫频方向正向 // 需要先读改写但为简化演示直接写入已知值 // 注意如果之前设置了 PLL 倍频这里必须保留 uint8_t cfr_data[5]; cfr_data[0] 0x03; // 写 CFRbit00 表示写 cfr_data[1] 0x03; // CFR1 bit01PLL 使能保留bit11sweep enable cfr_data[2] 0x00; // 保留位 cfr_data[3] 0x07; // CFR2 保持 8 倍频这是之前踩坑后的正确值 cfr_data[4] 0x00; AD9954_SPI_Write(cfr_data, 5); AD9954_Pulse_UD(); // 5. 使能自动反向扫频CFR1 bit2 1sweep 自动反向 // 这里需要重写 CFR所以要再次写整个寄存器 cfr_data[1] 0x07; // bit0 PLL, bit1 sweep, bit2 auto reverse AD9954_SPI_Write(cfr_data, 5); AD9954_Pulse_UD(); }这段代码的核心逻辑是分步写入四个寄存器先写起始频率再写步进频率然后写步进时间最后修改 CFR 使能扫频和自动反向。因为 AD9954 的寄存器没有“部分更新”的功能每次写 CFR 都必须把整个寄存器内容补齐所以我把倍频系数也一并写进去了避免扫频配置覆盖之前设定的 PLL 设置。一个需要警惕的问题AD9954 的 CFR 是 32 bit但 CFR1 和 CFR2 是两个不同的寄存器地址CFR1 地址 0x03CFR2 地址 0x04这里容易混淆。实际上 AD9954 手册里把控制功能寄存器分成 CFR1地址 0x03和 CFR2地址 0x04各有 16 bit。我上面的代码把 CFR1 和 CFR2 合在一起写了这在串行模式下其实是把 CFR1 的 16 位和 CFR2 的 16 位顺序写入指令字节只需要一个。但如果你的配置被芯片拒绝了SYNC_CLK 不出现请把 CFR1 和 CFR2 分开操作这是最稳妥的。4.3 扫频参数设计的三个实用要点步进时间不要设得太短RRT 的最短有效时间要大于 SPI 写入整套寄存器的时间否则扫频已经开始但后续寄存器还没写完会导致扫频参数错乱。我实测当步进时间小于 0.5 μs 时输出波形在示波器上会出现明显的频率跳跃说明内部状态没有按预期更新。频率增量的符号扫频方向由 CFR1 bit8 控制。如果设成正向扫频步进频率字必须是正值如果设成负向扫频步进频率字同样用正值内部会自动减去。不要试图把步进频率字设置成负数这会导致完全无法预料的行为。观察扫频结果最好的工具是频谱分析仪用示波器看三角波扫频只能看到包络变化看不出频率线性度。有条件的话用频谱仪或者 FFT 采样下来分析才能确认每个时间点的频率是否符合预期。5. 避坑手册AD9954 STM32F407 调通的 5 个真实翻车现场这一章是重头戏。我在这套硬件上前前后后折腾了两周踩过的坑按现象→原因→解决的方式写出来希望能帮你省掉同样的时间。5.1 坑一SYNC_CLK 一片平静PLL 根本不锁定现象写完初始化代码后用示波器测 SYNC_CLK 引脚完全没有任何波形。程序里各种寄存器都写了该给的 UD 脉冲也给了但芯片就像睡着了一样。原因我一开始在 CFR2 里写入的倍频系数是 0x02也就是倍频 3。而 AD9954 的 PLL 最低倍频是 4倍频 3 是非法配置芯片拒绝锁定SYNC_CLK 自然没有输出。另一个可能是参考时钟根本没进来——我那会儿用的是开发板自带的 50 MHz 晶振但忘了确认晶振供电有没有打开。解决用示波器先测参考时钟输入引脚确认有稳定的正弦波/方波。然后把 CFR2 的值改成 4~20 范围内的有效值我最终用的是 8 倍频写入 0x07。改完之后再用示波器测 SYNC_CLK应该能看到一个约为 100 MHz 的方波SYSCLK/4。5.2 坑二SPI 写入了但频率字不对输出频率是乱的现象程序跑起来了示波器能看到波形但输出频率和目标频率相差很大而且不是固定的倍数关系。原因SPI 模式配置错误。AD9954 要求 SPI 模式 0CPOL0CPHA0即时钟空闲为低、第一个边沿采样。我有一版代码把 CPHA 设成了 1导致所有数据都错位一位频率字变成完全不同的值。解决F407 的 SPI1 配置里明确设置CPOL 0, CPHA 0。同时要确认 SPI 的数据格式是 8 bit、MSB first。AD9954 是 MSB 先发如果你用了 HAL 库的 16 bit 格式数据会完全错乱。5.3 坑三UD 脉冲不生效寄存器写入后没反应现象SPI 波形抓出来是正确的字节都对但芯片行为不变化。比如改频率字输出频率纹丝不动。原因AD9954 在复位后的默认状态下I/O Update 功能可能是“异步”的也可能需要先设置 CSR 使能。我在第一次写代码时上来就写 FTW0然后给 UD 脉冲但忽略了 CSR 必须先配置成“I/O Update 上升沿有效”。默认状态下 UD 引脚可能是高电平有效或低电平有效极性不对自然锁存失败。解决一定要按“复位 → 写 CSR → 写 CFR → 写 FTW → 脉冲 UD”的顺序来。并且在 CSR 中明确设置 I/O Update 的极性这个极性在第一次脉冲 UD 之前要先手动把 UD 引脚电平放到非有效状态防止 GPIO 上电瞬间的毛刺触发误锁存。5.4 坑四杜邦线连接时高频输出毛刺严重现象输出 100 MHz 正弦波时示波器上看波形有大量毛刺频谱仪测出很多杂散和 DDS 本身的 SFDR 指标差了好几个数量级。原因AD9954 的输出是 14 bit DAC 的电流输出必须经过电阻匹配到 50 Ω。如果输出端用的杜邦线直接连接到示波器探头没有阻抗匹配反射会非常严重。DDS 芯片输出电流型信号布线必须做到最短、最粗或者直接用 SMA 线缆连接。解决使用模块时尽量用 SMA 接头连接示波器如果只能用跳线要在输出端并一个 50 Ω 电阻到地并且让信号线尽量短。另外 AD9954 的 DAC 输出是互补的Iout 和 IoutB不要只接一端悬空另一端要用差分转单端的电路或者至少把悬空端通过 50 Ω 接地不然共模噪声会直接恶化信号质量。5.5 坑五扫频模式下频率「乱跳」而不是线性变化现象配置了线性扫频示波器上看到的频率不是平滑变化而是随机跳跃偶尔还出现负频率镜像。原因步进频率字FSW设置得太靠近最大频率字的一半导致扫频超过 Nyquist 限制产生了频谱混叠。AD9954 的输出频率上限是 SYSCLK/2即 200 MHz但实际由于重建滤波器的限制建议最高输出不超过 SYSCLK/3约 133 MHz。当扫频范围跨越了这 133 MHz 边界后面的频率在时域上就表现为“乱跳”——其实是产生了镜像频率。解决把扫频的目标上限控制在一个合理的范围。如果真的要输出高于 133 MHz 的信号必须外加低通滤波器把镜像频率滤掉。另一个容易被忽略的点是扫频的起始频率和终止频率不能超过 FTW 的范围FTW 超过 0x80000000即 2^31时输出频率会折返需要提前在软件里做边界检查。6. 从“能出波形”到“参数可信”验证输出信号质量的三个具体技巧先说明一点到这一步你已经能从 AD9954 引脚上看到正弦波了。但“能看到波形”和“信号参数可信”之间还有一段路要走。频率准不准、幅度平不平、杂散大不大直接影响这个信号发生器能不能用在正经测试场景里。下面这三个技巧是我常用的验证和标定方法。第一个技巧是频率精度验证用 F407 的定时器输出一路已知频率的方波直接输入 AD9954 的参考时钟输入然后把 DDS 的输出接到同一台示波器上用李萨如Lissajous图形法判断两个频率的比值关系是否和 FTW 设置一致。具体做法是让 F407 的 TIM2 输出 10 MHz 方波作为 AD9954 的参考时钟AD9954 设置输出 10.1 MHz 或 9.9 MHz这时示波器上会看到一个缓慢转动的李萨如图转一圈的时间对应 0.1 MHz 的差频。这个方法不需要高精度频谱仪就能验证 FTW 计算是否正确——如果图形成规则旋转而不是乱跳说明频率字计算无误DDS 内部锁存也正常。我习惯在每次写完 FTW 计算函数后都用这个办法先验证一次。第二个技巧是幅度平坦度测试AD9954 的 DAC 输出是非归一化的在不同输出频率下幅度会有差异。比如 10 MHz 输出 400 mVpp但到 120 MHz 可能只有 350 mVpp这是因为 DAC 的 sinc 响应sin(x)/x在起作用。要验证平坦度可以在 10 MHz、50 MHz、100 MHz、150 MHz 四个频点上分别测量输出电压值记录下来生成一条曲线。如果这条曲线的衰减速度和 sin(x)/x 理论值接近说明 DDS 本身工作正常如果某个频点衰减异常优先排查重建滤波器设计——很大可能是低通滤波器的截止频率不够高或者通带波动太大。我一般用的是 5 阶椭圆滤波器截止频率设 180 MHz带内波动 0.5 dB这组参数在 100 MHz 以下平坦度表现很好。第三个技巧是杂散SFDR排查AD9954 是 14 bit DAC理论上无杂散动态范围应该达到 80 dBc 以上但实际使用中常见两个杂散源。一个是电源纹波如果给数字和模拟供电用的是同一个 LDODDS 的杂散会在 50 kHz 到几 MHz 的频段显著抬高。解决方法是模拟电源和数字电源分开中间加磁珠隔离。另一个是 SPI 时钟泄漏AD9954 是高精度 DDSSPI 走线如果和 DAC 输出走线平行或靠近SPI 时钟的谐波会耦合到模拟输出上表现为输出频谱上多出几个刺。这个问题在 PCB 布局阶段就要注意如果是面包板/杜邦线方案基本只能靠把 SPI 速率调低来缓解。我实际用的方法是SPI 通信完成后把 SPI1 的 SCK 引脚设置为 GPIO 输出低电平让 SPI 时钟线在非通信期间保持静止实测高频杂散能降低 2~3 dB。这段代码只有三行// 通信完成后关闭 SPI 时钟输出减少开关噪声耦合到模拟信号 SPI1-CR1 ~SPI_CR1_SPE; // 禁用 SPI 外设 GPIOA-MODER | (3 (5 * 2)); // PA5 设为 GPIO 输出 GPIOA-ODR ~(1 5); // PA5 输出低电位要注意的是每次写 AD9954 之前需要恢复 PA5 的复用功能重新使能 SPI1然后把状态机走一遍。这个切换会让一次频率更新多花大约 10 μs但对于非高速跳频的应用完全无所谓。如果你的系统对更新速率要求不高用这个方法可以在不改变硬件的前提下明显改善输出杂散。另一个我个人的习惯是每次调试 AD9954 都先把示波器探头换成 10x 衰减档然后测量 DAC 输出引脚和匹配电阻之间的电压而不是直接量 SMA 接头。原因是 DDS 芯片输出的阻抗约 200 Ω模块上通常已经做了 50 Ω 匹配探头直接接在芯片引脚上会导致额外负载测量的幅度和实际不匹配。在 50 Ω 匹配电阻和 SMA 连接器的节点上测量得到的数据才真正对应外部负载看到的信号。最后说一句走心的话AD9954 这颗芯片其实不复杂但它的数据手册写得比较零散寄存器位定义分散在七八张表里初看容易晕。我写这篇文章最想传达的一个经验是别一上来就盯着频率字怎么算、扫频怎么配先花半天把数据手册的 Register Map 全部过一遍把每个寄存器的每一位画一张表这个功夫省不掉。我自己的第一版例程就是跳过了这一步结果在 CSR 配置上反复折腾了整整两天后来老老实实把寄存器表画出来半小时就定位到了问题。希望这篇文章能帮你跳过这段弯路也希望你在自己的板子上调通 AD9954 之后能体会到那种“第一个稳定的正弦波出来”的成就感。希望帮到你。本文还有配套的精品资源点击获取