基于STM32的AD9833可编程波形发生器驱动开发与调试指南
2026/9/12 18:20:48 网站建设 项目流程

这几天有个项目要用到可编程波形发生器,我在 AD9833 和 AD9834 之间反复纠结了几轮,最后选了 AD9833。这芯片在嵌入式圈子里实在经典:三线 SPI 控制、内置 28 位频率寄存器、DDS 原理实现正弦波/三角波/方波输出,一颗芯片解决信号源问题。这篇东西我想把它聊透——从芯片手册怎么读,到频率怎么算,再到驱动代码怎么写,最后还有我在实际调试中踩过的一堆坑。只要你手里有块 STM32 开发板和一颗 AD9833,照着这篇文章走一遍,基本就能把波形点出来。


1. 为什么选 AD9833:DDS 方案背后的取舍

1.1 相位累加器:先把 DDS 原理一句话讲清楚

很多人第一次接触 DDS 时会被“直接数字频率合成”这个名词唬住,其实拆开看并不复杂。DDS 的核心是一个相位累加器加一张波形查找表:每个时钟周期,累加器把频率控制字累加一次,累加结果的高位作为查表地址,查出来的幅度值送给 DAC 输出,就得到了连续波形。相位累加器每溢出一次,就代表正弦波走完了一个完整周期。频率控制字越大,累加器溢出越快,输出频率就越高。

这个结构最大的好处是频率切换能做到“相位连续”。你改频率字,只是在改累加速度,当前波形走到哪儿还是从哪儿继续走,不会出现像直接切换分频器那样波形断层。这也是 DDS 能横扫信号源领域的原因之一。AD9833 内部就是这套结构:28 位相位累加器、正弦查找表、10 位 DAC,外加一个 SPI 从机接口,封装小、功耗低,非常适合嵌入式手持设备或板级信号源。

1.2 AD9833 在 DDS 芯片里到底是什么定位

ADI 的 DDS 产品线很多,从低频到射频都有。AD9833 属于完全集成的低频段 DDS 芯片,主频支持最高 25 MHz,输出频率最高可以做 12.5 MHz 左右,但实际用到几 MHz 以内比较稳。它内置 10 位 DAC,输出正弦波、三角波,也能输出方波——方波不是 DAC 出来的,是内部比较器把 MSB 引出来的。

同系列里还有 AD9834,比 9833 多了个内置比较器和一些调制功能;AD9850/AD9851 则是高频段产品,最高能到几十上百 MHz,但外围更复杂。选型时如果只是做音频扫频、传感器激励、简易函数信号发生器,9833 完全够用。如果后面要一路输出幅度更大的驱动信号,那再加一级运放就够了,没必要一开始就上高成本芯片。我当时选它的理由很直接:资料多、例程多、价格相对透明,遇到问题随便一搜就有前人踩坑记录。对于嵌入式开发来说,生态成熟本身就是巨大的隐形成本节约。

1.3 什么场景适合 AD9833,什么场景不适合

说实话,每颗芯片都有它擅长和不擅长的地方。AD9833 适合的场景包括:低频信号源、VCO 替代、扫频激励源、生物电信号模拟、传感器激励、音频测试等。它不适合的典型场景有两个:一是高精度幅度控制,因为它是电流型 DAC 输出,外部要搭运放,幅度稳定性受参考电压和运放温漂影响;二是需要频率和相位严格同步的多通道信号,9833 只有一路输出,要同步就得用多片加同步机制,比较麻烦。

另外注意它的输出不是轨到轨的电压信号,IOUT 引脚输出的是电流,通常需要接电阻转成电压,再接运放缓冲或放大。很多新手一上来直接用示波器探针点 IOUT,发现波形幅度只有几百毫伏就开始怀疑芯片坏了,其实不是那么回事。这个我在后面调试部分会详细说。


2. 芯片手册该看什么:引脚、寄存器与 SPI 时序

2.1 引脚说明与最小系统搭建

AD9833 是 10 引脚 MSOP 封装,非常小,焊的时候要小心。引脚功能其实很简单:VDD 供电、DGND/AGND 地、MCLK 接时钟输入、FSYNC 是帧同步信号(相当于片选)、SCLK 是 SPI 时钟、SDATA 是 SPI 数据输入,还有 IOUT 是 DAC 电流输出,VOUT 是内部比较器输出。

这里要专门说一下 MCLK。AD9833 没有内置振荡器,必须由外部提供时钟源。最常见做法是在 MCLK 和 DGND 之间接一个 25 MHz 晶振,再加上两个负载电容。也可以用外部有源晶振直接把时钟信号送进 MCLK。如果你的 MCU 引脚资源富余,还有一种更灵活的方式:用 MCU 的定时器输出或 PWM 直接产生 MCLK 时钟信号。好处是可以软件调主频,坏处是时钟抖动比较明显,高频输出时频谱纯度会变差。对大多数低频应用无所谓,但如果你要输出比较干净的正弦波,老老实实用晶振。

最小系统搭建时,除了晶振,还要注意电源去耦。VDD 引脚旁边放一个 0.1 μF 陶瓷电容,再并一个 1 μF~10 μF 钽电容,电源走线尽量短,AGND 和 DGND 建议单点汇合再连到主地。我做第一版 PCB 时贪图方便,AGND 和 DGND 直接大面积连在一起,结果输出波形上叠了一层数字噪声,折腾了好久才想到是地分割的问题。

2.2 寄存器映射:控制字、频率字、相位字

AD9833 的寄存器空间一共就四类:控制寄存器、频率寄存器 0、频率寄存器 1、相位寄存器 0、相位寄存器 1。所有通信都是 16 位字,SPI 先发高位字节。芯片通过 16 位字的高两位(D15、D14)来识别写的是哪类寄存器,这个映射关系必须背下来。

我用一张表帮大家整理清楚:

寄存器D15:D14数据位说明
控制寄存器00D13~D0 控制位配置 B28、RESET、输出模式等
频率寄存器 010D13~D0 频率字低/高 14 位分两次写入,共 28 位
频率寄存器 111D13~D0 频率字低/高 14 位分两次写入,共 28 位
相位寄存器 001D13 选寄存器,D12 保留,D11~D0 相位数据12 位相位字
相位寄存器 101D13 选寄存器,D12 保留,D11~D0 相位数据12 位相位字

控制寄存器里常用位包括:B28(D13)决定频率寄存器是一次写两个 16 位字还是单次只写一半;RESET(D9)复位内部逻辑,置 1 时 DAC 输出为 0,写频率前建议先置 1;OPBITEN(D6)选择输出模拟波形还是数字方波;MODE(D2)选择正弦还是三角波。

频率字是 28 位,分两次写。B28=1 时,第一次写入的 14 位是低字节,第二次是高字节,顺序不能反。B28=0 时,你需要手动通过 HLB(D12)来指定当前写的 14 位是高字节还是低字节。日常开发建议直接用 B28=1 模式,省心很多。

2.3 SPI 时序:模式选择与字节序坑

AD9833 的 SPI 接口是典型的三线从机:SCLK、SDATA、FSYNC。手册时序图显示,FSYNC 拉低后开始传输一个 16 位字,SCLK 上升沿采样数据,FSYNC 拉高结束传输。很多资料说它是 SPI Mode 2,也就是 CPOL=1、CPHA=1,我自己在 STM32 上用 Mode 2 跑得非常稳。也有部分开发板用 Mode 1(CPOL=0、CPHA=1)能工作,但时序裕量会差一些。

SCLK 最高可以到 40 MHz,理论值跑得很快,但实际用 SPI 从机往往不取决于芯片极限,而取决于你的 PCB 走线、引脚内部上拉、以及 MCU 的 GPIO 翻转速率。我第一次调试时直接把 SPI 分频配成 4,在面包板上飞线连接,结果十次通信有三次丢字。后来把分频降到 64,SPI 时钟压到 500 kHz 左右,立刻就稳定了。等确认逻辑没问题再把分频往回升,这是嵌入式调试的基本操作。

字节序这里要特别提醒。AD9833 要求先发高字节,再发低字节。STM32 的 HAL 库在发送多字节数据时是按数组顺序发送的,所以必须手动组装成高字节在前。我见过不少人直接用(uint8_t *)&data把 uint16_t 指针强转成字节指针发送,这种写法在小端模式下会先发低字节,芯片收到的完全是乱的。后面驱动代码部分我会给出正确的做法。


3. 频率计算详解:从公式到寄存器值

3.1 频率分辨率与 28 位频率字

频率计算是整个驱动里最容易出错也最核心的环节。AD9833 输出频率的公式非常简洁:

fout = (fMCLK × FREQREG) / 2^28

其中 FREQREG 是 28 位频率寄存器的值,范围 0 到 268435455,fMCLK 是主时钟频率。分母 2^28 是相位累加器的模数,这个模数固定不变,所以频率分辨率完全由主时钟决定。

如果采用标准 25 MHz 晶振,分辨率就是 25000000 / 268435456 ≈ 0.0931 Hz。也就是说,你设置频率的最小步进不到 0.1 Hz。这个精度对绝大多数应用是绰绰有余的。值得注意的是,频率越高,量化误差占比越小,低频时反而要小心取整误差。比如你要输出 1 Hz,算出来的频率字约等于 10.74,取整成 10 或 11 都会带来百分之几的偏差,这在超低频测试场景可能不可接受。解决办法是提高 MCLK 频率,或者对输出频率做软件校准。

3.2 目标频率反推寄存器值:10 kHz 实例

驱动代码里实际写的是反推过程:已知目标频率 fout 和主时钟 fMCLK,求 FREQREG。公式变形:

FREQREG = (fout × 2^28) / fMCLK

拿一个具体例子走一遍:主时钟 25 MHz,目标输出 10 kHz 正弦波。

FREQREG = (10000 × 268435456) / 25000000 = 107374.1824

取整得到 107374,换算成十六进制是 0x1A36E。这个数要拆成两个 14 位段写入:

  • 低 14 位:107374 & 0x3FFF = 0x236E(十进制 9070)
  • 高 14 位:(107374 >> 14) & 0x3FFF = 0x0006(十进制 6)

拆完后先写低 14 位,再写高 14 位。为什么这么拆?因为 28 位寄存器在 SPI 传输中要拆成两个 16 位字,每个字只有 D13~D0 是有效数据,高两位 D15、D14 用来标识写的是哪个寄存器。如果你先写了高 14 位再写低 14 位,芯片会认为先收到的是低字节,最终频率完全不对。这个顺序错误属于高频事故,排查时一定要先检查。

3.3 相位计算与双频点切换

相位寄存器是 12 位,满量程 4096 对应 360 度。所以相位角度转换成寄存器值的公式是:

PHASEREG = (相位角 / 360) × 4096

比如 90 度就是 1024,180 度就是 2048。这个映射很简单,几乎不会出错。真正有用的是双频点切换功能——AD9833 有两个频率寄存器 FREQ0 和 FREQ1,两个相位寄存器 PHASE0 和 PHASE1,通过控制字里的 FSELECT 和 PSELECT 位来切换。

这个功能在扫频应用里很好用。比如你需要输出 A 频率和 B 频率交替切换,可以先把 FREQ0 和 FREQ1 都配置好,然后在运行时只改 FSELECT 位,不需要重新写频率字。切换速度比重新写 SPI 快得多,而且相位连续性更好。同理,PSELECT 可以在两个预设相位间切换,做 BPSK 调制或者相位跳变测试很合适。


4. 驱动实现:从零写一个 AD9833 驱动

4.1 CubeMX 里的 SPI 配置

我用 STM32F103 做示例,CubeMX 里配置 SPI 时把关键参数设置成下面这样:

  • SPI Mode:Transmit Only Master
  • Data Size:8 bit
  • Clock Polarity:High(CPOL=1)
  • Clock Phase:2 Edge(CPHA=1)
  • NSS:软件控制,不用硬件 NSS
  • Baud Rate Prescaler:先配 64 分频,稳定后再提速
  • First Bit:MSB First

关于数据位宽,我强烈建议配 8 bit 而不是 16 bit。AD9833 明明是 16 位字协议,但配 8 bit 模式下用两个字节拼接反而更直观,代码里对高低字节的控制更明确,调试时打断点也容易看。16 bit 模式在 HAL 库里发数据时还要处理字节序和 DMA 对齐问题,没必要自找麻烦。

CS 引脚我直接选一个普通 GPIO 输出口,CubeMX 里配成 GPIO_Output,推挽输出,初始电平拉高。注意 FSYNC 这个引脚的时序要求是:在写入过程中必须保持低电平,写完一个 16 位字后再拉高。如果你用硬件 SPI 自动切换 NSS,反而可能出现片选时序对不上的情况,软件 GPIO 控制最稳妥。

4.2 核心函数:写寄存器、设频率、设相位

先写最底层的寄存器写入函数。这个函数是所有操作的基础,核心就是组装字节序加控制 CS 时序:

void ad9833_write_16bit(uint16_t data) { uint8_t buf[2]; buf[0] = (data >> 8) & 0xFF; // 先发高字节 buf[1] = data & 0xFF; // 再发低字节 AD9833_CS_LOW(); HAL_SPI_Transmit(&hspi1, buf, 2, 10); AD9833_CS_HIGH(); }

注意HAL_SPI_Transmit的第三个参数是数据长度,这里传 2 表示字节数,不是位宽。有些人的习惯是封装一层抽象层,把所有寄存器操作都通过这一层走,这样后续换 MCU 平台时只需要改这一个函数。

然后是设置频率函数。这个函数接收一个 28 位频率字,自动拆分并写入指定频率寄存器:

void ad9833_set_frequency(uint32_t freq_word, uint8_t freq_sel) { uint16_t base = freq_sel ? AD9833_FREQ1_REG : AD9833_FREQ0_REG; // 写入前先拉高 RESET,避免产生中间频率 ad9833_write_16bit(AD9833_CTRL_B28 | AD9833_CTRL_RESET); // 先写低 14 位,再写高 14 位 ad9833_write_16bit(base | (freq_word & 0x3FFF)); ad9833_write_16bit(base | ((freq_word >> 14) & 0x3FFF)); // 解除 RESET,恢复正常输出 ad9833_write_16bit(AD9833_CTRL_B28); }

注意这里有几个细节。第一,写频率字前后都写了控制字,先置位 RESET,再清除 RESET。为什么?如果不先置位 RESET,芯片在低 14 位写入成功、高 14 位还没写的窗口期会输出一个中间频率,这在扫频或精确切换频率的场景会造成明显毛刺。先 RESET 让 DAC 输出归零,写完两个字节后再解除 RESET,输出就是干净的目标频率。第二,freq_sel决定写 FREQ0 还是 FREQ1,如果只需要一路输出,保持 freq_sel=0 就行。

相位和波形模式设置函数也不复杂:

void ad9833_set_phase(uint16_t phase_word, uint8_t phase_sel) { uint16_t base = phase_sel ? AD9833_PHASE1_REG : AD9833_PHASE0_REG; ad9833_write_16bit(base | (phase_word & 0x0FFF)); }

相位字只有 12 位,直接取低 12 位拼上寄存器基址即可。

4.3 完整初始化流程与波形输出

初始化流程建议走这样一个顺序:先硬件拉高 CS,延时等待电源稳定;然后写控制字,把 B28 置 1、RESET 置 1,进入频率寄存器写入模式;接着写 FREQ0 和 FREQ1 的初始频率字,再写 PHASE0 和 PHASE1 的初始相位;最后写一个解除 RESET 的控制字,让芯片开始输出。

void ad9833_init(void) { AD9833_CS_HIGH(); HAL_Delay(10); // 控制字:B28=1,RESET=1 ad9833_write_16bit(AD9833_CTRL_B28 | AD9833_CTRL_RESET); // 写频率:FREQ0 = 10 kHz,FREQ1 = 1 kHz ad9833_set_frequency(107374, 0); // 10 kHz @ 25 MHz ad9833_set_frequency(10737, 1); // 1 kHz @ 25 MHz // 写相位:PHASE0 = 0 度,PHASE1 = 90 度 ad9833_set_phase(0, 0); ad9833_set_phase(1024, 1); // 解除 RESET,选择 FREQ0、PHASE0,输出正弦波 ad9833_write_16bit(AD9833_CTRL_B28); }

这里要注意 1 kHz 对应的频率字计算:1000 × 268435456 / 25000000 = 10737.41824,取整 10737,误差不到 0.05%。如果你要求频率特别准,可以反算实际输出频率再做软件校准。解除 RESET 时顺便把 FSELECT 和 PSELECT 都设为 0,这样默认走 FREQ0 和 PHASE0。

波形输出类型通过 MODE 位控制。MODE=0 输出正弦波,MODE=1 输出三角波。输出方波需要把 OPBITEN 置 1,从 VOUT 引脚引出,波形极性由 SIGN/PIN 位决定。平时常用的正弦波配置,控制字保持 B28=1 就够了。


5. 调试实录:波形不对、频率不准、没输出怎么办

5.1 常见问题速查表

我把调试中遇到的问题整理成了一张表,按排查优先级排列:

现象大概率原因检查方法
IOUT 无波形,输出直流电平RESET 位仍为 1,或 DAC 掉电检查控制字 D9、D7 位
频率完全不匹配频率字高低 14 位写反,或 SPI 字节序反了用逻辑分析仪抓 SPI 报文逐位核对
波形有毛刺或频率跳变写频率字过程中未置位 RESET写频率前先发 RESET=1 控制字
输出幅度异常小未接 IOUT 负载电阻,或后端缺少运放在 IOUT 接 200Ω 电阻到 AGND
波形是三角波而非正弦波MODE 位被置 1检查控制字 D2 位
输出是方波而非正弦波OPBITEN 被置 1检查控制字 D6 位
SPI 通信偶发丢字SCLK 太快或 CS 时序不稳降低 SPI 分频,检查 CS 拉低/拉高时序

第一个要排查的永远是 SPI 报文本身。用手头的逻辑分析仪抓 FSYNC、SCLK、SDATA 三根线,对照手册时序图检查每一帧 16 位数据的对齐关系。我的经验是,90% 的“芯片不工作”最终都指向控制器发出的报文和预期不一致,而不是芯片本身坏掉。

5.2 我实际踩过的坑

第一个坑就是字节序。我第一次调的时候直接用HAL_SPI_Transmit(&hspi1, (uint8_t *)&data, 2, 10),结果输出频率怎么都不对。后来抓波形一看,数据线上发的字节顺序反了,AD9833 先收到了低字节。从那以后我所有 SPI 器件都统一用字节数组手动组装,再也不信侥幸。

第二个坑是 IOUT 输出。我把示波器探头直接点在 IOUT 引脚上,看到的是一个幅度很低且带直流的奇怪波形,一度以为是芯片坏了。后来翻手册才发现 IOUT 是电流输出,需要外接负载电阻把电流转成电压。手册推荐的是 200Ω 电阻到 AGND,然后再接一级运放做缓冲或增益。搞清楚这一点后,波形立刻就正常了。

第三个坑是最隐蔽的——复位位没有解除。初始化代码里写了AD9833_CTRL_B28 | AD9833_CTRL_RESET之后,后面再写频率字base | (freq_word & 0x3FFF)时,这个操作并不会改变 RESET 位的值。如果你最后忘记写一个清除 RESET 的控制字,芯片会一直处于复位状态,IOUT 输出一直为零。这个问题最坑的地方在于,SPI 通信抓包看全是正常的,寄存器也写了,但输出就是没有。我当时查了半天,最后用逻辑分析仪逐位对比才发现控制字的 D9 位一直是 1。所以初始化流程里,清除 RESET 的那一步必须显式写出来,宁可多写几条控制字,也不要省。

第四个坑是关于参考电压和运放的。AD9833 的满量程电流大约是 3 mA 量级,接 200Ω 电阻后电压幅度在 0 到 0.6V 左右,是单极性信号。如果你需要双极性、更大幅度的输出,后级就得加运放做偏置和放大。这个在项目一开始就要规划好,不然做出来的信号源输出范围根本满足不了需求。

5.3 关于主时钟和频率精度的经验

还有一个容易被忽略的点:AD9833 的频率精度完全取决于 MCLK。公式里的 fMCLK 是实际主时钟频率,不是标称值。普通 25 MHz 贴片晶振的精度通常是 ±20 ppm,算下来 10 kHz 输出可能偏差 0.2 Hz,低频段无所谓,但如果做高精度扫频源,建议用温补晶振或者精度等级更高的晶振。

另外,如果你像前面说的那样用 MCU 的 PWM 或定时器输出做 MCLK,那么主时钟的精度和抖动就取决于 MCU 内部时钟,和晶振方案没法比。只适合做功能验证,不适合做精度要求高的产品。我后来在项目里直接把 25 MHz 有源晶振焊到 AD9833 的 MCLK 旁边,输出的频谱干净很多,频率计读数也稳定了。

最后分享一个小技巧:调试时写一个频率扫描函数,比如从 100 Hz 扫到 100 kHz,每步 100 Hz,在示波器上观察波形变化。这个操作可以同时验证 SPI 通信、频率寄存器写入逻辑、以及后级运放的带宽,比单点测试更能暴露问题。扫频发现某个频点波形突然失真,多半是后级运放摆率不够;如果扫频过程波形连续、幅度保持,那整套链路基本就没问题了。

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

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

立即咨询