上一节中,我们详细讨论了SPI的协议内容并从设计组件的角度给出了SPI协议中所需要的诸多内容,以供读者参考
这一节中,我们自定义如下标准的SPI主从设备进行设计,需要注意的是,本篇文章中所涉及的SPI仅供初学者学习参考,并没有采用实际工业开发中所涉及到的代码标准和思考,也并未进行综合与后仿。
我们想要在本篇博客中用SPI的方式操控从设备:一个位宽为8bit,深度为16位的寄存器组,寄存器组在数字IC设计中颇为常见,也颇为基础,非常适合在本篇博客中用以展示SPI串行总线的奥妙,CPU的内部就需要寄存器组作为最低level的存储层级,因为SPI协议的数据通信单方向只有一条线,因此我们需要使用这一根线来控制寄存器组的读写时序,本寄存器组的读写逻辑由作者自行定义。
需要两拍才能执行完毕,第一拍从data_in发送地址和模式,其中地址为前四位,模式为后四位,若需要往寄存器组中写数据,第一拍的后四位需要为“1111”,写模式第二拍从data_in发送需要写入的发送数据
(需要注意的是,受限于SPI的串型接口,一拍的相当于SCK的八倍频时钟信号,即需要八个时钟周期接收到一个字节,为一拍)
需要两拍才能执行完毕,第一拍从data_in发送地址和模式,其中读地址为前四位,模式为后四位,若需要从寄存器组中读数据,第一拍的后四位需要为“0000”,读模式第二拍从data_out发送需要读出的发送数据
(需要注意的是,受限于SPI的串型接口,一拍的相当于是SCK的八倍频时钟信号,即需要八个时钟周期接收到一个字节,为一拍)
单独的reg_array没有办法按照SPI协议直接相连,因为SPI是一款串行总线,而我们上面提到的reg_array使用的是并行接口,因此我们需要一个SPI的控制器来与reg_array连接,根据协议的内容,这个控制器的功能为
在【数字IC】深入浅出理解SPI协议,我们探讨了为什么要对SPI时钟信号进行分频,并确定了2,4,8,16分频的四个参数,按照偶数分频的思想,我们可以采用寄存器级联法或计数器法的方法进行分频,二者均可满足目的,作者选择了更为简单的寄存器级联法来进行分频操作
在主设备端,我们同样需要一个控制器来发送数据,这个控制器首先需要对MCU/CPU来的数据进行串并转换,其次,它也需要告诉SPI的四条线,什么时候nss拉低,什么时候发送数据,因此我们肯定需要按照状态对其进行划分,区分状态机的跳转。
IDLE:是待机状态,当其接收到css的拉低信号时,跳转到S0
S0:S0状态代表该从设备已经被选中了
S1和S2:是读取操作的状态机
S3和S4:是写入操作的状态机
本状态机跳转图由Questasim自动生成,相关操作更新于
如何自动生成设计文件的状态机跳转图(仿真工具使用技巧)
module reg_array(
input clk,
input rst_n,
input nss,
input enable, // to ensure that it can sample data_in
input [7:0] data_in,
output reg [7:0] data_out,
output done//读取操作的完成信号);
localparam IDLE = 3'b000;
localparam S0 = 3'b001;
localparam S1 = 3'b010;
localparam S2 = 3'b011;
localparam S3 = 3'b100;
localparam S4 = 3'b101;
reg [2:0] state;
reg [2:0] nstate;
reg [7:0] array [0 : 15];
reg [3:0] address;
//状态机第一段
always@(posedge clk or negedge rst_n)
if(!rst_n)
state <= IDLE ;
else
state <= nstate;
//状态机第二段
always@(*)
begin
case(state)
IDLE : nstate = !nss ? S0 :IDLE;
S0 : nstate = (enable & data_in[3:0] == 4'b0000) ? S1 : (enable & data_in[3:0] == 4'b1111) ? S3 : S0;
S1 : nstate = (enable) ? S2 : S1;
S2 : nstate = !nss ? S0 : IDLE;
S3 : nstate = (enable) ? S4 : S3;
S4 : nstate = !nss ? S0 : IDLE;
default: nstate = IDLE;
endcase
end
//数据读取操作
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
array[0] <= 16'h0000;
array[1] <= 16'h0000;
array[2] <= 16'h0000;
array[3] <= 16'h0000;
array[4] <= 16'h0000;
array[5] <= 16'h0000;
array[6] <= 16'h0000;
array[7] <= 16'h0000;
array[8] <= 16'h0000;
array[9] <= 16'h0000;
array[10] <= 16'h0000;
array[11] <= 16'h0000;
array[12] <= 16'h0000;
array[13] <= 16'h0000;
array[14] <= 16'h0000;
array[15] <= 16'h0000;
end
else if (nstate == S2)
data_out <= array[address];
else if (nstate == S4)
array[address] <= data_in;
else
data_out <= data_out;
//地址操作
always@(posedge clk or negedge rst_n)
if(!rst_n)
address <= 4'h0;
else if(state == S0 && enable)
address <= data_in[7:4];
else
address <= address;
assign done = (state==S2 && nstate == S0) ? 1 : 0 ;
endmodule
`timescale 1ns / 1ps
module reg_array_tb ();
reg clk;
reg rst_n;
reg nss;
reg enable;
reg [7:0] data_in;
wire [7:0] data_out;
wire done;
reg_array u1 (clk,rst_n,nss,enable,data_in,data_out,done);
task reset;
begin
clk = 0;
rst_n = 1;
nss = 1;
enable = 0;
@(negedge clk);
rst_n = 0;
@(negedge clk);
rst_n = 1;
@(negedge clk);
@(negedge clk);
end
endtask
task write;
input [3:0] w_address;
input [7:0] w_data;
begin
@(negedge clk);
nss = 0;
enable = 0;
@(negedge clk);
enable = 1;
data_in = {w_address,4'b1111};
@(negedge clk);
data_in = w_data;
@(negedge clk);
end
endtask
task read;
input [3:0] r_address;
begin
@(negedge clk);
nss = 0;
enable = 0;
@(negedge clk);
enable = 1;
data_in = {r_address,4'b0000};
@(negedge clk);
enable = 0;
// data_in = r_data;
repeat(8) @(negedge clk);
enable = 1;
end
endtask
always #5 clk = !clk;
initial
begin
reset;
write(4'h3,8'h1a);
write(4'ha,8'ha4);
read(4'ha);
read(4'h3);
#2000 $stop;
end
endmodule
需要关注的是,SPI_slave_controller设计虽然有诸多接口,但是实际上与主设备相连的只有四个,即,sck,nss,mosi,miso,其它的的接口都是体现在从设备整体内部的互联,体现了SPI通信的四根线的原则。
module SPI_slave_controller
#(parameter CPOL = 1'b0,
parameter CPHA = 1'b0)
(input mosi,
input nss,
input sck,
input rst_n,
input done,// done信号来控制什么时候该发送读出的数据
input [7:0] data_in, //来自reg_array的data_in(读出的数据)
output reg enable,
output[7:0]data_out, //送往reg_array的data_out(写入寄存器组的数据)
output reg miso);
reg [0:7] receive_reg;
reg [0:7] send_reg;
reg [3:0] r_cnt;
reg [3:0] s_cnt;
// 数据采样
always@(posedge sck or negedge rst_n )
if(!rst_n)
receive_reg <= 8'h00;
else if(nss)
receive_reg <= receive_reg;
else
receive_reg[r_cnt]<= mosi;
//采样计数器,接收满一个字节重置
always@(posedge sck or negedge rst_n)
if(!rst_n)
r_cnt <= 4'h0;
else if(nss || r_cnt == 4'h7)
r_cnt <= 4'h0;
else
r_cnt <= r_cnt + 1'b1;
//reg_array的控制信号生成
always@(posedge sck or negedge rst_n)
if(!rst_n)
enable <= 1'b0;
else if(!nss && r_cnt == 4'h7)
enable <= 1'b1;
else
enable <= 1'b0;
assign data_out = enable ? receive_reg : 8'h00;
// 数据的发送
always@(negedge sck or negedge rst_n)
if(!rst_n)
send_reg <= 8'h00;
else if(done)
send_reg <= data_in;
else if(!nss && s_cnt < 4'h8)
miso <= send_reg[s_cnt];
else
miso <= 1'bx;
//数据发送计数器
always@(negedge sck or negedge rst_n)
if(!rst_n)
s_cnt <= 4'h0;
else if(nss || s_cnt == 4'h7)
s_cnt <= 4'h0;
else
s_cnt <= s_cnt + 1'b1;
endmodule
这里按照主设备的发送时序,写了名为r_data的task,以此用来仿真,另外的的一个task是时钟复位task,此处的仿真文件,直接将控制器和寄存器组作为整体来进行仿真,没有单独区分控制器的仿真
`timescale 1ns / 1ps
module SPI_slave_tb ();
reg mosi;
reg nss;
reg sck;
reg rst_n;
wire done;
wire [7:0] data_in;
wire enable;
wire [7:0]data_out;
wire miso;
reg_array u1 (.clk(sck),.rst_n(rst_n),.nss(nss),.enable(enable),.data_in(data_out),.data_out(data_in),.done(done));
SPI_slave_controller #(0,0)u2 (.mosi(mosi),.nss(nss),.sck(sck),.rst_n(rst_n),.done(done),
.data_in(data_in),.enable(enable),.data_out(data_out),.miso(miso));
task reset;
begin
sck = 0;
rst_n = 1;
nss = 1;
@(negedge sck);
rst_n = 0;
@(negedge sck);
rst_n = 1;
@(negedge sck);
@(negedge sck);
end
endtask
task r_data;
input [7:0] r_data;
begin
@(negedge sck);
nss = 0;
#0.5
mosi = r_data[7];
@(negedge sck);
mosi = r_data[6];
@(negedge sck);
mosi = r_data[5];
@(negedge sck);
mosi = r_data[4];
@(negedge sck);
mosi = r_data[3];
@(negedge sck);
mosi = r_data[2];
@(negedge sck);
mosi = r_data[1];
@(negedge sck);
mosi = r_data[0];
end
endtask
always #5 sck = !sck;
initial
begin
reset;
r_data(8'haf);
r_data(8'h13);
r_data(8'ha0);
r_data(8'h00);
#1000
$stop;
end
endmodule
拉低nss后,我们按照slave的读写时序,先写后读,先向地址a中写入13,再从a地址中读出写入的数据,发现二者相符,需要注意的是,此slave的读取操作的第二拍,主设备依旧会发送数据,体现了【数字IC】深入浅出理解SPI协议所说SPI协议的本质是两个移位寄存器不断地互相交换数据,不过读操作第二拍发送的数据不会对寄存器的存储值产生影响
module BaudratePrescaler #(parameter Prescaler = 2'b01)
( input clk,
input rst_n,
output reg clk_scaler
);
reg scaler_2;
reg scaler_4;
reg scaler_8;
reg scaler_16;
always@(posedge clk or negedge rst_n)
if(!rst_n)
scaler_2 <= 1'b0;
else
scaler_2 <= !scaler_2;
always@(posedge scaler_2 or negedge rst_n)
if(!rst_n)
scaler_4 <= 1'b0;
else
scaler_4 <= !scaler_4;
always@(posedge scaler_4 or negedge rst_n)
if(!rst_n)
scaler_8 <= 1'b0;
else
scaler_8 <= !scaler_8;
always@(posedge scaler_8 or negedge rst_n)
if(!rst_n)
scaler_16 <= 1'b0;
else
scaler_16 <= !scaler_16;
always@(*)
case(Prescaler)
2'b00: clk_scaler = scaler_2;
2'b01: clk_scaler = scaler_4;
2'b10: clk_scaler = scaler_8;
2'b11: clk_scaler = scaler_16;
default: clk_scaler = 1'b0;
endcase
endmodule
`timescale 1ns / 1ps
module BaudratePrescaler_tb ();
reg clk;
reg rst_n;
wire clk_scaler;
BaudratePrescaler #(0) u0 (clk,rst_n,clk_scaler);
BaudratePrescaler #(1) u1 (clk,rst_n,clk_scaler);
BaudratePrescaler #(2) u2 (clk,rst_n,clk_scaler);
BaudratePrescaler #(3) u3 (clk,rst_n,clk_scaler);
BaudratePrescaler #(4) u4 (clk,rst_n,clk_scaler);
task reset;
begin
@(negedge clk);
rst_n = 0;
@(negedge clk);
@(negedge clk);
rst_n = 1;
end
endtask
initial
begin
clk = 0;
rst_n = 1;
reset;
#1000
$stop;
end
always #5 clk = !clk;
endmodule
我们按照Prescaler的值分别例化了五个module,分别是二分频,四分频,八分频,十六分频和错误分频,观察输出的值,符合预期,设计成立。
w_IDLE是复位以后的状态,当接收到enable信号后跳转到w_S1状态
w_S1是发送前7bit的状态
w_S2是发送最后1bit的状态
为什么需要用两个状态来表示同样的一个发送行为呢?跳转到5.2.1即可寻找到答案。
module SPI_master
#(parameter Prescaler = 2'b00,
parameter CPOL = 0,
parameter CPHA = 1)
(input clk,
input rst_n,
input enable,
input [7:0] data_in,
input miso,
output sck_o,
output reg mosi,
output nss);
localparam w_IDLE = 2'b00;
localparam w_S1 = 2'b01;
localparam w_S2 = 2'b10;
reg [1:0] w_state;
reg [1:0] w_nstate;
reg [7:0] send_reg_1;
reg send_reg_2;
reg [0:7] receive_reg;
reg [2:0] w_cnt;
reg [2:0] r_cnt;
wire [7:0] data_out;
reg nss_r;
always@(negedge clk or negedge rst_n)
if(!rst_n)
w_state <= w_IDLE;
else
w_state <= w_nstate;
always@(*)
case(w_state)
w_IDLE : w_nstate = enable ? w_S1 : w_IDLE;
w_S1 : w_nstate = (w_cnt == 3'h6) ? w_S2 : w_S1;
w_S2 : w_nstate = enable ? w_S1 : w_IDLE;
default: w_nstate = w_IDLE;
endcase
always@(negedge clk or negedge rst_n)
if(!rst_n)
send_reg_1 <= 8'h00;
else if(w_state == w_IDLE && enable || w_state == w_S1 || w_state == w_S2)
send_reg_1 <= data_in;
else
send_reg_1 <= 8'h00;
always@(negedge clk or negedge rst_n)
if(!rst_n)
send_reg_2 <= 1'h0;
else if(w_state == w_S1 && w_cnt == 3'h1)
send_reg_2 <= send_reg_1[1];
else
send_reg_2 <= 1'h0;
always@(negedge clk or negedge rst_n)
if(!rst_n)
w_cnt <= 3'h0;
else if(w_state == w_S1 || w_state == w_S2 )
w_cnt <= w_cnt + 1'b1;
else if (w_cnt == 3'h7)
w_cnt <= 3'h0;
else
w_cnt <= w_cnt;
always@(*)
if(w_state == w_S1)
mosi = send_reg_1[w_cnt];
else if(w_state == w_S2)
mosi = send_reg_2;
else
mosi = 1'bx;
always@(negedge clk or negedge rst_n)
if(!rst_n )
nss_r <= 1'b1;
else if(w_state == w_IDLE && enable || w_state == w_S1 || w_state == w_S2)
nss_r <= 1'b0;
else if(w_state == w_IDLE || !enable)
nss_r <= 1'b1;
else
nss_r <= nss_r;
assign nss = nss_r;
// read_statement
always@(posedge clk or negedge rst_n)
if(!rst_n)
r_cnt <= 3'h0;
else if(r_cnt < 3'h7 && enable)
r_cnt <= r_cnt + 1'b1;
else if(r_cnt == 3'h7)
r_cnt <= 3'h0;
else
r_cnt <= r_cnt;
always@(posedge clk or negedge rst_n)
if(!rst_n)
receive_reg <= 8'h00;
else if(!nss_r)
receive_reg[r_cnt] <= miso;
else
receive_reg <= receive_reg;
assign data_out = r_cnt == 3'h7 ? receive_reg : 8'hx;
assign sck_o = (!rst_n) ? CPOL : clk ;
endmodule
当8bit的数据传输进来后,依次按从高到低的顺序进行发送,存储这8bit的寄存器是send_reg_1,但是这种形式没有办法pipeline起来,因为8bit寄存器内部数值的更新会暂停整个数据的发送(相当于需要暂停一个时钟周期),因此使用了一个send_reg_2,来保留最后1bit的数据,通过增大面积的方式减少了暂停产生的SPI带宽损失。
这里的仿真也同样是将波特率分频器和主设备控制器共同进行仿真,第一段代码为二者连接的.v文件,第二段代码为二者的仿真文件。
module top_module
(input clk,
input rst_n,
input enable,
input [7:0] data_in_top,
input miso,
output nss,
output sck_o,
output mosi
);
wire clk_sacler;
wire [7:0] data_in;
BaudratePrescaler u1 (.clk(clk),.rst_n(rst_n),.clk_scaler(clk_scaler));
SPI_master u2 (.clk(clk_scaler),.rst_n(rst_n),.enable(enable),
.data_in(data_in_top),.miso(miso),.sck_o(sck_o),.mosi(mosi),.nss(nss));
endmodule
`timescale 1ns / 1ps
module top_module_tb();
reg clk;
reg rst_n;
reg enable;
reg [7:0] data_in;
reg miso;
wire sck_o;
wire mosi;
wire nss;
top_module u1 (.clk(clk),.rst_n(rst_n),.enable(enable),.data_in_top(data_in),.miso(miso));
task reset;
begin
clk = 0;
rst_n=1;
enable = 0;
data_in = 8'h00;
miso = 1;
@(negedge clk);
rst_n = 0;
@(negedge clk);
rst_n = 1;
enable =1;
#0.5
data_in = 8'h1f;
end
endtask
always #5 clk = !clk;
initial
begin
reset;
#100;
enable = 0;
#1000;
enable = 1;
#2000;
$stop;
end
endmodule
感兴趣的读者可以参考 【数字IC手撕代码】Verilog同步FIFO,增加相对应的模块和控制信号,使整个SPI的设计更为完善
这里的案例只引入了CPOL和CPHA的一种状态,如果想要添加更多的状态可以使用条件编译的ifdef的方式来进行
我们案例的从设备只支持读写,其实以EEPROM——常用于SPI控制的内存芯片为例,读写的命令可以非常复杂,如顺序读写,随机读写,错误检测,错误矫正等诸多内容可以进行添加
实际上,本设计仅为学习参考使用,配合【数字IC】深入浅出理解SPI协议使读者对于协议和电路实现有基本的认识才是本篇博文的目的所在。