FPGA通过SPI配置高速ADC:4线与3线时序及Verilog实现详解

发布时间:2026/10/24 9:37:09
FPGA通过SPI配置高速ADC:4线与3线时序及Verilog实现详解

简介:这套文档整理自FPGA通过SPI配置ADC的系列博文,面向数字IC/FPGA工程师及嵌入式开发者,解决ADC初始化配置与时序设计的实际问题。内容包括SPI总线协议基础、4线与3线接口差异、以ADS52J90为例的读写时序分析,以及寄存器配置与时钟参数约束等,由浅入深,可直接用于工程参考。资源包含1个docx文档,压缩包约787KB,携带方便。已有1870人学习下载。读者可从中获得从SPI信号定义到SEN、SCLK、SDIN时序配合的完整知识链,并对TI、ADI等常见ADC芯片的配置流程建立清晰认知,适合作为FPGA驱动开发、硬件调试时的快速查阅手册。

1. 高速ADC上电乱码?先查SPI配置:一篇文章理清4线和3线的时序与代码

高速ADC上电乱码的现象很常见:某开发者的板子上换了一颗多通道ADC,FPGA那边采回来的数据完全不可用,示波器抓波形也看不出规律。排查到最后,问题往往出在SPI配置上。现在的ADC芯片在内部集成了大量控制寄存器,采样率、通道开关、增益这些功能都要靠SPI写入配置才能生效,配置不对,芯片就跟黑匣子一样无法预测。这篇笔记围绕FPGA通过SPI配置ADC这条主线,把4线、3线两种模式的读写时序、关键参数、Verilog实现和实际调试中容易踩的坑理清楚,适合正在调ADC采集链路、又对SPI时序细节不熟的工程师。

2. SPI协议与4线、3线选型:先认准信号线,再谈读写时序

2.1 四根线的职责:SDI、SDO、SCLK、CS各管什么

SPI叫串行外设接口,本质是一种高速、全双工的同步通信总线。它只占4根信号线:SDI负责接收FPGA写入的寄存器地址和数据,SDO负责把回读的寄存器值送出来,SCLK是FPGA给出的串行时钟,CS是片选使能信号。对ADC这类器件来说,SPI配置接口做的事情很简单——把内部控制寄存器的值读出来或者写进去,从而控制采样率、通道数、输出格式这些行为。

有一种常见的误解是:SPI既然是全双工,那读写就一定可以同时进行。实际上在ADC配置场景里,绝大多数操作是半双工使用:要么写地址加数据,要么读回一段寄存器值。全双工特性在ADC配置中很少用上,信号方向反而是更需要关心的。SDI和SDO这两个名字是从ADC视角命名的,到了FPGA这边,SDI是输出引脚,SDO才轮到输入。搞反这个方向,信号根本接不到一起,配置自然失败。我一般习惯在原理图上直接用FPGA视角标注网络名,避免后期查线时来回倒腾。

2.2 三线模式的本质不是少一根线,而是多了一个三态门

4线模式读写方向分离,时序逻辑简单,是很多ADC的首选。3线模式把SDI和SDO合并成一根SDIO,同一根线上既要送地址又要读数据,于是方向控制成了核心问题。写操作阶段,SDIO由FPGA驱动,表现为输出口;读操作阶段,写完地址后ADC会把SDIO接管过去,此时FPGA必须把自己的引脚切成输入状态。如果两边同时驱动这根线,轻则读回数据全是错的,重则损伤引脚。

所以选3线模式前要确认两件事:FPGA的IO是否支持inout三态;是否愿意为方向切换多写一段控制逻辑。很多工程师觉得3线省一根线就选了它,实际写代码时才发现方向切换时机拿捏不准,调试成本反而更高。我自己的习惯是:PCB上引脚富裕就优先用4线,只有引脚紧张或芯片只提供3线接口时才用3线。另外,某些所谓3线模式更简单,SDIO只做输入用,不读寄存器,那就不涉及三态问题,实现起来和4线接近。

2.3 厂商命名不一致:SEN、CSB、SDIN、SDOUT的对照关系

不同厂商对SPI引脚的命名习惯差异很大,这是新手最容易懵的地方。本文后面会用到的某半导体厂商ADS52J90,用的是SEN、SCLK、SDIN、SDOUT这套命名;另一家半导体厂商的AD9249则叫CSB、SCLK、SDIO。实际功能完全对应:

通用命名ADS52J90AD9249方向(FPGA视角)
CS片选SENCSB输出
SCLK时钟SCLKSCLK输出
SDI数据入SDINSDIO输入阶段输出
SDO数据出SDOUTSDIO输出阶段输入

这张对照表是排查接线错误的第一道工具。收到一份工程代码时,我习惯先把原理图上的网络名与datasheet引脚名做一遍映射,再对着表检查FPGA约束文件里的引脚分配,很多时序错乱问题其实在命名阶段就已经埋下了。

3. 4线SPI读写时序拆解:以某半导体厂商ADS52J90为例

3.1 写操作:8bit地址加16bit数据,24bit要一口气写完

看ADS52J90的写时序图,SDIN上的位流布局很清晰:先送A7~A0共8bit寄存器地址,紧接着送D15~D0共16bit寄存器数据。整个写过程SEN保持低电平,写之前和写完以后SEN都是高电平,SCLK按固定频率给24个上升沿,每个上升沿锁存一位SDIN。

这里容易忽略的是节奏。地址和数据之间不能有间隙,FPGA在发送完第8个SCLK上升沿后,下一拍必须立刻把SDIN切到数据的最高位,中间不能因为状态机跳转产生多余的等待周期。如果多等了一个时钟周期,位流整体错位,写入的寄存器地址和值就全错了。写操作成功的标准是回读该地址时能读到刚才写入的值,所以调试阶段我习惯写完一个关键寄存器就回读一次,而不是把所有寄存器写完再统一回读。

3.2 读操作:地址照写,数据在下降沿输出、上升沿采集

读操作分成两段。前半段和写操作一样,先在SDIN上送出8bit寄存器地址,但紧接着的16bit不再是SDIN上的数据,而是SDOUT上的寄存器值。关键区别在采样时机:SDOUT上的数据是在SCLK下降沿之后才输出的,而且输出后还有一段tOUT_DV延迟,datasheet给出的范围是12ns~28ns。如果在下降沿直接采集SDOUT,大概率采到的是上一bit的尾巴或者过渡电平。

最稳妥的做法是在SCLK上升沿采集SDOUT。此时上一bit的数据已经稳定输出了一段时间,FPGA内部再打两拍同步,可以干净地采到每一位。这个“下降沿输出、上升沿采集”的对称关系是4线SPI读操作的核心。实际代码里,我一般用一个移位寄存器在SCLK上升沿采样SDOUT,采满16bit后一次性锁存到并行寄存器里。

3.3 时序参数与裕量策略:盯着datasheet逐项核对

ADS52J90的datasheet给出了明确的时序参数,几个关键值如下:

参数含义最小值设计建议
tSCLKSCLK周期50ns频率不超过20MHz,实际工程建议用5MHz
tSEN_SUSEN下降沿至第一个SCLK上升沿8ns设计上留10倍以上裕量
tDSUSDIN数据至SCLK上升沿的建立时间5ns用寄存器直接输出即可满足
tOUT_DVSDOUT输出延迟12ns~28ns采集中避开这段区间,选上升沿采样

裕量策略核心原则是:任何时序参数都不要贴着最小值设计。SCLK最高20MHz是极限值,实际跑5MHz甚至1MHz完全够用,配置阶段又不追求速率。SEN、SDIN的建立时间通过寄存器级联输出天然满足,不需要额外处理。只有把SCLK推到20MHz附近时才需要考虑IOB寄存器和输出延迟约束,低速配置下这些属于过度设计。

4. 3线SPI时序与三态控制:以AD9249为例的命令格式和切换时机

4.1 命令格式:R/W位、字节数位、13bit地址、8bit数据

AD9249的3线SPI在数据格式上和4线模式有一个根本差异:多了一段命令前缀。一次完整的读写序列由四部分组成:1bit的R/~W命令位、2bit的W1/W0字节数位、13bit寄存器地址A12~A0、最后是8bit寄存器数据D7~D0。R/~W为高表示读,低表示写;W1W0一般设为00,表示单字节读写。

因此写一个寄存器时,SDIO上要连续送出的位流是:0(写)+00+13bit地址+8bit数据,共24bit。读一个寄存器时则是:1(读)+00+13bit地址,后面的8bit数据由ADC在SDIO上输出。对比一下,4线模式写24bit是“8bit地址+16bit数据”,3线模式写24bit是“3bit命令+13bit地址+8bit数据”,同样是24bit,位流结构完全不同,混用必错。我在代码注释里会直接把位流拼接写出来,比如{1'b0, 2'b00, addr[12:0], data[7:0]},这样过一个月再看也不会糊涂。

4.2 SDIO方向切换:tEN_SDIO和tDIS_SDIO都按10ns参考

3线SPI最难的环节不在数据格式,而在SDIO的方向切换。按AD9249的时序要求,写地址的最后1bit A0在SCLK上升沿被锁存后,ADC会把SDIO从输入口转为输出口,此时FPGA必须同步把SDIO从输出转为输入。这个切换有个最小时间要求:tEN_SDIO最小10ns,参考零点为SCLK下降沿。反过来,读操作完成后SDIO从输出回到输入,对应tDIS_SDIO最小也是10ns,参考零点为SCLK上升沿。

FPGA侧对应的做法是:在SDIO管脚上例化三态门,用一个Tri_en信号控制方向。写地址阶段Tri_en拉高,FPGA驱动SDIO;等到A0发送完毕的那个SCLK下降沿之后再等一拍,把Tri_en拉低,SDIO变成高阻输入,ADC随后输出的数据就可以被正常采样。这个切换时机如果早了半拍,ADC还没完成输入输出转换,SDIO上就是未知态;晚了半拍,ADC已经开始驱动SDIO,FPGA还在驱动,两边必然有一个出错。调试时用示波器同时抓SCLK和SDIO波形,能清楚看到驱动源切换点是否干脆。

5. 避坑指南:FPGA配置ADC的5个翻车现场与解决办法

以下几条是我在多个采集项目里攒下的血泪经验,每一条都在真实板子上翻过车。

5.1 现象:SCLK卡着20MHz上限跑,寄存器时好时坏

原因:tSCLK最小值50ns,意味着20MHz是极限频率。实际PCB上负载电容、走线长度、电平转换都会引入额外延迟,贴着极限跑必然出现偶发时序违例。

解决:把SCLK分频降下来,40MHz系统时钟8分频得到5MHz是稳且快的好选择。想再保守就16分频到2.5MHz,配置阶段不追求速度,稳定优先。

5.2 现象:AD9249回读数据全是0xFF

原因:SDIO三态切换时机不对,FPGA在ADC还没把SDIO驱动起来时就开始采样,采到了高阻态上的弱上拉电平,读回结果自然全是1。

解决:把Tri_en拉低的时间点放在A0锁存后的SCLK下降沿之后,再多等一个系统时钟周期,确保跨过tEN_SDIO的10ns最小间隔。

5.3 现象:寄存器写进去的值回读对不上,地址是乱的

原因:把3线模式的“3bit命令+13bit地址+8bit数据”当成了4线模式的“8bit地址+16bit数据”来发送,位流错位导致寄存器完全错乱。

解决:动手前先在代码里把位流拼接写清楚,区分是{addr[7:0], data[15:0]}还是{1'b0, 2'b00, addr[12:0], data[7:0]},防止结构混淆。

5.4 现象:SEN拉低和第一个SCLK上升沿几乎同步,偶发失败

原因:tSEN_SU要求SEN下降沿至少提前第一个SCLK上升沿8ns,状态机里SEN和SCLK由同一拍产生时,这个建立时间几乎为零。

解决:状态机进入发送阶段时先拉低SEN,空一个周期再启动SCLK翻转,多等一个时钟周期,换来的是可靠性。

5.5 现象:SDOUT接到了FPGA的输出端,信号一直是高电平

原因:命名混淆,习惯按ADC视角理解引脚,SDOUT是ADC的输出,但接到FPGA侧应当作为输入。方向反了,总线被FPGA输出驱动,自然读不到正确数据。

解决:画原理图时统一采用FPGA视角命名,FPGA输出连SDIN、SEN、SCLK,FPGA输入连SDOUT,约束文件里再逐脚核对一遍方向。

6. Verilog实现4线SPI:分频、位发送与回读采样的精简写法

4线SPI的Verilog实现可以拆成三块:SCLK分频、bit发送、回读采样。下面给一个以ADS52J90为目标的最小骨架。

// spi_cfg.v - 4线SPI配置最小实现骨架 module spi_cfg #( parameter CLK_DIV = 8 // 40MHz / 8 = 5MHz )( input wire clk, input wire rst_n, input wire start, input wire [7:0] addr, // 8bit寄存器地址 input wire [15:0] wdata, // 16bit写数据 output reg sclk, output reg sen, output reg sdin, input wire sdout, output reg [15:0] rdata ); reg [3:0] cnt; // 分频计数 reg [5:0] bit_cnt; // 0~23 reg [3:0] state; // 状态机主体省略 endmodule

实现要点:SCLK由计数器翻转产生,SEN拉低要早于第一个SCLK上升沿至少一个系统时钟周期;发送阶段每个SCLK上升沿把sdin更新为待发送位流的最高位,左移推进;回读阶段在SCLK上升沿采集sdout。

// 分频器:生成5MHz SCLK always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 4'd0; sclk <= 1'b1; end else if (cnt == CLK_DIV/2 - 1) begin cnt <= 4'd0; sclk <= ~sclk; end else begin cnt <= cnt + 1'b1; end end

注意:这里的分频方式是占空比50%的简单分频,工程中如果SCLK有相位要求,建议用PLL生成。

分频器在系统时钟计满半周期时翻转一次SCLK,CLK_DIV=8时正好是5MHz。注意SEN不能用sclk域逻辑产生,必须放在系统时钟域单独控制,这样才能保证tSEN_SU。

仿真阶段我一般做三件事:第一,抓sclk和sdin/sdout的相对位置,确认上升沿采样;第二,写一个简单的ADC行为模型,验证回读值等于写入值;第三,把SCLK故意设到20MHz跑一次,看代码在最差时序下是否还能工作。从那以后我每次拿到一颗新的ADC,都会先翻datasheet把时序参数列成表,再对着表写代码,最后仿真确认,这套流程帮我少踩了很多坑。希望帮到你。

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