首页 / 资讯中心 / 文章详情

高速ADC的SPI配置时序与三线SDIO实战排错

高速ADC的SPI配置时序与三线SDIO实战排错 ★ FEATURED ARTICLE
简介面向FPGA驱动开发与硬件调试人员的SPI配置ADC学习资料聚焦如何通过SPI读写ADC内部寄存器并完成稳定时序设计。压缩包仅含1个docx文档大小约787KB将系列文章整理成便于检索的专题材料适合按原理、时序、代码实现顺序查阅。内容从SPI四线制与三线制差异讲起延伸到ADS52J90等ADC的4线读写时序、SEN/SCLK/SDIN/SDOUT信号对应关系、24bit写操作格式8bit地址加16bit数据以及读操作中SDOUT采样位置并提示tSCLK、tSEN_SU、tDSU等datasheet参数与裕量设置。已有1871人学习可帮助读者建立从协议概念到寄存器配置、从时序图到FPGA实现要点的完整认知降低调试ADC配置接口时的排错成本。1. 四根线配置一颗 ADCSPI 为什么成了高速模数转换的默认入口给一块 FPGA 板子上电配置逻辑只往 ADC 发了 24 个时钟回读出来的寄存器值却始终是 0——这类现场我见过太多次最后查出问题的位置几乎都不是“代码写错”而是对 SPI 这条链路上的时序契约理解得不够细。SPISerial Peripheral Interface串行外设接口只用四根线就完成了全双工收发省管脚、好布线于是绝大多数高速 ADC 都把配置入口做成了它。但真正的门槛不在协议本身而在这颗 ADC 把 SPI 改成了什么样子帧长多少、地址几位、数据在哪个沿有效、双向总线什么时候换方向。这些细节决定了你写出来的是“能跑的波形”还是“能工作的配置”。2. 移位寄存器与采样沿SPI 全双工到底是怎么“同时”收发2.1 一个环形移位寄存器解释了全双工为什么不需要第五根线把 SPI 拆到最底层它其实是两个移位寄存器首尾相接构成的环。主机里有一个 8 位或 24 位移位寄存器从机里也有一个同样位宽的移位寄存器两者的输出接对方的输入共用同一个时钟。每一个时钟沿到来两个寄存器同时左移一位主机的最高位被推给从机从机的最低位被推回主机。时钟走满一个位宽两个寄存器就完成了一次“交换”。这个模型解释了三件事。第一为什么没有单独的“读”线和“写”线——收发是同一个移位动作的两面物理上根本分不开。第二为什么 SPI 没有应答机制——它不像 I2C 那样有 ACK 位你发出去的东西从机到底收没收到协议层面是不会告诉你的只能靠回读寄存器自己确认。第三为什么 CS 这么关键移位寄存器要工作必须先有一个“现在开始一帧”的信号来复位位计数这个信号就是 CS。提示正因为没有应答ADC 的 SPI 配置几乎都要配一个“写完再读回比对”的校验步骤别指望写下去就一定生效。2.2 CPOL/CPHA 与“上升沿采样”是怎么被定下来的标准 SPI 定义了四种模式由 CPOL空闲时时钟电平和 CPHA在第一个还是第二个沿采样组合而成。很多人背过这张表但没想过它为什么存在。原因在于主机和从机对“第几个沿算第一个”必须有共同理解否则采到的位会整体错开一位——这就是典型的“读回值刚好是正确值左移一位”的故障。高速 ADC 普遍选择空闲低电平、上升沿采样的组合。这个选择的动机很实际SCLK 空闲时保持低电平第一个上升沿就是明确的“第一位开始”帧的起点没有歧义而数据在下降沿更新、上升沿采样等于给数据留了半个时钟周期的建立时间。半个周期在 5MHz 下是 100ns在 20MHz 下是 25ns这个数字直接决定了你后面能留多少裕量。2.3 命名对不上手册SEN、SDIN、SDOUT 与 CS、SDI、SDO 的映射不同厂商给同一组信号起了不同名字这是让很多人第一次读手册就读不下去的原因。某款高速 ADC下文记作 ADC-A的 SPI 模块管脚叫 SEN、SCLK、SDIN、SDOUT看起来和通用的 CS、SCLK、SDI、SDO 完全不是一回事但逐个对上就会发现SEN 就是片选使能SDIN 是主机写入从机的数据SDOUT 是从机吐回主机的数据SCLK 不变。这个命名差异不是厂商任性。它反映的是功能视角不同通用命名描述的是“方向”而 ADC 手册描述的是“这个脚在这颗芯片上承担什么职责”。SEN 除了片选还兼任整颗芯片 SPI 状态机的使能SDIN 进来的是“地址 数据”的混合流不是纯数据。所以读手册时与其纠结名字不如直接找时序图上这几个信号和时钟的相对关系——那才是唯一不会骗人的部分。2.4 CS 不是电源开关是帧边界初学者常把 CS 理解成“打开芯片的开关”于是写代码时让它在整段配置过程里一直保持低电平。这种写法在简单外设上能跑在 ADC 上大概率翻车。CS 的真正作用是告诉 ADC 内部的移位计数器现在从第 0 位开始。如果 CS 一直不拉高下一次配置的地址位就会接在上一次的尾巴上ADC 会把这段连续的数据流当成一个超长帧来解析结果就是全部错位。ADC-A 的手册要求很明确无论读写操作期间 SEN 必须为低操作开始前和结束后SEN 必须为高。一帧结束就拉高下一个寄存器重新拉低帧与帧之间留出足够的空隙时间。这不是形式要求而是从机硬件复位计数器的唯一依据。3. 4 线时序拆解24bit 帧里地址、数据和建立时间谁先谁后3.1 写帧的 24 个节拍8bit 地址 16bit 数据ADC-A 的写操作帧结构非常规整先发 8 位寄存器地址 A7~A0紧接着发该地址对应的 16 位数据 D15~D0一共 24 位中间不能断。SEN 在整帧期间保持低电平SCLK 提供 24 个上升沿每个上升沿锁存 SDIN 上的一位。这里有个容易想当然的地方为什么是“地址在前、数据在后”而不是反过来因为 ADC 内部要先把地址译码成片内寄存器的行选择信号数据才有地方落脚。地址先到时内部写使能可以随数据位一起流水推进数据先到则必须先缓存 16 位再回头找地址多一级缓冲。在高速 ADC 里每一个额外的缓冲级都意味着延迟和面积所以顺序几乎不可能反过来。写操作的三条共同准则可以归纳成一句话SEN 拉低才工作SDIN 在 SCLK 上升沿写入数据组成永远是“地址 数值”。这三条放到几乎所有支持 4 线 SPI 的 ADC 上都成立差别只在地址位宽和数据位宽。3.2 手册里的四个时间参数决定了代码里能留多少余量时序图上那些密密麻麻的 t 参数不是给看 datasheet 的人增加负担的每一条都会直接翻译成代码里的一个约束。ADC-A 的关键参数如下参数含义手册值对代码的直接约束tSCLKSCLK 周期最小值50ns时钟频率上限 20MHz40MHz 系统时钟最多 2 分频tSEN_SUSEN 下降沿到首个 SCLK 上升沿≥8nsCS 拉低后不能马上打时钟中间要插等待tDSUSDIN 相对 SCLK 上升沿的建立时间≥5ns数据必须提前稳定不能和采样沿同拍更新tOUT_DVSCLK 下降沿到 SDOUT 数据有效12~28ns读数据不能在下降沿立即采要等稳定窗口注意 tOUT_DV 是一个区间不是固定值。手册给出 12~28ns意味着最坏情况下要等 28ns 数据才稳。做余量计算时永远用最坏值5MHz 的 SCLK 半周期是 100ns减去 28ns 还有 72ns 余量非常安全但如果为了提速把 SCLK 拉到 20MHz半周期只剩 25ns减掉 28ns 直接变成负的——这就是为什么“上升沿采样最稳”这句话在低频成立、在高频必须重新算一遍。3.3 用系统时钟打拍产生 SCLK分频器 状态机的 Verilog 骨架假设 FPGA 系统时钟 40MHz要得到 5MHz 的 SCLK就是 8 分频。常见的实现思路是用计数器翻转一个寄存器输出而不是拿计数器的某一位直接当时钟用——后者会引入门控时钟综合工具会警告时序也更难收敛。// 40MHz 系统时钟8 分频得到 5MHz 的 SCLK // 关键点sclk_r 是由寄存器打拍输出的不是门控时钟 localparam DIV 8; reg [2:0] div_cnt; reg sclk_r; reg sclk_r_d; // 上一拍用来做边沿检测 always (posedge clk_40m or negedge rst_n) begin if (!rst_n) begin div_cnt 3d0; sclk_r 1b0; // 空闲低电平对应 CPOL0 sclk_r_d 1b0; end else if (spi_busy) begin sclk_r_d sclk_r; if (div_cnt (DIV/2 - 1)) begin div_cnt 3d0; sclk_r ~sclk_r; // 每半个周期翻转一次占空比 50% end else begin div_cnt div_cnt 1b1; end end else begin div_cnt 3d0; sclk_r 1b0; // 帧结束回到空闲电平 sclk_r_d 1b0; end end // 边沿检测后续所有移位动作都挂在这两个脉冲上 wire sclk_rise (sclk_r 1b1) (sclk_r_d 1b0); wire sclk_fall (sclk_r 1b0) (sclk_r_d 1b1);DIV取 8 对应 5MHz改成 4 就是 10MHz改成 2 就是 20MHz 的极限值。div_cnt计到DIV/2 - 1翻转保证高低电平各占一半。把边沿检测成sclk_rise和sclk_fall两个单周期脉冲是为了让后续逻辑全部工作在 40MHz 时钟域里不用跨时钟域处理——这是软核 SPI 能稳定的关键习惯。有了时钟接下来是帧状态机。核心思路是数据在下降沿更新在上升沿计数这样数据永远比采样沿早半个周期就位。reg [23:0] tx_sr; // 写帧{A7~A0, D15~D0} reg [15:0] rx_sr; // 读帧16bit 回读数据 reg [7:0] addr_sr; // 读帧先发出去的 8bit 地址 reg [4:0] bit_cnt; reg cs_n; localparam S_IDLE 3d0, S_WR 3d1, S_GAP 3d2, S_RDA 3d3, S_RDD 3d4, S_DONE 3d5; always (posedge clk_40m or negedge rst_n) begin if (!rst_n) begin cs_n 1b1; bit_cnt 5d0; state S_IDLE; end else begin case (state) S_IDLE: if (start) begin tx_sr {reg_addr, reg_wdata}; // 地址在高位数据在低位 cs_n 1b0; bit_cnt 5d0; state S_WR; end S_WR: begin // 下降沿移出下一位距下一个上升沿还有半个周期满足 tDSU if (sclk_fall) tx_sr {tx_sr[22:0], 1b0}; if (sclk_rise) begin if (bit_cnt 5d23) begin // 24 位发完 cs_n 1b1; // 帧结束立刻拉高 bit_cnt 5d0; state S_GAP; end else begin bit_cnt bit_cnt 1b1; end end end S_GAP: begin // 帧间隔CS 保持高给从机复位帧状态机的时间 if (gap_cnt GAP_MAX) begin addr_sr reg_addr; cs_n 1b0; bit_cnt 5d0; state S_RDA; end end S_RDA: begin if (sclk_fall bit_cnt ! 5d7) addr_sr {addr_sr[6:0], 1b0}; if (sclk_rise) begin if (bit_cnt 5d7) begin // 地址发完 bit_cnt 5d0; state S_RDD; // 此后只收不发 end else begin bit_cnt bit_cnt 1b1; end end end S_RDD: begin if (sclk_rise) begin rx_sr {rx_sr[14:0], sdo}; // 上升沿采 SDOUT if (bit_cnt 5d15) begin cs_n 1b1; state S_DONE; end else begin bit_cnt bit_cnt 1b1; end end end default: state S_IDLE; endcase end end这段骨架里有两个刻意的设计。第一写帧用 24 位移位寄存器一次性装好地址加数据移位方向从高位到低位tx_sr[23]直接接 SDIN省掉了额外的位选择逻辑。第二移位动作挂在sclk_fall上而不是sclk_rise这样 SDIN 在每个上升沿前已经稳定了整整半个周期tDSU 的 5ns 要求轻松满足哪怕 PCB 上时钟和数据走线有几 ns 的偏差也吃得下。状态机里S_GAP那一步经常被省略。省掉的后果不是立刻报错而是某些寄存器偶发写不进去——因为从机的帧计数器没被 CS 复位干净下一帧的地址位被它当成了上一帧的延续。3.4 读帧里最容易被忽略的一段tOUT_DV 与上升沿采样的余量计算读操作分两段先往 SDIN 发 8 位地址然后从 SDOUT 收 16 位数据。理论上的说法是“地址最后一位在上升沿锁存后接下来每个下降沿 SDOUT 输出一位”。但手册里那句“每次下降沿输出的数据要经过 tOUT_DV 才稳定”才是真正决定成败的一句。把时间轴摊开看SCLK 下降沿到来 → ADC 内部把寄存器位推到 SDOUT 引脚 → 信号经过输出缓冲和 PCB 走线 → 12~28ns 后电平才真正稳定。如果 FPGA 在下降沿附近去采采到的很可能是上一位的残留值或者过渡中的电平。而在下一个上升沿采距离下降沿已经过了半个周期5MHz 下是 100ns即便按最坏的 28ns 算也还有 72ns 余量。所以“在上升沿采 SDOUT 最稳”不是一句经验口诀而是一道可以算的题半周期 − tOUT_DV(max) − FPGA 输入建立时间 0。频率提上去之后这道题要重算算不过就得降频或者改用带延迟校准的 IDELAY 原语。代码里S_RDD状态把采样动作钉在sclk_rise上就是在兑现这个结论。4. 3 线 SDIO 的三态窗口读操作为什么是最容易翻车的一步4.1 3 线帧格式1bit 读写标志 2bit 字节数 13bit 地址 8bit 数据3 线模式下 SDI 和 SDO 合并成一根 SDIO省了一根线代价是帧格式必须自己带方向信息。某款多通道 ADC下文记作 ADC-B的帧结构是1 位 R/~W 标志高电平读、低电平写 2 位字节数字段 W1W0一般填 0表示每次只读写一个寄存器 13 位地址 A12~A0 8 位数据 D7~D0。对比 4 线模式就看出差别了4 线是“地址 数据”靠物理上两根独立的线来区分方向3 线是“命令 地址 数据”方向信息被编码进了数据流的第一位。写操作时把 R/~W 置 0后面跟着地址和要写的值流程和 4 线几乎一样。真正的麻烦全在读操作上。4.2 读操作的 I/O 翻转窗口tEN_SDIO 与 tDIS_SDIO 到底卡在哪里读操作要发 1 2 13 16 位头部然后收 8 位数据。关键的一幕发生在第 16 个上升沿地址最后一位 A0 被锁存的那一瞬间ADC 内部会把 SDIO 从输入切成输出准备往外吐数据。这个切换不是瞬时完成的。手册给出两个参数tEN_SDIO 是 SDIO 由输入变输出的转换时间最小 10ns参考零点是 SCLK 下降沿tDIS_SDIO 是由输出变输入的转换时间最小也是 10ns参考零点是 SCLK 上升沿。这两个数字组合起来定义了 FPGA 必须让出总线的窗口——在 ADC 开始驱动之前FPGA 的驱动器必须已经进入高阻否则两边同时驱动一根线轻则读出乱码重则长时间对拉烧管脚。我一般会这么理解这两个参数tDIS_SDIO 说的是“ADC 什么时候松手”tEN_SDIO 说的是“ADC 什么时候接手”FPGA 的释放动作必须落在这两者之间的空档里。放在时序图上就是地址最后一位的上升沿之后、下一个下降沿再往后 10ns 之前这段时间里把tri_en拉低最安全。4.3 FPGA 侧 inout 与 tri_en 的实现细节3 线模式的 FPGA 端口必须声明成inout内部再拆成“要发出去的值”和“要不要驱动”两个信号。这是 Verilog 里少数几个必须依赖三态门原语的场景。// SDIO 三态tri_en1 时 FPGA 驱动总线0 时释放为高阻 // 注意 sdio 必须是顶层端口综合工具才会把它映射到 IOBUF inout sdio; assign sdio tri_en ? sdio_out : 1bz; reg tri_en; always (posedge clk_40m or negedge rst_n) begin if (!rst_n) begin tri_en 1b0; // 复位后一律不驱动总线 end else begin case (state) S_WR: tri_en 1b1; // 写操作全程驱动 S_RD_H: tri_en (bit_cnt 5d15); // 读操作前 16 位命令地址驱动 // 第 16 个上升沿之后立刻释放落在 tEN_SDIO 窗口内 default: tri_en 1b0; endcase end end // 内部把 SDIO 拆成 SDI / SDO 两个逻辑信号后续逻辑与 4 线完全一致 wire sdi_int sdio; // 从机吐回来的数据 wire sdo_int tx_sr[23]; // 要发出去的数据 assign sdio_out sdo_int;assign sdio tri_en ? sdio_out : 1bz这一行是整个 3 线实现的核心。1bz是高阻态综合工具看到它才会把管脚配置成真正的双向 IO写成1b0或1b1就变成了单向输出读操作永远读不到东西。tri_en的拉低时刻用bit_cnt 15判断是因为头部正好 16 位。这个判断必须基于位计数而不是固定延时——延时随温度和电压漂移位计数不会。释放之后sdi_int就接管了后续 8 个上升沿的采样动作和 4 线模式里采 SDOUT 的逻辑一模一样。4.4 双向总线上的竞争、上拉与“中间电平”三线 SDIO 上最容易观测到的异常现象是用示波器看到电平停在 1.6V 左右既不是高也不是低。这不是信号完整性问题而是两个驱动器在很短的时间内同时导通形成了分压。根源几乎都是tri_en释放得太晚撞上了 ADC 开始输出的时刻。除了时序还有两个硬件层面的细节值得提前处理。第一SDIO 上加一个 10k 级别的上拉电阻让总线在双方都不驱动的那几十纳秒里有一个确定电平不会因为寄生电容缓慢漂移而被误判成有效数据。第二FPGA 侧的inout管脚不要开内部弱上拉又外接强上拉两个上拉叠加会让上升沿变陡、过冲变大反而更容易在高速下出错。注意三线模式省下的那根线代价是把“方向控制”这件事从物理层挪到了逻辑层。任何一次tri_en时序上的疏忽都会直接表现为总线冲突而不是一个温和的读错误。5. 排查清单波形看着对却不工作的五种现场5.1 写进去读回还是旧值现象配置流程跑完回读某个寄存器读出来的值和写入值不一致而且往往是复位默认值。逻辑分析仪上 SCLK、SDIN 波形看着完全正常。原因最常见的是帧间 CS 没有拉高或者拉高时间太短。ADC 内部的帧计数器没有被复位下一帧的地址位被拼到上一帧尾部导致地址译码错位写操作落到别的寄存器上甚至根本没落进去。其次是写保护类寄存器没有先解锁——不少 ADC 有专门的保护位不打开就拒绝对关键寄存器写入。解决确认每帧结束cs_n立刻拉高并按手册的 tCSH 要求保持若干个 SCLK 周期配置序列开头先写保护解锁寄存器写完立刻回读比对不一致就重试一次并把错误计数打出来。回读这一步不能省它就是这套系统的黑匣子。5.2 读回值刚好错一位现象读回值和正确值呈左移或右移一位的关系比如应该读到0x1234实际读到0x2468或0x091A。原因位计数和时钟沿对不上。典型情况是读地址阶段多打了一拍把地址最后一位当成了数据第一位或者 CS 释放提前采样窗口整体前移。也可能是 SCLK 和数据在 PCB 上的走线延迟差太大导致实际采样点偏离了设计意图。解决把bit_cnt的初值和终止条件在仿真里逐拍打出来对照时序图数格子不要靠肉眼数波形确认读地址阶段最后一位的上升沿之后才开始收数据如果怀疑走线延迟把 SCLK 降一档再看频率降下来错误消失基本就能确认是时序裕量不足。5.3 换一块板子就偶发误码现象实验室里跑得好好的换一批板子或者温度变化之后配置偶尔失败重新上电又好。原因时序裕量吃得太紧。SDIN 相对 SCLK 上升沿的建立时间只剩几 ns一旦 PCB 走线长度、器件批次或者温度带来一点点偏移就踩线了。这类问题在室温下的样板间里几乎不可能复现属于典型的血泪经验。解决把数据更新动作放到 SCLK 下降沿等于把建立时间从“几 ns”拉到“半个周期”同时把 SCLK 频率从极限值往下调一档牺牲一点配置时间换稳定在综合约束里给 SPI 输出脚加 output delay让工具帮你把这条路径算进去。5.4 读操作时 SDIO 电平卡在中间值现象三线模式读寄存器示波器上 SDIO 在一段区间里稳定在 1.5~1.7V读回数据全是乱码芯片还有点发热。原因FPGA 和 ADC 同时驱动总线。tri_en拉低得太晚ADC 已经开始输出FPGA 还在推自己的电平两边对拉。反过来如果tri_en拉低太早ADC 还没接手总线进入浮空采到的是噪声。解决把释放点严格算进 tEN_SDIO 窗口也就是最后一个地址位的上升沿之后、下一个下降沿再往后 10ns 之前总线上加确定性上拉条件允许的话用 ILA 同时抓tri_en和 SDIO确认释放动作和 ADC 接管动作之间确实有空档。5.5 上电第一次配置全部失效重配一次就好现象冷启动后配置流程跑完ADC 工作不正常重新执行一遍配置一切正常。原因ADC 内部上电复位还没完成或者内部基准、PLL 还没锁定此时写进去的寄存器被忽略。有些芯片的手册会写“上电后需等待若干 ms 才能访问 SPI”但这句话经常藏在电气特性表的脚注里很容易漏掉。解决上电后先做固定延时再开始配置延时值按手册给的最大上电时间再加一倍余量配置完成后回读若干关键寄存器做校验不一致就整体重配一次把“配置成功”做成一个明确的状态位而不是上电就跑、跑完不管。6. 软核 SPI 还是硬核 SPI把取舍算清楚再动手做到这一步绕不开一个选择自己写状态机还是用 FPGA 里现成的 SPI 硬核。两者没有绝对优劣关键看这颗 ADC 的帧格式离标准有多远。维度软核状态机 移位寄存器硬核 SPI 控制器帧格式灵活性任意24bit、命令13bit 地址都能拼受限于固定位宽和固定时序模板时序裕量控制精确到单个系统时钟周期由 IP 参数决定可调粒度粗三态方向控制自己写tri_en窗口可精确摆放部分 IP 支持但翻转点不透明资源开销几百个 LUT可忽略省 LUT但占用专用硬核调试可见性所有中间信号可抓内部状态机是黑匣子我自己的判断标准很简单如果这颗 ADC 的帧里有“命令字节”“字节数字段”这类非标准结构或者读操作需要精确控制总线翻转点就老实写软核。硬核适合那些帧格式完全符合标准 SPI 的外设比如普通的 Flash、传感器用起来确实省事但只要碰上 3 线 SDIO 这种必须在特定比特位翻转方向的场景硬核的抽象层反而成了障碍你得绕着它做各种 workaround最后还是回到手写状态机。验证方法上我更依赖“写—回读—比对”这条闭环而不是单纯看波形。波形能告诉你时序对不对回读才能告诉你配置到底生效没有。具体做法是配置序列里的每一个关键寄存器写完立刻读回和期望值逐位比对不一致就把地址、期望值、实际值一起打到调试端口上。这套机制加上 ILA 抓 CS、SCLK、SDIN、SDOUT 四路信号逐拍对照手册基本上能把 90% 的问题挡在实验室里。还有一个容易被忽略的取舍SCLK 频率到底定多高。手册给的上限是能力边界不是推荐值。配置过程本身只占整个系统启动的几毫秒为了这几毫秒把时序裕量压到极限换来的风险远大于收益。我一般的做法是取手册上限的一半到三分之一把省下来的时间裕量留给温度、批次和布线差异。从那以后我每次给一颗新 ADC 写配置逻辑都强制走一遍“先算 tOUT_DV 余量、再定 SCLK 频率、写完必回读比对”的流程哪怕这颗芯片和上一颗看起来只差一个型号后缀。SPI 这条链路简单到让人放松警惕而它出问题的方式又偏偏安静得没有任何提示。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站