简介:这份资源是面向嵌入式开发者的AD9959与STM32F103联合工程代码包,聚焦DDS直接数字频率合成应用,适合已具备一定单片机基础、希望快速上手高频信号源开发的读者。工程基于正点原子STM32mini开发板实测通过,涵盖AD9959驱动、SPI通信控制、频率与相位寄存器配置,并配套LCD显示交互代码,可用于通信、雷达及测试测量等场景的原型验证。压缩包共158个文件,约2.9MB,以C源文件与头文件为主体,辅以工程配置、启动汇编、链接脚本、编译中间文件及烧录固件,目录结构完整,便于直接编译与二次修改。目前已有739人学习下载,可作为理解STM32与AD9959协同工作的实用起点,帮助读者掌握DDS参数更新与显示联调的完整思路。
1. AD9959 与 STM32 Mini 板:四通道 DDS 到底能玩出什么花样
如果你手里正好有一块 STM32 最小系统板,又碰巧拿到一片 AD9959,那你大概率会经历一个从兴奋到困惑的过程:四通道、10 位 DAC、最高 500 MSPS 采样率、支持多种调制模式,参数表看着很唬人,但真正把它跑起来,你会发现连最基本的正弦波都出不来。问题不在芯片,而在于 AD9959 的控制接口和寄存器配置比常见的 AD9833、AD9850 复杂得多,它需要精确的 SPI 时序、严格的上电复位流程,以及一套能同时管理四个通道的寄存器写入逻辑。
这个标题指向的是一套典型的嵌入式硬件方案:用 STM32 最小系统板驱动 AD9959 四通道 DDS 模块,实现多路可调频、可调幅、可调相的正弦波输出。它适合做信号源、锁相环参考、多通道相干激励、甚至简易的射频前端本振。难点集中在三处:SPI 通信的时序匹配、通道寄存器的地址映射、以及输出幅度与频率的联合校准。接下来的内容会从硬件连接到寄存器操作,再到实际调试中遇到的坑,一步步把这条路走通。
2. 先把硬件链路搭对:STM32 与 AD9959 的引脚映射和供电边界
2.1 为什么 AD9959 不能直接照搬 AD9833 的接线方式
AD9833 是单通道、10 位、25 MSPS 的 DDS,控制接口简单到几乎可以软件模拟 SPI 随便跑。AD9959 完全不同:它有四个独立的 DDS 核,共享一个 500 MSPS 的系统时钟,每个通道有独立的频率控制字、相位偏移字和幅度控制字。更关键的是,AD9959 的 SPI 接口支持最高 25 MHz 的 SCLK,但前提是你的 STM32 能稳定输出这个频率,并且数据建立和保持时间满足芯片手册要求。
我一般会把 STM32 的 SPI 配置成主机模式,时钟极性 CPOL=0、相位 CPHA=0,数据宽度 8 位,MSB 先行,预分频系数根据主频调整到 SCLK 在 10 MHz 到 20 MHz 之间。不要一上来就冲 25 MHz,线长超过 10 cm 后信号完整性会迅速恶化,波形出现毛刺,寄存器写入随机失败。这是血泪经验:先用 5 MHz 跑通,再逐步往上提。
供电部分需要特别注意。AD9959 的模拟电源和数字电源是分开的,典型值是 1.8 V 核心电压和 3.3 V IO 电压。很多模块把 1.8 V 用低压差线性稳压器从 3.3 V 降下来,这时候你要确认低压差线性稳压器的负载能力至少 300 mA,因为四个通道同时工作时电流会明显上升。STM32 最小系统板的 3.3 V 输出通常只有 100 mA 到 200 mA,直接给 AD9959 模块供电大概率会翻车,表现为输出频率漂移或者通道间串扰突然变大。
2.2 最小系统板的引脚分配与 SPI 片选逻辑
以常见的 STM32F103C8T6 最小系统板为例,SPI1 的默认引脚是 PA5(SCLK)、PA6(MISO)、PA7(MOSI)。AD9959 只需要单向写入,所以 MISO 可以悬空,但建议还是接上,方便回读寄存器验证写入是否成功。片选 CS 用 PA4,复位 RESET 用 PB0,IO_UPDATE 用 PB1,这三个控制引脚用普通推挽输出即可。
| AD9959 引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| SCLK | PA5 | SPI 时钟,空闲低电平 |
| SDIO_0 | PA7 | 串行数据输入 |
| CS | PA4 | 片选,低有效 |
| RESET | PB0 | 复位,高有效,至少保持 1 个时钟周期 |
| IO_UPDATE | PB1 | 上升沿将缓冲寄存器内容更新到活动寄存器 |
| PWR_DWN | 悬空或接 3.3 V | 掉电控制,不用时拉高 |
接线时有一个容易被忽略的点:AD9959 的 SDIO_0 在单线模式下既是输入也是输出,但如果你只做写入,把它当纯输入用就行。不过有些模块会把 SDIO_0 到 SDIO_3 都引出来,支持四线并行写入,速度更快但占用引脚更多。对于最小系统板,单线模式足够,因为寄存器数量不多,每次配置也就几十个字节。
2.3 上电时序与复位脉冲的最小可靠实现
AD9959 对上电顺序有要求:先给 1.8 V 核心电,再给 3.3 V IO 电,最后拉高 RESET 并保持至少 1 个 SCLK 周期。很多模块内部已经做了电源时序管理,你只需要在软件里保证复位脉冲宽度足够。我一般会在初始化函数里先拉高 RESET,延时 10 ms,再拉低,再延时 10 ms,然后才开始 SPI 通信。
// AD9959 复位与初始化时序 void AD9959_Init(void) { // 配置 SPI1 为模式0,8位数据,预分频到 9 MHz 左右 SPI1_Init(); // 复位引脚推挽输出 GPIO_Init_RESET(); GPIO_Init_IO_UPDATE(); GPIO_Init_CS(); // 片选拉高,空闲状态 CS_HIGH(); // 复位脉冲:高 -> 低 -> 高,宽度大于 1 个 SCLK 周期 RESET_HIGH(); delay_ms(10); RESET_LOW(); delay_ms(10); RESET_HIGH(); delay_ms(10); // 此时 AD9959 内部寄存器为默认值,等待配置 }这段代码的逻辑很直接:先把通信引脚置到空闲状态,再给复位引脚一个干净的上升沿。参数上,延时 10 ms 是保守值,实际手册要求的最小复位脉宽只有几纳秒,但考虑到最小系统板的晶振起振时间和电源稳定时间,多等一会儿不会错。IO_UPDATE 在初始化阶段保持低电平,等所有寄存器写完之后再给上升沿。
3. 寄存器写入才是核心:通道地址映射与频率控制字计算
3.1 AD9959 的寄存器空间与通道选择机制
AD9959 的寄存器分为两类:全局寄存器和通道寄存器。全局寄存器包括功能寄存器 FR1、FR2,以及通道选择寄存器 CSR。通道寄存器包括每个通道的频率调谐字 FTW、相位偏移字 POW、幅度控制字 ACR,以及一些辅助寄存器。
关键点在于:你不能直接往某个通道的 FTW 写数据,必须先通过 CSR 选中目标通道,再写入对应的寄存器地址。CSR 的 bit0 到 bit3 分别对应通道 0 到通道 3,可以同时选中多个通道,实现同步更新。这个机制让四通道相干输出变得非常方便,但也容易出错:如果你忘记设置 CSR,数据会写到默认通道或者被忽略。
寄存器地址是 7 位,写入时先发地址字节,再发数据字节。地址字节的最高位是读写标志,0 表示写,1 表示读。数据字节数取决于寄存器宽度,FTW 是 32 位,需要发 4 个字节,MSB 先行。
3.2 频率调谐字计算:从目标频率到 32 位整数
AD9959 的输出频率公式是:
f_out = (FTW × f_sysclk) / 2^32
其中 f_sysclk 是系统时钟频率,典型值是 500 MHz。FTW 是 32 位无符号整数,范围从 0 到 2^32 - 1。举个例子,如果你想输出 10 MHz,FTW = (10e6 × 2^32) / 500e6 = 85899345.92,取整后写入 85899346。
实际写代码时,要用 64 位整数做中间运算,避免 32 位溢出。下面是一个计算函数:
// 根据目标频率计算 32 位频率调谐字 // f_out: 目标输出频率,单位 Hz // f_sysclk: 系统时钟频率,单位 Hz,典型值 500000000 uint32_t AD9959_CalcFTW(double f_out, double f_sysclk) { // 使用 double 保证精度,最后四舍五入到整数 double ftw = (f_out * 4294967296.0) / f_sysclk; // 边界检查:FTW 不能超过 2^32 - 1 if (ftw >= 4294967296.0) { ftw = 4294967295.0; } return (uint32_t)(ftw + 0.5); }参数说明:f_out 是你要输出的频率,单位 Hz;f_sysclk 是 AD9959 的实际系统时钟,如果你用的是内部锁相环倍频,要确认倍频后的频率。常见做法是用 25 MHz 晶振,内部锁相环倍频到 500 MHz,这时候 f_sysclk 就是 500000000。注意浮点运算在 STM32 上如果没有硬件浮点单元会比较慢,但频率计算只在初始化时做一次,不影响实时性。
3.3 相位偏移与幅度控制字的写入顺序
相位偏移字是 14 位,范围 0 到 16383,对应 0 到 360 度。计算公式是 POW = (phase_deg / 360.0) × 16384。幅度控制字是 10 位,范围 0 到 1023,对应输出幅度从 0 到最大值。但要注意,AD9959 的输出幅度还受外部电阻和低通滤波器影响,ACR 只是数字增益。
写入顺序很重要:先写 FTW,再写 POW,最后写 ACR。每写一个寄存器,都要拉低 CS,发送地址和数据,再拉高 CS。所有通道的寄存器写完之后,给 IO_UPDATE 一个上升沿,数据才会从缓冲寄存器更新到活动寄存器。如果你在写的过程中给了 IO_UPDATE,会出现部分通道更新、部分通道没更新的情况,输出波形会短暂跳变。
// 向指定通道写入频率调谐字 // ch: 通道号 0-3 // ftw: 32 位频率调谐字 void AD9959_WriteFTW(uint8_t ch, uint32_t ftw) { // 先设置通道选择寄存器 AD9959_WriteReg(CSR_ADDR, 1 << ch); // 写入 FTW 寄存器,地址 0x04,4 字节数据 CS_LOW(); SPI_WriteByte(0x04); // 地址,写模式 SPI_WriteByte((ftw >> 24) & 0xFF); SPI_WriteByte((ftw >> 16) & 0xFF); SPI_WriteByte((ftw >> 8) & 0xFF); SPI_WriteByte(ftw & 0xFF); CS_HIGH(); // 注意:此时数据还在缓冲寄存器,需要 IO_UPDATE 才生效 }这段代码里,CSR_ADDR 是通道选择寄存器的地址,通常是 0x00。1 << ch 生成通道掩码,比如 ch=0 就是 0x01,ch=3 就是 0x08。SPI_WriteByte 是底层字节发送函数,需要根据你的 STM32 型号实现。写完之后不要忘记在合适的地方调用 IO_UPDATE。
4. 调试阶段最容易翻车的四个地方:避坑与排查
4.1 现象:输出频率总是目标值的一半或两倍
原因通常有两个:一是系统时钟频率搞错了,比如你以为内部锁相环倍频到了 500 MHz,实际还是 25 MHz 直通;二是 FTW 计算时用了 2^31 而不是 2^32。AD9959 的频率分辨率是 32 位,不是 31 位。检查方法很简单:写一个已知 FTW,比如 2^31,如果输出是 f_sysclk/2,说明公式对了;如果输出是 f_sysclk/4,说明你用了 2^31 做分母。
解决方式是回读系统时钟配置寄存器,确认锁相环倍频系数。常见配置是 25 MHz 晶振 × 20 = 500 MHz,倍频系数写在 FR1 寄存器里。如果你用的是模块自带的晶振,先查模块原理图确认晶振频率,不要凭感觉猜。
4.2 现象:某个通道完全没有输出,其他通道正常
先检查 CSR 寄存器的通道掩码有没有写对。比如你想选通道 2,掩码应该是 0x04,如果你写成了 0x02,实际选中的是通道 1。另一个可能是该通道的 ACR 寄存器被写成了 0,幅度为零自然没有输出。回读 ACR 寄存器确认一下,如果读回来是 0,重新写入一个非零值。
还有一种情况是通道的 POW 寄存器被写入了极端值,导致输出相位正好抵消。不过这种情况概率较低,优先查 CSR 和 ACR。
4.3 现象:输出波形上有规律性的毛刺,频率越高越明显
这是典型的 SPI 时序问题。SCLK 太快、线太长、没有加串联匹配电阻,都会导致数据在 AD9959 端采样错误。先用示波器看 SCLK 和 SDIO_0 的波形,如果上升沿有明显的振铃,在 STM32 输出端串一个 22 Ω 到 33 Ω 的电阻,靠近 AD9959 放置。同时把 SPI 预分频调大,降低 SCLK 频率,看毛刺是否消失。
另一个可能的原因是 IO_UPDATE 信号在 SPI 通信过程中被意外触发。检查代码里有没有在中断里操作 IO_UPDATE,或者 GPIO 初始化时把它设置成了复用功能。IO_UPDATE 应该是纯推挽输出,只在所有寄存器写完后手动拉高再拉低。
4.4 现象:四通道同时输出时,通道间串扰明显,幅度不一致
AD9959 的四个通道共用同一个系统时钟和电源,通道间串扰主要来自电源耦合和地弹。首先确保模拟地和数字地在 AD9959 模块上单点连接,STM32 的地和模块的地用粗线短接。其次,每个通道的输出端都有低通滤波器,检查滤波器元件是否焊接良好,特别是电感和电容的值是否一致。
幅度不一致通常是 ACR 寄存器写入值不同,或者外部负载电阻不一致。先回读四个通道的 ACR,确认数值相同。如果相同但幅度还是不一样,用示波器分别测量四个通道的输出峰峰值,差异在 5% 以内算正常,超过 10% 就要检查滤波器元件公差。
5. 进阶技巧:用 IO_UPDATE 同步四通道相位与扫频输出
5.1 四通道相干输出的实现要点
四通道相干输出意味着四个通道的频率相同、相位差固定。实现方法是:先通过 CSR 同时选中四个通道,然后依次写入相同的 FTW,再分别写入不同的 POW。注意 POW 的写入也要在同一个 IO_UPDATE 周期内完成,否则相位差会随时间漂移。
具体操作顺序是:设置 CSR 为 0x0F,写入 FTW;然后设置 CSR 为 0x01,写入通道 0 的 POW;设置 CSR 为 0x02,写入通道 1 的 POW;以此类推。所有写完后,给一个 IO_UPDATE 脉冲。这样四个通道会在同一个时钟沿更新,相位关系精确保持。
5.2 用定时器触发 IO_UPDATE 实现线性扫频
线性扫频需要频繁更新 FTW,如果每次都用软件延时,扫频速度会很慢。更好的做法是用 STM32 的定时器产生周期性中断,在中断里计算新的 FTW 并写入,然后触发 IO_UPDATE。定时器频率决定了扫频步进速率,比如 10 kHz 的中断频率,每步频率增加 1 kHz,扫频速度就是 10 MHz/s。
// 定时器中断服务函数,用于线性扫频 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 当前频率累加步进 current_freq += freq_step; if (current_freq > freq_max) { current_freq = freq_min; // 循环扫频 } // 计算新的 FTW 并写入所有通道 uint32_t ftw = AD9959_CalcFTW(current_freq, 500000000.0); AD9959_WriteFTW(0, ftw); AD9959_WriteFTW(1, ftw); AD9959_WriteFTW(2, ftw); AD9959_WriteFTW(3, ftw); // 触发 IO_UPDATE,四个通道同步更新 IO_UPDATE_HIGH(); delay_us(1); IO_UPDATE_LOW(); } }这段代码里,freq_step 是每次中断的频率增量,freq_min 和 freq_max 是扫频范围。注意 IO_UPDATE 的高电平宽度至少 1 个系统时钟周期,这里用 1 微秒延时是保守值。如果扫频速度要求很高,可以把 IO_UPDATE 的脉冲宽度压缩到几个时钟周期,但不要低于手册要求的最小值。
5.3 输出幅度校准的实用方法
AD9959 的输出幅度和 ACR 值不是严格线性的,尤其在接近满量程时会出现压缩。我一般会用一个简单的查表法:在 0 到 1023 之间取 16 个点,用示波器测量实际输出峰峰值,然后做线性插值。这样可以把幅度误差控制在 2% 以内。
校准表格可以存在 STM32 的闪存里,上电时加载到内存。每次设置幅度时,先查表得到实际 ACR 值,再写入寄存器。这个方法虽然土,但比盲目相信数据手册的线性度靠谱得多。
最后说一个我自己的习惯:每次调试 AD9959 之前,先用示波器确认系统时钟引脚上有干净的 500 MHz 信号。如果时钟本身抖动很大,后面所有配置都是白费力气。这个习惯帮我省下了至少两天反复查寄存器的时间。希望帮到你。
本文还有配套的精品资源,点击获取