目录
2.3、最后介绍一下我的开发板上QSPI Flash硬件原理图
4.2、 如何用Matlab产生存放在ROM中的.coe文件格式的数据
4.3、 标准SPI总线操作QSPI Flash思路与代码编写
4、读状态寄存器(Read Status Register)指令
4.4、 如何处理双向信号(Verilog中用关键词inout定义的信号都是双向信号)
4.5、 四线SPI总线操作QSPI Flash思路与代码编写
1、写写状态寄存器(Write Status Register)指令
2、四线写数据(Quad Input Page Program)指令
5.1、 用Verilog编写QSPI Flash驱动的意义何在?
5.2、 关于在代码中同时使用时钟的上升沿和下降沿操作时有什么风险
一、 软件平台与硬件平台
软件平台:
1、操作系统:Windows-8.1
2、开发套件:ISE14.7
3、仿真工具:ModelSim-10.4-SE
4、Matlab版本:Matlab2014b/Matlab2016a
硬件平台:
1、 FPGA型号:Xilinx公司的XC6SLX45-2CSG324
2、 Flash型号:WinBond公司的W25Q128BV Quad SPI Flash存储器
提示:如果图片不清晰,请把图片在浏览器的新建标签页打开或保存到本地打开。
二、 原理介绍
2.1、W25Q128BV芯片封装及引脚
上一篇博客《SPI总线的原理与FPGA实现》中已经有关于标准SPI协议的原理与时序的介绍,这里不再赘述。本节主要是讨论QSPI(Quad SPI,四线SPI总线)的相关内容。我的开发板上有一片型号是W25Q128BV的Quad SPI Flash存储器,本文将以它为例子来说明QSPI操作的一些内容。
W25Q128BV的Quad SPI Flash存储器的Top View如下图所示
这块芯片一共有8个有用的管脚,其每个管脚的功能定义如下图所示
由上图可知2号管脚DO(IO1),3号管脚 /WP(IO2),5号管脚DI(IO0)以及7号管脚/HOLD(IO3)均为双向IO口,所以在编写Verilog代码的时候要把它们定义为inout类型,inout类型的信号既可以做输出也可以作为输入,具体在代码里面如何处理后文会有介绍。
QSPI Flash每个引脚的详细描述如下:
1、Chip Select(/CS)
片选信号Chip Select(/CS)的作用是使能或者不使能设备的操作,当CS为高时,表示设备未被选中,串行数据输出线(DO或IO0,IO1,IO2,IO3)均处于高阻态,当CS为低时,表示设备被选中,FPGA可以给QSPI Flash发送数据或从QSPI Flash接收数据。
2、串行数据输入信号DI以及串行输出信号DO
W25Q128BV支持标准SPI协议,双线SPI(Dual SPI)协议与四线SPI(Quad SPI)协议。标准的SPI协议在串行时钟信号(SCLK)的上升沿把串行输入信号DI上的数据存入QSPI Flash中,在串行时钟信号(SCLK)的下降沿把QSPI Flash中的数据串行化通过单向的DO引脚输出。而在Dual SPI与Quad SPI中,DI与DO均为双向信号(既可以作为输入,也可以作为输出)。
3、Write Project(/WP)
写保护信号的作用是防止QSPI Flash的状态寄存器被写入错误的数据,WP信号低电平有效,但是当状态寄存器2的QE位被置1时,WP信号失去写保护功能,它变成Quad SPI的一个双向数据传输信号。
4、HOLD(/HOLD)
HOLD信号的作用是暂停QSPI Flash的操作。当HOLD信号为低,并且CS也为低时,串行输出信号DO将处于高阻态,串行输入信号DI与串行时钟信号SCLK将被QSPI Flash忽略。当HOLD拉高以后,QSPI Flash的读写操作能继续进行。当多个SPI设备共享同一组SPI总线相同的信号的时候,可以通过HOLD来切换信号的流向。和WP信号一样,当当状态寄存器2的QE位被置1时,HOLD信号失去保持功能,它也变成Quad SPI的一个双向数据传输信号。
5、串行时钟线
串行时钟线用来提供串行输入输出操作的时钟。
2.2、W25Q128BV的内部结构框图如下图所示:
更多详细的内容请阅读W25Q128BV的芯片手册。由于本文要进行4线SPI的操作,但QSPI Flash默认的操作模式是标准单线SPI模式,所以在每次进行4线SPI操作的时候一定要先把状态寄存器2的QE位(倒数第2位)置1,然后才能进行QSPI操作。
2.3、最后介绍一下我的开发板上QSPI Flash硬件原理图
如下图所示:
三、 目标任务
1、编写标准SPI 协议 Verilog代码来操作QSPI Flash,并用ChipScope抓出各个指令的时序与芯片手册提供的时序进行对比
2、在标准SPI协议的基础上增加Quad SPI的功能,并用ChipScope抓出Quad SPI的读写数据的时序
3、对比标准SPI与Quad SPI读写W25Q128BV的ChipScope时序,感受二者的效率差距
四、 设计思路与Verilog代码编写
4.1、 命令类型的定义
W25Q128BV一共有35条命令,这里不可能把所有命令的逻辑都写出来,所以截取了一部分常用的命令作为示例来说明QSPI Flash的操作方法。由于命令数目很多,所以在这个部分先对各个命令类型做一个初步定义,下文的代码就是按照这个定义来编写的。
命令编号 |
命令类型(自定义) |
命令码(芯片手册定义) |
命令功能 |
1 |
5’b0XXXX |
8’h00 |
无 |
2 |
5’b10000 |
8’h90 |
读设备ID |
3 |
5’b10001 |
8’h06 |
写使能 |
4 |
5’b10010 |
8’h20 |
扇区擦除 |
5 |
5’b10011 |
8’h05/8’h35 |
读状态寄存器1/2 |
6 |
5’b10100 |
8’h04 |
关闭写使能 |
7 |
5’b10101 |
8’h02 |
写数据操作(单线模式) |
8 |
5’b10110 |
8’h01 |
写状态寄存器 |
9 |
5’b10111 |
8’h03 |
读数据操作(单线模式) |
10 |
5’b11000 |
8’h32 |
写数据操作(四线模式) |
11 |
5’b11001 |
8’h6b |
读数据操作(四线模式) |
说明:
1、命令类型是我自己随便定义的,可以随便修改。命令码是芯片手册上定义好,不能修改,更详细的内容请参考W25Q128芯片手册。
2、命令类型的最高位是使能位,只有当最高位为1时,命令才有效(在代码里面写的就是只有当最高位为1时才能进入SPI操作的状态机)。
3、进行四线读写操作之前,一定要把四线读写功能的使能位打开,方法是通过写状态寄存器命令把状态寄存器2的QE位(倒数第二位)置1。
4.2、 如何用Matlab产生存放在ROM中的.coe文件格式的数据
上一节设计了一个把存放在ROM中的数据用SPI总线发出来的例子,ROM里面只存放了10个数据,所以可以直接把这10个数据填写到.coe文件就可以了,由于QSPI Flash的页编程(写数据)指令最大支持256字节的写操作,所以下面的例子的功能是把ROM中存放的256个字节(8-bit)数据先写入QSPI Flash中,然后在读出来。由于数据太多(256个),所以一个一个填写肯定不现实,所以可以利用Matlab来直接产生.coe文件,Matlab的完整代码如下:
width=8; %rom中数据的宽度
depth=256; %rom的深度
y=0:255;
y=fliplr(y); %产生要发送的数据,255,254,253,...... ,2,1,0
fid = fopen('test_data.coe', 'w'); % 打开一个.coe文件
% 存放在ROM中的.coe文件第一行必须是这个字符串,16表示16进制,可以改成其他进制
fprintf(fid,'memory_initialization_radix=16;\n');
% 存放在ROM中的.coe文件第二行必须是这个字符串
fprintf(fid,'memory_initialization_vector=\n');
% 把前255个数据写入.coe文件中,并用逗号隔开,为了方便知道数据的个数,每行只写一个数据
fprintf(fid,'%x,\n',y(1:end-1));
% 把最后一个数据写入.coe文件中,并用分号结尾
fprintf(fid,'%x;\n',y(end));
fclose(fid); % 关闭文件指针
用Matlab2014b运行上面的代码以后会在与这个.m文件相同的目录下产生一个.coe文件,这个.coe文件可以导入到ROM中。
4.3、 标准SPI总线操作QSPI Flash思路与代码编写
上一篇博客《SPI总线的原理与FPGA实现》已经介绍过用spi_module这个模块去读取QSPI Flash的Manufacturer/Device ID,事实上除了上篇博客提供的那种方法以外,还可以直接在时钟信号的下降沿发送数据,时钟信号的上升沿来接受数据来完成读ID的操作,当FPGA在时钟的下降沿发送数据的时候,那么时钟的上升沿刚好在数据的正中间,QSPI Flash刚好可以在这个上升沿把数据读进来,读操作则正好相反。但是有很多有经验的人告诉我在设计中如非必要最好不要使用时钟下降沿触发的设计方法,可能是因为大多数FPGA里面的Flip Flops资源都是上升沿触发的,如果在Verilog代码采用下降沿触发的话 ,综合的时候会在CLK输入信号前面综合出一个反相器,这个反相器可能会对时钟信号的质量有影响,具体的原因等我再Google上继续搜索一段时间在说。这个例子由于状态机相较前几篇博客来说相对复杂,所以接下来写代码我还是采用下降沿发送数据,上升沿接收数据的方式来描述这个状态机。
接下来的任务就是抽象出一个状态机。上一篇博客仅仅读一个ID就用了6个状态,所以采用上一篇博客的设计思路显然不太现实,但对于初学者而言,上一篇博客仍然有一个基本的指引作用。通过阅读QSPI Flash的芯片手册,可以发现,所有的命令其实至多由以下三个部分组成:
1、发送8-bit的命令码
2、发送24-bit的地址码
3、发送数据或接收数据
所有命令的状态跳变图可由下图描述
所以按照这个思路来思考的话抽象出来的状态机的状态并不多。单线模式的状态为以下几个:
1、空闲状态:用来初始化各个寄存器的值
2、发送命令状态:用来发送8-bit的命令码
3、发送地址状态:用来发送24-bit的地址码
4、读等待状态:当读数据操作正在进行的时候进入此状态等待读数据完毕
5、写数据状态(单线模式):在这个状态FPGA往QSPI Flash里面写数据
6、结束状态:一条指令操作结束,并给出一个结束标志
完整的代码如下:
`timescale 1ns / 1ps
module qspi_driver
(
output O_qspi_clk , // SPI总线串行时钟线
output reg O_qspi_cs , // SPI总线片选信号
output reg O_qspi_mosi , // SPI总线输出信号线,也是QSPI Flash的输入信号线
input I_qspi_miso , // SPI总线输入信号线,也是QSPI Flash的输出信号线
input I_rst_n , // 复位信号
input I_clk_25M , // 25MHz时钟信号
input [4:0] I_cmd_type , // 命令类型
input [7:0] I_cmd_code , // 命令码
input [23:0] I_qspi_addr , // QSPI Flash地址
output reg O_done_sig , // 指令执行结束标志
output reg [7:0] O_read_data , // 从QSPI Flash读出的数据
output reg O_read_byte_valid , // 读一个字节完成的标志
output reg [3:0] O_qspi_state // 状态机,用于在顶层调试用
);
parameter C_IDLE = 4'b0000 ; // 空闲状态
parameter C_SEND_CMD = 4'b0001 ; // 发送命令码
parameter C_SEND_ADDR = 4'b0010 ; // 发送地址码
parameter C_READ_WAIT = 4'b0011 ; // 读等待
parameter C_WRITE_DATA = 4'b0101 ; // 写数据
parameter C_FINISH_DONE = 4'b0110 ; // 一条指令执行结束
reg [7:0] R_read_data_reg ; // 从Flash中读出的数据用这个变量进行缓存,等读完了在把这个变量的值给输出
reg R_qspi_clk_en ; // 串行时钟使能信号
reg R_data_come_single ; // 单线操作读数据使能信号,当这个信号为高时
reg [7:0] R_cmd_reg ; // 命令码寄存器
reg [23:0] R_address_reg ; // 地址码寄存器
reg [7:0] R_write_bits_cnt ; // 写bit计数器,写数据之前把它初始化为7,发送一个bit就减1
reg [8:0] R_write_bytes_cnt ; // 写字节计数器,发送一个字节数据就把它加1
reg [7:0] R_read_bits_cnt ; // 写bit计数器,接收一个bit就加1
reg [8:0] R_read_bytes_cnt ; // 读字节计数器,接收一个字节数据就把它加1
reg [8:0] R_read_bytes_num ; // 要接收的数据总数
reg R_read_finish ; // 读数据结束标志位
wire [7:0] W_rom_addr ;
wire [7:0] W_rom_out ;
assign O_qspi_clk = R_qspi_clk_en ? I_clk_25M : 0 ; // 产生串行时钟信号
assign W_rom_addr = R_write_bytes_cnt ;
// 功能:用时钟的下降沿发送数据
always @(negedge I_clk_25M)
begin
if(!I_rst_n)
begin
O_qspi_cs <= 1'b1 ;
O_qspi_state <= C_IDLE ;
R_cmd_reg <= 0 ;
R_address_reg <= 0 ;
R_qspi_clk_en <= 1'b0 ; //SPI clock输出不使能
R_write_bits_cnt <= 0 ;
R_write_bytes_cnt <= 0 ;
R_read_bytes_num <= 0 ;
R_address_reg <= 0 ;
O_done_sig <= 1'b0 ;
R_data_come_single <= 1'b0 ;
end
else
begin
case(O_qspi_state)
C_IDLE: // 初始化各个寄存器,当检测到命令类型有效(命令类型的最高位位1)以后,进入发送命令码状态
begin
R_qspi_clk_en <= 1'b0 ;
O_qspi_cs <= 1'b1 ;
O_qspi_mosi <= 1'b0 ;
R_cmd_reg <= I_cmd_code ;
R_address_reg <= I_qspi_addr ;
O_done_sig <= 1'b0 ;
if(I_cmd_type[4] == 1'b1)
begin //如果flash操作命令请求
O_qspi_state <= C_SEND_CMD ;
R_write_bits_cnt <= 7 ;
R_write_bytes_cnt <= 0 ;
R_read_bytes_num <= 0 ;
end
end
C_SEND_CMD: // 发送8-bit命令码状态
begin
R_qspi_clk_en <= 1'b1 ; // 打开SPI串行时钟SCLK的使能开关
O_qspi_cs <= 1'b0 ; // 拉低片选信号CS
if(R_write_bits_cnt > 0)
begin //如果R_cmd_reg还没有发送完
O_qspi_mosi <= R_cmd_reg[R_write_bits_cnt] ; //发送bit7~bit1位
R_write_bits_cnt <= R_write_bits_cnt-1'b1 ;
end
else
begin //发送bit0
O_qspi_mosi <= R_cmd_reg[0] ;
if ((I_cmd_type[3:0] == 4'b0001) | (I_cmd_type[3:0] == 4'b0100))
begin //如果是写使能指令(Write Enable)或者写不使能指令(Write Disable)
O_qspi_state <= C_FINISH_DONE ;
end
else if (I_cmd_type[3:0] == 4'b0011)
begin //如果是读状态寄存器指令(Read Register)
O_qspi_state <= C_READ_WAIT ;
R_write_bits_cnt <= 7 ;
R_read_bytes_num <= 1 ;//读状态寄存器指令需要接收一个数据
end
else if( (I_cmd_type[3:0] == 4'b0010) || (I_cmd_type[3:0] == 4'b0101) || (I_cmd_type[3:0] == 4'b0111) || (I_cmd_type[3:0] == 4'b0000) )
begin // 如果是扇区擦除(Sector Erase),页编程指令(Page Program),读数据指令(Read Data),读设备ID指令(Read Device ID)
O_qspi_state <= C_SEND_ADDR ;
R_write_bits_cnt <= 23 ; // 这几条指令后面都需要跟一个24-bit的地址码
end
end
end
C_SEND_ADDR: // 发送地址状态
begin
if(R_write_bits_cnt > 0) //如果R_cmd_reg还没有发送完
begin
O_qspi_mosi <= R_address_reg[R_write_bits_cnt] ; //发送bit23~bit1位
R_write_bits_cnt <= R_write_bits_cnt - 1 ;
end
else
begin
O_qspi_mosi <= R_address_reg[0] ; //发送bit0
if(I_cmd_type[3:0] == 4'b0010) // 扇区擦除(Sector Erase)指令
begin //扇区擦除(Sector Erase)指令发完24-bit地址码就执行结束了,所以直接跳到结束状态
O_qspi_state <= C_FINISH_DONE ;
end
else if (I_cmd_type[3:0] == 4'b0101) // 页编程(Page Program)指令
begin
O_qspi_state <= C_WRITE_DATA ;
R_write_bits_cnt <= 7 ;
end
else if (I_cmd_type[3:0] == 4'b0000) // 读Device ID指令
begin
O_qspi_state <= C_READ_WAIT ;
R_read_bytes_num <= 2 ; //接收2个数据的Device ID
end
else if (I_cmd_type[3:0] == 4'b0111) // 读数据(Read Data)指令
begin
O_qspi_state <= C_R