FPGA驱动ADS8681 ADC:SPI时序与VHDL状态机实现要点
2026/9/16 3:22:49 网站建设 项目流程

简介:这是一份适用于赛灵思现场可编程门阵列的模数转换器驱动工程压缩包,面向需要实现高精度数据采集的硬件工程师与可编程逻辑学习者。转换器型号为ADS8681,是十六位八通道的高精度模数转换器,资源包提供了完整驱动方案,涵盖接口定义、时序控制、数据读取、多通道切换等关键逻辑,可直接导入Vivado或ISE进行编译综合与下载验证。压缩包共一百二十九个文件,容量约四百一十一KB,包含硬件描述语言源码、约束文件、比特流文件以及工程配置文件等,同时包括日志、综合映射报告与管脚规划文件,可用于分析工程构建流程与排错。已有九百三十八人学习/下载,适合正在调试可编程逻辑器件与模数转换器通信或希望借鉴官方驱动框架的开发者。资源整体结构紧凑,按工程目录组织,便于快速定位源码、约束和生成文件,是学习可编程逻辑与高速模数转换器接口设计的实用参考资料。

1. 拿到 ads8681.bit 之后,FPGA 驱动 ADS8681 的坑才算开始

第一次把 ADS8681 这颗 16 位 ADC 接到 Xilinx FPGA 上的人,十有八九会在 SDO 上读到一串稳定的 0xFFFF。不是芯片坏了,也不是板上信号被干扰,而是驱动状态机对 SCLK 极性的理解和 ADC 不一致。这个 ads8681xin 工程打包的就是一套经过综合的 Xilinx FPGA 驱动 ADS8681 方案,里面既有 VHDL 源码路径,也保留了 PlanAhead 生成的可下载位流 ads8681.bit、位流生成日志 ads8681.bgn、完整编译报告 ads8681.bld 和命令记录 ads8681.cmd_log。它解决的不只是让数据出来,还包括多通道切换、输入范围配置和转换完成后数据读回这些真实项目会遇到的时序问题。适合正在做数据采集、测试仪器或 fpga 图像处理前端的工程师拿来做参考,fpga 入门的读者也可以借助这个工程理解 ADC 驱动的基本骨架。

2. ADS8681 SPI 帧与转换时序:写 VHDL 驱动前必须吃透的三个参数

2.1 16 位转换结果藏在 32 个 SCLK 里:SCLK 极性与采样沿

ADS8681 的串行口不是简单的主从收发,而是一个命令-数据双工帧。每个片选周期内,主机在 SDI 线上发 16 位命令,同时 ADC 从 SDO 线上把上一次或当前转换结果移出。全帧通常拉 32 个 SCLK:前 16 拍写入命令,后 16 拍接收转换值。如果只需要读结果,最简单的用法是前 16 拍发全零的 NO-OP 命令,随后 16 拍从 SDO 上把转换数据收进移位寄存器。这个 32 拍的帧结构是驱动状态机的骨架,很多误读都是因为只发了 16 个 SCLK,拿到的却是命令回显。

SPI 极性和相位必须按 Mode 0 处理:CPOL=0,CPHA=0。也就是说 SCLK 空闲为低电平,ADC 在 SCLK 下降沿更新 SDO,FPGA 在上升沿采样。Xilinx FPGA 里的普通输入引脚可以直接采样,不需要做电平转换;但如果你从别的 SPI 外设拷贝过来一套模式 3 的控制器,CPOL=1,就会看到转换结果按位错开甚至出现恒定 0xFFFF。用逻辑分析仪抓 SCLK 和 SDO 时,重点数一下 SDO 数据是否在 CS 拉低后第二个上升沿附近已经稳定;如果发现数据整体右移一位,先怀疑相位不是极性。这个问题的排查成本通常比想象中高,因为同样的 VHDL 语法在 Modelsim 仿真里跑得好好的,上板就错。

下面把 ADS8681 侧的关键信号列出来,VHDL 顶层端口基本就是这些信号再加一个 FPGA 系统时钟和复位。

信号名I/O(FPGA 视角)作用说明
SCLK输出SPI 位时钟,空闲为低
SDI输出命令输入,配置寄存器和 NO-OP 都走这根线
SDO输入转换结果输出,下降沿更新
CONVST输出转换启动信号,下降沿有效
BUSY输入转换期间为高,完成后拉低
RVS输入复位,低有效,按需连接

这张表的重点是 BUSY。很多现成代码只实现了 SCLK/SDI/SDO 三根线,跳过了 CONVST 和 BUSY,结果只能靠固定延时来等待转换完成,这会让吞吐率大打折扣。这个工程里既然有完整的 PlanAhead 工程,顶层肯定已经把 CONVST 和 BUSY 引出来了,只是综合后的 ads8681.bit 默认引脚需要根据自己的 PCB 重新映射。

2.2 CONVST 下降沿启动转换:BUSY 比固定延时可靠

ADS8681 的转换流程是:FPGA 把 CONVST 从高拉低,ADC 在内部触发 SAR 转换。转换期间 BUSY 保持高电平,转换完成后再拉低。转换时间会随配置略有变化,手册给出的典型值只能作为参考,真正可靠的方法是状态机里等待 BUSY 下降沿,只有 BUSY 拉低后才能开始读 SPI 帧。如果引脚不够用,也可以用计时器等待 10us 左右,但这样每个转换周期都会多出几微秒的等待时间,采集速度会明显下降。

下面是一段 VHDL 状态机片段,用于发出 CONVST 脉冲并等待 BUSY,这种写法在本工程里非常常见:

-- 状态机片段:发出 CONVST 脉冲后等 BUSY 释放 case state is when IDLE => if start = '1' then convst <= '0'; -- 拉低 CONVST,启动转换 state <= TRIGGER; end if; when TRIGGER => convst <= '1'; -- 恢复高电平 state <= WAIT_BUSY; when WAIT_BUSY => if busy = '0' then -- ADC 完成转换 state <= READ_SPI; end if; when READ_SPI => -- 32 拍 SCLK 的移位接收工程,下一章展开 state <= DONE; when others => state <= IDLE; end case;

convst在 IDLE 状态默认输出高电平,检测到start为 1 后先拉低一整个时钟周期,再在 TRIGGER 状态恢复高电平。WAIT_BUSY分支的判断条件是busy = '0',这是一种常见做法,能自动适应不同温度下转换时间波动。如果板子上没有把 BUSY 引到 FPGA,那就要用一个计数器在 TRIGGER 状态之后延时,例如系统时钟 50MHz 下计数 500 次得到 10us,再进入 READ_SPI。需要特别注意的是,busy是来自芯片的异步输入,进入状态机之前最好经过两级触发器同步,否则在时钟边沿附近变化时会出现亚稳态,表现为偶尔读回一个错误转换结果,非常难排查。

2.3 配置输入范围与通道:SPI 命令帧的复用逻辑

ADS8681 的输入范围、内部基准、省电模式等配置项都是通过 SPI 写寄存器完成的。这里容易混淆的是:16 位命令帧里既包含读写控制位,又包含地址,还需要和转换结果复用 SDO。对单通道 ADS8681 而言,配置写入通常在启动转换之前完成,不需要在转换过程中插帧。工程源码里一般会有两个命令入口:一个cmd_wr用于写配置寄存器,一个cmd_noop用于读转换结果。常见做法是把配置命令做成一组常量,初始化阶段逐条写入,之后每个转换周期都发 NO-OP,保证 SDO 上读到的是 ADC 转换数据而不是寄存器回读值。

工程描述里提到的 8 通道,一般来自 ADS8684/ADS8688 这类同系列型号。ADS8681 封装形式多为单通道,多通道驱动的状态机结构完全相同,差别只在于寄存器里需要多写一个通道选择字段。多通道扫描时,每次转换结束后通道计数器加一,再写入新的通道配置,随后触发下一次 CONVST,就能形成顺序扫描。如果直接把厂商 demo 的多通道代码套到单通道 ADS8681 上,往往会在初始化阶段卡在寄存器回读,因为单通道芯片未定义通道选择位,返回数据全零。理解这一层,后面看ads8681_cmd.vhd时就不会被通道字段的注释带偏。

3. Xilinx FPGA 里的可综合状态机:从 CONVST 到 16 位结果的全部状态

3.1 系统时钟分频出 SCLK:计数器实现而不是 PLL 分频

如果只是 fpga 入门,通常会把 SCLK 直接连到全局时钟引脚,但 ADC 驱动不需要这么重的资源。在 Xilinx FPGA 上驱动 ADS8681,系统时钟一般是板载 50MHz 或 100MHz,而 SPI SCLK 要求通常在 10MHz 量级。最直接的做法是用 PLL 生成一个独立时钟,但跨时钟域采样会让状态机复杂化。我一般会用计数器把系统时钟分频得到 SCLK,并生成一个sclk_en时钟使能信号,这样所有状态跳变和移位操作都发生在同一个系统时钟域里,综合后时序报告也容易收敛。

下面这段 VHDL 展示了如何生成 10MHz SCLK 和对应的时钟使能:

-- 系统时钟 50MHz,分频到 10MHz SCLK(计数器 0 到 4) process(clk50, rst_n) begin if rst_n = '0' then clk_div <= (others => '0'); sclk <= '0'; sclk_en <= '0'; elsif rising_edge(clk50) then if clk_div = 4 then clk_div <= (others => '0'); sclk <= not sclk; -- 翻转一次,形成 10MHz sclk_en <= '1'; else clk_div <= clk_div + 1; sclk_en <= '0'; end if; end if; end process;

这里clk_div是 3 位计数器,计数到 4 时翻转sclk并拉高sclk_en。50MHz 除以 5 再翻转,得到的 SCLK 周期为 100ns,正好 10MHz。sclk_en只在翻转后的第二个半周期出现一次脉冲,后续状态机在sclk_en = '1'时进行数据采样,这样不需要真正的双沿时钟也能在上升沿采样 SDO。需要修改采样率时,把计数终值从 4 改大即可,例如改成 9 就是 5MHz SCLK,波形占空比仍然近似 50%,可以满足 ADS8681 的 SCLK 参数要求。注意sclk_en只是一个使能脉冲,不能把它接到时钟端口上,否则会引入新的时钟域。

3.2 三段式 SPI 接收状态机:32 拍移位采样

WAIT_BUSY条件满足后,状态机进入 SPI 读数据阶段。这里需要把 CS 拉低,然后在每个sclk_en脉冲上对 SDO 采样,同时把发送命令移位到 SDI 上。ADS8681 是下降沿更新 SDO,主机在 SCLK 上升沿采样,因此sclk_en最好对应 SCLK 的上升沿时刻。采样过程要持续 32 拍:第 0 到第 15 拍发送命令并丢弃 SDO,第 16 到第 31 拍发送 NO-OP 并采样 SDO。

下面是接收状态机的核心片段,可以理解为ads8681.vhdspi_read进程的简化版本:

-- 32 拍 SPI 读:cmd_shift 是发送移位寄存器,rx_shift 是接收移位寄存器 when SPIREAD => if sclk_en = '1' then bit_cnt <= bit_cnt + 1; sd_i <= cmd_shift(15); -- 先发高位 cmd_shift(15 downto 1) <= cmd_shift(14 downto 0); cmd_shift(0) <= '0'; rx_shift(15 downto 1) <= rx_shift(14 downto 0); rx_shift(0) <= sd_o; -- 每个上升沿采 SDO if bit_cnt = 31 then cs_n <= '1'; data_out <= rx_shift; -- 把第 16~31 拍收到的结果锁存 state <= RESULT; elsif bit_cnt = 15 then -- 前 16 拍命令发送完成,从第 16 拍开始才关心 SDO cmd_shift <= "0000000000000000"; -- 后续 16 拍发 NO-OP end if; end if;

这段代码把命令发送和结果接收放在同一个移位循环里。cmd_shift在最初 16 拍里逐位输出配置命令,第 16 拍之后换成全零 NO-OP;rx_shift全程右移,但只有当bit_cnt大于等于 16 时的数据才被保留到最终结果。data_outbit_cnt = 31时锁存,此时 CS 拉高,一个完整 SPI 帧结束。若你想把结果直接送入 FIFO 或 AXI 接口,只需要在这个状态拉高data_valid信号。这个设计里没有使用 SPI 的忙等循环,状态转移完全由sclk_en驱动,因此 SCLK 频率再高,主状态机也不会漏采数据。

3.3 双沿采样、异步复位与 ILA 调试信号

很多从网上拷贝的 ADS8681 代码喜欢用rising_edge(sclk) or falling_edge(sclk)来处理双沿,这在 Xilinx 普通逻辑单元上并不友好,综合器会报出大量关于双沿寄存器的警告,布线后的时序余量也很难做正。常见做法是像 3.1 节那样,把 SCLK 作为普通信号输出,内部只在系统时钟域用sclk_en采样。这样既模拟了双沿采样的效果,又避免了跨时钟域,Modelsim 仿真时看到的波形和实际电路行为也更接近。

调试阶段建议把bit_cntstatesdo_bufdata_out这四组信号标记为 debug 探针,用 Vivado 的 ILA 或老工程里的 ChipScope 在线观测。上板抓波形时重点对比三件事:CS 拉低期间 SCLK 是否为连续方波,SDO 是否在 SCLK 下降沿之后稳定,data_out是否在 CS 拉高前一个周期就已经稳定。只要这三个条件满足,数据跳变问题基本都出在后续的 FIFO 读取逻辑或寄存器地址映射上,而不是 ADC 驱动本身。下面是一张常见状态转移表,排错时可以直接对照:

当前状态转移条件下一状态输出行为
IDLEstart=1TRIGGERconvst 拉低
TRIGGER无条件WAIT_BUSYconvst 恢复高
WAIT_BUSYbusy=0SPIREAD准备 32 拍 SPI
SPIREADbit_cnt=31RESULTcs_n 拉高,锁存 data_out
RESULTdata_ack=1IDLEdata_valid 拉高

这张表的含义是:状态机只在data_ack被子模块消费后才回到 IDLE,避免上一帧数据还没被取走就被下一帧覆盖。实际工程中data_ack通常来自一个异步 FIFO 的写请求信号,如果 FIFO 写满,驱动状态机应停在 RESULT 状态等待,而不是继续启动转换。这个细节决定了高吞吐率下数据是否会丢帧,很多人仿真正常但上板丢数据,就是少了这个握手。

4. PlanAhead/Vivado 工程集成:ads8681.bit、.bgn、.bld 与时序约束的使用方法

4.1 工程产物逐个拆解:.bit 是交付物,.bgn 和 .bld 是排错入口

解压ads8681xin.zip后看到的ads8681.bgnads8681.bitads8681.bldads8681.cmd_log并不是多余的编译垃圾,它们是判断工程是否可信的第一手资料。ads8681.bit是 BitGen 生成的 FPGA 配置文件,可以直接用 iMPACT 或 Vitis 烧写到 Xilinx 器件里;ads8681.bgn是 BitGen 的运行日志,里面记录位流生成时的警告,比如某个引脚被自动分配了位置;ads8681.bld是 ISE/PlanAhead 在综合或实现阶段打印的完整报告,排错时应该先在这里搜 ERROR。ads8681.cmd_log则记录了 PlanAhead 执行过的每一条命令,可以反推出工程从创建到生成位流的完整流程。

拿到一个陌生 fpga 项目时,我会先看ads8681.bld里有没有CRITICAL WARNING,尤其是引脚分配和时钟约束相关的警告。下面这条命令可以直接把最影响判断的信息拉出来:

grep -nE "ERROR|CRITICAL WARNING|PAR_STATUS" ads8681.bld

grep-n参数让输出带行号,-E启用扩展正则。CRITICAL WARNING在 Xilinx 工具里通常是约束覆盖或时钟域未约束的警告,放任不管可能导致综合出的硬件与预期时序不一致。如果输出的文件里同时存在ERROR:PlaceERROR:Route,基本可以断定原工程不是在当前这版芯片上完成的,需要重新适配芯片型号和封装。再看ads8681_pad.csv,这是 PlanAhead 导出的引脚分配表,比.bld更直观,直接打开能看到每个顶层信号对应的封装引脚和 IOSTANDARD。planAhead_pid*.debug是图形界面进程的运行记录,一般不用管它。

文件主要用途排错入口
ads8681.bit下载到 FPGA 的位流烧录失败时检查下载器 ID
ads8681.bgnBitGen 日志搜索 ERROR:BitGen
ads8681.bld综合/实现报告搜索 CRITICAL WARNING
ads8681.cmd_log工程命令历史确认工具版本与流程
ads8681.drcDRC 报告查看引脚和 bank 电压规则
ads8681_pad.csv引脚分配表核对 FPGA 引脚号

4.2 引脚约束:单端 SPI 不是 LVDS,IOSTANDARD 别选错

ADS8681 的数字接口是标准的单端 3.3V SPI,不像高速图像接口需要 LVDS 接收。如果板级设计没有做电平转换,FPGA 侧的SCLKSDISDOCONVSTBUSY都应该约束为 LVCMOS33 或与 ADC 供电电压匹配的电平。有些人看到 FPGA 上有差分输入对,就想把 SDO 接成 LVDS,这会导致 IBUFDS 的输入共模电压不满足要求,采样回来的数据完全不对。fpga 的 lvds 接收更适合接高速 ADC 的同步串行 DDR 输出,而不是 ADS8681 这种普通 SPI 接口。

下面是一段适配 Xilinx 工程的 XDC 约束示例,放在ads8681.xdc里即可:

set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports SCLK] set_property -dict {PACKAGE_PIN U19 IOSTANDARD LVCMOS33} [get_ports SDO] set_property -dict {PACKAGE_PIN R18 IOSTANDARD LVCMOS33} [get_ports SDI] set_property -dict {PACKAGE_PIN P18 IOSTANDARD LVCMOS33} [get_ports CONVST] set_property -dict {PACKAGE_PIN P19 IOSTANDARD LVCMOS33} [get_ports BUSY] set_property -dict {PACKAGE_PIN N18 IOSTANDARD LVCMOS33} [get_ports RVS]

每一条set_property都作用在一个顶层端口上,PACKAGE_PIN后面的字符是芯片封装上的具体引脚编号,必须替换成你原理图里实际连接的引脚。IOSTANDARD必须与 FPGA bank 的 VCCO 电压一致,如果该 bank 供电是 2.5V,就需要把LVCMOS33改成LVCMOS25,否则实现阶段会报端口 DRC 错误。对于时序要求较高的高速采样,还要给 SDO 加set_input_delay,因为 ADC 的 SDO 输出有传播延迟,FPGA 只靠 SCLK 上升沿采样可能采到边沿上的抖动。

提示:如果工程是在 ISE 14.x 下做的,约束文件后缀是.ucf,写法与 XDC 不同。复制本文代码到旧工程前,先确认综合工具版本,否则语法会被直接忽略。

4.3 上板验证:ILA 抓波形的探针选择与触发条件

位流下载之后,不要急着看上位机数据,先在工程里例化一个 ILA 或 ChipScope 核,观察sclk_enbit_cntsdo_syncdata_out这四个信号。ILA 的采样时钟用系统时钟clk50,采样深度设置 1024 即可,因为 SPI 帧时间很短,深度太大反而让触发定位困难。触发条件设置为bit_cnt == 0,这样能捕捉到完整的一次 32 拍通信过程。

如果 ILA 里看到bit_cnt走到 31 后没有回到 0,说明状态机停在了RESULT等待data_ack;这时检查后级 FIFO 的almost_full标志是否持续为高。如果bit_cnt乱跳,优先怀疑sclk_en被复位信号干扰,或者rst_n在 SPI 传输过程中被外部按键拉低。这些排查方法同样适用于 Vivado 的硬件管理器,探针列表里只保留本设计写进mark_debug的信号,不要试图观察内部经过IBUF之前的原始输入引脚,它们在综合时通常会被优化掉。用ads8681.drc里的报告配合在线波形,基本能在十分钟内定位是时序问题还是逻辑问题。

5. 最后再查一遍:验证 ADS8681 驱动是否真正匹配的三个实战技巧

5.1 回读配置寄存器,确认 SPI 命令通路不是空转

第一个验证技巧是用回读命令把初始化阶段写入的寄存器内容读出来。ADS8681 支持写读共用一个命令帧结构,在 NO-OP 之前先发一个读地址,后续 SDO 上返回的数据就应该等于之前写入的配置值。如果回读结果一直是全零,说明初始化写命令根本没有被 ADC 接受,需要检查 SDI 数据是否在正确的 SCLK 边沿稳定下来。这个测试可以在 FPGA 内部用一个简单的比较器完成,也可以把回读值通过串口打出来。不要跳过这一步,因为 SDO 上能读到转换结果不代表寄存器通路正常,有的工程命令帧发错,但碰巧 SDO 上有一段固定电平,也会被误判为数据。

5.2 GND 和基准电压标定:先把零点找到再谈精度

第二个技巧是把模拟输入 AIN 分别接到 AGND 和板载基准电平,然后观察data_out的值。AIN 接 AGND 时,理想输出应该接近0x0000或由寄存器配置的双极性偏移决定,最常见的双极性 ±10.24V 模式下中点对应0x8000。如果 GND 测试值在低 3 位不停跳动,说明驱动时序基本正确,问题在电源噪声或 PCB 布局;如果读回的值稳定在0xFFFF,则采样沿不对或者 SDI 命令帧发错。做完零点再做满量程,两个点的差值除以跨距能得到实际增益系数,写入校准寄存器后可以修正整体增益误差。这个标定顺序不要反过来,先确认零点,再谈满量程,否则错误的偏移会掩盖驱动层面的 bug。

5.3 统计 BUSY 高电平时间,反推是否还有采样余量

最后一个验证技巧是用 FPGA 计数器测量 BUSY 高电平持续了多少个系统时钟周期,连续统计 64 次再求平均值,得到真实转换时间。比如 50MHz 时钟下计数到 160,说明转换时间约 3.2us,与手册典型值相符。如果测量值明显偏大,比如每次都要 8us,大概率是 SPI 读操作和 CONVST 启动发生了冲突,ADC 在等待主机结束上一帧才开始新转换。通过这个指标还能判断当前采样率是否已经逼近器件上限。把这三个技巧全部跑通后,再根据统计到的 BUSY 时间调整 SCLK 分频系数,才算把这套驱动的最后一段时序余量量出来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询