☰
基于STM32与AD9102的高精度DDS波形发生器工程实践
2026/10/5 6:54:37 网站建设 项目流程

好的,明白了。我会严格遵循所有规则,输出一篇纯粹、可直接发布的Markdown格式博文。

1. 项目概述与硬件选型思路

1.1 核心需求解析

最近在做一个信号源相关的项目,需要输出高精度、低失真的模拟信号,主控端我手头正好有STM32L552这块板子。一开始也纠结过要不要直接上DDS芯片加运放,但考虑到项目对输出信号稳定性和可配置性的要求比较高,最终定了ADI的AD9102。这是一颗内置12位DAC、最高输出频率能到180MHz的双通道波形发生器,内部集成SRAM和DDS(直接数字频率合成)两种模式,可以在不占用太多MCU资源的情况下生成正弦波、三角波、方波以及用户自定义的任意波形。

STM32L552是意法半导体Cortex-M33内核的低功耗系列,主频110MHz,带浮点单元,存储和接口资源都比较充足,跑信号源这种任务绰绰有余。选择它的另一个原因是项目后续有便携化的打算,L5系列的低功耗特性可以给电池供电方案留出余地。在整个调试过程中,我遇到过一些比较隐蔽的坑,比如配置顺序、寄存器初始化的细节、以及时钟同步等,写这篇文章的目的是把这些经验整理出来,给后面要用同款芯片组合的工程师省点时间。

这套方案适合谁参考?如果你正在用STM32F1/F4/L4等常见系列驱动AD9102,或者手上也有L552想试试高级定时器与SPI配合的场景,这篇博文应该能帮你绕开我踩过的那些坑。文章不会只贴代码,更重要的是把每一步为什么这么做、配置背后的寄存器逻辑讲清楚。

1.2 为什么选择STM32L552 + AD9102组合

选型的时候我对比过几套方案。

  • 一是用STM32F103配合AD9833这类简单DDS,优点是上手快、资料多,但缺点是输出带宽和波形质量有限,而且AD9833没有内置RAM,做任意波形必须靠MCU实时刷新,高速时容易卡顿;
  • 二是用FPGA直接驱动高速DAC,性能确实强,但开发周期和功耗都不友好;
  • 三是最终选定的STM32L552 + AD9102组合,兼顾输出性能、开发效率和后续迭代空间。

AD9102的DDS模式可以输出最高180MHz的正弦波,刷新率由内部时钟决定,MCU只需要在启动时把频率控制字、幅度控制字写进去,之后就不需要频繁干预。这一点在需要长时间稳定输出的场景里非常关键,比如生成特定频率的激励信号用于传感器检测。

STM32L552提供的SPI接口可以很方便地和AD9102对接,最高SPI时钟可到50MHz左右(具体取决于分频配置),配合DMA可以一次把波形表传给AD9102的SRAM。此外,L552的PGA(可编程增益放大器)等相关外设虽然这次没有直接用,但后续如果要加信号调理电路,片上资源也能简化设计。整体看,这套组合是用“中等性能MCU + 集成信号链芯片”完成高精度波形输出的一种典型工程路径。

2. 关键硬件电路设计要点

2.1 AD9102供电与参考电压设计

AD9102的数字部分和模拟部分的供电都需要低纹波电源。我这边用的是LDO单独供电,避免开关电源的高频纹波直接耦合到模拟输出。AVDD和DVDD要分别加磁珠和去耦电容,推荐10uF钽电容+100nF陶瓷电容组合,尽可能靠近芯片电源引脚放置。参考电压VREF的选择直接影响输出幅度精度和温漂特性,建议使用外部高精度基准源。

这里有一个容易忽略的细节:AD9102的参考输入阻抗不高,如果用内部2.5V基准,虽然省事,但温漂系数一般在±20ppm/°C左右,在高精度场景下可能不够。我最终推荐外部基准方案,选了一颗温漂在±3ppm/°C的基准芯片,实测输出幅度的温度稳定性好了很多。

注意:AD9102的AVDD和DVDD如果使用同一个LDO,建议在DVDD路径串一个磁珠,防止数字开关噪声倒灌进模拟电源域。这个细节在芯片手册的布局建议里不是特别显眼,但不做的话,输出频谱上很容易看到杂散。

2.2 SPI接口与引脚分配

STM32L552的SPI接口和AD9102的SPI从机模式对接时,需要注意以下几点:

  • AD9102的SPI最大时钟不能超过40MHz(实际建议留裕量,我用的是10MHz,稳定优先);
  • 数据格式是MSB先行,支持CPOL=0、CPHA=0或CPOL=1、CPHA=1两种模式;
  • AD9102的CS引脚是低电平有效,整个读写期间必须保持低,不能像普通SPI设备那样按字节拉高拉低,否则容易导致寄存器写入错位。

我用的引脚分配如下所示,实际可根据自己板子调整,但建议把SPI引脚尽量靠近AD9102,减小走线寄生电容。

信号STM32L552引脚说明
SCKPB3SPI1_SCK
MOSIPB5SPI1_MOSI,AD9102的SDI
CSPB6软件控制,AD9102的CSB
RESETPB7AD9102的复位引脚,低有效
TRIGPB4可选,用于DDS触发或Pattern触发

AD9102还有一个关键引脚是SYNC,多片同步时需要用到。如果只用一片,SYNC直接接地即可,但AD9102的SYNC引脚和其他数字引脚的逻辑电平不一样,参考的是DVDD电平,别接到模拟电源上。

2.3 输出端调理电路

AD9102输出是差分电流形式,典型满量程电流在8mA左右,需要外接电阻转换成电压。我用的方案是IOUTP和IOUTN各接一个200Ω电阻到地,然后通过差分转单端的电路(比如用高速运放搭差分放大器)得到单端信号。

如果不需要太宽的带宽,也可以用变压器做差分转单端,但要注意低频截止特性。示波器实测下来,我的电路在50MHz以内幅度平坦度保持在±0.1dB以内,这个是纯电阻负载下的结果,加了运放后会略微变差,不过一般应用足够了。

注意:AD9102的IOUTP和IOUTN之间的负载电阻不宜过大,否则输出电压摆幅会接近电源轨,影响线性度。200Ω是一个比较均衡的取值,折算到DAC输出电压范围大概是±800mV,正好在多数运放的共模输入范围内。

3. 驱动固件架构与初始化流程

3.1 固件整体分层设计

驱动代码我没有一股脑写在一个文件里,而是分了这么几层:

  • ad9102_hal.c/h:芯片级驱动,包括寄存器读写、DDS配置、模式配置、校准等;
  • ad9102_wave_table.c/h:波形表处理,包括正弦波生成、任意波形填充、以及一些常见的调制波形;
  • app_signal_gen.c/h:应用层,负责对接上层命令、参数解析和状态管理。

这样分层的直接好处是:如果后续换MCU,只需要改最底层的SPI读写函数,芯片驱动逻辑可以原封不动地搬到新平台。调试过程中也方便单层验证,比如先用芯片驱动读寄存器确认SPI链路OK,再往上走波形配置。

3.2 寄存器读写基础函数实现

// ad9102_hal.c #include "ad9102_hal.h" #include "spi.h" static void AD9102_CS_LOW(void) { HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_RESET); } static void AD9102_CS_HIGH(void) { HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET); } static void AD9102_Delay_us(uint32_t us) { // 简单阻塞延时,实际项目可改用定时器或DWT for (uint32_t i = 0; i < us * 4; i++) { __NOP(); } } // 写寄存器:addr为寄存器地址,data为数据 // AD9102的SPI帧格式:[R/W(1bit) | addr(15bit) | data(16bit)] int AD9102_WriteReg(uint16_t addr, uint16_t data) { uint8_t txBuf[4]; uint32_t frame = 0; if (addr > 0x7FFF) { return -1; // 地址超范围 } frame = (0x0000 << 15) | (addr & 0x7FFF); frame = (frame << 16) | (data & 0xFFFF); txBuf[0] = (frame >> 24) & 0xFF; txBuf[1] = (frame >> 16) & 0xFF; txBuf[2] = (frame >> 8) & 0xFF; txBuf[3] = frame & 0xFF; AD9102_CS_LOW(); HAL_SPI_Transmit(&hspi1, txBuf, 4, 100); AD9102_CS_HIGH(); return 0; } // 读寄存器 int AD9102_ReadReg(uint16_t addr, uint16_t *data) { uint8_t txBuf[4]; uint8_t rxBuf[4] = {0}; uint32_t frame = 0; if (addr > 0x7FFF || data == NULL) { return -1; } frame = (0x0001 << 15) | (addr & 0x7FFF); frame = (frame << 16) | 0x0000; txBuf[0] = (frame >> 24) & 0xFF; txBuf[1] = (frame >> 16) & 0xFF; txBuf[2] = (frame >> 8) & 0xFF; txBuf[3] = frame & 0xFF; AD9102_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, txBuf, rxBuf, 4, 100); AD9102_CS_HIGH(); *data = (rxBuf[2] << 8) | rxBuf[3]; return 0; }

这段代码有几个细节需要特别说明一下。

第一个是AD9102的SPI帧格式,它是32位的,前16位是命令/地址段,后16位是数据段。命令段的最高位是R/W位,写入时置0,读时置1,剩下15位是寄存器地址。我在写这个函数的时候,一开始想当然地按照普通SPI设备的做法,CS拉低后直接发地址和数据,但结果总是读出来是0,后来查手册才发现是帧格式理解错了,地址和数据是拼在一个32位帧里的,不是分两次发送。

第二个是关于CS毛刺问题。AD9102要求CS拉低后,SCK的第一个有效边沿必须稳定,不能在CS刚拉低时就有时钟跳变。我测试中发现,如果HAL_SPI_Transmit在CS拉低之后有额外的初始化延迟,一般没问题;但如果SPI外设之前有残留时钟,需要先调用__HAL_SPI_CLEAR_OVRFLAG之类清理一下。

3.3 初始化流程与寄存器配置

AD9102的初始化顺序建议严格按照下面的流程来做,顺序错了可能会出现输出异常但不报错的情况,排查起来非常费时:

  1. 拉低RESET引脚,保持至少10us,再拉高,让芯片内部状态回到默认;
  2. 等待至少1ms,让内部LDO稳定;
  3. 配置SPI接口相关寄存器;
  4. 设置时钟分频和DDS主配置;
  5. 设置触发电平、信号类型等;
  6. 配置SRAM或DDS模式;
  7. 更新寄存器(部分寄存器需要写UPDATE寄存器才生效);
  8. 启动输出,用示波器验证波形。
// ad9102_hal.c // AD9102寄存器地址定义 #define AD9102_REG_SPICONFIG 0x0000 #define AD9102_REG_DDSCONFIG 0x0001 #define AD9102_REG_DDSCONFIG2 0x0004 #define AD9102_REG_TRIGGER 0x0002 #define AD9102_REG_SIGNALTYPE 0x0005 #define AD9102_REG_SRAMCONFIG 0x0006 #define AD9102_REG_SRAMDATA 0x0007 #define AD9102_REG_UPDATE 0x001F void AD9102_Init(void) { // 复位 HAL_GPIO_WritePin(AD9102_RESET_GPIO_Port, AD9102_RESET_Pin, GPIO_PIN_RESET); AD9102_Delay_us(20); HAL_GPIO_WritePin(AD9102_RESET_GPIO_Port, AD9102_RESET_Pin, GPIO_PIN_SET); AD9102_Delay_us(1000); // 配置SPI:CPOL=0, CPHA=0, MSB先行,在芯片驱动里确认 AD9102_WriteReg(AD9102_REG_SPICONFIG, 0x0000); // DDS配置:内部时钟,无分频 AD9102_WriteReg(AD9102_REG_DDSCONFIG, 0x0000); // 信号类型:选择DDS正弦波输出 AD9102_WriteReg(AD9102_REG_SIGNALTYPE, 0x0001); // 触发方式:软件触发 AD9102_WriteReg(AD9102_REG_TRIGGER, 0x0001); // 更新寄存器,使配置生效 AD9102_WriteReg(AD9102_REG_UPDATE, 0x0001); }

这里有一个比较重要的设计点:AD9102的很多寄存器不是写进去立刻生效的,而是要等UPDATE寄存器被写一次后才会更新内部配置。这个机制有点类似FPGA里的配置寄存器重新加载,好处是可以多个寄存器原子更新,避免中间态,坏处是如果忘了这一句,前面所有配置都是白写。我第一次调试时,信号源死活没输出,查了两天,最后对着手册逐行排查才发现是UPDATE没触发。

3.4 DDS模式与SRAM模式的切换

AD9102支持DDS和SRAM两种波形生成方式,区别和选型建议如下表所示。

模式原理输出频率范围典型应用使用难度
DDS通过频率控制字生成正弦波,相位累加器实现连续相位最高可达180MHz单一频率正弦波、扫频信号简单,频率可实时调整
SRAM用户把波形数据写入内部SRAM,DAC按地址循环输出取决于采样率和数据长度任意波形、调制波形、复合信号较复杂,需要生成波形表

之所以把频率控制字单独拎出来说,是因为AD9102的DDS频率计算公式和很多教科书上的DDS公式略有差异,它内部有24位频率控制字和参考时钟分频控制,写错了输出频率会差很远。

DDS输出频率计算公式如下:

  • ( f_{out} = (FTW \times f_{DAC}) / 2^{24} )

其中FTW就是24位频率控制字,fDAC是AD9102内部DAC采样时钟,为外部参考时钟经分频后的值。举个例子,如果外部REFCLK是100MHz,DAC时钟也配置成100MHz,那么要输出10kHz的正弦波,FTW的计算过程是:

  • ( FTW = (10000 \times 2^{24}) / 100000000 = 16.777216 )

取整后FTW=17,再把17转换成16进制就是0x11。注意这里有个量化误差问题,FTW是整数,不能精确表示所有频率。在输出低频时,由于分辨率是(f_{DAC}/2^{24}),大概0.006Hz一个步进,多数场合够用,但如果要输出极低频率(比如0.1Hz),这个步进可能不够,需要考虑用SRAM模式或外部时钟降频。

3.5 波形表生成与DMA传输

SRAM模式下,需要把波形数据写入AD9102内部的SRAM。这块SRAM是1024个16bit字的存储深度,对于一般任意波形来说基本够用。你可以用MATLAB生成波形数据然后导出为C数组,也可以直接在单片机里用查表法生成正弦波点。

下面这段代码展示了如何生成一个正弦波的波形表,并用DMA写入AD9102的SRAM。ADC/DAC的满量程是2的12次方,这里做了12位精度映射。注意AD9102的DAC数据是二进制补码格式,0x000代表负满量程,0x7FF代表零,0xFFF代表正满量程。不熟悉补码格式的人容易在这里搞错,导致正弦波变成半波整流。

// ad9102_wave_table.c #include "ad9102_wave_table.h" #include <math.h> #define WAVE_TABLE_SIZE 1024 // 生成12位正弦波表,phase为初始相位,amplitude为幅度(0~2048) void AD9102_GenerateSineTable(uint16_t *table, uint32_t size, float phase, uint16_t amplitude) { for (uint32_t i = 0; i < size; i++) { float rad = 2.0f * 3.14159265358979f * i / size + phase; float val = sinf(rad) * amplitude; int32_t dacVal = (int32_t)val + 2048; // 0x800即中点 if (dacVal > 4095) dacVal = 4095; if (dacVal < 0) dacVal = 0; // 转成12位补码形式 uint16_t code = (uint16_t)dacVal & 0x0FFF; // AD9102 SRAM数据格式需要位宽配置为12位,左对齐? // 根据寄存器配置决定是否左移4位 table[i] = code; } } // 通过DMA写入SRAM,需要事先在初始化SPI时使能DMA void AD9102_WriteSRAM_DMA(const uint16_t *table, uint32_t size) { // 设置SRAM的地址指针为0 AD9102_WriteReg(AD9102_REG_SRAMCONFIG, 0x0000); // 写入SRAM数据寄存器(一次4字节:地址+数据,此处简化为循环调用) // 实际用DMA传输时,需要构造32位帧,并用SPI的DMA通道发送 for (uint32_t i = 0; i < size; i++) { AD9102_WriteReg(AD9102_REG_SRAMDATA, table[i]); } }

这段代码在性能上有可优化空间,尤其是AD9102_WriteReg每次都拉高CS,而AD9102的SRAM数据写入其实可以连续写多个32位帧,只要CS一直拉低即可。实际项目里可以封装一个AD9102_WriteReg_Multi函数,先拉低CS,然后用DMA连续发送多个帧,最后再拉高CS,效率会高很多。

注意:如果你用的是DMA传输,SPI发送完成中断触发后,不要立刻拉高CS,最好等DMA的完全传输完成(TCIF标志)置位后再拉高。否则最后一个字节可能还没完全从移位寄存器送出,就被CS拉高截断了,最后一个字会丢失。

4. 输出波形异常排查实录

4.1 波形输出静默:怎么排查都没输出的问题

这个是最常见的故障。我调试时遇到了一个情况:AD9102配置完成后,示波器上什么信号都看不到。排查过程如下:

  • 先用万用表量AVDD和DVDD,电压正常;
  • 再用示波器看REFCLK引脚,发现没有时钟输入,原来是我在CubeMX里把外部时钟输出引脚复用错了,REFCLK根本没连上。

换到正确引脚后,还是没有输出。继续查发现AD9102的SPI状态寄存器不对,读出来的ID和手册对不上。这个问题最终定位到是SPI通信工作模式配置不对。AD9102的SPI支持两种模式,但我用的HAL库默认SPI模式是CPOL=0、CPHA=0,而AD9102在上电默认状态可能要求CPOL=1、CPHA=1。这个需要仔细看手册,不能想当然。

4.2 正弦波失真:顶部和底部削平

输出波形削平,第一反应是DAC满量程超出范围。排查步骤:

  • 检查VREF电压,正常2.5V;
  • 检查输出负载电阻,200Ω,正常;
  • 检查幅度寄存器和增益配置,发现问题。

AD9102的输出幅度除了受DAC满量程控制,还有一个独立的增益寄存器。默认增益是1,但我看之前的初始化代码把增益误写成了最大值2倍,这样DAC在接近满量程时自然削波。解决办法是把增益寄存器恢复成1,并且用示波器测一下实际输出电压范围,确认在DAC的线性区间内。

4.3 SRAM模式下波形错乱

SRAM波形错乱,常见的几个原因按优先级排查:

  1. 波形表数据格式不正确,比如补码格式换算错误,导致波形出现跳变;
  2. SRAM写入地址没有正确复位,导致DAC从错误地址开始循环;
  3. TRIG触发信号的使用方式不对,SRAM模式在部分配置下需要外部或软件触发才开始循环输出。

我遇到的问题就是SRAM地址计数器没有复位。AD9102在SRAM模式下,地址通常是自动递增循环的,但上电初始指向的地址不一定是0。需要显式写一次SRAMCONFIG寄存器,把地址归零。加上这步后,波形就完全正常了。

5. 完整代码示例与使用说明

5.1 初始化与主流程整合

上面零散展示了几个函数片段,这里给一个完整可编译的流程参考。

// main.c 核心流程 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); // AD9102上电复位 AD9102_HardReset(); // 初始化芯片 AD9102_Init(); // 配置DDS输出10kHz正弦波 // 假设DAC时钟=100MHz,FTW=0x11 AD9102_SetDDSFrequency(10000); // 设置幅度为满量程的50% AD9102_SetAmplitude(2048); // 启动输出 AD9102_Start(); while (1) { // 主循环可以处理其他任务,AD9102自动输出信号 } }

5.2 重要函数接口说明

  • AD9102_WriteReg(addr, data):写寄存器底层函数,所有配置的基础;
  • AD9102_ReadReg(addr, &data):读寄存器函数,用于调试和校验;
  • AD9102_HardReset():拉低复位引脚并延时,确保上电时序正确;
  • AD9102_Init():按推荐顺序初始化关键寄存器;
  • AD9102_SetDDSFrequency(freq_hz):根据DAC时钟自动计算FTW并写入;
  • AD9102_SetAmplitude(amplitude):设置DAC输出幅度;
  • AD9102_Start():软件触发开始输出。

实际项目中,建议把AD9102_SetDDSFrequency里的DAC时钟值定义成宏,因为不同的硬件设计可能参考时钟不同,统一管理可以避免到处改数。比如我这边是100MHz,就定义一个#define AD9102_DAC_CLK_HZ 100000000UL,后面所有计算都基于这个宏。

5.3 代码仓库使用建议

代码基于STM32CubeIDE开发,使用了HAL库。如果你用的是标准外设库,SPI读写部分和中断配置需要对应调整,但AD9102侧的寄存器逻辑完全一致。建议在调试时先用逻辑分析仪抓SPI波形,确认时序正确,再接到AD9102上调试硬件,两个问题不要混在一起排查。

6. 工程实践中的避坑经验总结

6.1 硬件设计阶段的避坑清单

按重要性从高到低排序:

  1. 稳压电源去耦一定要做好,否则模拟输出频谱上会有明显的电源噪声杂散;
  2. 参考电压源的温漂问题,最好一开始就用外部高精度基准,不要图省事用内部基准,后续升级会很难受;
  3. PCB布线时,AD9102的模拟输出和数字SPI总线保持足够距离,地平面尽量不要被长走线割裂。

6.2 软件调试阶段的避坑清单

  1. 每次上电后先读一次寄存器ID。等确认SPI链路完全正常,再继续做后续配置,能省掉大量无效排查;
  2. 配置寄存器后养成写UPDATE寄存器的习惯,并可以用回读方式校验关键配置;
  3. 不要为了追求性能把SPI时钟调的过高,信号完整性在长线上很难保证。稳定运行优先。

6.3 常见问题速查表

问题现象可能原因排查/解决办法
无输出REFCLK未接入或引脚配置错误检查时钟,用示波器看REFCLK波形
无输出SPI配置错误按手册确认CPOL/CPHA,读寄存器ID
正弦波顶部/底部削平输出幅度过大或增益配置错误降低幅度设置,检查增益寄存器
输出波形噪声大电源去耦不足查看电源纹波,加强磁珠和电容滤波
SRAM波形错乱数据格式、地址未复位检查补码格式,显式归零地址
输出频率和设定值不一致FTW计算有误或参考时钟不一致核对FTW公式,确认DAC时钟值

说实话,AD9102这颗芯片的文档和驱动资源相比主流DDS芯片还是要少一些,很多细节需要翻手册抠。我把自己调试过程中的这些经验写下来,就是希望能帮后面的人少走几步弯路。如果你在调试中遇到文章里没写到的情况,欢迎评论区留言交流,我看到会尽量回复。

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

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

立即咨询