一 SPI通信协议
串行外围设备接口(Serial Peripheral Interface,SPI)是一种高速、全双工、同步通讯协议,广泛应用于EEPROM、Flash、ADC、DSP等器件控制和数据传输。SPI协议主要包含CS_N、SCK、MOSI、MISO信号线,它们的作用如下:
(1) 片选信号线 (Chip Select,CS_N):SPI协议中没有设备地址,通过将从设比的CS_N设置为低电平来选择从设备,整个SPI通讯以CS_N信号线为低电平开始,以CS_N线为高电平结束。
(2) 时钟信号线 (Serial Clock,SCK):用于同步通讯数据。
(3) 主设备输入/从设备输出信号线(Master Output Slave Input,MOSI):主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,数据方向由主机到从机。
(4)主设备输出/从设备输入信号线(Master Intput Slave Output,MISO):主机从这条信号线读入数据,从机的数据通过这条线输出到主句,数据方向为从机到主机。
1.1CPOL/CPHA及通讯模式
SPI通讯协议一共有四种通讯模式,即模式0、模式1、模式2、模式3,四种模式由时钟极性(Clock Porilarity,CPOL)和时钟相位(Clock Phase,CPHA)来决定。CPOL规定了空闲状态时SCK时钟信号的电平状态,CPHA规定了数据采样在SCK时钟的奇数边沿还是偶数边沿。四种模式如下表所示
工作模式 | CPOL、CPHA | |
模式0 | CPOL=0 CPHA=0 | 空闲状态时SCK为低电平,数据采样在SCK奇数沿,数据更新在SCK偶数沿,奇数边沿为上升沿,偶数边沿为下降沿。 |
模式1 | CPOL=0 CPHA=1 | 空闲状态时SCK为低电平,数据采样在SCK偶数沿,数据更新在SCK奇数沿,奇数边沿为上升沿,偶数边沿为下降沿。 |
模式2 | CPOL=1 CPHA=0 | 空闲状态时SCK为高电平,数据采样在SCK奇数沿,数据更新在SCK偶数沿,奇数边沿为下降沿,偶数边沿为上升沿。 |
模式3 | CPOL=1 CPHA=1 | 空闲状态时SCK为高电平,数据采样在SCK偶数沿,数据更新在SCK奇数沿,奇数边沿为下降沿,偶数边沿为上升沿。 |
1.2通讯过程
通讯过程以模式0为例如下图所示。CS_N、SCK、MOSI信号均有主机控制产生,MISO信号由从机产生。CS_N用于选择从机设备,信号由高变低为起始信号,由低变高为停止信号,表示本次通讯结束;SCK时时钟信号,用于同步数据;MOSI和MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入和输出时同时进行的。
观察图中的②③④⑤标号处,MOSI 及 MISO 的数据在 SCK 的下降沿期间变化输出, 在 SCK 的上升沿时被采样。即在 SCK 的上升沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO 为下一次表示数据做准备。
二 SPI_Flash(M25P16)读ID 实验
2.1实验内容
通过控制按键读取M25P16芯片ID标识,当检测到按键按下时,通过SPI协议读取Flash芯片的制造厂商标识20h、设备标识(内存类型20h、内存容量15h),读写时钟速率为12.5MHz。读ID指令RDID为8’h9F,读出数据字为1~3字节, RDID时序如下图所示。
2.2 读ID工程代码
读ID工程主要包括顶层例化spi_rdid_top、读ID模块spi_rdid、按键消抖模块key_filter三个部分。spi_rd_id模块如下:
`timescale 1ns/1ps
module spi_rdid(
input clk,
input rst_n,
input key_in,
input miso,
output reg cs_n,
output reg sck,
output reg mosi
);
parameter DEVICE_ID = 8'h9f; //读ID指令
parameter IDLE = 3'b001; //空闲状态
parameter WR_INST = 3'b010; //写入ID指令状态
parameter RD_ID = 3'b100; //读出flash数据状态
reg [2:0] curr_state;
reg [2:0] next_state;
reg [4:0] clk_cnt; //系统时钟计数
reg [1:0] sck_cnt; //spi时钟计数器
reg [2:0] bit_cnt; //数据位计数器
reg [1:0] byte_cnt; //字节计数器
reg [7:0] miso_r; //对miso数据存储的移位寄存器
reg [7:0] recv_data; //读出设备ID数据
reg recv_flag; //读出一个字节数据有效信号
reg miso_flag; //对miso数据进行移位存储的标志信号
//同步时序描述状态转移
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
curr_state <= IDLE;
else
curr_state <= next_state;
end
//组合逻辑描述状态转移规律
always @(*)
begin
next_state <= IDLE;
case(curr_state)
IDLE:begin
if(key_in) //当检测到按键按下
next_state <= WR_INST;
else
next_state <= IDLE;
end
WR_INST:begin
if(byte_cnt == 2'd0 && clk_cnt == 5'd31) //当RD_ID指令写入完成
next_state <= RD_ID;
else
next_state <= WR_INST;
end
RD_ID:begin
if(byte_cnt == 2'd3 && clk_cnt == 5'd31)
next_state <= IDLE;
else
next_state <= RD_ID;
end
default: next_state <= IDLE;
endcase
end
//clk_cnt:系统时钟计数器
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_cnt <= 5'd0;
else if(curr_state != IDLE)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= 5'd0;
end
//sck_cnt:sck时钟计数器,对clk进行4分频
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
sck_cnt <= 2'd0;
else if(cs_n == 1'b0)
sck_cnt <= sck_cnt + 1'b1;
else
sck_cnt <= 2'd0;
end
//bit_cnt:数据位计数器
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
bit_cnt <= 3'd0;
else if(sck_cnt == 2'd1)
bit_cnt <= bit_cnt + 1'b1;
else
bit_cnt <= bit_cnt;
end
//byte_cnt:字节计数器
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
byte_cnt <= 2'd0;
else if(byte_cnt == 2'd3 && clk_cnt == 5'd31)
byte_cnt <= 2'd0;
else if(clk_cnt == 5'd31)
byte_cnt <= byte_cnt + 1'b1;
else
byte_cnt <= byte_cnt;
end
//cs_n:片选信号
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
cs_n <= 1'b1;
else if(key_in)
cs_n <= 1'b0;
else if(byte_cnt == 2'd3 && clk_cnt == 5'd31)
cs_n <= 1'b1;
else
cs_n <= cs_n;
end
//sck:spi工作时钟
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
sck <= 1'b0;
else if(sck_cnt == 2'd1)
sck <= 1'b1;
else if(sck_cnt == 2'd3)
sck <= 1'd0;
else
sck <= sck;
end
//mosi:主机输出RD_ID指令到从机
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
mosi <= 1'b0;
else if(byte_cnt == 2'd0 && clk_cnt == 5'd31)
mosi <= 1'b0;
else if(curr_state == WR_INST)
mosi <= DEVICE_ID[7 - bit_cnt];
else
mosi <= mosi;
end
//miso_r:对miso数据进行移位存储
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
miso_r <= 8'd0;
else if(miso_flag == 1'b1)
miso_r <= {miso_r[6:0],miso};
else
miso_r <= miso_r;
end
//miso_flag:移位标志信号产生
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
miso_flag <= 1'b0;
else if(curr_state == RD_ID && sck_cnt == 2'd1)
miso_flag <= 1'b1;
else
miso_flag <= 1'b0;
end
//recv_flag:读出设备ID有效信号
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
recv_flag <= 1'b0;
else if(curr_state == RD_ID && clk_cnt == 5'd30)
recv_flag <= 1'b1;
else
recv_flag <= 1'b0;
end
//recv_data:读出的设备ID数据
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
recv_data <= 8'd0;
else if(recv_flag == 1'b1)
recv_data <= miso_r;
else
recv_data <= recv_data;
end
endmodule
按键消抖模块key_filter如下:
`timescale 1ns/1ps
module key_filter(
input clk,
input rst_n,
input key_in, //按键输入
output reg key_flag //检测到按键按下标志信号
);
localparam CNT_MAX = 999_999; //计数最大值时,时间为20ms
reg [19:0] clk_cnt; // 系统时钟计数器
//clk_cnt:系统时钟计数器
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_cnt <= 20'd0;
else if(key_in == 1'b1)
clk_cnt <= 20'd0;
else if(key_in == 1'b0 && clk_cnt < CNT_MAX)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= clk_cnt;
end
//key_flag:检测到按键按下标志信号
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
key_flag <= 1'b0;
else if(clk_cnt == CNT_MAX - 1'b1)
key_flag <= 1'b1;
else
key_flag <= 1'b0;
end
endmodule
顶层完成模块例化spi_rdid_top:
`timescale 1ns/1ps
module spi_rdid_top(
input sys_clk, //系统时钟输入50MHz
input rst_n, //系统复位
input key_in, //按键输入
input miso, //spi主入从出信号
output cs_n, //spi片选信号
output sck, //spi读写时钟信号
output mosi //spi主出从入信号
);
wire key_flag; //按键按下表示信号
//例化spi_rdid模块
spi_rdid u_spi_rdid(
.clk(sys_clk),
.rst_n(rst_n),
.key_in(key_flag),
.miso(miso),
.cs_n(cs_n),
.sck(sck),
.mosi(mosi)
);
//例化key_filter模块
key_filter u_key_filter(
.clk(sys_clk),
.rst_n(rst_n),
.key_in(key_in),
.key_flag(key_flag)
);
endmodule
仿真代码如下:
`timescale 1ns/1ps
module spi_rdid_tb();
reg sys_clk;
reg rst_n;
reg key_in;
wire miso;
wire cs_n;
wire sck;
wire mosi;
initial
begin
sys_clk = 1'b1;
rst_n = 1'b0;
key_in = 1'b1;
#200
rst_n = 1'b1;
#100
key_in = 1'b0;
#21_000_000;
key_in = 1'b1;
end
always #10 sys_clk <= ~sys_clk;
m25p16 memory (
.c (sck ),
.data_in (mosi ),
.s (cs_n ),
.w (1'b1 ),
.hold (1'b1 ),
.data_out (miso )
);
spi_rdid_top u_spi_rdid_top(
.sys_clk(sys_clk),
.rst_n(rst_n),
.key_in(key_in),
.miso(miso),
.cs_n(cs_n),
.sck(sck),
.mosi(mosi)
);
endmodule
仿真结果如下:
仿真结果如上图所示,在curr_state为WR_ISNT(010)时,通过mosi向spi_flash发送读ID指令8’b1001_1111,然后curr_state跳转到RD_ID(100)状态,此时FPGA通过miso接收flash传入的设备ID标识数据,通过recv_data将接收的8位串行数据寄存,接收的3个字节数据依次为8’h20、8’h20、8’h15,与手册上数据一致。
2.3 板级验证
将代码下载到cyclone IV开发板中,通过singal tap抓取fpga接收到的数据,抓取cs_n下降沿,可以看到当miso将读指令8’b1001_1111写入spi_flash后,flash的设备ID标识数据通过miso传输到FPGA,FPGA接收到的miso数据依次为8’h20、8’h20、8’h15。