● SPI传输协议
● SPI配置方式
● SPI代码实现
● SPI的优缺点
SPI 主要应用在 EEPROM, Flash, 实时时钟(RTC), 数模转换器(ADC), 数字信号处理器(DSP) 以及数字信号解码器之间。它在芯片中只占用四根管脚 (Pin) 用来控制以及数据传输, 节约了芯片的 pin 数目, 同时为 PCB 在布局上节省了空间。正是出于这种简单易用的特性, 现在越来越多的芯片上都集成了 SPI技术。
![图片[1]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/101672997010.png)
在数据传输的过程中, 每次接收到的数据必须在下一次数据传输之前被采样. 如果之前接收到的数据没有被读取, 那么这些已经接收完成的数据将有可能会被丢弃, 导致 SPI 物理模块最终失效. 因此, 在程序中一般都会在 SPI 传输完数据后, 去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的.
![图片[2]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/71672997014.png)
![图片[3]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/91672997015.png)
CPOL(clock polarity): 时钟极性, 表示 SPI 在空闲时, 时钟信号是高电平还是低电平. 若 CPOL 被设为 1, 那么该设备在空闲时 SCK 管脚下的时钟信号为高电平. 当 CPOL 被设为 0 时则正好相反;
CPHA(clock phase): 时钟相位, 表示 SPI 设备是在 SCK 管脚上的时钟信号变为上升沿时触发数据采样, 还是在时钟信号变为下降沿时触发数据采样. 若 CPHA 被设置为 1, 则 SPI 设备在时钟信号变为下降沿时触发数据采样, 在上升沿时发送数据. 当 CPHA 被设为 0 时也正好相反.
![图片[4]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/81672997017.png)
SSPSR(Synchronous Serial Port Register): 泛指 SPI 设备里面的移位寄存器(Shift Regitser), 它的作用是根据设置好的数据位宽(bit-width) 把数据移入或者移出 SSPBUF;
Controller: 泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式.
通常情况下, 我们只需要对上图所描述的四个管脚(pin) 进行控制即可实现整个 SPI 设备之间的数据通信:
SCK(Serial Clock): 主要的作用是 Master 设备往 Slave 设备传输时钟信号, 控制数据交换的时机以及速率;
SS/CS(Slave Select/Chip Select): 用于 Master 设备片选 Slave 设备, 使被选中的 Slave 设备能够被 Master 设备所访问;
SDO/MOSI(Serial Data Output/Master Out Slave In): 在 Master 上面也被称为 Tx-Channel, 作为数据的出口, 主要用于 SPI 设备发送数据;
SDI/MISO(Serial Data Input/Master In Slave Out): 在 Master 上面也被称为 Rx-Channel, 作为数据的入口, 主要用于SPI 设备接收数据。
![图片[5]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/11672997019.png)
首先, 在这里解释一下两个概念:
CPOL: 时钟极性, 表示 SPI 在空闲时, 时钟信号是高电平还是低电平. 若 CPOL 被设为 1, 那么该设备在空闲时 SCK 管脚下的时钟信号为高电平. 当 CPOL 被设为 0 时则正好相反.
CPHA: 时钟相位, 表示 SPI 设备是在 SCK 管脚上的时钟信号变为上升沿时触发数据采样, 还是在时钟信号变为下降沿时触发数据采样. 若 CPHA 被设置为 1, 则 SPI 设备在时钟信号变为下降沿时触发数据采样, 在上升沿时发送数据. 当 CPHA 被设为 0 时也正好相反.
例如,SPI 数据传输模式被设置成 CPOL = 1, CPHA = 1. 这样, 在一个 Clock 周期内, 每个单独的 SPI 设备都能以全双工(Full-Duplex) 的方式, 同时发送和接收 1 bit 数据, 即相当于交换了 1 bit 大小的数据. 如果 SPI 总线的 Channel-Width 被设置成 Byte, 表示 SPI 总线上每次数据传输的最小单位为 Byte, 那么挂载在该 SPI 总线的设备每次数据传输的过程至少需要 8 个 Clock 周期(忽略设备的物理延迟). 因此, SPI 总线的频率越快, Clock 周期越短, 则 SPI 设备间数据交换的速率就越快.
2. SSPSR
SSPSR 是 SPI 设备内部的移位寄存器(Shift Register). 它的主要作用是根据 SPI 时钟信号状态, 往 SSPBUF 里移入或者移出数据, 每次移动的数据大小由 Bus-Width 以及 Channel-Width 所决定.
Bus-Width 的作用是指定地址总线到 Master 设备之间数据传输的单位.
例如, 我们想要往 Master 设备里面的 SSPBUF 写入 16 Byte 大小的数据: 首先, 给 Master 设备的配置寄存器设置 Bus-Width 为 Byte; 然后往 Master 设备的 Tx-Data 移位寄存器在地址总线的入口写入数据, 每次写入 1 Byte 大小的数据(使用 writeb 函数); 写完 1 Byte 数据之后, Master 设备里面的 Tx-Data 移位寄存器会自动把从地址总线传来的1 Byte 数据移入 SSPBUF 里; 上述动作一共需要重复执行 16 次.
Channel-Width 的作用是指定 Master 设备与 Slave 设备之间数据传输的单位. 与 Bus-Width 相似, Master 设备内部的移位寄存器会依据 Channel-Width 自动地把数据从 Master-SSPBUF 里通过 Master-SDO 管脚搬运到 Slave 设备里的 Slave-SDI 引脚, Slave-SSPSR 再把每次接收的数据移入 Slave-SSPBUF里.
通常情况下, Bus-Width 总是会大于或等于 Channel-Width, 这样能保证不会出现因 Master 与 Slave 之间数据交换的频率比地址总线与 Master 之间的数据交换频率要快, 导致 SSPBUF 里面存放的数据为无效数据这样的情况.
3. SSPBUF
我们知道, 在每个时钟周期内, Master 与 Slave 之间交换的数据其实都是 SPI 内部移位寄存器从 SSPBUF 里面拷贝的. 我们可以通过往 SSPBUF 对应的寄存器 (Tx-Data / Rx-Data register) 里读写数据, 间接地操控 SPI 设备内部的 SSPBUF.
例如, 在发送数据之前, 我们应该先往 Master 的 Tx-Data 寄存器写入将要发送出去的数据, 这些数据会被 Master-SSPSR 移位寄存器根据 Bus-Width 自动移入 Master-SSPBUF 里, 然后这些数据又会被 Master-SSPSR 根据 Channel-Width 从 Master-SSPBUF 中移出, 通过 Master-SDO 管脚传给 Slave-SDI 管脚, Slave-SSPSR 则把从 Slave-SDI 接收到的数据移入 Slave-SSPBUF 里. 与此同时, Slave-SSPBUF 里面的数据根据每次接收数据的大小(Channel-Width), 通过 Slave-SDO 发往 Master-SDI, Master-SSPSR 再把从 Master-SDI 接收的数据移入 Master-SSPBUF.在单次数据传输完成之后, 用户程序可以通过从 Master 设备的 Rx-Data 寄存器读取 Master 设备数据交换得到的数据.
4. Controller
Master 设备里面的 Controller 主要通过时钟信号(Clock Signal)以及片选信号(Slave Select Signal)来控制 Slave 设备. Slave 设备会一直等待, 直到接收到 Master 设备发过来的片选信号, 然后根据时钟信号来工作.
Master 设备的片选操作必须由程序所实现. 例如: 由程序把 SS/CS 管脚的时钟信号拉低电平, 完成 SPI 设备数据通信的前期工作; 当程序想让 SPI 设备结束数据通信时, 再把 SS/CS 管脚上的时钟信号拉高电平.
![图片[6]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/101672997019.png)
但是,随着从机数量的增加,来自主机的片选线的数量也增加。这会快速增加主机需要提供的输入和输出数量,并限制可以使用的从机数量。可以使用其他技术来增加常规模式下的从机数量,例如使用多路复用器产生片选信号。
![图片[7]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/71672997020.png)
使用该方法时,由于数据是从一个从机传播到下一个从机,所以传输数据所需的时钟周期数与菊花链中的从机位置成比例。例如,在上图所示的8位系统中,为使第3个从机能够获得数据,需要24个时钟脉冲,而常规SPI模式下只需8个时钟脉冲。
时钟周期和通过菊花链的数据传播如下图所示:
![图片[8]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/61672997021.png)
1. 主机模块(master)接口定义:
module spi_master(
input clk_40k, //时钟信号,40kHz
input rst_n, //复位信号,低有效
input [7:0] data_in, //主机准备要输出给从机的数据,8位宽
input send_start, //通信使能信号,高有效,宽度为1个时钟周期(40kHz),收到该信号后开始一次主从设备通信
output [7:0] data_out, //主机从从机接收到的数据,8位宽
output data_out_vld, //输出数据有效标志,高电平有效,宽度为1个时钟周期(40kHz)
output cs_n, //从设备片选使能信号,低有效,低电平时选中从设备与主设备进行通信,处于通信状态时维持低电平
output sclk, //同步时钟,1kHz,空闲时置低电平
input miso, //主机当前从从机收到的串行数据
output mosi //主机当前发送给从机的串行数据
);
module spi_slave(
input rst_n, //复位信号,低有效
input cs_n, //从设备片选使能信号
input sclk, //SPI时钟,1kHz空闲时置低电平
input mosi, //从机从主机接收到的串行数据
output miso, //从机要发送给主机的串行数据
output [7:0] reg0_out, //内部寄存器0的值
output [7:0] reg1_out, //内部寄存器1的值
output [7:0] reg2_out, //内部寄存器2的值
output [7:0] reg3_out //内部寄存器3的值
);
3. 电路功能描述:
4. SPI传输格式:
SPI每帧数据包含16位,最先发送的第0位为读写控制位,该位为0代表master向slave写数据,为1则代表master从slave读数据;随后发送的第1-7位为地址位,先发高位地址再发低位地址,9-16位为数据位,高位数据先发。所有数据均在sclk的上升沿产生,下降沿采样。
![图片[9]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/41672997021.png)
![图片[10]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/61672997022.png)
module spi_master(
input clk_40k,
input rst_n,
input [7:0] data_in,
input send_start,
output [7:0] data_out,
output data_out_vld,
output cs_n,
output sclk,
input miso,
output mosi
);
reg cs_n_r;
reg sclk_r;
reg mosi_r;
reg data_out_vld_r;
reg [7:0] data_out_r;
reg wr_rd;
reg [1:0] reg_num;
reg [6:0] clk_cnt;
reg [3:0] bit_cnt;
reg [7:0] rx_data;
reg [15:0] tx_data;
parameter reg0 = 2'd0;
parameter reg1 = 2'd1;
parameter reg2 = 2'd2;
parameter reg3 = 2'd3;
parameter reg0_address = 7'b0000000; //address of reg0
parameter reg1_address = 7'b0000001; //address of reg1
parameter reg2_address = 7'b0000010; //address of reg2
parameter reg3_address = 7'b0000011; //address of reg3
//cs_n
assign cs_n = cs_n_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
cs_n_r <= 1'b1;
else if(send_start)
cs_n_r <= 1'b0;
else if(data_out_vld_r)
cs_n_r <= 1'b1;
end
//clk_cnt
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
clk_cnt <= 7'b0;
else if(cs_n_r)
clk_cnt <= 7'b0;
else if(clk_cnt == 7'd39)
clk_cnt <= 7'b0;
else
clk_cnt <= clk_cnt + 1'b1;
end
//bit_cnt
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
bit_cnt <= 4'b0;
else if(clk_cnt == 7'd39)
bit_cnt <= bit_cnt + 1'b1;
end
//sclk
assign sclk = sclk_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
sclk_r <= 1'b0;
else if(clk_cnt == 7'd19 || clk_cnt == 7'd39)
sclk_r <= ~sclk_r;
end
//wr_rd
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
wr_rd <= 1'b0;
else if(cs_n_r)
wr_rd <= 1'b0;
else if(reg_num == reg3 && bit_cnt == 4'd15 && clk_cnt == 7'd39)
wr_rd <= 1'b1;
end
//send
//reg_num
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
reg_num <= reg0;
else if(cs_n_r)
reg_num <= reg0;
else if(bit_cnt == 4'd15 && clk_cnt == 7'd39)
case(reg_num)
reg0:reg_num <= reg1;
reg1:reg_num <= reg2;
reg2:reg_num <= reg3;
reg3:reg_num <= reg0;
endcase
end
//tx_data
always @ (posedge sclk_r or negedge rst_n)
begin
if(~rst_n)
tx_data <= 16'b0;
else if(wr_rd && bit_cnt == 4'd0)
tx_data <= {1'b1,reg2_address,8'b0};
else if(~wr_rd && bit_cnt == 4'd0)
case(reg_num)
reg0:tx_data <= {1'b0,reg0_address,data_in};
reg1:tx_data <= {1'b0,reg1_address,data_in[1:0],data_in[7:2]};
reg2:tx_data <= {1'b0,reg2_address,data_in[3:0],data_in[7:4]};
reg3:tx_data <= {1'b0,reg3_address,data_in[5:0],data_in[7:6]};
endcase
else
tx_data <= {tx_data[14:0],tx_data[15]};
end
//mosi
assign mosi = tx_data[15];
//recieve
//rx_data
always @ (posedge sclk_r or negedge rst_n)
begin
if(~rst_n)
rx_data <= 8'b0;
else if(wr_rd)
rx_data <= {rx_data[6:0],miso};
end
//data_out_vld
assign data_out_vld = data_out_vld_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
data_out_vld_r <= 1'b0;
else if(wr_rd && bit_cnt == 4'd15 && clk_cnt == 7'd39)
data_out_vld_r <= 1'b1;
else
data_out_vld_r <= 1'b0;
end
//data_out
assign data_out = data_out_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
data_out_r <= 8'b0;
else if(wr_rd && bit_cnt == 4'd15 && clk_cnt == 7'd39)
data_out_r <= rx_data;
end
endmodule
module spi_slave(
input rst_n,
input cs_n,
input sclk,
input mosi,
output miso,
output [7:0] reg0_out,
output [7:0] reg1_out,
output [7:0] reg2_out,
output [7:0] reg3_out
);
reg miso_r;
reg [7:0] reg1_out_r;
reg [7:0] reg2_out_r;
reg [7:0] reg3_out_r;
reg [7:0] reg0_out_r;
reg start;
reg wr_rd;
reg [3:0] bit_cnt;
reg [6:0] reg_addr;
reg [7:0] reg_data;
parameter reg0_address = 7'b0000000; //address of reg0
parameter reg1_address = 7'b0000001; //address of reg1
parameter reg2_address = 7'b0000010; //address of reg2
parameter reg3_address = 7'b0000011; //address of reg3
//start
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
start <= 1'b0;
else
start <= 1'b1;
end
//bit_cnt
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
bit_cnt <= 4'b0;
else if(start)
bit_cnt <= bit_cnt + 1'b1;
end
//wr_rd
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
wr_rd <= 1'b0;
else if(bit_cnt == 4'b0 && mosi == 1'b0)
wr_rd <= 1'b0;
else if(bit_cnt == 4'b0 && mosi == 1'b1)
wr_rd <= 1'b1;
end
//reg_addr
always @ (negedge sclk or negedge rst_n)
begin
if(~rst_n)
reg_addr <= 7'b0;
else if(bit_cnt >= 4'd1 && bit_cnt <= 4'd7)
reg_addr <= {reg_addr[5:0],mosi};
end
//reg_data
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
reg_data <= 8'b0;
else if(!wr_rd && bit_cnt >= 4'd7)
reg_data <= {reg_data[6:0],mosi};
else if(wr_rd && bit_cnt == 4'd6)
reg_data <= reg2_out_r;
else if(wr_rd && bit_cnt >= 4'd7)
reg_data <= {reg_data[6:0],reg_data[7]};
end
//reg_out
assign reg0_out = reg0_out_r;
assign reg1_out = reg1_out_r;
assign reg2_out = reg2_out_r;
assign reg3_out = reg3_out_r;
always @ (negedge sclk or negedge rst_n)
begin
if(~rst_n)
begin
reg0_out_r <= 8'b0;
reg1_out_r <= 8'b0;
reg2_out_r <= 8'b0;
reg3_out_r <= 8'b0;
end
else if(!wr_rd && bit_cnt == 4'd0)
case(reg_addr)
reg0_address: reg0_out_r <= reg_data;
reg1_address: reg1_out_r <= reg_data;
reg2_address: reg2_out_r <= reg_data;
reg3_address: reg3_out_r <= reg_data;
endcase
end
//miso
assign miso = miso_r;
always @ (negedge sclk or posedge rst_n)
begin
if(~rst_n)
miso_r <= 1'b0;
else if(wr_rd && bit_cnt >= 4'd7)
miso_r <= reg_data[7];
end
endmodule
3. Testbench(tb):
`timescale 1us/1us
module tb();
regclk_40k;
regrst_n;
reg [7:0] data_in;
regsend_start;
wiresclk;
wirecs_n;
wiremosi;
wiremiso;
wire [7:0] data_out;
wire data_out_vld;
wire [7:0] reg0_out;
wire [7:0] reg1_out;
wire [7:0] reg2_out;
wire [7:0] reg3_out;
spi_master i_spi_master(
.clk_40k (clk_40k),
.rst_n (rst_n),
.data_in (data_in),
.send_start (send_start),
.sclk (sclk),
.cs_n (cs_n),
.mosi (mosi),
.miso (miso),
.data_out (data_out),
.data_out_vld (data_out_vld)
);
spi_slave i_spi_slave(
.rst_n (rst_n),
.cs_n (cs_n),
.sclk (sclk),
.mosi (mosi),
.miso (miso),
.reg0_out (reg0_out),
.reg1_out (reg1_out),
.reg2_out (reg2_out),
.reg3_out (reg3_out)
);
initial
begin
rst_n = 1'b0;
#10rst_n = 1'b1;
end
initial
begin
clk_40k = 1'b0;
forever
#1clk_40k = ~clk_40k;
end
initial
begin
send_start = 1'b0;
data_in = 8'd0;
forever
begin
#200;
data_in = $random()%256;
send_start = 1'b1;
#2
send_start = 1'b0;
#8000;
end
end
endmodule
![图片[11]-FPGA SPI协议(Verilog实现)-Pangomicro紫光同创社区-FPGA CPLD-ChipDebug](http://chipdebug.com/wp-content/uploads/2023/01/61672997023.png)
2. 复位信号rst_n低电平有效,正常传输时始终处于高电平;
3. 开始传输时send_start信号拉高,传输结束时data_out_vld信号拉高;
4. SPI主设备将输入数据data_in并行转mosi串行输出,SPI从设备将接收到的串行存入数据,将移位后的数据data_out并行转miso串行输出。
2. 允许数据逐位传递;
3. 允许数据传输暂停;
4. 硬件结构简单,不需要精密时钟;
5. 从机不需要唯一地址,也不需要收发器。
2. 支持传输距离较短;
3. 硬件层面没有定义校错协议和从机应答信号。
没有回复内容