资讯详情

FPGA通过SPI配置ADC:4线/3线时序与Verilog实现避坑

📅 2026/10/29 20:59:42 | 华诺云谱 👁 阅读
FPGA通过SPI配置ADC:4线/3线时序与Verilog实现避坑
简介《FPGA通过SPI对ADC配置系列文章》是一份面向FPGA工程师、硬件开发者和高速ADC调试入门者的技术文档围绕如何用FPGA经SPI接口完成ADC寄存器配置展开兼顾协议基础与工程时序分析。压缩包共1个docx文件约787KB内容以系列文章形式组织便于按主题连续阅读和检索。文档从SPI串行外设接口的基本概念讲起说明4线模式中SDI、SDO、SCLK、CS的作用以及3线模式将SDI与SDO合并为SDIO的差异随后以TI的ADS52J90为例拆解SEN、SCLK、SDIN、SDOUT的写/读时序、24bit地址加数据格式、SEN使能要求和tSCLK、tSEN_SU、tDSU等时序参数并提示代码实现中的裕量问题。已有1871人学习适合需要理解ADC配置流程、对照datasheet分析时序或排查SPI配置问题的读者参考。1. 项目定位FPGA通过SPI配置ADC这份系列文档解决什么问题板子上的高速ADC上电后寄存器是默认值采样格式、通道使能、测试模式都没配FPGA通过SPI读回来全是0查了两天才发现是CS拉低的时间不够。这类问题在数据采集板卡上太常见了ADC能出时钟、能出数据但配置寄存器写不进去或者写进去读出来对不上。FPGA通过SPI对ADC配置这件事说到底是把datasheet里的时序图翻译成Verilog状态机再把时间参数翻译成分频系数和采样沿。这份系列文档一共五篇从SPI是什么讲起先拆4线模式的读写时序再拆3线模式的三态控制最后落到Verilog实现。它适合做数据采集、软件无线电、医疗成像前端这类板卡的FPGA逻辑工程师也适合刚接手ADC配置模块、对着时序图不知道从哪下手的开发者。SPI ADC这个组合看着简单真到上板阶段翻车点基本都在时序裕量和三态切换上。2. 硬件层拆解4线/3线SPI信号线、命名映射与三态口2.1 4线SPI的4根线到底谁驱动谁4线SPI的4根线是SDI、SDO、SCLK、CS。方向固定SCLK和CS永远由FPGA输出SDI由FPGA输出进ADCSDO由ADC输出进FPGA。这4根线里SCLK是节奏CS是门控SDI/SDO是数据。很多ADC厂商不按通用名命名比如某厂商把CS叫SEN把SDI叫SDIN把SDO叫SDOUTSCLK不变。看datasheet第一件事就是把命名映射列出来不然时序图对不上代码。通用名某厂商命名方向作用CSSENFPGA → ADC帧使能低有效SDISDINFPGA → ADC地址和数据输入SDOSDOUTADC → FPGA寄存器数据输出SCLKSCLKFPGA → ADC串行时钟4线模式的好处是读写可以同时进行但ADC配置场景里读写很少并发所以4线更多是为了布线和调试方便读的时候SDO单独一根示波器好抓。FPGA侧这4根线一般直接接普通IO不需要特殊原语但SCLK如果频率高建议加IOBUF或者用ODDR输出避免时钟走普通布线资源。2.2 3线SPI为什么不是简单少一根线3线SPI把SDI和SDO合并成SDIO一根线分时复用。写的时候FPGA驱动SDIO读的时候ADC驱动SDIO。问题就在这里一根线上有两个驱动器必须靠三态门分时切换。FPGA端的SDIO要定义成inout内部用tri_en控制tri_en1时FPGA输出tri_en0时FPGA端高阻ADC输出。有些ADC的3线模式更简单SDIO只做输入只能写不能读这种实现起来就是普通输出口不用三态。但带读回功能的3线ADC三态控制是绕不开的。3线模式省一根线代价是方向切换的时序要卡准。切早了FPGA和ADC同时驱动仿真出X上板可能发热切晚了ADC已经开始输出FPGA还没释放读到的第一位可能是错的。SDIO线上一般会加一个上拉电阻保证总线空闲时电平确定这个电阻在原理图上别省。2.3 时序参数从datasheet里怎么读时序图上的箭头不是装饰每个箭头对应一个参数。常见的有以下几个代码里不是直接写ns值而是换算成分频系数和计数器阈值。参数含义示例值代码落地方式tSCLKSCLK周期最小值50ns分频后周期要大于它40MHz 8分频得5MHztSEN_SUSEN下降沿到第一个SCLK上升沿8nsCS拉低后延时几个系统时钟再动SCLKtDSUSDIN相对SCLK上升沿的建立时间5ns数据在SCLK下降沿改上升沿采样tOUT_DVSDOUT在SCLK下降沿后的有效时间12~28nsFPGA在下一个上升沿采样tEN_SDIOSDIO由输入切输出的时间10ns写完地址最后一位后立即拉低tri_entDIS_SDIOSDIO由输出切回输入的时间10ns读完数据后延时再拉高tri_en系统时钟40MHz周期25ns要产生5MHz SCLK就是8分频半周期4个系统时钟等于100ns远大于tSCLK的50ns裕量够。tSEN_SU8ns在8分频下CS拉低到第一个SCLK上升沿隔了100ns也够。关键是tOUT_DV28nsFPGA在SCLK上升沿采样时数据已经稳定了至少半个SCLK周期所以上升沿采样是安全的。注意不同ADC的tOUT_DV差异很大有的只有几ns这时候要么降低SCLK频率要么把采样点往后挪。不能拿一款芯片的代码直接套另一款。另外时序参数要看最坏情况不能只看典型值。datasheet给的范围是12ns到28ns代码里要按28ns算裕量而不是按12ns算。我一般会把SCLK频率先降到1MHz跑通再逐步往上提提不上去就说明裕量不够回头看是哪个参数卡住了。3. 4线SPI时序与实现24bit帧、读回采样沿与Verilog状态机3.1 写时序24bit一帧SEN全程拉低4线写操作一帧24bit先8bit地址A7~A0再16bit数据D15~D0。SEN在整帧期间保持低帧前帧后为高。SCLK每个上升沿采样SDIN的一位24个上升沿采完一帧。这里有一个容易忽略的点地址和数据之间没有间隙第8个上升沿采完A0第9个上升沿马上采D15中间不能多打拍。// 4线SPI写寄存器24bit 8bit addr 16bit data // 行为级写法便于仿真理解综合时改状态机 task spi_write; input [7:0] addr; input [15:0] data; integer i; begin sen_n 1b0; // 帧开始SEN拉低 #(SEN_SETUP); // 满足tSEN_SU实际给100ns for (i 23; i 0; i i - 1) begin sclk 1b0; if (i 15) sdi addr[i-16]; // 先发地址高位在前 else sdi data[i]; // 再发数据高位在前 #(HALF_PERIOD); // 数据建立时间 sclk 1b1; // 上升沿ADC采样 #(HALF_PERIOD); end sclk 1b0; #(SEN_HOLD); // 帧后保持 sen_n 1b1; // 帧结束SEN拉高 end endtask逻辑说明循环从23到0前8次i15走地址分支后16次走数据分支。sclk先拉低再放数据保证数据在上升沿前稳定。HALF_PERIOD是半个SCLK周期按5MHz算100ns。这个task里的延时不可综合只用于仿真验证时序关系真正上板要改成状态机加计数器。参数说明SEN_SETUP至少8ns代码里给100nsSEN_HOLD同样留裕量。地址位宽8位数据位宽16位这是这款ADC的帧格式换芯片要按datasheet改。3.2 读时序先写地址再在下降沿输出、上升沿采样读操作分两段先往SDIN写8bit地址然后ADC在SCLK下降沿把对应寄存器的16bit数据从SDOUT逐位输出。注意SDOUT在下降沿变化经过tOUT_DV后才稳定所以FPGA在下一个上升沿采样最稳。很多新手会在下降沿直接采仿真看着对上板因为tOUT_DV不够高位偶尔出错。// 读寄存器先写8bit地址再收16bit数据 // SDOUT在SCLK下降沿更新FPGA在上升沿采样 localparam RD_ADDR 2d1, RD_DATA 2d2; reg [4:0] bit_cnt; reg [15:0] rd_shift; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 5d0; rd_shift 16d0; end else case (state) RD_ADDR: begin sen_n 1b0; sclk 1b0; sdi addr[7 - bit_cnt]; // 高位先出 if (bit_cnt 5d7) begin bit_cnt 5d0; state RD_DATA; end else begin bit_cnt bit_cnt 1b1; end end RD_DATA: begin sclk ~sclk; // 产生SCLK if (sclk) // 上升沿采样SDOUT rd_shift {rd_shift[14:0], sdo}; if (bit_cnt 5d15) begin sen_n 1b1; // 收完16bitSEN拉高 state IDLE; end else if (!sclk) begin bit_cnt bit_cnt 1b1; end end endcase end逻辑说明RD_ADDR状态逐位发地址RD_DATA状态翻转SCLK并在上升沿移位收数据。sclk~sclk放在时钟沿里实际综合出来是分频计数器更稳这里为了看清采样沿。rd_shift是16位移位寄存器每来一个上升沿左移一位把sdo塞到最低位。参数说明bit_cnt位宽5位够数24SEN在RD_DATA收完第16位后拉高满足帧后保持时间。读回的数据要和写进去的值比对不一致就说明时序或者地址有问题。3.3 SCLK分频与状态机骨架系统时钟40MHz8分频得5MHz。分频计数器3位计数0~7计到3拉低、计到7拉高占空比50%。分频计数器要自由跑不要用门控时钟否则综合和时序分析都麻烦。// 40MHz - 5MHz SCLK8分频 reg [2:0] div_cnt; reg sclk_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 3d0; sclk_r 1b0; end else if (div_cnt 3d7) begin div_cnt 3d0; end else begin div_cnt div_cnt 1b1; end sclk_r (div_cnt 3d4) ? 1b0 : 1b1; // 前4拍低后4拍高 end // 主状态机 localparam IDLE 3d0, WR_REG 3d1, WR_WAIT 3d2, RD_ADDR_S 3d3, RD_DATA_S 3d4, DONE 3d5; reg [2:0] state, next_state;逻辑说明div_cnt自由跑sclk_r按计数值切高低这样SCLK和系统时钟同步避免门控时钟。状态机用三段式或两段式都行关键是每个状态里sclk的边沿要和数据对齐。WR_REG状态发24bit写帧WR_WAIT等SEN拉高后再进读读状态按3.2的流程走。参数说明分频系数8对应5MHz如果要改频率改div_cnt比较值即可比如4分频得10MHz但要确认不超过tSCLK最小值的倒数。状态机编码用3位够6个状态实际项目里状态多了要扩位宽。4. 3线SPI时序与实现SDIO三态切换与inout写法4.1 3线帧格式控制命令13bit地址8bit数据3线一帧24bit1bit R/W 2bit W1W0 13bit地址A12~A0 8bit数据D7~D0。R/W1读0写W1W0一般设00表示每次读写一个寄存器。写操作1bit 0 2bit 00 13bit地址 8bit数据。读操作1bit 1 2bit 00 13bit地址然后SDIO切输出ADC在下8个SCLK下降沿吐出8bit数据。注意读操作的命令段只有16bit不是24bit数据段由ADC驱动。// 3线SPI帧拼接写10013bit addr8bit data // 读10013bit addr然后SDIO切输入收8bit wire [23:0] wr_frame {1b0, 2b00, addr13, data8}; wire [15:0] rd_frame {1b1, 2b00, addr13}; // 16bit命令段逻辑说明读帧只发16bit命令数据段由ADC驱动。写帧24bit全由FPGA驱动。R/W位在最高位W1W0固定00地址13位数据8位。参数说明addr13是13位地址data8是8位数据如果芯片支持多字节读写W1W0要改但配置寄存器场景一般设00。4.2 SDIO方向切换tEN_SDIO与tDIS_SDIO读操作时FPGA写完地址最后1bit A0后ADC在接下来的SCLK下降沿把SDIO从输入切输出切换时间tEN_SDIO最小10ns。FPGA必须在同一个下降沿附近把tri_en拉低让出总线。写操作结束时ADC在SCLK上升沿后tDIS_SDIO最小10ns切回输入FPGA再把tri_en拉高。这个切换点卡不准读回的数据第一位就会丢或者出现总线冲突。// SDIO三态控制tri_en1 FPGA输出tri_en0 FPGA高阻 inout sdio; wire sdi_int sdio; // 读回的数据 reg sdo_int; // 要写出的数据 reg tri_en; assign sdio tri_en ? sdo_int : 1bz; // 读操作发完地址最后1bit后拉低tri_en always (posedge clk or negedge rst_n) begin if (!rst_n) tri_en 1b1; else if (state RD_ADDR_S bit_cnt 5d15) tri_en 1b0; // 地址发完让出SDIO else if (state RD_DATA_S bit_cnt 5d7) tri_en 1b1; // 8bit收完收回SDIO end逻辑说明assign那行是三态门本体tri_en为0时sdio呈高阻ADC可以驱动。sdi_int直接接sdio读的时候拿到的就是ADC输出。读地址阶段tri_en为1FPGA驱动地址发完立即拉低正好卡在ADC切换输出的下降沿附近。参数说明tEN_SDIO和tDIS_SDIO都要大于等于10ns8分频下半个SCLK是100ns裕量足够。如果SCLK提到20MHz半周期25ns仍大于10ns但仿真要重点看切换沿。tri_en的复位值设1保证上电后FPGA先控制总线。4.3 三态控制的Verilog写法与仿真观察点三态控制的坑主要在仿真如果testbench里ADC模型没有在正确时刻驱动sdio或者FPGA的tri_en和ADC的oe信号重叠sdio会出现X。仿真时重点看三个信号tri_en、sdio、adc_oe要求在任何时刻最多只有一个为有效驱动。我一般会在testbench里加一条断言跑回归时自动抓总线冲突。// 仿真检查tri_en和adc_oe不能同时为1 always (posedge clk) begin if (tri_en 1b1 adc_oe 1b1) $display(ERROR: SDIO bus conflict at %0t, $time); end逻辑说明这是仿真断言上板前跑一遍能提前发现方向切换重叠。adc_oe是ADC行为模型的输出使能实际芯片内部有tEN_SDIO延迟模型里也要加#10延迟才真实。参数说明断言里的1b1表示有效驱动如果模型用高阻表示释放比较时要用而不是避免X传播。仿真通过后再上板能省很多返工。5. 避坑排查仿真、时序、三态与上板翻车点5.1 读回来全是0或全1现象FPGA发起读操作rd_shift一直是16h0000或16hFFFF写进去的配置读不回来。原因SEN/CSB没有在整帧期间保持低或者帧前建立时间不够ADC根本没进入SPI模式。也有可能是SCLK频率超过tSCLK最小值对应的上限ADC采样不到。解决检查状态机里sen_n的赋值确保读写全程为0帧前至少留tSEN_SU。用示波器抓SEN和第一个SCLK上升沿看间隔。SCLK先降到1MHz试能读回来再提频。5.2 写进去读出来地址偏移一位现象写地址0x10读0x10回来是写0x08的值数据整体右移了一位。原因SDIN在SCLK上升沿采样但代码在下降沿驱动数据或者bit_cnt边界差一拍地址最后一位被当成数据第一位。读写状态切换时SCLK多打了一个沿也会这样。解决统一模型FPGA在SCLK下降沿改SDINADC在上升沿采样。仿真里把SCLK、SDIN、sen_n三个信号对齐看数第8个上升沿对应的SDIN是不是A0。状态切换时把SCLK强制拉低一拍再进下一状态。5.3 3线读操作SDIO出现X现象仿真波形里sdio在读写切换处变红读回数据不定上板偶尔读到0xFF。原因FPGA的tri_en拉低太晚ADC已经输出两边同时驱动。或者testbench里ADC模型的输出使能没有延迟和FPGA的tri_en完全重叠。解决在写完地址最后1bit的SCLK下降沿立即拉低tri_en并确认ADC模型的tEN_SDIO延迟。加4.3的断言跑回归。上板时用示波器看SDIO在切换沿有没有台阶有台阶说明两边同时在驱动。5.4 上板SCLK频率不对或波形振铃现象示波器看SCLK不是5MHz或者上升沿有过冲ADC偶尔配置失败。原因分频计数器复位值写错比较值差一或者PCB走线长、端接不对SCLK作为时钟信号振铃明显。解决先仿真看div_cnt和sclk_r确认8分频。上板先用1MHz跑通再逐步提频。SCLK走线尽量短必要时串阻。如果FPGA引脚支持用ODDR输出SCLK波形更干净。5.5 换一款ADC后时序不满足现象同一套代码换到另一款ADC写配置偶尔成功偶尔失败读回数据错位。原因不同厂商tDSU、tOUT_DV、tEN_SDIO差异大原代码的裕量不够。帧格式也可能不同比如地址位宽从8位变13位。解决把时间参数和帧格式抽成parameter按新datasheet重算分频系数和采样沿读回校验加超时重试。换芯片先跑读写回环再动采样通路。6. 二次开发把寄存器配置改成参数化表驱动一开始我是每配一个寄存器就改一次状态机后来寄存器上了30多个改到怀疑人生。现在我的做法是把配置序列抽成两张表地址表和数据表状态机只负责按索引遍历写完一个自动跳下一个。换项目时只改表不动状态机。// 参数化寄存器表换项目只改这里 localparam REG_NUM 4; wire [7:0] reg_addr [0:REG_NUM-1] {8h10, 8h11, 8h12, 8h13}; wire [15:0] reg_data [0:REG_NUM-1] {16h0001, 16h0002, 16h0003, 16h0004}; reg [3:0] reg_idx; // 状态机里写完一帧后 if (reg_idx REG_NUM - 1) begin reg_idx 4d0; state DONE; end else begin reg_idx reg_idx 1b1; state WR_REG; end这段代码把“写哪个寄存器”和“怎么写”解耦。验证方法也简单写完之后切到读模式按同样的索引把寄存器读回来和reg_data逐项比对不一致就拉一个error标志。我一般会在上板前先把读回比对跑通再去看采样数据。另一个改造点是3线和4线共用一套状态机用parameter SPI_MODE区分4线时sdo和sdi分开3线时用tri_en控制sdio。这样同一份代码能覆盖两种板卡不用维护两个分支。表驱动的代价是寄存器地址和数据占一点BRAM或者LUT但对配置寄存器这种几十到几百项的量级资源完全不是问题。从那以后我每次换ADC都强制先把读写回环跑一遍再动采样通路。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑