第一次在项目上用到AD9122时,我的第一反应是“这芯片太挑接口了”。16位双通道、最高1200MSPS的更新率,如果老老实实按并行CMOS方式喂数据,接口速率得上Gbps级别,一般的FPGA和PCB布线根本招架不住。所以LVDS数据接口与SPI配置寄存器,就成了整个设计绕不开的两座山:SPI负责让DAC听指挥,LVDS负责把高速数据安稳送进去,任何一边出问题,输出端都是一片乱码。
这篇文章把我调通AD9122过程中梳理出来的SPI配置流程、LVDS接口设计、FPGA侧实现和调试排障经验全部整理出来。适合正在做FPGA+DAC高速信号链的硬件工程师和逻辑工程师参考,也适合刚接触ADI高速DAC的开发者快速建立整体认知。看完之后,你至少能少踩一半的坑。
1. 项目整体架构与设计思路
1.1 为什么选择AD9122:高速DAC在信号链路中的定位
AD9122是ADI公司推出的一款双通道、16位、最高1200MSPS的高速DAC芯片。单看参数,它不算极端,但强在内部集成了完整的数字插值滤波器和灵活的输入接口,可以直接接收来自FPGA的LVDS/CMOS并行数据,再经过内部PLL和调制链路输出高质量模拟信号。实际项目中我们主要拿它做宽带波形发生、基带信号输出以及任意波发生器,它在ADC/DAC构成的信号链里,属于“数字域”和“模拟域”交界处的关键器件。
选型的时候对比过其他几款DAC,AD9122的优势在于接口可配置性非常好。可以用标准CMOS接口,也可以用LVDS接口;支持单通道、双通道以及复用模式;插值倍数可以从1倍到8倍可调。换句话说,它并不会强迫你把接口速率拉到极限,而是给你足够的配置空间去迁就FPGA的实际工作频率。这个特性在高速设计中非常值钱,因为系统瓶颈往往不是DAC本身,而是数字侧的数据传输能力。
1.2 从DAC工作原理看系统瓶颈
高速DAC的基本工作过程并不复杂:每个DAC时钟周期,数字输入端口收到一个采样值,内部转换器把它变成对应的模拟输出电平,理想情况下是一串时间上离散的模拟样本,再由后级低通滤波器重建出连续波形。问题在于,当DAC更新率越来越快,前端并口数据速率就会成为“堵点”。
做一个简单的计算。假设DAC要跑500MSPS,数据位宽16bit,如果使用CMOS并行接口,那么每个时钟周期需要同时翻转16根单端信号线,接口数据率相当于500M×16=8Gbps。更难受的是,CMOS单端信号在高速翻转时容易引入地弹、串扰和EMI,跑上300MHz就已经很挣扎了。如果改成LVDS DDR差分接口,情况会好很多:8对差分线在DDR模式下,每根线的等效速率可以做到1Gbps,DCI时钟只需500MHz,整个接口的压力明显下降。
如果你再打开AD9122内部的插值滤波器,让数字输入速率降到DAC更新率的1/2、1/4甚至1/8,那么接口时钟和数据率可以进一步大幅降低。比如DAC输出400MSPS,使用4倍插值,则数据输入只需要100MHz即可,LVDS接口的时序余量瞬间变得宽裕。这也是为什么很多AD9122方案里FPGA主时钟通常只有100MHz~200MHz,而DAC输出却能跑到几百兆甚至上GHz的原因。
1.3 时钟树与数据流设计:整体架构如何组织
我这里画了一张“文字版系统框图”,方便你对照着看:
- 系统参考时钟(通常来自低抖动晶振或干净的外部时钟源)输入给FPGA和AD9122的参考时钟引脚。
- FPGA内部产生基带数据(DDS、调制器、波形表等),送去LVDS发送逻辑。
- LVDS数据线连接FPGA与AD9122,包含数据线和一对DCI数据时钟线。
- AD9122通过SPI接口接收FPGA或MCU的配置命令,完成复位、时钟分频、插值、接口模式、使能等设置。
- DAC模拟输出经变压器/放大器送到外部接口,或送入下一级射频链路。
系统中每个模块的功能和设计考量,我整理成了表格方便对照:
| 模块 | 核心器件/接口 | 设计关注点 |
|---|---|---|
| 参考时钟源 | 晶振/时钟芯片 | 抖动、稳定性、与DAC时钟的同步关系 |
| 数字数据源 | FPGA | 数据格式、位序、使能时序 |
| LVDS接口 | 差分对、DCI | 阻抗、等长、端接、建立保持余量 |
| SPI配置接口 | SCLK/SDIO/CSB | 时序模式、上电顺序、回读验证 |
| DAC模拟输出 | 变压器/LPF/运放 | 插值镜像抑制、幅度、偏置、杂散 |
整个系统里,最容易被忽视却最关键的其实是时钟。AD9122的工作时钟可以由外部直接提供,也可以通过内部PLL倍频/分频得到。数据DCI时钟和DAC采样时钟之间的相位关系,直接影响输入数据的建立保持窗口。所以设计时钟树时,我通常会先确定“DAC采样率”和“插值倍数”,再反推数据输入带宽,最后确定FPGA侧的逻辑时钟和DDR数据传输格式。顺序不能反,否则接口时序很容易一塌糊涂。
2. AD9122的SPI配置体系深入拆解
2.1 SPI引脚、帧格式与上电时序
AD9122的SPI控制口逻辑上非常简单,只有四根线:SCLK、SDIO、CSB,以及可选的SDO。SDO用于回读数据,如果你的系统对配置可靠性要求不高,也可以只用三线模式,但我在实际调试中强烈建议保留SDO,因为很多寄存器配置错误可以通过回读快速定位。
SPI帧格式值得多说两句。ADI很多DAC的寄存器帧不是普通的8bit地址+8bit数据,而是采用“R/W标志位+6位或7位地址+8位数据”的紧凑结构,总帧长可能是12bit或16bit。以AD9122为例,SPI帧按16bit组织:最高位为读写标志(1表示读,0表示写),中间是寄存器地址,低8位是数据。同时默认MSB first,也就是高位先发。这一点和STM32、MCU上常见的8bit SPI外设不同,如果你直接套用HAL库的8bit模式,最终配置进去的寄存器地址和值大概率是乱的。
上电时序上,我一般遵守这样的顺序:先给AD9122供电并等待电源稳定,再给RESET引脚一个有效的复位脉冲,然后通过SPI写入软复位命令和初始化序列,最后配置时钟、接口模式和输出使能。DAC前端的高速数字输入可以在初始化完成后再开启,避免在寄存器配置过程中DAC处于未定义状态时收到无效数据。
2.2 寄存器读写实现:写一个可靠的SPI驱动
因为AD9122的SPI配置速率不需要很高,实际工作在10MHz甚至更低就完全够用,我通常直接用GPIO模拟SPI,不依赖硬件外设,这样可移植性最好。下面这段代码是我惯用的模板,你可以直接参考:
#define AD9122_SCLK_L() gpio_write(SCLK_PIN, 0) #define AD9122_SCLK_H() gpio_write(SCLK_PIN, 1) #define AD9122_CSB_L() gpio_write(CSB_PIN, 0) #define AD9122_CSB_H() gpio_write(CSB_PIN, 1) #define AD9122_SDIO_W(v) gpio_write(SDIO_PIN, v) #define AD9122_SDIO_R() gpio_read(SDIO_PIN) static void spi_delay(void) { // 空转几个周期,保证SCLK频率不超过手册上限 for (volatile int i = 0; i < 8; i++); } static void ad9122_spi_frame(uint16_t frame) { AD9122_CSB_L(); for (int i = 15; i >= 0; i--) { AD9122_SCLK_L(); AD9122_SDIO_W((frame >> i) & 0x01); spi_delay(); AD9122_SCLK_H(); spi_delay(); } AD9122_SCLK_L(); AD9122_CSB_H(); } int ad9122_spi_read_reg(uint8_t addr, uint8_t *val) { uint16_t frame = (0x8000) | ((uint16_t)(addr & 0x7F) << 8); uint8_t rdata = 0; AD9122_CSB_L(); // 先发地址帧 for (int i = 15; i >= 8; i--) { AD9122_SCLK_L(); AD9122_SDIO_W((frame >> i) & 0x01); spi_delay(); AD9122_SCLK_H(); spi_delay(); } AD9122_SCLK_L(); // 高阻释放总线,转为接收模式 gpio_set_dir(SDIO_PIN, GPIO_IN); for (int i = 7; i >= 0; i--) { AD9122_SCLK_H(); spi_delay(); rdata |= (AD9122_SDIO_R() << i); AD9122_SCLK_L(); spi_delay(); } gpio_set_dir(SDIO_PIN, GPIO_OUT); AD9122_CSB_H(); *val = rdata; return 0; }这段代码用到了“先发地址帧,再切换SDIO方向读数据”的方式。注意在读取阶段,SDIO要切换成输入模式,否则会拉死总线。如果你的MCU或者FPGA引脚方向切换不好操作,也可以用独立的SDO引脚,读寄存器时直接读SDO,写寄存器时不理它,逻辑上会简单很多。
有一点要提醒:初始化序列里经常需要重复写很多寄存器,我习惯把寄存器表和值定义成结构体数组,一行一个,顺序执行。不要一边调试一边随手改代码,很容易漏项。下面是一个初始化序列的示意写法:
typedef struct { uint8_t addr; uint8_t val; } reg_cfg_t; const reg_cfg_t ad9122_init_seq[] = { {0x00, 0x18}, // 软复位并等待 {0x00, 0x00}, // 退出软复位 // 后续按手册配置时钟、插值、接口模式等 // {0x10, 0x20}, // {0x22, 0x00}, // ... };具体寄存器的地址和位域,一定要以官方datasheet寄存器表为准。不同版本芯片或者不同封装,个别保留位可能有差异,不能照搬网上随意贴出来的配置列表。
2.3 初始化流程与关键寄存器配置顺序
AD9122的初始化虽然寄存器很多,但配置顺序是有逻辑的。我总结的通用流程是:
- 芯片上电并完成硬复位或软复位。
- 配置SPI接口本身(如SDO使能、MSB/LSB优先级),确保回读可用。
- 配置DAC参考时钟与内部PLL/分频器,确定DAC采样率。
- 配置数字插值滤波器倍率和数据输入格式(二进制补码或偏移二进制)。
- 配置输入接口模式(LVDS/CMOS、DDR/SDR、双通道/单通道等)。
- 配置与DCI时钟相关的延迟或时钟对齐参数。
- 解除DAC输出power-down,使能各通道输出。
- 回读关键寄存器,确认配置全部生效。
为什么要这么严格的顺序?因为有些寄存器之间存在依赖关系,比如接口模式寄存器可能依赖插值倍率,而插值倍率又和数据通路宽度相关。如果先开输出再配时钟,DAC可能在一小段时间内输出未知状态,进而打坏后级放大器或者干扰其他电路。
另外,建议每次初始化完成后做一次全面回读。很多老工程师嫌麻烦,但AD9122这类芯片寄存器一旦因时序冲突没写进去,后面症状非常隐蔽:输出没有、杂散大、数据乱序、通道串扰,各种现象都有可能出现。回读一遍寄存器,可以在十分钟内排除一大批低级错误。
2.4 配置阶段最容易踩的三个坑
第一个坑是软复位之后没有等待。AD9122的软复位和其他SPI器件一样需要若干个时钟周期才能真正完成,如果复位命令刚写入就立即写下一个寄存器,间隔太短可能会丢失配置。我通常会在软复位后加上至少1ms延时,或者用回读方式确认复位完成。
第二个坑是SDO/SDIO引脚复用关系不清。默认情况下SDIO是双向数据脚,SDO可以输出回读数据。如果你使用三线模式,SDO要悬空或做相应处理;如果你使用四线模式,SDIO和SDO的角色分工会变化。这个配置不弄对,读数据永远是0xFF或者0x00。
第三个坑是SPI时钟极性和相位的匹配。AD9122手册里明确给出了SCLK、SDIO和CSB之间的时序关系,多数模式是CPOL=0、CPHA=0(空闲低、上升沿采样)。但如果你习惯用STM32的硬件SPI,默认不一定匹配,必须切换CPOL/CPHA。如果不确定,先用GPIO模拟SPI最稳妥,等系统跑通了再考虑换成硬件SPI或DMA,可以省去大量排查时序问题的痛苦。
3. LVDS数据接口设计与FPGA侧实现
3.1 高速DAC输入接口的信号完整性设计
AD9122的LVDS输入接口是整个系统里最容易出物理层问题的地方。首先要保证差分阻抗连续可控:走线阻抗按100Ω差分设计,FPGA输出到DAC接收端的整条路径上尽量少打过孔,避免stub。DAC接收端一般需要就近放置100Ω端接电阻,这个电阻的位置非常关键,必须靠近DAC引脚,否则反射会对时序和噪声产生明显影响。
等长约束方面,组内差分对的正负信号线长度差通常控制在5~10mil以内,同一组数据lane之间尽量做到等长。如果FPGA和DAC距离较远,组与组之间的skew可以通过调整IODELAY或DCLK相位来补偿,但组内偏斜只能靠布线解决。我踩过一次很深的坑:某块板上D0到D7的lane布线走线长度差异超过100mil,结果在200MHz DDR速率下,数据窗口变得非常窄,几乎到了码间的临界状态,最后只能降速运行。后来重新布线把等长控制在20mil以内,问题立刻消失。
电源和地平面也不容忽视。高速LVDS信号参考面必须连续,不能跨分割。如果FPGA和DAC分别处在两块不同的地平面区域,必须保证参考地平面的连通性,否则回流路径会绕远,产生共模噪声。很多“原理图没问题但波形惨不忍睹”的案例,最后都查到是地平面问题。
3.2 FPGA端LVDS/DDR数据发送实现
FPGA侧通常只需要关注两件事:数据按时序打出去,的DCI时钟按正确相位送过去。在Xilinx的7系列FPGA上,我习惯用OSERDESE2发送DDR数据,用ODDR发送DCI时钟。
下面是一个简单的发送逻辑示意:
// 并行数据进入,DDR模式输出 OSERDESE2 #( .DATA_RATE_OQ("DDR"), .DATA_RATE_TQ("BUF"), .DATA_WIDTH(4), .INIT_OQ(1'b0), .SERDES_MODE("MASTER") ) u_oserdes_d0 ( .D1(par_d0[0]), .D2(par_d0[1]), .D3(par_d0[2]), .D4(par_d0[3]), .OCE(1'b1), .CLK(serdes_clk), .CLKDIV(par_clk), .OQ(lvds_d0_p), .OFB(), .T(1'b0), .T1(1'b0), .T2(1'b0), .T3(1'b0), .T4(1'b0), .TBYTEIN(1'b0), .TBYTEOUT(), .TCE(1'b0), .RST(1'b0) );DCI时钟使用ODDR输出,让时钟信号在数据眼的中心位置翻转,以保证DAC侧建立保持条件。具体相位差值要根据手册中tDS、tDH参数,以及PCB走线延时来评估,必要时在逻辑里插入可配置的延迟。
ODDR #( .DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0), .SRTYPE("SYNC") ) u_oddr_dci ( .Q(dci_p), .C(serdes_clk), .CE(1'b1), .D1(1'b1), .D2(1'b0), .R(1'b0), .S(1'b0) );这里注意,OSERDESE2的DATA_WIDTH选择4,意味着每个并行时钟周期输出2bit到DDR线上。CLK频率是par_clk的两倍,par_clk就是你DDR数据端口的并行时钟。如果数据位宽是16bit,用8对LVDS,每对lane对应两bit并行数据,那么总共需要8个OSERDESE2实例。
3.3 时序分析与约束设计
FPGA工程里,如果忘了加LVDS接口的时序约束,工具大概率会给你“宽松但不可用”的结果,跑起来后DAC端时序余量几乎为零。正确做法是给DCI时钟和数据都指定输出延迟约束。
举个例子,假设DCI时钟频率为100MHz,DAC要求数据在时钟边沿前后分别满足tDS=0.5ns、tDH=0.5ns,PCB走线延时差异控制在0.2ns以内,那么输出延迟约束可以写成:
create_generated_clock -name dci_clk \ -source [get_pins u_pll/clkout] \ -divide_by 1 [get_ports {dci_p dci_n}] set_output_delay -clock dci_clk -max 0.9 \ [get_ports {lvds_data_p[*] lvds_data_n[*]}] set_output_delay -clock dci_clk -min 0.1 \ [get_ports {lvds_data_p[*] lvds_data_n[*]}]这里的max、min具体取值要根据DAC手册减去一个安全裕量,同时还要考虑PCB走线延时。整体原则是“把预算留给接口”,FPGA内部逻辑只要满足时序约束,就可以保证数据在DAC端落在有效窗口内。时序跑完后,建议在时序报告里重点看输出端口的slack,如果裕量不足,优先考虑调整输出相位,而不是盲目增加逻辑流水。
3.4 位对齐、延迟调整与训练序列
高速并行接口还有一个隐性难题:多根lane之间以及数据与DCI时钟之间可能存在相位偏移。如果DAC端没有自动对齐功能,就需要在上电时做一个简单的训练流程:FPGA发送一个固定pattern,比如0xAAAA或者0xF0F0,然后观察DAC输出的模拟波形或回读状态,确认每个数据bit都落在了正确的位上。
如果发现个别bit错位,优先调整对应lane的IODELAY。Xilinx的IODELAY可以按tap级别(约78ps)精细调节,AD9122这类DAC对几百ps的延迟调整非常敏感。也可以考虑调节DCI时钟的相位,让整个数据窗口整体移动,但这会影响所有lane,通常作为粗调手段。
我的经验是先用软件工具把DAC输入模式配成“交替校验”或“单通道测试”模式,再用逻辑分析仪观测中间信号,缩小问题范围。如果条件允许,在FPGA内部例化一个ILA监测并行采样结果,能直接看出哪一根lane对不上。没有逻辑分析仪时,通过DAC输出正弦波判断:相位错位会让输出波形出现明显毛刺或高频杂散,严重时波形直接乱掉。训练序列配合频谱观察,基本能把问题定位到具体lane。
4. 系统调试方法与指标测试
4.1 从SPI到输出的第一束波形
新板子拿回来,不要急着跑高速信号。先写一个极小的FPGA工程,只做三件事:初始化AD9122、发送直流电平码、低速输出方波。用示波器看DAC输出电压是否为正负交替或直流稳定,这一步能快速验证SPI配置是否成功、输出通道是否正常。
具体做法是:先用SPI把AD9122配成单通道、低速率、无插值模式,FPGA输出固定的16bit码值,比如0x8000对应中间电平,0xFFFF对应正满幅,0x0000对应负满幅。观察DAC输出电压是否随码值变化,如果输出不变,先查SPI回读寄存器,确认寄存器真的写进去了。再查power-down位,很多初期调试“没输出”的问题,都是输出使能位没打开。
确认直流输出正常后,再生成一个低频正弦查找表,以1kHz或者10kHz频率输出。这时DAC输出应该是干净的低频正弦。如果正弦波形正常,说明从FPGA到DAC的数据通道和DAC模拟输出链路基本打通,可以逐步提高速率和插值倍数。
4.2 频谱指标怎么看
当信号跑起来以后,不要只盯着示波器看“有没有波”,要上频谱仪看“波形纯不纯”。AD9122的主要指标包括SFDR、谐波失真、杂散抑制、噪声基底等。对调试来说,我通常会看三个地方:
一是基波旁边有没有明显的谐波分量,比如HD2、HD3。谐波过大通常和DAC输出幅度、变压器匹配或者输出偏置有关,也可能和DAC内部未校正电路有关,需要排除。
二是指定带宽内有没有非谐波相关的杂散尖峰。这些尖峰往往来自时钟串扰、电源噪声或者数字数据串扰,可能需要检查时钟走线隔离、电源去耦是否到位。
三是整个输出频谱的噪声基底是否平稳。如果基底出现异常隆起,多半是数据源噪声或时钟抖动过大,这时候就要优先优化参考时钟的质量,而不是继续折腾DAC配置。
插值滤波器有没有生效,通过频谱也可以清楚看到。使用4倍插值后,DAC输出频谱中靠近高频段的镜像分量会被推到原来4倍以外的位置,如果在F_clk/4附近还能看到镜像能量,说明插值配置可能不对,或者输出重建滤波器带宽设置不合理。
4.3 数据错位、毛刺与眼图劣化的现场排查
调试中我遇到过最典型的问题:输出正弦波在示波器上看起来大方向是对的,但叠加了随机毛刺,频谱上杂散明显增多。排查思路一般从“降低接口速率”开始。把DAC更新率和插值倍数降下来,如果毛刺消失,说明问题大概率来自数据时序或者信号完整性;如果毛刺依旧,则重点怀疑DAC配置或模拟输出链路。
时序导致的毛刺有一个典型特征:毛刺位置相对数据变化点固定,和数据pattern强相关。你可以先把数据设置成全1或全0交替,观察毛刺是否消失。数据交替时如果出现尖刺,基本就是数据建立时间不足或lane间skew过大。这时候去检查眼图,如果没有高速示波器,也可以通过调节DCI相位扫描看毛刺的变化,找到稳定的相位窗口。
还有一种隐蔽情况:明明LVDS布线没问题,SPI寄存器也写进去了,但输出仍然乱码。这时候要考虑DAC接收端的数据格式配置,比如二进制补码和偏移二进制的差别。如果FPGA发送的是补码数据而DAC被配置成偏移二进制接收,整条波形会直接反转或整体移位,看起来像是“数据错位”。
4.4 常见问题速查表
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| DAC完全无输出 | 供电/复位/输出使能位未配置 | 检查电源、复位时序、回读寄存器 |
| 输出波形整体相位反向 | 数据格式补码/偏移二进制不匹配 | 调整寄存器或FPGA数据格式 |
| 输出正弦有规律毛刺 | lane间skew过大、DCI相位不对 | 降低速率、调节IODELAY、检查等长 |
| 杂散尖峰明显 | 时钟串扰、电源噪声 | 检查时钟隔离、去耦电容、参考时钟 |
| 高频时输出幅度严重跌落 | 插值倍率或重建滤波器不匹配 | 核对插值配置与模拟滤波器带宽 |
| SPI回读全为0xFF | SDO/片选/时钟极性错误 | 检查SPI模式、SDO上下拉、接线 |
这张表不能替代完整调试,但基本覆盖了我调试过程中的高频问题,适合打印出来贴在工位旁边。
5. 工程实践中的几个重要提醒
5.1 电源噪声对高速DAC的隐性影响
AD9122内部既有数字电路、又有模拟电路,还有PLL,电源引脚非常敏感。如果给DAC供电的LDO或DC-DC噪声太大,输出频谱上会直接体现出杂散和噪声基底恶化。我在一块板上遇到过“输出波形正常、SFDR很差”的问题,查了很久才发现是附近一个DC-DC的开关频率耦合进了DAC电源走线。后来在DAC电源引脚前增加LC滤波,并调整了PCB布局,指标才恢复正常。建议从布局阶段就给DAC电源区域“画圈”,数字电源和模拟电源严格分区,去耦电容尽量靠近引脚,地孔就近打过。
5.2 时钟是整个系统的灵魂
很多高速DAC的问题根源并不在DAC本身,而在参考时钟。AD9122的时钟抖动会直接变成输出相位噪声,进而影响SFDR和EVM。我见过有人为了省成本,用FPGA内部PLL输出的时钟当DAC参考时钟,结果频谱指标始终不过。参考时钟的抖动性能非常重要,至少在几十飞秒量级才能保证好的性能,所以外部时钟源选型要谨慎,不能省。
5.3 设计清单与上手建议
如果你是从零开始做AD9122方案,我建议按下面的顺序推进:
- 先花半天时间读懂datasheet里的SPI寄存器表和LVDS时序参数。
- 画板前把差分对的阻抗、等长、端接方式全部确认好。
- FPGA工程分两步:先只写SPI初始化,验证寄存器读写;再加LVDS数据发送,从低速开始逐步提速。
- 调试过程中始终用“回读确认寄存器”作为前提,寄存器没确认之前不要去碰数据通路。
- 遇到奇怪的模拟现象,先怀疑时钟和电源,再怀疑数据时序,最后才怀疑芯片本身。
根据我个人的体会,AD9122这种高速DAC其实调通不难,难的是在每个环节都保留足够的设计余量。SPI低速就稳扎稳打,LVDS高速就认真做好等长和端接,时钟和电源舍得下本钱。这个项目做完之后,有一个习惯我一直留着:每次布线前先把差分对约束建好再跑等长,而不是等走完线再去补。省下来的时间,足够你把频谱指标再测三轮。