做FPGA数据采集AD7606应该算得上是最容易起步的ADC之一了。8通道同步采样、16位精度、双极性输入输出侧又同时给了并行和SPI两条路所以很多采集方案里都能看到它。说实话这颗芯片的驱动本身不难写难的是很多人第一次接触就被“两种读取模式”搞晕了并行是DB[15:0]上加CS/RD直接读SPI则是靠SCLK把数据一位位吐出来到底什么场合用哪套Verilog代码分别怎么写才能保证采样率不掉这篇文章就把我验证过的AD7606驱动拆开讲覆盖并行模式读取和SPI模式读取两套代码的状态机设计、时序要点、仿真模型和调试经验。如果你正在做电力数据采集、振动监测、医疗设备或者任何需要多通道同步采样的FPGA项目这套思路可以直接抄。1. AD7606为什么值得单独写一套FPGA驱动1.1 芯片接口不是二选一而是同一组引脚的不同用途很多第一次接触AD7606的同学会误以为这芯片有两种封装或者两种型号一种带并行口一种带SPI口。其实不是。芯片内部还是同一个核8个采样保持器加一个16位SAR ADC转换完成后数据锁存在输出寄存器里。区别只在外部引脚怎么复用。并行模式下DB[15:0]这16根线直接输出转换结果每拉低一次RD总线上就出现一个通道的数据读8个通道就是8个RD周期。SPI模式下这组引脚被重新分配一部分变成DOUTA、DOUTB串行数据线SCLK负责把数据移位出来。控制位PAR/SER决定了走哪条路1是并行0是串行。所以写驱动之前第一件事不是打开代码编辑器而是先确认板子上这组引脚接了哪里、PAR/SER被固定到什么电平。还有几个引脚是两种模式共用且必须配置的OS[2:0]决定过采样倍率一般是000关闭过采样RANGE决定模拟输入量程逻辑0是±5V逻辑1是±10VRESET用于复位。这些引脚在驱动里虽然大多数时间不需要动态操作但必须在初始化阶段赋一个确定值绝对不能悬空。悬空状态下芯片可能在上电瞬间被噪声拉到错误状态采样数据就会整体异常。1.2 并行模式与SPI模式的实际选型建议选型不是越高级越好而是看你的约束条件。我列一个实际的对比表对比项并行模式SPI模式数据引脚数量16根DB[15:0]2根DOUTADOUTB必备控制线CS、RD、BUSY、CONVSTCS、SCLK、BUSY、CONVST读8通道所需SCLK/RD次数8次RD脉冲约64或128个SCLK周期对FPGA引脚占用高约20个IO低约5个IO数据读取速度快一次RD约几十ns受SCLK频率限制隔离方案成本数字隔离器数量多隔离通道少成本低FPGA内部逻辑开销状态机简单拼装数据不难需要移位寄存器和位计数一般建议是这样的如果FPGA引脚不紧张优先并行模式因为状态机直观、调试方便、速度上限高。如果系统里FPGA同时要接DDR、PCIe或者对外接口IO已经排得非常满那就上SPI模式省下的十几根脚非常宝贵。还有一种常见场景是模拟前端需要隔离SPI模式只需要隔离两三条信号线隔离芯片的成本和面积都能降下来。另外提醒一句SPI模式下如果只接DOUTA一根线有些接法只能拿到部分通道要读满8个通道通常需要把DOUTA和DOUTB同时接进FPGA下面第4部分我会按双线同时采样的方式写驱动。2. 并行读取模式四个信号的节奏和状态机2.1 一次采样的完整信号关系并行模式下最关键的就是CONVST、BUSY、CS、RD这四个信号。很多人把它想复杂了其实节奏非常固定。先把CONVST拉低保持一段时间手册典型要求至少25ns然后拉高。CONVST的上升沿就是转换启动命令芯片收到这个上升沿后把8个通道的模拟输入同时采样并开始转换。转换期间BUSY信号输出高电平转换完成后BUSY拉低。注意BUSY下降沿才是“数据已经锁存到输出寄存器”的标志从这个时刻开始才允许做读取操作。用Verilog描述时启动一次采集的流程是IDLE - 拉低CONVST - 拉高CONVST - 等待BUSY变高 - 等待BUSY变低 - 进入读数据阶段为什么要把“等待BUSY变高”单独设计一个状态因为CONVST拉高之后BUSY不会立刻变高中间有很短的时间差直接跳去等BUSY变低没问题但如果用BUSY边沿做触发必须先把BUSY同步过两级寄存器。这也是初学者最容易漏掉的地方AD7606的BUSY是异步信号跟FPGA主时钟没有任何相位关系直接拿它做组合逻辑判断很容易出现亚稳态。2.2 读八通道数据时的CS/RD循环BUSY变低之后芯片已经准备好8个通道的结果。并行模式下读取方式很直接先拉低CS使能输出然后每给一个RD低脉冲DB总线上就换一个通道的数据。通道顺序是固定的从V1到V8不存在地址线也不需要额外指定通道号。每次RD周期的典型做法是拉低CS拉低RD等待数据建立在RD下降沿之后的某个稳定点把DB[15:0]锁存进FPGA拉高RD重复第2到第5步直到8个通道全部读完拉高CS结束本次读操作。这里最关键的时序参数是RD下降沿到数据真正有效的间隔手册里一般叫t9典型值是几十纳秒级别。FPGA采样点必须落在数据有效窗口内。说白了就是RD刚拉低时DB总线上还不是当前通道的稳定数据你立刻去采采到的可能是上一通道的尾巴或者中间跳变状态。实际项目里我一般会让RD低电平保持2到3个系统时钟周期再把采样点放在第2个或第3个时钟沿。以50MHz主时钟为例一个周期20nsRD低电平保持40~60ns数据完全来得及建立。2.3 状态机如何把这些时序组织起来状态机设计的大致思路是这样的IDLE空闲等待外部启动信号startCONVST低电平状态把ad_convst拉低CONVST高电平状态拉高ad_convst启动转换等待BUSY变高状态转换已经开始等待BUSY变低状态转换结束可以读取读数据循环状态拉低CS和RD延时后锁存数据拉高切换通道完成状态输出done信号回到IDLE。读取循环里要有一个通道计数器。每读一次计数器加一读到第7个也就是V8后说明8个通道都拿到了拉高CS并进入完成状态。计数器既用于控制循环次数也用于决定当前锁存的数据应该存进ch1还是ch8。有一个细节值得强调复位或者初始化时CS、RD、CONVST这些输出引脚必须设成默认高电平。AD7606的控制信号大多数是低电平有效上电如果引脚悬空或者被拉低芯片会进入奇怪的状态。Verilog里做复位逻辑时把ad_cs、ad_rd、ad_convst全部置1把ad_par_ser按模式固定这样最稳妥。3. 并行模式Verilog驱动代码可复用的模块框架3.1 端口定义和关键参数并行驱动的顶层模块我一般写成下面这样。参数部分把等待周期做成可配置的方便在不同主频下复用module ad7606_parallel_drv #( parameter CLK_FREQ 50_000_000, parameter RD_WAIT_CYC 3 )( input wire clk, input wire rst_n, input wire start, // 启动一次采样高有效 output reg done, // 本次采样完成标志 output reg [15:0] ch1, ch2, ch3, ch4, output reg [15:0] ch5, ch6, ch7, ch8, // AD7606物理引脚接口 output reg ad_convst, output reg ad_cs, output reg ad_rd, output reg ad_par_ser, // 1:并行模式 0:串行模式 output reg ad_bytsel, // 0:16位数据模式 output wire ad_os0, ad_os1, ad_os2, output wire ad_range, // 0:±5V 1:±10V input wire ad_busy, input wire [15:0] ad_db ); assign ad_os0 1b0; assign ad_os1 1b0; assign ad_os2 1b0; assign ad_range 1b1; // 根据量程需求修改RD_WAIT_CYC就是RD低电平后等待采样的时钟周期数。系统主频越高需要的等待周期越多系统主频越低等待周期可以减少。用一个参数把它显式暴露出来换平台时只改参数不用动状态机逻辑。3.2 状态机与数据锁存核心状态机的代码如下。为了阅读方便我省略了每个分支里所有寄存器的完整赋值列表只保留了状态跳转的骨架实际使用时要补上对应输出信号的默认赋初值逻辑localparam S_IDLE 5d0; localparam S_CONVST_LOW 5d1; localparam S_CONVST_HIGH 5d2; localparam S_WAIT_BUSY_H 5d3; localparam S_WAIT_BUSY_L 5d4; localparam S_RD_SET 5d5; localparam S_RD_WAIT 5d6; localparam S_RD_CAPTURE 5d7; localparam S_RD_HIGH 5d8; localparam S_FINISH 5d9; reg [4:0] state; reg [3:0] rd_cnt; reg [1:0] busy_sync; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; ad_convst 1b1; ad_cs 1b1; ad_rd 1b1; done 1b0; rd_cnt 4d0; busy_sync 2b00; end else begin busy_sync {busy_sync[0], ad_busy}; // 两级同步 case (state) S_IDLE: begin done 1b0; if (start) state S_CONVST_LOW; end S_CONVST_LOW: begin ad_convst 1b0; state S_CONVST_HIGH; end S_CONVST_HIGH: begin ad_convst 1b1; state S_WAIT_BUSY_H; end S_WAIT_BUSY_H: begin if (busy_sync[1]) state S_WAIT_BUSY_L; end S_WAIT_BUSY_L: begin rd_cnt 4d0; if (!busy_sync[1]) state S_RD_SET; end S_RD_SET: begin ad_cs 1b0; ad_rd 1b0; state S_RD_WAIT; end S_RD_WAIT: begin state S_RD_CAPTURE; end S_RD_CAPTURE: begin case (rd_cnt) 4d0: ch1 ad_db; 4d1: ch2 ad_db; 4d2: ch3 ad_db; 4d3: ch4 ad_db; 4d4: ch5 ad_db; 4d5: ch6 ad_db; 4d6: ch7 ad_db; 4d7: ch8 ad_db; endcase state S_RD_HIGH; end S_RD_HIGH: begin ad_rd 1b1; ad_cs 1b1; if (rd_cnt 4d7) state S_FINISH; else begin rd_cnt rd_cnt 1b1; state S_RD_SET; end end S_FINISH: begin done 1b1; state S_IDLE; end default: state S_IDLE; endcase end end这里S_RD_WAIT只给了一个周期主要是状态机骨架。如果你用50MHz时钟我会把S_RD_WAIT扩展成3个周期办法是加一个RF_WAIT_CYC计数器或者直接连续排三个等状态。采样点放在最后一个等状态上确保信号已经过了t9稳定区间。这种细节其实就是并行驱动比SPI驱动好调试的地方——逻辑非常直白问题几乎只出在等待周期不够长。3.3 接入FIFO或者AXI的扩展方法上面的模块只是把数据存进ch1到ch8这8个寄存器。真实项目里一般不会直接读寄存器而是希望转换完成后自动把数据写进FIFO或者通过AXI-Lite让CPU来取。如果接FIFO做法是把done信号当作写使能把{ch1,ch2,...}打包成一个128位数据写进FIFOZynq软核那边再按固定偏移拆出8个通道。如果接AXI-Stream则可以在S_FINISH状态里发起一次握手把8个通道组成两拍AXI数据发出去。这样采样线程和消费线程之间就有了缓冲不会因为CPU处理慢丢数据。还有一个可选的改进把独立寄存器改成内部数组reg [15:0] ch_data [0:7]配合一个写地址计数代码会更紧凑也更方便改成任意通道数。不过Xilinx Vivado默认支持SystemVerilog用数组端口没问题如果PCB上已经有固定的ch1到ch8输出端口需求那还是拆开写最直观。4. SPI模式读取DOUTA/DOUTB、SCLK边沿和状态机4.1 串行模式下数据是怎么排列的SPI模式下AD7606的引脚分配会变化。并行总线DB[15:0]中的一部分会被复用成串行数据线典型接法是DOUTA负责输出前4个通道DOUTB负责输出后4个通道两个输出共用SCLK。也就是说每个SCLK周期同时从两根线上各移出一位16个SCLK周期后你就同时得到了两个通道的完整16位数据。这样读满8个通道只需要64个SCLK周期而不是一通道16位乘以8那么夸张。如果你的板子上只连接了DOUTA一根串行线那一次转换只能拿到4个通道的数据需要换一种数据帧格式才能把8个通道都串出来。但标准连接和大多数例程采用的是双线同时读取硬要单线只会让驱动白白变慢。这里强调一下不同批次的AD7606、AD7606B、AD7606C在串行输出帧结构上可能有差异写代码之前一定要对着自己手上芯片型号的数据手册看时序图。本文的代码是按“DOUTA输出前4通道、DOUTB输出后4通道、共用SCLK”的常规接法写的如果你的芯片手册画的是单线串行8通道输出就把采样输入的dout_b直接置0代码逻辑同样适用。4.2 从CS到SCLK的时序细节SPI模式的时序比并行稍微绕一点主要是SCLK和数据边的配合。CS拉低后不能立刻开始打SCLK。手册里有一个叫静默时间的参数要求CS下降沿到第一个SCLK下降沿之间留出足够时间典型值是几百纳秒。这个时间是用来等芯片内部输出电路准备数据的。BUSY下降后也是这样不要刚看到BUSY变低就立刻拉CS稍微让状态机在中间转一圈再拉CS再等一段时间最后才开始出SCLK。SCLK的空闲电平是高的。数据在SCLK下降沿从芯片移出FPGA应该在SCLK上升沿采样。简单理解就是产生一个下降沿让芯片把数据放到DOUT线上等到线上的数据稳定了再产生上升沿在上升沿这个点把数据采回来。如果用50MHz主时钟我一般做4分频SCLK大概是12.5MHz每半个周期两个主时钟周期一个等待电平稳定一个用来采样非常稳。4.3 SPI驱动核心代码SPI驱动的顶层模块端口上除了控制信号之外多了SCLK、dout_a、dout_b三根线module ad7606_spi_drv #( parameter CLK_FREQ 50_000_000, parameter SCLK_DIV 4 )( input wire clk, input wire rst_n, input wire start, output reg done, output reg [15:0] ch1, ch2, ch3, ch4, output reg [15:0] ch5, ch6, ch7, ch8, output reg ad_convst, input wire ad_busy, output reg ad_cs, output reg sclk, input wire dout_a, input wire dout_b );状态机前面一半跟并行模式一样都是触发转换、等BUSY上升下降后面则进入串行读出阶段。我一般用一个位计数器bit_cnt和一个通道计数器ch_cnt。每产生一个SCLK周期在上升沿处采样dout_a和dout_b移入两个16位移位寄存器当bit_cnt计到15时说明一个通道读完了把两个移位寄存器的值锁存到对应的输出通道寄存器里然后ch_cnt加一bit_cnt清零。核心片段如下localparam S_IDLE 5d0; localparam S_CONV_LOW 5d1; localparam S_CONV_HIGH 5d2; localparam S_WAIT_BUSY_H 5d3; localparam S_WAIT_BUSY_L 5d4; localparam S_CS_SET 5d5; // CS拉低并等待静默时间 localparam S_SCLK_LOW 5d6; // SCLK拉低芯片移出数据 localparam S_HOLD 5d7; // 等待数据稳定 localparam S_SCLK_HIGH 5d8; // SCLK拉高采样边沿 localparam S_FINISH 5d9; reg [4:0] state; reg [7:0] wait_cnt; reg [3:0] bit_cnt; reg [2:0] ch_cnt; reg [15:0] shift_a, shift_b; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; sclk 1b1; // 空闲高 ad_cs 1b1; ad_convst 1b1; shift_a 16d0; shift_b 16d0; bit_cnt 4d0; ch_cnt 3d0; done 1b0; end else begin case (state) S_IDLE: begin done 1b0; if (start) state S_CONV_LOW; end S_CONV_LOW: begin ad_convst 1b0; state S_CONV_HIGH; end S_CONV_HIGH: begin ad_convst 1b1; state S_WAIT_BUSY_H; end S_WAIT_BUSY_H: begin if (busy_sync[1]) state S_WAIT_BUSY_L; end S_WAIT_BUSY_L: begin if (!busy_sync[1]) begin ad_cs 1b0; state S_CS_SET; end end S_CS_SET: begin // 静默等待让芯片准备好输出 if (wait_cnt 8d20) begin wait_cnt 8d0; state S_SCLK_LOW; end else wait_cnt wait_cnt 1b1; end S_SCLK_LOW: begin sclk 1b0; // 下降沿芯片移出下一位 state S_HOLD; end S_HOLD: begin // 等数据稳定 state S_SCLK_HIGH; end S_SCLK_HIGH: begin sclk 1b1; // 上升沿此时数据稳定 shift_a {shift_a[14:0], dout_a}; shift_b {shift_b[14:0], dout_b}; if (bit_cnt 4d15) begin bit_cnt 4d0; case (ch_cnt) 3d0: begin ch1 shift_a; ch5 shift_b; end 3d1: begin ch2 shift_a; ch6 shift_b; end 3d2: begin ch3 shift_a; ch7 shift_b; end 3d3: begin ch4 shift_a; ch8 shift_b; end endcase if (ch_cnt 3d3) begin state S_FINISH; end else begin ch_cnt ch_cnt 1b1; state S_SCLK_LOW; end end else begin bit_cnt bit_cnt 1b1; state S_SCLK_LOW; end end S_FINISH: begin ad_cs 1b1; done 1b1; state S_IDLE; end default: state S_IDLE; endcase end end这段代码的思路是用状态机每一步产生一个SCLK相位变化而不是用计数分频器持续翻转SCLK。这样好处特别明显——每个相位之间可以插等待周期方便把SCLK降到任意频率调试的时候不用重新算分频系数。采样动作放在S_SCLK_HIGH状态此时SCLK刚拉高DOUT线上的数据是在S_HOLD状态里稳定下来的所以上升沿采到的一定是正确值。如果主频更高比如100MHz就把S_HOLD拆成两个甚至三个状态或者用一个wait_cnt统一控制。这套状态机模板换平台时几乎不用改结构只调等待周期。5. 验证和调试仿真模型、逻辑分析仪和常见坑5.1 Testbench里怎么模拟AD7606裸跑硬件之前一定要先写个Testbench把状态机跑通。AD7606在仿真里可以做得非常简单不需要真的构建模拟前端模型只模拟它的数字行为就够了。并行模式下Testbench要做三件事复位后把ad_busy置低检测到ad_convst上升沿时延迟一段时间把ad_busy拉高再延迟一段时间拉低模拟一次转换完成检测到ad_rd下降沿时延迟约几十纳秒然后把ad_db总线更新成当前通道的预置数据。注意第三步要自带一个数据表比如reg [15:0] test_data [0:7]; always (negedge ad_rd) begin #25 ad_db test_data[rd_cnt]; // 模拟t9延迟 end这样仿真波形里就能清楚看到rd拉低、数据延迟出现、FPGA采样点落位、数据正确锁存。如果你发现采回来的数据和test_data对不上十有八九是采样点落在t9窗口前面了。SPI模式仿真稍微复杂一点。Testbench需要在sclk下降沿时把DOUTA和DOUTB更新为当前位always (negedge sclk) begin dout_a test_data[ch_idx * 2][bit_idx]; dout_b test_data[ch_idx * 2 1][bit_idx]; end用这个模型可以验证位计数是否正确、通道计数跳变时机是否对、最后done信号是否在读完8个通道后才拉高。我在调试SPI驱动时有一半问题都是靠仿真先暴露的比如bit_cnt初始化位置不对导致首尾丢位。5.2 板上时序观察要点仿真通过不代表板子一定正常板级调试时逻辑分析仪是必需品。我建议把至少五个信号接进逻辑分析仪CONVST、BUSY、CS、RD、SCLK。然后依次观察几个时间点CONVST上升沿到BUSY上升沿的间隔是否符合手册转换时间BUSY下降沿之后CS是否还等了一段时间才拉低并行模式下RD低电平宽度和DB数据建立关系SPI模式下CS下降沿到SCLK第一个下降沿的静默时间SCLK的波形占空比和边沿位置。这些如果都符合预期再去看数据。如果数据不对先从数字时序找原因不要一上来就怀疑ADC精度。之前有次我在板子上看到SPI读回来的数据全是乱码排查半天发现是SCLK在FPGA内部产生后又经过一段很长PCB走线信号反射严重下降沿位置漂了。后来把SCLK输出加了一级寄存器约束并把走线缩短问题立刻消失。5.3 我遇到过的读取异常和对应解法这里整理几个我在实际调试中踩过、也帮同事排查过的典型问题现象可能原因排查思路数据全为0或者固定值模拟输入悬空、RANGE配错、DB线连接错误给固定直流电压看代码是否线性变化数据全部满量程输入超量程、RANGE引脚配置与预期不符示波器量V1引脚确认实际电压范围个别通道数据错位通道顺序与PCB布局不一致检查原理图网络名不要轻信引脚顺序读取值始终是上一帧在BUSY高电平期间就开始读扩展等待BUSY变低后的延时并行读回数据偶尔跳变RD采样点落在t9窗口内增加RD低电平等待周期SPI读回数据方向反了采样边沿和移出边沿搞反检查SCLK空闲极性和上升沿采样点采样率一高就错RD或SCLK周期不满足最小时间用逻辑分析仪看脉冲宽度是否够还有一个特别容易犯的低级错误是把AD7606输出的16位数据当无符号数处理。AD7606输出的是二进制补码中间零点0x0000正向满量程接近0x7FFF负向满量程0x8000。换算电压时如果是±10V量程公式是电压 带符号编码 × 10 / 32768比如编码0x4000对应大约5V0xC000对应大约-5V。如果当成无符号数直接除以65535再乘以量程结果会把负半轴完全丢掉。调试建议是先用一个可调直流源给某个通道输入固定电压比如给V1接2.5V看读回来的编码是否大致对应0x2000附近±10V量程下。确认了编码换算再谈滤波和校准。写在后面一点实际体会两套驱动都调通之后我的感受是AD7606的驱动难度真不在状态机而在时序余量的把握。并行模式只要能确保RD采样点足够靠后基本不会出问题SPI模式只要把静默时间和采样边沿处理好稳定性也不差。真到出问题时优先怀疑时序而不是怀疑代码逻辑。如果让我给一个最实在的建议动手写代码之前先把芯片手册上的时序图找出来在纸上把CONVST、BUSY、CS、RD、SCLK的波形画一遍再对着状态机一个状态一个状态走一遍。这一步看起来费时间实际上能帮你省掉后面一整天的排查时间。采集板上最后那层“就差一个坑没踩”的体验往往就是从这张波形图开始的。