17 SPI FLASH读写

SPI 协议简介

SPI 即 Serial Periphera linterface 的缩写,顾名思义就是串行外围设备接口,主要用于与FLASH、实时时钟、AD 转换器等外设模块的通信,它是一种高速的全双工同步的通信总线。
SPI 设备分为主设备和从设备,SPI 通信必须由主设备发起,主设备通过片选引脚(CSn)来选择对应的从设备,通过时钟引脚(SCK)向从设备提供时钟,通过数据输出引脚(MOSI)引脚向从设备发送数据,通过数据输入引脚(MISO)引脚来读取从设备返回的数据,如下是 SPI 的总线拓扑图(分别是1个主设备对应1个从设备和1个主设备对应多个从设备):
在这里插入图片描述
在这里插入图片描述

SPI 时序

通过 SPI 的时钟极性(CPOL)和相位( CPHA)可以组合出4种工作模式,如下表所示是对应的4种工作模式:
在这里插入图片描述

  1. 模式0(CPOL = 0, CPHA = 0)
    CPOL = 0:空闲时是低电平,第一个跳变沿是上升沿,第二个跳变沿是下降沿
    CPHA = 0:数据在第一个跳变沿(上升沿)采样
    在这里插入图片描述
  2. 模式1(CPOL = 0, CPHA = 1)
    CPOL = 0:空闲时是低电平,第一个跳变沿是上升沿,第二个跳变沿是下降沿
    CPHA = 1:数据在第二个跳变沿(下降沿)采样
    在这里插入图片描述
  3. 模式2(CPOL = 1, CPHA = 0)
    CPOL = 1:空闲时是高电平,第一个跳变沿是下降沿,第二个跳变沿是上升沿
    CPHA = 0:数据在第一个跳变沿(下降沿)采样
    在这里插入图片描述
  4. 模式3(CPOL = 1, CPHA = 1)
    CPOL = 1:空闲时是高电平,第一个跳变沿是下降沿,第二个跳变沿是上升沿
    CPHA = 1:数据在第二个跳变沿(上升沿)采样
    在这里插入图片描述

FLASH 简介

实验使用的 FLASH 芯片型号为W25Q128,它是华邦公司推出的一款的 NOR FIash 芯片,其存储空间为128 Mbit,相当于16MB,支持 Standard SPI、Dual SPI 和 Quad SPI 三种 SPI 协议通信方式,最大传输数据速率可达104MHZ,如下是芯片的引脚示意图:
在这里插入图片描述
在这里插入图片描述

存储结构

整个存储阵列被分成被为 256 给我块,每个块又分为 16 个扇区,每个扇区由分为 16 个页,每页包含 256 个字节。
在这里插入图片描述

操作指令

在这里插入图片描述在这里插入图片描述
FLASH 操作指令有很多,其中常用的有读ID指令(0x9F)、写使能指令(0x06)、扇区擦除指令(每次擦除4KB,0x20)、全擦除指令(0xC7)、读指令(0x03)、写指令(又叫页编程,0x02)、读状态寄存器1指令(0x05)。

FLASH 操作时序

  1. 读ID指令
    在操作 FLASH 之前应读取其ID,校验 FLASH 型号,如下是读 ID 的时序:
    在这里插入图片描述
  2. 写使能指令
    在进行擦除、写入操作前需要先发送写使能指令,其时序如下:
    在这里插入图片描述
  3. 扇区擦除指令
    W25Q128在写数据时只能将1修改为0,所以在写入新数据之前必须要进行擦除操作,将FLASH中的存储单元全部设置为1,如下是扇区擦除时序:
    在这里插入图片描述
  4. 全擦除指令
    除了扇区擦除指令外,还有全擦除指令,其时序如下:
    在这里插入图片描述
  5. 读指令
    读取 FLASH 一次可以读取多个字节,其时序如下:
    在这里插入图片描述
  6. 写指令
    向 FLASH 写入数据时不能跨页(页大小256B),若需要写入多页或者写入数据跨页则需要分多次写入,如下是写入数据的时序:
    在这里插入图片描述
  7. 读状态寄存器1指令
    在进行擦除、写入操作后需要轮询状态寄存器0的bit0,以检查擦除或写入操作是否结束(全擦除指令耗时很长),如下是读状态寄存器的时序:
    在这里插入图片描述

硬件设计

FLASH 芯片的硬件原理图较为简单,芯片本身就 8 个引脚,其原理图如下:
在这里插入图片描述

代码编写

代码一共分为3个模块,分别是 SPI 驱动模块、FLASH 驱动模块、FLASH 读写测试模块,其功能如下:
SPI 驱动模块;提供 SPI 总线收发数据的功能。
FLASH 驱动模块;基于 SPI 驱动模块实现 FLASH 的一些基本操作,如读 ID、写使能、写、擦除、读、读状态寄存器等。
FLASH 读写测试模块;利用 FLASH 驱动模块提供的 FLASH 基本操作进行 FLASH 读ID、擦除、写、读等测试,测试过程中状态 LED 常灭,测试出错状态 LED闪烁,测试完成状态 LED 常亮。

SPI 驱动模块

module spi_driver #(
	parameter SPI_CS_MAX = 1,					//片选数量
	parameter SPI_BITS = 8,						//SPI位宽
	parameter SPI_CLK_PERIOD = 4,				//SPI时钟周期,以系统时钟为参考,最小为4
	parameter SPI_MODE = 0,						//SPI模式
	parameter ALMOST_DONE_ADVANCE = 0,			//即将传输完成信号提前输出时间,小于SPI_CLK_PERIOD-1
	parameter ALMOST_IDLE_ADVANCE = 0			//即将空闲信号提前输出时间,小于SPI_CLK_PERIOD-1
)
(
	input sys_rst_n,							//系统复位
	input sys_clk,								//系统时钟

	input [SPI_BITS-1:0] tx_data,				//需要发送的数据
	input tr_start,								//启动传输

	output reg [SPI_BITS-1:0] rx_data,			//接收到的数据
	output reg tr_done,							//传输完成
	output reg almost_tr_done,					//传输即将完成

	output tr_idle,								//SPI空闲
	output reg almost_tr_idle,					//SPI即将空闲

	input [SPI_CS_MAX-1:0] sel_cs,				//片选设置

	output spi_clk,								//SPI时钟
	output spi_mosi,							//SPI MOSI
	input spi_miso,								//SPI MISO
	output [SPI_CS_MAX-1:0] spi_cs				//SPI片选
);

//SPI时钟周期,只能是偶数分频
localparam CLK_PERIOD = (SPI_CLK_PERIOD / 2 * 2);

//spi传输忙标志
reg tr_busy;

//SPI时钟周期计数器,按SPI时钟周期进行计数
reg [15:0] clk_period_count;

//传输bit计数
reg [7:0] bit_cnt;

//发送移位寄存器
reg [SPI_BITS-1:0] tx_shift_reg;
//接收移位寄存器
reg [SPI_BITS-1:0] rx_shift_reg;


//空闲标志
assign tr_idle = ~tr_busy;

//启动SPI传输
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		tr_busy <= 1'b0;
	else if((tr_start == 1'b1) && (tr_busy == 1'b0))
		tr_busy <= 1'b1;
	else if((tr_busy == 1'b1) && (bit_cnt == (SPI_BITS - 1)) && (clk_period_count == (CLK_PERIOD - 1)))
		tr_busy <= 1'b0;
end

//SPI即将空闲
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		almost_tr_idle <= 1'b1;
	else if((tr_start == 1'b1) && (almost_tr_idle == 1'b1))
		almost_tr_idle <= 1'b0;
	else if((almost_tr_idle == 1'b0) && (bit_cnt == (SPI_BITS - 1)) && (clk_period_count == (CLK_PERIOD - 1 - ALMOST_IDLE_ADVANCE)))
		almost_tr_idle <= 1'b1;
end

//按SPI时钟周期进行计数
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		clk_period_count <= 0;
	else if(tr_busy == 1'b1)
		if(clk_period_count < (CLK_PERIOD - 1))
			clk_period_count <= clk_period_count + 1;
		else
			clk_period_count <= 0;
	else
		clk_period_count <= 0;
end

//进行传输计数
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		bit_cnt <= 0;
	else if(tr_busy == 1'b1) begin
		if(clk_period_count == (CLK_PERIOD - 1)) begin
			if(bit_cnt < (SPI_BITS - 1))
				bit_cnt <= bit_cnt + 1;
		end
	end
	else
		bit_cnt <= 0;
end

//输出接收到的数据,接收完最后1bit时输出
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		rx_data <= 0;
		tr_done <= 0;
	end
	else if((tr_busy == 1'b1) && (bit_cnt == (SPI_BITS - 1)) && (clk_period_count == (CLK_PERIOD - 1))) begin
		rx_data <= rx_shift_reg;
		tr_done <= 1;
	end
	else
		tr_done <= 0;
end

//输出传输完成预告
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		almost_tr_done <= 0;
	else if((tr_busy == 1'b1) && (bit_cnt == (SPI_BITS - 1)) && (clk_period_count == (CLK_PERIOD - 1 - ALMOST_DONE_ADVANCE)))
		almost_tr_done <= 1;
	else
		almost_tr_done <= 0;
end

generate
	if(SPI_MODE == 0) begin
		//输出SPI时钟,模式0
		//时钟默认为低电平,工作时前半段为低电平,后半段为高电平
		assign spi_clk = ((tr_busy == 1'b1) && (clk_period_count > (CLK_PERIOD / 2 - 1))) ? 1'b1 : 1'b0;
	end
	else if(SPI_MODE == 1) begin
		//输出SPI时钟,模式1
		//时钟默认为低电平,工作时前半段为高电平,后半段为低电平
		assign spi_clk = ((tr_busy == 1'b1) && (clk_period_count <= (CLK_PERIOD / 2 - 1))) ? 1'b1 : 1'b0;
	end
	else if(SPI_MODE == 2) begin
		//输出SPI时钟,模式2
		//时钟默认为高电平,工作时前半段为高电平,后半段为低电平
		assign spi_clk = ((tr_busy == 1'b1) && (clk_period_count > (CLK_PERIOD / 2 - 1))) ? 1'b0 : 1'b1;
	end
	else begin
		//输出SPI时钟,模式3
		//时钟默认为高电平,工作时前半段为高电平,后半段为低电平
		assign spi_clk = ((tr_busy == 1'b1) && (clk_period_count <= (CLK_PERIOD / 2 - 1))) ? 1'b0 : 1'b1;
	end
endgenerate

//启动时锁定数据,随后进行发送数据移位操作
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		tx_shift_reg <= (1 << SPI_BITS) - 1;
	else if((tr_start == 1'b1) && (tr_busy == 1'b0))
		tx_shift_reg <= tx_data;
	else if((tr_busy == 1'b1) && (bit_cnt < (SPI_BITS - 1)) && (clk_period_count == (CLK_PERIOD - 1)))
		tx_shift_reg <= {
   tx_shift_reg[6:0], tx_shift_reg[7]};
end
//输出SPI MOSI
assign spi_mosi = tx_shift_reg[7];

//采样SPI MOSI
//在时钟周期的3/4处采样
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		rx_shift_reg <= 0;
	else if(tr_busy == 1'b1) begin
		if(clk_period_count == (CLK_PERIOD - CLK_PERIOD / 4 - 1))
			rx_shift_reg = {
   rx_shift_reg[6:0], spi_miso};
	end
	else
		rx_shift_reg <= 0;
end

//输出片选信号
assign spi_cs = sel_cs;

endmodule

FLASH 驱动模块

module flash_driver  #(
	parameter SPI_CLK_PERIOD = 4,
	parameter SPI_CS_DELAY = 500,
	parameter DATA_REQ_ADVANCE = 1
)
(
	input sys_rst_n,
	input sys_clk,

	input flash_start,
	input [7:0] flash_cmd,
	input [24:0] flash_addr,

	input [8:0] wr_data_len,
	input [7:0] wr_data,
	output wr_data_req,

	input [8:0] rd_data_len,
	output [7:0] rd_data,
	output rd_data_flag,

	output reg [23:0] flash_id,
	output reg flash_id_flag,

	output reg [7:0] flash_sr_reg,
	output reg flash_sr_reg_flag,

	output flash_idle,

	output spi_clk,
	output spi_mosi,
	input spi_miso,
	output spi_cs
);

//状态机的状态
localparam IDLE_STATE = 8'h01;			//空闲状态
localparam RDID_STATE = 8'h02;			//读FLASH ID状态
localparam WREN_STATE = 8'h04;			//写使能状态
localparam SSE_STATE = 8'h08;			//子扇区擦除状态
localparam BE_STATE = 8'h10;			//全擦除
localparam READ_STATE = 8'h20;			//读状态
localparam PP_STATE = 8'h40;			//写状态(页编程)
localparam RDSR_STATE = 8'h80;			//读状态寄存器状态

//指令集
localparam RDID_CMD = 8'h9f;			//读ID指令
localparam WREN_CMD = 8'h06;			//写使能指令
localparam SSE_CMD = 8'h20;				//子扇区擦除指令
localparam BE_CMD = 8'hc7;				//全擦除指令
localparam READ_CMD = 8'h0b;			//读指令
localparam PP_CMD = 8'h02;				//写指令(页编程)
localparam RDSR_CMD = 8'h05;			//读状态寄存器指令

//传输计数
reg [15:0] tx_count;
reg [15:0] rx_count;

//当前状态
reg [7:0] current_state;
//下一刻的状态
reg [7:0] next_state;
//当前状态结束标志,切换到下一个状态
reg state_done;

//片选前延时
reg [16:0] cs_front_delay_count;
//片选后延时
reg [16:0] cs_back_delay_count;
//指令操作延时,部分命令发送完成后需要延时
reg [16:0] cmd_back_ddelay_count;

//SPI需要发送的数据
reg [7:0] spi_tx_data;
//启动SPI发送
reg spi_tr_start;
//SPI片选控制
reg spi_ctrl_cs;

//SPI接收到的数据
wire [7:0] spi_rx_data;
//SPI传输完成
wire spi_tr_done;
//SPI即将传输完成
wire spi_almost_tr_done;

//SPI空闲
wire spi_tr_idle;
//SPI即将空闲
wire spi_almost_tr_idle;

//状态跳转
always @(posedge sys_clk)begin
	if(!sys_rst_n)
		current_state <= IDLE_STATE;
	else
		current_state <= next_state;
end

//根据当前状态确定下一刻状态
always @(*)begin
	case(current_state)
		IDLE_STATE: begin
			if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == RDID_CMD))
				next_state = RDID_STATE;
			else if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == WREN_CMD))
				next_state = WREN_STATE;
			else if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == SSE_CMD))
				next_state = SSE_STATE;
			else if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == BE_CMD))
				next_state = BE_STATE;
			else if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == READ_CMD))
				next_state = READ_STATE;
			else if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == PP_CMD))
				next_state = PP_STATE;
			else if((state_done == 1'b0) && (flash_start == 1'b1) && (flash_cmd == RDSR_CMD))
				next_state = RDSR_STATE;
			else
				next_state = IDLE_STATE;
		end
		RDID_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = RDID_STATE;
		end
		WREN_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = WREN_STATE;
		end
		SSE_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = SSE_STATE;
			end
		BE_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = BE_STATE;
		end
		READ_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = READ_STATE;
		end
		PP_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = PP_STATE;
		end
		RDSR_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = RDSR_STATE;
		end
		default:
			next_state = IDLE_STATE;
	endcase
end

//空闲标志输出
assign flash_idle = ((current_state == IDLE_STATE) && (state_done == 1'b0)) ? 1'b1 : 1'b0;

//进行计数器计数,输出延时信号
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		cs_front_delay_count <= 0;
		cs_back_delay_count <= 0;
		cmd_back_ddelay_count <= 0;
		state_done <= 1'b0;
	end
	else begin
		case(current_state)
			IDLE_STATE: begin
				cs_front_delay_count <= 0;
				cs_back_delay_count <= 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值