
简介这是一份基于 Verilog 的 FPGA 模数转换驱动工程面向嵌入式系统开发者和数字电路学习者围绕 TI 公司 12 位 ADC128S022 设计了从接口定义到状态机控制的完整逻辑重点解决芯片通信时序、数据取样与跨时钟域同步等实际问题。压缩包为 rar 格式共 83 个文件、约 3.38MB内容涵盖 .v/.vhd 源文件、.qpf/.qsf Quartus 工程配置、.do 仿真脚本以及 .vsdx 模块图、.xls 时序表、.wlf 仿真波形等支持文档便于从代码、结构与时序多角度掌握驱动流程。已有 798 人浏览学习适合具备数字电路基础、正在调试高速 ADC 或学习 FPGA 状态机设计的进阶者使用。工程内既有 ADC128S022_top.v 顶层模块和 testbench 激励文件也保留了 RTL、simulation、output_files、incremental_db 等完整目录方便快速还原开发环境并直接运行仿真。通过这份驱动设计读者可以具体了解 ADC 转换时序中的启动信号、完成信号处理以及双缓冲区避免丢数、错误检测和同步电路等关键环节对完成同类数据采集项目具有直接参考价值。1. 为什么非要用FPGA去驱动ADC128S022实际开发中很多工程师拿着MCU直接软模拟SPI去读ADC128S022采样率上到200kHz就CPU占用爆表。这个项目把驱动逻辑完全下沉到FPGA里用Verilog实现了一个可综合的ADC128S022驱动包含Quartus工程和ModelSim/Vivado仿真testbench典型应用是多通道数据采集、示波器前端、工业模拟量输入。适合两类人一类是刚接触FPGA想拿真实芯片练手的另一类是MCU软SPI被中断压垮、想换FPGA收尾的。下面按时序、状态机、仿真和排错的顺序把驱动拆开讲代码可以直接抄进自己的采集模块。2. ADC128S022的SPI时序和接口定义先看波形再写代码很多资料里写“ADC128S022有START、EOC、RD引脚”那是拿并行ADC的通用逻辑套上来的。实际芯片是标准SPI接口只有CS、SCLK、DIN、DOUT四个信号线电源和参考电压另算。FPGA驱动要做的不是控制转换开始而是正确发起SPI读事务并且在同一帧里把通道配置写进去。ADC128S022是TI的8通道、12位、最高1MSPS的ADCSPI接口支持Mode 1或Mode 3。实际驱动里大多数用mode 3空闲时SCLK为高数据在SCLK下降沿输出、在上升沿锁存。下面表格是常用接法信号方向作用CSFPGA输出片选拉低启动一次传输整个16个SCLK期间保持低SCLKFPGA输出串行时钟由FPGA内部时钟分频得到最高16MHzDINFPGA输出命令输入SCLK上升沿被ADC锁存DOUTFPGA输入转换结果输出SCLK下降沿更新我的建议是先把SCLK降到1MHz左右跑通再去压最大速率。常见问题是FPGA里把SCLK当普通信号处理没有加IO约束导致PCB延迟和引脚到内部寄存器的走线延迟加起来超过半个周期读回来的数据就错位。2.1 引脚和SPI模式不是并行接口上表里DOUT的采样方式和DIN的发送沿是决定驱动成败的关键。很多从并行ADC转过来的开发者习惯等一个“转换完成”信号但ADC128S022没有EOC它把转换结果放在下一帧的DOUT上。也就是说FPGA必须严格按照16位一帧的格式持续与ADC交互不能像并行ADC那样发一次启动就干等。CS拉低后SCLK就开始推动内部状态机前置的4个无效位之后才是真正的12位数据所以驱动里接收移位寄存器的宽度至少是16位最后再截取高12位。这种设计的好处是节省引脚坏处是时序窗口敏感。CS、SCLK、DIN、DOUT四条线在PCB上长度差太远时高速模式下会出现采样偏移。我一般会在顶层模块里把SCLK分频系数做成参数上板调试时先跑慢速波形稳定后再往高速调。2.2 16位传输帧和通道配置一次完整的SPI事务包含16个SCLK周期。DIN上要发送的16位序列由三部分组成3位通道地址ADD2~ADD0、3位掉电模式PD2~PD0和10位任意填充。通道地址决定本次转换选择哪一个模拟输入映射关系如下通道地址(ADD2:0)选择通道000IN0001IN1010IN2011IN3100IN4101IN5110IN6111IN7DOUT返回的16位序列中前4位是无效前导位后12位是转换结果MSB在前。由于ADC128S022有流水线延迟第N帧写入的通道选择命令对应的转换结果要到第N1帧才能读到。上电后的第一帧结果没有意义必须丢弃。掉电模式默认填000表示正常工作如果做低功耗设计可以按手册填不同值让芯片在转换间隙休眠但要注意唤醒时间连续高速采集时反而会增加出错概率。2.3 顶层模块端口定义与参数化基于以上分析我把顶层模块设计成参数化形式方便不同板卡直接复用module adc128s022_driver #( parameter CLK_FREQ 50_000_000, // FPGA系统时钟频率 parameter SCLK_FREQ 5_000_000 // 串行时钟频率 )( input wire clk, input wire rst_n, // ADC物理接口 output reg cs_n, output reg sclk, output wire din, input wire dout, // 用户逻辑接口 input wire [2:0] ch_sel, // 当前选择的通道 input wire start, // 单次采样触发 output reg valid, // 数据有效标志 output reg [11:0] adc_data // 12位转换结果 );din定义成wire是因为它直接从移位寄存器输出由状态机控制。ch_sel和start是给上层逻辑用的如果不做轮询可以把ch_sel固定成一个常数。这个接口最大的好处是屏蔽了底层时序上层只需要等待valid脉冲即可读取adc_data。3. 状态机实现把16个SCLK周期切成三段驱动ADC128S022的核心是状态机。我不推荐用一段式状态机因为后续要加超时、掉电模式、多通道轮询一段式改一次要动一大批逻辑。用三段式状态机把状态转移、次态逻辑、输出寄存器分开仿真和排错都清楚。3.1 为什么用三段式状态机一段式状态机把状态寄存器和组合逻辑写在一个always块里代码紧凑但可读性差二段式把次态和输出混在一起容易出现锁存器三段式把触发器输出单独用寄存器锁存是多通道采集场景下最稳妥的写法。这里的状态就三个IDLE等待start拉高并释放CSTRANSFER产生16个SCLK周期同时发送命令、接收数据DONE锁存数据并把valid拉高一个周期然后回到IDLE。这个流程里没有多余状态但为毛刺超时预留了插入位置。3.2 核心状态机代码下面给出可综合的状态机核心逻辑localparam IDLE 2d0; localparam TRANSFER 2d1; localparam DONE 2d2; reg [1:0] state, next_state; reg [3:0] bit_cnt; // 记录16个SCLK周期 reg [7:0] sclk_div; // 分频计数器 reg sclk_d; // sclk打拍用于产生边沿脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end always (*) begin next_state state; case (state) IDLE: if (start) next_state TRANSFER; TRANSFER: if (bit_cnt 4d15 sclk_d sclk) next_state DONE; DONE: next_state IDLE; endcase end这里的结束条件在SCLK最后一个上升沿到来时触发因为此时16位数据已经全部移入接收寄存器。bit_cnt在TRANSFER状态下根据SCLK上升沿递增当它等于15时表示传输完成。sclk_d sclk是上升沿检测的常见写法组合逻辑脉冲不作为时钟使用只作为使能信号避免产生多驱动时钟域。3.3 SCLK生成与数据采样打架SCLK生成要放在状态机旁边但和数据处理分开写否则综合工具会优化出奇怪的结果。我常用的写法是always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk_div 0; sclk 1b1; // 空闲高电平对应SPI mode3 end else if (state TRANSFER) begin if (sclk_div (CLK_FREQ / SCLK_FREQ / 2 - 1)) begin sclk_div 0; sclk ~sclk; end else begin sclk_div sclk_div 1b1; end end else begin sclk_div 0; sclk 1b1; end end注意分频系数计算需要保证CLK_FREQ能被SCLK_FREQ整除否则实际SCLK频率会有偏差。例如50MHz主频、5MHz SCLK分频计数值是4即每5个时钟翻转一次正好得到5MHz。数据接收要抓住DOUT的更新时刻DOUT在SCLK下降沿更新所以FPGA应该在SCLK上升沿采样always (posedge clk or negedge rst_n) begin if (!rst_n) rx_shift 16h0; else if (state TRANSFER sclk_pos) // sclk_pos是上升沿脉冲 rx_shift {rx_shift[14:0], dout}; end组合产生的rsclk_pos脉冲宽度是一个系统时钟周期用这个使能去采样DOUT等效于在SCLK上升沿处抓取数据。实际工程里可以定义一个wireassign sclk_pos sclk !sclk_d;其中rsclk_d是在系统时钟下对sclk打一拍的寄存器注意这样会产生一个组合逻辑脉冲不能当全局时钟只能当电平使能。3.4 命令发送与多通道轮询DIN发送的是16位命令序列可以用移位寄存器在TRANSFER过程中逐位送出。更简洁的做法是先把命令装进tx_cmd然后在SCLK低电平期间更新DINreg [15:0] tx_cmd; reg din_reg; always (*) begin tx_cmd {ch_sel, 3b000, 10h000}; end always (posedge clk or negedge rst_n) begin if (!rst_n) din_reg 1b0; else if (state TRANSFER sclk_low_phase) din_reg tx_cmd[15 - bit_cnt]; end assign din din_reg;tclk_low_phase是SCLK为低的使能信号确保ADC在SCLK上升沿看到的DIN数据是稳定的。多通道轮询时只需要在DONE状态把ch_sel_reg加1然后让上层逻辑在下一次start到来时读取新的通道号。由于ADC有流水线延迟通道切换后的第一次adc_data仍然是上一个通道的结果建议在DONE状态把adc_data和当前ch_sel_reg同时打一拍输出这样上层拿到的是对齐的通道和数据。4. 仿真验证和时序坑testbench怎么搭才有效拿到项目后直接上板是错误做法。这个资源里带了ADC128S022_tb.v我的建议是先跑完整仿真再碰硬件。Testbench里要模拟ADC的行为而不仅仅是拉高拉低几个信号。4.1 在testbench里写一个ADC行为模型不需要从网上下载模型在Verilog testbench里写一个简化的行为模型就够了。核心是监听CS下降沿然后根据SCLK的上升沿接收DIN命令在SCLK下降沿返回数据。下面是最简的DOUT行为模型reg [15:0] tx_data_stub; integer bit_count; always (negedge sclk) begin if (!cs_n) begin if (bit_count 0) tx_data_stub {4b0000, adc_stub_data, 4b0000}; dout tx_data_stub[15 - bit_count]; bit_count (bit_count 15) ? 0 : bit_count 1; end end这里的adc_stub_data是testbench手动赋值的12位结果比如直接赋值12h800仿真波形里能清楚看到转换结果是否出现在正确位置。行为模型里还要模拟流水线延迟即命令写入后数据在下一帧返回。bit_count在CS低电平时计数CS拉高后清零。4.2 仿真中常见的“三个想不到”第一个坑是CS拉低后DOUT并不会立即有效它需要几十ns的响应时间。如果你在CS下降沿立刻采样DOUT就会采到高阻态。第二个坑是SCLK空闲电平有的开发板把SCLK空闲拉低而ADC128S022预期空闲高电平如果配置成mode0或mode2结果会整体错位。第三个坑是CS恢复时间手册要求两帧之间CS必须保持高电平至少一段时间如果驱动里连续启动转换而CS高电平时间不够ADC状态机不会复位。仿真时把这三项对应到波形里对照数据手册的时序参数看一遍再上板。4.3 跨时钟域与亚稳态处理DOUT由ADC的SCLK下降沿驱动FPGA内部用SCLK上升沿采样两者差半个SCLK周期一般情况下没问题。但当SCLK接近16MHz时PCB走线延迟加上FPGA内部时钟偏斜采样裕度很小。常见做法是跨时钟域打两拍reg dout_meta, dout_sync; always (posedge clk) begin dout_meta dout; dout_sync dout_meta; end提示打两拍只适用于SCLK远小于系统时钟的场景。如果SCLK超过10MHz建议把SCLK当作输入时钟直接采或者用FPGA的IODDR原语。对ADC128S022的常见应用SCLK 5MHz以下打两拍足够。5. 上板后的三个验证技巧SignalTap、ILA和示波器最后聊点板上调试的实用技巧。5.1 用SignalTap II观测内部状态机Quartus工程里可以插入SignalTap II JTAG逻辑分析仪。把state、bit_cnt、sclk、adc_data这些信号加进去但注意复位后的初始采样需要设置触发条件。最容易犯的错误是SignalTap II采样时内部的cs_n已经是IO引脚上的信号由于引脚约束或反相波形里看起来CS永远低。建议在RTL里先把cs_n打一拍生成cs_n_db再挂到SignalTap上。5.2 固定电压验证12位码值给ADC输入一个精确的1.000V前提是参考电压为2.048V然后看adc_data值。12位ADC在2.048V参考下理论值是1.000 / 2.048 * 4096 2000。如果读到的值在1998到2002之间说明驱动时序基本正确。固定值偏大或偏小一两个LSB大概率是参考电压或输入分压问题不用怀疑逻辑。这个方法比用正弦波更容易定位问题因为不需要处理采样时刻和相位对齐。5.3 低速到高速排查信号完整性上板后用示波器直接量SCLK和DOUT。如果SCLK有大量毛刺或振铃可能是FPGA输出驱动强度太高在引脚约束里降低IO电流等级。还有一个有效的小技巧把SCLK频率降到500kHz排除高速信号完整性问题后再逐步提高分频系数看哪个频率点开始出错。这个“从低速到高速”的顺序能快速区分时序逻辑问题和板级物理问题。另外Quartus里对SCLK信号加set_output_delay约束对DOUT加set_input_delay约束会让综合工具规划IO时序更准确。工程里的.qsf文件如果没带约束建议手动补上这两行。没有正确约束时低速SCLK可能能跑一到高速偶发帧数据会随机错位。本文还有配套的精品资源点击获取