目录
一、SPI通信协议
1.1 SPI物理层
1.2 SPI协议层
二、实战
2.1 SPI控制FLASH实现全擦除代码编写
2.2 上板验证
SPI通信模式为主-从模式 ,分为一主一从、一主多从:
片选线CS用于主机选择对应的从机进行通信,片选线置低电平为通信开始信号,被拉高则为开始信号。
SPI协议有四种通信模式(通过CPOL和CPHA控制),
其中CPOL控制当没有数据传输的时候SCK的电平状态;CPHA控制数据采样位置,为0的时候位置为奇数边沿,为1的时候为偶数边沿。
下图传输为高位在前,先传输高位,每次传输数据的个数没有限制。
程序的固化流程:
生成jic文件:
选择生成好的jic文件烧录:
固化后的程序即使断电也不会丢失,如果想去除flash中的固化程序,可以进行全擦除操作。
第一种方法可以到下载器中进行全擦除:
第二种方法就是我们要讲的,自己编写程序对flash芯片进行全擦除。
首先打开flash芯片的手册:
找到全擦除对应的指令BE(Bulk Erase):
然后找到BE指令的介绍:
从介绍里面可以知道:
然后查看一下写使能指令的信息:
还有一个要注意的地方就是输入时序的问题,tSLCH、tCHSH、tSHSL这三个时间要注意:
tSLCH为从片选信号拉低到第一个bit到来的时间间隔,最小值为5ns(如下图)。
tCHSH为从最后一个bit写入完成到片选信号拉高的时间间隔,最小值为5ns。
tSHSL为两个指令之间需要等待的时间,最小值为100ns。
基于上面的内容我们可以得到我们实验的时序图:
实验顶层模块框图:
内部结构:
按键消抖子模块(为了简单这个按键消抖模块我就不做了,直接使用key_in控制):
全擦除控制子模块:
全擦除控制模块波形:
flash_be_ctrl模块verilog代码:
module flash_be_ctrl(
input wire sys_clk ,
input wire sys_rst_n ,
input wire key_in , //按键信号
output reg cs_n , //片选信号,低电平有效
output reg sck , //spi通信时钟信号,采用的是12.5Hz
output reg mosi //主机输出的数据
);
parameter WREN_INS = 8'b0000_0110;
parameter BE_INS = 8'b1100_0111;
reg key_en;
reg key_flag;
reg [2:0] state; //状态机,0为闲置状态
reg [4:0] cnt_clk;
reg [2:0] cnt_byte;
reg [1:0] cnt_sck;
reg [2:0] cnt_bit;
//检测按键是否按下,生成key_flag信号,并且只检测一次
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_en <= 1'b1;
key_flag <= 1'b0;
end
else if(key_in == 1'b0 && key_en == 1'b1) begin
key_en <= 1'b0;
key_flag <= 1'b1;
end
else begin
key_en <= key_en;
key_flag <= 1'b0;
end
end
//cs_n片选信号
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
cs_n <= 1'b1;
end
else if(key_flag == 1'b1 || (cnt_clk == 5'd31 && cnt_byte == 3'd3)) begin
cs_n <= 1'b0; //拉低
end
else if((cnt_clk == 5'd31 && cnt_byte == 3'd2) || (cnt_clk == 5'd31 && cnt_byte == 3'd6)) begin
cs_n <= 1'b1; //拉高
end
else begin
cs_n <= cs_n;
end
end
//cnt_clk计数信号
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
cnt_clk <= 5'd0;
end
else if(cnt_clk == 5'd31 || state == 3'd0) begin
cnt_clk <= 5'd0;
end
else begin
cnt_clk <= cnt_clk + 1'b1;
end
end
//cnt_byte计数信号
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
cnt_byte <= 3'd0;
end
else if(cnt_byte == 3'd6 && cnt_clk == 5'd31) begin
cnt_byte <= 3'd0;
end
else if(cnt_clk == 5'd31) begin
cnt_byte <= cnt_byte + 1'b1;
end
else begin
cnt_byte <= cnt_byte;
end
end
//cnt_sck计数信号
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
cnt_sck <= 2'd0;
end
else if(cnt_byte == 3'd1 || cnt_byte == 3'd5) begin
cnt_sck <= cnt_sck + 1'b1;
end
else begin
cnt_sck <= 2'd0;
end
end
//cnt_bit计数信号
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
cnt_bit <= 3'd0;
end
else if(cnt_byte != 3'd1 && cnt_byte != 3'd5) begin
cnt_bit <= 3'd0;
end
else if(cnt_sck == 2'd2) begin
cnt_bit <= cnt_bit + 1'b1;
end
else begin
cnt_bit <= cnt_bit;
end
end
//state状态机
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
state <= 3'd0; //IDLE
end
else if(key_flag == 1'd1) begin
state <= 3'd1; //WREN
end
else if(cnt_clk == 5'd31 && cnt_byte == 3'd2) begin
state <= 3'd2; //DELAY
end
else if(cnt_clk == 5'd31 && cnt_byte == 3'd3) begin
state <= 3'd3; //BE
end
else if(cnt_clk == 5'd31 && cnt_byte == 3'd6) begin
state <= 3'd0; //IDLE
end
else begin
state <= state;
end
end
//输出sck时钟信号
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
sck <= 1'b0;
end
else if(cnt_sck == 2'd2 || (cnt_sck == 2'd0 && sck == 1'b1)) begin
sck <= ~sck;
end
else begin
sck <= sck;
end
end
//输出mosi数据
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
mosi <= 1'b0;
end
else if(cnt_byte != 3'd1 && cnt_byte != 3'd5) begin
mosi <= 1'b0;
end
else if(cnt_byte == 3'd1 && cnt_sck == 2'd0) begin
//WREN信号
case(cnt_bit)
3'd0: mosi <= WREN_INS[7];
3'd1: mosi <= WREN_INS[6];
3'd2: mosi <= WREN_INS[5];
3'd3: mosi <= WREN_INS[4];
3'd4: mosi <= WREN_INS[3];
3'd5: mosi <= WREN_INS[2];
3'd6: mosi <= WREN_INS[1];
3'd7: mosi <= WREN_INS[0];
endcase
end
else if(cnt_byte == 3'd5 && cnt_sck == 2'd0) begin
//BE信号
case(cnt_bit)
3'd0: mosi <= BE_INS[7];
3'd1: mosi <= BE_INS[6];
3'd2: mosi <= BE_INS[5];
3'd3: mosi <= BE_INS[4];
3'd4: mosi <= BE_INS[3];
3'd5: mosi <= BE_INS[2];
3'd6: mosi <= BE_INS[1];
3'd7: mosi <= BE_INS[0];
endcase
end
else begin
mosi <= mosi;
end
end
endmodule
testbench代码:
module tb_flash_be_ctrl();
reg sys_clk;
reg sys_rst_n;
reg key_in;
wire cs_n;
wire sck;
wire mosi;
always #10 sys_clk = ~sys_clk;
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
key_in <= 1'b1;
#20
sys_rst_n <= 1'b1;
end
initial begin
#200
key_in <= 1'b0;
#100
key_in <= 1'b1;
end
flash_be_ctrl flash_be_ctrl_inst(
.sys_clk(sys_clk) ,
.sys_rst_n(sys_rst_n) ,
.key_in(key_in) , //按键信号
.cs_n(cs_n) , //片选信号,低电平有效
.sck(sck) , //spi通信时钟信号,采用的是12.5Hz
.mosi(mosi) //主机输出的数据
);
endmodule
仿真波形:
如果想额外添加flash仿真文件可以在如下窗口添加:
代码编写完成后我们就可以上板验证了。
各个端口绑定的引脚如下:
下面介绍一下怎么找到对应的引脚。首先找到flash的原理图:
FLASH_NCE为片选信号,对应的端口为D2:
key_in信号对应M1端口:
并且按下时为低电平:
EPCS_ASDO为flash芯片的输入mosi,对应引脚C1:
EPCS_CLK为传入flash芯片的同步时钟信号sck,对应引脚为H1:
其余两个是系统时钟和系统复位信号,这个我就不作说明了。
配置好引脚后,重新编译,发现报错信息:
这些要修改io口的配置:
改成如下内容:
三个位置对应三个错误,改完后重新编译通过。
然后就是上板测试,首先要烧录一个测试程序到板子上(要进行固化),然后再烧录我们的spi全擦除程序到板子上,烧录完成后按下key1,等待一段时间后再重新上电,重新上电后就可以看到之前固化的程序消失了,全擦除实现成功!