SPI通信协议
1. SPI介绍
- SPI(Serial Peripheral Interface,串行外围设备接口)通信协议,同步串行接口技术,是一种高速、全双工、同步通信总线,在芯片中只占用四根管脚用来控制及数据传输。
- 应用:EEPROM、Flash、RTC、ADC、DSP等。
- 优缺点:全双工通信,通讯方式较为简单,相对数据传输速率较快;没有应答机制确认数据是否接受,在数据可靠性上有一定缺陷(与I2C相比)。
- 物理层:采用主从模式的控制方式,支持单Master,多Slave
CLK(Serial Clock):时钟信号线,用于同步通讯数据
MOSI(Master Output, Slave Input):主设备输出 / 从设备输入
MISO(Master Input, Slave Output):主设备输入 / 从设备输出
CS(Chip Select):片选信号线,低有效
- 协议层:默认从高位开始交换数据,四个通信模式。
CPOL(clock polarity):时钟极性。从设备空闲时,SCK的状态
CPHA(clock phase):时钟相位。规定数据采样是在时钟信号的奇数边沿还是偶数边沿
使用奇偶边沿是因为时钟极性有两种情况
2. 实现模式0下的SPI协议
- 介绍一下模式0,CPOL = 0, CPHA = 0,空闲时刻cslk为低电平,奇数边沿,也就是上升沿的时候master和slave进行数据采样,这也就要求在复位无效后,第一个上升沿到来前,master和slave要把对方带采样的数据寄存输出。
- 各级模块调用关系
画图好慢,但是vivado生成的RTL图连线好乱
2.1 top顶层模块
module top_spi(
input wire clk, // 50MHz
input wire rst_n, // 复位,低有效
//
input wire [7:0] wr_data, // 主机待发送数据
input wire wr_en, // 主机发送使能信号
input wire rd_en, // 主机接收使能信号
output wire [7:0] rd_data, // 主机接收缓冲区
output wire wr, // 主机发送标志信号
output wire rd // 主机接收接收缓冲区数据标志信号
);
wire [8:0] clk_cnt;
wire [3:0] cnt;
wire cs;
wire sclk;
wire mosi;
wire miso;
// 时钟计数器,50MHz时钟进行500分频产生100KMz的sclk时钟
// clk_cnt == clk_value / 4 * 4 - 1 上升沿
// clk_cnt == clk_value / 4 * 1 - 1 高电平
// clk_cnt == clk_value / 4 * 2 - 1 下降沿
// clk_cnt == clk_value / 4 * 3 - 1 低电平
parameter clk_value = 500;
// 主机模块
spi_master # (
.clk_value(clk_value)
)
spi_master0 (
// 系统时钟和复位
.clk(clk),
.rst_n(rst_n),
// SPI接口
.miso(miso),
.mosi(mosi),
.sclk(sclk),
.cs(cs),
// 主机控制信号
.wr_data(wr_data),
.wr_en(wr_en),
.rd_en(rd_en),
.rd_data(rd_data),
.wr(wr),
.rd(rd),
.clk_cnt(clk_cnt),
.cnt(cnt)
);
// 从机模块
spi_slave # (
.clk_value(clk_value)
)
spi_slave0 (
.clk(clk),
.rst_n(rst_n),
.simo(mosi),
.sclk(sclk),
.cs(cs),
.somi(miso),
.rd_en(wr_en),
.wr_en(rd_en),
.clk_cnt(clk_cnt),
.cnt(cnt),
.slave_data(slave_data)
);
endmodule
2.2 master模块
module spi_master(
input wire clk, // 50MHz
input wire rst_n, // 复位,低有效
// SPI接口信号
input wire miso, // 主收从发
output wire mosi, // 主发从收
output wire sclk, // SPI工作时钟
output wire cs, // 从设备片选,低有效
//
input wire [7:0] wr_data, // 主机待发送数据
input wire wr_en, // 主机发送使能信号
input wire rd_en, // 主机接收使能信号
output wire [7:0] rd_data, // 主机接收缓冲区
output wire wr, // 主机发送标志信号
output wire rd, // 主机接收接收缓冲区数据标志信号
output wire [8:0] clk_cnt,
output wire [3:0] cnt
);
// 时钟计数器,50MHz时钟进行500分频产生100KMz的sclk时钟
// clk_cnt == clk_value / 4 * 4 - 1 上升沿
// clk_cnt == clk_value / 4 * 1 - 1 高电平
// clk_cnt == clk_value / 4 * 2 - 1 下降沿
// clk_cnt == clk_value / 4 * 3 - 1 低电平
parameter clk_value = 500;
// 用来产生sclk时钟信号和cs片选信号
control #(
.clk_value(clk_value)
)
control0 (
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en), // 主机写请求
.rd_en(rd_en), // 主机读请求
.sclk(sclk), // SCLK时钟信号
.cs(cs), // 从机片选信号
.clk_cnt(clk_cnt), // 时钟计数器
.cnt(cnt) // 数据计数器
);
// 发送
spi_tx #(
.clk_value(clk_value)
)
spi_tx0 (
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.wr_data(wr_data),
.clk_cnt(clk_cnt),
.cnt(cnt),
.wr(wr),
.mosi(mosi)
);
// 接收
spi_rx #(
.clk_value(clk_value)
)
spi_rx0 (
.clk(clk),
.rst_n(rst_n),
.rd_en(rd_en),
.clk_cnt(clk_cnt),
.cnt(cnt),
.miso(miso),
.rd(rd),
.rd_data(rd_data)
);
endmodule
2.2.1 control控制模块
module control(
input wire clk, // 50MHz
input wire rst_n,
input wire wr_en, // 主机写使能
input wire rd_en, // 主机读使能
output reg sclk,
output reg cs,
output reg [8:0] clk_cnt,
output reg [3:0] cnt
);
// parameter clk_value = 9'd500;
parameter clk_value = 9'b111110100;
// 状态声明
parameter IDLE = 2'b00; // 空闲状态
parameter WORK = 2'b01; // 工作状态
parameter STOP = 2'b10; // 停止状态
// 状态寄存器
reg [1:0] curr_state;
reg [1:0] next_state;
// 时钟计数器
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 9'd0;
end else if ( clk_cnt == (clk_value - 1) ) begin
clk_cnt <= 9'd0;
end else begin
clk_cnt <= clk_cnt + 1'd1;
end
end
// 状态转移
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
curr_state <= IDLE;
end else if (clk_cnt == (clk_value / 4 * 2 - 1)) begin // 状态再sclk下降沿改变
curr_state <= next_state;
end else begin
curr_state <= curr_state;
end
end
// 次态产生组合逻辑
always @ (wr_en or rd_en or cnt) begin
case (curr_state)
IDLE: begin
if (wr_en || rd_en) next_state <= WORK; // 写/读使能,开始工作
else next_state <= IDLE;
end
WORK: begin
if (cnt == 4'd8) next_state <= STOP; // 8bit数据已完成写/读
else next_state <= WORK;
end
STOP: begin
if (wr_en || rd_en) next_state <= WORK; // 写/读使能,继续工作
else next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
// 三段式状态机输出
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk <= 1'b0;
cs <= 1'b1;
cnt <= 4'd0;
end else begin
case (curr_state)
IDLE: begin
sclk <= 1'b0;
cs <= 1'b1;
cnt <= 4'd0;
end
WORK: begin
cs <= 1'b0;
if (clk_cnt == clk_value / 4 * 4 - 1) begin // 上升沿位置
sclk <= 1'b1;
cnt <= cnt + 1'd1; // 模式0工作
end else if (clk_cnt == clk_value / 4 * 2 - 1) begin // 下降沿位置
sclk <= 1'b0;
cnt <= cnt;
end else begin
sclk <= sclk;
cnt <= cnt;
end
end
STOP: begin
sclk <= 1'b0;
cs <= 1'b1;
cnt <= 4'd0;
end
default: begin
sclk <= 1'b0;
cs <= 1'b1;
cnt <= 4'd0;
end
endcase
end
end
endmodule
2.2.2 tx发送模块
// 发送
// 模式0下,sclk下降沿输出
module spi_tx(
input wire clk,
input wire rst_n,
input wire wr_en, // 写/发送使能信号
input wire [7:0] wr_data, // 写/发送数据
input wire [8:0] clk_cnt, // 时钟计数器
input wire [3:0] cnt, // 数据计数器
output reg wr, // 写/发送标志信号,低有效
output reg mosi // 主发从收
);
parameter clk_value = 500;
// 状态声明
parameter IDLE = 2'b00; // 空闲状态
parameter WORK = 2'b01; // 工作状态
parameter STOP = 2'b10; // 停止状态
// 状态寄存器
reg [1:0] curr_state;
reg [1:0] next_state;
// 状态转移
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
curr_state <= IDLE;
end else if (clk_cnt == clk_value / 4 * 2 - 1) begin
curr_state <= next_state;
end
end
// 次态产生组合逻辑
always @ (wr_en or cnt) begin
case (curr_state)
IDLE: begin
if (wr_en) next_state <= WORK;
else next_state <= IDLE;
end
WORK: begin
if (cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
end
STOP: begin
if (wr_en) next_state <= WORK;
else next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
// 三段式状态机输出
always @ (*) begin
if (!rst_n) begin
mosi <= 1'b0;
wr <= 1'b1;
end else begin
case (curr_state)
IDLE: begin
mosi <= 1'b0;
wr <= 1'b1;
end
WORK: begin
if (clk_cnt == clk_value / 4 * 2 - 1) begin
case (cnt)
4'd0: mosi <= wr_data[7];
4'd1: mosi <= wr_data[6];
4'd2: mosi <= wr_data[5];
4'd3: mosi <= wr_data[4];
4'd4: mosi <= wr_data[3];
4'd5: mosi <= wr_data[2];
4'd6: mosi <= wr_data[1];
4'd7: mosi <= wr_data[0];
default: mosi <= 1'b0;
endcase
end
wr <= 1'b0;
end
STOP: begin
mosi <= 1'b0;
wr <= 1'b1;
end
default: begin
mosi <= 1'b0;
wr <= 1'b1;
end
endcase
end
end
endmodule
2.2.3 rx接收模块
// 接收
// 模式0下,下降沿输出,上升沿进行数据采样
module spi_rx(
input wire clk,
input wire rst_n,
input wire rd_en, // 读/接收使能信号
input wire [8:0] clk_cnt, // 时钟计数器
input wire [3:0] cnt, // 数据计数器
input wire miso, // 主收从发
output reg rd, // 读/接收数据标志信号,低有效
output reg [7:0] rd_data // 读/接收数据
);
parameter clk_value = 500;
// 状态声明
parameter IDLE = 2'b00; // 空闲状态
parameter WORK = 2'b01; // 工作状态
parameter STOP = 2'b10; // 停止状态
// 状态寄存器
reg [1:0] curr_state;
reg [1:0] next_state;
// 状态转移
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
curr_state <= IDLE;
end else if (clk_cnt == clk_value / 4 * 2 - 1) begin
curr_state <= next_state;
end
end
// 次态产生逻辑
always @ (rd_en or cnt) begin
case (curr_state)
IDLE: begin
if (rd_en) next_state <= WORK;
else next_state <= IDLE;
end
WORK: begin
if (cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
end
STOP: begin
if (rd_en) next_state <= WORK;
else next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
// 三段式状态机输出
always @ (*) begin
if (!rst_n) begin
rd <= 1'b1;
rd_data <= 8'b0;
end else begin
case (curr_state)
IDLE: begin
rd <= 1'b1;
rd_data <= 8'b0;
end
WORK: begin
if (clk_cnt == clk_value / 4 * 4 - 1) begin
case (cnt)
4'd0: rd_data[7] <= miso;
4'd1: rd_data[6] <= miso;
4'd2: rd_data[5] <= miso;
4'd3: rd_data[4] <= miso;
4'd4: rd_data[3] <= miso;
4'd5: rd_data[2] <= miso;
4'd6: rd_data[1] <= miso;
4'd7: rd_data[0] <= miso;
default: rd_data <= 8'b0;
endcase
end
rd <= 1'b0;
end
STOP: begin
rd <= 1'b1;
end
default: begin
rd <= 1'b1;
rd_data <= 8'b0;
end
endcase
end
end
endmodule
2.3 slave模块
// 从机模块
/*
只需要一个module实现,接发都在这一个模块中
用reg寄存发送最高位的数据,把内部寄存器用成移位的,接收来的寄存器放在最低位,用数据计数器cnt数8次交换
*/
module spi_slave(
input wire clk,
input wire rst_n,
// SPI从机接口信号
input wire simo, // 从收主发
input wire sclk, // SPI工作时钟
input wire cs, // 从机片选,低有效
output reg somi, // 从发主收
// 控制信号
input wire rd_en,
input wire wr_en,
input wire [8:0] clk_cnt, // 时钟计数器
input wire [3:0] cnt, // 数据计数器
// 寄存器信息
output reg [7:0] slave_data
);
// clk_cnt == clk_value / 4 * 4 - 1 上升沿
// clk_cnt == clk_value / 4 * 1 - 1 高电平
// clk_cnt == clk_value / 4 * 2 - 1 下降沿
// clk_cnt == clk_value / 4 * 3 - 1 低电平
parameter clk_value = 9'd500;
// 状态声明
parameter IDLE = 2'b00; // 空闲状态
parameter WORK = 2'b01; // 工作状态
parameter STOP = 2'b10; // 停止状态
// 状态寄存器
reg [1:0] curr_state;
reg [1:0] next_state;
reg flag;
// 状态转移
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
curr_state <= IDLE;
end else if (clk_cnt == clk_value / 4 * 2 - 1) begin
curr_state <= next_state;
end
end
// 次态产生逻辑
// 默认发送和接收是同时进行的
always @ (rd_en or wr_en or cnt) begin
case (curr_state)
IDLE: begin
if (rd_en) next_state <= WORK;
else next_state <= IDLE;
end
WORK: begin
if (cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
end
STOP: begin
if (rd_en) next_state <= WORK;
else next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
// 三段式状态机的输出情况
// 当进入交换状态WORK时,先发送1bit数据
always @ (cs or cnt) begin
if (!rst_n) begin
somi <= 1'b0; // 发送1'b0
slave_data <= 8'b1111_0101; // 初始化从机寄存器
flag <= 1'b0;
end else begin
case (curr_state)
IDLE: begin
somi <= 1'b0;
slave_data <= 8'b1111_0101;
end
WORK: begin
somi <= slave_data[7];
slave_data[7:0] <= {slave_data[6:0], simo};
flag <= simo;
end
STOP: begin
somi <= 1'b0;
slave_data <= 8'b1111_0101;
end
default: begin
somi <= 1'b0;
slave_data <= 8'b1111_0101;
end
endcase
end
end
endmodule
2.4 测试
module sim_spi();
reg clk;
reg rst_n;
reg [7:0] wr_data; // 主机待发送
reg wr_en; // 主机发送使能信号
reg rd_en; // 主机接收使能信号
wire [7:0] rd_data; // 主机接收缓冲区
wire wr; // 主机发送标志信号
wire rd; // 主机接收标志信号
top_spi top_spi0 (
.clk(clk),
.rst_n(rst_n),
.wr_data(wr_data),
.wr_en(wr_en),
.rd_en(rd_en),
.rd_data(rd_data),
.wr(wr),
.rd(rd)
);
// 50MHz产生
parameter clk_period_50M = 20;
always #(clk_period_50M / 2) clk = ~clk;
// 信号初始化
initial begin
clk = 0;
rst_n = 0; // 复位使能,低有效
wr_data = 8'b0; // 主机待发送数据
// 这是主机内部寄存器的已有数据
wr_en = 1'b0; // 主机发送使能,高有效
rd_en = 1'b0; // 主机接收使能,高有效
end
initial begin
end
initial begin
wr_data = 8'b1010_1010; // 这是主机待发送的数据
#200 rst_n = 1'b1; // 复位无效
#200 wr_en = 1'b1; // 发送写使能
rd_en = 1'b1; // 接收读使能
#5000 //miso = 1'b1;
#10000 //miso = 1'b1;
#10000 //miso = 1'b0;
#10000 //miso = 1'b1;
#10000 //miso = 1'b0;
#10000 //miso = 1'b0;
#10000 //miso = 1'b1;
#10000 //miso = 1'b1;
#5000 wr_en = 1'b0;
rd_en = 1'b0;
end
endmodule
3. 问题分析与总结
- 上面的波形放大后,局部显示如下,为什么master和slave的发送没有同步在一个时钟?
原因:master的发送是组合逻辑,当curr_state发生变化就输出一次,slave的输出组合逻辑由cs触发,在control模块,cs的变化是时序逻辑,所以晚了一拍
4. 参考
基于FPGA的SPI主机数据接收发送控制器