简介:基于STM32F407的AD9954直接数字合成(DDS)信号发生器完整工程例程,面向嵌入式开发者、电子工程爱好者与高校师生,适用于测试测量、通信组网、科研实验等多种场景。AD9954是高精度直接数字合成芯片,配合STM32F407的Cortex-M4内核可生成高分辨率、宽频率范围模拟波形;资源包共179个文件、约5.28MB,含C语言源码、头文件、Keil工程配置文件、HEX固件及编译中间文件,构成一套可直接编译烧录运行的完整开发工程。完整代码涵盖SPI通信配置、寄存器初始化、频率/幅度控制、波形选择等核心模块,注释清晰,已有3549人学习下载。开发者可从中掌握直接数字合成工作原理、寄存器配置方法、串行外设接口时序设计以及固件调试技巧,压缩包内还包含该芯片开发板移植到F407微控制器的完整设计思路与联调注意事项,是学习与开发信号发生器的重要参考资料。
1. 拿 STM32F407 和 AD9954 拼信号发生器:为什么这组合能打又不省心
做信号发生器选型时,不少人被 DDS 芯片的 datasheet 劝退——寄存器几百个、时序图密密麻麻,最后只能去买成品模块,一旦要调频率分辨率、扫频速度或相位连续切换,发现黑匣子根本改不动。用 STM32F407 直接驱动 AD9954 的好处,是把频率字、相位字、幅度控制全部攥在自己手里,USB 或串口一接,上位机下发指令就能实时出波形。这套方案的适用人群很明确:要做实验室仪器、中频激励源、超声波驱动或任意波形前端的硬件工程师,以及想把 DDS 玩明白、不看别人封装库的嵌入式开发者。
AD9954 是四通道直接数字频率合成器,内部最高系统时钟 400 MSPS,14 位 DAC 输出,频率分辨率靠 32 位频率转换字实现,100 MHz 以内输出频率能做到毫赫兹级步进。STM32F407 主频 168 MHz,自带 SPI 和 DMA,跑控制逻辑绰绰有余。常见做法是 SPI 配置 AD9954 内部寄存器,再用 PS0/PS1 引脚或 I/O 更新引脚触发输出切换。这个标题里的例程,本质就是一套「寄存器配置 + 频率/相位/幅度控制 + 扫频逻辑」的可复用工程——拿到后改引脚、改频率字就能用于自己的项目。
但真正动手后会遇到一连串问题:AD9954 的 SPI 时序和 STM32F407 的 SPI 外设不匹配、IO Update 信号时序不够导致寄存器写入不生效、扫频时相位不连续造成毛刺、输出幅度和预期差几个 dB。这些不是看 datasheet 就能秒懂的,需要在逻辑分析仪和频谱仪上一点点调。下面按「寄存器模型 → 最小工程 → 关键参数 → 踩坑记录 → 验证进阶」的顺序把整套做法讲透。
2. AD9954 的寄存器模型和控制时序:写对之前先看懂两件事
2.1 SPI 写时序:一次有效写操作由哪几段组成
AD9954 的串行通信口支持 SPI 兼容模式,但和普通 SPI 从机不太一样。每次写操作分三部分:先发 8 位指令字节(包含读/写位、字节数减 1、寄存器地址的高 6 位),再发寄存器地址的低 8 位,最后发数据字节。寄存器按字访问,字长可以是 8、16、24、32、40、48 或 64 位,具体由指令字节里的 N 位决定。例如 CFR1 是 32 位寄存器,写 CFR1 时指令字节要写成 0x03(写、4 字节减 1 得 3、地址高 6 位),然后地址低 8 位 0x02,再连续发 4 个字节。
用 STM32F407 的 SPI2 或 SPI3 做主机时,关键是把 SPI 配置成 MSB First、CPOL=0、CPHA=0(模式 0)。AD9954 在 SYNC_CLK 上升沿采样数据,主机必须在 SCK 上升沿把数据稳定在 SDIO 上。有的例程为了省事把 SPI 速率拉到 10 MHz 以上,此时线材过长或杜邦线接触不良,读写就会间歇性失败。我一般控制在 5 MHz 左右,配置寄存器这种低频操作完全够用。
逻辑上,一次写 AD9954 寄存器的函数长这样:
void AD9954_WriteRegister(uint8_t addr, uint8_t *data, uint8_t byteCount) { uint8_t instruction = 0x00; // bit7=0 写操作,bit6:3=(byteCount-1),bit2:0=寄存器地址高3位(实际用低6位地址时高3位为0) instruction = ((byteCount - 1) << 3) | (addr >> 8); AD9954_CS_LOW(); AD9954_SPI_SendByte(instruction); AD9954_SPI_SendByte(addr & 0xFF); for (uint8_t i = 0; i < byteCount; i++) { AD9954_SPI_SendByte(data[i]); } AD9954_CS_HIGH(); }注意这里指令字节的高位不是直接拼地址高 8 位——数据手册里指令字节的 bit2:0 是地址的 bit10:8,而 AD9954 寄存器地址总共就 11 位。由于常用寄存器地址都小于 0x7F,addr >> 8 通常为 0,这段代码对 CFR1、CFR2、CTW0 这些寄存器完全适用。
写完寄存器后必须拉高 IO Update 引脚产生上升沿,AD9954 才会把缓存寄存器内容送入有效寄存器。IO Update 的最小脉冲宽度在 100 MHz 系统时钟下约 5 个 SYNC_CLK 周期,实际操作时给 1 μs 以上的延时,最稳妥的是拉高后延时 200 ns,再拉低,再延时 200 ns。
void AD9954_IOUpdate(void) { AD9954_IO_UPDATE_HIGH(); delay_us(1); AD9954_IO_UPDATE_LOW(); delay_us(1); }这个 IO Update 是新手最容易忽略的。寄存器写进去了但输出不变,十有八九是没做 IO Update,或者脉冲宽度不够。另外注意:AD9954 的 CS 在整个写操作期间必须保持低电平,SCK 空闲时不能有毛刺,否则指令字节错位,后面所有数据全乱。
2.2 频率/相位/幅度控制字怎么算:从目标频率到 32 位十六进制
AD9954 的频率转换字 FTW 计算公式是 FTW = f_out × 2^32 / SYSCLK。SYSCLK 由外部参考时钟经 PLL 倍频得到,比如参考 10 MHz、PLL 倍频 40 倍,则 SYSCLK = 400 MHz。要输出 25 MHz,FTW = 25e6 × 4294967296 / 400e6 = 268435456,即 0x10000000。
实际例程里算频率字有两种写法:浮点直接除,或者用整数运算避免浮点库。浮点在 STM32F407 上有 FPU,完全没性能压力,直接写就行:
uint32_t AD9954_CalcFTW(double freq_Hz, double sysclk_Hz) { double ftw = freq_Hz * 4294967296.0 / sysclk_Hz; return (uint32_t)(ftw + 0.5); }相位字 POW 是 14 位,满刻度 16384 对应 360°,所以相位偏移 = pow / 16384 × 360°。幅度字 AOM 是 10 位,但注意 AD9954 的幅度控制不是直接线性 DAC 码——它通过 AOM 对 DAC 输出幅度做衰减,0 对应满幅度,1023 对应最大衰减。实际输出幅度还受 DAC 满量程电流和外部滤波网络影响。
把这些算好的字按寄存器映射写入 CTW0/CTW1、CPW0/CPW1、ACR 等寄存器,再配合 PS0/PS1 引脚选择 Profile 0 或 Profile 1 输出。这样频率和相位切换不需要重新写 SPI,只要翻转引脚电平即可,适合需要高速跳频的场景。不加 PLL 倍频、直接用参考时钟做 SYSCLK 也可以,但输出频率上限只有几十 MHz,相位噪声特性取决于参考源。例程里通常会做一个 SYNC_CLK 校准步骤,把实际 SYSCLK 修正后再算 FTW,否则 0.1 ppm 的参考偏差会直接叠加到输出频率上。
3. STM32F407 的 SPI 配置与引脚分配:把最小驱动跑通
3.1 SPI2 主机模式初始化:时钟、极性、速率怎么设
STM32F407 有 SPI1、SPI2、SPI3,理论上都能驱动 AD9954,但要注意引脚冲突和 DMA 可用性。我常用 SPI2,PA4 做 CS、PA5 做 SCK、PA6 做 MISO、PA7 做 MOSI,IO Update 用 PB0,PS0/PS1 用 PB1/PB2。CS 用普通 GPIO 手动控制,不用 SPI 外设的 NSS 硬件管理,因为 AD9954 的 CS 时序要求每次写操作期间保持低电平,硬件 NSS 在配置不当的时候会有多余的片选脉冲。
初始化配置如下:
void AD9954_SPI_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // PA4 CS 输出,PA5 SCK 复用,PA7 MOSI 复用,PA6 MISO 复用 GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_OUT; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_OType = GPIO_OType_PP; gpio.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &gpio); GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI2); // PB0 IO_UPDATE,PB1 PS0,PB2 PS1 GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_OUT; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); SPI_StructInit(&spi); spi.SPI_Mode = SPI_Mode_Master; spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi.SPI_DataSize = SPI_DataSize_8b; spi.SPI_NSS = SPI_NSS_Soft; spi.SPI_CPOL = SPI_CPOL_Low; spi.SPI_CPHA = SPI_CPHA_1Edge; spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; spi.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI2, &spi); SPI_Cmd(SPI2, ENABLE); }SPI2 挂在 APB1 上,时钟 42 MHz,预分频 32 后 SPI 速率约 1.3125 MHz,这个速率对 AD9954 的 200 Mbps 上限来说太低了,但对配置寄存器足够。如果后续要频繁更新频率字做扫频,可以把预分频降到 8,但前提是 PCB 走线短、信号完整。用杜邦线飞线时,高速 SPI 会因串扰产生误码,这是排错时最隐蔽的问题。
代码里把 GPIOB 的 PB0/PB1/PB2 都初始化成输出,时序上要先拉低 CS 再发数据,最后拉高 CS。注意 SPI_NSS_Soft 模式下如果不手动管理 NSS,SSI 位必须设为 1,否则 SPI 会进入从机模式。调用 SPI_StructInit 后 SSI 默认是 0,必须在 SPI_Init 之前手动置 1,这是很多人初始化后 SPI 不工作的原因。
3.2 初始化序列:从复位到配置 CFR1/CFR2 的正确顺序
AD9954 上电后要先复位,把内部寄存器清成默认值。复位有两种方式:硬件复位引脚拉低再拉高,或者通过 CFR2 的 SYNC_CLK 使能位和软件复位位。例程里常用 GPIO 控制硬件复位,因为简单可靠:
void AD9954_Reset(void) { AD9954_RESET_HIGH(); delay_us(10); AD9954_RESET_LOW(); delay_us(10); }复位后配置顺序一般是这样:先写 CFR2 设置 PLL 倍频数和系统时钟参数,再写 CFR1 设置工作模式,最后写各 Profile 寄存器和幅度控制。CFR2 的地址是 0x01,32 位字,bit7:0 是 PLL 倍频数 N,bit8 是 PLL 使能位,bit9 是 VCO 选择位。例如 10 MHz 参考、40 倍频,N=40,PLL 使能,VCO 选择 3 倍频段。这个配置直接决定 SYSCLK,必须和外部晶振匹配。
CFR1 的地址是 0x02,32 位字。常用位段包括:bit0 使能 DAC 输出,bit1 选择 DAC 输出极性,bit2 使能电荷泵,bit8 使能 SDIO 输入,bit16 使能 IO Update 自动清除。一个常见配置是 0x00000001,只开启 DAC 输出,其余默认。如果要用 PS0/PS1 切换 Profile 寄存器,CFR1 的 bit11:10 要配置成对应模式;如果要用 OSK 幅度键控,bit9 也要一起处理。这些不是每个例程默认开,需要根据项目需求单独改。
uint8_t cfr2[4] = {0x00, 0x00, 0x00, 0x00}; uint8_t cfr1[4] = {0x00, 0x00, 0x00, 0x00}; // CFR2: PLL 使能,N=40,VCO=3 倍频段 cfr2[0] = 0x01; // 高位 cfr2[1] = 0x00; cfr2[2] = 0x00; cfr2[3] = 0x29; // 0b00101001: VCO 选择 bit9:8=01, PLL 使能 bit8=1, N=40 AD9954_WriteRegister(0x01, cfr2, 4); delay_ms(10); // PLL 锁定时间 // CFR1: 使能 DAC 输出 cfr1[0] = 0x00; cfr1[1] = 0x00; cfr1[2] = 0x00; cfr1[3] = 0x01; AD9954_WriteRegister(0x02, cfr1, 4); AD9954_IOUpdate();SPI 发送时高位在前,cfr2[0] 是最高字节。CFR2 的 bit8 是 PLL 使能,0x29 的低字节是 0b00101001,其中 bit8 对应到低字节的第 0 位?这里需要仔细对位:寄存器是 32 位,地址从高位字节开始发送,所以 cfr2 数组里最后一个元素对应寄存器的 bit7:0。0x29 即 0b00101001,bit7=0, bit6=0, bit5=1, bit4=0, bit3=1, bit2=0, bit1=0, bit0=1,这表示 PLL 使能位 bit8 如果按这个字节的 bit0 来算就错了。正确写法是 N=40 放在 bit6:0,bit8 是 PLL 使能,应该出现在字节 3 的 bit0 但该字节只能表示 bit7:0。所以要重新组织:CFR2 的 bit8 属于第二个字节的 bit0,而不是第三个字节。
实际配置 CF2 时,寄存器位段跨字节是常态。最稳妥的做法是定义一个 32 位整数,用位操作设置后再拆成 4 字节:
uint32_t cfr2_val = 0; cfr2_val |= (1 << 8); // PLL 使能 cfr2_val |= (0x2 << 9); // VCO 倍频段 cfr2_val |= (40 << 0); // N=40 uint8_t cfr2[4]; for (int i = 0; i < 4; i++) { cfr2[i] = (cfr2_val >> (24 - i * 8)) & 0xFF; }用移位拆字节比手工填数组直观得多,也避免跨字节位域错位的问题。PLL 倍频数 N 的范围是 4~255,不同参考频率对应不同 VCO 倍频段,具体查 datasheet 的 PLL 配置表。如果参考频率是 20 MHz 而要 800 MHz SYSCLK,倍频 40 倍,VCO 段可能需要 4,改 VCO 位即可。这里最容易踩坑的是以为 N 写进去就完事了,实际上 VCO 段选错会导致 PLL 失锁,输出频率乱跳。
4. 例程功能拆解与参数设置:定频、扫频和幅度控制的落地写法
4.1 设置输出频率:写 CTW0 并触发的完整函数
AD9954 有 4 个 Profile(0~3),每个 Profile 有独立的频率字和相位字寄存器。通过 PS0/PS1(CFR1 的 bit11:10 配置为对应模式)选择当前激活 Profile。最常见的用法是只用 Profile 0,所有配置都写 CTW0(地址 0x04)和 CPW0(地址 0x05),PS0/PS1 固定为 0。
写频率的代码段:
void AD9954_SetFreq(double freq_Hz, double sysclk_Hz) { uint32_t ftw = AD9954_CalcFTW(freq_Hz, sysclk_Hz); uint8_t data[4]; data[0] = (ftw >> 24) & 0xFF; data[1] = (ftw >> 16) & 0xFF; data[2] = (ftw >> 8) & 0xFF; data[3] = ftw & 0xFF; AD9954_WriteRegister(0x04, data, 4); AD9954_IOUpdate(); }这里 0x04 是 CTW0 的地址。如果只改频率不改相位,CTW0 一个寄存器就够了。执行完 IO Update 后输出立即改变。数据手册建议 IO Update 之后要等至少 4 个 SYSCLK 周期再发送下一次 SPI 写,实际操作中加 1 μs 延时完全足够。
要注意的是,AD9954 内部是并行数据路径,频率字写入后会在 SYNC_CLK 域同步更新,所以 IO Update 的上升沿必须和 SYNC_CLK 边沿对齐。手册要求 IO Update 信号的建立时间和保持时间各约 5 ns,GPIO 翻转速度能达到,但如果用了长飞线,信号完整性变差,更新瞬间可能采样到亚稳态,输出频率偶发错误。近距离布线或加 33 Ω 串联电阻是常用缓解手段。
4.2 扫频输出:相位连续性和步进延时怎么设
AD9954 本身支持线性扫频吗?严格说,它的频率字寄存器是直接写的,不像某些 DDS 芯片内置扫频斜坡发生器。要实现扫频,要么单片机频繁更新 CTW0,要么用两个 Profile 交替切换。单片机更新方式受限于 SPI 速率和 IO Update 周期,每步最小时延约 10 μs。如果扫频步进 1000 步,全扫一遍 10 ms,这对于大多数激励源场景够用。
用定时器中断实现扫频,伪代码:
volatile uint32_t sweepIndex = 0; volatile uint32_t sweepSteps = 1000; volatile double sweepStart, sweepStop; void TIM3_IRQHandler(void) { if (sweepIndex < sweepSteps) { double f = sweepStart + (sweepStop - sweepStart) * sweepIndex / sweepSteps; uint32_t ftw = AD9954_CalcFTW(f, SYSCLK_HZ); AD9954_SetFreqRaw(ftw); // 只写寄存器不等待 IO Update 之外的多余延时 sweepIndex++; } TIM_ClearITPendingBit(TIM3, TIM_IT_Update); }频率字的计算放在中断里没问题,float 运算在 F407 上只耗几十个周期。但要注意:每步都调 AD9954_IOUpdate,会让中断执行时间拉长。如果扫频步进间隔在微秒级,中断里做 SPI 写和 IO Update 会占用大量 CPU 时间,主循环响应变差。此时可以用 DMA + 定时器自动发送,或者接受 10 μs 级的最短步进。
相位连续性是扫频的关键。AD9954 的频率字更新时,内部相位累加器不会清零,所以每次更新 FTW 都是相位连续切换。这一点比用锁相环扫频的相位跳变小得多。但有一个前提:IO Update 的触发必须和输出信号的相位对齐,否则从累加器角度看不连续。实际上 IO Update 只是加载寄存器,相位累加器一直在跑,更新 FTW 后输出频率在下个 SYNC_CLK 周期平滑过渡,所以不存在相位跳变。用 Profile 切换方式(PS0/PS1 翻转)虽然切换更快,但两个 Profile 的相位字如果没配好,切换瞬间会有相位台阶。
4.3 幅度控制:ACR 寄存器的 10 位衰减码怎么映射
AD9954 的幅度控制有两个层级:一是 CFR1 里的 DAC 满量程电流设置,二是 ACR(地址 0x03)里的幅度控制字。ACR 是 16 位寄存器,bit7:0 保留,bit8 使能幅度控制,bit17:10 是 10 位 AOM。ACR 的 bit8 置 1 后,输出幅度按 AOM 衰减;bit8 为 0 时,AOM 不生效,输出满幅度。
写 ACR 的代码:
void AD9954_SetAmplitude(uint16_t aom) // aom 范围 0~1023 { if (aom > 1023) aom = 1023; uint32_t acr_val = 0; acr_val |= (1 << 8); // 使能幅度控制 acr_val |= ((uint32_t)aom << 10); // AOM 到 bit17:10 uint8_t data[2]; data[0] = (acr_val >> 8) & 0xFF; data[1] = acr_val & 0xFF; AD9954_WriteRegister(0x03, data, 2); AD9954_IOUpdate(); }ACR 只有 16 位,但 AOM 落在 bit17:10,这意味着 acr_val 可能超过 16 位范围。实际上 ACR 寄存器是 16 位,AOM 的 bit17:10 对应寄存器 bit7:0 是低字节?这里要重新读 datasheet:AD9954 的 ACR 地址是 0x03,字长 16 位,bit7:0 是保留位,bit8 是幅度控制使能,bit9 是幅度控制模式选择,bit17:10 是 AOM。但 16 位寄存器里怎么会有 bit17?这说明 AD9954 的 ACR 实际是 18 位字?查看 AD9954 手册,ACR 是 16 位寄存器,但它的 AOM 是 10 位,位于 bit15:6,而 bit5:0 保留。所以上面的写法是错的。
正确的 ACR 布局:bit15:6 = AOM,bit5:1 保留,bit0 = 幅度控制使能。写 AOM 时左移 6 位,不是左移 10 位。这个细节必须查手册确认,不同 DDS 芯片的寄存器布局差异大。在例程里如果发现幅度不变化或变化非线性,先检查 AOM 位移是否正确。
AD9954 的输出幅度还受 DAC 外部阻抗匹配影响。DAC 输出是电流型,满量程电流由外部电阻设置,典型值 10 mA 左右。输出电压幅度 = 电流 × 负载阻抗,如果负载是高阻且没接端接电阻,输出会偏高。可以在 DAC 输出端加一个 50 Ω 到地,再用变压器耦合到 50 Ω 系统,得到标准的 0 dBm 输出。幅度控制字 AOM 实际是控制 DAC 输出电流的比例,线性度在中段较好,接近 0 或满量程时有非线性。
5. 避坑指南:SPI 写不进去、输出频率不对、扫频毛刺的排查记录
5.1 现象一:寄存器写操作「成功」了,但输出频率纹丝不动
花了一晚上调代码,SPI 波形用逻辑分析仪看完全正确,指令字节、地址、数据都对,但输出始终是默认频率或者干脆没输出。问题通常不在 SPI 时序,而在 IO Update 没生效或 CFR1 的 DAC 输出没使能。
原因之一是 IO Update 引脚没有接到单片机的 GPIO 或接错了引脚。AD9954 有些型号的评估板把 IO Update 命名成 IOUP,和 IO_UPDATE 是同一个引脚。另一个原因是 IO Update 脉冲宽度不足,GPIO 翻转速度很快,但 AD9954 要求 IO Update 高电平至少持续若干个 SYNC_CLK 周期,如果外部 SYNC_CLK 还没稳定(PLL 未锁定时),这段时间的 IO Update 会被忽略。
解决方法是:先确认 CFR2 的 PLL 配置正确,等待至少 10 ms 让 PLL 锁定,再发 IO Update。软件上拉高后延时 1 μs,拉低再延时 1 μs,彻底满足时序。另外检查 CFR1 的最低位是否为 1(DAC 输出使能),很多例程默认 CFR1 是 0x00000000,导致 DAC 电流输出被关闭,SPI 再怎么写也没有波形。
5.2 现象二:输出频率比设定值偏了千分之一,且随温度漂移
频率准确度取决于 SYSCLK 是否精确。AD9954 内部 PLL 以外部参考时钟为基准,如果参考是普通有源晶振(±50 ppm),输出频率就会有同样量级的误差。还有 VCO 倍频段选择不当导致 PLL 失锁,频率完全乱跳。
我用频谱仪实测时发现 10 MHz 参考晶振实际是 9.99982 MHz,频率偏移 -18 ppm,FTW 计算出来之后输出就偏差了。解决办法是做一次 SYNC_CLK 校准:把 AD9954 的 SYNC_CLK 引脚输出频率读出来,SYNC_CLK = SYSCLK / 4,通过频率计或单片机输入捕获测出实际 SYSCLK,然后用实测值代入 FTW 公式。校准之后输出频率误差可以从 ppm 级降到 ppb 级,前提是参考晶振本身短期稳定。
如果校准后还是不对,检查 PLL 的 VCO 倍频段。AD9954 的 VCO 有四个频段,参考频率不同要选对应段,选错会导致 PLL 无法锁定。PLL 锁定状态可以通过 CFR2 的 readback 位查询,但通过 SPI 读寄存器需要额外设计。更快的验证方法是直接测 SYNC_CLK 频率,如果 SYSCLK 是 400 MHz,SYNC_CLK 应该是 100 MHz。不平衡的话查倍频段配置。
5.3 现象三:扫频输出在起点和终点有毛刺,中间偶尔出现尖峰
扫频毛刺的根源往往是频率字更新和 IO Update 的竞争条件。如果主循环正在写 CTW0,定时器中断同时触发 IO Update,两次写操作交错,AD9954 先加载了前半部分数据就触发更新,导致输出频率瞬间跳到错误值,出现毛刺。
解决方案是保证寄存器写入和 IO Update 构成原子操作:在写寄存器序列的开始关闭 IO Update 中断,或者用一个互斥标志确保同一时刻只有一个任务在操作 SPI。更彻底的办法是放弃中断,在主循环里轮询定时器标志,这样 SPI 操作天然串行。实测扫频步进时间从 10 μs 放宽到 50 μs,但毛刺完全消除。
另一个毛刺来源是 AD9954 的模拟输出滤波器。DAC 输出是阶梯波,包含大量镜像频率成分,必须接低通滤波器。如果滤波器截止频率设置过高,镜像频率泄漏会在扫频时和基频产生混叠,频谱仪上看到毛刺。通常滤波器截止频率设在最高输出频率的 1.5 倍左右,例如最高输出 80 MHz,低通截止 120 MHz。PCB 布线时滤波器电容地线走线太长也会引入寄生电感,表现为特定频率的谐振峰。
5.4 现象四:输出幅度比预期低 6 dB,且幅度控制字变化不连续
6 dB 差异通常是差分转单端电路不平衡造成的。AD9954 的 DAC 输出是互补电流输出(IOUT 和 IOUTN),直连单端负载会损失一半电流,幅度降低 6 dB。正确做法是用变压器把差分转成单端,或者用运放做差分转单端。如果用变压器,注意带宽和插入损耗,低频段变压器饱和会使幅度下降。
幅度控制字不连续则要检查 ACR 的位定义。前面提过 AOM 是 10 位,我的例程里最开始写成了左移 10 位,结果幅度只在 AOM 超过 64 时才变化,且跳变明显。后来改为左移 6 位,幅度就平滑了。遇到现象时先回读寄存器确认实际写入值,再比对数据手册的位段图。AD9954 的数据手册里 ACR 的 bit15:6 是 AOM,这和数据手册更新版本可能不一样,务必以最新 datasheet 为准。
还有一种情况是例程里把 AOM 解释成线性幅度百分比,但 AD9954 的 AOM 对 DAC 电流的控制不是线性百分比,而是 10 位二进制加权电流源,低段位权重小,高段位权重大。如果期望线性 dB 变化,应该用对数映射。这个坑在需要精确幅度扫频(比如频谱分析仪校准)时会暴露出来,普通应用只要单调即可。
5.5 现象五:单片机重启后 AD9954 输出乱码,必须重新上电才能恢复
掉电重启后 AD9954 的寄存器有不确定值,尤其是 PLL 配置不对时,SYSCLK 可能是参考时钟直通或错误的倍频,输出全是杂散。单片机复位后如果只配置了 SPI,没复位 AD9954,就会出现在这种状态。
解决方法是把 AD9954 的复位引脚接到单片机的 GPIO,上电初始化时先拉低复位引脚,保持至少 10 μs,再拉高释放,让 AD9954 进入确定状态,然后再配置寄存器。如果复位引脚没接,可以尝试通过 SPI 写 CFR2 的软件复位位,但有些版本芯片软件复位不可靠。硬件复位最保险。另外注意 AD9954 的电源上电顺序,DAC 模拟电源和数字电源要同时上电,否则 POR 电路可能没充分复位。
6. 验证输出与进阶技巧:频谱仪实测和单次触发扫频的正确姿势
AD9954 例程调通后,不要只看示波器上有正弦波就认为大功告成。示波器只能验证频率大致对、幅度有变化,但杂散、相位噪声、镜像抑制这些指标都看不见。我的习惯是接频谱仪实测:先看主信号频率是否精确落在设定值,再看镜像频率是否被滤波器抑制到 -40 dBc 以下,最后看近端杂散是否来自 DDS 的相位截断噪声。AD9954 的相位累加器是 32 位,但查表只有 14 位,相位截断产生的杂散发在特定频率偏移处,幅度和输出频率有关。如果杂散在目标频段内干扰系统,可以尝试改变 SYSCLK 让杂散移出关注带宽,或者用抖动的相位累加器来扩散杂散能量。
进阶玩法是把 AD9954 和 STM32F407 的 DAC 结合,做一个带扫频触发和频率回读的完整信号源。单片机可以同时产生调幅信号和调频信号——用 DDS 作为载波源,单片机 DAC 输出调制波形。实测时用双通道示波器看 DDS 输出和解调信号,可以验证 AM 深度和 FM 频偏是否符合预期。
还有一个小技巧是配置 AD9954 的 OSK(幅度键控)功能,用一个 GPIO 引脚就能实现脉冲调幅,而不需要每步都通过 SPI 更新 ACR。OSK 引脚拉高时输出幅度按 ACR 设定,拉低时输出衰减到最低。这个功能在做脉冲激励时特别有用,省掉了中断里多次 SPI 写的时间。我一开始没注意到 CFR1 里的 OSK 使能位,一直用软件改幅度字,脉冲响应速度不够。改成 OSK 引脚控制后,调制带宽从几百 Hz 提升到几百 kHz,整个方案瞬间实用起来。
最后把工程里的频率字计算公式抽成一个通用的参数表,方便不同参考时钟和 SYSCLK 组合下快速套用。常见做法是在工程头文件里预留宏定义:
#define AD9954_REF_CLK_HZ 10000000.0 #define AD9954_PLL_MULT 40 #define AD9954_SYSCLK_HZ (AD9954_REF_CLK_HZ * AD9954_PLL_MULT)这样换晶振、换倍频数时只改两个宏,不用全局搜索硬编码。我吃过一次亏:某次项目把参考晶振从 10 MHz 换成 12 MHz,忘了改倍频数,PLL 输出变成 480 MHz 超出芯片规格,AD9954 直接失锁,输出全是乱码。用宏之后这种低级错误基本不会再犯。希望你也能在例程基础上做出满足自己指标的信号源,少走我走过的弯路。
本文还有配套的精品资源,点击获取