用Verilog造轮子:手把手实现SPI主从模块的RTL设计与仿真
在物联网芯片、传感器接口、显示驱动乃至嵌入式存储控制器中,SPI(Serial Peripheral Interface)协议的身影无处不在。它简单、高效、全双工,是芯片间短距离通信的“常青树”。然而,当你打开一个FPGA或ASIC项目,需要快速集成一个SPI控制器时,是选择从IP库中调用一个“黑盒”,还是选择自己动手,从时序图开始,一行行敲出可综合的RTL代码?对于追求极致控制、深度定制或希望彻底理解硬件交互本质的中级开发者而言,后者往往是更富吸引力的选择。自己“造轮子”的过程,不仅是对协议本身的深刻复盘,更是对寄存器传输级(RTL) 设计思想、时钟域处理、三态总线控制等核心硬件设计技能的绝佳锤炼。
本文将带你深入SPI协议的腹地,抛开现成IP的便利,从零开始构建一个参数化、可配置的SPI主从模块。我们将聚焦于如何将一份标准的SPI时序图,精准地翻译成可综合的Verilog代码,并利用ModelSim等工具完成从功能验证到覆盖率分析的全流程。无论你是希望为特定应用定制SPI接口,还是想通过一个完整案例来巩固RTL建模的实战能力,这篇文章都将提供一条清晰的路径。我们会涉及CPOL、CPHA的配置、MOSI/MISO的三态驱动、片选信号的同步化处理,以及如何编写高效的测试平台来保证设计的正确性。
1. 从协议到电路:SPI核心时序的RTL映射
SPI协议的精髓在于其极简的同步串行机制:一个主设备通过SCLK(时钟)、MOSI(主出从入)、MISO(从出主入)和CS(片选)四根线控制一个或多个从设备。其灵活性体现在时钟极性(CPOL)和时钟相位(CPHA)的四种组合模式上。在动手写代码之前,我们必须将这些抽象的时序参数,转化为具体的、可操作的硬件行为描述。
CPOL与CPHA的硬件实现逻辑:
- CPOL (Clock Polarity):决定了SCLK的空闲状态电平。CPOL=0表示空闲时为低电平;CPOL=1则为高电平。这在RTL中直接影响我们生成SCLK的初始值和翻转条件。
- CPHA (Clock Phase):决定了数据在时钟的哪个边沿被采样和驱动。CPHA=0表示数据在SCLK的第一个边沿(若CPOL=0则为上升沿,反之为下降沿)被采样,在下一个边沿更新;CPHA=1则相反。
对于RTL设计者,这意味着我们的状态机或计数器必须精确地在正确的时钟边沿(或基于系统时钟的某个位置)锁存输入数据(MISO或MOSI),并更新输出数据。一个常见的策略是使用一个比SCLK频率高得多的系统主时钟(sys_clk)来产生SCLK并同步采样数据,这样可以避免在设计中引入多个时钟域,简化时序分析。
注意:在RTL设计中,我们通常避免直接使用生成的SCLK作为触发寄存器的时钟沿,而是使用系统时钟
sys_clk来同步所有逻辑。SCLK应被视为一个由sys_clk驱动的寄存器输出信号。这是同步设计的基本原则,能有效避免时钟偏斜(skew)和亚稳态问题。
一个典型的SPI主设备数据传输周期(以8位数据、CPOL=0、CPHA=0为例)可以分解为以下RTL可识别的步骤:
- 空闲阶段:CS为高,SCLK保持CPOL定义的空闲电平(此处为低),MOSI/MISO为高阻或无关。
- 启动传输:CS拉低。经过一个小的建立时间(
t_SUCS)后,主设备在SCLK的第一个边沿(上升沿)到来之前,将第一位数据(MSB)放到MOSI线上。 - 数据移出/移入:
- 在SCLK的上升沿,从设备采样MOSI线(主->从数据有效)。
- 在SCLK的下降沿,主设备可以采样MISO线(从->主数据有效),同时主设备准备下一位数据并更新MOSI线。
- 结束传输:最后一位数据交换完成后,CS拉高,SCLK恢复到空闲电平。
为了在RTL中实现这一流程,我们需要一个状态机或一个精心设计的计数器来协调CS、SCLK的生成以及数据的移位操作。下面是一个SPI主设备核心状态机的简化Verilog描述框架:
// SPI Master Control FSM (Simplified)
localparam S_IDLE = 3'b000;
localparam S_ASSERT_CS = 3'b001;
localparam S_SHIFT = 3'b010;
localparam S_DEASSERT_CS = 3'b011;
reg [2:0] state, next_state;
reg [3:0] bit_cnt; // 位计数器,0-7
reg sclk_int; // 内部SCLK寄存器
reg mosi_int; // 内部MOSI数据寄存器
reg [7:0] shift_reg_tx; // 发送移位寄存器
reg [7:0] shift_reg_rx; // 接收移位寄存器
always @(posedge sys_clk or posedge sys_rst) begin
if (sys_rst) begin
state <= S_IDLE;
bit_cnt <= 4'd0;
sclk_int <= 1'b0; // CPOL=0
// ... 其他寄存器复位
end else begin
state <= next_state;
// 根据状态和bit_cnt生成SCLK
case(state)
S_SHIFT: begin
if (some_condition) sclk_int <= ~sclk_int; // 翻转SCLK
end
default: sclk_int <= (CPOL == 1'b1); // 回到空闲电平
endcase
// 数据移位逻辑
if (state == S_SHIFT && sclk_falling_edge_detected) begin
// 在SCLK下降沿(对于CPHA=0)采样MISO,并移位
shift_reg_rx <= {shift_reg_rx[6:0], miso_in};
// 准备下一位MOSI数据
mosi_int <= shift_reg_tx[7]; // 输出MSB
shift_


107

被折叠的 条评论
为什么被折叠?



