FPGA 实战:基于 Verilog 的 ADC128S022 SPI 驱动设计与全流程仿真调试
一、项目背景与目标在 FPGA 数据采集系统中SPI 协议是 ADC 芯片最常用的接口之一。本文以ADC128S0228 通道 12 位 SAR 型 ADC为驱动对象使用纯 Verilog 实现 SPI 主机控制器完成通道配置、串行数据接收、并行结果输出的全功能设计并基于 Vivado 2019.2 搭建完整 Testbench 仿真环境模拟 ADC 输出正弦波数据验证时序正确性与数据采集精度。开发环境开发工具Vivado 2019.2目标器件xc7a35tfgg484-1Artix-7 系列仿真工具XSim硬件接口SPI 模式 0CPOL0, CPHA0二、ADC128S022 核心时序原理ADC128S022 是 TI 的 12 位多通道 ADCSPI 接口特性如下帧结构单次转换固定 16 个 SCLK 周期CS_N 全程保持低电平前 4 周期主机通过 DIN 发送通道地址3 位有效1 位前导零后 12 周期ADC 通过 DOUT 串行输出 12 位采样结果高位在前时序规则SCLK 空闲为高下降沿 ADC 更新输出数据上升沿主机采样输入通道寻址3 位地址对应 8 个单端输入通道三、SPI 主机模块 Verilog 设计3.1 模块架构整体采用计数器驱动的状态机架构不使用复杂的三段式状态机通过分级计数器实现时序控制系统时钟分频计数器产生 SPI 时钟使能脉冲SPI 帧计数器控制 16 个周期的完整转换流程移位寄存器串行采集 DOUT 数据最终输出 12 位并行结果3.2 端口定义表格信号名方向位宽功能说明clk输入1系统时钟默认 50MHzrst_n输入1异步复位低电平有效start输入1转换启动脉冲高电平触发一次采集channel输入3ADC 通道选择地址SCLK输出1SPI 串行时钟DIN输出1主机发往 ADC 的串行数据通道地址CS_N输出1片选信号低电平有效DOUT输入1ADC 发往主机的串行采样数据done输出1转换完成标志高电平脉冲有效data输出1212 位并行采样结果输出四、Testbench 仿真激励设计为了脱离硬件验证 SPI 时序Testbench 实现了两个核心功能模拟 ADC 输出行为自定义任务gene_DOUT在 CS_N 拉低后跟随 SCLK 下降沿逐位输出 16 位串行数据完全贴合 ADC128S022 的输出时序加载正弦波测试数据通过$readmemh系统函数读取外部 txt 文件将 4096 点 12 位正弦波存入存储器依次送入模拟 ADC验证采集数据的连续性与正确性五、仿真调试踩坑全记录本项目调试过程中遇到了多个典型 FPGA 仿真问题均已定位并修复整理如下坑 1$readmemh 文件读取失败memory 全为 X 态现象仿真波形中 memory 数组全部为未知态 X控制台警告cannot be opened for reading原因使用相对路径../../sim_data/时层级计算错误。Vivado XSim 仿真工作目录为SPI.sim/sim_1/behav/xsim/向上两级仅到sim_1目录无法访问工程根目录下的sim_data文件夹解决方案直接使用绝对路径指定数据文件彻底避免路径层级问题verilogdefine sin_data_file /home/xuhaitao/FPGA_project/SPI/sim_data/sin_12bit.txt坑 2Data truncated 数据截断警告现象控制台大量警告Data truncated while reading Datafile从第 256 个地址开始持续报错原因memory 定义为 12 位寄存器但 txt 中是 4 位十六进制数据对应 16 位高位数据被强制截断解决方案使用 Python 生成严格 3 位十六进制的 12 位数据位宽完全匹配消除截断警告坑 3外层循环变量 i 恒为 0仿真死循环现象内层 address 循环持续运行但 i 始终保持初始值 0无法完成多轮遍历原因address 定义为 12 位reg [11:0]最大值只能到 4095循环条件address4096永远成立address 加 1 溢出后自动回卷为 0内层循环永不退出外层 i 无法自增解决方案使用 32 位整型integer作为循环变量地址寄存器仅负责存储器索引从根源避免位宽溢出坑 4SPI 模块时序不工作SCLK 无翻转现象SCLK 持续为高电平CS_N 不动作整个模块无响应原因两处底层逻辑错误分频计数器复位条件写反高电平时反而清零计数器cnt_flag判断对象错误误将 1 位的 flag 与 10 比较永远无法触发脉冲解决方案修正复位条件与判断逻辑确保分频脉冲正常产生驱动 SPI 状态机运行六、仿真结果验证修复所有问题后仿真波形验证正常时序正确性CS_N、SCLK、DIN 符合 SPI 模式 0 规范16 个 SCLK 周期完成一帧转换数据正确性data输出值与 memory 中存储的正弦波数据完全一致采集无错位流程完整性3 轮 4096 点正弦波遍历正常执行i 变量可正常递增到 2标志信号正常start 触发后转换启动done 信号在转换结束时产生一个周期脉冲波形关键观测点放大到微秒级可看到完整的 SPI 单帧时序整体视角下 data 信号呈现连续的正弦波变化规律address 地址随转换完成持续递增边界处正常跳转七、完整工程代码7.1 正弦波数据生成脚本gen_sin.pypython运行import math depth 4096 width 12 offset 2 ** (width - 1) # 直流偏置2048 amplitude 2 ** (width - 1) - 1 # 峰峰值2047 with open(sin_12bit.txt, w) as f: for i in range(depth): val int(offset amplitude * math.sin(2 * math.pi * i / depth)) f.write(f{val:03X}\n)执行命令python3 gen_sin.py生成 4096 行 3 位十六进制数据。7.2 SPI 驱动模块SPI.vverilogtimescale 1ns / 1ps module SPI( input clk, input rst_n, input start, input [2:0]channel, output reg SCLK, output reg DIN, output reg CS_N, input DOUT, output reg done, output reg [11:0]data ); reg en; reg [2:0]r_channel; reg [3:0]cnt; reg cnt_flag; reg [5:0]SCLK_CNT; reg [11:0]r_data; // 通道号锁存 always(posedge clk or negedge rst_n)begin if(!rst_n) r_channel d0; else if(start) r_channel channel; else r_channel r_channel; end // 转换使能控制 always (posedge clk or negedge rst_n)begin if(!rst_n)begin en 1b0; end else if(start) en 1b1; else if(done) en 1b0; else en en; end // 分频计数器 always (posedge clk or negedge rst_n)begin if(!rst_n)begin cnt d0; end else if(en)begin if(cnt d10) cnt d0; else cnt cnt 1; end else cnt d0; end // 分频脉冲标志 always(posedge clk or negedge rst_n)begin if(!rst_n)begin cnt_flag 1b0; end else if(cnt d10) cnt_flag 1b1; else cnt_flag 1b0; end // SPI帧计数器 always(posedge clk or negedge rst_n)begin if(!rst_n) SCLK_CNT d0; else if(en)begin if(SCLK_CNT d33) SCLK_CNT d0; else if(cnt_flag) SCLK_CNT SCLK_CNT 1b1; else SCLK_CNT SCLK_CNT; end else SCLK_CNT d0; end // SPI时序输出与数据采样 always(posedge clk or negedge rst_n)begin if(!rst_n)begin SCLK 1b1; CS_N 1b1; DIN 1b1; end else if(en)begin case(SCLK_CNT) 6d0:begin CS_N 1b0;end 6d1:begin SCLK 1b0;DIN 1b0;end 6d2:begin SCLK 1b1;end 6d3:begin SCLK 1b0;end 6d4:begin SCLK 1b1;end 6d5:begin SCLK 1b0;DIN r_channel[2];end 6d6:begin SCLK 1b1;end 6d7:begin SCLK 1b0;DIN r_channel[1];end 6d8:begin SCLK 1b1;end 6d9:begin SCLK 1b0;DIN r_channel[0];end 6d10,6d12,6d14,6d16,6d18,6d20,6d22,6d24,6d26,6d28,6d30,6d32: begin SCLK 1b1;r_data {r_data[10:0],DOUT};end 6d11,6d13,6d15,6d17,6d19,6d21,6d23,6d25,6d27,6d29,6d31: begin SCLK 1b0;end 6d33:begin CS_N 1b1;end default:begin CS_N 1b1;end endcase end else begin SCLK 1b1; CS_N 1b1; DIN 1b1; end end // 转换完成标志 always(posedge clk or negedge rst_n)begin if(!rst_n)begin done 1b0; end else if(SCLK_CNT d33) done 1b1; else done 1b0; end // 采样结果锁存输出 always(posedge clk or negedge rst_n)begin if(!rst_n)begin data 1b0; end else if(SCLK_CNT d33) data r_data; else data data; end endmodule7.3 仿真测试文件tb.vverilogtimescale 1ns/1ns define sin_data_file /home/xuhaitao/FPGA_project/SPI/sim_data/sin_12bit.txt module SPI_tb; reg clk; reg rst_n; reg start; reg [2:0]channel; wire SCLK; wire DIN; wire CS_N; reg DOUT; wire done; wire [11:0]data; reg [11:0]memory[4095:0]; reg [11:0]address; integer i; integer addr_i; SPI SPI_inst( .clk(clk), .rst_n(rst_n), .start(start), .channel(channel), .SCLK(SCLK), .DIN(DIN), .CS_N(CS_N), .DOUT(DOUT), .done(done), .data(data) ); initial clk 1b1; always#10 clk ~clk; initial $readmemh(sin_data_file,memory); initial begin rst_n 1b0; channel d0; start 1b0; DOUT 1b0; address 0; #100; rst_n 1b1; #100; channel 3; for(i0;i3;ii1)begin for(addr_i0; addr_i4096; addr_iaddr_i1)begin address addr_i[11:0]; start 1; #20; start 0; gene_DOUT({4d0, memory[address]}); (posedge done); #200; end end #20000; $stop; end // 模拟ADC串行输出任务 task gene_DOUT; input [15:0]vdata; reg [4:0]cnt; begin cnt 0; wait(!CS_N); while(cnt16)begin (negedge SCLK) DOUT vdata[15-cnt]; cnt cnt 1b1; end end endtask endmodule7.4 Vivado 工程创建 Tcl 脚本tclcreate_project SPI /home/xuhaitao/FPGA_project/SPI -part xc7a35tfgg484-1 file mkdir /home/xuhaitao/FPGA_project/SPI/SPI.srcs/sources_1/new close [ open /home/xuhaitao/FPGA_project/SPI/SPI.srcs/sources_1/new/SPI.v w ] add_files /home/xuhaitao/FPGA_project/SPI/SPI.srcs/sources_1/new/SPI.v file mkdir /home/xuhaitao/FPGA_project/SPI/SPI.srcs/sim_1/new set_property SOURCE_SET sources_1 [get_filesets sim_1] close [ open /home/xuhaitao/FPGA_project/SPI/SPI.srcs/sim_1/new/tb.v w ] add_files -fileset sim_1 /home/xuhaitao/FPGA_project/SPI/SPI.srcs/sim_1/new/tb.v update_compile_order -fileset sources_1 update_compile_order -fileset sim_1 launch_simulation八、总结本项目完整实现了 ADC128S022 的 SPI 驱动设计从模块架构设计到 Testbench 仿真验证覆盖了 FPGA SPI 接口开发的全流程。调试过程中遇到的路径问题、位宽截断、溢出死循环、逻辑写反等问题都是 FPGA 入门阶段的典型易错点。通过本项目可以掌握SPI 协议的硬件实现方式与时序约束Testbench 模拟外设行为的编写方法$readmemh 系统函数的使用与路径问题排查Verilog 位宽溢出的隐性 bug 定位技巧分级计数器驱动时序逻辑的设计思路后续可基于此模块扩展多通道轮询采集、FIFO 缓存、数据滤波等功能搭建完整的 FPGA 数据采集系统。