简介
在之前讲 {% 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 (三线式) 通信
该种接线方式将数据接口 (MISO、MOSI) 整合成了一个双向数据接口 (SDIO),不是标准的 SPI 形式,这种 SPI 通信只支持半双工通信,但这是一种常用和被广泛支持的变体,通常称为 “三线 SPI” 或 “半双工 SPI”。三线式 SPI 通信同样支持多设备共享 SDIO 总线。
用一张表格来对比四线式 SPI 和 三线式 SPI 的区别:
| 对比项 | 标准 SPI (四线式) | 三线 SPI (四线式) |
|---|---|---|
| 信号线 | 4根,SCLK, MOSI, MISO, CS | 3根:SCLK, SDIO, CS |
| 数据线 | 两根独立:发送 (MOSI) 和接收 (MISO) 分开 | 一根共用:发送和接收都通过同一条 SDIO 线 |
| 传输模式 | 全双工:可同时发送和接收 | 半双工:同一时刻只能发送或接收,不能同时进行 |
| 引脚数量 | 较多 | 较少,节省I/O资源 |
关于 SDIO 如何切换数据方向:这个要看具体的芯片数据手册,每款芯片的数据手册对于换向的时机都是不一样的。如我调试时的某款 ADC 芯片 SDIO 通信时序如下 (高位先发):
bit 功能 功能介绍 23 R/W# 读写控制位,读操作时写 1,写操作时写 0 [22:08] A0 to A14 15 位寄存器地址 [07:00] Do to D7 7 位数据,读操作时为向寄存器写入该数据,写操作时为读回指定地址的寄存器数据,读操作时此时的 SDIO 方向为输入
SPI 的四种通信模式
由 CPOL(时钟极性) 和 CPHA(时钟相位) 控制
- CPOL(时钟极性): 控制 SCLK 电平在空闲时的状态
- 若 CPOL=0 则当 SPI 总线空闲时, SCLK 处于低电平状态
- 若 CPOL=1 则当 SPI 总线空闲时, SCLK 处于高电平状态
- CPHA(时钟相位): 控制数据的采样边沿
- 若 CPHA=0 则在时钟信号的第一个跳变沿 (通常是上升沿) 进行数据采样
- 若 CPHA=1 则在时钟信号的第二个跳变沿 (通常是下降沿) 进行数据采样
| 模式 | CPOL (时钟极性) | CPHA (时钟相位) | 说明 |
|---|---|---|---|
| 0 | 0 | 0 | 空闲时低电平, 上升沿采集 |
| 1 | 0 | 1 | 空闲时低电平, 下降沿采集 |
| 2 | 1 | 0 | 空闲时高电平, 上升沿采集 |
| 3 | 1 | 1 | 空闲时高电平, 下降沿采集 |
SPI 模块参数以及端口定义
| 参数 | 功能描述 |
|---|---|
| DATA_WIDTH | 数据位宽 |
| CPOL | 时钟极性 |
| CPHA | 时钟相位 |
| DLK_DIV | SCLK 的时钟分频系数 |
| MOSI_IDLE_STATE | MOSI 信号线在空闲时的状态 |
| CONTINUOUS_CLK | 是否产生连续的时钟信号,若设置为 1’b0,则表示时钟仅在 SPI 活跃时产生。若设置为 1’b1,则表示时钟信号始终产生 |
| 端口 | I/O | 作用 |
|---|---|---|
| clk | I | 模块的时钟输入 |
| rst_n | I | 模块复位信号,低电平有效 |
| dat_tx[N-1:0] | I | 要通过 SPI 总线发送的数据(N 为数据位宽) |
| dat_rx[N-1:0] | O | 由从机接收到的数据(N 为数据位宽) |
| start_op | I | 一个时钟周期的脉冲信号,用于开始 SPI 通信 |
| spi_end | O | 一个时钟周期的脉冲信号,表示 SPI 发送或接收完成 |
| spi_sclk | O | spi 的时钟信号线 |
| spi_cs_n | O | spi 的片选信号线 |
| spi_miso | I | SPI 的一根数据线,由从机输出,主机输入 |
| spi_mosi | O | SPI 的一根数据线,由主机输出,从机输入 |
| data_bit_cnt | O | 发送的比特位计数器,用于在其顶层实现 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 signalalways @(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 ; endend
// generate spi_sclk_internal signalalways @(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 ; endend
// assign continouous clockgenerate 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_edge和sample_edge则表示 SPI 数据的更新边沿和采样边沿,依照 CPHA 参数的设置在上升沿或下降沿更新/采样数据。
wire spi_sclk_negedge = spi_sclk_internal & spi_sclk_inv ; // pulse signal, indicates SCLK negedgewire 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 CPHAwire 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 mosireg [DATA_WIDTH-1:0 ] data_rx_buffer ; // rx data buffer, shift in from misoreg [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 negedgewire 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 CPHAwire 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 signalalways @(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 ; endend
// spi busy indicatoralways @(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 ; endend
// REGION_HEADER------------------------------------------------------------------------------------// generate spi_sclk_inv signalalways @(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 ; endend
// generate spi_sclk_internal signalalways @(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 ; endend
// assign continouous clockgenerate 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 bitalways @(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 ; endend
// send dataalways @(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}} ; endend
// receive dataalways @(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 ; endend
// move data from data_rx_buffer to data_rx output and generate spi_end signalalways @(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 ; endend
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 ; endend
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 ; endend
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 ; endend
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 位根据读写控制来调整方向将判断条件设置为你需要的值即可。
