引言:SPI是常用的板级通信协议,在FPGA板级通信中,许多重要的从器件都对SPI协议有所支持。因此,掌握SPI通信的FPGA片上实现对于FPGA工程开发具有重要的意义。本文设计了一个基于SPI模式0的主机通信控制器,系统性阐明了SPI设计的全流程。希望本文的设计能够对更多的人有所帮助。
一、SPI通信协议简介
1.物理层引线
SPI通信的最小结构为一主一从结构,主机向从机提供信号发送接收时钟SCLK。在物理实现中,主机与从机之间存有四根引线,即MOSI(主收从发)、MISO(主发从收)、SCK(通信时钟)、nSS(从机片选信号)。其中,通过在主机上增加片选信号输出端的数目,可以使得主机控制更多的从机,实现一主多从的SPI通信。
2.SPI四种模式
SPI通信协议规定了4钟工作模式,在实际应用中应当保证主机和从机工作在相同的工作模式下。SPI工作模式通过时钟极性CPOL和时钟相位CPHA联合指定。其中没时钟极性CPOL指定SCK在空闲状态时的电平,时钟极性CPHA指定在SCK的何种边缘进行数据采样。其标识如下:
CPOL=1 | CPOL=0 | |
CPHA=1 | MODE3:SCK空闲为高,上升沿采样 | MODE1:SCK空闲为低,下降沿采样 |
CPHA-0 | MODE2:SCK空闲为高,下降沿采样 | MODE0:SCK空闲为低,上升沿采样 |
其中,SPI中最常用的时模式0和模式2.
二、FPGA实现设计
由于我们设计的目标为SPI模式0主机控制器,因此我需要实现在SCK上升沿时对数据进行采样。由于在时钟上升沿进行数据采样,那么,接收/发送状态机的状态应当早于SCK时钟上升沿提前准备好接收/发送状态。
对于发送状态机,必须在SCK时钟上升沿到来前将需发送的数据在MOSI信号线上准备好。因此其状态的跳变应当较时钟上升沿到来更早。因此,本文设定发送状态机的在SCK时钟的下降沿进行状态变换,在SCK的低电平中心将需发送的数据压至MOSI线上。
对于接收状态机,为了保证状态跳变的一致性,也采用SCK下降沿进行状态跳变时刻。接收状态机在SCK上升沿时将数据采集至接收缓冲口,并在采集完一字节数据后生成信号标志脉冲。这个脉冲将保持一个SCK周期以供FPGA其余模块识别。
对于SPI主机控制器,另一个重要的模块是称之为control的控制模块,它是用来生成SCK时钟与nSS片选信号的模块。由于本文设计只有一个从机,因此只单纯的控制nSS的电平,不对多条nSS进行选择控制。SCK时钟的生成也是依靠状态机,其机理为当状态机处于IDLE空闲状态时,检测到读/写请求,在时钟相位变为下降沿时进入工作状态,并输出8个完整的SCK时钟。此后,状态机转入STOP状态,并在一个周期内检测是否有新的请求信号到来,若无抵达空闲状态,若有,转入工作状态。
本文设计的代码如下,如果要修改SPI工作模式,请根据头文件的相位定义修改代码:
module SPI(
clk,rst_n,miso,mosi,sclk,ss_n,wr,rd,data_wr,data_rd,wr_req,rd_req
);
input clk; //时钟信号,设定为50MHz
input rst_n; //异步复位同步释放
//SPI接口信号
input miso; //主收从发信号
output mosi; //主发从收信号
output sclk; //SPI发送接收时钟SCLK
output ss_n; //SPI从设备片选信号
//功能指示与数据传输信号
output wr; //SPI主机发送空闲信号
output rd; //SPI主机接收缓冲区数据可读信号
input[7:0]data_wr; //需通过SPI主机发送的数据
input wr_req; //SPI主机数据发送请求信号
input rd_req; //SPI主机读读从机数据请求信号
output[7:0]data_rd; //SPI主机接收机缓冲区数据
//
wire[8:0]phase; //SCLK时钟相位值
wire[3:0]cnt; //发送/接收字节计数器
//
//控制模块:片选信号及SCLK时钟信号控制
control U1(
.clk(clk), //时钟信号
.rst_n(rst_n), //全局复位信号
.sclk(sclk), //SCLK时钟信号
.ss_n(ss_n), //从机片选信号
.phase(phase), //SCLK时钟相位信息
.wr_req(wr_req), //主机写请求
.rd_req(wr_req), //主机读请求
.cnt(cnt) //字节计数器
);
//
//发送模块
spi_tx U2(
.clk(clk), //
.rst_n(rst_n), //
.wr_req(wr_req), //写请求信号
.data_wr(data_wr), //写数据
.phase(phase), //SCLK相位
.wr(wr), //SPI主机发送空闲信号
.mosi(mosi), //主发从收信号
.cnt(cnt) //字节计数器
);
//
//接收模块
spi_rx U3(
.clk(clk), //
.rst_n(rst_n), //
.rd_req(rd_req), //读请求信号
.data_rd(data_rd), //读数据
.phase(phase), //SCLK相位
.rd(rd), //SPI主机接收缓冲可读信号
.miso(miso), //主收从发信号
.cnt(cnt) //字节计数器
);
//
endmodule
`include "global_definition.v"
module control(
clk,rst_n,sclk,ss_n,phase,wr_req,rd_req,cnt
);
input clk;
input rst_n;
output reg sclk; //SCLK时钟信号
output reg ss_n; //从机片选信号
output reg[8:0]phase; //SCLK时钟相位
input wr_req; //主机写请求
input rd_req; //主机读请求
output cnt; //字节计数器
///
reg[1:0]state; //状态机
reg[1:0]next_state; //下一状态
//状态机状态变换尽可能贴合BCD码变换顺序,以减少电路成本并降低组合逻辑风险
parameter IDLE = 2'b00; //空闲状态
parameter WORK = 2'b01; //工作状态
parameter STOP = 2'b11; //停止状态
reg[3:0]cnt; //并串转换计数器
///
//相位计数器
always@(posedge clk or negedge rst_n)
if(!rst_n) phase <= 9'd0;
else if(phase == 9'd499) phase <= 9'd0; //分频数500,SPI时钟速率100KHz
else phase <= phase + 9'd1;
///
//状态机转移
always@(posedge clk or negedge rst_n)
if(!rst_n) state <= IDLE;
else if(`SCLK_NEG)state <= next_state; //状态只在SCLK时钟下降沿时改变
else state <= state;
///
//状态组合判定
always@(wr_req or rd_req or cnt)
case(state)
IDLE : if(wr_req||rd_req) next_state <= WORK; //空闲状态遭遇读/写请求,下次状态发生改变
else next_state <= IDLE;
WORK : if(cnt == 4'd8) next_state <= STOP; //发送/接收一字节数据后,状态转移至STOP
else next_state <= WORK;
STOP : if(wr_req||rd_req) next_state <= WORK; //在STOP状态下,若观测到请求,则继续工作,否则休眠
else next_state <= IDLE;
default : next_state <= IDLE;
endcase
///
//状态机输出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
sclk <= 1'b0;
ss_n <= 1'b1;
cnt <= 4'd0;
end
else begin
case(state)
IDLE : begin
sclk <= 1'b0;
ss_n <= 1'b1;
cnt <= 4'd0;
end
WORK : begin
ss_n <= 1'b0;
if(`SCLK_POS)begin //时钟上升沿
sclk <= 1'b1; //SCLK时钟拉高
cnt <= cnt + 4'd1; //字节计数器加一
end
else if(`SCLK_NEG)sclk <= 1'b0; //
else sclk <= sclk;
end
STOP : begin
cnt <= 4'd0;
sclk <= 1'b0;
ss_n <= 1'b1;
end
default : begin
cnt <= 4'd0;
sclk <= 1'b0;
ss_n <= 1'b1;
end
endcase
end
///
endmodule
`include "global_definition.v"
module spi_tx(
clk,rst_n,wr_req,data_wr,phase,wr,mosi,cnt
);
input clk;
input rst_n;
input wr_req; //主机写请求信号
input[7:0]data_wr; //写数据
input[8:0]phase; //SCLK相位
output reg wr; //写空闲标志
output reg mosi; //主发从收信号
input[3:0]cnt; //字节计数器
reg[1:0]state;
reg[1:0]next_state;
parameter IDLE = 2'b00;
parameter WORK = 2'b01;
parameter STOP = 2'b11;
//状态机转移
always@(posedge clk or negedge rst_n)
if(!rst_n) state <= IDLE;
else if(`SCLK_NEG) state <= next_state;
//状态组合判定
always@(wr_req or cnt)
case(state)
IDLE : if(wr_req) next_state <= WORK;
else next_state <= IDLE;
WORK : if(cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
STOP : if(wr_req) next_state <= WORK;
else next_state <= IDLE;
default : state <= IDLE;
endcase
//状态机输出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
mosi <= 1'b0;
wr <= 1'b1;
end
else begin
case(state)
IDLE : begin
mosi <= 1'b0;
wr <= 1'b1;
end
WORK : begin //在SCLK下降沿状态改变后,紧跟
if(`SCLK_LOW)begin //SCLK低电平时将数据放置总线上
case(cnt)
4'd0 : mosi <= data_wr[7];
4'd1 : mosi <= data_wr[6];
4'd2 : mosi <= data_wr[5];
4'd3 : mosi <= data_wr[4];
4'd4 : mosi <= data_wr[3];
4'd5 : mosi <= data_wr[2];
4'd6 : mosi <= data_wr[1];
4'd7 : mosi <= data_wr[0];
default : mosi <= 1'b0;
endcase
end
wr <= 1'b0;
end
STOP : begin
wr <= 1'b1;
mosi <= 1'b0;
end
default : begin
wr <= 1'b1;
mosi <= 1'b0;
end
endcase
end
endmodule
`include "global_definition.v"
module spi_rx(
clk,rst_n,rd_req,data_rd,phase,rd,miso,cnt
);
input clk;
input rst_n;
input rd_req; //主机读请求信号
output reg[7:0]data_rd; //读出数据
input[8:0]phase; //SCLK相位
output reg rd; //读数据有效标志
input miso; //主收从发信号
input[3:0]cnt; //字节计数器
reg[1:0]state;
reg[1:0]next_state;
parameter IDLE = 2'b00;
parameter WORK = 2'b01;
parameter STOP = 2'b11;
//状态机转移
always@(posedge clk or negedge rst_n)
if(!rst_n) state <= IDLE;
else if(`SCLK_NEG) state <= next_state;
//状态组合判定
always@(rd_req or cnt)
case(state)
IDLE : if(rd_req) next_state <= WORK;
else next_state <= IDLE;
WORK : if(cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
STOP : if(rd_req) next_state <= WORK;
else next_state <= IDLE;
default : state <= IDLE;
endcase
//状态机输出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
rd <= 1'b0;
data_rd <= 8'd0;
end
else begin
case(state)
IDLE : begin
rd <= 1'b0;
data_rd <= 8'd0;
end
WORK : begin //在SCLK下降沿状态改变后,紧跟
if(`SCLK_POS)begin //SCLK上升沿时读取总线上数据
case(cnt)
4'd0 : data_rd[7] <= miso;
4'd1 : data_rd[6] <= miso;
4'd2 : data_rd[5] <= miso;
4'd3 : data_rd[4] <= miso;
4'd4 : data_rd[3] <= miso;
4'd5 : data_rd[2] <= miso;
4'd6 : data_rd[1] <= miso;
4'd7 : data_rd[0] <= miso;
default : data_rd <= 8'd0;
endcase
end
rd <= 1'b0;
end
STOP : begin
rd <= 1'b1;
end
default : begin
rd <= 1'b0;
data_rd <= 8'd0;
end
endcase
end
endmodule
头文件如下:
`define SCLK_POS (phase == 9'd499) //定义SCLK上升沿
`define SCLK_HIG (phase == 9'd124) //定义SCLK高电平
`define SCLK_NEG (phase == 9'd249) //定义SCLK下降沿
`define SCLK_LOW (phase == 9'd374) //定义SCLK低电平