☰
AD9834驱动代码详解:SPI时序、频率字换算与常见故障排查
2026/10/14 11:59:34 网站建设 项目流程

简介:AD9834是一款可输出正弦波、方波与三角波的高性能DDS芯片,广泛应用于测量仪器、信号发生器和嵌入式教学项目。这份驱动压缩包体积仅约7KB,内含2个C源文件与2个头文件:源文件分别实现芯片初始化、频率/幅度/波形设定以及SPI或I2C通信读写,头文件则导出函数接口、寄存器地址和数据结构,方便快速移植到各类MCU平台。代码采用分层设计将驱动逻辑与硬件协议解耦,阅读后可掌握DDS芯片配置、串行总线时序和嵌入式驱动开发思路,也可直接集成到现有项目中生成精确模拟信号,并能根据嵌入式Linux、RTOS或裸机环境自行适配。目前已有1046人学习下载,适合具备一定C语言基础的硬件工程师、电子爱好者和相关专业学生入门参考。

1. 一块 AD9834 驱动代码,卡住你的通常是这 16 位帧

AD9834 驱动代码看起来只是几行 SPI 写寄存器,真正跑起来的人都知道,坑全藏在 16 位帧的位定义和 FSYNC 时序里。这颗 DDS 芯片只需要一个 SPI 主机就能输出正弦、三角、方波,频率分辨率在 25 MHz 主频下能做到 0.1 Hz 级别,所以信号源、扫频激励、频率可变的载波生成都喜欢用它。问题在于网上流传的驱动版本参差不齐,有的把寄存器选择位写反,有的在连续写频率字时让片选跳变,结果输出频率和设定值差出几倍甚至完全没有波形。这篇文章围绕一份最小可用的 AD9834 驱动代码展开,把寄存器映射、SPI 时序、频率字换算和常见故障讲清楚,新手能照着复制,熟手能直接对照排查。

2. 写驱动前先读寄存器映射:五种 16 位帧与 SPI 时序

AD9834 对外只认一种通信格式:16 位一帧,MSB 先行。芯片内部有三类寄存器:控制寄存器、两个频率寄存器、两个相位寄存器,不同寄存器靠每帧最高两位区分。驱动代码的核心,就是把这 16 位帧拼对。

2.1 五种 16 位帧:最高两位决定写哪里

控制寄存器、FREQ0、FREQ1、PHASE0、PHASE1 共用同一条 SPI 链路,区分方式只有帧头那两位。我习惯在驱动头文件里先把这些帧基地址定义好,后续所有函数都基于这些宏拼帧,可读性和可维护性都会好很多。

帧类型D15D14其余位含义帧基值
控制寄存器00D13~D0 为控制位0x0000
FREQ0 寄存器10D13~D0 为 14 位频率段0x8000
FREQ1 寄存器11D13~D0 为 14 位频率段0xC000
PHASE0 寄存器01D13=0,D12=0,D11~D0 为相位字0x4000
PHASE1 寄存器01D13=1,D12=0,D11~D0 为相位字0x6000

频率寄存器是 28 位宽度,一帧只有 14 位有效数据,所以写一个频率字必须发两帧:一帧写高 14 位,一帧写低 14 位。推荐先写高段再写低段,这个顺序在数据手册的典型操作序列里出现过,实际上反过来也能工作,但在两帧之间芯片处于中间状态时,先写高段能减少一次意外触发输出的窗口期。相位寄存器是 12 位宽度,一帧就能写完,剩下来 D12 位置 0,不要给它塞数据。

#define AD9834_CTRL 0x0000u #define AD9834_FREQ0 0x8000u #define AD9834_FREQ1 0xC000u #define AD9834_PHASE 0x4000u #define AD9834_PHASE_SEL (1u << 13)

PHASE0 和 PHASE1 共用 0x4000 这个基值,用 D13 区分,这就是为什么AD9834_PHASE_SEL单独拆一个宏。写相位时把AD9834_PHASE | (sel ? AD9834_PHASE_SEL : 0)作为帧头,再或上低 12 位相位字。

2.2 FSYNC 与 SCLK:连续两帧时别让片选跳变

AD9834 的 FSYNC 引脚承担片选职责,低有效。写一帧时先拉低 FSYNC,之后 SCLK 一个边沿移入一位,第 16 个 SCLK 结束后把 FSYNC 拉高。这里最容易翻车的点在于:写 28 位频率字需要连续两帧,规范时序要求 FSYNC 在整个传输期间保持低电平,直到第二帧最后一位移完再拉高。

如果 FSYNC 在两帧之间来了一个高脉冲,芯片会把第二帧当成一次新的访问。很多单片机的硬件 NSS 会在每次传输结束后自动释放片选,哪怕你的 SPI 数据长度配置成了 16 位,两次连续调用发送函数之间 NSS 也会产生一次跳变。结果是频率字只有一半被正确锁存,输出频率和设定值对不上,而且每次上电还不一样。解决方法是把 FSYNC 配成普通 GPIO,发送期间手动拉低,两帧发完再拉高,彻底绕开硬件 NSS 的自动行为。

SCLK 极性方面,AD9834 在 SCLK 上升沿采样数据,主机应配置为 CPOL=0、CPHA=0,也就是常说的 SPI Mode 0,MSB first。如果配成 Mode 1(CPHA=1),数据在下降沿被采样,MOSI 上的数据在上升沿附近发生跳变,采样窗口刚好落在数据变化点上,结果是偶发的错位乱码。GPIO 模拟 SPI 时同理,先置数据、再拉高 SCLK、再拉低 SCLK,严格按这个顺序推位。

2.3 控制寄存器里的六个位:RESET、SLEEP1、SLEEP12、OPBITEN、DIV2、MODE

控制寄存器一帧 16 位,D15、D14 为 0,剩下真正频繁操作的是 D13 到 D8 这六位。它们决定了芯片的工作模式、输出波形和功耗状态,初始化失败大多和其中某一位的默认值有关。

位名称置 1 效果清 0 效果
D13RESET内部逻辑复位,输出为零正常运行
D12SLEEP1关闭 DAC,IOUT 无输出DAC 正常
D11SLEEP12关闭内部 MCLK 时钟时钟正常
D10OPBITENVOUT 输出比较器方波VOUT 关闭
D9DIV2比较器输入为 MCLK/2比较器输入为 DAC 输出频率
D8MODEIOUT 输出三角波IOUT 输出正弦波

正常正弦输出时,控制字保持 0x0000 即可。上电初始化阶段,需要先写 0x2000(RESET 置 1),频率相位配置完成后写 0x0000 解除复位。很多人把运行时的控制字也写成 0x2000,结果芯片一直处于复位状态,IOUT 上看不到任何波形,这是最常见的初始化错误。

3. 最小可用的 AD9834 驱动代码:初始化、频率与相位设置

这一章直接给一份能落地的 C 驱动。代码分为四层:底层 SPI 发送、单帧写入、频率/相位封装、初始化序列。平台相关的部分只有一处,换 MCU 时改一个函数。

3.1 底层 SPI 帧函数:手动控制片选对后续太重要

/* 平台相关:向 SPI 移位寄存器发送 16 位数据,MSB first */ void spi_write_16(uint16_t data); /* 单帧写 AD9834:一帧结束后拉高 FSYNC */ void ad9834_write(uint16_t frame) { AD9834_FSYNC_LOW(); spi_write_16(frame); AD9834_FSYNC_HIGH(); }

spi_write_16是唯一需要根据平台改的函数,换成你 SDK 里的SPI_SendData16或者自己用 GPIO 模拟都可以。AD9834 的 FSYNC 低有效,拉低后主机开始发送,发送完 16 位再拉高,这是最小的一帧操作。注意 FSYNC 拉低到第一个 SCLK 沿之间要留一点建立时间,数据手册要求的是纳秒级,GPIO 翻转本身的延迟通常就够,但如果你的 SPI 时钟跑得很高,建议在拉低后加一个空操作延时。

3.2 写频率寄存器与相位寄存器:两帧连续发送的封装

void ad9834_write_freq(uint16_t freq_reg, uint32_t freq_word) { uint16_t hi = (uint16_t)(freq_reg | ((freq_word >> 14) & 0x3FFFu)); uint16_t lo = (uint16_t)(freq_reg | (freq_word & 0x3FFFu)); AD9834_FSYNC_LOW(); spi_write_16(hi); /* 先写高 14 位 */ spi_write_16(lo); /* 再写低 14 位,FSYNC 保持低 */ AD9834_FSYNC_HIGH(); } void ad9834_write_phase(uint8_t phase_sel, uint16_t phase_word) { uint16_t frame = AD9834_PHASE | (phase_sel ? AD9834_PHASE_SEL : 0u) | (phase_word & 0x0FFFu); ad9834_write(frame); }

freq_reg传AD9834_FREQ0或AD9834_FREQ1,不能传控制寄存器地址。freq_word是 28 位无符号数,函数把它拆成高 14 位和低 14 位各占一帧,两帧之间 FSYNC 不抬高,保证 28 位数据被一次性锁存。ad9834_write_phase里phase_sel选 PHASE0 或 PHASE1,phase_word低 12 位有效,0~4095 对应 0° 到 360°。相位字的数值是 12 位,超过 4095 会被掩码丢掉,调用前最好自己做范围检查。

3.3 上电初始化序列:先复位再解除,延时别省

void ad9834_init(uint32_t freq_word, uint16_t phase_word) { /* 1. 复位,让内部逻辑进入确定状态 */ ad9834_write(AD9834_CTRL | AD9834_RESET); /* 2. 配置 FREQ0 与 PHASE0,此时 DAC 尚未输出 */ ad9834_write_freq(AD9834_FREQ0, freq_word); ad9834_write_phase(0, phase_word); /* 3. 解除复位,DAC 开始输出 */ ad9834_write(AD9834_CTRL); delay_ms(10); }

三步顺序不能乱。上电后芯片默认处于复位态,先写 RESET 置 1 是为了让内部逻辑进入确定状态;接着配置频率和相位,此时 DAC 虽然没输出,但寄存器已经准备好;最后把 RESET 清 0,DAC 开始正常转换。延时 10 ms 是保守值,大多数模块 1 ms 就够,但初始化只做一次,多等几毫秒换可靠性很划算。省掉这个延时,第一次输出可能带着异常瞬态,对于需要干净波形的场景会很难看。运行时要改频率,直接调用ad9834_write_freq就行,不需要再走一遍复位流程。

3.4 主程序调用示例:先算字,再初始化

#define FMCLK_HZ 25000000.0 int main(void) { spi_init(); /* Mode 0, MSB first */ gpio_fsync_init(); /* FSYNC 引脚为 GPIO 输出 */ uint32_t fword = ad9834_calc_freq_word(10000.0, FMCLK_HZ); ad9834_init(fword, 0); /* 10 kHz 正弦波,初相 0° */ while (1) { fword = ad9834_calc_freq_word(12345.0, FMCLK_HZ); ad9834_write_freq(AD9834_FREQ0, fword); delay_ms(500); } }

ad9834_calc_freq_word的定义放在第四章,先在这里用起来。注意运行时改频率直接写 FREQ0 即可,如果每次改频率都把 RESET 打开,输出会中断一下,错过这个坑后面会省很多事。

4. 频率字与相位字计算:25 MHz MCLK 下的参数怎么设

DDS 的调频本质是调分频比,AD9834 内部用 28 位累加器对主时钟 MCLK 做相位累加,输出频率和频率字之间是线性关系。这一章把公式、查表、边界条件讲透。

4.1 频率公式与取整:2^28 与 MCLK 的关系

输出频率 fout 的计算公式为:

fout = FREQREG × MCLK / 2^28

其中 FREQREG 是写入频率寄存器的 28 位无符号整数。反过来,给定目标频率求频率字:

FREQREG = fout × 2^28 / MCLK

在 25 MHz 主频下,频率分辨率是 25000000 / 268435456 ≈ 0.0931 Hz,也就是说 0.1 Hz 级别的步进确实能做到。频率字换算时不能直接整数除法截断,要四舍五入,否则 1 kHz 会差出接近 1 Hz 的误差,在某些测量场景不可接受。

uint32_t ad9834_calc_freq_word(double freq_hz, double mclk_hz) { double raw = (freq_hz * 268435456.0) / mclk_hz; return (uint32_t)(raw + 0.5); }

268435456.0是 2^28,加 0.5 再转整型相当于四舍五入。这个函数返回的只是频率字,不负责检查上限。当输入频率超过 MCLK/2 时,raw 会超过 0x0FFFFFFF,输出会出现频谱混叠,实际波形不再是你想要的正弦。工程上一般建议输出频率不超过 MCLK 的 40%,留出抗混叠滤波器的过渡带,否则高次谐波会非常难滤。

4.2 常用目标频率的字表:25 MHz 与 75 MHz 各一份

很多 AD9834 模块的主时钟是 25 MHz,也有部分板卡为了更高输出频率把 MCLK 做到 75 MHz。两种主频下,同样目标频率对应的频率字差别很大,写死任意一个表都是隐患。建议把 MCLK 做成宏,换板卡只改一处。

目标频率25 MHz 下频率字75 MHz 下频率字
100 Hz1074358
1 kHz107373579
10 kHz10737435791
100 kHz1073742357914
1 MHz107374183579139
10 MHz10737418235791394

这张表可以直接当调试参考。比如你在 25 MHz 板卡上设置 10 kHz,频率字应该是 107374,如果算出来是个完全不同的数字,先回头检查 MCLK 宏是否写错。注意 75 MHz 下 10 MHz 输出已经接近 MCLK 的 13%,尚在可用范围,但如果继续往上输出,谐波会明显增大。

4.3 相位字与波形配置:正弦、三角、方波的寄存器组合

相位字是 12 位,0~4095 均匀分布在一个圆周上,相位角和相位字的关系是:

angle = 2π × PHASEREG / 4096

常用相位直接查表:0° 对应 0,90° 对应 1024,180° 对应 2048,270° 对应 3072。两个相位寄存器配合 PSEL 引脚可以在输出过程中做相位跳变,适合 PSK 调制。波形配置则通过控制寄存器那六位组合实现。

目标波形IOUT 引脚输出需要追加的控制位控制字
正弦波正弦无0x0000
三角波三角MODE0x0100
方波,频率=fout被禁用OPBITEN0x0400
方波,频率=MCLK/2被禁用OPBITENDIV2

方波输出时 IOUT 被禁用,VOUT 引脚才是输出。DIV2 这个位很多人会忽略,它改变的是比较器输入频率:置 1 时 VOUT 输出 MCLK/2 的方波,置 0 时 VOUT 输出和 DAC 同频的方波。想要 fout 方波,控制字必须保持 0x0400,多加一个 DIV2 就会翻车。

5. AD9834 驱动常见问题排查:输出没有、频率翻倍、SPI 错位

驱动代码跑不通时,不要先怀疑芯片坏了。AD9834 引脚少、模式固定,绝大多数问题出在寄存器值、FSYNC 时序和外部匹配上。下面五条是实际调试中最常见的场景。

5.1 IO 口配置不对:初始化后完全没有波形

现象:初始化函数执行完,IOUT 和 VOUT 接示波器都看不到信号,示波器上是一条平线。

原因:最常见的是 FSYNC 用了 SPI 硬件 NSS,发送完 16 位后 NSS 自动拉高,连续写频率字的两帧被打断,芯片收到的寄存器值不对。其次是 SPI 引脚复用配置错误,MOSI 和 SCLK 没有正确映射到芯片引脚。还有一种可能是芯片一直处于 RESET 状态,控制字里 RESET 位没有清 0。

解决:把 FSYNC 改为普通 GPIO 手动控制,确认发送频率字时两帧之间 FSYNC 保持低电平。用示波器量 SCLK、MOSI、FSYNC 三根线的电平逻辑,静态时 FSYNC 应为高,发送时拉低。最后检查控制寄存器序列,确认最后一步写的是 0x0000 而不是 0x2000。

5.2 输出幅度异常小:IOUT 是电流输出不是电压输出

现象:波形出来了,但幅度只有几十毫伏,调整频率字和相位字都没有改善。

原因:IOUT 引脚输出的是 DAC 电流,不是电压。AD9834 内部 DAC 需要外部端接电阻或运放做电流转电压,直接接示波器高阻探头,电流没有完整回路,幅度被压到几乎看不到。

解决:在 IOUT 对 AGND 之间接一个电阻,常见从 200Ω 到 1kΩ 都可以,用示波器测电阻两端的电压。电阻越大幅度越高,但受 DAC 输出摆幅限制,太大容易削顶。我的习惯是先接 510Ω 看波形,确认频率正确后再按目标幅度调整。如果要接入后端电路,用运放做 I/V 转换更规范,失调和带宽都要比纯电阻好。

5.3 频率是设定值一半:DIV2 与 MCLK 双重怀疑

现象:设置 10 kHz,IOUT 输出是 5 kHz,波形干净且频率稳定不跳。

原因:两个方向都要查。控制字里 DIV2 置位只会影响 VOUT 方波路径,IOUT 输出不对,最大嫌疑是 MCLK 实际频率和代码里的宏不一致。比如板上晶振丝印是 25 MHz,实际焊了 12.5 MHz;或者你按 75 MHz 算,实际主时钟只有 37.5 MHz。

解决:用示波器量 AD9834 MCLK 引脚的输入频率,把实测值代回ad9834_calc_freq_word重新计算。别信板卡丝印,也别信代码注释,量出来才是真的。如果量出来确实是 25 MHz 但输出仍减半,检查控制字里有没有多写 DIV2 位,VOUT 和 IOUT 分开测,两个引脚的表现不同。

5.4 改频率不生效:写错寄存器编号或 FSYNC 被打断

现象:初始化输出正常,运行时调用更新函数改频率,输出纹丝不动,频率完全没有变化。

原因:最常见的是把 FREQ1 当成了 FREQ0,但没去选择 FREQ1 对应的输出寄存器。AD9834 有 FSELECT 引脚控制输出选 FREQ0 还是 FREQ1,如果这个引脚悬空或默认选了 FREQ0,你写 FREQ1 自然不生效。其次是写频率字时 FSYNC 被硬件 NSS 拉高后再复位,28 位数据只写进去一半,寄存器值变成未知。

解决:确认代码里ad9834_write_freq的第一个参数传的是AD9834_FREQ0(0x8000),不要误传控制寄存器地址。再看 FSYNC 波形,两帧之间应该是一条连续低电平。最后检查 FSELECT 引脚的实际电平,确认和你要写的寄存器编号一致。

5.5 SPI 模式配错导致乱码:Mode 0 的边界条件

现象:能输出波形但频率完全不对,和设定值毫无关系,每次重新上电后还不一样,看起来像随机数。

原因:SPI 配成了 Mode 1,CPOL=0、CPHA=1,数据在下降沿被采样,和 AD9834 的上升沿采样错开半个时钟周期,导致部分位被读错。GPIO 模拟 SPI 时如果先拉高 SCLK 再放数据,同样会造成数据建立时间不足。

解决:主机侧配置 CPOL=0、CPHA=0、MSB first。GPIO 模拟时严格按「先置数据、再拉高 SCLK、再拉低 SCLK」的顺序推位。用示波器同时看 MOSI 和 SCLK,确认数据在 SCLK 上升沿之前已经就位,如果 MOSI 在上升沿附近还在跳变,说明时序有问题。

6. 进阶技巧:用 FSELECT 做 FSK,用定时器做线性扫频

驱动跑通之后,AD9834 有两个很实用的进阶玩法值得试。

6.1 FSK:两个频率寄存器加一根引脚

AD9834 内部有 FREQ0/FREQ1 两个独立频率寄存器,物理引脚 FSELECT 负责在两者之间做硬件选择。这意味着初始化阶段把两个频率字分别写入,运行时只翻转 FSELECT 的 GPIO 电平,输出频率就在两个载频之间跳变,不需要 SPI 参与。相比在中断里逐次重写频率字,这种方式不占用总线、不阻塞 CPU,跳变延迟基本等于引脚电平翻转时间,适合 FSK 调制一类需要快速切换的场景。

/* 初始化阶段预置两个载频 */ ad9834_init(fword_lo, 0); ad9834_write_freq(AD9834_FREQ1, fword_hi); /* 运行时:按基带数据翻转 FSELECT,电平决定输出 FREQ0 还是 FREQ1 */ fselect_pin_write(bit_value);

某个电平对应哪个寄存器,取决于模块上 FSELECT 引脚的实际接法。通电后先各写一个不同频率,测一下输出再定逻辑,不要盲目相信注释里的极性。

6.2 定时器中断里做线性扫频

另一种常见需求是扫频激励,比如从 1 kHz 扫到 10 kHz 测系统幅频响应。直接在定时器中断里改频率字就能实现:

void sweep_tick_1ms(void) { static double freq = 1000.0; freq += 50.0; /* 每毫秒步进 50 Hz */ if (freq > 10000.0) freq = 1000.0; ad9834_write_freq(AD9834_FREQ0, ad9834_calc_freq_word(freq, FMCLK_HZ)); }

扫频步进和更新周期要根据 MCLK 和输出频段调整。AD9834 更新一次频率字需要两帧共 32 个 SCLK 周期,按 1 MHz SCLK 算是 32 微秒,中断里调用会阻塞一小段时间,放在 1 ms 定时器里没问题。如果 SCLK 只有几百 kHz,扫频周期就要拉长。步进太大波形会有明显台阶感,步进太小扫完一遍太慢,这个看具体需求调试。

之前某项目里我用 AD9834 做扫频激励源,就是被 DIV2 这个位折腾了半天,最后把示波器打在 VOUT 上才发现方波频率对不上。这类芯片驱动不难,难的是把每一个位的后果想清楚,遇到问题先回到寄存器定义本身,比换一版驱动更有用。希望帮到你。

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

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

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

立即咨询