简介:这是一份面向嵌入式开发者的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、可编程的 4~20 倍参考时钟倍频器(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 MHz(SPI2/3 为 21 MHz),AD9954 的串行接口最高支持 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 MHz(SPI 时钟源为 APB2 的 84 MHz,分频 8),这样既不会太快导致信号质量下降,也完全够 AD9954 的更新速度(每条指令 24 bit,10.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 V,F407 读回来会出问题,这时需要加电平转换或使用 1.8 V 兼容的缓冲器。
我见过一位开发者的板子就是这样:AD9954 用 3.3 V 单电源,F407 的 SPI 输出直接怼到 AD9954,示波器看波形一切正常,但频率偶尔跳变。排查下来发现是杜邦线太长加上 SPI 速率过高,导致 AD9954 的输入时钟毛刺触发误锁存。这是个典型的电平+布局问题,后面在避坑章节我会专门展开。
3. 搭建最小系统:从引脚连接到第一个调通例程
3.1 引脚分配与硬件连接清单
我习惯在写代码前先把硬件连接固定下来,因为 STM32F407 的 SPI1 引脚有重映射,但在布局时最好用默认映射,避免后面调 AF 配置时搞混。以下是我用的引脚分配:
| 功能 | STM32F407 引脚 | 说明 |
|---|---|---|
| SPI1_SCK | PA5 | SPI 时钟,模式 0(CPOL=0, CPHA=0) |
| SPI1_MOSI | PA7 | 数据输出,AD9954 只收不发,MISO 不用 |
| AD9954_RESET | PC0 | 复位信号,高有效,至少保持 20 ns |
| AD9954_UD | PC1 | I/O Update,上升沿锁存串口数据 |
| AD9954_PS0 | PC2 | Profile 选择,接地或悬空(单 Profile) |
| AD9954_PS1 | PC3 | 同上,悬空即可 |
| 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) { // 模式 0,MSB 先行;每字节先发高位,再发低位 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 值:bit0=0(不使能 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 倍频(bit0),SELECT_PLL 倍频系数在 CFR2 的低 5 位 cfr_data[1] = 0x01; // CFR1:PLL enabled cfr_data[2] = 0x00; // CFR1:保留位 // CFR2:0x02 表示 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 FTW,SYSCLK 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 enable) | 1 bit |
| FTW0 | 起始频率字 | 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 = 100,SYSCLK = 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; // 写 CFR,bit0=0 表示写 cfr_data[1] = 0x03; // CFR1 bit0=1(PLL 使能保留),bit1=1(sweep 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 = 1(sweep 自动反向) // 这里需要重写 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 地址 0x03,CFR2 地址 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 模式 0(CPOL=0,CPHA=0),即时钟空闲为低、第一个边沿采样。我有一版代码把 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 以上,但实际使用中常见两个杂散源。一个是电源纹波:如果给数字和模拟供电用的是同一个 LDO,DDS 的杂散会在 50 kHz 到几 MHz 的频段显著抬高。解决方法是模拟电源和数字电源分开,中间加磁珠隔离。另一个是 SPI 时钟泄漏:AD9954 是高精度 DDS,SPI 走线如果和 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 之后,能体会到那种“第一个稳定的正弦波出来”的成就感。希望帮到你。
本文还有配套的精品资源,点击获取