简介面向FPGA开发与高速ADC调试场景的系列学习文档围绕用FPGA经SPI配置ADC这一常见工程需求梳理从协议基础到读写时序实现的关键知识适合具备一定数字逻辑基础、需要理解ADC寄存器配置流程的工程师与学习者参考。压缩包仅含1个docx文档约787KB内容按系列文章组织便于集中阅读和查阅。文档先说明SPI的全双工四线结构与3线SDIO合并方式再以TI的ADS52J90为例拆解SEN、SCLK、SDIN、SDOUT的写时序与读时序强调地址加数据的24bit写入格式、SEN使能规则及FPGA在SCLK上升沿采样SDOUT的稳定性同时结合tSCLK、tSEN_SU、tDSU等参数说明时序裕量并提及ADI AD9249的三线配置。已有1871人学习适合作为SPI配置ADC的入门梳理与工程排错参考。1. 一根时钟线定生死SPI 为什么能决定整条采集链能不能出数不少做 FPGA 采集的人第一次碰高速 ADC 时都以为电源干净、采样时钟给对、数据总线接上波形就该出来了。真正上手调试才发现芯片上电后数据口是死寂的——不是器件坏了而是内部寄存器还停在出厂默认状态采样率、通道使能、输出编码格式、掉电模式这些位根本没人写过。写这些位的通道就是 SPI。SPISerial Peripheral Interface串行外设接口用四根甚至三根线就把 FPGA 变成了能读写 ADC 内部控制寄存器的主机也正因为线少、协议简单它成了绝大多数 ADC 的标配配置口。把 FPGA 通过 SPI 对 ADC 的配置机制拆到底其实就两件事一帧位流里地址与数据的边界由谁划定以及 FPGA 送出去的那根 SCLK 凭什么能被 ADC 在正确的时刻采到。这两件事想清楚了后面的三线、四线、Verilog 实现都只是同一套逻辑的变体。2. 4 线时序拆帧24bit 里 A7~A0 与 D15~D0 的边界是怎么定死的2.1 一帧写操作就是 24 个上升沿切分点由协议说了算四线模式的物理连接很朴素一根 SDI 从 FPGA 流向 ADC一根 SDO 反向流回一根 SCLK 由 FPGA 提供一根 CS不同厂商叫 SEN、CSB、SS本质相同做片选。真正决定成败的不是这四根线的接法而是位流如何切分。以示例中那颗高速 ADC 为例一次写操作要求 SDIN 上连续送出 24bit前 8bit 是寄存器地址 A7~A0紧接着 16bit 是写进该地址的数值 D15~D0。中间不允许停顿因为 ADC 内部没有“半个帧”的状态SEN 一旦拉高前面收到的位全部作废。从状态机视角看这件事被翻译成一个很具体的约束SEN 拉低之后必须正好产生 24 个 SCLK 上升沿第 1 到第 8 个上升沿锁地址第 9 到第 24 个锁数据第 24 个上升沿结束后才能拉高 SEN。少一个沿最后一个数据位丢失多一个沿ADC 会把多余位当成下一帧的地址开头整个寄存器映射就错位了。常见的实现思路是把帧长做成参数而不是写死 24// 帧长参数化地址位宽 数据位宽 parameter ADDR_W 8; // 4 线模式 8bit 地址 parameter DATA_W 16; // 4 线模式 16bit 数据 localparam FRAME_W ADDR_W DATA_W; // 24 localparam IDLE 3d0, ASSER 3d1, // SEN 拉低等待 tSEN_SU SHIFT 3d2, // 连续产生 FRAME_W 个 SCLK DEASS 3d3, // SEN 拉高等待 tSEN_H DONE 3d4; reg [4:0] bit_cnt; // 递减到 0 表示一帧结束 reg [2:0] state;bit_cnt从FRAME_W-1递减到 0 的这个计数就是位流切分的唯一依据。改芯片时只要改ADDR_W和DATA_W状态机的骨架一行都不用动——3 线模式那种 13bit 地址、8bit 数据的命令字也是靠这两个参数直接复用。2.2 上升沿采样这条约定直接把 SCLK 极性和数据更新沿锁死了时序图上那句“SDIN 的数据在 SCLK 上升沿被写入 SPI”看着像一句废话实际上它同时约束了两端。ADC 端在上升沿采样就意味着在上升沿到来的那一刻SDIN 必须已经稳定了至少 tDSU示例器件给出的是 5ns并且在上升沿之后还要保持一段时间不能被立刻改写。这条约束传导到 FPGA 侧结论只有一个数据必须在 SCLK 的下降沿更新。因为下降沿到下一个上升沿之间隔着半个时钟周期只要这个半周期大于 tDSU 加上 PCB 走线延时和 IO 缓冲延时建立时间就自动满足。反过来如果为了省事在上升沿改数据就等于在 ADC 采样的同一瞬间让数据跳变采到的是旧值还是新值完全看两边器件的速度差异——这类问题在仿真里看不出来上板之后表现为“偶尔写进去偶尔写不进去”是最难查的一类玄学。对应的代码结构通常是两个 always 块一个管发送、一个管接收都以系统时钟为节拍用sclk_fall/sclk_rise作为使能脉冲// sclk_fall / sclk_rise 是分频计数器产生的单周期使能脉冲 always (posedge sys_clk or negedge rst_n) begin if (!rst_n) begin sdi_d 1b0; shift_reg {FRAME_W{1b0}}; bit_cnt 5d0; end else if (sclk_fall bit_cnt ! 5d0) begin sdi_d shift_reg[FRAME_W-1]; // 下降沿换数据 shift_reg {shift_reg[FRAME_W-2:0], 1b0}; // 左移低位补 0 bit_cnt bit_cnt - 1b1; end end // 读阶段在上升沿采 ADC 送回来的 SDOUT always (posedge sys_clk or negedge rst_n) begin if (!rst_n) rx_shift {DATA_W{1b0}}; else if (sclk_rise rd_phase) rx_shift {rx_shift[DATA_W-2:0], sdout_i}; // 上升沿采最稳 endshift_reg里装的正是{reg_addr[7:0], reg_data[15:0]}这 24bit 的拼接结果最高位先出正好对应 A7 先行的顺序。sdi_d在下降沿翻转rx_shift在上升沿采样两边错开半个周期这是这套代码能稳定工作的全部秘密。2.3 读操作复用同一套帧头只是后半段换了个驱动方读寄存器的操作经常被误解成“另一套时序”其实前半段完全一样SEN 拉低SDIN 上送 8bit 地址SCLK 提供时钟。区别只在于第 8 个上升沿之后——写操作继续在 SDIN 上送 16bit 数据读操作则让 SDIN 保持无效电平改由 ADC 在 SDOUT 上吐数据。这里有个容易被忽略的细节SDOUT 的数据是在 SCLK 的下降沿更新的而不是上升沿。示例器件给出的输出有效延时 tOUT_DV 是 12ns 到 28ns也就是说从下降沿算起要过 12ns 到 28ns 这段时间SDOUT 才真正稳定。FPGA 如果在下降沿就去采采到的可能是正在翻转的中间电平而在下一个上升沿去采此时距离数据稳定已经过去了大半个周期是整条时序链上最安全的位置。这个“ADC 下降沿输出、FPGA 上升沿捕获”的错位采样是四线模式下读操作能工作的根本原因也是后面三线模式省掉一根 SDO 线之后仍然能复用的同一套逻辑。四线的好处正在于此收发物理通路分离不需要任何方向切换发送模块和接收模块可以同时挂在总线上互不干扰。3. 时序参数怎么落到代码里tSCLK、tSEN_SU、tDSU 不是抄一遍就完事3.1 50ns 的 tSCLK 下限换算出来是 20MHz但真正的天花板在别处手册上 tSCLK 最小值 50ns直接换算就是 SCLK 频率上限 20MHz很多人算到这一步就停了觉得跑 5MHz 绰绰有余。这个结论没错但它只覆盖了写操作。写操作里数据从 FPGA 出去建立保持时间由 FPGA 自己控制只要半周期大于 tDSU 就没事。读操作的天花板完全是另一回事。前面提到 SDOUT 的 tOUT_DV 最大到 28ns这个时间是从 SCLK 下降沿开始算的。要让 FPGA 在下一个上升沿采到稳定数据半个周期必须大于 28ns也就是周期大于 56ns频率不超过约 17.8MHz。换句话说手册标的 20MHz 是写操作的上限读操作被输出延时卡在了更低的位置。如果按 20MHz 去跑读回校验波形上看不出任何异常读回来的值却时不时错一两个 bit这种翻车最考验人对参数表的理解深度。工程上一般的做法是留足裕量把 SCLK 定在 5MHz 到 10MHz 之间。系统时钟 40MHz 做 8 分频得到 5MHz周期 200ns相对 28ns 的输出延时还有七倍余量代价只是配置一颗几十个寄存器的 ADC 多花几百微秒——相对于整条采集链的启动时间这个代价可以忽略。// 40MHz 系统时钟 8 分频 - 5MHz SCLK占空比 50% reg [2:0] clk_cnt; reg sclk_r; always (posedge sys_clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 3d0; sclk_r 1b0; end else if (clk_cnt 3d3) begin sclk_r 1b1; // 第 4 拍拉高 clk_cnt clk_cnt 1b1; end else if (clk_cnt 3d7) begin sclk_r 1b0; // 第 8 拍拉低 clk_cnt 3d0; end else begin clk_cnt clk_cnt 1b1; end end wire sclk_fall (clk_cnt 3d7); wire sclk_rise (clk_cnt 3d3);分频计数器顺便把sclk_fall和sclk_rise两个使能脉冲免费送出来了前面那两个 always 块直接拿它们当时钟使能不需要真的把 SCLK 当全局时钟用。这个做法避免了门控时钟带来的一系列问题也让整条 SPI 逻辑老老实实待在系统时钟域里时序分析简单很多。3.2 tSEN_SU 与 tDSU 描述的是 ADC 输入端的窗口FPGA 侧要反着算tSEN_SU 最小值 8ns意思是 SEN 的下降沿必须比第一个 SCLK 上升沿早至少 8ns。tDSU 5ns意思是 SDIN 上的数据必须比上升沿早至少 5ns 稳定。这两个参数描述的都是 ADC 引脚上看到的波形不是 FPGA 内部寄存器的输出时刻——中间隔着 IO 缓冲延时、PCB 走线延时、以及输出负载导致的边沿变缓。常见的实现思路是在状态机里插空拍。ASSER状态不是拉低 SEN 就立刻进SHIFT而是原地等几个系统时钟周期。5MHz 的 SCLK 半周期是 100ns等 2 个 40MHz 系统时钟50ns就已经远超 8ns 的要求。这种“用时间换确定性”的做法看起来浪费但它把时序裕量从“刚好够”变成了“根本不可能不够”在板子批次不同、器件替换的情况下能救命。数据侧的 tDSU 因为采用了下降沿更新天然就有半个周期100ns的提前量5ns 的要求被轻松覆盖。真正需要留意的是 SEN 拉高之后到下一次拉低之间的恢复时间以及某些器件要求的 SEN 高电平最短持续时间。这些参数在手册里往往藏在表格角落写代码时如果状态机一路狂飙不插空拍就会踩到。3.3 把窗口写成约束set_output_delay 与 IO 延时的不确定性代码能仿真通过不代表上板能跑。综合工具并不知道 SCLK 和 SDI 之间有一个 5ns 的建立关系它只会按普通的 IO 去布局布线。要让工具知道这件事得靠约束文件把外部器件的时序窗口“反标”进来# 以 SCLK 为参考时钟约束 SDI/SEN 的输出延时 create_generated_clock -name spi_sclk -source [get_pins clk_div_reg[2]/Q] \ -divide_by 2 [get_ports sclk_pad] set_output_delay -clock spi_sclk -max 3.0 [get_ports {sdi_pad sen_pad}] set_output_delay -clock spi_sclk -min -1.0 [get_ports {sdi_pad sen_pad}] # SDOUT 是输入按 ADC 的 tOUT_DV 上下限折算到 SCLK 上升沿 set_input_delay -clock spi_sclk -max 28.0 [get_ports sdout_pad] set_input_delay -clock spi_sclk -min 12.0 [get_ports sdout_pad]-max和-min分别对应建立检查和保持检查。这里刻意把-min写成负值是为了给保持关系留出余量——外部器件在时钟沿之后还会保持一段时间数据FPGA 不能采得太早。set_input_delay里的 28 和 12 直接来自 tOUT_DV 的上下限这两个数字抄错时序报告会给你一个“通过”的假象。需要强调的是这套约束只在 SCLK 确实被当作时钟资源处理时才有意义。如果像前面那样用使能脉冲的方式驱动 SPISCLK 只是一个普通输出管脚这时约束的重点就变成了set_max_delay和set_min_delay这类点对点约束。两种做法的取舍放到最后一章细说。4. 3 线模式的真正难点SDIO 三态切换与两个方向的时间窗4.1 命令字比 4 线多了一段方向位、字节数、13bit 地址三线模式把 SDI 和 SDO 合并成一根 SDIO省下的那根线不是免费的代价是帧格式变复杂了。示例中的多通道 ADC 要求每次传输以命令字开头1bit 方向位、2bit 字节数字段、13bit 寄存器地址然后是 8bit 数据。整个命令字加数据一共 24bit长度和四线的 24bit 恰好相同但语义完全不同——四线是“8 位地址 16 位数据”三线是“16 位命令 8 位数据”。那个 2bit 的字节数字段在多数场景下写成 00表示单次只读写一个寄存器。它的存在是为了支持连续地址的批量读写配置大段寄存器表时能省掉重复的地址开销。手写状态机时如果无脑写 00功能上没毛病只是配置几十个寄存器会多花成倍的时间。方向位的极性是最容易踩坑的地方。示例器件里写操作该位置 1、读操作置 0但换一颗芯片完全可能反过来。这不是协议标准是各家的私有约定移植代码时第一件要核对的事就是这一位。4.2 tEN_SDIO 与 tDIS_SDIO 的参考零点为什么一个在下降沿一个在上升沿三线模式的核心难点不在帧格式而在于同一根线上两个方向的驱动器什么时候交接。ADC 端和 FPGA 端都挂着三态门谁驱动谁高阻必须严格错开否则轻则读回数据错乱重则两端同时驱动造成大电流。手册给了两个关键参数。tEN_SDIO 最小值 10ns描述 ADC 端 SDIO 从输入切换为输出、数据稳定所需的时间参考零点是 SCLK 的下降沿。tDIS_SDIO 最小值也是 10ns描述 ADC 端从输出切换为输入、释放总线所需的时间参考零点是 SCLK 的上升沿。这两个参考零点不同原因在于触发切换的事件不同。读操作时ADC 在上升沿锁存完最后一个地址位 A0此刻它才“知道”接下来要往外送数据于是从下一个下降沿开始驱动 SDIO10ns 后数据稳定——所以参考下降沿。写操作时ADC 是在检测到片选有效或者第一个上升沿之后才释放总线所以参考上升沿。理解了这个因果关系就不会死记这两个参数而是能从“谁在什么时刻决定改变方向”推出来。4.3 Tri_en 拉低的时刻决定了会不会撞总线FPGA 侧的三态控制最终落在一个inout端口和一个使能信号上inout wire sdio_pad; // 顶层双向管脚 reg sdio_out; // 内部待发送数据 reg tri_en; // 1: FPGA 驱动; 0: 高阻交给 ADC 驱动 assign sdio_pad tri_en ? sdio_out : 1bz; wire sdio_in sdio_pad; // 读回时直接采管脚电平写操作全程tri_en保持 1FPGA 独占总线。读操作前半段送 13bit 地址也必须保持 1因为地址是 FPGA 发的关键在于最后一个地址位 A0 被 ADC 在上升沿锁存之后tri_en必须在 ADC 开始驱动之前拉低。拉得太早地址最后几位还没送完就撒手ADC 收到的是残缺地址拉得太晚ADC 已经开始驱动而 FPGA 还占着总线两端对顶。安全的窗口是A0 所在的那个上升沿之后、下一个下降沿之前拉低这样既保证地址完整又赶在 ADC 输出数据之前让出总线。tDIS_SDIO那 10ns 正好保护了这个交接过程——ADC 从输出切回输入也需要时间不会在 FPGA 刚拉低的瞬间就反向驱动。// 读操作地址段结束的那一拍拉低 tri_en always (posedge sys_clk or negedge rst_n) begin if (!rst_n) tri_en 1b1; else if (state SHIFT rd_phase bit_cnt 5d8) tri_en 1b0; // 最后 1bit 地址已发出让出总线 else if (state DEASS) tri_en 1b1; // 帧结束恢复驱动为下一帧做准备 end这里bit_cnt 5d8的判断对应的是“地址段还剩最后一位”的时刻。这个数字跟着地址位宽走13bit 地址就是 88bit 地址就得改成别的值——写死数字是三线模式移植时最常见的错误来源。实际调波形时我一般会把tri_en和sclk一起抓到逻辑分析仪上看确认下降沿的位置落在两个 SCLK 沿之间而不是压在沿上。5. SPI 配置排查读回值是 0x0000 或 0xFFFF 时该从哪查起5.1 现象读回恒为 0x0000 —— 总线根本没人驱动写完配置再读回来校验结果 16 个 bit 全是 0而且换哪个地址读都是 0。这个现象的第一嫌疑不是时序是通路。原因通常是 SDOUT或三线模式下的 SDIO在 FPGA 侧被接成了一个恒定的输入或者三态使能tri_en从头到尾没拉低过FPGA 一直在驱动总线ADC 那边根本没机会说话。排查办法是先不读寄存器直接测管脚电平。在读操作的窗口期把探头搭上去如果电平始终被 FPGA 拉在某个固定值说明三态控制没生效如果电平是浮空的、随手指触碰乱跳说明 ADC 压根没驱动。前者查tri_en的赋值条件后者查片选和帧头是否真的发出去了。血泪经验是这类问题九成出在状态机进入读阶段的条件写错rd_phase一直是 0代码走的还是写通路。5.2 现象读回恒为 0xFFFF —— 上拉把浮空读成了 1同样是读回全错但这次是 16 个 1。这个现象比全 0 更有迷惑性因为乍看像“寄存器默认值就是全 1”。真正的原因往往是总线上挂了一颗上拉电阻而 ADC 侧根本没有驱动于是 FPGA 采到的全是上拉电平。与 5.1 的区别在于全 0 说明有人在下拉或者被强驱动全 1 说明是浮空加弱上拉。分辨这两者的方法是看读回值是否随时间或温度变化——浮空采到的值在临界点附近会抖。解决思路是确认 ADC 的 SDIO/SDOUT 是否真的进入了输出状态。有些器件要求先写某个电源控制寄存器把内部输出缓冲使能默认是关的还有些器件在片选拉高后会立刻释放总线如果 FPGA 读取得太慢数据已经没了。5.3 现象只有最高位或最低位错 —— 采样点偏了半个周期读回值大部分正确错误集中在帧的首位或末位这是典型的采样时刻偏移。原因在于那个“ADC 下降沿输出、FPGA 上升沿采样”的错位约定被破坏了。常见的情况是代码里误用了sclk_fall去采 SDOUT或者分频计数器的极性配反导致实际采样点落在数据翻转的瞬间。解决起来要分两步。先用逻辑分析仪把 SCLK 和 SDOUT 一起抓下来看数据跳变沿和时钟沿的相对位置确认采样点确实在数据稳定的中段再把代码里采样使能从sclk_fall改成sclk_rise验证。如果改完就好了说明之前确实采在了翻转沿上。需要注意的是某些器件的 tOUT_DV 上限比较大示例里是 28ns在 SCLK 频率较高时上升沿采样可能已经逼近数据保持的边界这时唯一的解法是降频而不是继续调相位。5.4 现象换一颗芯片就全乱 —— 地址位宽与方向位极性没核对同一套代码在 A 芯片上跑得好好的换到 B 芯片上读回值全是乱的连帧边界都对不上。这几乎一定是帧格式差异地址从 8bit 变成 13bit数据从 16bit 变成 8bit方向位极性反了或者命令字里多了字节数字段。4 线 24bit 和 3 线 24bit 看着长度一样逐位拆开完全是两回事。移植时我养成的习惯是先列一张对照表把这几个字段逐项填清楚再动代码地址位宽、数据位宽、方向位在帧首还是帧尾、取什么极性、是否存在字节数字段、片选是低有效还是高有效、输出数据在哪个时钟沿更新。这张表填完参数化代码里改几个parameter就能跑不填直接改就会陷入“改一处好一点、再改一处又坏回去”的循环。6. 分频时钟还是使能脉冲SPI 主机模块该做成什么形状写到这里摆在面前的是一个绕不开的设计取舍SPI 主机的 SCLK到底该用分频出来的真实时钟去驱动还是像前面示例那样用一个系统时钟域里的使能脉冲去“模拟”时钟沿。方案优点代价适用场景分频出真实 SCLK波形干净占空比精确时序约束直观引入第二个时钟域跨域处理麻烦门控时钟易被综合工具警告高速率、需要严格占空比系统时钟 使能脉冲单时钟域逻辑全同步参数化容易SCLK 是组合输出需要额外约束频率上限受系统时钟限制中低速配置、多器件复用表格里没有绝对答案但如果配置速率在 10MHz 以下第二种方案的收益明显更大。整条 SPI 逻辑待在系统时钟域里读写状态机、移位寄存器、三态控制全都可以用普通的同步逻辑写不需要考虑跨域握手代价只是 SCLK 的频率必须是系统时钟的整数分频灵活性略低一点。真正需要警惕的是那种“两种方案混着用”的写法用分频时钟驱动状态机又用系统时钟采数据。这类代码在仿真里可能碰巧对上板后随着温度和电压漂移就会出现偶发错误而且极难复现。关于可复用性我在吃过几次亏之后定下一条规矩SPI 主机模块的端口上不要出现任何跟具体芯片有关的东西。帧长、地址位宽、方向位极性、字节数字段是否存在全部做成参数模块对外只暴露start、addr、wdata、rdata、done这几个通用信号。这样四线模式和三线模式的差异就被压缩到了最外层的一个三态控制块上内部状态机一行不改。验证方法也值得单独说一句。不要等整板联调才发现 SPI 有问题先在仿真里对着手册时序图逐位比对波形重点看三个位置片选下降沿到第一个时钟沿的间隔、数据相对时钟沿的提前量、最后一个时钟沿到片选拉高的间隔。仿真通过之后再上板用逻辑分析仪抓同样的三个位置两边对得上这颗芯片基本就拿下了。从那以后我每次开一颗新 ADC都会强制走一遍“先只写不读、只读一个已知默认值寄存器、再全表读回比对”的三步流程绝不跳步。这套流程看着笨但它把问题锁在了最小的范围内比一上来就全表配置、出问题再从头怀疑要快得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?