☰
STM32F103C8驱动AD9854:DDS信号源SPI驱动程序详解
2026/10/15 16:28:33 网站建设 项目流程

简介:基于STM32F103C8的AD9854驱动程序,面向嵌入式开发者与信号生成应用场景。AD9854是一款高精度DDS器件,可输出正弦、方波、三角波与脉冲波形;配套驱动涵盖初始化配置、SPI传输、波形动态调整、状态检测及上层API封装,便于快速集成到实际项目中。压缩包内共176个文件,以c与h源码文件为核心,包含示例工程main.c、Keil工程文件uvproj/uvopt、链接脚本sct及hex/axf烧录文件,另有连接图与构建辅助脚本,整体约3.95MB。资源包对引脚连接、寄存器操作和波形控制方式均有具体实现,并考虑电源管理、抗干扰等工程问题,适合正在学习DDS或需要快速完成信号源功能的开发者参考。已有2309人学习下载,具备较好的实践参考价值。

1. 先搞清楚:这个驱动到底在解决什么问题

做信号源、扫频仪或者收发信机本振的时候,你会发现 PLL 方案虽然频率高,但步进和切换速度很难兼顾。而 AD9854 这种 DDS 芯片,内部用相位累加器直接合成频率,48 位频率字能把步进做到微赫兹级别,双通道还能输出正交信号。问题在于,AD9854 的配置接口不是随手就能点亮的——它需要严格的时序、地址帧和更新脉冲。本文要讲的,就是基于 stm32f103c8 写一套能落地、能复现、不靠运气的 AD9854 驱动程序,用 SPI 模式省引脚,把频率、相位、幅度全部变成可控制的 API。适合正在做信号源、想要快速驱动 AD9854 却卡在时序和寄存器细节上的人。

2. 为什么是 AD9854 加 STM32F103C8:从选型到驱动架构

2.1 双通道 DDS 能做什么:从频率字到正交输出的完整链路

AD9854 的核心是一条非常经典的数字合成链路:频率字(Frequency Tuning Word, FTW)送入相位累加器,以系统时钟为节拍不断累加,累加结果作为相位查表地址,查出的正弦幅度送进内部 DAC,最终在 I 和 Q 两条通道上得到差分电流输出。频率字的位宽是 48 位,输出频率与系统时钟的关系是一个确定性的公式:输出频率 = FTW × 系统时钟 / 2^48。这个公式不是参考设计里的近似,而是 DDS 的数学本质,所以你驱动写好之后,完全可以拿频率计和这个公式互相验证。

双通道的价值在于两个输出通道共用同一个相位累加参考,但可以分别写独立的频率字和相位字。I/Q 正交输出在做 IQ 调制、解调或者矢量信号源的时候特别方便,比单通道 DDS 外加移相器要稳定得多。AD9854 还支持 Chirp 扫频模式,但这属于进阶功能,寄存器配置和驱动层结构会多一块状态机逻辑。我的建议是先把最基本的单频点功能跑通,再碰扫频。

2.2 为什么用 SPI 而不是并行总线:C8 的引脚预算与速度权衡

STM32F103C8 是 48 脚的封装,Flash 64KB,RAM 20KB。如果给 AD9854 走并行模式,数据线加地址线加控制线至少要 20 个引脚,整颗芯片的引脚预算直接被吃干,其他外设全得让路。而 AD9854 本身支持串行 SPI 配置模式,只需要 SCK、数据线、片选、更新四根线,对 C8 来说压力小得多。更关键的是,DDS 的频率字并不需要像 DAC 波形数据那样持续高速刷新——你设置一个频点后,芯片自己会持续输出,驱动层的写入频率很低,SPI 完全够用。

速度上,AD9854 的 SPI 时钟上限远高于我们实际使用的配置速度。我在工程里把 SPI1 的分频设为 8,主频 72MHz 下得到 9MHz 的 SCLK,驱动稳定,没有任何时序压力。如果你用的是外部低速晶振给 STM32 提供时钟,或者板子走线比较长,分频设到 16、32 也不会有任何体验差异。真正影响输出频率精度的是 AD9854 的参考时钟,不是 SPI 速度,这点先有认知,后面才不会调错方向。

2.3 寄存器访问模型:地址帧、数据帧与更新时序

AD9854 的串行寄存器访问模型比普通 SPI 外设多了一个关键概念:UPDATE 脉冲。写寄存器分两步,第一步是 SPI 帧传输,把地址和数据移进芯片内部的临时寄存器;第二步是拉一次 UPDATE 引脚,临时寄存器的内容才会原子地生效到工作寄存器。如果你只做第一步不拉 UPDATE,寄存器写入等于白写,输出不会有任何变化,这是新手最容易栽的地方。

串行帧格式按芯片手册定义,一片完整的写操作是 8 位地址加 32 位数据,地址的高 7 位是寄存器地址,最低位是读写标志,写操作时该位为 0。48 位的频率字在串行模式下没法塞进一个 32 位数据帧,必须拆成两帧:先写高 32 位,再写低 16 位(左对齐),具体拆分时序在手册的串行写章节有两页图。我第一次写驱动时没看那个图,把 48 位直接当 32 位写,自然是失败的。现在我会把寄存器写函数设计成通用的帧函数,频率设置函数内部做拆分调用。

3. 硬件接线与最小系统:先把能启动的地基打好

3.1 引脚分配与初始化顺序

我用标准库来做这套驱动,工程里 GPIO 和 SPI1 的分配如下表。SCK、数据线、CS 都挂在 SPI1 的默认引脚上,UPDATE 用普通 GPIO 控制,不占用 SPI 硬件信号。

STM32F103C8 引脚复用功能连接 AD9854 引脚说明
PA5SPI1_SCKSCLK串行时钟,空闲低电平
PA7SPI1_MOSISDIO数据线,只写模式不需要读回
PA4普通 GPIOCS低有效片选,软件控制
PA3普通 GPIOUPDATE更新脉冲,软件控制

初始化顺序里有个容易被忽略的点:要先配置 GPIO 和 SPI 外设,再配置 UPDATE 引脚,最后给 AD9854 上电复位。反过来如果芯片先上电,SPI 引脚还处于浮空状态,芯片可能误判电平进入未知的接口模式。我一般习惯在 main 函数最开始就调用硬件初始化,再延时 50ms 等参考时钟稳定,然后才执行 AD9854 的寄存器写入。这个顺序在多个板子上验证过,可以减少很诡异的启动问题。

3.2 参考时钟、电源与输出级:三个常见的玄学失败点

AD9854 的参考时钟输入是差分形式的,芯片上有 REFCLK 和 REFCLKB 两个引脚。单端信号源接入时,REFCLK 接信号,REFCLKB 通过 100nF 电容到地,这是最常见的接法。注意这里必须交流耦合,不能把有源晶振的输出直接直流怼进去,否则会破坏芯片内部的输入偏置,芯片发烫而且输出频率乱跳。接好之后用万用表量一下 REFCLK 引脚的静态电压,正常应该在供电电压的一半附近,如果接近 0V 或接近 VCC,赶紧断电查耦合电容。

电源去耦是另一个翻车点。AD9854 的数字和模拟电源引脚不少,每个电源引脚旁边放 100nF 陶瓷电容,同时在芯片附近再放一个 10μF 钽电容。我的经验是电源去耦不到位时,输出频谱上会出现杂散,根本不是驱动代码的问题,却会浪费你大量时间在软件上排查。输出级同样要注意:AD9854 输出的是差分电流信号,满量程电流在毫安级别,直接怼示波器探头是看不到正常波形的。常见做法是差分输出接 1:1 高频变压器转单端,或者用运放搭跨阻放大,之后再进示波器或频谱仪。

3.3 接口模式选择与复位时序

AD9854 的并行/串行接口模式由芯片上的模式选择引脚决定,具体组合要看手册里的接口模式表。不同批次或封装的板子上,这两个引脚的丝印和默认电平可能不一样。拿到板子第一件事不是写代码,而是用万用表量一下这两个引脚的静态电平,确认当前硬件处于串行模式。如果默认在并行模式,你发 SPI 帧是完全没有反应的,而且没有报错可查。

我的做法是在硬件设计阶段就把这两个引脚用电平固定住,不用跳线。这样固件里不用做任何判断。如果你拿到的是别人的板子,那就先量电平再决定软件方案。复位时序上,给 AD9854 上电后要等待至少几十毫秒,让内部电路和参考时钟稳定下来,再进行初次寄存器写入。第一次写入建议先写控制寄存器,把系统时钟倍频和内部工作模式设好,再写频率字。反过来的顺序容易出怪问题,比如频率输出正确但幅度异常,或者两个通道串扰。

4. 编写 STM32F103C8 的 AD9854 SPI 驱动:代码逐段拆解

4.1 SPI1 外设初始化与 GPIO 复用配置

驱动底层全部基于标准外设库,方便对照寄存器手册理解。第一步是打开 GPIOA 和 SPI1 的时钟,配置 PA5 和 PA7 为复用推挽输出,PA4 和 PA3 为通用推挽输出。这里的细节是 PA4 的 CS 虽然是软件控制,但复用配置里要把它设置为通用推挽,而不是复用功能,否则会受 SPI 硬件 NSS 信号干扰。

void AD9854_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCK: PA5, MOSI: PA7, 复用推挽 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* CS: PA4, UPDATE: PA3, 普通推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_3); /* SPI1 主机模式, 空闲低电平, 第一边沿采样 */ SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

这段代码里最关键的两个参数是SPI_CPOL_Low和SPI_CPHA_1Edge,对应 SPI 模式 0。AD9854 手册的串行时序图默认 SCLK 空闲为低电平,数据在第一个边沿被采样。你的板子如果之前跑过其他 SPI 设备,极有可能沿用了模式 3 的配置,直接套用到 AD9854 上会出现写着没写入、时好时坏的诡异现象。所以每次新建工程我都强调检查 SPI 极性和相位,这是驱动层第一道关卡。

4.2 寄存器写帧函数与 UPDATE 脉冲

接下来是驱动核心的帧传输函数。一次完整的写操作由三部分组成:拉低 CS、逐字节发送 5 字节帧、拉高 CS,最后再单独发一个 UPDATE 脉冲。很多人会把 UPDATE 和 CS 合并成一个引脚控制,这是不对的,CS 每帧都要翻转,而 UPDATE 是在一帧或一组相关寄存器写完之后才拉一下,两者语义不同。

static void AD9854_WriteFrame(uint8_t reg, uint32_t data) { uint8_t buf[5]; uint8_t i; /* 高 7 位为寄存器地址, 最低位写标志置 0 */ buf[0] = (reg << 1) | 0x00; /* 32 位数据按大端顺序排列 */ buf[1] = (uint8_t)(data >> 24); buf[2] = (uint8_t)(data >> 16); buf[3] = (uint8_t)(data >> 8); buf[4] = (uint8_t)(data); GPIO_ResetBits(GPIOA, GPIO_Pin_4); /* CS 拉低 */ for (i = 0; i < 5; i++) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, buf[i]); } while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); GPIO_SetBits(GPIOA, GPIO_Pin_4); /* CS 拉高 */ } void AD9854_PulseUpdate(void) { GPIO_SetBits(GPIOA, GPIO_Pin_3); /* UPDATE 高电平 */ delay_us(1); GPIO_ResetBits(GPIOA, GPIO_Pin_3); }

代码逻辑上要注意两个时序点。第一,CS 拉低之后到第一个字节发送之间不需要额外延时,因为 SPI 移位寄存器的建立时间足够;但 CS 拉高之前必须等 SPI 忙标志清除,否则最后一字节可能没发完就断开了。第二,UPDATE 高电平持续时间我给 1μs,这远大于 AD9854 手册要求的内部建立时间,实际使用中非常可靠。delay_us在工程里用 SysTick 实现,如果你用的是裸机主频 72MHz,也可以写一个简单的循环延时替代。

4.3 频率、相位与幅度设置函数

寄存器写通之后,上层应用需要的是人话接口:给个频率值、相位值、幅度值,驱动自己去算频率字和拆帧。频率设置函数要注意 48 位频率字的拆分规则,以及浮点计算的舍入误差。四舍五入加 0.5 再取整是必要的,否则某些频率点会偏一两个 LSB,频率精度微赫兹级别时这是可见误差。

#define AD9854_SYSCLK (300.0e6) /* 参考时钟与 PLL 倍频后的系统时钟 */ #define AD9854_FTW_BASE (281474976710656.0) /* 2^48 */ void AD9854_SetFreq(uint32_t channel, double freq_hz) { uint64_t ftw; uint32_t hi, lo; uint8_t reg; if (freq_hz < 0) freq_hz = 0; if (freq_hz > AD9854_SYSCLK / 2) freq_hz = AD9854_SYSCLK / 2; /* 输出频率 = FTW * SYSCLK / 2^48 */ ftw = (uint64_t)(freq_hz * AD9854_FTW_BASE / AD9854_SYSCLK + 0.5); hi = (uint32_t)(ftw >> 16); /* 48 位中高 32 位 */ lo = (uint32_t)(ftw & 0xFFFF); /* 低 16 位 */ reg = (channel == 0) ? 0x02 : 0x06; /* I 路和 Q 路的频率寄存器起始地址 */ AD9854_WriteFrame(reg, hi); AD9854_WriteFrame(reg, lo << 16); /* 低 16 位左对齐放入 32 位数据帧 */ AD9854_PulseUpdate(); }

参数说明里有两个边界要交代。频率上限我限制在系统时钟的一半,这是 DDS 的奈奎斯特限制,超过一半输出会出现镜像频率,表现为频谱上能量跑到错误的位置。寄存器起始地址按通道区分:I 路从 0x02 开始,Q 路从 0x06 开始,实际使用时你拿到手的手册上会标注清晰的寄存器表。如果芯片手册显示你的型号地址映射不同,以手册为准,这段代码的结构不需要变,改地址就行。相位和幅度函数类似,相位字 14 位,90 度对应的数值是 90 × 16384 / 360 = 4096;幅度字 12 位,满量程 4095。

4.4 main 函数里的最小调用示例

所有底层函数封装好之后,应用层调用简单到让人怀疑是否漏了什么。初始化、设置频率、设置相位、设置幅度,四行搞定。第一次跑这个示例,打开示波器看输出,如果能看到干净的正弦波,说明整个链路就是通的。

int main(void) { delay_init(); AD9854_GPIO_Init(); delay_ms(50); AD9854_SetFreq(0, 1000000.0); /* I 路输出 1MHz */ AD9854_SetFreq(1, 1000000.0); /* Q 路输出 1MHz */ AD9854_SetPhase(0, 0); /* I 路相位 0 度 */ AD9854_SetPhase(1, 4096); /* Q 路相位 90 度 */ AD9854_SetAmp(0, 4095); /* 满幅度 */ AD9854_SetAmp(1, 4095); while (1) { delay_ms(1000); AD9854_SetFreq(0, 1234567.0); /* 每秒变一次频率, 验证动态配置 */ } }

这段调用示例里有个容易被误会的点:AD9854_SetFreq每次都会自动拉 UPDATE,所以你不要在应用层再重复调用更新函数。如果某个工程需要同时改频率和相位,希望两个参数一起生效,那么应该把设置函数拆成「写寄存器」和「发 UPDATE」两个阶段,先写完所有参数再统一更新。我这里为了示例简单,把 UPDATE 内聚在了每个参数函数内部,这是单参数更新场景的便利写法,也是多参数同步场景的坑。实际项目里我建议维护一个AD9854_SyncUpdate(),在所有参数写完后调用。

5. AD9854 驱动调试避坑:现象、原因与修复方案

5.1 写了寄存器但输出完全没有反应

现象:用示波器看差分输出引脚,什么波形都看不到,直流电平也异常。驱动代码加了打印日志,SPI 发送流程正常执行,但芯片像死了一样。

原因:最常见的有三种。一是接口模式没选对,芯片还停在并行模式,SPI 帧被忽略。二是 UPDATE 引脚从来没拉过,寄存器只写进了临时区,没有生效。三是数据引脚接错,比如把 SDO 当 SDIO 用了。

解决:第一步量模式选择引脚电平,对照手册确认串行模式。第二步检查驱动里是否每次写寄存器后都调用了AD9854_PulseUpdate。第三步用万用表量 CS、SCK、SDIO 在写操作时是否有电平变化,SCK 和 SDIO 上的活动一眼就能看出来。我在调试时会在写寄存器前后加两个 GPIO 翻转,接逻辑分析仪看时序是否完整,比瞎猜效率高得多。

5.2 输出频率刚好是设定值的一半

现象:设置 10MHz,频率计读回来是 5MHz,但波形形状正常,幅度也正常。把设定频率改小,输出频率仍然严格维持一半关系。

原因:AD9854 内部系统时钟实际是参考时钟经过 PLL 倍频后的值。如果驱动里计算的 SYSCLK 值是 300MHz,而芯片内部的 PLL 倍频配置没有被正确写入,或者倍频系数被无意设到了默认值 1,实际系统时钟就是参考时钟本身,输出频率自然只有预期的一半。另一个可能的坑是参考时钟输入接法不对,REFCLKB 悬空导致内部输入幅度不足,芯片可能对时钟做了二分频。

解决:先确认参考时钟频率,用频率计量晶振输出,然后检查控制寄存器里 PLL 倍频配置确实写进去了。我一般在初始化函数里加一个写后读回的操作,虽然 AD9854 读回需要切换 SDIO 方向,但这一步能直接确认寄存器写入是否生效。如果倍频没问题,再看 REFCLKB 的耦合电容,把负端接 100nF 到地而不是悬空。

5.3 寄存器写入时好时坏,断断续续

现象:有时频率设置成功,有时 SPI 发完了却没生效,重新初始化一次又好了。波形上有时候还会出现突然跳到另一个频率的毛刺。

原因:SPI 极性和相位不匹配是最常见的原因,表现为大部分帧能成功但偶尔失败,因为边界采样点的建立保持时间不满足。第二个原因是 UPDATE 脉冲太窄,或者 UPDATE 与 SPI 帧传输之间没有留足时间,芯片内部状态机还在处理上一帧时来了新的更新请求。第三个原因是 CS 拉高后没有等待 SPI 忙标志清除,最后一字节被截断。

解决:SPI 模式确认改成模式 0,即空闲低电平、第一边沿采样;这是 AD9854 手册里最常用的配置。UPDATE 高电平时间从我代码里的 1μs 改到 10μs 也完全没有问题,别偷工减料。CS 拉高前必须等SPI_I2S_FLAG_BSY清除。这三件事都做对,这种玄学问题基本绝迹。

5.4 输出幅度异常,或者通道间串扰严重

现象:和正常板子对比,同一份固件在某块板上输出幅度明显偏小,或者 I 路设置频率后 Q 路输出也跟着变。

原因:幅度字寄存器没写或者写成了 0,输出静态电平没有信号。串扰常见原因是电源去耦差,特别是模拟电源和数字电源共用同一个 LDO 而没有分开滤波;也可能差分输出对地阻抗不平衡,导致共模信号窜到另一通道。驱动寄存器层面的串扰很少见,优先查硬件。

解决:先用满量程幅度字 4095 写入,确实输出幅度恢复正常,那就是软件漏写。如果幅度仍不对,查输出端电阻和变压器,把差分对的对地阻抗调平衡。电源方面,模拟电源引脚和数字电源引脚各自独立走线,每路都放 100nF 电容,问题基本能解决。

5.5 芯片上电后发烫,输出频率乱跳

现象:上电几分钟内芯片温度明显升高,输出频率不稳定,甚至出现频率漂移。

原因:参考时钟输入直流耦合,破坏了输入端的内部偏置网络,导致输入级工作点不对。另一个可能是供电电压超过手册绝对最大值,或者电源引脚去耦严重不足,内部电路自激振荡。

解决:断电检查参考时钟耦合方式,REFCLK 输入端必须有交流耦合电容,REFCLKB 按单端接法接电容到地。供电电压用万用表实测,不要相信 LDO 的标称值。如果去耦电容缺失,先就近补上,不要直接上电继续调试。遇到过两块板子同样代码,一块正常一块发烫,最后都是硬件差异,软件调不出这个结果。

6. 进阶:把驱动程序变成可用的信号源——扫频与验证技巧

扫频是 AD9854 驱动最常见的进阶需求。做法有两种,一类是芯片内部自带的 Chirp 模式,设置起始频率、结束频率和步进时间,芯片自动扫,MCU 完全不用管。另一类是 MCU 定时器驱动软件扫频,每隔一段时间更新一次频率字。我的习惯是:粗扫用软件扫频,频点变化节奏可控;精细线性扫描用 Chirp 模式,扫频速率均匀且不占 MCU。

软件扫频的关键参数是步进时间和频率步进值。步进时间不要小于寄存器写入加 UPDATE 生效的时间总和,实测 100μs 是一个可靠的边界;频率步进要按实际需要的分辨率设,太密集会导致扫频一周期时间过长。扫频代码就是把AD9854_SetFreq放进定时器中断回调里,每次加一个固定的频率增量,中断周期对应步进时间。需要保证频率精度时,浮点累加比整数累加更稳,因为浮点的舍入误差不会像整数那样被逐步放大。

验证驱动是否正常,三板斧足够。第一斧是频率计,直接验证输出频率与设定值的偏差,用公式回算是检验驱动计算是否正确的唯一标准。第二斧是示波器 FFT,看频谱上有没有不该有的杂散和镜像,能快速发现混叠问题。第三斧是李萨如图形验证正交性,把 I 路和 Q 路分别接到示波器的两个通道,X-Y 模式下如果看到稳定的圆形,说明两路相位差正确,正交链路没问题。做 IQ 信号源时这个验证无比重要。

我的习惯是每一块新板子到手,第一件事不写任何业务逻辑,只跑一遍初始化加固定 1MHz 输出,用频率计确认频率,再用示波器看波形形状。这一关过了,再往上加扫频、调幅度、改相位才有意义。顺序反过来的话,业务逻辑里的 bug 和硬件问题纠缠在一起,排查成本立刻翻倍,这个血泪教训我记了很久。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询