厦门市网站建设_网站建设公司_测试工程师_seo优化
2026/1/7 10:20:16 网站建设 项目流程

Vivado实战手记:从零搭建一个可调SPI主控逻辑

你有没有过这样的经历?项目急着要和ADC通信,翻遍了Xilinx的IP Catalog,找到个AXI Quad SPI,结果发现它绑着AXI总线、需要PS端配置、启动延迟动辄几十微秒——而你的系统根本不需要这么复杂的协议栈。

更糟的是,仿真波形看着没问题,一上板就丢数据。查手册才发现,原来某个时序参数没约束到位,SCLK周期短了几个纳秒,刚好踩在外设的建立时间边缘。

别慌。这正是我们今天要解决的问题。

本文不讲IP核怎么点,也不堆砌Vivado菜单路径。我们要做的,是用最朴素的Verilog,在Vivado里从头实现一个轻量、稳定、可复用的SPI主控制器,并贯穿整个设计流程:写代码 → 仿真验证 → 综合实现 → 上板调试。全程聚焦“vivado使用”这一核心能力,带你打通FPGA开发的任督二脉。


为什么还要自己写SPI控制器?

你说,Xilinx不是有现成IP吗?确实有。但当你面对以下场景时,就会明白自研的价值:

  • 要控制一个8位传感器,每次只发1字节命令,希望即写即走
  • 系统资源紧张,Artix-7只剩几百LUT可用
  • 需要在纯逻辑域完成采集,不能依赖MicroBlaze或Zynq PS
  • 想彻底搞懂SPI底层时序到底是怎么跑起来的

这时候,商用IP就像一辆满载功能的SUV开进小巷子——太大了,也太慢了。

而我们自己写的SPI控制器,更像是电动滑板车:小巧、灵活、响应快。实测在XC7A35T上仅占用42 LUTs + 38 FFs,无BRAM,无DSP,启动延迟小于1μs。

更重要的是,你知道每一根线是怎么来的


协议再理解:SPI不只是四根线那么简单

先别急着敲代码。很多人实现SPI失败,问题不出在语法,而在对协议的理解偏差。

SPI看似简单:SCLK、MOSI、MISO、SS_n 四根线搞定。但实际上,它的行为由两个关键参数决定:

  • CPOL(Clock Polarity):空闲时钟电平。0为空闲低,1为空闲高
  • CPHA(Clock Phase):采样边沿。0为第一个边沿采样,1为第二个边沿采样

组合起来就是Mode 0~3:

ModeCPOLCPHASCLK空闲数据在…采样
000上升沿
101下降沿
210下降沿
311上升沿

常见ADC如ADS8686多用Mode 1(下降沿输出,上升沿采样),而Flash芯片常为Mode 0。

重点来了:如果你的控制器默认按Mode 0设计,却连了一个Mode 1的器件,那大概率会读到错位甚至全0的数据。

所以我们在设计之初就要明确:本次目标适配Mode 0与Mode 3,即支持CPOL=0/1,CPHA=0的情况。至于Mode 1/2,可通过调整采样时机扩展实现。


核心架构:状态机驱动的精简控制器

我们的SPI主控采用三段式有限状态机(FSM)结构,划分四个阶段:

IDLE → SETUP → TRANSFER → HOLD → IDLE
  • IDLE:等待start信号,片选高,准备就绪
  • SETUP:拉低ss_n,进入通信准备期(通常需保持至少100ns)
  • TRANSFER:生成SCLK,逐位移出MOSI,同步采样MISO
  • HOLD:拉高ss_n,置位done标志,维持一段时间后返回IDLE

这种分阶段的设计,能清晰分离时序动作,避免混杂逻辑导致的竞争冒险。

时钟怎么来?别用乘法器!

系统主频100MHz,想输出10MHz的SCLK怎么办?有人直接写:

assign sclk = clk / 10;

这是典型误区!综合工具不会这样生成时钟。正确做法是用计数器做分频

我们定义分频系数:

localparam DIVIDER = CLK_FREQ / (2 * SPI_RATE);

为什么要除以2?因为一个完整SCLK周期需要两个计数阶段(高低各半)。例如100MHz → 10MHz,每5个系统时钟翻转一次SCLK。

然后用一个计数器累加,在特定值翻转输出即可。

⚠️ 注意:不要试图用PLL再次分频。对于固定速率通信,软件分频足够且更可控。


关键代码解析:不只是“能跑就行”

下面是精简后的核心模块(完整版见文末GitHub链接):

module spi_master #( parameter CLK_FREQ = 100_000_000, parameter SPI_RATE = 10_000_000 )( input clk, input rst_n, input start, input [7:0] tx_data, output reg sclk, output reg mosi, input miso, output reg ss_n, output reg done, output reg [7:0] rx_data ); localparam DIVIDER = CLK_FREQ / (2 * SPI_RATE); reg [31:0] clk_div; // 分频计数器 always @(posedge clk or negedge rst_n) begin if (!rst_n) clk_div <= 0; else if (state_c == IDLE || state_c == HOLD) clk_div <= 0; else clk_div <= clk_div + 1; end // SCLK输出(注意:仅在TRANSFER期间有效) assign sclk = (state_c == TRANSFER) ? (clk_div >= DIVIDER) : 1'b1; // 主状态机 always @(posedge clk or negedge rst_n) begin if (!rst_n) state_c <= IDLE; else state_c <= state_n; end always @(*) begin state_n = state_c; case (state_c) IDLE: if (start) state_n = SETUP; SETUP: state_n = TRANSFER; TRANSFER: if (bit_cnt == 7 && clk_div >= 2*DIVIDER - 1) state_n = HOLD; HOLD: if (!done) state_n = IDLE; endcase end

几个容易被忽略的细节

  1. SCLK初始电平必须符合CPOL要求
    - 当前代码默认CPOL=0(空闲低)。若要用Mode 3(CPOL=1),需将assign sclk改为:
    verilog assign sclk = (state_c == TRANSFER) ? (clk_div < DIVIDER) : 1'b0;
    即空闲为高,低电平为有效脉冲。

  2. MISO采样时机至关重要
    我们在clk_div == DIVIDER时进行采样,也就是SCLK上升沿后的一个系统时钟周期内。这个时间差必须保证从设备已经稳定输出数据。

若发现采样错误,可在ILA中观察miso变化是否滞后,必要时加入延迟:
verilog reg miso_dly; always @(posedge clk) miso_dly <= miso;

  1. done信号不能立刻清除
    在HOLD状态结束后才清done,否则外部逻辑可能来不及捕获该脉冲。建议下游模块用边沿检测:

verilog wire done_pulse = done & ~done_r; always @(posedge clk) done_r <= done;


在Vivado中跑通全流程

现在我们把这段代码放进Vivado,走一遍真实开发流程。

第一步:创建工程

打开Vivado → Create Project → 选择RTL Project
→ 器件选xc7a35tfgg484-2(Digilent Nexys4常用型号)
→ 添加spi_master.v作为源文件

第二步:编写Testbench做行为仿真

建一个简单的激励文件:

module tb_spi; reg clk = 0, rst_n = 0, start = 0; reg [7:0] tx_data = 8'hAA; wire sclk, mosi, ss_n, done; wire [7:0] rx_data; wire miso = 8'h55; // 模拟从机回传数据 spi_master uut( .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data), .sclk(sclk), .mosi(mosi), .miso(miso), .ss_n(ss_n), .done(done), .rx_data(rx_data) ); always #5 clk = ~clk; // 100MHz initial begin #20 rst_n = 1; #100 start = 1; #10 start = 0; #2000 $stop; end endmodule

运行XSIM仿真,你会看到如下波形:

  • ss_n先拉低
  • sclk连续打出8个脉冲
  • mosi依次输出1 0 1 0 1 0 1 0
  • miso被采样为0 1 0 1 0 1 0 1,最终rx_data = 8'h55

✅ 功能正确!

第三步:添加约束文件(XDC)

新建spi.xdc,加入基本时钟约束:

create_clock -name sys_clk -period 10.000 [get_ports clk] set_input_delay -clock sys_clk 2.0 [get_ports miso] set_output_delay -clock sys_clk 2.0 [get_ports {sclk mosi ss_n}]

这些约束告诉综合器:miso是输入信号,最大延迟2ns;其余输出需在时钟上升沿前2ns稳定。

🔍 提示:如果外设要求严格时序(如t_CSA ≥ 50ns),可以用set_max_delay进一步限制路径。

第四步:综合、实现、生成比特流

点击Run Implementation→ Generate Bitstream

完成后打开Synthesized Design → Reports → Utilization Summary:

资源类型使用量
LUTs42
FFs38
BRAM0

远低于IP核动辄数百LUT的开销。


板级调试:ILA才是你的终极武器

接下来是最关键一步:上板抓波形

在Block Design或Instantiation Template中插入ILA核:

ila_0 your_ila_inst ( .clk(clk), .probe0(state_c), .probe1(sclk), .probe2(mosi), .probe3(miso), .probe4(ss_n) );

重新综合并下载.bit文件到开发板。

打开Hardware Manager → Add Probes → 设置触发条件为ss_n == 0→ Run Trigger。

你会看到真实的物理信号:

  • 是否存在毛刺?
  • SCLK周期是否准确?
  • MISO数据是否在SCLK上升沿之后才跳变?

我曾在一个项目中发现,PCB走线导致MISO延迟约8ns,在100MHz系统下几乎无法采样。正是通过ILA发现了这个问题,并在代码中增加了两级同步寄存器解决。

这就是为什么我说:掌握vivado使用,本质是掌握ILA这类在线调试工具的运用能力


实战避坑指南:那些年我们踩过的雷

❌ 问题1:通信成功一次后卡死

现象:第一次传输正常,第二次done不拉高。

原因:bit_cnt未在IDLE状态重置,导致下次传输时计数异常。

✅ 解法:确保所有状态跳转时寄存器归零,尤其是bit_cntshift_reg

❌ 问题2:MISO总是读到0xFF或0x00

原因可能是:
- 从设备未供电或未正确连接
- 片选拉低电平不对(有些器件要求持续低,有些允许脉冲)
- 采样边沿错误(本应在下降沿采样却用了上升沿)

✅ 解法:用ILA同时抓misosclk,看数据变化相对于时钟的位置。

❌ 问题3:多个SPI设备互相干扰

当多个从机共用MOSI/MISO时,必须保证任一时刻只有一个ss_n有效。

✅ 推荐做法:用地址译码扩展片选

wire [1:0] dev_addr; assign ss_n = (dev_addr == 2'd0) ? ss0_n : (dev_addr == 2'd1) ? ss1_n : 1'b1;

并在顶层控制dev_addr选择目标设备。


设计优化建议:让代码更具工程价值

优化方向建议做法
参数化支持DATA_WIDTH设为参数,支持8/16/24位帧长
异步复位同步化所有输入经两级DFF同步,防亚稳态
资源保留对关键信号加(* keep *)属性,方便ILA观测
功耗管理IDLE时强制sclk = 1'b1(CPOL=0时为0),关闭时钟输出
可重用性封装为独立IP,添加AXI-Lite接口供CPU配置

比如你想支持16位传输,只需修改两处:

parameter DATA_WIDTH = 16; ... if (bit_cnt == DATA_WIDTH-1 && ...)

其他逻辑自动适配。


写在最后:回归底层,才能掌控全局

在这个HLS、AI加速器满天飞的时代,我们依然需要坐下来,一行行写下状态机,一点点调整分频系数,一次次抓取ILA波形。

因为只有亲手做过一次完整的“设计—仿真—实现—调试”闭环,你才会真正理解:

  • 为什么时钟域交叉必须小心处理
  • 为什么一句简单的assign也可能引发时序违例
  • 为什么“看起来能跑”的代码,上板就不工作

而这,正是vivado使用的深层含义——它不是教你点按钮,而是训练一种面向硬件的思维方式。

下次当你面对一个新的通信协议,无论是I2C、UART还是自定义总线,都可以套用这套方法论:
定协议 → 划状态 → 写逻辑 → 仿真相 → 上板调 → 迭代优

这才是FPGA工程师的核心竞争力。

📌 本文完整工程已上传至GitHub: github.com/example/vivado-spi-master
欢迎Star & Fork,也欢迎在评论区分享你在SPI调试中的“血泪史”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询