FPGA 实战:基于 Verilog 的 ADC128S022 SPI 驱动设计与全流程仿真调试

一、项目背景与目标

在 FPGA 数据采集系统中,SPI 协议是 ADC 芯片最常用的接口之一。本文以ADC128S022(8 通道 12 位 SAR 型 ADC) 为驱动对象,使用纯 Verilog 实现 SPI 主机控制器,完成通道配置、串行数据接收、并行结果输出的全功能设计,并基于 Vivado 2019.2 搭建完整 Testbench 仿真环境,模拟 ADC 输出正弦波数据,验证时序正确性与数据采集精度。

开发环境:

  • 开发工具:Vivado 2019.2
  • 目标器件:xc7a35tfgg484-1(Artix-7 系列)
  • 仿真工具:XSim
  • 硬件接口:SPI 模式 0(CPOL=0, CPHA=0)

二、ADC128S022 核心时序原理

ADC128S022 是 TI 的 12 位多通道 ADC,SPI 接口特性如下:

  1. 帧结构:单次转换固定 16 个 SCLK 周期,CS_N 全程保持低电平
  2. 前 4 周期:主机通过 DIN 发送通道地址(3 位有效,1 位前导零)
  3. 后 12 周期:ADC 通过 DOUT 串行输出 12 位采样结果,高位在前
  4. 时序规则:SCLK 空闲为高,下降沿 ADC 更新输出数据,上升沿主机采样输入
  5. 通道寻址:3 位地址对应 8 个单端输入通道

三、SPI 主机模块 Verilog 设计

3.1 模块架构

整体采用计数器驱动的状态机架构,不使用复杂的三段式状态机,通过分级计数器实现时序控制:

  • 系统时钟分频计数器:产生 SPI 时钟使能脉冲
  • SPI 帧计数器:控制 16 个周期的完整转换流程
  • 移位寄存器:串行采集 DOUT 数据,最终输出 12 位并行结果

3.2 端口定义

表格

信号名 方向 位宽 功能说明
clk 输入 1 系统时钟,默认 50MHz
rst_n 输入 1 异步复位,低电平有效
start 输入 1 转换启动脉冲,高电平触发一次采集
channel 输入 3 ADC 通道选择地址
SCLK 输出 1 SPI 串行时钟
DIN 输出 1 主机发往 ADC 的串行数据(通道地址)
CS_N 输出 1 片选信号,低电平有效
DOUT 输入 1 ADC 发往主机的串行采样数据
done 输出 1 转换完成标志,高电平脉冲有效
data 输出 12 12 位并行采样结果输出

四、Testbench 仿真激励设计

为了脱离硬件验证 SPI 时序,Testbench 实现了两个核心功能:

  1. 模拟 ADC 输出行为 :自定义任务gene_DOUT,在 CS_N 拉低后,跟随 SCLK 下降沿逐位输出 16 位串行数据,完全贴合 ADC128S022 的输出时序
  2. 加载正弦波测试数据 :通过$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文件夹

  • 解决方案 :直接使用绝对路径指定数据文件,彻底避免路径层级问题

    verilog

    复制代码
    `define sin_data_file "/home/xuhaitao/FPGA_project/SPI/sim_data/sin_12bit.txt"

坑 2:Data 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;循环条件address<4096永远成立,address 加 1 溢出后自动回卷为 0,内层循环永不退出,外层 i 无法自增
  • 解决方案 :使用 32 位整型integer作为循环变量,地址寄存器仅负责存储器索引,从根源避免位宽溢出

坑 4:SPI 模块时序不工作,SCLK 无翻转

  • 现象:SCLK 持续为高电平,CS_N 不动作,整个模块无响应
  • 原因 :两处底层逻辑错误
    1. 分频计数器复位条件写反,高电平时反而清零计数器
    2. cnt_flag判断对象错误,误将 1 位的 flag 与 10 比较,永远无法触发脉冲
  • 解决方案:修正复位条件与判断逻辑,确保分频脉冲正常产生,驱动 SPI 状态机运行

六、仿真结果验证

修复所有问题后,仿真波形验证正常:

  1. 时序正确性:CS_N、SCLK、DIN 符合 SPI 模式 0 规范,16 个 SCLK 周期完成一帧转换
  2. 数据正确性data输出值与 memory 中存储的正弦波数据完全一致,采集无错位
  3. 流程完整性:3 轮 4096 点正弦波遍历正常执行,i 变量可正常递增到 2
  4. 标志信号正常:start 触发后转换启动,done 信号在转换结束时产生一个周期脉冲

波形关键观测点:

  • 放大到微秒级可看到完整的 SPI 单帧时序
  • 整体视角下 data 信号呈现连续的正弦波变化规律
  • address 地址随转换完成持续递增,边界处正常跳转

七、完整工程代码

7.1 正弦波数据生成脚本(gen_sin.py)

python

运行

复制代码
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.v)

verilog

复制代码
`timescale 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 <= 1'b0;
    end
    else if(start)
        en <= 1'b1;
    else if(done)
        en <= 1'b0;
    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 <= 1'b0;
    end
    else if(cnt == 'd10)
        cnt_flag <= 1'b1;
    else
        cnt_flag <= 1'b0;
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 + 1'b1;
        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 <= 1'b1;
        CS_N <= 1'b1;
        DIN <= 1'b1;
    end
    else if(en)begin
        case(SCLK_CNT)
        6'd0:begin CS_N <= 1'b0;end
        6'd1:begin SCLK <= 1'b0;DIN <= 1'b0;end
        6'd2:begin SCLK <= 1'b1;end
        6'd3:begin SCLK <= 1'b0;end
        6'd4:begin SCLK <= 1'b1;end
        6'd5:begin SCLK <= 1'b0;DIN <= r_channel[2];end
        6'd6:begin SCLK <= 1'b1;end
        6'd7:begin SCLK <= 1'b0;DIN <= r_channel[1];end
        6'd8:begin SCLK <= 1'b1;end
        6'd9:begin SCLK <= 1'b0;DIN <= r_channel[0];end
        6'd10,6'd12,6'd14,6'd16,6'd18,6'd20,6'd22,6'd24,6'd26,6'd28,6'd30,6'd32:
        begin SCLK <= 1'b1;r_data <= {r_data[10:0],DOUT};end
        6'd11,6'd13,6'd15,6'd17,6'd19,6'd21,6'd23,6'd25,6'd27,6'd29,6'd31:
        begin SCLK <= 1'b0;end
        6'd33:begin CS_N <= 1'b1;end
        default:begin CS_N <= 1'b1;end
        endcase
    end
    else begin
        SCLK <= 1'b1;
        CS_N <= 1'b1;
        DIN <= 1'b1;
    end
end

// 转换完成标志
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        done <= 1'b0;
    end
    else if(SCLK_CNT == 'd33)
        done <= 1'b1;
    else 
        done <= 1'b0;
end

// 采样结果锁存输出
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        data <= 1'b0;
    end
    else if(SCLK_CNT == 'd33)
        data <= r_data;
    else 
        data <= data;
end

endmodule

7.3 仿真测试文件(tb.v)

verilog

复制代码
`timescale 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 = 1'b1;
always#10 clk = ~clk;

initial $readmemh(`sin_data_file,memory);

initial begin
    rst_n = 1'b0;
    channel = 'd0;
    start = 1'b0;
    DOUT = 1'b0;
    address = 0;
    #100;
    rst_n = 1'b1;
    #100;
    channel = 3;
    for(i=0;i<3;i=i+1)begin
        for(addr_i=0; addr_i<4096; addr_i=addr_i+1)begin
            address = addr_i[11:0];
            start = 1;
            #20;
            start = 0;
            gene_DOUT({4'd0, 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(cnt<16)begin
        @(negedge SCLK) DOUT = vdata[15-cnt];
        cnt = cnt + 1'b1;
    end
end
endtask

endmodule

7.4 Vivado 工程创建 Tcl 脚本

tcl

复制代码
create_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 入门阶段的典型易错点。

通过本项目可以掌握:

  1. SPI 协议的硬件实现方式与时序约束
  2. Testbench 模拟外设行为的编写方法
  3. $readmemh 系统函数的使用与路径问题排查
  4. Verilog 位宽溢出的隐性 bug 定位技巧
  5. 分级计数器驱动时序逻辑的设计思路

后续可基于此模块扩展多通道轮询采集、FIFO 缓存、数据滤波等功能,搭建完整的 FPGA 数据采集系统。