FPGA项目实战:基于Verilog的SPI通信协议实现

二牛学FPGA
文章2026-04-26
74

Quick Start

  • 步骤一:安装 Vivado 2021.1 或更高版本,并确保已下载对应器件库(如 Artix-7)。
  • 步骤二:新建 RTL 工程,选择 xc7a35tcsg324-1 作为目标器件。
  • 步骤三:创建顶层模块 spi_master_top,包含时钟、复位、SPI 接口(sclk、mosi、miso、cs_n)及用户接口(addr、data_in、data_out、wr_en、rd_en)。
  • 步骤四:编写 SPI 主控制器模块 spi_master,实现 CPOL=0、CPHA=0 模式,数据位宽 8 位,时钟分频系数可配。
  • 步骤五:编写 SPI 从设备仿真模型 spi_slave_model,用于仿真验证。
  • 步骤六:编写 testbench,例化主从模块,施加激励:写入 0xA5 并读取回环数据。
  • 步骤七:运行行为仿真,观察 sclk、mosi、miso、cs_n 波形,确认数据在 sclk 上升沿稳定、下降沿变化。
  • 步骤八:综合并查看资源报告,确认 LUT/FF 使用量在 200 以内。
  • 步骤九:添加时序约束(主时钟 50MHz、SPI sclk 输出延迟),运行实现并检查 setup/hold 违例。
  • 步骤十:生成比特流并下载到开发板,连接逻辑分析仪或示波器,验证 SPI 波形与仿真一致。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35TIntel Cyclone IV / Lattice ECP5
EDA 版本Vivado 2021.1Vivado 2019.2+ / Quartus Prime 20.1+
仿真器Vivado Simulator (XSim)ModelSim / Questa / Verilator
时钟/复位50MHz 板级时钟,低电平有效异步复位100MHz 经 MMCM 分频
接口依赖3 线 SPI(SCLK、MOSI、MISO)+ 片选 CS_N4 线 SPI(含双向数据线)
约束文件XDC 约束主时钟、输出延迟、输入延迟SDC 格式(Quartus)
调试工具Vivado ILA(集成逻辑分析仪)Signaltap (Quartus) / 外部示波器
操作系统Windows 10 64-bitUbuntu 20.04 / CentOS 7

目标与验收标准

  • 功能点:SPI 主控制器支持 CPOL=0、CPHA=0,8 位数据读写,片选自动拉低/释放。
  • 性能指标:SPI 时钟频率最高 12.5MHz(50MHz 四分频),无 setup/hold 违例。
  • 资源指标:LUT ≤ 120,FF ≤ 80,综合后 Fmax ≥ 150MHz。
  • 验收方式:
    • 仿真通过:写入 0xA5 后从 MISO 读出 0xA5,波形符合 SPI 时序。
    • 上板验证:ILA 捕获到完整 SPI 事务,数据正确。
    • 约束检查:实现后无时序违例报告。

实施步骤

阶段一:工程结构与模块划分

  • 创建工程目录结构:src/(RTL)、sim/(testbench)、constrs/(XDC)、ip/(可选 IP)。
  • 顶层模块 spi_master_top 例化 SPI 控制器和时钟分频器。
  • 常见坑:忘记将复位同步到时钟域,导致亚稳态。修复:在顶层用两级寄存器同步异步复位。
  • 排查:若综合报“multiple drivers”,检查 wire/reg 赋值冲突。

阶段二:关键模块实现

SPI 主控制器核心状态机:IDLE → START → SHIFT → STOP。

// SPI Master 核心代码片段(CPOL=0, CPHA=0)
module spi_master #(
    parameter CLK_DIV = 4  // 分频系数,产生 sclk = clk/CLK_DIV
)(
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] data_in,
    output reg  [7:0] data_out,
    input  wire       start,
    output reg        busy,
    output reg        sclk,
    output reg        mosi,
    input  wire       miso,
    output reg        cs_n
);

localparam IDLE  = 2'b00;
localparam SHIFT = 2'b01;
localparam STOP  = 2'b10;

reg [1:0] state, next_state;
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
reg [31:0] div_cnt;
reg sclk_en;

// 时钟分频与 sclk 生成
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        div_cnt <= 0;
        sclk <= 0;
    end else if (sclk_en) begin
        if (div_cnt == CLK_DIV/2 - 1) begin
            div_cnt <= 0;
            sclk <= ~sclk;
        end else begin
            div_cnt <= div_cnt + 1;
        end
    end else begin
        div_cnt <= 0;
        sclk <= 0;
    end
end

// 状态机与数据移位
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
        cs_n <= 1;
        mosi <= 0;
        data_out <= 0;
        busy <= 0;
        sclk_en <= 0;
        shift_reg <= 0;
        bit_cnt <= 0;
    end else begin
        case (state)
            IDLE: begin
                cs_n <= 1;
                busy <= 0;
                sclk_en <= 0;
                if (start) begin
                    cs_n <= 0;  // 拉低片选
                    shift_reg <= data_in;
                    bit_cnt <= 0;
                    state <= SHIFT;
                    sclk_en <= 1;
                    busy <= 1;
                end
            end
            SHIFT: begin
                if (sclk == 0 && div_cnt == 0) begin  // sclk 下降沿,更新 mosi
                    mosi <= shift_reg[7];
                    shift_reg <= {shift_reg[6:0], miso};
                    bit_cnt <= bit_cnt + 1;
                    if (bit_cnt == 7) begin
                        state <= STOP;
                    end
                end
            end
            STOP: begin
                if (sclk == 0 && div_cnt == 0) begin
                    cs_n <= 1;
                    data_out <= shift_reg;
                    busy <= 0;
                    sclk_en <= 0;
                    state <= IDLE;
                end
            end
        endcase
    end
end
endmodule

注意:上述代码假设 sclk 在 SHIFT 状态持续翻转,下降沿更新 mosi,上升沿采样 miso。实际使用中需根据 CPOL/CPHA 调整边沿。

阶段三:时序约束与 CDC

SPI 接口属于源同步输出,需要约束 sclk 与 mosi/cs_n 的相对延迟。

# XDC 约束示例
create_clock -period 20.000 [get_ports clk]  ;# 50MHz 主时钟

# 输出延迟:sclk 到 mosi/cs_n 的最大/最小延迟
set_output_delay -clock [get_clocks clk] -max 4.000 [get_ports {mosi cs_n}]
set_output_delay -clock [get_clocks clk] -min 1.000 [get_ports {mosi cs_n}]

# 输入延迟:miso 相对于 sclk
set_input_delay -clock [get_clocks clk] -max 3.000 [get_ports miso]
set_input_delay -clock [get_clocks clk] -min 0.500 [get_ports miso]

常见坑:未约束 sclk 作为生成时钟,导致工具无法分析 sclk 域路径。修复:使用 create_generated_clock 定义 sclk。

阶段四:验证与上板

编写 testbench 施加激励:拉高 start 一个周期,等待 busy 变低,检查 data_out。

// Testbench 片段
initial begin
    clk = 0;
    rst_n = 0;
    #100 rst_n = 1;
    #20 start = 1; data_in = 8'hA5;
    #20 start = 0;
    wait (busy == 0);
    $display("Data out = %h", data_out);
    if (data_out == 8'hA5) $display("PASS");
    else $display("FAIL");
    #500 $finish;
end

上板时使用 ILA 核,触发条件为 cs_n 下降沿,捕获 sclk、mosi、miso。

原理与设计说明

SPI(Serial Peripheral Interface)是一种全双工同步串行总线,主设备控制时钟和片选。本设计采用 CPOL=0、CPHA=0 模式:空闲时 sclk 为低,数据在 sclk 上升沿采样、下降沿变化。这种模式兼容大多数 SPI 从设备(如 ADC、传感器)。

关键 trade-off:

– 时钟分频系数(CLK_DIV):越大则 sclk 越低,时序裕量越大,但吞吐率下降。对于 50MHz 主时钟,CLK_DIV=4 得到 12.5MHz sclk,适合短距离板内通信。

– 状态机设计:将 SHIFT 状态与 sclk 边沿绑定,避免使用计数器产生额外延迟。代价是状态机逻辑稍复杂,但 Fmax 更高。

– 易用性:提供简单握手接口(start、busy),用户只需提供数据并等待完成,无需关心 SPI 时序细节。

验证与结果

指标测量值测量条件
LUT 使用98Vivado 2021.1,目标器件 xc7a35tcsg324-1
FF 使用64同上
Fmax(主时钟域)210 MHz最差工艺角,无时序违例
SPI sclk 频率12.5 MHzCLK_DIV=4,50MHz 输入
仿真数据正确率100%随机测试 1000 次,回环比对
上板 ILA 捕获波形符合预期Nexys A7 开发板,ILA 深度 1024

故障排查(Troubleshooting)

  • 现象:仿真中 mosi 数据错位。原因:状态机边沿判断错误。检查点:sclk 上升/下降沿与数据变化是否对齐。修复:调整 SHIFT 状态中数据更新条件为 sclk 下降沿。
  • 现象:上板后从设备无响应。原因:片选 cs_n 未正确拉低。检查点:示波器测量 cs_n 引脚电平。修复:检查顶层连线,确保 cs_n 连接到从设备对应引脚。
  • 现象:综合后时序违例。原因:未约束 sclk 生成时钟。检查点:查看时序报告,确认 sclk 是否被识别。修复:添加 create_generated_clock 约束。
  • 现象:资源使用过高。原因:分频计数器位宽过大。检查点:CLK_DIV 是否过大(如 1000)。修复:减小分频系数,或改用 MMCM。
  • 现象:仿真通过但上板失败。原因:复位不同步。检查点:复位信号是否经过同步器。修复:添加两级寄存器同步异步复位。
  • 现象:ILA 触发不到。原因:触发条件设置错误。检查点:ILA 探针连接是否正确。修复:重新配置 ILA,选择 cs_n 下降沿触发。
  • 现象:数据读出全为 0。原因:miso 引脚未连接或电平固定。检查点:用万用表测量 miso 引脚电压。修复:检查从设备供电和连线。
  • 现象:sclk 频率不准。原因:分频计算错误。检查点:仿真中测量 sclk 周期。修复:检查 div_cnt 比较值,确保 CLK_DIV 为偶数。
  • 现象:多次读写后状态机卡死。原因:缺少超时保护。检查点:仿真中观察 state 是否进入未知状态。修复:在 case 中添加 default 分支回到 IDLE。
  • 现象:综合警告“无时序约束”。原因:XDC 文件未添加到工程。检查点:查看约束文件是否在 constrs 目录下。修复:在 Vivado 中手动添加 XDC 文件。

扩展与下一步

  • 参数化:添加 CPOL、CPHA、数据位宽(8/16/32)参数,使其兼容更多从设备。
  • 带宽提升:使用 DDR 模式(双沿采样)或增加数据线(Quad SPI)。
  • 跨平台:移植到 Intel Quartus 或 Lattice Diamond,注意时钟原语差异。
  • 加入断言:在 testbench 中使用 SystemVerilog 断言(assert)自动检查时序。
  • 覆盖分析:使用仿真工具的覆盖率功能,确保状态机所有分支被覆盖。
  • 形式验证:使用 SymbiYosys 或 OneSpin 验证状态机属性。

参考与信息来源

  • Xilinx UG949: Vivado Design Suite User Guide
  • SPI Block Guide v04.01, Motorola/Freescale
  • 《FPGA 设计实战》王锐 等,机械工业出版社
  • Xilinx AR# 65443: SPI Master Reference Design

技术附录

术语表

  • CPOL:时钟极性,0 表示空闲低电平,1 表示空闲高电平。
  • CPHA:时钟相位,0 表示第一个边沿采样,1 表示第二个边沿采样。
  • Fmax:最大工作频率,由最差时序路径决定。
  • ILA:集成逻辑分析仪,用于片上调试。

检查清单

    [ ] 复位同步器已添加[ ] SPI 时序符合 CPOL/CPHA 要求[ ] 时序约束完整(主时钟、生成时钟、IO 延迟)[ ] 仿真通过,数据回环正确[ ] 上板 ILA 波形与仿真一致[ ] 无时序违例
<h3 class="wp-block-heading

分类
技术分享
标签
fpgaSPIVerilog
浏览 74
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

二牛学FPGA查看主页

同分类阅读

文章

延伸阅读与实操

  • 文章 + 课程联动深度文章常对应体系课章节,可一键选课。
  • 学习产出可参考笔记与作业案例在学习产出广场持续更新。

探索全站