简介FPGA通过SPI对ADC配置系列文章以连载形式梳理SPI总线基础与FPGA配置ADC的完整流程面向FPGA工程师、嵌入式开发者及硬件学习者解决从协议原理到时序实现的核心问题。文档从SPI四线SDI/SDO/SCLK/CS与三线SDIO的机制讲起再以ADS52J90为例展开4线模式读写时序、SEN/SCLK/SDIN/SDOUT信号行为及tSCLK/tSEN_SU/tDSU等关键参数并归纳寄存器地址加数值的写入规则与时钟沿采样要点兼顾AD9249等三线模式延伸。文中还给出4线写操作共同准则与读操作关键点避免因SEN时序或时钟沿选择不当导致通信失败。整份资源包约787KB包含1个docx文档内容由浅入深既有概念图解又有可落地的代码分析思路适合作为ADC配置开发的常备参考。目前已有1870人学习下载配套系列内容能帮助读者快速建立SPI配置知识框架并规避时序裕量不足等常见问题。1. FPGA通过SPI配置ADC要解决的不只是“能写寄存器”ADC不是插上电就能出正确数据的。多数高速、多通道ADC上电后处于“能用但不好用”的状态默认采样率、默认通道选择、默认增益往往和你想要的对不上必须先把寄存器写对。而寄存器最通用的入口就是SPI在FPGA侧做这件事不是为省一颗MCU而是为确定性和速度FPGA能在几十个时钟周期内完成一次配置配置触发还能和采集信号同步。这篇笔记想把整条链路讲透从看懂ADC手册里的寄存器表和时序图到写出一套能上板的SPI配置模块再顺带把最容易翻车的四个场景摆出来。适合刚接手采集板卡的数字逻辑工程师也适合被“配置完还是不对”反复折磨的熟手。2. 把ADC手册翻译成SPI配置帧先对齐协议再写代码直接动手写Verilog前我一般先做一件事把ADC数据手册翻到“SPI接口”和“寄存器映射”两个章节把所有和时序有关的数字抄到一张纸上。FPGA侧的配置逻辑本质上是在复现手册时序协议没对齐后面代码再顺也是白费。2.1 SPI四种模式里ADC配置为什么默认选模式0SPI是四线制SCLK、MOSI、MISO、CS片选。频率和位宽可以变但时序关系由两对参数决定——CPOL决定SCLK空闲电平CPHA决定数据采样沿。组合出来就是四种模式模式CPOLCPHASCLK空闲电平数据采样沿000低上升沿101低下降沿210高上升沿311高下降沿绝大多数ADC的配置接口都支持模式0和模式1少数同时支持模式2、模式3。实际工程里大家几乎默认用模式0去适配SCLK空闲为低主机在下降沿更新MOSI从机在上升沿采样从机在下降沿更新MISO主机在上升沿采样。也就是说主机发数据的时机和收数据的时机天然错开不需要做额外的半周期对齐。另一个实际原因是很多ADC内部的上电默认状态就是按模式0配置的用其他模式得先确认手册里“SPI时序图”标明的是哪个CPOL/CPHA否则第一次通信就可能错位。选模式的顺序建议是先看手册里SPI时序图如果明确写了支持模式0直接用如果只给了一张时序图没有标模式去看SCLK空闲电平空闲为低且数据在上升沿锁存就是模式0。FPGA侧不用做“自动适配多模式”配置阶段固定模式0采集数据的并行接口另说。2.2 从手册里找齐三样东西指令帧长、寄存器映射、上电时序写配置模块之前要把ADC手册里的三张表提炼出来缺一不可。第一是指令帧格式。SPI配置帧不是一个字节打天下常见的有这么几类帧长常见位段划分典型用途16位1位读/写 7位地址 8位数据中小规模寄存器型ADC16位1位读/写 4位地址 11位数据配置项带增益/偏移的ADC20位1位读/写 7位地址 12位数据软复位、通道序列配置24位8位地址 16位数据并行风格寄存器直接映射有的ADC还支持“数据回读”发读指令时后面跟着的MISO上会返回寄存器当前值。这一点后面第6章单独讲但选帧长时就要先把读写位的位置确认好。第二是寄存器映射表。表里每一行描述一个寄存器地址、位段含义、复位值、读写属性。不要只在例程代码里复制一堆魔法数字要把它整理成一张自己的配置清单字段包括寄存器地址、配置值、作用、写入时序约束。这样后面第4章写配置表时地址和数据能直接填进去。第三是上电时序。手册“应用信息”或者“时序特性”章节里会给出几个关键时间上电稳定时间t_POR、CS拉低到第一个SCLK沿的最短时间、SCLK高/低最小脉宽、CS拉低最小宽度、前后两条配置帧之间的间隔。这些数值是配置模块里延时计数器的主要依据。2.3 手工描一条配置帧的时间轴从CS拉低到拉高把三张表参数拼起来一条完整配置帧的时间轴是这样的系统上电后先等t_POR期间CS保持高、SCLK保持空闲电平拉低CS保持t_CS然后在SCLK驱动下按MSB在前发送R/W位、地址位和数据位最后一个SCLK沿结束后保持一小段t_HOLD再拉高CSCS保持高电平一段时间才能发下一条帧。把这套流程固化成FPGA逻辑前我用一个小脚本把寄存器清单转成配置帧常量省得手算十六进制数MODE write # write 或 read ADDR 0x01 # 寄存器地址 DATA 0x03 # 写寄存器时的数据 if MODE write: word (0 15) | (ADDR 8) | DATA else: word (1 15) | (ADDR 8) | 0 print(f16h{word:04X}, // {MODE} addr0x{ADDR:02X} data0x{DATA:02X})这段脚本对应“16位帧1位R/W 7位地址 8位数据”的格式。换成其他帧长时改移位位数即可例如20位帧就把地址左移12位、数据放在低12位。输出结果能直接粘进Verilog配置表减少人工换算出错的可能。注意这里R/W位约定是高位置1表示读不同ADC可能反过来以手册为准。3. 写一个可复用的SPI主机模块状态机加移位寄存器的标准套路配置帧组织好后需要一个SPI主机模块负责把帧按位发出去。这个模块做好之后可以复用到所有SPI外设配置场景不止ADC。核心就是一个状态机加移位寄存器关键在“什么时候放数据、什么时候采样、什么时候拉CS”这三个时机的处理。3.1 接口定义和参数把模式0、位宽、分频比做成可配参数我一般会把SPI主机做成参数化模块位宽和SCLK分频比都用parameter暴露出来这样同一套代码既能配置16位寄存器型ADC也能配置20位指令帧的器件。核心代码module spi_master #( parameter DATA_WIDTH 16, // 单条传输的比特数按ADC帧长设置 parameter CLK_DIV 8 // SCLK频率 系统时钟 / CLK_DIV需为偶数且4 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低有效 input wire start, // 启动一次传输单周期脉冲 output reg busy, // 传输忙标志高有效 input wire [DATA_WIDTH-1:0] tx_data, // 要发出的配置帧 output reg [DATA_WIDTH-1:0] rx_data, // 从MISO收回的数据 output reg sclk, // SPI时钟模式0空闲为低 output reg mosi, // 主机输出 input wire miso, // 从机输出 output reg cs_n // 片选低有效 ); localparam [1:0] IDLE 2d0, XFER 2d1, DONE 2d2; reg [1:0] state; reg [$clog2(CLK_DIV)-1:0] div_cnt; // SCLK分频计数器 reg [$clog2(DATA_WIDTH)-1:0] bit_cnt; // 当前发送到的位序号 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; busy 1b0; sclk 1b0; mosi 1b0; cs_n 1b1; div_cnt 0; bit_cnt 1; rx_data 0; end else begin case (state) IDLE: begin sclk 1b0; // 模式0空闲为低 cs_n 1b1; busy 1b0; if (start) begin busy 1b1; cs_n 1b0; // 先拉低CS给从机留出t_CS mosi tx_data[0]; // 第一个bit提前放好 bit_cnt 1; div_cnt 0; state XFER; end end XFER: begin // 上升沿前的最后一个计数值此刻采样MISO if (div_cnt CLK_DIV/2 - 1) begin rx_data[bit_cnt - 1] miso; if (bit_cnt DATA_WIDTH) begin state DONE; // 最后一个bit已经采完 end end // 下降沿计数值更新MOSI准备发下一个bit else if (div_cnt CLK_DIV - 1) begin if (bit_cnt DATA_WIDTH) begin mosi tx_data[bit_cnt]; bit_cnt bit_cnt 1; end end div_cnt (div_cnt CLK_DIV - 1) ? 0 : div_cnt 1; end DONE: begin cs_n 1b1; // 采样结束后再释放CS busy 1b0; state IDLE; end endcase end end endmodule这个模块只做两件事把tx_data逐位移出去同时把MISO逐位收进rx_data。参数里DATA_WIDTH决定了单条帧的位宽CLK_DIV决定了SCLK频率。模式固定为0如果想支持其他模式需要在SCLK空闲电平和采样沿上分别加参数控制先不用折腾ADC配置用模式0足够覆盖大多数器件。3.2 状态机里的三个关键时机放数据、采样、释放CS这个模块的时序逻辑是经过设计的三个时刻不要随便挪第一个关键时机在IDLE进入XFER时CS拉低的同时直接把tx_data[0]放到MOSI上。这很重要。SPI从机在CS拉低后等待第一个SCLK上升沿到来时直接采样第一位如果MOSI还在上一位残留值第一位就采错了。先放数据、再等时钟沿是从机的读操作能成立的前提。第二个关键时机在div_cnt CLK_DIV/2 - 1时采样MISO。这个时刻对应SCLK的上升沿。为什么不等sclk真正变成高电平再采样因为sclk由分频计数器产生如果直接在sclk上升沿触发采样数据可能在边沿竞争时取到不确定值。用计数器的“边沿前一个节拍”做采样给组合逻辑留出稳定时间仿真和上板都不会出现亚稳态窗口。第三个关键时机在DONE状态里释放CS。最后一个bit在XFER中采样完成后状态先跳到DONE下一拍才把CS拉高。CS不是在边界沿上立刻释放而是错开一拍这能从时序上保证从机把最后一个数据正确锁存。实际工程里有的从机对“最后一位采样与CS释放之间的保持时间”有明确要求错开一拍总比紧贴边沿稳妥。3.3 参数怎么调SCLK频率、位宽和手册上的t_XXX对上CLK_DIV和位宽这两个参数是配置模块最常改的地方。SCLK频率的计算公式是f_sclk f_clk / CLK_DIV比如系统时钟100MHzCLK_DIV取10SCLK就是10MHz。取8就是12.5MHz取4是25MHz。两个约束条件决定CLK_DIV最小值一是手册里SCLK最高频率比如某ADC写“最大SCLK 50MHz”二是实际PCB走线和MISO建立时间。我一般会把配置阶段的SCLK降到手册上限的四分之一先把功能跑通再逐步提速找边界。配置寄存器本身是低频操作慢一点不影响采集性能没必要一上来就顶着上限跑。位宽参数直接对应该ADC的指令帧长。有一种常见错误是ADC帧长16位但只发了8位就拉高CS从机把后面半帧当噪声处理寄存器写了个残缺值。正确做法是把整帧拼成一个完整变量DATA_WIDTH设成完整帧长让移位寄存器一次发完。4. 从一条指令到一张配置表写一个下发配置的状态机单个SPI主机模块解决了“怎么把一条帧发出去”的问题但ADC初始化通常要连着写好几条寄存器。接下来要做一个上层控制状态机负责按顺序取配置项、启动SPI传输、等待完成、再发下一条。这个模块就是配置引擎。4.1 配置表怎么组织用case语句当只读存储器用最简单的做法是把配置项写成一个组合逻辑case用配置序号做索引输出完整的SPI帧。以“1位R/W 7位地址 8位数据”的16位帧为例reg [15:0] cfg_word; always (*) begin case (cfg_idx) 4d0: cfg_word {1b0, 7h00, 8h80}; // 软复位地址0x00写入0x80 4d1: cfg_word {1b0, 7h01, 8h03}; // 通道使能地址0x01写入0x03 4d2: cfg_word {1b0, 7h02, 8h28}; // 采样率配置地址0x02写入0x28 4d3: cfg_word {1b0, 7h03, 8h01}; // PGA增益地址0x03写入0x01 4d4: cfg_word {1b0, 7h04, 8h00}; // 功耗模式地址0x04写入0x00 default: cfg_word 16h0000; endcase end这种写法可综合也方便核对每个case项就是手册寄存器表里的一行。cfg_idx是配置序号从0递增发完一条就加1直到全部发完。如果不喜欢case也可以把配置项初始化到Block RAM里用地址读取但那样需要额外准备初始化文件case语句在一个工程里看起来最直白。这里有个容易踩的细节拼接符顺序必须和手册位段一致。上面代码里高位是R/W位中间是地址低位是数据。如果某颗ADC手册画的是“地址在高位、数据在低位、读写位夹在中间”拼接顺序也要跟着变别想当然套用。4.2 下发引擎上电延时、帧间隔、完成信号一次管够有了配置表还需要一个有限状态机控制下发节奏。流程是上电后先等t_POR然后逐条取出配置项、拉高start脉冲、等待SPI主机busy拉高再回落、再等一个帧间隔t_CS继续下一条。全部发完置cfg_done信号。主体状态机如下localparam IDLE 0, WAIT_POR 1, GEN_START 2, WAIT_BUSY 3, WAIT_FREE 4, GAP 5, FINISH 6; reg [15:0] timer; reg [7:0] cfg_idx; reg cfg_done; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; timer 0; cfg_idx 0; cfg_done 1b0; end else begin case (state) IDLE: begin if (trig) begin // 外部触发一次初始化配置 cfg_idx 0; cfg_done 1b0; timer T_POR; // 上电稳定延时 state WAIT_POR; end end WAIT_POR: begin if (timer 0) state GEN_START; else timer timer - 1; end GEN_START: begin spi_start 1b1; // 拉一个时钟周期的start脉冲 state WAIT_BUSY; end WAIT_BUSY: begin spi_start 1b0; if (spi_busy) state WAIT_FREE; end WAIT_FREE: begin if (!spi_busy) begin if (cfg_idx NUM_CFG - 1) state FINISH; else begin cfg_idx cfg_idx 1; timer T_CS; // 两条配置帧之间的间隔 state GAP; end end end GAP: begin if (timer 0) state GEN_START; else timer timer - 1; end FINISH: begin cfg_done 1b1; state IDLE; end endcase end end注意GEN_START在拉高spi_start的同一拍cfg_word要根据当前的cfg_idx组合出来所以cfg_word的组合逻辑直接由cfg_idx驱动不经过寄存器这样start有效时tx_data已经稳定。spi_master模块在IDLE状态检测到start后进入XFER因此这个start脉冲必须是一个系统时钟周期宽别用持续高电平否则会重复启动。T_POR和T_CS两个参数按手册填。T_POR是系统复位释放后到CS首次拉低之间的等待时间常见值从几百微秒到几十毫秒不等。T_CS是两条配置帧CS拉高后的最小间隔通常几个微秒就够。如果手册没写明t_POR保守起见取10毫秒用计数器换算成系统时钟个数。4.3 为什么这套流程比CPU配置更适合FPGA侧有些工程师会问ADC旁边明明有控制器为什么不让它通过GPIO模拟SPI来配非要FPGA自己写状态机常见的原因有三个。第一是确定性。FPGA配置状态机的每个延时都是时钟周期的整数倍不受操作系统调度、总线占用、中断响应影响。在高精度同步采集系统里所有ADC的配置完成时刻可精确对齐这对多通道相位一致性很关键。第二是可触发。配置引擎可以由采集帧同步信号直接启动这样在运行过程中需要切换增益或通道时硬件自己能卡在安全窗口里完成重配而不需要等外部处理器发指令。第三是省事。数据采集链路里本来就有FPGASPI主机和配置引擎加起来占资源极少比外部控制器通过并行总线间接控制要简单得多。配置过程还能随时用片内逻辑分析仪抓波形不必依赖外部工具。5. 板级调试避坑SPI配置ADC最容易翻车的四个场景代码写完不代表配置就对了板级调试才是翻车重灾区。下面这几个场景我都遇到过按“现象、原因、解决”总结照着排查能省不少时间。5.1 现象上电后ADC不工作寄存器读出来全是零现象是ADC的转换数据一直为0或者固定值SPI回读所有寄存器全是0x00看起来像芯片没起来。原因一上电时序没满足。有些ADC需要电源稳定后再等t_POR期间不能拉低CS否则内部上电复位不完整寄存器处于不可用状态。FPGA里复位释放和第一个CS拉低之间的延时少于手册要求就会复现这个问题。原因二CS引脚在上电瞬间有毛刺。FPGA的IO在配置完成前电平不定如果CS直接接到普通IO且没有拉电阻上电过程中可能被拉低一下让ADC进入一个不完整的SPI事务之后所有配置都被忽略。解决方式CS信号在FPGA顶层加一个上电后延时释放逻辑让CS在上电后至少再过t_POR才允许拉低。同时检查板子原理图确认CS有没有外部上拉电阻没有的话在FPGA内部约束一个弱上拉。最简单的验证方法是用工具抓上电后CS、SCLK两根线的波形看CS是否有非预期的低脉冲。5.2 现象配置随机失败时好时坏现象是同一份代码这次配置成功、下次失败回读寄存器值随机错几位而且SCLK频率越高越明显。原因一般是SCLK频率过高导致MISO建立时间不足或者SPI信号上存在反射造成的毛刺。配置阶段SCLK是分频出来的信号本身没有专门做IO时序约束布线稍长时边沿抖动和串扰会让从机采到错误电平。解决方式分三步先把CLK_DIV调大让SCLK降到手册上限的1/4看问题是否消失再用片内逻辑分析仪抓MISO重点看SCLK上升沿附近的MISO是否在窗口内稳定最后检查SPI三根线在PCB上是否等长走线、有没有串电阻。我一般习惯在MOSI和SCLK上串22欧到33欧电阻消除过冲配置稳定性立刻上一个台阶。5.3 现象配置“成功”了但读回来对不上现象是写入时总线事务正常CS、SCLK波形看着都对但读回来的数据和写入值不一致甚至只有某几位对得上。原因通常是帧长或位段理解错了。比如手册写的是20位帧代码里按16位拼那么地址位和数据位错位寄存器值被打散。另一类是读写位方向搞反把写指令当读指令发给从机从机自然回不了数据。解决方式回到手册的SPI时序图把每一格的位定义标出来数一遍总位数再对照代码里的拼接顺序。验证时写一个只发一条固定帧的测试用例用逻辑分析仪解析出每一位和期望帧逐位比对不要直接在完整初始化流程里猜。第2章的Python脚本能在这里派上用场把帧常量打印成二进制和十六进制按位核对。5.4 现象配置完成后一启动采样数据就错乱现象是寄存器配置回读都正常但一启动ADC转换输出数据出现周期性毛刺、通道错位或数值整体漂移。原因是在转换过程中又改写了寄存器。有些ADC在转换状态机运行中不允许写配置寄存器尤其是采样率、通道序列这类与模拟前端相关的项。如果配置引擎在采集启动后又被外部信号触发重配就可能打断正在进行的转换造成一两个采样点错乱。解决方式把配置动作限定在“没有转换请求”的窗口里。一般做法是用ADC的转换完成信号DRDY/EOC做门控配置引擎只在转换结束后的空闲期响应trig信号。另一种做法是参考手册里的“寄存器更新窗口”约束比如有的ADC要求CS拉高后至少等待t_HOLD才能开始下一次转换把这个时间在采集状态机里也预留出来。6. 进阶技巧回读校验与转换间隙动态重配配置链路跑通后还可以往上加一层保障把“写完就拉倒”变成“写完读回来核对”。同时掌握在转换间隙做动态重配的方法能让板卡在运行中切换量程或通道而不丢数据。6.1 用全双工把配置读回来SPI主机本质是全双工发读指令的同时MISO上就会返回寄存器内容。所以回读校验不用改硬件只需要在配置表里加一条读指令R/W位置1数据段占位全0发送完成后检查rx_data的低8位是否等于期望值。// 回读寄存器0x01期望值是0x03 assign rd_cmd {1b1, 7h01, 8h00}; ... // 发送完成后在spi_master的rx_data里取低8位 if (rx_data[7:0] ! 8h03) begin cfg_err 1b1; // 配置失败可触发重配 end回读校验的价值在于它能暴露芯片没起来、SPI引脚虚焊、配置期间被毛刺打断等问题。如果板卡上有多颗ADC我习惯在初始化流程的最后对每颗都做一次回读全部通过才拉高“系统就绪”信号。6.2 动态重配的安全窗口怎么找动态重配的要点是找到安全窗口转换正在进行时不要碰寄存器转换结束到下次触发之间的空闲期才是重配窗口。我的做法是把配置引擎的触发信号接到ADC的转换完成脉冲上用这个脉冲的后沿去启动重配状态机并保证重配所需的总时间小于两次转换之间的间隔。如果间隔不足就先把新配置缓存住等下一个空闲窗口再执行。这个机制对切换增益、切换输入通道、改变采样率都适用也是把“配置”从一次性初始化升级为运行期能力的关键。6.3 一次把SCLK拉到上限的教训之前调一块采集板手册写着SCLK上限50MHz我直接把CLK_DIV设成2跑到50MHz结果配置时好时坏。查了一整天最后用片内逻辑分析仪看到MISO在采样沿附近还在边沿变化——PCB走线太长建立时间不够。把SCLK降到12.5MHz后一切正常。那之后我养成了一个习惯配置类SPI永远不顶上限跑功能跑通后如果想提速再一点一点往上压。这个习惯帮我避开了很多“看似芯片有问题其实是时序裕量不足”的坑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?