关于SPI协议的基础知识这里就不在叙述了,感兴趣的小伙伴可以自行百度。本文基于verilog语言,实现SPI的四种通信模式,封装成通用模块。
更新时间:2023年7月11日
更新原因:之前的版本存在bug,通用性不够。
spi_comm #(
.CPOL (0), //时钟极性
.CPHA (0), //时钟相位
.SPI_4P (0), //1:四线制 0:三线制
.CMD_BYTE (3), //指令长度,单位字节
.DATA_BYTE (1) //数据长度,单位字节
)your_instance_name(
.clk (clk ), // input wire [0 :0] |模块时钟
.reset (reset ), // input wire [0 :0] |模块复位
.stert (stert ), // input wire [0 :0] |开始信号
.cmd (cmd ), // input wire [CMD_BYTE*8-1 :0] |发送命令
.valid (valid ), // output wire [0 :0] |读回数据有效
.data (data ), // output wire [DATA_BYTE*8-1:0] |读回数据
.done (done ), // output wire [0 :0] |结束标志
.scs_n (scs_n ), // output wire [0 :0] |SPI|片选
.sclk (sclk ), // output wire [0 :0] |SPI|时钟
.sdio (sdio ), // output wire [0 :0] |SPI|三线制|数据线
.mosi (mosi ), // output wire [0 :0] |SPI|四线制|主机发送
.miso (miso ) // input wire [0 :0] |SPI|四线制|主机接收
);
CPOL:配置模块时钟极性
CPHA:配置模块时钟相位
SPI_4P:配置模块的通信接口的引脚数量
CMD_BYTE:配置通信中指令的长度
DATA_BYTE:配置通信中数据的长度
关于CPOL和CPHA的相关说明在这里不做详细的描述,本模块在设计时采用了CPHA模糊化设计,即使CPHA配置错误也能与从机正常通信,但是CPOL必须配置正确。
特点:本模块是一个通用型SPI主机模块,实现SPI的四种通信模式可配置,指令长度可配置,数据长度可配置。
stert :开始通信端口 |上升沿触发
cmd :本次通信的CMD指令 |该数据会在stert的上升沿被模块锁存直至本次通信结束
valid :返回数据有效标志 |仅在读CMD指令时有效
data :返回的数据 |仅在读CMD指令时有效
done :本次通信结束标志
scs_n :片选信号 |低电平有效
sclk :通信时钟 |和本模块时钟之间的比例为1:8
sdio :数据线 |当SPI_4P=0时有效,当SPI_4P=1时端口为高阻态
mosi :主机发送端口 |当SPI_4P=1时有效,当SPI_4P=0时端口为高阻态
miso :主机接收端口 |当SPI_4P=1时有效
指令长度是由读写位、地址位和数据位构成的。
CMD={R/W,ADDR,DATA}
CMD命令的最高位为读写位,CMD[CMD_BYTE*8-2 :DATA_BYTE*8]为地址位,CMD[DATA_BYTE*8-1:0]为数据位。
当CMD命令的最高位,即CMD[CMD_BYTE*8-1] = 1 时该指令为读指令,
此时CMD命令的数据位,即CMD[DATA_BYTE*8-1:0]无效,并且该CMD指令会通过valid和data端口返回一个数据。
//
// Company:
// Engineer: Zheng LiGuo
// Create Date: 2023/6/28
// Design Name: common_module
// Module Name: spi_comm
// Project Name:
// Target Devices: spi_slave
// Tool Versions: vivado 2018.3
// Description: 通用SPI主机模块
// 支持配置项:
// 1.时钟极性
// 2.时钟相位
// 3.几线制
// 4.指令长度 |单位:字节
// 5.数据长度 |单位:字节
// Dependencies: CMD下发模块
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
/*===============================================
//----------- Begin Cut here for spi_comm Template ---//
//===============================================
// 通用SPI主机模块
//===============================================
spi_comm #(
.CPOL (0), //时钟极性
.CPHA (0), //时钟相位
.SPI_4P (0), //1:四线制 0:三线制
.CMD_BYTE (3), //指令长度,单位字节
.DATA_BYTE (1) //数据长度,单位字节
)your_instance_name(
.clk (clk ), // input wire [0 :0] |模块时钟
.reset (reset ), // input wire [0 :0] |模块复位
.stert (stert ), // input wire [0 :0] |开始信号
.cmd (cmd ), // input wire [CMD_BYTE*8-1 :0] |发送命令
.valid (valid ), // output wire [0 :0] |读回数据有效
.data (data ), // output wire [DATA_BYTE*8-1:0] |读回数据
.done (done ), // output wire [0 :0] |结束标志
.scs_n (scs_n ), // output wire [0 :0] |SPI|片选
.sclk (sclk ), // output wire [0 :0] |SPI|时钟
.sdio (sdio ), // output wire [0 :0] |SPI|三线制|数据线
.mosi (mosi ), // output wire [0 :0] |SPI|四线制|主机发送
.miso (miso ) // input wire [0 :0] |SPI|四线制|主机接收
);
// INST_TAG_END ------ End spi_comm Template ---------
===============================================*/
`resetall
`timescale 1ns / 1ps
module spi_comm #(
parameter [0:0] CPOL = 0, //时钟极性
parameter [0:0] CPHA = 0, //时钟相位
parameter [0:0] SPI_4P = 0, //1:四线制 0:三线制
parameter [3:0] CMD_BYTE = 3, //指令长度,单位字节
parameter [3:0] DATA_BYTE = 1 //数据长度,单位字节
)(
input wire clk , // input wire [0 :0] |模块时钟
input wire reset , // input wire [0 :0] |模块复位
input wire stert , // input wire [0 :0] |开始信号
input wire [CMD_BYTE*8-1 :0] cmd , // input wire [CMD_BYTE*8-1 :0] |发送命令
output wire valid , // output wire [0 :0] |读回数据有效
output wire [DATA_BYTE*8-1:0] data , // output wire [DATA_BYTE*8-1:0] |读回数据
output wire done , // output wire [0 :0] |结束标志
output wire scs_n , // output wire [0 :0] |SPI|片选
output wire sclk , // output wire [0 :0] |SPI|时钟
inout wire sdio , // inout wire [0 :0] |SPI|三线制|数据线
output wire mosi , // output wire [0 :0] |SPI|四线制|主机发送
input wire miso // input wire [0 :0] |SPI|四线制|主机接收
);
//=================================================
// 寄存器&网线定义
//=================================================
reg resv_r; //接收引脚缓存器
reg flag_rd = 'd0; //读操作标志寄存器
reg [2 :0] stert_r3 = 'd0; //开始信号缓存器
reg [CMD_BYTE*8-1:0] cmd_r = 'd0; //CMD命令缓存器
reg [3:0] NOW_S = 'd1; //状态机当前状态
reg [3:0] NEX_S = 'd1; //状态机下一状态
reg [7:0] time_cnt = 'd0; //时钟计数器
reg [5:0] send_cnt = CMD_BYTE*8-1; //bit计数器
reg flag_cnt1 = 'd0; //bit交换完成标志寄存器
reg [DATA_BYTE*8-1:0] data_r = 'd0; //读模式下接收数据寄存器
wire stert_u; //开始信号上升沿标志
//=================================================
// 状态机状态定义
//=================================================
localparam [3:0] S0_0 = 4'b0001; //空闲状态
localparam [3:0] S0_1 = 4'b0010; //跳转至发送
localparam [3:0] S0_2 = 4'b0100; //发送命令
localparam [3:0] S0_3 = 4'b1000; //跳转回空闲
//=================================================
// 时钟计数器标志定义
//=================================================
localparam [7:0] flag_time1 = 'd2; //跳出跳转态时刻(跳转至发送)
localparam [7:0] flag_time2 = 'd8; //交换一个bit时间宽度
localparam [7:0] flag_time3 = 'd3 //CPHA=0时接收bit时刻
localparam [7:0] flag_time4 = 'd6; //CPHA=1时接收bit时刻
localparam [7:0] flag_time5 = 'd5; //SPI时钟脉宽
//===============================================
// 同步端口信号到本地时钟
//===============================================
always @(posedge clk) begin
if(SPI_4P) resv_r <= miso;
else resv_r <= sdio;
end
//===============================================
// 进程状态机
//===============================================
always @(posedge clk) begin
if(reset) NOW_S <= S0_0 ;
else NOW_S <= NEX_S;
end
always @(*) begin
case(NOW_S)
S0_0 : begin if(stert_r3[2:1] == 2'b01) NEX_S = S0_1; else NEX_S = S0_0; end
S0_1 : begin if(time_cnt == flag_time1) NEX_S = S0_2; else NEX_S = S0_1; end
S0_2 : begin if(flag_cnt1 ) NEX_S = S0_3; else NEX_S = S0_2; end
S0_3 : begin if(time_cnt == flag_time1) NEX_S = S0_0; else NEX_S = S0_3; end
default : begin NEX_S = S0_0; end
endcase
end
always @(posedge clk) begin
case(NOW_S)
S0_0 : begin
stert_r3 <= {stert_r3[1:0],stert};
time_cnt <= 'd0;
send_cnt <= CMD_BYTE*8-1;
flag_cnt1 <= 'd0;
data_r <= 'd0;
if(stert_r3[1:0] == 2'b01) cmd_r <= cmd;
else cmd_r <= cmd_r;
if(cmd_r[CMD_BYTE*8-1]) flag_rd <= 'd1;
else flag_rd <= 'd0;
end
S0_1 : begin
if(time_cnt == flag_time1) time_cnt <= 'd0;
else time_cnt <= time_cnt+1;
end
S0_2 : begin
if(time_cnt == flag_time2) time_cnt <= 'd0;
else time_cnt <= time_cnt+1;
if(time_cnt == flag_time2) send_cnt <= send_cnt-1;
else send_cnt <= send_cnt;
if(send_cnt == 'd0 && time_cnt == flag_time2-1) flag_cnt1 <= 'd1;
else flag_cnt1 <= 'd0;
if(flag_rd && send_cnt<(DATA_BYTE*8)) begin
if(CPHA) begin
if(time_cnt==flag_time4) data_r[send_cnt] <= resv_r;
else data_r <= data_r;
end else begin
if(time_cnt==flag_time3) data_r[send_cnt] <= resv_r;
else data_r <= data_r;
end
end else
data_r <= data_r;
end
S0_3 : begin
if(time_cnt == flag_time1) time_cnt <= 'd0;
else time_cnt <= time_cnt+1;
if(NOW_S == S0_3 && time_cnt == flag_time1) flag_rd <= 'd0;
else flag_rd <= flag_rd;
if(NOW_S == S0_3 && time_cnt == flag_time1) cmd_r <= 'd0;
else cmd_r <= cmd_r;
end
default : begin
flag_rd <= 'd0;
time_cnt <= 'd0;
send_cnt <= CMD_BYTE*8-1;
flag_cnt1 <= 'd0;
data_r <= 'd0;
end
endcase
end
//===============================================
// 输入输出端口
//===============================================
assign valid = flag_rd ? ((NOW_S == S0_3) ? 1'b1 : 1'b0) : 1'b0;
assign data = data_r;
assign done = (NOW_S == S0_3 && time_cnt==flag_time1) ? 'd1 : 'd0;
assign scs_n = (NOW_S != S0_0) ? 'd0 : 'd1;
assign sclk = (CPOL == 'd0 ) ? (NOW_S == S0_2 ? ((time_cnt>=flag_time3 && time_cnt<=flag_time5) ? 'd1 : 'd0) : 'd0) :
(NOW_S == S0_2 ? ((time_cnt>=flag_time3 && time_cnt<=flag_time5) ? 'd0 : 'd1) : 'd1) ;
assign sdio = (SPI_4P) ? 'dz : ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz);
assign mosi = (SPI_4P) ? ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz) : 'dz;
endmodule
//
// Company:
// Engineer: Zheng LiGuo
// Create Date: 2023/6/28
// Design Name: common_module
// Module Name: spi_comm
// Project Name:
// Target Devices: spi_slave
// Tool Versions: vivado 2018.3
// Description: 通用SPI主机模块
// 支持配置项:
// 1.时钟极性
// 2.时钟相位
// 3.几线制
// 4.指令长度 |单位:字节
// 5.数据长度 |单位:字节
// Dependencies: CMD下发模块
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
/*===============================================
//----------- Begin Cut here for spi_comm Template ---//
//===============================================
// 通用SPI主机模块
//===============================================
spi_comm #(
.CPOL (0), //时钟极性
.CPHA (0), //时钟相位
.SPI_4P (0), //1:四线制 0:三线制
.CMD_BYTE (3), //指令长度,单位字节
.DATA_BYTE (1) //数据长度,单位字节
)your_instance_name(
.clk (clk ), // input wire [0 :0] |模块时钟
.reset (reset ), // input wire [0 :0] |模块复位
.stert (stert ), // input wire [0 :0] |开始信号
.cmd (cmd ), // input wire [CMD_BYTE*8-1 :0] |发送命令
.valid (valid ), // output wire [0 :0] |读回数据有效
.data (data ), // output wire [DATA_BYTE*8-1:0] |读回数据
.done (done ), // output wire [0 :0] |结束标志
.scs_n (scs_n ), // output wire [0 :0] |SPI|片选
.sclk (sclk ), // output wire [0 :0] |SPI|时钟
.sdio (sdio ), // output wire [0 :0] |SPI|三线制|数据线
.mosi (mosi ), // output wire [0 :0] |SPI|四线制|主机发送
.miso (miso ) // input wire [0 :0] |SPI|四线制|主机接收
);
// INST_TAG_END ------ End spi_comm Template ---------
===============================================*/
`resetall
`timescale 1ns / 1ps
module spi_comm #(
parameter [0:0] CPOL = 0, //时钟极性
parameter [0:0] CPHA = 0, //时钟相位
parameter [0:0] SPI_4P = 0, //1:四线制 0:三线制
parameter [3:0] CMD_BYTE = 3, //指令长度,单位字节
parameter [3:0] DATA_BYTE = 1 //数据长度,单位字节
)(
input wire clk , // input wire [0 :0] |模块时钟
input wire reset , // input wire [0 :0] |模块复位
input wire stert , // input wire [0 :0] |开始信号
input wire [CMD_BYTE*8-1 :0] cmd , // input wire [CMD_BYTE*8-1 :0] |发送命令
output wire valid , // output wire [0 :0] |读回数据有效
output wire [DATA_BYTE*8-1:0] data , // output wire [DATA_BYTE*8-1:0] |读回数据
output wire done , // output wire [0 :0] |结束标志
output wire scs_n , // output wire [0 :0] |SPI|片选
output wire sclk , // output wire [0 :0] |SPI|时钟
inout wire sdio , // inout wire [0 :0] |SPI|三线制|数据线
output wire mosi , // output wire [0 :0] |SPI|四线制|主机发送
input wire miso // input wire [0 :0] |SPI|四线制|主机接收
);
//=================================================
// 寄存器&网线定义
//=================================================
reg resv_r; //接收引脚缓存器
reg flag_rd = 'd0; //读操作标志寄存器
reg [2 :0] stert_r3 = 'd0; //开始信号缓存器
reg [CMD_BYTE*8-1:0] cmd_r = 'd0; //CMD命令缓存器
reg [3:0] NOW_S = 'd1; //状态机当前状态
reg [3:0] NEX_S = 'd1; //状态机下一状态
reg [7:0] time_cnt = 'd0; //时钟计数器
reg [5:0] send_cnt = CMD_BYTE*8-1; //bit计数器
reg flag_cnt1 = 'd0; //bit交换完成标志寄存器
reg [DATA_BYTE*8-1:0] data_r = 'd0; //读模式下接收数据寄存器
wire stert_u; //开始信号上升沿标志
//=================================================
// 状态机状态定义
//=================================================
localparam [3:0] S0_0 = 4'b0001; //空闲状态
localparam [3:0] S0_1 = 4'b0010; //跳转至发送
localparam [3:0] S0_2 = 4'b0100; //发送命令
localparam [3:0] S0_3 = 4'b1000; //跳转回空闲
//=================================================
// 时钟计数器标志定义
//=================================================
localparam [7:0] flag_time1 = 'd2; //跳出跳转态时刻(跳转至发送)
localparam [7:0] flag_time2 = 'd8; //交换一个bit时间宽度
localparam [7:0] flag_time3 = 'd5; //跳出跳转态时刻(跳转回空闲)
localparam [7:0] flag_time4 = 'd3; //CPHA=0时接收bit时刻
localparam [7:0] flag_time5 = 'd6; //CPHA=1时接收bit时刻
//===============================================
// 同步端口信号到本地时钟
//===============================================
always @(posedge clk) begin
if(SPI_4P) resv_r <= miso;
else resv_r <= sdio;
end
//===============================================
// 获取开始信号上升沿 |缓存CMD |产生本地开始信号
//===============================================
always @(posedge clk) stert_r3 <= {stert_r3[1:0],stert};
always @(posedge clk) begin
if(stert_r3[1:0] == 2'b01)
cmd_r <= cmd;
else
cmd_r <= cmd_r;
end
assign stert_u = (stert_r3[2:1] == 2'b01) ? 1'b1 : 1'b0;
always @(posedge clk) begin
if(stert_r3[1:0] == 2'b01) begin
if(cmd[CMD_BYTE*8-1])
flag_rd <= 'd1;
else
flag_rd <= 'd0;
end else if(NOW_S == S0_3 && time_cnt == flag_time3)
flag_rd <= 'd0;
else
flag_rd <= flag_rd;
end
//===============================================
// 进程状态机
//===============================================
always @(posedge clk) begin
if(reset) NOW_S <= S0_0 ;
else NOW_S <= NEX_S;
end
always @(*) begin
case(NOW_S)
S0_0 : begin if(stert_u ) NEX_S = S0_1; else NEX_S = S0_0; end
S0_1 : begin if(time_cnt == flag_time1) NEX_S = S0_2; else NEX_S = S0_1; end
S0_2 : begin if(flag_cnt1 ) NEX_S = S0_3; else NEX_S = S0_2; end
S0_3 : begin if(time_cnt == flag_time3) NEX_S = S0_0; else NEX_S = S0_3; end
default : begin NEX_S = S0_0; end
endcase
end
//===============================================
// 时钟计数器
//===============================================
always @(posedge clk) begin
case(NOW_S)
S0_0 : begin time_cnt <= 'd0; end
S0_1 : begin if(time_cnt == flag_time1) time_cnt <= 'd0; else time_cnt <= time_cnt+1; end
S0_2 : begin if(time_cnt == flag_time2) time_cnt <= 'd0; else time_cnt <= time_cnt+1; end
S0_3 : begin if(time_cnt == flag_time3) time_cnt <= 'd0; else time_cnt <= time_cnt+1; end
default : begin time_cnt <= 'd0; end
endcase
end
//===============================================
// 比特计数
//===============================================
always @(posedge clk) begin
if(NOW_S == S0_2) begin
if(time_cnt == flag_time2)
send_cnt <= send_cnt-1;
else
send_cnt <= send_cnt;
end else
send_cnt <= CMD_BYTE*8-1;
end
//============================================
// bit交换完成标志
//============================================
always @(posedge clk) begin
if(NOW_S == S0_2) begin
if(send_cnt == 'd0 && time_cnt == flag_time2-1)
flag_cnt1 <= 'd1;
else
flag_cnt1 <= 'd0;
end else
flag_cnt1 <= 'd0;
end
//===============================================
// 读命令下接收bit
//===============================================
always @(posedge clk) begin
if(flag_rd) begin
if(send_cnt<(DATA_BYTE*8)) begin
if(CPHA) begin
if(time_cnt==flag_time5)
data_r[send_cnt] <= resv_r;
else
data_r <= data_r;
end else begin
if(time_cnt==flag_time4)
data_r[send_cnt] <= resv_r;
else
data_r <= data_r;
end
end else
data_r <= data_r;
end else
data_r <= 'd0;
end
//===============================================
// 输入输出端口
//===============================================
assign valid = flag_rd ? ((NOW_S == S0_3) ? 1'b1 : 1'b0) : 1'b0;
assign data = data_r;
assign done = (NOW_S == S0_3 && time_cnt==flag_time3) ? 'd1 : 'd0;
assign scs_n = (NOW_S != S0_0) ? 'd0 : 'd1;
assign sclk = (CPOL == 'd0 ) ? (NOW_S == S0_2 ? ((time_cnt>=flag_time4 && time_cnt<=flag_time3) ? 'd1 : 'd0) : 'd0) :
(NOW_S == S0_2 ? ((time_cnt>=flag_time4 && time_cnt<=flag_time3) ? 'd0 : 'd1) : 'd1) ;
assign sdio = (SPI_4P) ? 'dz : ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz);
assign mosi = (SPI_4P) ? ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz) : 'dz;
endmodule
由于模块的通用性,这次更新删除了仿真部分
很抱歉之前的版本存在bug。也是我最近的项目需要用到SPI通信时才发现的。希望读者能给我反馈使用和阅读时发现的错误。也欢迎各位读者和我交流verilog知识。