从零构建SPI主控制器:Vivado实战全记录
你有没有遇到过这样的场景?手头有个传感器,文档写得清清楚楚“支持SPI接口”,可你的FPGA板子上偏偏没有现成的IP核可用。这时候,是去翻Xilinx库找现成模块,还是自己动手写一个?
我选后者。
今天就带你用Vivado + Verilog,从无到有实现一个SPI主控制器——不靠IP、不调例程,一行行代码敲出来,一步步验证调通。这不仅是一次接口设计实践,更是对FPGA开发全流程的深度锤炼。
SPI不是“串口”,它是高速通信的底层利器
先别急着打开Vivado,咱们先把协议吃透。很多人把SPI当成和UART一样的“串口”,其实大错特错。
SPI(Serial Peripheral Interface)是由Motorola提出的一种同步、全双工、主从式串行总线,典型四线结构:
- SCLK:时钟线,由主设备驱动;
- MOSI:主发从收;
- MISO:主收从发;
- CS/SS:片选信号,低电平有效。
它没有地址帧、不需要应答机制,通信完全由主设备掌控。正因为这种“霸道总裁”式的控制方式,SPI才能做到几十MHz的传输速率——远超I2C,也比UART灵活得多。
但自由意味着责任。SPI最大的坑在哪?四个字:模式匹配。
四种模式怎么选?看懂CPOL和CPHA就够了
SPI允许配置两种关键参数:
- CPOL(Clock Polarity):空闲时钟电平
- CPHA(Clock Phase):数据采样边沿
组合起来就是四种工作模式:
| CPOL | CPHA | 模式说明 |
|---|---|---|
| 0 | 0 | 空闲低,上升沿采样 |
| 0 | 1 | 空闲低,下降沿采样 |
| 1 | 0 | 空闲高,上升沿采样 |
| 1 | 1 | 空闲高,下降沿采样 |
比如ADXL345加速度计常用Mode 3(CPOL=1, CPHA=1),而W25Q64 Flash芯片多用Mode 0(CPOL=0, CPHA=0)。两边配错了,哪怕波形再漂亮,数据也是乱的。
所以第一原则:查手册!查手册!查手册!
我们这次的目标是从Mode 0开始,搞定最常见的配置。
动手写Verilog:状态机才是灵魂
打开Vivado,新建RTL工程,目标器件选Zybo Z7上的XC7Z020。然后新建spi_master.v文件,准备开干。
核心思路很清晰:用状态机控制流程,分频器生成SCLK,移位寄存器处理数据。
系统时钟100MHz,目标SCLK为10MHz,那就要每5个周期翻转一次(半周期计数)。数据8位,高位先行,CPHA=0 → 在SCLK第一个边沿采样。
module spi_master ( input clk, input rst_n, input start, input [7:0] data_in, output reg sclk, output mosi, input miso, output reg cs_n, output reg done, output reg [7:0] data_out );端口定义简单直接。重点来了——状态机怎么划分?
四个状态走天下:IDLE → START → TRANSFER → STOP
localparam STATE_IDLE = 2'd0; localparam STATE_START = 2'd1; localparam STATE_TRANSFER = 2'd2; localparam STATE_STOP = 2'd3; reg [1:0] state;- IDLE:等
start信号; - START:拉低CS,准备发数据;
- TRANSFER:逐位移出MOSI,同时从MISO接收;
- STOP:释放CS,置位
done。
中间最关键的TRANSFER阶段,我们要在每个SCLK周期完成三件事:
1. 把当前最高位送到MOSI;
2. 读取MISO并左移进接收寄存器;
3. 发送寄存器左移一位,补0;
因为是Mode 0,SCLK空闲为低,上升沿驱动数据,下降沿采样。所以我们让SCLK在计数器归零时翻转,并在此刻更新MOSI。
always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sclk <= 1'b0; end else if (sclk_en && sclk_cnt == 0) sclk <= ~sclk; end注意这里sclk_en只在TRANSFER状态下使能,避免干扰其他阶段。
至于数据传输部分:
recv_reg <= {recv_reg[6:0], miso}; shift_reg <= {shift_reg[6:0], 1'b0};典型的移位操作。发送寄存器不断左移,低位补0;接收寄存器则把新来的bit挤进去。
最后当bit_cnt == 7时,表示第8位已经发出,下一个SCLK后就可以进入STOP状态了。
整个逻辑干净利落,资源占用极小——一个小型状态机+几个寄存器,就能撑起一套完整通信协议。
Vivado实战:仿真、约束、烧录一步不能少
代码写完只是开始,真正考验在工具链。
先仿真:别急着上板,波形对了再说
创建测试平台tb_spi_master.v,给个100MHz时钟,复位后拉高start信号。
关键点在于模拟从机行为。我们可以假设从机回传固定值0xA5:
wire [7:0] slave_out = 8'hA5; assign miso = (!cs_n && sclk) ? slave_out[7-bit_cnt] : 1'bz;运行Behavioral Simulation,观察波形:
cs_n是否在start后及时拉低?- SCLK是否稳定输出10MHz方波?
- MOSI是否依次送出0x5A的每一位(即0101_1010)?
- MISO回传的数据能否被正确采集成0xA5?
重点关注建立时间与保持时间。如果发现数据还没稳定就被采样,就得调整时序逻辑。
小技巧:在Waveform窗口右键选择“Radix → Binary”,方便查看每一位变化。
再约束:引脚分配决定生死
仿真通过后,必须添加.xdc文件绑定物理引脚。这个步骤极其重要——哪怕逻辑完美,接错一个PIN就全盘皆输。
根据Zybo Z7原理图,分配如下:
set_property PACKAGE_PIN J15 [get_ports clk] ; # 100MHz clock set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property PACKAGE_PIN G18 [get_ports rst_n] ; # Reset button set_property IOSTANDARD LVCMOS33 [get_ports rst_n] set_property PACKAGE_PIN E18 [get_ports mosi] set_property IOSTANDARD LVCMOS33 [get_ports mosi] set_property PACKAGE_PIN D18 [get_ports miso] set_property IOSTANDARD LVCMOS33 [get_ports miso] set_property PACKAGE_PIN F18 [get_ports sclk] set_property IOSTANDARD LVCMOS33 [get_ports sclk] set_property PACKAGE_PIN G19 [get_ports cs_n] set_property IOSTANDARD LVCMOS33 [get_ports cs_n]建议使用Vivado的“I/O Planning”视图进行图形化布局,直观又不易出错。
最后综合与实现
点击“Run Synthesis” → “Run Implementation” → “Generate Bitstream”。
如果出现警告“Some pins have no driver”,检查是否遗漏了输出连接;
如果有时序违例(Timing Violation),可能需要优化分频逻辑或插入流水级。
一切顺利的话,你会得到一个.bit文件。
上板调试:ILA让你看清每一拍
下载前,强烈建议插入ILA核抓波形。毕竟实际硬件环境复杂,线长、噪声、电源波动都可能导致仿真没出现的问题。
如何添加ILA?
- 在Sources面板中右键 → Add Source → Add IP;
- 搜索
ila,添加ila_0; - 配置输入探针数量(至少5个:sclk, mosi, miso, cs_n, done);
- 将待观测信号连接过去:
ila_0 u_ila ( .clk(clk), .probe0(sclk), .probe1(mosi), .probe2(miso), .probe3(cs_n), .probe4(done) );重新综合并生成比特流。
烧录后打开Hardware Manager,连接设备,启动ILA捕获。设置触发条件为start == 1,你就能看到真实运行中的SPI时序!
常见问题排查:
- CS没拉低?→ 检查状态跳转逻辑;
- SCLK频率不对?→ 核对分频系数;
- 接收数据错位?→ 查看采样边沿是否与外设要求一致;
- done信号未置起?→ 检查bit_cnt计数边界。
有了ILA,调试效率提升十倍不止。
能不能更进一步?这些扩展值得尝试
基础版跑通之后,不妨思考几个进阶方向:
多从机管理怎么做?
可以通过解码逻辑扩展多个CS信号:
output reg [3:0] cs_array_n; // 控制4个从设备CPU先发命令指定目标设备编号,再启动传输。
支持不同SPI模式?
可以把CPOL/CPHA作为输入参数,动态配置SCLK生成逻辑和采样时机。不过要注意,某些模式下需要延迟半个周期再开始采样。
和MicroBlaze联动?
完全可以把这个模块封装成AXI-Lite外设,挂在总线上。CPU通过写寄存器来发送数据,读寄存器获取结果。这才是真正的“软硬协同”。
甚至可以结合AXI DMA,实现大批量数据自动搬运,适用于图像传感器或高速ADC采集场景。
写在最后:为什么我们要亲手造轮子?
你可能会问:Xilinx不是有AXI Quad SPI IP吗?干嘛还要自己写?
答案很简单:理解本质,掌控细节。
IP黑盒固然方便,但它屏蔽了太多底层信息。当你面对一个通信失败的现场,只知道“IP没输出”是没有意义的。而如果你亲手写过SPI控制器,你会本能地想到:
- 是不是CPOL配反了?
- 分频系数算错了?
- CS释放太早?
- 时钟域没同步?
这些问题的答案,藏在每一行代码里,也藏在每一次ILA抓波中。
掌握Vivado全流程,不只是会点按钮生成比特流。而是要能从协议理解、代码实现、仿真验证、物理约束到硬件调试,形成闭环能力。
下次当你需要对接一个新的SPI传感器,或者要在资源紧张的低端FPGA上省下几百LUT,你会发现:原来我自己就是一个IP generator。
如果你正在学习FPGA开发,不妨就从这个SPI控制器开始练起。评论区留下你的实现心得,我们一起讨论优化方案。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考