欢迎光临
我们一直在努力

SPI 总线详解:从物理层拓扑到 Verilog 主机驱动实战

SPI(串行外围设备接口 Serial Peripheral Interface)

文章目录

  • SPI(串行外围设备接口 Serial Peripheral Interface)
    • 一、SPI总线特点
    • 二、物理层
      • 1、一主一从拓扑
      • 2. 一主多从拓扑
    • 三、协议层
      • 3.1 时钟极性 CPOL(Clock Polarity)
      • 3.2 时钟相位 CPHA(Clock Phase)
      • 3.3 完整数据传输流程
    • 四、SPI 与 IIC 简单对比(硬件面试高频)
    • 五、Verilog 驱动 SPI 主机接口
      • 5.1 驱动代码
      • 5.2 仿真代码及波形

摘要:SPI 是一种高速、同步、串行、全双工的通信总线,采用主从架构,由主机产生 SCLK 时钟并控制片选信号 CS_N 选中从机。本文从总线特点、物理层拓扑(一主一从、一主多从)、协议层(CPOL/CPHA 四种模式)入手,梳理完整数据传输流程,并与 IIC 做对比,最后给出可综合的 Verilog SPI 主机驱动及仿真代码,适合硬件工程师与 FPGA 开发者参考。

一、SPI总线特点

  • 传输速率快,通常可以达到几十 MHz,相比 IIC 总线拥有更高的通信带宽。
  • 物理接口简单,主流为 4 线 SPI,部分设备支持 3 线 SPI(省略 MISO/MOSI 实现半双工)。
  • 边沿采样机制:依靠时钟边沿(上升沿/下降沿)完成数据采样。
  • 无硬件应答 ACK 机制,主机发送数据后无法确认从机是否正确收到数据,抗错误能力弱,软件层面需要做校验来提升可靠性。
  • 拓扑支持:一主一从、一主多从,不支持多主机模式。
  • 通信属性:同步、串行、全双工

    • 同步:由主机输出 SCLK 同步时钟,收发时序完全跟随时钟;
    • 串行:数据按 bit 逐位移位传输;
    • 全双工:MOSI、MISO两条数据线,同一时刻可以同时发送和接收数据。

    二、物理层

    SPI 引脚全部为推挽输出,不需要外接上拉电阻。 四根核心信号线:

    信号全称方向作用
    SCLK Serial Clock 主机输出 同步时钟,由主机产生,控制全部通信时序
    MOSI Master Out Slave In 主机→从机 主机输出数据,从机接收数据
    MISO Master In Slave Out 从机→主机 从机输出数据,主机接收数据
    CS_N(SS_N) Chip Select 主机输出 片选信号,低电平有效,选中对应从机设备

    1、一主一从拓扑

    单主机搭配单个从机,主机的 CS_N 直接连接从机片选引脚;通信时 CS_N 拉低选中从机,通信结束拉高释放。在这里插入图片描述

    全双工工作:SCLK 驱动下,MOSI 发数据的同时 MISO 接收从机返回数据。

    2. 一主多从拓扑

    两种常用方案:

  • 独立片选(常用):主机输出多路独立 CS_N,每一路 CS_N 分别接一个从机;同一时刻只拉低一路片选,只和一个从机通信,其余从机 CS_N 为高电平处于未选中状态。
  • 菊花链模式:多个从机首尾串联,MOSI 接第一个从机,最后一个从机 MISO 回传给主机,所有从机共用同一组 SCLK、CS_N;适合多设备级联,但传输时延会变大。在这里插入图片描述
  • SPI 没有总线仲裁,不存在多主机,只能由一台设备作为主机产生 SCLK 时钟。

    三、协议层

    SPI 通信由两个关键参数决定采样时序:时钟极性 CPOL、时钟相位 CPHA,组合出 4 种 SPI 工作模式,主机和从机必须配置为完全相同模式,否则数据采样错误。

    3.1 时钟极性 CPOL(Clock Polarity)

    决定总线空闲状态下 SCLK 的电平

    • CPOL = 0:空闲时 SCLK 为低电平;
    • CPOL = 1:空闲时 SCLK 为高电平。

    3.2 时钟相位 CPHA(Clock Phase)

    决定在哪一个时钟边沿采样数据。

    • CPHA = 0:在 SCLK 第一个跳变沿采样数据;
    • CPHA = 1:在 SCLK 第二个跳变沿采样数据。 在这里插入图片描述

    四种工作模式组合:

    • Mode 0:CPOL=0,CPHA=0(最常用模式)
    • Mode1:CPOL=0,CPHA=1
    • Mode2:CPOL=1,CPHA=0
    • Mode3:CPOL=1,CPHA=1

    3.3 完整数据传输流程

  • 片选选中从机:主机将对应从机的 CS_N 片选信号拉低,选中目标从机,总线进入通信状态。
  • 主机输出同步时钟SCLK:主机持续输出 SCLK 时钟脉冲,全部数据移位跟随 SCLK 边沿完成交互。
  • MOSI主机发送,从机采样
    • 主机先把待发送 bit 放置到 MOSI 数据线,保证数据电平稳定;
    • 在约定的时钟边沿,从机对 MOSI 进行采样,获取主机发来的数据;
    • 时钟下一个边沿到来前,主机更新 MOSI 上的下一位数据。
  • MISO从机发送,主机采样
    • 和MOSI同步并行工作;从机将输出bit放到MISO线上;
    • 在约定时钟边沿,主机采样MISO获取从机返回的数据;

    SPI本质是移位寄存器交互:每一个SCLK时钟,同时完成一位发送+一位接收。

  • 通信结束:全部bit传输完成,主机将CS_N片选信号拉高,释放从机,SCLK回到空闲电平。
  • 重要特性:SPI是移位交换机制,只要输出一个时钟脉冲,就会同时完成1bit发送 + 1bit接收;哪怕你只想写数据,MISO线上依然会收到从机输出的无效bit;只想读数据,MOSI也需要输出填充bit(一般输出0)。

    四、SPI 与 IIC 简单对比(硬件面试高频)

    对比项SPIIIC
    信号线 4 线(3 线) SDA、SCL 两线
    速率 几十 MHz,速度快 标准 100k,快速 400k
    应答 无 ACK 应答 有 ACK/NACK 应答
    拓扑 一主多从,不支持多主机 支持多主机+总线仲裁
    输出类型 推挽输出,无需上拉 开漏输出,必须外接上拉电阻
    全双工 支持全双工 半双工

    五、Verilog 驱动 SPI 主机接口

    5.1 驱动代码

    //写spi的主机
    //1.接口分析:时钟、复位、上层给到spi接口的开始信号、数据、定义的总传输长度、要写(发送给从机的长度)、接口之间的四线结构
    //输出的数据、以及读写、一次通信完成的握手信号
    //2.四个时序图转换为代码
    module top(
    input wire clk ,
    input wire rst_n ,
    input wire start_spi ,//上层给到spi接口的开始信号
    input wire [7:0] data_i_spi ,//上层给到spi接口的数据
    input wire [7:0] data_len ,//上层给到spi接口的总传输长度
    input wire [7:0] wr_lenth ,//上层给到spi接口的要写(发送给从机的长度)
    output wire [7:0] data_o_spi ,//输出的数据
    output wire done_w ,//握手信号
    output wire done_r ,//握手信号
    output wire done_spi ,//握手信号
    output wire SCK ,//四线
    output wire CS_N ,//四线
    output wire MOSI ,//四线
    input wire MISO //四线
    );
    //参数
    parameter CPOL = 1 ,//时钟极性
    CPHA = 0 ,//时钟相位
    delay = 6 ;//分频系数

    //状态定义
    localparam IDLE = 4'b0001,
    START = 4'b0010,
    DATA = 4'b0100,
    STOP = 4'b1000;

    reg [3:0] state_c,state_n;

    //状态转移
    wire IDLE_START,
    START_DATA,
    DATA_STOP ,
    STOP_IDLE ;

    //内部信号
    reg [2:0] cnt_clk;//分频时钟计数
    reg [7:0] cnt_bit;//比特计数

    reg [1:0] start_reg ;//输入寄存信号
    reg [7:0] data_reg ;//输入寄存信号
    reg [7:0] data_len_r ;//输入寄存信号
    reg [7:0] wr_lenth_r ;//输入寄存信号

    reg SCK_R ;//时钟寄存
    reg [7:0] data_temp ;//输出数据的暂存信号
    reg MOSI_R ;//主机输出寄存
    reg CS_N_R ;//片选信号寄存

    //开始信号的寄存
    always @(posedge clk or negedge rst_n) begin
    if (rst_n == 0) begin
    start_reg <= 2'b00;
    end
    else begin
    start_reg <= {start_reg[0],start_spi};
    end
    end

    //状态机第一段
    always @(posedge clk or negedge rst_n) begin
    if (rst_n == 0) begin
    state_c <= IDLE;
    end
    else begin
    state_c <= state_n;
    end
    end

    //状态机第二段
    always @(*) begin
    if (rst_n == 0) begin
    state_n = IDLE;
    end
    else begin
    case (state_c)
    IDLE :state_n = (IDLE_START) ? START : state_c ;
    START :state_n = (START_DATA) ? DATA : state_c ;
    DATA :state_n = (DATA_STOP ) ? STOP : state_c ;
    STOP :state_n = (STOP_IDLE ) ? IDLE : state_c ;
    default:state_n = IDLE;
    endcase
    end
    end

    //状态转移条件
    assign IDLE_START = (state_c == IDLE ) && start_reg == 2'b01;
    assign START_DATA = (state_c == START) ;
    assign DATA_STOP = (state_c == DATA ) && cnt_bit == data_len_r*8 – 1 && cnt_clk == delay – 1;
    assign STOP_IDLE = (state_c == STOP ) && cnt_clk == delay – 1;

    //状态机第三段
    always @(posedge clk or negedge rst_n) begin
    if (rst_n == 0) begin
    cnt_clk <= 0 ;//分频时钟计数
    cnt_bit <= 0 ;//比特计数

    CS_N_R <= 1 ;//片选信号
    SCK_R <= CPOL;//分频时钟信号
    data_reg <= 0 ;//输入寄存信号
    data_len_r <= 0 ;//输入寄存信号
    wr_lenth_r <= 0 ;//输入寄存信号
    data_temp <= 0 ;//输出数据的暂存信号
    end
    else begin
    case (state_c)
    IDLE :begin//寄存、等待开始信号
    cnt_clk <= 0;//分频时钟计数
    cnt_bit <= 0;//比特计数

    CS_N_R <= 1;//片选信号
    SCK_R <= CPOL;
    data_reg <= data_i_spi;//输入寄存信号
    data_len_r <= data_len;//输入寄存信号
    wr_lenth_r <= wr_lenth;//输入寄存信号
    data_temp <= 0;//输出数据的暂存信号
    end
    START :begin//拉低片选信号
    CS_N_R <= 0;//片选信号拉低
    end
    DATA :begin//数据的更新,接收,发送
    if (cnt_clk == delay – 1) begin//分频时钟计数,也是一个bit计数器
    cnt_clk <= 0;
    end
    else begin
    cnt_clk <= cnt_clk + 1;
    end

    if (cnt_clk == delay – 1) begin//比特计数
    if (cnt_bit == data_len_r*8 – 1) begin
    cnt_bit <= 0;
    end
    else begin
    cnt_bit <= cnt_bit + 1;
    end
    end

    //时钟
    if (cnt_clk == delay/2 || (cnt_clk == 0 && cnt_bit > 0 )) begin
    SCK_R <= ~SCK_R;
    end

    //数据更新
    if ( cnt_clk == 0) begin
    data_reg <= data_i_spi;
    end

    //根据极性来进行采样和寄存
    case (CPHA)
    0:begin//第一个沿进行采集
    //数据发送
    if (cnt_clk == 0) begin//数据已经在上一个字节末尾进行更新
    MOSI_R <= data_reg[7 – cnt_bit%8];//高位优先
    end
    //数据接收/采集
    if (cnt_clk == delay/2 – 2) begin
    data_temp <= {data_temp[6:0],MISO};
    end
    end
    default:begin//第二个沿进行采集
    //数据发送
    if (cnt_clk == delay/2 ) begin
    MOSI_R <= data_reg[7 – cnt_bit%8];//高位优先
    end
    //数据接收/采集
    if (cnt_clk == delay – 2) begin
    data_temp <= {data_temp[6:0],MISO};
    end
    end
    endcase
    end
    STOP :begin//持续一个周期后拉高片选产生done_spi结束本次通信
    if (cnt_clk == delay – 1) begin
    cnt_clk <= 0;
    CS_N_R <= 1;
    end
    else begin
    cnt_clk <= cnt_clk + 1;
    end

    SCK_R <= CPOL;

    end
    default:begin
    cnt_clk <= 0 ;//分频时钟计数
    cnt_bit <= 0 ;//比特计数

    CS_N_R <= 1 ;//片选信号
    SCK_R <= CPOL;//分频时钟信号
    data_reg <= 0 ;//输入寄存信号
    data_len_r <= 0 ;//输入寄存信号
    wr_lenth_r <= 0 ;//输入寄存信号
    data_temp <= 0 ;//输出数据的暂存信号
    end
    endcase
    end
    end

    //输出信号
    // assign data_o_spi = ((CPHA == 0 && cnt_clk == delay/2 && cnt_bit%8 == 7) || (CPHA == 1 && cnt_clk == delay – 1 && cnt_bit%8 == 7)) ? data_temp : data_o_spi ;
    assign data_o_spi = data_temp ;
    assign done_r = (CPHA == 0 && cnt_clk == delay/2 + 1 && cnt_bit%8 == 7 && cnt_bit > wr_lenth*8) || (CPHA == 1 && cnt_clk == 1 && cnt_bit%8 == 0 && ((cnt_bit > wr_lenth*8) || state_c == STOP)) ;
    assign done_w = (CPHA == 0 && cnt_clk == 1 && cnt_bit%8 == 7 && cnt_bit <= wr_lenth*8) || (CPHA == 1 && cnt_clk == delay/2 + 1 && cnt_bit%8 == 7 && cnt_bit <= wr_lenth*8) ;
    assign done_spi = STOP_IDLE ;
    assign SCK = SCK_R ;
    assign CS_N = CS_N_R ;
    assign MOSI = MOSI_R ;

    endmodule

    5.2 仿真代码及波形

    `timescale 1ns/1ps
    module tb ();

    reg clk ;
    reg rst_n ;
    reg start_spi ;
    reg [7:0] data_i_spi;
    wire [7:0] data_o_spi;
    wire done_w ;
    wire done_r ;
    wire done_spi ;
    wire SCK ;
    wire CS_N ;
    wire MOSI ;

    initial begin
    clk <= 0;
    rst_n <= 0;
    start_spi <= 0;
    #100
    rst_n <= 1;
    #100
    start_spi <= 1;
    #100
    start_spi <= 0;
    end

    always #10 clk = ~clk;

    always @(posedge clk or negedge rst_n) begin
    if (rst_n == 0) begin
    data_i_spi <= 8'haa;
    end
    else if (done_r || done_w) begin
    data_i_spi <= data_i_spi + 8'h11;
    end
    end

    top top_inst(
    .clk (clk ),
    .rst_n (rst_n ),
    .start_spi (start_spi ),//上层给到spi接口的开始信号
    .data_i_spi (data_i_spi),//上层给到spi接口的数据
    .data_len (5 ),//上层给到spi接口的总传输长度
    .wr_lenth (3 ),//上层给到spi接口的要写(发送给从机的长度)
    .data_o_spi (data_o_spi),//输出的数据
    .done_w (done_w ),//握手信号
    .done_r (done_r ),//握手信号
    .done_spi (done_spi ),//握手信号
    .SCK (SCK ),//四线
    .CS_N (CS_N ),//四线
    .MOSI (MOSI ),//四线
    .MISO (MOSI ) //四线
    );

    endmodule

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » SPI 总线详解:从物理层拓扑到 Verilog 主机驱动实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址