原理分析
对于 spi 通讯协议,相比较于 IIC ,可以说 spi 快很多,而且 spi 是可以全双工的,具有单独的发送线路和接收线路,我们从它的时序来进行分析吧。
从图中我们可以看到有一个是使能信号 SS ,空闲是高电平,当使能时为低电平,只有拉低时数据才能进行发送和接收。
还有 CLK 时钟信号, spi 就是通过时钟信号的跳变来进行读取数据,时钟信号有两种模式,在 0 模式下,时钟信号空闲为低电平,在 1 模式下,时钟信号空闲为高电平。
因为 spi 是全双工的,所以有两个数据线,分别是 MOSI 和 MISO ,I 和 O 分别代表 input 和 output ,这样就很好记哪个是数据发送,呢个是数据接收了。数据线也有两个模式,在 0 模式下在第一个时钟跳变沿读取数据,第二个时钟跳变沿改变数据,而在 1 模式下是第一个时钟条边沿改变数据,第二个时钟跳变沿读取数据。
通过时钟两种模式和数据发送接收的两种模式就可以组成四种模式,又因为 FPGA 可以作为主机或者从机,所以一共有八种模式,接下来我会详细讲解这四种模式对于 FPGA 的实现。
主机模式
0 0模式
发送
module spi_tx( //定义spi发送模块。
input Clk, //定义系统时钟信号。
input Reset_n, //定义复位信号。
input [7:0]spi_Data, //定义需要发送数据,由上层模块输入数据。
input spi_En, //定义发送使能信号。
output reg spi_Clk, //定义spi时钟信号。
output reg spi_TXD, //定义spi数据发送信号。
output reg spi_done //定义发送完成使能信号。
);
reg [7:0]cnt_Clk; //定义一个八位计数器,用来对系统时钟分频来控制spi时钟频率。
parameter MAX_CLK = 8'd1; //定义分频大小。
wire clk_done; //定义spi时钟半个周期使能信号。
reg [4:0]cnt_state; //定义一个5位寄存器来记录发送数据状态。
parameter MAX_STATE = 5'd16; //定义最大状态。
always @(posedge Clk or negedge Reset_n) begin //定义系统时钟分频计数器。
if (!Reset_n) //复位清零。
cnt_Clk <= 8'd0;
else if (!spi_En) begin //使能有效时,spi时钟才工作,取反是因为低电平有效。
if (MAX_CLK <= cnt_Clk) //计满清零。
cnt_Clk <= 8'd0;
else //否则加一。
cnt_Clk <= cnt_Clk + 1'b1;
end
else //使能无效清零。
cnt_Clk <= 8'd0;
end
assign clk_done = (cnt_Clk == MAX_CLK); //每计满一次使能一次。
always @(posedge Clk or negedge Reset_n) begin //定义状态变换信号。
if (!Reset_n) //复位清零。
cnt_state <= 5'd0;
else if (!spi_En) begin //使能有效状态才可以增加。
if (MAX_STATE <= cnt_state) //计满清零。
cnt_state <= 5'd0;
else if (clk_done) //系统时钟分频计数器计满加一。
cnt_state <= cnt_state + 1'b1;
else //否则不变。。
cnt_state <= cnt_state;
end
else //使能无效清零。
cnt_state <= 5'd0;
end
always @(posedge Clk or negedge Reset_n) begin //定义数据发送模块。
if (!Reset_n) begin //复位清零,数据位在空闲时为高阻态。
spi_Clk <= 1'b0;
spi_TXD <= 1'bz;
end
else if (!spi_En) begin //使能有效。
case (cnt_state) //更据状态来赋值。
0: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[0]; end //初始状态令时钟低电平,将数据最低位0位发送到数据信号上。
1: spi_Clk <= 1'b1; //时钟上升沿读取数据,下同。
2: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[1]; end //时钟下降沿将改变数据,下同。
3: spi_Clk <= 1'b1;
4: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[2]; end
5: spi_Clk <= 1'b1;
6: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[3]; end
7: spi_Clk <= 1'b1;
8: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[4]; end
9: spi_Clk <= 1'b1;
10: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[5]; end
11: spi_Clk <= 1'b1;
12: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[6]; end
13: spi_Clk <= 1'b1;
14: begin spi_Clk <= 1'b0; spi_TXD <= spi_Data[7]; end
15: spi_Clk <= 1'b1;
default: begin spi_Clk <= 1'b0; spi_TXD <= 1'bz; end //default用来避免产生锁存器占用资源。
endcase
end
else begin //使能无效清零,数据恢复高阻态。
spi_Clk <= 1'b0;
spi_TXD <= 1'bz;
end
end
always @(posedge Clk) begin //发送完成使能信号模块。
if (cnt_state == MAX_STATE && clk_done) //当状态为最后状态且时钟计满时数据发送完成,拉高发送完成信号。
spi_done <= 1'b1;
else //否则清零。
spi_done <= 1'b0;
end
endmodule
接收
module spi_rx( //定义spi接收模块。
input Clk, //定义系统时钟信号。
input Reset_n, //定义复位信号。
input spi_En, //定义接收使能信号。
input spi_RXD, //定义spi数据接收信号。
output reg spi_Clk, //定义spi时钟信号。
output reg [7:0]spi_Data, //定义接收数据,输出到上层模块。
output reg spi_done //定义接收完成使能信号。
);
reg [7:0]cnt_Clk; //定义一个八位计数器,用来对系统时钟分频来控制spi时钟频率。
parameter MAX_CLK = 8'd1; //定义分频大小。
wire clk_done; //定义spi时钟半个周期使能信号。
reg [4:0]cnt_state; //定义一个5位寄存器来记录发送数据状态。
parameter MAX_STATE = 5'd16; //定义最大状态。
reg [7:0]wait_Data; //定义数据寄存器用来暂存接收数据。
always @(posedge Clk or negedge Reset_n) begin //定义系统时钟分频计数器。
if (!Reset_n) //复位清零。
cnt_Clk <= 8'd0;
else if (!spi_En) begin //使能有效时,spi时钟才工作,取反是因为低电平有效。
if (MAX_CLK <= cnt_Clk) //计满清零。
cnt_Clk <= 8'd0;
else //否则加一。
cnt_Clk <= cnt_Clk + 1'b1;
end
else //使能无效清零。
cnt_Clk <= 8'd0;
end
assign clk_done = (cnt_Clk == MAX_CLK); //每计满一次使能一次。
always @(posedge Clk or negedge Reset_n) begin //定义状态变换信号。
if (!Reset_n) //复位清零。
cnt_state <= 5'd0;
else if (!spi_En) begin //使能有效状态才可以增加。
if (MAX_STATE <= cnt_state) //计满清零。
cnt_state <= 6'd0;
else if (clk_done) //系统时钟分频计数器计满加一。
cnt_state <= cnt_state + 1'b1;
else //否则不变。。
cnt_state <= cnt_state;
end
else //使能无效清零。
cnt_state <= 5'd0;
end
always @(posedge Clk or negedge Reset_n) begin //定义数据接收模块。
if (!Reset_n) begin //复位清零。
spi_Clk <= 1'b0;
wait_Data <= 8'd0;
spi_Data <= 8'd0;
end
else if (!spi_En) begin //使能有效。
case (cnt_state) //更据状态来接收。
0: spi_Clk <= 1'b0; //初始状态令时钟低电平,等待数据变化。
1: begin spi_Clk <= 1'b1; wait_Data[0] <= spi_RXD; end //时钟上升沿读取数据,下同。
2: spi_Clk <= 1'b0; //时钟下降沿等待数据改变,下同。
3: begin spi_Clk <= 1'b1; wait_Data[1] <= spi_RXD; end
4: spi_Clk <= 1'b0;
5: begin spi_Clk <= 1'b1; wait_Data[2] <= spi_RXD; end
6: spi_Clk <= 1'b0;
7: begin spi_Clk <= 1'b1; wait_Data[3] <= spi_RXD; end
8: spi_Clk <= 1'b0;
9: begin spi_Clk <= 1'b1; wait_Data[4] <= spi_RXD; end
10: spi_Clk <= 1'b0;
11: begin spi_Clk <= 1'b1; wait_Data[5] <= spi_RXD; end
12: spi_Clk <= 1'b0;
13: begin spi_Clk <= 1'b1; wait_Data[6] <= spi_RXD; end
14: spi_Clk <= 1'b0;
15: begin spi_Clk <= 1'b1; wait_Data[7] <= spi_RXD; end
16: begin spi_Clk <= 1'b0; spi_Data <= wait_Data; end
default: spi_Clk<= 1'b0; //default用来避免产生锁存器占用资源。
endcase
end
else begin //使能无效清零。
spi_Clk <= 1'b0;
wait_Data <= 8'd0;
end
end
always @(posedge Clk) begin //接收完成使能信号模块。
if (cnt_state == MAX_STATE && clk_done) //当状态为最后状态且时钟计满时数据发送完成,拉高接收完成信号。
spi_done <= 1'b1;
else //否则清零。
spi_done <= 1'b0;
end
endmodule
从机模式
0 0模式
00模式下始终空闲为低电平,在第一个时钟沿进行数据读取。
发送
module spi_tx( //定义spi发送模。
input Clk, //定义系统时钟。
input Reset_n, //定义复位信号。
input spi_Clk, //定义spi时钟信号。
input spi_En, //定义spi使能信号。
input [7:0]spi_Data, //定义需要发送的数据,由外部输入进来,可以由模块内部产生,定义一个reg类型的8位数据就行。
output reg spi_TXD, //定义spi的发送口。
output reg spi_done //定义spi数据发送完成信号。
);
wire podge_En; //定义使能信号上升沿检测信号。
wire nedge_En; //定义使能信号下降沿检查信号。
reg [1:0]state_En; //定义一个两位的寄存器来储存使能信号的上一时刻和这一时刻的信号。
wire podge_Clk; //定义时钟信号上升沿检查信号。
wire nedge_Clk; //定义时钟信号下降沿检查信号。
reg [1:0]state_Clk; //定义一个两位的寄存器来储存时钟信号的上一时刻和这一时刻的信号。
reg [3:0]cnt_state; //定义一个四位寄存器来记录数据发送状态。
always @(posedge Clk or negedge Reset_n) begin //定义使能信号检测魔抗。
if (!Reset_n) //复位清零
state_En <= 0;
else //将此时刻和上一时刻的使能信号状态记录。
state_En <= {state_En[0], spi_En};
end
assign podge_En = (state_En == 2'b01); //将上升沿储存到podge_En里,如果有上升沿,值为1,否则为0.
assign nedge_En = (state_En == 2'b10); //储存下降沿。
always @(posedge Clk or negedge Reset_n) begin //定义时钟信号检测模块。
if (!Reset_n) //复位清零。
state_Clk <= 0;
else //将时钟信号此时刻和上一时刻的2状态记录。
state_Clk <= {state_Clk[0], spi_Clk};
end
assign podge_Clk = (state_Clk == 2'b01); //储存上升沿。
assign nedge_Clk = (state_Clk == 2'b10); //储存下降沿。
always @(posedge Clk or negedge Reset_n) begin //定义发送状态模块。
if (!Reset_n) //复位状态清零。
cnt_state <= 4'd0;
else if (!spi_En) begin //因为使能信号是低电平有效,所以将使能信号取反。
if (cnt_state == 4'd7 && podge_Clk) //在使能信号有效下,因为一次发送8个数据,所以有8个状态,当状态为7时,清零。
cnt_state <= 4'd0;
else if (podge_Clk) //当检测到时钟下降沿时,数据状态自增1。
cnt_state <= cnt_state + 1'd1;
else //其他情况保持不变。
cnt_state <= cnt_state;
end
else //在使能信号无效后,状态清零。
cnt_state <= 4'd0;
end
always @(posedge Clk or negedge Reset_n) begin //定义发送模块。
if (!Reset_n) //复位时,数据线状态为高阻态。
spi_TXD <= 1'bz;
else if (nedge_En) //因为第一个时钟跳变就要读取数据,所以在使能信号开始,也就是使能信号下降沿时就要将第一个数据的状态输入进去,否则读出高阻态。
spi_TXD <= spi_Data[0];
else if (!spi_En) begin //在使能信号有效情况下可以改变输出数据状态。
if (nedge_Clk) begin //当检测到时钟下降沿改变数据。
case (cnt_state) //通过状态值来改变数据。
4'd0:spi_TXD <= spi_Data[0];
4'd1:spi_TXD <= spi_Data[1];
4'd2:spi_TXD <= spi_Data[2];
4'd3:spi_TXD <= spi_Data[3];
4'd4:spi_TXD <= spi_Data[4];
4'd5:spi_TXD <= spi_Data[5];
4'd6:spi_TXD <= spi_Data[6];
4'd7:spi_TXD <= spi_Data[7];
default:spi_TXD <= 1‘bz; //其他任意错误状态输出高阻态。
endcase
end
else if (nedge_En) //保险作用。
spi_TXD <= spi_Data[0];
else //其他情况数据保持不变。
spi_TXD <= spi_TXD;
end
else //使能失效,数据输出高阻态。
spi_TXD <= 1'bz;
end
always @(posedge Clk or negedge Reset_n) begin //发送完成信号模块。
if (!Reset_n) //复位信号为低电平。
spi_done <= 1'd0;
else if (cnt_state == 4'd7 && podge_Clk) //当检测到数据状态为7并且有时钟上升沿时,读取数据完成,将信号拉高。
spi_done <= 1'd1;
else //其他情况拉低。
spi_done <= 1'd0;
end
endmodule
接收
module spi_rx( //定义接收模块。
input Clk, //定义系统时钟。
input Reset_n, //定义复位。
input spi_Clk, //定义spi时钟信号。
input spi_En, //定义spi接收使能信号。
input spi_RXD, //定义spi数据线。
output reg [7:0]spi_Data, //定义接收数据输出口。
output reg spi_done //定义接收完成信号。
);
wire podge_En; //定义寄存器储存寄存器,用于储存使能信号上升沿。
wire nedge_En; //储存使能信号下降沿。
reg [1:0]state_En; //定义一个二位寄存器来储存上一时刻和下一时刻使能信号状态。
wire podge_Clk; //储存时钟信号上升沿。
wire nedge_Clk; //储存时钟信号下降沿。
reg [1:0]state_Clk; //定义一个二位寄存器来储存上一时刻和下一时刻时钟信号状态。
reg [3:0]cnt_state; //定义一个四位寄存器来记录数据接收状态。
reg [7:0]wait_Data; //用来暂存数据。
always @(posedge Clk or negedge Reset_n) begin //使能信号检测模块。
if (!Reset_n) //复位清零。
state_En <= 0;
else //将使能信号上一时刻和下一时刻的状态记录。
state_En <= {state_En[0], spi_En};
end
assign podge_En = (state_En == 2'b01); //将使能上升沿储存,若是上升沿则为1,否则为0.
assign nedge_En = (state_En == 2'b10); //将使能下降沿储存。
always @(posedge Clk or negedge Reset_n) begin //时钟信号检测模块。
if (!Reset_n) //复位清零。
state_Clk <= 0;
else //将时钟信号上一刻和下一刻的状态记录。
state_Clk <= {state_Clk[0], spi_Clk};
end
assign podge_Clk = (state_Clk == 2'b01); //将时钟上升沿储存。
assign nedge_Clk = (state_Clk == 2'b10); //将时钟下降沿储存。
always @(posedge Clk or negedge Reset_n) begin //数据接收状态模块。
if (!Reset_n) //复位状态清零。
cnt_state <= 0;
else if(!spi_En) begin //使能信号低电平有效,所以取反,当使能有效,进入这个条件。
if (cnt_state == 4'd7) //因为有8个数据,所以当状态为7时,清零。
cnt_state <= 0;
else if (nedge_Clk) //当时钟下降沿时,接收数据状态改变。
cnt_state <= cnt_state + 1'b1;
else //其他情况状态保持。
cnt_state <= cnt_state;
end
else //使能无效时状态清零。
cnt_state <= 0;
end
always @(posedge Clk or negedge Reset_n) begin //定义数据接收模块。
if (!Reset_n) begin //复位将暂存数据清空,把数据输出清空。
spi_Data <= 8'd0;
wait_Data <= 8'd0;
end
else if (!spi_En) begin //使能信号有效。
if (podge_Clk) begin //在时钟信号上升沿时进行数据读取。
case (cnt_state) //更据状态读取数据。
0:wait_Data[0] <= spi_RXD;
1:wait_Data[1] <= spi_RXD;
2:wait_Data[2] <= spi_RXD;
3:wait_Data[3] <= spi_RXD;
4:wait_Data[4] <= spi_RXD;
5:wait_Data[5] <= spi_RXD;
6:wait_Data[6] <= spi_RXD;
7:wait_Data[7] <= spi_RXD;
default:wait_Data <= 8'd0; //其他错误情况将数据清空。
endcase
end
end
else //将数据从数据暂存寄存器中读取出来。
spi_Data <= wait_Data;
end
always @(posedge Clk or negedge Reset_n) begin //定义接收完成信号模块。
if (!Reset_n) //复位清零。
spi_done <= 1'b0;
else if (podge_En) //当使能信号结束,使能信号上升沿时拉高。
spi_done <= 1'b1;
else //其他情况情况。
spi_done <= 1'b0;
end
endmodule