FPGA驱动ADS1256:Verilog状态机实现24位高精度数据采集 简介面向 FPGA 开发者的 ADS1256 高精度模数转换器驱动设计资源解决在 Quartus 环境下用 Verilog 控制 ADS1256 完成多通道数据采集的核心问题。该器件为 16 位低噪声 ADC配合 FPGA 可实现高速高精度采样适用于医疗、工业检测等场景。压缩包共 434 个文件、约 8.31MB既包含 .v 驱动源码和 .qpf/.qsf 工程文件也包含大量 Quartus 编译生成的 .cdb/.hdb/.tdf 中间文件及 .sof 配置文件并附带 readme 说明可清晰还原从源码编辑、综合布局到下载验证的完整工程流程。已有 944 人学习下载。对正在学习 FPGA 与 ADC 接口设计的工程师或学生而言其中 Verilog 驱动注释与仿真工程能帮助理解 SPI 时序、寄存器配置和读取流程也能直接在 Quartus 中观察工程组织节省环境搭建与排错时间。 之前翻到一个旧的驱动压缩包名字叫ADS1256.zip_ADS1256FPGA_ads1256 verilog_ads1256 驱动_dropv54_forg后缀那串乱码估计是上传者随手敲的但里面那套 ADS1256 的 Verilog 驱动倒是让我想起不少自己在这颗芯片上折腾的经历。这篇就把 FPGA 驱动 ADS1256 的完整工程思路写出来从寄存器到状态机从仿真到上板适合正在做数据采集、多通道 DAQ、称重仪表这类项目的朋友参考。ADS1256 是一颗 24 位 Δ-Σ ADC8 通道输入SPI 接口支持 30kSPS 的采样率在工业采集、精密测量里非常常见。很多第一次接触 FPGA 驱动它的人拿到芯片第一反应是“SPI 嘛四根线简单”结果真正开始写 Verilog 状态机才发现难点根本不在线序而在于 DRDY 握手、寄存器配置时序、SCLK 边界条件这些细节。这里有大量“手册上写了但没强调、别人代码里带了但没解释”的坑。1. 拿到这类工程先分清“芯片特性”和“驱动逻辑”1.1 ADS1256 到底难在哪里ADS1256 说到底是颗 ADC但它不是那种“给个 CS、发个时钟数据就自动从 DOUT 吐出来”的芯片。它的工作模式更接近“主机主动问它才回答”——转换完成以后芯片会拉低 DRDY 引脚告诉你“数据好了”但它不会主动把数据推上 SPI 总线。你必须在自己方便的时候向它发送 RDATA 命令0x01再给 24 个 SCLK 脉冲才能把 24 位转换结果读出来。这个机制决定了驱动的核心逻辑其实是一个“带握手信号的 SPI 事务序列”等 DRDY → 发命令 → 读数据 → 攒成 24 位 → 切通道 → 再等 DRDY。真正考验 Verilog 功力的地方是把这个序列用状态机串起来并且保证每一个时间边界都不违规。1.2 一个标准驱动包里通常有什么一般网上流传的 ADS1256 FPGA 工程解压开结构都差不多我按实际项目的标准列一下ads1256_top.v顶层模块负责例化 SPI 控制状态机、寄存器配置逻辑、数据锁存逻辑。ads1256_config.v初始化序列上电后写入 STATUS、MUX、ADCON、DRATE 这些寄存器的配置字节。tb_ads1256.vTestbench仿真时模拟 ADS1256 的 DOUT/DRDY 行为用来验证驱动时序。引脚约束文件.ucf或者.xdc这个必须根据你自己的 FPGA 板卡改不同板子 pin 分配差异很大。有些包还会带串口模块或者 ILA 抓波形的工程用来把采集到的数据吐出来看。我建议你拿到任何一个驱动包先别看代码先看端口列表。如果顶层端口只有clk、rst_n、drdy、sclk、din/dout和一组并行数据输出那这个包八成是纯底层驱动如果还有uart_txd、fifo_wr这类接口说明它已经集成了数据回传链路。搞清楚你拿到的是“半成品”还是“完整链路”能省很多重复造轮子的时间。1.3 为什么这种采集场景适合用 FPGA用单片机也能驱动 ADS1256但 FPGA 的价值主要体现在几个地方时序严格可控SPI 时序、DRDY 采样窗口、通道切换节奏全部由硬件状态机控制不会被中断、任务调度打断采集速率稳定。多通道轮询不吃力单片机驱动 8 通道轮询时每切换一次通道就要等一次转换完成中断处理频繁很容易丢事件FPGA 状态机跑起来CPU 几乎不用参与。多片 ADC 并行扩展方便一个 FPGA 可以用同一个 SCLK 同时驱动多片 ADS1256各自用 CS 选通扩展成十几二十个模拟采集通道。与 MCU 对接灵活常见做法是把 FPGA 当采集前端通过 FMC、并口或者 AXI-Lite 和 STM32H743 这类处理器通信FPGA 只管把数据准备好处理器来取。所以如果你在做的项目是“多通道、高速率、需要长时间稳定采集”的方向用 FPGA 做驱动层是合理的选择。2. 写驱动前必须吃透的寄存器与命令字2.1 四个核心寄存器拆解ADS1256 的寄存器不多真正需要管的就四个地址从 0x00 到 0x03。地址寄存器名作用0x00STATUS状态寄存器包含芯片 ID、输入缓冲、自动校准等控制位0x01MUX多路复用器控制决定当前采集哪一路输入0x02ADCON包含时钟输出使能、PGA 增益、传感器检测电流设置0x03DRATE输出数据速率设置决定 ADC 转换频率MUX 寄存器是 FPGA 驱动里最常写的一个它的高 4 位决定 P 输入低 4 位决定 N 输入。比如差分输入就是 P 选 AINx、N 选 AINy单端输入就是把 N 固定接到 AINCOM。实际驱动里每次切换通道其实就是在重写 MUX 寄存器然后等一次 DRDY 下降沿。ADCON 寄存器里最常用的是 PGA 增益位。增益可以配成 1、2、4、8、16、32 甚至 64值越大有效分辨率通常越低这点后面排查噪声问题时还要提到。DRATE 寄存器决定输出速率范围从 2.5SPS 到 30kSPS。需要特别注意的是不同芯片批次、不同主时钟下同一字节值对应的速率可能不同写驱动时不要直接抄别人代码里的值一定要对照芯片手册的 DRATE 表确认。2.2 常用命令字一览除了寄存器操作ADS1256 还有一组命令Verilog 驱动里常用的就这几个命令操作码说明RDATA0x01读一次转换结果RDATAC0x03进入连续读模式STOP0x0F退出连续读模式RREG0x10 地址读寄存器WREG0x50 地址写寄存器SELFCAL0xF0自校准SYNC0xFC同步/重启转换注意 WREG 和 RREG 不是只发一个字节就完了。WREG 发完操作码还要再发一个字节表示“我要写几个寄存器减 1”然后跟着寄存器值。RREG 同理发完操作码要发“读几个寄存器减 1”然后才能读到数据。第一次写驱动时很容易漏掉这个“长度字节”导致后续时序全乱。2.3 初始化序列为什么是这个顺序一个规范的 ADS1256 初始化流程通常是这样的上电后先保持片选无效、SCLK 稳定等待电源和时钟稳定至少留出几百微秒。依次写入 DRATE、ADCON、MUX 寄存器配置好速率、增益、通道。发送 SELFCAL 命令做自校准。等待 DRDY 引脚出现下降沿表示校准完成新的转换结果可用。很多人会把 SELFCAL 放在配置寄存器之前这是不对的。自校准的结果和当前工作模式有关如果你是先校准、后改增益和速率等于校准用的是一套参数实际工作是另一套参数校准效果会打折扣。正确的顺序一定是“先配置再校准”。校准期间 DRDY 会保持高电平一段时间校准完成后拉低。但校准时间不是固定的它和 DRATE 配置的速率有关速率越低校准越慢。所以 Verilog 里不要写死等待几百个时钟最好的做法是用一个计数器做超时保护同时以 DRDY 下降沿作为校准完成标志。3. DRDY 信号与时序边界全工程真正的难点3.1 DRDY 是 ADC 给主机的“一声招呼”DRDY 在 ADS1256 里是输出引脚平时是高电平每次转换完成时拉低。它拉低意味着新的 24 位转换结果已经放到了输出移位寄存器里等待主机来读。要注意的是DRDY 低电平的持续时间很有限阻塞时间大概只有几个主时钟周期如果你不读它也不会一直等你读过数据后它才回到高电平。所以在 Verilog 里不能“看见 DRDY 拉低才开始慢慢组织命令”而是要在平时就把读命令状态准备好一旦检测到 DRDY 下降沿立刻启动 SPI 事务。这里我踩过的坑是最初把“检测 DRDY 低电平”和“发 RDATA 命令”放在同一个时钟周期里做结果因为组合逻辑延迟SPI 的 CS 拉到低电平时DRDY 采集窗已经过去了偶尔能读到数据偶尔读不到表现为“数据一会正常一会全 F”。3.2 跨时钟域同步不要省ADS1256 的 DRDY 引脚是异步信号和 FPGA 内部系统时钟完全没有相位关系。直接拿原始引脚信号做边沿检测会有亚稳态风险尤其是系统时钟比较快的情况下出现概率并不低。标准做法是打两拍同步再检测下降沿。Verilog 代码非常简单reg drdy_reg1, drdy_reg2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin drdy_reg1 1b1; drdy_reg2 1b1; end else begin drdy_reg1 adc_drdy; drdy_reg2 drdy_reg1; end end wire drdy_falling drdy_reg2 !drdy_reg1;用drdy_falling作为状态机的跳转条件而不是直接用adc_drdy。这个习惯在驱动所有异步引脚时都适用包括某些 ADC 的 BUSY、转换完成信号等。3.3 t6 和 SCLK 频率别卡到极限芯片手册的时序图里有一个参数值得每个写驱动的人注意DRDY 下降沿到第一个 SCLK 沿之间的最短时间要求。不同批次、不同手册版本给的量级不太一样但大致在几十到几百纳秒范围内。FPGA 侧处理这个问题最简单的方式是在检测到drdy_falling后不立刻启动 SPI 时钟而是用一个计数器先延时几十个系统时钟周期比如 50MHz 时钟下延时 20~40 个周期也就是 400~800ns让时序余量足够充足然后再进入发命令状态。同理SCLK 的极限频率也要控制。ADS1256 的 SCLK 不能超过主时钟频率的四分之一常用 7.68MHz 主时钟下SCLK 大概限制在 1.92MHz 左右网上也有按 2.1MHz 给的说法但工程上我一般留足余量把 SCLK 配到 1MHz 附近。SPI 时钟拉太高并不会让采集更快多少反而容易在长距离飞线上引入噪声和时序违规。3.4 24 个 SCLK 一个都不能多读数据时主机发完 RDATA 命令后再给 24 个 SCLKADC 会在 DOUT 引脚上依次移出 24 位数据MSB 在前。这个“24”必须严格由计数器控制不能多也不能少。如果 SCLK 个数多了会让 ADC 移位寄存器空转破坏下一次读数的边界如果少了则读不满 24 位数据整体错位。最典型的故障现象是读出来的数据看起来“很有规律”但和历史数据比对完全对不上或者高 8 位全是 0低 16 位在跳变。reg [4:0] bit_cnt; reg [23:0] shift_data; always (posedge clk) begin if (read_phase) begin if (sclk_pos_edge) begin shift_data {shift_data[22:0], adc_dout}; bit_cnt bit_cnt 1b1; end end end这里注意我实际工程里并没有直接在分频后的 SCLK 沿上采数据而是用系统时钟去检测“SCLK 应该在的位置”生成一个sclk_pos_edge脉冲然后在这个脉冲使能下采集 DOUT。这样所有逻辑都在同一个时钟域里时序收敛容易得多也避免出现跨时钟域采样问题。4. Verilog 状态机设计与多通道轮询实现4.1 状态机怎么划分才清晰一个足够用的 ADS1256 驱动状态机我习惯分成这些状态IDLE初始空闲等待启动信号。CONFIG依次写 MUX、ADCON、DRATE。CALIBRATE发送 SELFCAL等待校准完成。WAIT_DRDY等待下一次转换完成。READ_DATA发送 RDATA 命令读取 24 位结果。DATA_LATCH把 24 位结果锁存到输出寄存器。CH_UPDATE更新 MUX 通道号返回 WAIT_DRDY。通道轮询的逻辑就是在 CH_UPDATE 状态里把通道索引加一重新写 MUX 寄存器。如果只需要单通道可以把 CH_UPDATE 去掉锁定在固定 MUX 值即可。4.2 核心状态机参考代码我简化一下状态机核心代码去掉了一些复位、计数细节保留最关键的控制流localparam IDLE 4d0; localparam CONFIG 4d1; localparam CALIBRATE 4d2; localparam WAIT_DRDY 4d3; localparam READ_DATA 4d4; localparam DATA_LATCH 4d5; localparam CH_UPDATE 4d6; always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else case (state) IDLE: if (start) state CONFIG; CONFIG: if (cfg_done) state CALIBRATE; CALIBRATE: if (cal_done) state WAIT_DRDY; WAIT_DRDY: if (drdy_falling) state READ_DATA; READ_DATA: if (read_done) state DATA_LATCH; DATA_LATCH: state CH_UPDATE; CH_UPDATE: if (ch_done) state WAIT_DRDY; default: state IDLE; endcase end实际工程里CONFIG 状态内部还要分成多个子步骤因为写多个寄存器不能在一个状态里一拍完成通常要配合一个子计数器依次发出WREG 0x50 | 寄存器地址、寄存器数量-1、寄存器值这三个字节。同理READ_DATA 内部也要用一个子计数器控制第一个节拍发 RDATA 命令后续 24 个节拍移位读入数据。4.3 通道切换时一定记得丢弃第一次结果这是多通道轮询里最容易犯的错。MUX 寄存器写入新的通道选择后模拟输入开关切换需要一个建立时间紧接着的那次转换结果可能还是旧通道残余或者新通道欠稳定的值。我一般会在 CH_UPDATE 状态里增加一个标志位比如skip_flag重写 MUX 后把skip_flag置 1进入 WAIT_DRDY等到 DRDY 下降沿后如果skip_flag为 1就不进入 READ_DATA直接把状态切回 WAIT_DRDY 等待下一次转换。丢弃一次结果换来的是每一路数据都是稳定可靠的。WAIT_DRDY: begin if (drdy_falling) begin if (skip_flag) begin skip_flag 1b0; state WAIT_DRDY; end else state READ_DATA; end end4.4 读出的是 24 位二进制补码ADS1256 的输出格式是 24 位二进制补码。所以当读回来的shift_data[23] 1时这个数其实是负数需要做符号扩展或者求补码才能得到真正的电压值。在 FPGA 内部如果你只是把这个 24 位数据丢给后级那无所谓直接按补码理解就行。但如果你要在 FPGA 里换算成电压比如乘一个缩放系数就要先把它扩展成有符号数wire signed [24:0] value_signed {shift_data[23], shift_data};另外ADS1256 的满量程电压和 PGA 增益、基准电压都有关系换算公式一般是电压 输出码 / 0x7FFFFF * (2 * Vref / Gain)具体满量程范围要以手册为准。这部分拿到数据后在仿真平台里用已知电压验证一下最稳妥。5. 仿真验证与上板排查的实战路径5.1 没有实物也能验证时序很多人的 Testbench 只给 FPGA 驱动模块加了时钟和复位然后发现drdy信号一直处于高阻态状态机卡在 WAIT_DRDY 里整个仿真等于白跑。正确做法是在 TB 里写一个简单的 ADS1256 行为模型至少模拟 DRDY 和 DOUT 的行为。关键逻辑是每当驱动的 SCLK 边沿到来时根据当前处于“命令接收”还是“数据输出”阶段决定 DOUT 引脚的电平。简化版如下always (posedge sclk) begin if (in_cmd_phase) begin // 根据命令字节处理这里省略 end else if (bit_count 24) begin dout adc_data[23 - bit_count]; bit_count bit_count 1; end end把这个模型放进 TB你就可以验证状态机有没有在 DRDY 下降沿后正确发出 RDATA、有没有恰好移入 24 位、最终data_out是不是等于你喂进去的adc_data。我以前在模型里故意把数据设成24h555555如果驱动读出来还是24h555555说明位序、位数、时钟沿全对了。5.2 波形图上重点盯三个位置仿真跑通后打开波形窗口建议依次检查三个地方第一DRDY 下降沿和 CS 拉低之间的相对位置。如果 CS 拉低离 DRDY 太近说明延时计数器不够需要加大延时。第二SCLK 拉高的次数。在 READ_DATA 阶段应该刚好出现 24 个 SCLK 上升沿不多不少。这个在波形上数一遍最直观。第三DOUT 数据的采样点。确认驱动是在 SCLK 的哪个沿采样 DOUT保证采到的点位于数据稳定的中间区域而不是在数据跳变的边沿附近。这三处盯完时序问题基本都能暴露。5.3 上板常见问题排查表现象可能原因排查手段数据全 0 或全 FSPI 边沿选反、SCLK 极性错误、CS 没有正常拉低、DRDY 没有等到就发命令用逻辑分析仪抓 SPI 总线对比手册时序图数据错位/跳动24 个时钟不准、位序反了、通道切换后没有丢第一次结果波形上数 SCLK检查移位拼接顺序只有第一通道正常后续通道 MUX 寄存器没有重新写入检查 CH_UPDATE 状态下的寄存器写时序有时正常有时全 FDRDY 同步亚稳态、时序余量不足加两级寄存器同步增大 DRDY 到 SCLK 的延时噪声明显偏大PGA 增益过高、基准电源不干净、SCLK 频率过高先设增益 1 看底噪再逐步排查电源和时钟这块我特别有感触的是“有时正常有时全 F”。用逻辑分析仪抓了好几天最后发现是 DRDY 直接引脚做边沿检测导致的亚稳态概率不高但很恶心。打了两拍同步、加了延时计数器之后问题彻底消失。5.4 硬件层面的几个建议驱动写完后如果采出来的数据噪声偏大很多时候不是 Verilog 的问题而是板级问题。几个实践经验ADS1256 是高精度 Δ-Σ ADC模拟电源和数字电源最好分开走中间用磁珠或电感隔离地平面也要注意模拟地和数字地的回流路径。基准电压的质量直接决定转换精度尽量用低噪声基准芯片输入端用大电容去耦。SCLK、DIN、CS 这些 SPI 线在靠近芯片端串一个 33Ω 左右的小电阻可以抑制反射对信号完整性和噪声都有好处。SCLK 不要拉太高留足时序余量往往比极限频率更重要尤其是要跑长线场景时。另一个容易忽略的点是如果用了 PGA 增益高增益下噪声会明显变大这是正常的不是代码 bug。在增益 64 的模式下还期待和增益 1 一样的分辨率那基本不可能。所以选增益时要算清楚信号幅度和所需有效位数不要一味用高增益放大。6. 我对这套驱动做过的几个实用改动最后说几个我在实际项目中用过、比较顺手的改动方向。如果你只是想把代码跑起来原包可能够用但要做进正式产品下面这几个点值得参考。第一把 SCLK 生成方式从“分频边沿触发”改成“系统时钟使能采样”。这不止是为了时序收敛更关键的是所有逻辑都在一个时钟域里加 ILA 抓信号、加 FIFO、加跨时钟域处理都方便很多。驱动代码写完之后我基本不会再依赖内部生成的 SCLK 边沿做逻辑。第二输出接口加一个 valid 脉冲。读取完成的那一拍同时拉高data_valid用这个脉冲做后续 FIFO 写使能比让后级模块去数data_ready简单可靠得多。多通道时还可以把通道号一并输出这样后级拿到数据就知道是哪一路的。第三错误恢复要多一个超时分支。ADS1256 如果上电后主时钟异常DRDY 可能一直不拉低状态机如果没有超时跳转整条采集链路就永远卡死了。我会在 WAIT_DRDY 和 CALIBRATE 状态里各放一个超时计数器超时后自动回到 IDLE再触发一次复位和重新初始化这样系统自恢复能力会强很多。本文还有配套的精品资源点击获取