简介:面向Xilinx Spartan-6 FPGA开发者的双通道12位ADC(AD9226)输入测试程序,采用Verilog语言编写,基于ISE14.7工程环境组织。工程内部包含ad9226_test顶层模块,通过50MHz时钟为两路ADC提供采样时钟,并集成UART收发接口与ChipScope分频参数,便于用户直接观察双通道采样结果。资源包共163个文件,以Verilog源文件(.v)、UCF约束文件、ISE工程文件(.xise)及仿真/实现脚本(.tcl、.bat、.do)为主,同时提供已生成的bit位流和ngc网表文件,压缩包仅2.31MB,结构清晰、便于二次开发。已有1817人学习浏览。对于正在学习FPGA数据采集或需要快速验证AD9226双通道同步读取的工程师,这份代码提供了完整的工程参考,可帮助理解ADC驱动时序、跨时钟域处理及UART回传调试方法,节省从零搭建工程的时间。
1. 双通道 12bit ADC 与 AD9226:从一块单通道芯片开始
拿到一块双通道 12bit 采样板,第一件事不是写 Verilog,而是认清 AD9226 的真实身份。AD9226 经常成对出现在评估板上,但单片其实是 12 位、最高 65 MSPS 的单通道流水线 ADC。所谓双通道,一般是两片 AD9226 在同一采样时钟下并行工作,输出两路 12bit 码流,FPGA 需要同时把它们读进来。对 XILINX FPGA 工程师来说,这个标题背后是一套很固定的流程:约束 ADC 时钟和数据引脚,在 VERILOG 里完成双通道打拍、打包、缓存,最后用 ISE14.7 的 ChipScope 或仿真确认波形。ISE14.7 今天还在不少旧项目里运行,掌握这套逻辑源码和约束写法,能直接接手遗留工程。下面按接口时序、RTL 实现、ISE 工程约束和数值校准的顺序展开。
2. AD9226 并行数据时序:双通道同步采样的三个关键点
AD9226 是标准的并行 CMOS 接口:12 根数据线 D0-D11,加上一个外部输入的采样时钟。所有数据更新都发生在采样时钟沿附近,没有数据有效信号,也没有转换完成中断。也就是说,FPGA 只能靠采样时钟本身去锁存数据。双通道情况下,还要额外保证两片 ADC 对同一个模拟量在同一时刻采样,否则后续做 I/Q 分离或左右声道对比时,相位误差会直接写进数据。
2.1 取数窗口:为什么要在 clk_adc 上升沿打一拍
AD9226 内部是流水线结构,输出数据相对采样时刻有固定延迟,典型延迟大约 8 个时钟周期。这个延迟不需要在逻辑里补偿,因为每个通道都固定偏移同样拍数,真正要关注的是数据和时钟的建立保持关系。把 clk_adc 接到 FPGA 的全局时钟输入,把 12bit 数据接到同一个 bank 的普通 IO 上,宏观上大概率能满足时序要求。
reg [11:0] adc_a_d; reg [11:0] adc_b_d; always @(posedge clk_adc or negedge rst_n) begin if (!rst_n) begin adc_a_d <= 12'd0; adc_b_d <= 12'd0; end else begin adc_a_d <= adc_a_i; adc_b_d <= adc_b_i; end end assign adc_word = {adc_b_d, adc_a_d};这里 adc_a_i 和 adc_b_i 是两片 AD9226 的原始数据脚,经过寄存器打拍后再拼成 24bit 总线。不要直接对引脚拼接,否则综合后布线会让数据和时钟到达时间差异变大。adc_word 的位宽顺序也要固定下来,低 12bit 是 A 通道,高 12bit 是 B 通道,后面接 FIFO 或 ChipScope 都会按这个顺序看。
2.2 双通道采样时钟必须同源
两片 AD9226 同时采样,最基本的要求就是采样时钟同源。常见做法是外部有源晶振经过时钟 buffer 分三路,分别给 A 片、B 片和 FPGA。FPGA 内部再用 BUFG 把它当采样时钟域。不要用 PLL 生成两个相位不同的 clk_adc 分别送给两片 ADC,PCB 上的时钟 skew 很难压到 1ns 以内,最终会变成两个通道间的固定相位差。
| 项目 | 设计建议 |
|---|---|
| 时钟源 | 外部有源晶振或 FPGA PLL 输出,尽量由同一根时钟树分扇出 |
| IO 电平 | LVCMOS33,bank 电压 3.3V |
| 数据线 | 每片 12 根,建议排在同一 bank 的连续引脚 |
| 接地 | ADC 的 AGND 与 FPGA GND 单点连接,避免地环路 |
注意 AD9226 的 VREF 决定输入满量程,但数字输出始终是 12bit 偏移二进制。FPGA 侧不需要关心参考电压具体是 1V 还是 2V,只需要把它当作 0 到 4095 的码值处理,真正换算成电压是在后续校准阶段。
2.3 时序约束:ISE14.7 里必须写的三行约束
在 ISE14.7 中,如果不加输入时序约束,Place & Route 不会对 ADC 数据和时钟做建立保持分析,结果就是这版能跑,换一颗 FPGA 或温度一变数据就开始跳。常见的 UCF 写法如下:
NET "clk_adc" TNM_NET = "clk_adc"; TIMESPEC "TS_clk_adc" = PERIOD "clk_adc" 20 ns HIGH 50%; OFFSET = IN 4 ns VALID 20 ns BEFORE "clk_adc" RISING;第一行把 clk_adc 定义成全局时序网络,第二行声明采样周期 20ns,也就是 50MHz。最后一行是告诉工具,ADC 数据在 clk_adc 上升沿前 4ns 已经有效,并且在有效电平保持 20ns。如果板级走线特别长,可以把 4ns 改成 2ns,但不要整行删掉。没有这条约束,时序报告会认为输入数据是异步的,连个参考标准都没有。
2.4 三个最容易被新人带偏的细节
- AD9226 输出编码是偏移二进制,不是二进制补码。中间值 2048 对应输入电压中间点,低于 2048 表示输入低于中值,调试时先接 0V 和满量程看码值。
- 数据线上的 RC 滤波不要加大电容。串一个 22Ω 电阻没问题,但并 100pF 电容会明显拉慢上升沿,等于吃掉建立时间。
- 两片 ADC 模拟地不要通过细走线连到 FPGA,单独铺一块 AGND 区域,单点接到 FPGA 地,否则底噪会同时出现在两个通道上。
3. VERILOG 逻辑源码:双通道采集、缓存与读回实现
3.1 模块与端口规划
ISE14.7 工程里,采集逻辑通常分成同步寄存、采集状态机、片上缓存三块。这里不用 FIFO IP core,而是用 reg 数组做简单缓存,原因是为了让工程在老旧 ISE 版本之间迁移时不依赖 IP 版本,直接复制 RTL 就能综合。端口规划如下表所示。
| 信号 | 方向 | 位宽 | 说明 |
|---|---|---|---|
| clk_adc | input | 1 | AD9226 采样时钟,建议由 BUFG 驱动 |
| rst_n | input | 1 | 低电平复位 |
| trig_i | input | 1 | 外部触发信号,上升沿开始采集 |
| adc_a_i | input | 12 | 第一片 AD9226 数据 |
| adc_b_i | input | 12 | 第二片 AD9226 数据 |
| dout | output | 24 | 回读数据,低 12bit A 通道,高 12bit B 通道 |
| dout_vld | output | 1 | 读数据有效 |
| cap_done | output | 1 | 采集完成标志 |
这里没有把系统读写总线放进来,只保留最核心的采集功能。如果需要挂一个软核或 UART,外部再加一层状态机读 dout 即可。
3.2 可综合的双通道采集 RTL
下面这段代码在 ISE14.7 里可以直接作为独立模块加入工程。参数 MEM_DEPTH 决定缓存点数,默认 1024,MEM_AW 是对应地址位宽。
module ad9226_dual_capture #( parameter DATA_W = 12, parameter MEM_DEPTH = 1024, parameter MEM_AW = 10 )( input wire clk_adc, input wire rst_n, input wire trig_i, input wire [DATA_W-1:0] adc_a_i, input wire [DATA_W-1:0] adc_b_i, output wire [DATA_W*2-1:0] dout, output wire dout_vld, output wire cap_done ); localparam S_IDLE = 2'd0; localparam S_CAP = 2'd1; localparam S_DONE = 2'd2; reg [1:0] state; reg [DATA_W-1:0] a_d; reg [DATA_W-1:0] b_d; reg [MEM_AW-1:0] wr_ptr; reg [MEM_AW-1:0] rd_ptr; reg [MEM_AW-1:0] cap_cnt; reg [DATA_W*2-1:0] mem [0:MEM_DEPTH-1]; always @(posedge clk_adc or negedge rst_n) begin if (!rst_n) begin a_d <= {DATA_W{1'b0}}; b_d <= {DATA_W{1'b0}}; end else begin a_d <= adc_a_i; b_d <= adc_b_i; end end always @(posedge clk_adc or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; wr_ptr <= {MEM_AW{1'b0}}; rd_ptr <= {MEM_AW{1'b0}}; cap_cnt <= {MEM_AW{1'b0}}; end else begin case (state) S_IDLE: begin wr_ptr <= {MEM_AW{1'b0}}; rd_ptr <= {MEM_AW{1'b0}}; cap_cnt <= {MEM_AW{1'b0}}; if (trig_i) state <= S_CAP; end S_CAP: begin mem[wr_ptr] <= {b_d, a_d}; wr_ptr <= wr_ptr + 1'b1; cap_cnt <= cap_cnt + 1'b1; if (cap_cnt == MEM_DEPTH - 1) state <= S_DONE; end S_DONE: begin rd_ptr <= rd_ptr + 1'b1; if (rd_ptr == MEM_DEPTH - 1) state <= S_IDLE; end endcase end end assign dout = mem[rd_ptr]; assign dout_vld = (state == S_DONE); assign cap_done = (state == S_DONE); endmodule代码的逻辑是:复位后停在 IDLE,等 trig_i 拉高后进入 CAP,每个 clk_adc 上升沿把两片 ADC 打拍后的数据拼成 24bit,写入 mem 地址 0、1、2 直到 MEM_DEPTH-1,写满自动进入 DONE。DONE 状态下 rd_ptr 每个时钟递增,dout 依次输出全部缓存数据,同时 dout_vld 保持高电平。输出完最后一笔,状态回到 IDLE,准备下一次触发采集。
参数说明要特别提一点:MEM_DEPTH 改成 2048 时,MEM_AW 必须同步改成 11。ISE14.7 的 XST 对 $clog2 的支持不够稳定,所以这里地址位宽直接手写,避免不同版本综合行为不一致。
3.3 状态机边界和读回时序
这个状态机有一个明显特点:采集结束后不需要外部复位,自动进入回读模式。整个 DONE 期间,dout_vld 始终为 1,所以下游可以在每个 clk_adc 上升沿都采样一次。第一个有效数据是启动触发前已经稳定的 A/B 通道码值,最后一个有效数据是刚才写入 mem[MEM_DEPTH-1] 的码值。
如果你要把数据送 UART,不能直接用这个 dout_vld,因为它持续一整段 DONE 窗口。更稳的做法是把 dout 和 dout_vld 再打一拍后接到一个简易沿检测器,用dout_vld && !dout_vld_d作为单周期读使能。这样每次回读周期只会产生一个读脉冲。
3.4 缓存深度与采样窗口的关系
缓存点数不是越大越好。AD9226 最高 65MSPS,缓存 1024 点时窗口长度约 15.7us,2048 点时约 31.5us。如果测试目标是看低频信号,需要提高缓存深度;如果只是验证 ADC 通断,1024 点足够,而且综合后 BRAM 占用更少。用寄存器数组实现时,深度超过 2048 后布线压力会明显增加,此时应该换成 Xilinx FIFO IP 或 Block RAM 原语。
4. ISE14.7 工程约束:UCF、时钟和 ChipScope 联合调试
4.1 工程目录与源文件组织
在 ISE14.7 中新建工程时,我一般不会把所有文件堆在同一个目录。一个最小可复现的工程至少分成四个目录:rtl 放 Verilog 源码,ucf 放引脚和时序约束,chipscope 放生成的 IP core,sim 放 testbench。这样后期改动引脚约束时,不会误碰逻辑源码。
工程向导里选择目标芯片时要按实际板卡型号选,比如 Spartan-6 xc6slx16-2csg324。选错封装,UCF 的 LOC 会直接报错,因为引脚名不存在。
4.2 UCF 引脚约束和输入时序约束写法
以下是一段最小 UCF,里面引脚名是示意,必须按你自己的硬件原理图替换。AD9226 数据线每一根都需要单独约束,不能只约束总线名。
NET "clk_adc" LOC = P17 | IOSTANDARD = LVCMOS33; NET "adc_a_i[0]" LOC = P34 | IOSTANDARD = LVCMOS33; NET "adc_a_i[1]" LOC = P35 | IOSTANDARD = LVCMOS33; NET "adc_a_i[2]" LOC = P36 | IOSTANDARD = LVCMOS33; NET "adc_a_i[3]" LOC = P37 | IOSTANDARD = LVCMOS33; NET "adc_a_i[4]" LOC = P38 | IOSTANDARD = LVCMOS33; NET "adc_a_i[5]" LOC = P39 | IOSTANDARD = LVCMOS33; NET "adc_a_i[6]" LOC = P40 | IOSTANDARD = LVCMOS33; NET "adc_a_i[7]" LOC = P41 | IOSTANDARD = LVCMOS33; NET "adc_a_i[8]" LOC = P42 | IOSTANDARD = LVCMOS33; NET "adc_a_i[9]" LOC = P43 | IOSTANDARD = LVCMOS33; NET "adc_a_i[10]" LOC = P44 | IOSTANDARD = LVCMOS33; NET "adc_a_i[11]" LOC = P45 | IOSTANDARD = LVCMOS33;引脚约束做完,再把时钟和输入时序补上:
NET "clk_adc" TNM_NET = "clk_adc"; TIMESPEC "TS_clk_adc" = PERIOD "clk_adc" 20 ns HIGH 50%; OFFSET = IN 4 ns VALID 20 ns BEFORE "clk_adc" RISING;OFFSET 后面的 4ns 和 20ns 需要根据 PCB 走线长度调整。如果 ADC 数据线在板上绕了很长,建立时间余量会变小,可以先把 4ns 改为 2ns 跑一次,看时序报告里的 setup slack 是否大于零。若还是负,就要从布线长度和 IO logic delay 上找原因,不要试图用缩小约束值骗过工具。
4.3 用 ChipScope ILA 抓 adc_a_i 和 adc_b_i
老工程最常用的调试验证方式不是逻辑分析仪,而是 ChipScope。在 ISE14.7 里用 Core Generator 生成一个 ICON 和一个 ILA,ILA 的数据宽度至少设 24bit,采样深度设 1024,和缓存深度一致。然后在顶层模块里例化:
wire [35:0] icon_control0; ila_dual ila_dual_inst ( .control(icon_control0), .clk(clk_adc), .data({adc_a_i, adc_b_i}) );这里 data 端口用了 24bit,连接的是原始引脚总线,不是打拍后的寄存器。原因很简单:ChipScope 想要看的是 ADC 输入到 FPGA 引脚那一刻的码值,如果接 a_d 和 b_d,会看到延迟一拍后的结果,排查毛刺时容易误判。ChipScope 的 clk 必须接 clk_adc,因为数据变化频率完全由采样时钟决定,接错时钟会看到完全乱掉的波形。
4.4 仿真替代方案
ChipScope 适合上板看信号,但要验证状态机逻辑是否跳对,最好先跑 ModelSim 仿真。在 testbench 里简单产生一个 clk_adc 和 trig_i,每隔 10ns 翻转一次 clk_adc,把 adc_a_i 和 adc_b_i 分别接一个递增计数器。仿真时重点观察 cap_done 拉高后 dout 是否依次输出 mem[0] 到 mem[MEM_DEPTH-1],如果中间出现重复地址或跳地址,优先检查 wr_ptr 和 rd_ptr 是否在 IDLE 被正确清零。
5. 把 AD9226 的 12bit 码值换算成实际电压并校准
5.1 偏移二进制码到电压的换算公式
AD9226 输出偏移二进制编码,所以换算公式很简单。以单端输入范围 0 到 VREF 为例:
- 输入 0V 时,理想码值约为 0
- 输入 VREF/2 时,理想码值约为 2048
- 输入接近 VREF 时,理想码值接近 4095
实际电压换算公式为:Vin = code / 4095 x VREF。如果板子配置成差分输入,则先用 code 减去 2048,再乘以每个 LSB 代表的电压。关于 VREF 的大小,务必查看自己板子的原理图,因为不同评估板可能把 AD9226 的 VREF 接到不同电源或电阻网络,同一个码值对应的真实电压可能完全不同。
| 输入电压(假设 VREF=2V) | 理想 12bit 偏移二进制码 |
|---|---|
| 0V | 0x000 |
| 1V | 0x800 |
| 2V | 0xFFF |
把这三组值用信号源灌入 ADC,再看 ChipScope 抓到的码值。如果 0V 读到 0x200 而不是 0x000,说明板上有直流偏置;如果 2V 读到 0xE00,则说明满量程没有对齐,需要回查 VREF 和前端运放增益。
5.2 快速判断采集链路是否正常的两个经验
第一个经验:把 ADC 输入悬空时,观察 ChipScope 里码值的抖动幅度。正常情况下,输入悬空会引入噪声,但 12bit 码值的抖动量通常应该小于正负 10 个 LSB。如果抖动达到几百个 LSB,问题多半不在数字逻辑,而在模拟电源和 VREF 噪声上。此时需要先在 PCB 上用示波器量 AD9226 的 VREF 引脚,排除电源纹波。
第二个经验:接一个稳定的直流电压,连续采集 1024 点,码值应该始终落在同一个值附近。如果出现周期性跳到 0x000 或 0xFFF,基本可以判断是数字接口时序出问题,重新跑一遍 ISE14.7 的 timing report,看 OFFSET IN 那条路径的 slack。负 slack 出现时,优先缩短 ADC 数据线长度,而不是提高 FPGA 内部驱动强度。
拿到正确的码值后,把这个换算表填进 ChipScope 的 radix 设置,直接用十六进制数对电压关系,就能确定双通道采集的字节对齐和通道顺序是否正确。
本文还有配套的精品资源,点击获取