4215 字
21 分钟
FPGA SPI 通信

简介#

在之前讲 {% post_link ‘FPGA 串口通信’ %} 的时候,有讲到过串口通信是异步通信,而 SPI 通信是一个典型的同步通信。它需要主设备驱动时钟信号线,所有数据位的发送和接收都在时钟的边沿触发,收发双方不再需要各自校准波特率,也不用起始位/停止位等操作,因此相比于异步的串口通信,这种通信方式效率更高,时序更稳定。缺点则是占用了更多的管脚,在 PCB 的布局布线上相比于串口复杂一些。

标准 SPI(四线式) 接口#

标准 SPI 接口采用的是四线式接线方式,有 SCLK、MOSI、MISO 以及 SS#/CS# 四根信号线。这种方式的 SPI 接口支持全双工通信,即同时接收和发送。

  • SCK/SCLK:SPI 的串行时钟,由主设备 (Master) 驱动。
  • MOSI:全称:Master Out Slave In,是标准 SPI 的一根数据线,由主机输出,从机输入
  • MISO:全称:Master In Slave Out,是标准 SPI 的一根数据线,由从机输出,主机输入
  • SS#/CS#:全称:Slave Select/Chip Select 由主机驱动的片选信号线,用于多设备共享一个 SPI 总线时,选定指定设备有效的信号。该信号低电平有效

关于片选信号线:从机设备片选信号线为高电平时,不会接收来自 MOSI 数据线上的数据,同时设备本身也不会输出任何数据。

下面是一张多设备共享 SPI 通信的连接方式图 SPI 一主多从接线方式

半双工 SPI (三线式) 通信#

该种接线方式将数据接口 (MISO、MOSI) 整合成了一个双向数据接口 (SDIO),不是标准的 SPI 形式,这种 SPI 通信只支持半双工通信,但这是一种常用和被广泛支持的变体,通常称为 “三线 SPI” 或 “半双工 SPI”。三线式 SPI 通信同样支持多设备共享 SDIO 总线。

用一张表格来对比四线式 SPI 和 三线式 SPI 的区别:

对比项标准 SPI (四线式)三线 SPI (四线式)
信号线4根,SCLK, MOSI, MISO, CS3根:SCLK, SDIO, CS
数据线两根独立:发送 (MOSI) 和接收 (MISO) 分开一根共用:发送和接收都通过同一条 SDIO 线
传输模式全双工:可同时发送和接收半双工:同一时刻只能发送或接收,不能同时进行
引脚数量较多较少,节省I/O资源

关于 SDIO 如何切换数据方向:这个要看具体的芯片数据手册,每款芯片的数据手册对于换向的时机都是不一样的。如我调试时的某款 ADC 芯片 SDIO 通信时序如下 (高位先发):

SPI-SDIO通信时序

bit功能功能介绍
23R/W#读写控制位,读操作时写 1,写操作时写 0
[22:08]A0 to A1415 位寄存器地址
[07:00]Do to D77 位数据,读操作时为向寄存器写入该数据,写操作时为读回指定地址的寄存器数据,读操作时此时的 SDIO 方向为输入

SPI 的四种通信模式#

CPOL(时钟极性)CPHA(时钟相位) 控制

  • CPOL(时钟极性): 控制 SCLK 电平在空闲时的状态
    • CPOL=0 则当 SPI 总线空闲时, SCLK 处于低电平状态
    • CPOL=1 则当 SPI 总线空闲时, SCLK 处于高电平状态
  • CPHA(时钟相位): 控制数据的采样边沿
    • CPHA=0 则在时钟信号的第一个跳变沿 (通常是上升沿) 进行数据采样
    • CPHA=1 则在时钟信号的第二个跳变沿 (通常是下降沿) 进行数据采样
模式CPOL
(时钟极性)
CPHA
(时钟相位)
说明
000空闲时低电平, 上升沿采集
101空闲时低电平, 下降沿采集
210空闲时高电平, 上升沿采集
311空闲时高电平, 下降沿采集

SPI 模块参数以及端口定义#

参数功能描述
DATA_WIDTH数据位宽
CPOL时钟极性
CPHA时钟相位
DLK_DIVSCLK 的时钟分频系数
MOSI_IDLE_STATEMOSI 信号线在空闲时的状态
CONTINUOUS_CLK是否产生连续的时钟信号,若设置为 1’b0,则表示时钟仅在 SPI 活跃时产生。若设置为 1’b1,则表示时钟信号始终产生
端口I/O作用
clkI模块的时钟输入
rst_nI模块复位信号,低电平有效
dat_tx[N-1:0]I要通过 SPI 总线发送的数据(N 为数据位宽)
dat_rx[N-1:0]O由从机接收到的数据(N 为数据位宽)
start_opI一个时钟周期的脉冲信号,用于开始 SPI 通信
spi_endO一个时钟周期的脉冲信号,表示 SPI 发送或接收完成
spi_sclkOspi 的时钟信号线
spi_cs_nOspi 的片选信号线
spi_misoISPI 的一根数据线,由从机输出,主机输入
spi_mosiOSPI 的一根数据线,由主机输出,从机输入
data_bit_cntO发送的比特位计数器,用于在其顶层实现 SDIO 通信
module spi_master #(
    parameter                       DATA_WIDTH          = 4'd8             , // data width
    parameter                       CPOL                = 1'b0              , // Clock polarity
    parameter                       CPHA                = 1'b0              , // Clock phase
    parameter                       CLK_DIV             = 8'd10             , // input clk division, used to generate SCLK.
    parameter                       MOSI_IDLE_STATE     = 1'b0              , // when module goes to idle. the MOSI line logic level will be set to this value
    parameter                       CONTINUOUS_CLK      = 1'b0                // when set to 1'b1, the SCLK line will continouous or only toggle when cs is low logic if it sets to 1'b0.
)(
    input                           clk                                     , // module clock
    input                           rst_n                                   , // module reset signal, active low
    input       [DATA_WIDTH-1:0]    dat_tx                                  , // the data to be transmitted to slave device
    output  reg [DATA_WIDTH-1:0]    dat_rx                                  , // the data received from the slave device
    input                           start_op                                , // pulse, trigger transfer dat operation
    output  reg                     spi_end                                 , // signal is high when transferring data
    // PHY signals
    output                          spi_sclk                                , // serial clock
    output  reg                     spi_cs_n                                , // chip select signal, active low
    input                           spi_miso                                , // master in slave out
    output  reg                     spi_mosi                                , // master out slave in
    output  reg [7:0           ]    data_bit_cnt                              // bit transmit counter
);
// ...
endmodule

串行时钟的产生#

通过计数器产生 sclk_inv 信号,该信号为一个时钟周期的脉冲信号,每一次脉冲信号都代表着 SCLK 信号将会在下一个时钟周期跳变一次。

// REGION_HEADER------------------------------------------------------------------------------------
reg spi_sclk_inv ; // This signal is a pulse signal with a clk period, and on its rising edge, the SCLK signal will invert once.
reg spi_sclk_internal ; // internal serial clock divied from module clock
// generate spi_sclk_inv signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 8'd0 ;
spi_sclk_inv <= 1'b0 ;
end
else if (clk_cnt == CLK_DIV - 1'b1) begin
clk_cnt <= 8'd0 ;
spi_sclk_inv <= 1'b1 ;
end
else if (clk_cnt == CLK_DIV[7:1] - 1'b1) begin
clk_cnt <= clk_cnt + 1'b1 ;
spi_sclk_inv <= 1'b1 ;
end
else begin
clk_cnt <= clk_cnt + 1'b1 ;
spi_sclk_inv <= 1'b0 ;
end
end
// generate spi_sclk_internal signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_sclk_internal <= 1'b0 ;
end else if (spi_sclk_inv) begin
spi_sclk_internal <= !spi_sclk_internal ;
end else begin
spi_sclk_internal <= spi_sclk_internal ;
end
end
// assign continouous clock
generate
if (CONTINUOUS_CLK == 1'b1)
assign spi_sclk = spi_sclk_internal ^ CPOL ;
else
assign spi_sclk = (spi_cs_n == 1'b1) ? CPOL : (spi_sclk_internal ^ CPOL) ;
endgenerate
// REGION_FOOTER------------------------------------------------------------------------------------

时钟边沿的判断#

根据内部的 sclk_internal 信号,即可判断上升沿和下降沿。

  • sclk_internal高电平spi_sclk_inv 产生了脉冲信号的时候,表示 sclk_internal 将会在下个时钟周期跳变到低电平,此时则为 sclk_internal 的下降沿。
  • sclk_internal低电平spi_sclk_inv 产生了脉冲信号的时候,表示 sclk_internal 将会在下个时钟周期跳变到高电平,此时则为 sclk_internal 的上升沿。
  • update_edgesample_edge 则表示 SPI 数据的更新边沿和采样边沿,依照 CPHA 参数的设置在上升沿或下降沿更新/采样数据。
wire spi_sclk_negedge = spi_sclk_internal & spi_sclk_inv ; // pulse signal, indicates SCLK negedge
wire spi_sclk_posedge = !spi_sclk_internal & spi_sclk_inv ;
wire update_edge = (CPHA == 0) ? spi_sclk_negedge : spi_sclk_posedge ; // Select update and sample edges based on CPHA
wire sample_edge = (CPHA == 0) ? spi_sclk_posedge : spi_sclk_negedge ;

完整的 SPI 代码#

module spi_master #(
parameter DATA_WIDTH = 5'd24 , // data width, [1bit RW ctrl, 15 bits register addr, 8 bits data]
parameter CPOL = 1'b0 , // Clock polarity
parameter CPHA = 1'b0 , // Clock phase
parameter CLK_DIV = 8'd10 , // input clk division, used to generate SCLK.
parameter MOSI_IDLE_STATE = 1'b0 , // when module goes to idle. the MOSI line logic level will be set to this value
parameter CONTINUOUS_CLK = 1'b0 // when set to 1'b1, the SCLK line will continouous or only toggle when cs is low logic if it sets to 1'b0.
)(
input clk , // module clock
input rst_n , // module reset signal, active low
input [DATA_WIDTH-1:0] dat_tx , // the data to be transmitted to slave device
output reg [DATA_WIDTH-1:0] dat_rx , // the data received from the slave device
input start_op , // pulse, trigger transfer dat operation
output reg spi_end , // signal is high when transferring data
// PHY signals
output spi_sclk , // serial clock
output reg spi_cs_n , // chip select signal, active low
input spi_miso , // master in slave out
output reg spi_mosi , // master out slave in
output reg [7:0 ] data_bit_cnt // bit transmit counter
);
// NOTE_HEADER--------------------------------------------------------------------------------------
// SPI Modes | CPOL | CPHA | Note
// | (Clock Polarity) | (Clock Phase) |
// ==========+==================+===============+===================================================
// 0 | 0 | 0 | Clock low level when idle, data sampled on rising edge
// 1 | 0 | 1 | Clock low level when idle, data sampled on falling edge
// 2 | 1 | 0 | Clock high level when idle, data sampled on rising edge
// 3 | 1 | 1 | Clock high level when idle, data sampled on falling edge
// NOTE_FOOTER--------------------------------------------------------------------------------------
//================================================================================
// local parameter declarations
//================================================================================
//================================================================================
// reg declarations
//================================================================================
reg [DATA_WIDTH-1:0 ] data_tx_buffer ; // tx data buffer, shift out to mosi
reg [DATA_WIDTH-1:0 ] data_rx_buffer ; // rx data buffer, shift in from miso
reg [7:0 ] clk_cnt ; // a counter for frequency division
reg spi_sclk_inv ; // This signal is a pulse signal with a clk period, and on its rising edge, the SCLK signal will invert once.
reg spi_sclk_internal ; // internal serial clock divied from module clock
reg trans_end_d ;
reg trans_start ;
reg spi_start_op_d0 ;
reg spi_start_op_d1 ;
reg spi_busy ; // internal busy signal
//================================================================================
// wire declarations
//================================================================================
wire spi_sclk_negedge = spi_sclk_internal & spi_sclk_inv ; // pulse signal, indicates SCLK negedge
wire spi_sclk_posedge = !spi_sclk_internal & spi_sclk_inv ;
wire update_edge = (CPHA == 0) ? spi_sclk_negedge : spi_sclk_posedge ; // Select update and sample edges based on CPHA
wire sample_edge = (CPHA == 0) ? spi_sclk_posedge : spi_sclk_negedge ;
wire spi_start_op_pulse ;
//================================================================================
// assign declarations
//================================================================================
assign spi_start_op_pulse = !spi_start_op_d0 & spi_start_op_d1 ;
//================================================================================
// MAIN CODE
//================================================================================
// sync start_op signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_start_op_d0 <= 1'b0 ;
spi_start_op_d1 <= 1'b0 ;
end else begin
spi_start_op_d0 <= start_op ;
spi_start_op_d1 <= spi_start_op_d0 ;
end
end
// spi busy indicator
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_busy <= 1'b0 ;
end else if (spi_start_op_pulse) begin
spi_busy <= 1'b1 ;
end else if (spi_end) begin
spi_busy <= 1'b0 ;
end else begin
spi_busy <= spi_busy ;
end
end
// REGION_HEADER------------------------------------------------------------------------------------
// generate spi_sclk_inv signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 8'd0 ;
spi_sclk_inv <= 1'b0 ;
end
else if (clk_cnt == CLK_DIV - 1'b1) begin
clk_cnt <= 8'd0 ;
spi_sclk_inv <= 1'b1 ;
end
else if (clk_cnt == CLK_DIV[7:1] - 1'b1) begin
clk_cnt <= clk_cnt + 1'b1 ;
spi_sclk_inv <= 1'b1 ;
end
else begin
clk_cnt <= clk_cnt + 1'b1 ;
spi_sclk_inv <= 1'b0 ;
end
end
// generate spi_sclk_internal signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_sclk_internal <= 1'b0 ;
end else if (spi_sclk_inv) begin
spi_sclk_internal <= !spi_sclk_internal ;
end else begin
spi_sclk_internal <= spi_sclk_internal ;
end
end
// assign continouous clock
generate
if (CONTINUOUS_CLK == 1'b1)
assign spi_sclk = spi_sclk_internal ^ CPOL ;
else
assign spi_sclk = (spi_cs_n == 1'b1) ? CPOL : (spi_sclk_internal ^ CPOL) ;
endgenerate
// REGION_FOOTER------------------------------------------------------------------------------------
// count data bit
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_bit_cnt <= 8'd0 ;
end
else if (
spi_busy &&
update_edge &&
(data_bit_cnt < DATA_WIDTH)
) begin
data_bit_cnt <= data_bit_cnt + 1'b1 ;
end else if (update_edge && data_bit_cnt == DATA_WIDTH) begin
data_bit_cnt <= 8'd0 ;
end else begin
data_bit_cnt <= data_bit_cnt ;
end
end
// send data
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_mosi <= MOSI_IDLE_STATE ;
data_tx_buffer <= {DATA_WIDTH{1'b0}} ;
end else if (spi_start_op_pulse) begin
data_tx_buffer <= dat_tx ;
end
else if (
spi_busy &&
update_edge &&
(data_bit_cnt < DATA_WIDTH)
) begin
spi_mosi <= data_tx_buffer[DATA_WIDTH-1] ; // output data, MSB First
data_tx_buffer <= { // left-shift data
data_tx_buffer[DATA_WIDTH-2:0] ,
data_tx_buffer[DATA_WIDTH-1]
};
end else if (
spi_busy &&
update_edge &&
(data_bit_cnt >= DATA_WIDTH)
) begin
spi_mosi <= MOSI_IDLE_STATE ;
data_tx_buffer <= {DATA_WIDTH{1'b0}} ;
end
end
// receive data
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_rx_buffer <= {DATA_WIDTH{1'b0}} ;
end else if (spi_cs_n == 1'b0 && sample_edge) begin
data_rx_buffer[0] <= spi_miso ; // sample data
data_rx_buffer[DATA_WIDTH-1:1] <= data_rx_buffer[DATA_WIDTH-2:0]; // right-shif in sampled data to buffer
end else begin
data_rx_buffer <= data_rx_buffer ;
end
end
// move data from data_rx_buffer to data_rx output and generate spi_end signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_end <= 1'b0 ;
dat_rx <= {DATA_WIDTH{1'b0}} ;
end else if (update_edge && (data_bit_cnt == DATA_WIDTH)) begin
spi_end <= 1'b1 ;
dat_rx <= data_rx_buffer ;
end else begin
spi_end <= 1'b0 ;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
trans_end_d <= 1'b0 ;
end else begin
trans_end_d <= spi_end ;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
trans_start <= 1'b0 ;
end else if (spi_busy & update_edge) begin
trans_start <= 1'b1 ;
end else begin
trans_start <= 1'b0 ;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spi_cs_n <= 1'b1 ;
end else if (trans_start) begin
spi_cs_n <= 1'b0 ;
end else if (trans_end_d) begin // used a extended version to meet the timing requirements(clock to enable low time) of some device
spi_cs_n <= 1'b1 ;
end
end
endmodule

实现三线式 SPI 通信#

SDIO 通信完全不需要从头开始编写代码,以上方提供的 spi_master 模块的代码作为基础,将其作为一个子模块封装到顶层模块,再用 IOBUF 原语实现一个双向 IO 即可。

完整代码如下:

module sdio_interface (
input wire clk , // 模块的时钟信号
input wire rst_n , // 模块的复位信号,低电平有效
input wire rh_wl , // 读写控制信号, 读为高电平,写为低电平
input wire sdio_start_op , // 操作使能的脉冲信号
input wire [15:00] register_addr , // 16 位寄存器地址
input wire [07:00] dat_tx , // 要发送的数据
output wire [07:00] dat_rx , // 从 sdio 接口读取到的 8 位寄存器数据
output wire spi_sclk , // 串行时钟信号
output wire spi_cs_n , // 片选信号
inout wire sdio , // 双向 IO 端口
);
// 例化 spi_master 模块
wire spi_mosi ;
wire spi_miso ;
wire [23:00] dat_rx_t ; // 将 spi_master 模块接收到的 24 位数据暂存到此处,我们只需要低八位的寄存器数据
wire [07:00] data_bit_cnt;
wire sdio_dir = (data_bit_cnt <= 16) ? 1'b0 : rh_wl; // 前 16 位:读写控制位和寄存器地址,固定为输出方向,低 8 位根据读写控制来调整方向
assign dat_rx = dat_rx_t[07:00] ; // 只取出低八位数据
spi_master # (
.DATA_WIDTH ( 24 ), // data width, [1bit RW ctrl, 15 bits register addr, 8 bits data]
.CPOL ( 1'b0 ), // Clock polarity
.CPHA ( 1'b0 ), // Clock phase
.CLK_DIV ( 8'd10 ), // input clk division, used to generate SCLK.
.MOSI_IDLE_STATE ( 1'b0 ), // when module goes to idle. the MOSI line logic level will be set to this value
.CONTINUOUS_CLK ( 1'b0 ) // when set to 1'b1, the SCLK line will continouous or only toggle when cs is low logic if it sets to 1'b0.
) spi_master (
.clk ( clk ), // [I] [ ] module clock
.rst_n ( rst_n ), // [I] [ ] module reset signal, active-low
.dat_tx ( {rh_wl, register_addr, dat_tx} ), // [I] [DW-1:0] the data to be transmitted to slave device
.dat_rx ( dat_rx ), // [O] [DW-1:0] the data received from the slave device
.start_op ( sdio_start_op ), // [I] [ ] pulse, trigger SPI transfer
.spi_end ( spi_end ), // [O] [ ] busy is high level when transferring data
.spi_sclk ( spi_sclk ), // [O] [ 0:0] serial clock
.spi_cs_n ( spi_cs_n ), // [O] [ 0:0] chip select signal, active low
.spi_mosi ( spi_mosi ), // [I] [ 0:0] master in slave out
.spi_miso ( spi_miso ), // [O] [ 0:0] master out slave in
.data_bit_cnt ( data_bit_cnt ) // [O] [ 7:0] bit transmit counter
);
// 例化 IOBUF 原语
IOBUF # (
.DRIVE ( 12 ),
.IBUF_LOW_PWR ( "TRUE" ),
.IOSTANDARD ( "DEFAULT" ),
.SLEW ( "SLOW" )
) ms14d2600_spi_iobuf (
.O ( spi_miso ), // IOBUF 原语的输出端口,接到 spi_miso 中
.IO ( sdio ), // IOBUF 原语的双向端口,输出到模块端口
.I ( spi_mosi ), // IOBUF 原语的输入端口,接到 spi_mosi 中
.T ( sdio_dir ) // IOBUF 原语的方向控制端口,low: output, high: input
);
endmodule

这里我实现的三线式 SPI 通信适用于我上面提到的某款 ADC 芯片通信时序,对于 sdio 方向的更改时机可以通过修改代码中第 21 行:

wire sdio_dir = (data_bit_cnt <= 16) ? 1'b0 : rh_wl; // 前 16 位:读写控制位和寄存器地址,固定为输出方向,低 8 位根据读写控制来调整方向

将判断条件设置为你需要的值即可。

FPGA SPI 通信
https://blog.tyh123.top/posts/52a1f482/
作者
TYH
发布于
2026-04-19
许可协议
CC-BY-NC-SA 4.0