FLASH存储芯片(W25Q64)
背景
最近一直都在出差,断断续续抽了一周的时间才调通了FLASH存储芯片。在SPI通讯代码没问题的情况下,整体FLASH芯片驱动的代码编写以及调试难度属于中等,其中主要困难点在于W25Q64芯片的资料手册理解。我即使是到现在也仍旧还有一个疑问尚未解决,还希望大神解答。
代码功能介绍
功能实现flash扇区擦除、flash单byte读、flash单byte写:
1、FLASH擦除–完成FLASH指定地址扇区擦除
2、FLASH写入–完成FLASH指定地址的数据写入
3、FLASH读出–完成FLASH指定地址的数据读取
同时,为了方便观察代码调试,我们使用了VIO(Virtual INPUT OUTPUT)IP核以及ILA IP核参与调试。
软硬件配置
FLASH芯片:W25Q64
FPGA芯片:XC7A35T
编程工具:Vivado 2018.3
调试工具:串口调试助手软件
具体代码
flash_operation_top模块
/
//
// flash操作顶层
// 功能实现flash扇区擦除、flash单byte读、flash单byte写
// FLASH擦除--读状态寄存器、发送擦除指令+24位地址、读状态寄存器确认完成
// FLASH编程--读状态寄存器、发送写使能指令、发送编程指令+24位地址数据+需写入数据、读状态寄存器
// FLASH读出--读状态寄存器、发送读指令+24位地址数据
module flash_operation_top(
input clk,
input rst_n,
input flash_start,
input [7:0] flash_order, //flash需完成动作指令
input [7:0] need_write_data, //需要保存的数据
input [23:0] flash_address, //数据地址
input miso,
output sclk,
output cs,
output mosi,
output reg [7:0] read_flash_data,
output reg full_order_done //flash操作完毕
);
//
//
// 操作指令集
//
//
parameter WRITE_ORDER = 8'h02,
READ_ORDER = 8'h03,
BLOCK_ERASE_ORDER = 8'hD8, //目前代码并未匹配块擦除指令
SECTOR_ERASE_ORDER = 8'h20,
WRITE_ENABLE_ORDER = 8'h06,
READ_REGISTER_ORDER = 8'h05;
//
//
// 检测SPI发送与接收完成
//
//
wire spi_tx_done;
wire spi_rx_done;
wire spi_tx_done_pos;
wire spi_rx_done_pos;
reg spi_tx_done_0;
reg spi_tx_done_1;
reg spi_rx_done_0;
reg spi_rx_done_1;
assign spi_rx_done_pos = spi_rx_done_0 && !spi_rx_done_1;
assign spi_tx_done_pos = spi_tx_done_0 && !spi_tx_done_1;
always @(posedge clk, negedge rst_n) begin
if(!rst_n) begin
spi_tx_done_0 <= 'd0;
spi_tx_done_1 <= 'd0;
spi_rx_done_0 <= 'd0;
spi_rx_done_1 <= 'd0;
end else begin
spi_tx_done_0 <= spi_tx_done;
spi_tx_done_1 <= spi_tx_done_0;
spi_rx_done_0 <= spi_rx_done;
spi_rx_done_1 <= spi_rx_done_0;
end
end
//
//
// 启动FLASH操作
//
//
reg START;
always @(posedge clk, negedge rst_n) begin
if(!rst_n) begin
START <= 1'd0;
end else begin
if(flash_start) begin
START <= 1'd1;
end else begin
if(full_order_done) begin
START <= 1'd0;
end
end
end
end
//
//
// 指令发送状态机
//
//
parameter IDLE = 4'd0, //该状态下读取写保护位(S1)以及忙位(S0)
WRITE_ENABLE = 4'd1, //进行写使能
ERASE_STATE = 4'd2, //进行擦除操作--需先写使能
WRITE_STATE = 4'd3, //进行数据写入--需先写使能、擦除再写使能
READ_STATE = 4'd4; //读指令无需写使能
reg [3:0] current_state;
reg WEL_FLAG; //写保护位
reg BUSY_FLAG; //忙标志位
reg read_state_done; //读取状态寄存器完毕标志位
reg flash_en; //FLASH使能
reg done_tx_order; //已完成flash指令以及地址发送
reg done_tx_flash; //仅完成flash指令发送
reg operation_done; //子指令完成
reg ready_to_write; //已完成擦除,并再次进入写使能,可开始写数据
always @(posedge clk, negedge rst_n) begin
if(!rst_n) begin
current_state <= IDLE;
end else begin
case(current_state)
IDLE : begin
if(BUSY_FLAG && read_state_done) begin
current_state <= IDLE; //当处于忙时,只能执行读状态器指令
end else begin
if(read_state_done) begin
if(flash_order == WRITE_ENABLE_ORDER) begin
current_state <= WRITE_ENABLE;
end else begin
if(WEL_FLAG) begin
if(flash_order == READ_ORDER) begin
current_state <= READ_STATE;
end else begin
if(flash_order == WRITE_ORDER || flash_order == SECTOR_ERASE_ORDER) begin
if(ready_to_write) begin
current_state <= WRITE_STATE;
end else begin
if(erase_done_flag && flash_order == WRITE_ORDER) begin
current_state <= WRITE_ENABLE;
end else begin
if(flash_order == SECTOR_ERASE_ORDER && !done_enable) begin
current_state <= WRITE_ENABLE;
end else begin
current_state <= ERASE_STATE;
end
end
end
end
end
end else begin
if(flash_order == READ_ORDER) begin
current_state <= READ_STATE;
end else begin
current_state <= WRITE_ENABLE;
end
end
end
end
end
end
WRITE_ENABLE: begin //当写使能完成,需返回IDLE状态进行读状态寄存器
if(done_tx_order) begin
current_state <= IDLE;
end
end
ERASE_STATE : begin //当完成擦除,仍需返回IDLE状态进行读状态寄存器
if(operation_done) begin
current_state <= IDLE;
end
end
WRITE_STATE : begin
if(operation_done) begin
current_state <= IDLE;
end
end
READ_STATE: begin
if(operation_done) begin
current_state <= IDLE;
end
end
default: current_state <= IDLE;
endcase
end
end
wire [7:0] spi_rx_data;
reg start_read_flag; //可以开始读数据标志
reg [23:0] TEM_flash_address; //方便地址发送
reg [2:0] address_send_cnter; //发送地址计数器
reg [7:0] spi_flash_order; //待发送flash命令
reg erase_done_flag; //擦除完成标志位
reg done_enable;
always @(posedge clk, negedge rst_n) begin
if(!rst_n) begin
WEL_FLAG <= 1'd0; //掉电后、执行完指令
BUSY_FLAG <= 1'd0;
read_state_done <= 1'd0;
start_read_flag <= 1'd0;
flash_en <= 1'b0;
TEM_flash_address <= 24'd0;
address_send_cnter <= 3'd0;
done_tx_order <= 'b0;
done_tx_flash <= 'b0;
spi_flash_order <= 'd0;
operation_done <= 'd0;
erase_done_flag <= 'd0;
full_order_done <= 'd0; //外部需操作指令完成
ready_to_write <= 'd0;
done_enable <= 1'b0; //完成写使能指令
end else begin
case(current_state)
IDLE : begin
done_tx_flash <= 1'b0;
operation_done <= 'd0;
spi_flash_order <= 'd0;
full_order_done <= 'd0;
spi_flash_order <= READ_REGISTER_ORDER;
if(START) begin
flash_en <= 1'b1;
if(spi_rx_done_pos) begin
start_read_flag <= 1'd1; //可以开始读取flash返回数据
end else begin
if(receive_1byte_flag) begin
BUSY_FLAG <= flash_return_data[0];
WEL_FLAG <= flash_return_data[1];
start_read_flag <= 1'd0;
flash_en <= 1'b0;
read_state_done <= 1'b1;
end else begin
read_state_done <= 1'b0;
end
end
end else begin
flash_en <= 1'b0;
end
end
WRITE_ENABLE: begin
flash_en <= 1'b1;
operation_done <= 1'b0;
done_tx_order <= 1'b0;
spi_flash_order <= WRITE_ENABLE_ORDER;
read_state_done <= 1'b0;
if(spi_rx_done_pos) begin
flash_en <= 1'b0;
done_tx_order <= 1'b1;
done_enable <= 1'b1; //已经完成写使能
if(erase_done_flag) begin //已经完成过擦除、可进行写操作
ready_to_write <= 'd1;
end
end
end
ERASE_STATE : begin
done_tx_order <= 1'b0;
read_state_done <= 1'b0;
if(spi_rx_done_pos) begin
full_order_done <= 'd0;
done_tx_flash <= 1'b1; //当指令发送完成后,即发送地址
erase_done_flag <= 'd0; //已完成擦除标志位(当FLASH已完成擦除,write指令即可开始执行)
if(address_send_cnter <= 2) begin
spi_flash_order <= TEM_flash_address[23:16]; //还需发送24位地址
address_send_cnter <= address_send_cnter + 1'b1;
TEM_flash_address <= {TEM_flash_address[15:0],TEM_flash_address[23:16]};
end else begin
address_send_cnter <= 'b0;
done_tx_order <= 'b1;
operation_done <= 'd1;
flash_en <= 1'b0;
done_tx_flash <= 1'b0;
done_enable <= 1'b0;
if(flash_order == SECTOR_ERASE_ORDER) begin
full_order_done <= 'd1;
erase_done_flag <= 'd0; //此处的erase_done_flag仅用于页编程指令的后续状态跳转
end else begin
full_order_done <= 'd0;
erase_done_flag <= 'd1;
end
end
end else begin
if(!done_tx_flash) begin
TEM_flash_address <= flash_address;
full_order_done <= 'd0;
if(operation_done != 'd1) begin //由于状态跳转之前还会再次打一拍,故需对其进行处理
flash_en <= 1'b1;
spi_flash_order <= SECTOR_ERASE_ORDER;
end
end
end
end
WRITE_STATE : begin
done_tx_order <= 'b0;
erase_done_flag <= 'd0;
read_state_done <= 1'b0;
ready_to_write <= 'd0;
done_enable <= 1'b0;
if(spi_rx_done_pos) begin
full_order_done <= 'd0;
done_tx_flash <= 1'b1;
if(address_send_cnter <= 'd2) begin
spi_flash_order <= TEM_flash_address[23:16]; //还需发送24位地址
address_send_cnter <= address_send_cnter + 1'b1;
TEM_flash_address <= {TEM_flash_address[15:0],TEM_flash_address[23:16]};
end else begin
if(address_send_cnter == 'd3) begin //由于编程指令还需发送需要写的数据故多计数一次
spi_flash_order <= need_write_data; //后续可在此处进行修改使得模块能连续写
address_send_cnter <= address_send_cnter + 1'b1;
end else begin
flash_en <= 1'b0;
operation_done <= 'd1;
done_tx_order <= 'b1;
address_send_cnter <= 1'b0;
done_tx_flash <= 1'b0;
if(flash_order == WRITE_ORDER) begin
full_order_done <= 'd1;
end else begin
full_order_done <= 'd0;
end
end
end
end else begin
if(!done_tx_flash) begin
TEM_flash_address <= flash_address;
full_order_done <= 'd0;
if(full_order_done != 'd1) begin
flash_en <= 1'b1;
spi_flash_order <= WRITE_ORDER;
end
end
end
end
READ_STATE: begin
flash_en <= 1'b1;
done_tx_order <= 'b0;
read_state_done <= 1'b0;
if(spi_rx_done_pos) begin
done_tx_flash <= 1'b1;
if(address_send_cnter <= 'd2) begin
spi_flash_order <= TEM_flash_address[23:16]; //还需发送24位地址
address_send_cnter <= address_send_cnter + 1'b1;
TEM_flash_address <= {TEM_flash_address[15:0],TEM_flash_address[23:16]};
end else begin
spi_flash_order <= 8'b0;
start_read_flag <= 'd1;
end
end else begin
if(!done_tx_flash) begin
TEM_flash_address <= flash_address;
full_order_done <= 'd0;
spi_flash_order <= READ_ORDER;
end else begin
if(receive_1byte_flag) begin //后续可在此处进行修改使得模块能连续读
flash_en <= 1'b0;
operation_done <= 'd1;
done_tx_order <= 'b1;
address_send_cnter <= 'b0;
start_read_flag <= 'd0;
done_tx_flash <= 1'b0;
if(flash_order == READ_ORDER) begin
full_order_done <= 'd1;
end else begin
full_order_done <= 'd0;
end
end
end
end
end
endcase
end
end
//
//
// 读FLASH返回数据
// IDLE状态中读取状态寄存器的数据、读状态中读取flash存储数据
//
//
reg receive_1byte_flag; //接收到flash返回的1字节数据
reg [7:0] flash_return_data; //命令发送完毕,等待flash返回数据
always @(posedge clk, negedge rst_n) begin
if(!rst_n) begin
flash_return_data <= 'd0;
receive_1byte_flag <= 'd0;
end else begin
if(start_read_flag) begin
if(spi_rx_done_pos) begin
flash_return_data <= spi_rx_data;
receive_1byte_flag <= 'd1;
end
end else begin
receive_1byte_flag <= 'd0;
end
end
end
//
//
// SPI通讯顶层
// 发送、接收FLASH数据
//
//
spi_master flash_spi_inst(
.clk (clk),
.rst_n (rst_n),
.tx_data (spi_flash_order),
.spi_en (flash_en),
.miso (miso),
.mosi (mosi),
.cs (cs),
.sclk (sclk),
.tx_done (spi_tx_done),
.rx_done (spi_rx_done),
.TEM_rx_data (spi_rx_data)
);
//
//
// FLASH读取指令取出数据
//
//
always @(posedge clk) begin
if(flash_order == READ_ORDER && full_order_done) begin
read_flash_data <= flash_return_data;
end
end
//
//
// 调试用
//
//
/* ila_0 ila_inst(
.clk(clk),
.probe0(current_state),
.probe1({receive_1byte_flag,WEL_FLAG, BUSY_FLAG, start_read_flag, spi_rx_done_pos, operation_done, erase_done_flag}),
.probe2({full_order_done, sclk, miso, mosi, flash_en, cs, spi_rx_done, read_state_done}),
.probe3(read_flash_data),
.probe4(spi_flash_order),
.probe5(spi_rx_data)
); */
endmodule
代码总体难度不高,其实主要是芯片操作时序的理解。
状态寄存器(共8位–S7-S0)–指令为05H:
(S0)BUSY位(忙位)–当FLASH处于页编程、擦除、写状态寄存器等写指令时,此位为1表示FLASH不再处理除读状态寄存器指令之外的指令。
(S1)WEL位(写保护位)–在执行页编程、擦除指令之前写保护位都必须为1(进入写保护状态条件–此时WEL为0:1、FLASH上电;2、完成页编程、擦除指令之后)
写使能(06H)–在进行擦除、页编程指令之前都必须先进行写使能
写使能指令可以使写保护位置1
页编程(02H)–在进行页编程之前必须先进行擦除,由于W25Q64芯片数据只能由1变成0,故在写入数据之前必须先擦除,使得写入地址数据变为FF,即可开始正常写入数据。
FLASH写入流程(页编程02H)–读状态寄存器(05H)、写使能(06H)、扇区擦除(20H)+ 24位地址、写使能(06H)、读状态寄存器(05H)、页编程指令(02H)+ 24位地址 + 8位待写入数据
FLASH读取流程(读指令03H)–读状态寄存器(05H)、读指令(03H)+ 24位地址
FLASH擦除流程(扇区擦除20H)–读状态寄存器(05H)、扇区擦除(20H)+ 24位地址
注意:
1、若想连续写入数据,则在页编程指令后持续发送数据(页编程指令中最多一次送入256字节,即一页),片选持续选通。
2、若想连续读数据、则在读指令后片选持续选通。
Flash_top模块
`timescale 1ns / 1ps
//
// FLASH读写实验
// 功能点1:接收串口下发数据并将其写入FLASH指定地址
// 功能点2:读取指定FLASH地址的数据并将其通过串口发送
// 功能点3:对FLASH进行扇区擦除操作
//
module Flash_top(
input clk,
input rst_n,
input UART_RX,
input miso,
output cs,
output sclk,
output mosi,
output UART_TX
);
//
//
// 操作指令集
//
//
parameter WRITE_ORDER = 8'h02,
READ_ORDER = 8'h03,
SECTOR_ERASE_ORDER = 8'h20;
parameter default_address = 24'h0000FF;
//
//
// 读取串口下发数据
//
//
wire [7:0]Uart_TxData; //读取FLASH数据并发送
wire [7:0]Uart_RxData; //串口接收到的数据
wire rx_done; //串口接收完成标志位
wire tx_done; //串口发送完成标志位
uart_top uart_top_inst(
.clk (clk),
.rst_n (rst_n),
.rx_data (UART_RX),
.need_tx_data (Uart_TxData),
.tx_en (tx_en),
.tx_data (UART_TX),
.tx_done (tx_done),
.rx_done (rx_done),
.rx_men (Uart_RxData)
);
//
//
// FLASH驱动顶层
//
//
wire [7:0] read_flash_data;
wire full_order_done;
flash_operation_top flash_drive_inst(
.clk (clk),
.rst_n (rst_n),
.flash_start (rx_done),
.flash_order (vio_flash_order), //FLASH需完成动作指令
.need_write_data (Uart_RxData), //FLASH需写入的数据
.flash_address (vio_flash_address), //FLASH操作数据地址
.miso (miso),
.sclk (sclk),
.cs (cs),
.mosi (mosi),
.read_flash_data (Uart_TxData),
.full_order_done (full_order_done) //flash操作完毕
);
//
//
// 串口发出数据使能控制
//
//
reg tx_en = 'd0;
always @(posedge clk) begin
if(full_order_done && vio_flash_order == READ_ORDER) begin
tx_en <= 'd1;
end else begin
tx_en <= 'd0;
end
end
//
//
// FLASH在线调试模块--VIO核
//
//
wire [7:0] vio_flash_order;
wire [7:0] vio_flash_write;
wire [23:0] vio_flash_address;
vio_0 vio_inst(
.clk(clk),
.probe_in0(Uart_TxData), //8
.probe_out0(vio_flash_order), //8
.probe_out1(vio_flash_write), //8
.probe_out2(vio_flash_address) //24
);
endmodule
该模块主要是将FLASH读取数据通过串口上传至上位机方便查看,同时VIO核的使用可以使得调试更加方便。
SPI以及UART串口通讯代码均沿用前两篇博客中的模块,有兴趣的可以查阅一下
功能仿真
注意:其中aa0f0b为操作FLASH地址、aa为待写入数据;同时,在读指令发送完成后,一直持续发送0(实际上只要你片选持续选通,无论发送什么都对FLASH读无影响)。
写指令时序图
擦除指令时序图
读指令时序图
板级调试
读操作
从VIO指定为03H读指令,读取地址为00_0FFF,可以看出读取当前地址数据为FF,然后在串口助手里面可以看见发出的数据也为FF;
写操作
我们对FLASH的00_0FFF地址位写入串口发送的AA数据,页编程指令为02H。后续通过读取该地址可知写入操作成功,当前地址数据为AA。
擦除操作
我们输入扇区擦除指令20H对当前扇区进行擦除操作,如下图读回数据FF可知擦除成功。
仍存在疑问的地方
根据查阅到的博客以及官方手册,在FLASH重新上电后,其WEL位(写保护位)应置0,但实际情况是FLASH上电后即为1。同时,在执行完擦除、页编程指令后,该位也应该置0,但实际情况是依旧为1。这个情况导致我花费了大量的时间去调试,而且目前为止仍未找到原因。
最终的情况是,虽然在代码对WEL作了判断,但其实是无效的,因为该位始终为1;但是BUSY位是正常的,处于忙状态时会置1。同时,由于WEL位始终为1,但实际是并未使能的,故我在执行擦除、页编程指令之前都会先进行写使能。