|
本帖最后由 正点原子 于 2021-10-30 10:50 编辑
1)实验平台:正点原子新起点V2FPGA开发板
2) 章节摘自【正点原子】《新起点之FPGA开发指南 V2.1》
3)购买链接:https://detail.tmall.com/item.htm?id=609758951113
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-328002-1-1.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流QQ群:712557122
第四十五章 FLASH读写实验
FLASH存储器又称闪存,是一种长寿命的非易失性(在断电情况下仍能保持所存储的数据信息)存储器,一般用来保存一些重要的设置信息或者程序等等。通常采用SPI协议跟FLASH芯片进行数据交互。本节实验我们将带领大家一起去学习SPI通信协议,并基于SPI协议来对板载的FLASH芯片进行读写操作。
本章包括以下几个部分:
1.1简介
1.2实验任务
1.3硬件设计
1.4程序设计
1.5下载验证
1.1简介
新起点开发板上板载的FLASH芯片型号为W25Q16BV,它是由Winbond公司生产的一款高性能FLASH芯片。这款芯片的总容量为16Mbit,整个存储阵列被分成8,192个可编程页,每页的容量为256字节。它支持Standard SPI、Dual SPI和Quad SPI三种SPI协议通信方式,最大传输数据速率可达50MB/S。下面我们来一起认识一下这款芯片。
首先我们先来看看W25Q16BV的封装图,如下所示:
图 45.1.1 FLASH芯片封装图
从上图中我们可以看到W25Q16BV的封装是非常简单的,它一共就8个引脚,那么这8个引脚分别都有什么功能呢,我们继续往下看:
图 45.1.2 引脚功能表
从上表中可以看到1号引脚是片选信号,并且是低电平有效(引脚名字CS前面加了一个/一般表示低电平有效),当CS引脚拉低时FLASH芯片被选中,相当于使能,此时FLASH芯片可以进行后续指令操作;2号和5号引脚是FLASH芯片的数据输出输入引脚(Standard and Dual SPI模式下);3号引脚是写保护引脚(Quad SPI模式下被用作数据输出引脚),也是低电平有效,主要用来防止状态寄存器被写入(具体操作请参考数据手册)。4号和8号引脚是电源和GND;6号引脚是FLASH的时钟驱动引脚,在对FLASH进行各种指令操作或者读写数据时必须保证6号引脚有稳定且连续的驱动时钟。7号引脚是HOLD引脚,也是低电平有效,在Standard and Dual SPI模式下相当于FLASH暂停信号,当HOLD引脚和CS引脚同时拉低时,虽然此时FLASH芯片是被选中的,但是DO引脚会处于高阻抗状态,DI和CLK这两个引脚会忽略输入的数据和时钟,相当于DI和CLK处于无效状态。
说完了引脚最后我们再来看看W25Q16BV FLASH芯片的内部结构示意图,如下图所示:
图 45.1.3 内部结构示意图
在上图中我已经将FLASH芯片的内部结构图用三个框框起来了,其中一号框中的是FLASH芯片的指令控制逻辑单元,我们使用SPI协议向FLASH芯片发送不同的指令时,指令控制单元会处理不同的指令去执行对应的操作。2号框和3号框其实都是FLASH的存储单元,我们可以看到2MB(16Mb)的存储空间被划分为32个存储块,每个存储块是64KB,而单独一个64KB的存储块又被划分成16个更小的4KB存储块。这样划分最主要的目的是为了让数据的写入和擦除更加的灵活。从数据手册中我们可以知道W25Q16BV芯片的地址线是24位的,分别对应8位扇区地址、8位页地址和8位字节地址。因此我们在写入数据时就可以锁定某一扇区某一页开始写数据,如果你不想把整页都写满数据你还可以指定这一页具体从哪个字节开始写入数据(一页有256个字节)。那么擦除也有好几种方式比如我们按照存储块去擦除,上图中显示的是32个存储块,我们可以直接擦除某一个存储块中的数据,也可以选择擦除一个存储块中更小的4KB的小存储块,还可以选择整个芯片全擦除等等,这都是因为FLASH的内部将存储单元分了扇区和存储块,所以我们才能进行这样的操作。
接下来我们再来看看W25Q16BV的操作指令,如下图所示:
图 45.1.4 指令表
从上表中可以看到FLASH的操作指令还是不少的,但是我们本节实验并没有用到全部的指令,在这里我们只介绍用到的指令,对于其他指令感兴趣的同学可以去翻看数据手册了解。
Write Enable(06h):使能指令,Write Enable指令将状态寄存器中的Write Enable Latch (WEL)位设置为高电平,在执行页编辑、扇区擦除、块擦除、芯片擦除和写状态寄存器指令前必须先执行Write Enable指令。
Read Status Register-1(05h):读取状态寄存器1指令,这条指令的作用就是读取状态寄存器1的值。在W25Q16BV中存在两个状态寄存器可以用来指示FLASH芯片的可用性状态或者配置SPI的相关设置。在本节实验中我们主要是读取状态寄存器1的值,并且检测它的第零位也就是BUSY位是零还是一。当FLASH处于擦除或者写入数据时,状态寄存器1的BUSY位会拉高,当BUSY位重新恢复成低电平时代表FLASH擦除或者写入数据完成。
Page Program(02h):页编辑指令,可以理解成写数据指令,在上文中我们已经介绍过W25Q16BV的存储空间是分为扇区和页的,每一页又有256个字节的存储空间。当执行页编辑指令时就可以往对应的扇区对应的页中写入数据,一次性最多写入256个字节数据。这里尤其要注意一点,当整页写数据时你可以不必写满256个字节,小于256个字节也是可以正常写入的,但是千万不能超过256个字节数据,因为一旦超过256个字节,多余的数据就会返回这一页的开头重新写入,这样就会覆盖之前已经写入的数据。举个例子我们一次性写入260个数据,这样就多了4个数据出来,那么这4个数据就会回到这一页的开头把第0、1、2和3这四个数据覆盖掉。
Sector Erase-4KB(20h):区块擦除指令,在上文中我们已经提到过芯片内部的存储空间是被划分成一个一个小块的,我们可以直接对这些小块执行擦除指令。Sector Erase指令就是对4KB的小块执行擦除操作。除此之外还有Block Erase -32KB(52h)和Block Erase -64KB(D8h)指令,用于擦除更大的存储块,本节实验只使用Sector Erase区块擦除指令。
Chip Erase(c7h):全擦除指令,上一条指令我们介绍了芯片的区块擦除指令,可以擦除不同大小的区块,但是有的时候我们想直接将FLASH芯片进行格式化那怎么办呢?这个时候我们就可以执行全擦除指令了。全擦除指令会擦除整个FLASH芯片的所有存储数据。需要注意的是全擦除指令执行的比较慢,通常需要几秒钟才能完成全擦除(数据手册中给的典型值是3秒,实测大概2秒左右),在芯片处于全擦除期间我们只能对它执行访问状态寄存器指令操作,不能执行其他例如读写等操作。在擦除期间,状态寄存器1的最低位(BUSY位)始终处于高电平(不仅仅是全擦除指令,区块擦除指令、写数据指令执行时BUSY位也会拉高),当完成全擦除后BUSY位拉低,此时可以执行其他指令了。
Read Data(03h):读取数据指令,当FLASH中被写入数据后我们可以使用Read Data指令将数据读取出来。
W25Q16BV FLASH芯片就给大家介绍到这里了,更加详细的介绍大家可以去翻看数据手册,我们配套的资料盘A盘中(新起点开发板资料盘(A盘)\7_硬件资料\2_芯片资料\04_QSPI)已经放入了W25Q16BV FLASH芯片的数据手册。
接下来我们再来讲解本节实验最重要的一个点:SPI通信协议。
上文介绍FLASH芯片的时候就已经跟大家说了,我们跟FLASH芯片进行通信是基于SPI协议的,那么什么是SPI协议呢?SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。
SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制从设备 (Slave)。一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作。其工作状态图如下所示:
图 45.1.5 SPI主机控制从机
从上图中可以看到,三个从机的时钟信号(SCLK)、数据输入(MOSI)和数据输出(MISO)是共用的,只有片选信号(CS)是单独的,因此在进行主从机通信时,主机只需要通过CS信号选中对应的从机就可以和这个从机通信了。
宏观上我们知道主从机是如何工作的之后,我们接下来在来详细的分析基于SPI协议的主从机工作机制,其原理如下图所示:
图 45.1.6 SPI工作机制
上图中就是基于SPI协议的主从机通信原理图,下面先来解释一下上图中的结构:
SSPBUF:Synchronous Serial Port Buffer, 泛指 SPI 设备里面的内部缓冲区, 一般在物理上是以 FIFO 的形式, 保存传输过程中的临时数据;
SSPSR:Synchronous Serial Port Register, 泛指 SPI 设备里面的移位寄存器(Shift Regitser), 它的作用是根据设置好的数据位宽(bit-width) 把数据移入或者移出 SSPBUF;
Controller:泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式。
明白了SPI通信主从机的结构再去看SPI通信就很简单了,其实SPI通信就是主从机之间的数据交换,主机的SSPSR寄存器把要给从机的数据放到MOSI线上,然后从机的SSPSR寄存器把MOSI线上的数据移位寄存到自己的SSPBUF寄存器中去,同理从机的SSPSR寄存器也会把要给主机的数据放到MISO线上,然后主机的SSPSR寄存器把MISO线上的数据移位寄存到自己的SSPBUF寄存器中去。这样主从机之间就完成了一次数据交互。
这样的数据传输方式又被称为数据交换, SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个“发送者(Transmitter)” 或者 “接收者(Receiver)”。在每个 Clock 周期内,SPI 设备都会发送并接收一个 bit 大小的数据(不管主设备还是从设备), 相当于该设备有一个 bit 大小的数据被交换了。一个 Slave 设备要想能够接收到 Master 发过来的控制信号, 必须在此之前能够被 Master 设备进行访问 (Access)。所以Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选,把想要访问的 Slave 设备选中。在数据传输的过程中,每次接收到的数据必须在下一次数据传输之前被采样。如果之前接收到的数据没有被读取,那么这些已经接收完成的数据将有可能会被丢弃,导致 SPI 物理模块最终失效。
说了这么多大家脑海里应该对SPI通信有了一个大概的认知了,下面我们再来详细的讲解一下SPI通信的时序,如下图所示:
图 45.1.7 SPI协议时序图(图片来源于网络)
从时序图中我们可以看到SPI通信协议分为4种模式,大家仔细观察这4种模式不难发现它其实就是根据时钟信号SCK的极性和相位划分的。
我们先来看看时钟的极性,极性全称Clock Polarity,通常简写成CPOL。它是指时钟信号在空闲状态下是高电平还是低电平,当时钟空闲时为低电平即CPOL==0,反之则CPOL==1。
看完极性我们再来看看相位,相位全称Clock Phase,通常简写成CKPHA。它是指时钟信号开始有效的第一个边沿和数据的关系。不管是时钟的上升沿采样还是下降沿采样,采样边沿为了满足建立时间和保持时间一般都是在数据稳定周期的正中间发生触发采样。因此当时钟信号有效的第一个边沿处于数据稳定期的正中间时我们定义CKPHA==0,反之时钟信号有效的第一个边沿不处于数据稳定期的正中间我们定义CKPHA==1。这样当CKPHA==0模式时我们先发生数据采样然后输出数据,当CKPHA==1模式时我们先发生数据输出然后进行数据采样。
下面给出一张表来辅助大家记忆SPI通信协议的四种模式,如下所示:
图 45.1.8 SPI通信四种模式
到这里整个SPI相关的知识就讲解完了,为了让大家对SPI通信时序有个更好的理解我们以SPI模式0来给大家举个例子,我们先把模式0的时序图单独贴出来,如下所示:
图 45.1.9 SPI模式0
现在有一个主机想把一个8位二进制数据10101100发送给从机,而从机呢刚好也想把一个8位二进制数据11001010发送给主机(双工通信)。我们按照上图的时序来,首先主机要将从机片选信号SSEL拉低,此时代表从机被选中,在此之前SCK信号要始终处于低电平。SSEL拉低后时钟信号SCK开始启动,此时主机和从机的数据开始交互,怎么交互的呢,我用一张表格来给大家演示,如下表所示:
表 45.1.1 SPI通信过程表
上表中时钟信号1代表时钟上升沿,0代表下降沿。我们采用的时SPI通信模式0,所以第一个时钟上升沿到来时完成数据采样,主机将自己要发送的数据最高位放到MOSI(主机输出从机输入数据线)上,而从机也将自己要发送的数据最高位放到MISO(从机输出主机输入数据线)上,当第一个时钟的下降沿到来时主机的SSPSR把MISO上的数据存入SSPBUF,而从机的SSPSR则把MOSI上的数据存入自己的SSPBUF,这样主机从机就完成了1bit数据的交互。当第二个时钟上升沿到来时,重复第一个时钟的操作,就完成了2bit数据的交互,当8个时钟过去后,主机和从机就完成了8bit数据交互,此时主机SSPBUF中的数据由10101100变成了11001010,而从机SSPBUF中的数据也由11001010变成了10101100。
到此关于SPI通信协议就讲完了,接下来我们正式进入实战演练,开始我们的本节FLASH读写实验。
1.2实验任务
本节实验将对板载的FLASH芯片先进行全擦除,然后写入数据,再把数据读出,读出数据正确后再把写入数据的扇区进行扇区擦除,最后再读取一遍数据看看数据是否擦除成功。读写正确则点亮led0,读写错误则led灯闪烁。
1.3硬件设计
板载的FLASH芯片硬件原理图如下所示:
图 45.3.1 硬件原理图
FLASH芯片的硬件原理图是比较简单的,因为芯片本身就8个引脚,每个引脚的作用在上文简介篇我已经跟大家介绍过了,这里我们可以看到写保护引脚和HOLD引脚都接到了3.3V上,因此我们的开发板只能支持普通SPI模式,没办法支持DSPI和QSPI模式。关于DSPI和QSPI这里简单提一下,其实它们是专门针对FLASH这一类器件提出的,我们知道SPI协议是全双工通信模式,而往往我们在跟FLASH进行通信时是不需要全双工的,因此我们让它处于半双工工作模式,这样两根数据线都可以同时发送或者接收数据,数据传输速率提高了一倍,这种模式就是DSPI。而QSPI则在原本的SPI 4线模式上又加了两根数据线,实现6线的操作以达到更高的数据传输速率。关于DSPI和QSPI感兴趣的同学可以去查找一些资料自己学习一下,我们的开发板不支持这两种模式,所以在这里就不再过多讲述。
1.4程序设计
根据本节实验的实验任务我们设计了如下程序框图:
图 45.4.1 程序框图
从上图中可以看到本节实验整个工程分为5个模块,分别是顶层模块(flash_rw_test)、锁相环模块(pll_spi)读写模块(flash_rw)、LED灯模块(led_alarm)和SPI驱动模块(spi_drive)。工程的RTL视图如下所示:
图 45.4.2 RTL视图
接下来我们来看看每个模块的作用:
顶层模块(flash_rw_test):顶层模块的作用主要就是例化四个子模块。
锁相环模块(pll_spi):锁相环模块的作用主要是生成100Mhz的时钟给读写模块(flash_rw)和SPI驱动模块(spi_drive)使用。我们的板载FLASH芯片最高可以进行104Mhz的时钟操作,本节实验我们使用锁相环生成了100Mhz时钟再进行二分频产生50Mhz的FLASH工作时钟。之所以这样二分频操作主要是为了方便控制SPI协议的时钟极性。
读写模块(flash_rw) :读写模块的作用主要是产生要写入FLASH的数据和给SPI驱动模块(spi_drive)发送操作指令。
LED灯模块(led_alarm):LED灯模块就比较简单了,就是检测FLASH读写数据是否正确,如果正确那么SPI驱动模块(spi_drive)给出的错误标志信号为0,此时驱动LED0常亮,如果检测到读写错误标志拉高,则驱动LED0闪烁。
SPI驱动模块(spi_drive):SPI驱动模块是本节实验最重要的一个模块。它实现了基于SPI协议的FLASH通信时序,完成了对FLASH芯片擦除、写入数据、读取数据以及轮询状态寄存器等操作。并且对读出的数据进行检查给出读写正确与否的标志。
接下来我们就来详细的分析每个模块的代码,看看是如何实现这些功能的。
首先我们就来看看本节实验最重要的模块SPI驱动模块(spi_drive)的代码,在讲解这个模块代码前我先来带领大家了解一下FLASH指令的时序。在上文简介部分我已经跟大家介绍了本节实验所用到的6个操作指令,它们分别是Write Enable(06h)、Read Status Register-1(05h)、Page Program(02h)、Sector Erase-4KB(20h)、Chip Erase(c7h)以及Read Data(03h),这些指令的功能在简介篇我已经详细的介绍过了,这不再重复赘述。
下面我们直接来分析时序,先看第一个使能指令Write Enable(06h),它的操作时序如下所示:
图 45.4.3 使能指令Write Enable(06h)时序
学习完SPI协议再来看使能指令的时序就比较轻松了,我们可以看到它是采取SPI协议模式零的通信方式,因为时钟信号CLK的初始极性是低电平,第一个边沿处在第一个数据稳定周期的正中间,所以刚好符合SPI协议模式零。然后我们再来分析一下这个操作时序,就是当CS信号拉低时时钟信号开始有效,此时数据线DI(MOSI)上给出数据即可,DO(MISO)不用管它,因为只有读FLASH时才会用DO。时序虽然简单但是大家注意我在时序图中框出来的两个部分,其中第一个框是指CS信号拉低(下降沿)到时钟有效这一段时间,根据数据手册这一段时间必须保持至少5ns。而方框2则是当使能指令发送完成后需要拉高CS信号,但是此时使能指令还没有立即执行,需要再锁存一段时间才能重新拉低CS信号进行其他操作,这段锁存时间建议大家保持100ns。
接下来我们再来看看写指令时序和读指令时序,如下所示:
图 45.4.4 写指令时序
图 45.4.5 读指令时序
相比较于使能指令时序读写指令时序要稍微复杂一些,他们除了要发送8bit的指令外还要发送24bit的地址,在上文简介部分我也讲解过了这24bit地址包括8bit扇区地址、8bit页地址和8bit字节地址(一页有256个字节)。这里对于写指令尤其要注意,当我们把最后一个字节的数据传输完成后会拉高CS信号,此时数据并没有完成写入,还需要等待3秒左右的时间(数据手册给的3秒,实际要短一点)数据才能写完成。那么我们怎么知道数据写完成了呢,一种办法就是按照数据手册来,我就写完后延迟三秒再去发送其他指令,另一种方法就是轮询FLASH芯片的状态寄存器,简介部分我讲解过状态寄存器1的最低位(BUSY位)在FLASH处于写数据或者擦除数据期间是拉高的,当它拉低代表FLASH写入数据或者擦除数据完成。
下面给出读状态寄存器指令时序以及两种擦除数据指令的时序图:
图 45.4.6 读状态寄存器时序
图 45.4.7 全擦除指令时序
图 45.4.8 区块擦除指令时序
从上面这6个指令时序图中可以看到只有读数据指令和访问状态寄存器指令是使用到了DO(MISO)数据线,因为要读出数据,其他都是往FLASH中写入数据。时序不难,关键是怎么实现它,下面我们一起来看代码,因为代码较长所以我一段一段来给大家讲解。
- <font size="4">1 module spi_drive(</font>
- <font size="4">2 </font>
- <font size="4">3 input clk_100m ,</font>
- <font size="4">4 input sys_rst_n ,</font>
- <font size="4">5 </font>
- <font size="4">6 //user interface</font>
- <font size="4">7 input spi_start ,//spi开启使能。</font>
- <font size="4">8 input [7:0 ] spi_cmd ,//FLAH操作指令</font>
- <font size="4">9 input [23:0] spi_addr ,//FLASH地址</font>
- <font size="4">10 input [7:0 ] spi_data ,//FLASH写入的数据</font>
- <font size="4">11 input [3:0 ] cmd_cnt ,</font>
- <font size="4">12 </font>
- <font size="4">13 output idel_flag_r ,//空闲状态标志的上升沿 </font>
- <font size="4">14 output reg w_data_req ,//FLASH写数据请求 </font>
- <font size="4">15 output reg [7:0] r_data ,//FLASH读出的数据</font>
- <font size="4">16 output reg erro_flag ,//读出的数据错误标志</font>
- <font size="4">17 </font>
- <font size="4">18 //spi interface</font>
- <font size="4">19 output reg spi_cs ,//SPI从机的片选信号,低电平有效。</font>
- <font size="4">20 output reg spi_clk ,//主从机之间的数据同步时钟。</font>
- <font size="4">21 output reg spi_mosi ,//数据引脚,主机输出,从机输入。</font>
- <font size="4">22 input spi_miso //数据引脚,主机输入,从机输出。</font>
- <font size="4">23 </font>
- <font size="4">24 );</font>
- <font size="4">25 </font>
- <font size="4">26 //状态机</font>
- <font size="4">27 parameter IDLE =4'd0;//空闲状态</font>
- <font size="4">28 parameter WEL =4'd1;//写使能状态</font>
- <font size="4">29 parameter S_ERA =4'd2;//扇区擦除状态</font>
- <font size="4">30 parameter C_ERA =4'd3;//全局擦除</font>
- <font size="4">31 parameter READ =4'd4;//读状态</font>
- <font size="4">32 parameter WRITE =4'd5;//写状态</font>
- <font size="4">33 parameter R_STA_REG =4'd6;</font>
- <font size="4">34 </font>
- <font size="4">35 //指令集</font>
- <font size="4">36 parameter WEL_CMD =8'h06;</font>
- <font size="4">37 parameter S_ERA_CMD =8'h20;</font>
- <font size="4">38 parameter C_ERA_CMD =8'hc7;</font>
- <font size="4">39 parameter READ_CMD =8'h03;</font>
- <font size="4">40 parameter WRITE_CMD =8'h02;</font>
- <font size="4">41 parameter R_STA_REG_CMD=8'h05;</font>
复制代码
代码第1~24行是定义了SPI驱动模块的端口,每个端口我都写了注释,这里就不重复啰嗦了,大家可以直接看端口注释,需要注意的是第11行接入了一个cmd_cnt信号,这个信号是干嘛的呢?它是指令个数计数器,FLASH读写模块向SPI驱动模块发送指令,每发送一个指令cmd_cnt就会加一,我们在检查读出的数据是否正确时就要用到这个计数器,当计数器为6时刚好FLASH处于第一次读数据状态,在此状态我们检查读取的数据是否正确。之后会执行区块擦除指令将我们写入的数据擦除掉,所以之后再次读取数据时是读不到数据的。
代码第27~41行分别是SPI驱动模块的状态机状态和FLASH指令集,在下文讲解三段式状态机时会详细讲解。
代码68~211行大家看起来可能就比较头疼了,怎么会这么多always语句块,其实这些语句块都是辅助后面的三段式状态机运行的,下面我来带领大家一个语句块一个语句块来讲解。
首先看代码68~80行,这段代码是为了产生空闲状态的标志的上升沿。我们整个状态机的运行机制是每完成一个状态就会返回到空闲状态,处于空闲状态就意味着可以执行下一个指令,那怎么知道我此时处于空闲状态呢?在代码68行就定义了一个空闲状态标志(idel_flag),每当状态机当前状态处于空闲时就拉高idel_flag,反之拉低。而代码69到80行是为了产生idel_flag标志位的上升沿,将这个上升沿输出出去,给其他子模块使用。
代码82~89行这个语句块是用来产生写数据请求的,我们可以看到第85行代码的判断条件,其中有个bit_cnt,这个bit_cnt是数据位计数器,每向FLASH传输1bit数据,这个计数器就会加一。还有一个clk_cnt,这其实也是一个计数器,这个计数器的位宽为1,因此它的值只有0和1,这个计数器的作用是辅助bit_cnt计数器的,一个bit_cnt的时钟周期内clk_cnt会变化两次,这样就可以根据clk_cnt判断SPI驱动时钟是处于上升沿数据采样状态还是下降沿数据传输状态,处于采样状态数据必须保持稳定,当采样结束我们就可以变化数据了。了解了这两个计数器后再看第85行代码就好理解了,当状态机处于写数据状态、bit_cnt大于等于30(因为写数据时需要8bit指令加上24bit数据,所以前32bit都不用提供数据,这里之所以是bit_cnt>=30而不是bit_cnt>32是因为需要提前两个时钟周期发送数据请求,为什么提前两个周期请继续往下看)且(bit_cnt+2)%8==0(bit_cnt+2也是为了提前两个时钟周期,对8进行取模运算是因为一个字节数据等于8bit因此bit_cnt每增加8才向数据读写模块发送一次写数据请求)和clk_cnt==0时向数据读写模块发送写数据请求,这样读写数据模块就会给SPI驱动模块发送一字节数据。
代码第91~98行是当状态机处于读状态时将读出的数据进行移位寄存,第94行的判断条件也很好理解,前32bit是指令和地址,从第33bit开始是读出的数据,每次在clk_cnt==0时(SPI时钟处于数据传输状态)移位寄存一次。
代码第100~129行要一起看,其中109~116行是将移位寄存的数据取出来传递给r_data,这个r_data就是最终读出的数据了。第112行的判断条件就是每隔8bit提取一次数据,并且在clk_cnt==1时提取,因为移位寄存是在clk_cnt==0时进行的。然后再看代码100~107行,其实就是定义了一个计数器,r_data每提取一字节数据data_check就加一(注意第103行代码,data_check是在bit_cnt>=40以后才开始加一的,因为r_data第一次提取数据的时候是当bit_cnt==40时提取的,而第一个数据刚好就是0,因此在这个节点data_check不需要累加,因为data_check本身就是0,如果累加就会造成data_check与r_data刚好错位了),因为我们写入的数据是0到255,所以只要读出的数据也是0~255那么就代表读出的数据正确,因此代码第118~129就是比较data_check和r_data的值在第一次读取数据状态是否完全一样,只要有一个不同就代表读写错误就会拉高erro_flag错误标志。
代码第131~140行是将FLASH读写模块发送过来的数据移位寄存,从这里就可以看出为什么上文中写数据请求要提前两个时钟周期了,首先写数据请求发送出去到数据传递过来需要一个时钟周期,而数据过来后不是直接放到MOSI数据线上传递的,而是缓存进data_buffer,然后再进行传输的,这又要一个时钟周期。
代码第142~152行是将FLASH读写模块发送过来的指令先缓存到cmd_buffer里,然后再移位寄存。这里注意代码第145行又引入了一个新的计数器dely_cnt。这个计数器是当CS信号拉低后开始计数,数到一就停止计数。为什么要做这个计数器呢?上文讲解指令时序的时候我已经说过了,当CS拉低后是不能立即进行指令传输的,从CS的下降沿到SPI时钟有效这一段期间至少要保持5ns的延迟等待。因此dely_cnt就是起到这个作用,CS拉低dely_cnt开始计数,从0数到1刚好延迟了10ns,那么我们利用这个空闲就可以把FLASH读写模块传过来的指令缓存到cmd_buffer了,然后当dely_cnt数到一cmd_buffer开始移位寄存,将一个字节的数据1比特1比特的传输出去。
代码154~163行是地址的移位寄存,它和指令的移位寄存一模一样,这里不再重复赘述。
代码165~185行和代码200~211行就是产生clk_cnt、dely_cnt和bit_cnt这三个计数器的。但是大家注意代码第187~198又定义了一个新的计数器dely_state_cnt,这个计数器就是单纯的辅助状态机去工作了,当CS信号拉高它就开始计数,拉低就停止计数。它最大的作用就是当执行完使能指令后拉高CS信号不能立刻执行其他指令,还需要等待100ns左右才能拉低CS去执行其他指令,那怎么知道已经等待了100ns呢,就可以去看dely_state_cnt的值了,当它数到10代表已经等待了100ns。
接下来我们继续往下看,看看状态机是如何运转的:
- <font size="4">212 </font>
- <font size="4">213 //三段式状态机</font>
- <font size="4">214 always @(posedge clk_100m or negedge sys_rst_n )begin</font>
- <font size="4">215 if(!sys_rst_n)</font>
- <font size="4">216 current_state<=IDLE;</font>
- <font size="4">217 else</font>
- <font size="4">218 current_state<=next_state;</font>
- <font size="4">219 end</font>
- <font size="4">220 </font>
- <font size="4">221 always @(*)begin</font>
- <font size="4">222 </font>
- <font size="4">223 case(current_state)</font>
- <font size="4">224 </font>
- <font size="4">225 IDLE: begin</font>
- <font size="4">226 if(spi_start&&spi_cmd==WEL_CMD)</font>
- <font size="4">227 next_state=WEL;</font>
- <font size="4">228 else if(spi_start&&spi_cmd==C_ERA_CMD)</font>
- <font size="4">229 next_state=C_ERA;</font>
- <font size="4">230 else if(spi_start&&spi_cmd==S_ERA_CMD)</font>
- <font size="4">231 next_state=S_ERA;</font>
- <font size="4">232 else if(spi_start&&spi_cmd==READ_CMD)</font>
- <font size="4">233 next_state=READ;</font>
- <font size="4">234 else if(spi_start&&spi_cmd==WRITE_CMD)</font>
- <font size="4">235 next_state=WRITE;</font>
- <font size="4">236 else if(spi_start&&spi_cmd==R_STA_REG_CMD)</font>
- <font size="4">237 next_state=R_STA_REG;</font>
- <font size="4">238 else</font>
- <font size="4">239 next_state=IDLE;</font>
- <font size="4">240 end</font>
- <font size="4">241 </font>
- <font size="4">242 WEL: begin</font>
- <font size="4">243 if(stdone&&bit_cnt>=8)</font>
- <font size="4">244 next_state=IDLE;</font>
- <font size="4">245 else</font>
- <font size="4">246 next_state=WEL;</font>
- <font size="4">247 end</font>
- <font size="4">248 </font>
- <font size="4">249 S_ERA: begin</font>
- <font size="4">250 if(stdone)</font>
- <font size="4">251 next_state=IDLE;</font>
- <font size="4">252 else</font>
- <font size="4">253 next_state=S_ERA;</font>
- <font size="4">254 end</font>
- <font size="4">255 C_ERA: begin </font>
- <font size="4">256 if(stdone)</font>
- <font size="4">257 next_state=IDLE;</font>
- <font size="4">258 else</font>
- <font size="4">259 next_state=C_ERA;</font>
- <font size="4">260 end</font>
- <font size="4">261 READ: begin </font>
- <font size="4">262 if(stdone&&bit_cnt>=8)</font>
- <font size="4">263 next_state=IDLE;</font>
- <font size="4">264 else</font>
- <font size="4">265 next_state=READ;</font>
- <font size="4">266 end</font>
- <font size="4">267 WRITE: begin </font>
- <font size="4">268 if(stdone&&bit_cnt>=8)</font>
- <font size="4">269 next_state=IDLE;</font>
- <font size="4">270 else</font>
- <font size="4">271 next_state=WRITE;</font>
- <font size="4">272 end</font>
- <font size="4">273 R_STA_REG: begin </font>
- <font size="4">274 if(stdone)</font>
- <font size="4">275 next_state=IDLE;</font>
- <font size="4">276 else</font>
- <font size="4">277 next_state=R_STA_REG;</font>
- <font size="4">278 end</font>
- <font size="4">279 </font>
- <font size="4">280 default: next_state=IDLE; </font>
- <font size="4">281 endcase </font>
- <font size="4">282 end</font>
- <font size="4">283 </font>
复制代码
代码第213~283行就是三段式状态机的前两段了,其中代码的第214~219行是状态机第一段,在时序状态下跳转状态机。代码第221~282行是状态机的第二段,在组合逻辑下判断状态机的跳转条件。从第二段状态机可以看到每当执行完一个状态状态机就会跳到空闲状态,等待FLASH读写模块传递指令,识别到哪个指令就跳转到对应的状态去执行。
代码第284~447行就是状态机的第三段了,在每个状态执行对应的逻辑。我们来一个状态一个状态的分析。
空闲状态(IDLE):空闲状态好理解,就是什么操作也没有,所以此时将spi_cs信号拉高,spi_clk和spi_mosi信号拉低。
使能状态(WEL):使能状态首先我们要把状态完成信号拉(stdone)低,否则第二段状态机直接检测到stdone拉高就会跳转状态。然后拉低片选信号spi_cs,此时FLASH芯片被选中,但是不能立刻去执行指令操作,我们等待dely_cnt==1,然后在bit_cnt<8时发送使能指令。此时将spi_clk不断取反相当于二分频产生50Mhz的驱动时钟来驱动FLASH,这里要注意一点,我们定义了一个spi_clk0的中间变量,然后将spi_clk0的值再传给spi_clk,这样操作相当于将spi_clk延迟了一个时钟(注意这里延迟的一个时钟时基于100Mhz的,相对于FLASH操作时钟50Mhz来说就是延迟了半个时钟,这样刚好可以让时钟的第一个边沿处于第一个数据稳定期的正中间,达到SPI协议处于模式零的状态),这样刚好可以让第一个spi_clk时钟边沿处于数据采样状态。我们可以用SignalTap来抓取波形看一下,波形图如下所示:
图 45.4.9 使能指令波形图
从上图中可以看到当bit_cnt==8时其实spi_clk的最后一个上升沿已经过去了,说明此时数据已经被采样(最后一个spi_clk没必要保持一个完整周期,只要上升沿采样到数据即可,当然保持一个完整周期也行),那么我们就可以让spi_clk拉低了,因此当bit_cnt==8时我们就可以拉高stdone了,说明使能状态完成。
全擦除状态(C_ERA):全擦除状态大家就可以看到这里使用了上文提到的dely_state_cnt计数器了,因为使能状态结束后不能立刻执行其他指令,要等待100ns左右才能执行下一个指令,因此执行全擦除指令时我们先等待dely_state_cnt数到10然后再拉低片选信号进行下一步操作。对应的波形图如下所示:
图 45.4.10 全擦除指令波形图
从上图中可以看到执行完使能指令后spi_cs等待了一段时间后才拉低,然后开始执行全擦除指令,全擦除指令执行完后,要等待相当长一段时间,这段期间我们要访问状态寄存器,状态寄存器的BUSY位拉低代表擦除完成,否则不能执行其他指令。读取状态寄存器的波形图如下:
图 45.4.11 读取状态寄存器波形
从上图中可以看到当开始读取数据时MISO就有数据了,提醒大家注意,读取数据时是spi_clk的下降沿对应一个数据,刚好和写入数据反过来,上图中红色方框标记的就是BUSY位,大家可以看到bit_cnt等于16或者24、32、40时都是BUSY位,它们全部处于高电平,所以此时全擦除指令还没有生效,直到BUSY位为低电平时代表全擦除指令完成擦除操作,如下图所示:
图 45.4.12 擦除完成
从上图大家可以看到当bit_cnt等于114335320时BUSY位拉低了,代表此时擦除完成,可以进行下一个指令操作了。
接下来还有区块擦除状态,读写状态等都是大同小异,无非就是bit_cnt在小于8时给指令,8到32期间给地址,之后是给数据,波形图就不给大家一个一个列出来了,大家可以把我们的例程下载的开发板中去自己抓取波形看一下。为了方便大家理解整个状态的运转这里给大家画了一个状态跳转图,如下所示:
图 45.4.13 状态切换
到这里SPI驱动模块就给大家讲解完了。下面我们再来看看其他几个模块,FLASH读写模块的代码如下:
- <font size="4">1 module flash_rw(</font>
- <font size="4">2 </font>
- <font size="4">3 input sys_clk ,</font>
- <font size="4">4 input sys_rst_n ,</font>
- <font size="4">5 </font>
- <font size="4">6 input idel_flag_r ,</font>
- <font size="4">7 input w_data_req ,</font>
- <font size="4">8 output reg[3:0 ] cmd_cnt ,</font>
- <font size="4">9 output reg spi_start ,//spi开启使能。</font>
- <font size="4">10 output reg[7:0 ] spi_cmd ,</font>
- <font size="4">11 output reg[7:0 ] spi_data </font>
- <font size="4">12 </font>
- <font size="4">13 );</font>
- <font size="4">14 </font>
- <font size="4">15 //指令集</font>
- <font size="4">16 parameter WEL_CMD =16'h06;</font>
- <font size="4">17 parameter S_ERA_CMD =16'h20;</font>
- <font size="4">18 parameter C_ERA_CMD =16'hc7;</font>
- <font size="4">19 parameter READ_CMD =16'h03;</font>
- <font size="4">20 parameter WRITE_CMD =16'h02;</font>
- <font size="4">21 parameter R_STA_REG_CMD=8'h05 ;</font>
- <font size="4">22 </font>
- <font size="4">23 //reg define</font>
- <font size="4">24 reg[3:0] flash_start;</font>
- <font size="4">25 </font>
- <font size="4">26 //*****************************************************</font>
- <font size="4">27 //** main code</font>
- <font size="4">28 //*****************************************************</font>
- <font size="4">29 </font>
- <font size="4">30 always @(posedge sys_clk or negedge sys_rst_n )begin</font>
- <font size="4">31 if(!sys_rst_n)</font>
- <font size="4">32 flash_start<=0;</font>
- <font size="4">33 else if(flash_start<=5)</font>
- <font size="4">34 flash_start<=flash_start+1;</font>
- <font size="4">35 else</font>
- <font size="4">36 flash_start<=flash_start;</font>
- <font size="4">37 end</font>
- <font size="4">38 </font>
- <font size="4">39 always @(posedge sys_clk or negedge sys_rst_n )begin</font>
- <font size="4">40 if(!sys_rst_n)</font>
- <font size="4">41 cmd_cnt<=0;</font>
- <font size="4">42 else if(flash_start==4)</font>
- <font size="4">43 spi_start<=1'b1;</font>
- <font size="4">44 else if(idel_flag_r&&cmd_cnt<10)begin</font>
- <font size="4">45 cmd_cnt<=cmd_cnt+1;</font>
- <font size="4">46 spi_start<=1'b1;</font>
- <font size="4">47 end</font>
- <font size="4">48 else begin</font>
- <font size="4">49 cmd_cnt<=cmd_cnt;</font>
- <font size="4">50 spi_start<=1'b0;</font>
- <font size="4">51 end</font>
- <font size="4">52 end</font>
- <font size="4">53 </font>
- <font size="4">54 always @(posedge sys_clk or negedge sys_rst_n )begin</font>
- <font size="4">55 if(!sys_rst_n)</font>
- <font size="4">56 spi_data<=8'd0;</font>
- <font size="4">57 else if(w_data_req)</font>
- <font size="4">58 spi_data<=spi_data+1'b1;</font>
- <font size="4">59 else</font>
- <font size="4">60 spi_data<=spi_data;</font>
- <font size="4">61 end</font>
- <font size="4">62 </font>
- <font size="4">63 always @(*)begin</font>
- <font size="4">64 case(cmd_cnt)</font>
- <font size="4">65 0:spi_cmd=WEL_CMD;</font>
- <font size="4">66 1:spi_cmd=C_ERA_CMD;</font>
- <font size="4">67 2:spi_cmd=R_STA_REG_CMD;</font>
- <font size="4">68 3:spi_cmd=WEL_CMD;</font>
- <font size="4">69 4:spi_cmd=WRITE_CMD;</font>
- <font size="4">70 5:spi_cmd=R_STA_REG_CMD;</font>
- <font size="4">71 6:spi_cmd=READ_CMD;</font>
- <font size="4">72 7:spi_cmd=WEL_CMD;</font>
- <font size="4">73 8:spi_cmd=S_ERA_CMD;</font>
- <font size="4">74 9:spi_cmd=R_STA_REG_CMD;</font>
- <font size="4">75 10:spi_cmd=READ_CMD;</font>
- <font size="4">76 </font>
- <font size="4">77 default:;</font>
- <font size="4">78 endcase</font>
- <font size="4">79 end</font>
- <font size="4">80 </font>
- <font size="4">81 endmodule</font>
复制代码
FLASH读写模块的代码是比较简单的,主要就是用来生成指令和数据。代码30~37行定义了一个计数器flash_start,这个计数器的作用是产生一个spi_start开始信号用来启动SPI驱动模块,在代码第42行,当flash_start等于4时拉高spi_start,只拉高一个时钟周期。代码第39~52行是产生cmd_cnt计数器,每当检测到SPI驱动模块的状态空闲标志位的上升沿时cmd_cnt就会加一,而代码的第63~79行会根据cmd_cnt的值来发送不同的指令给SPI驱动模块。代码第54~61行是产生写数据,每当检测到一次写数据请求spi_data就会加一,写数据请求一共会拉高255次,因此写入FLASH的数据就是0~255。
FLASH读写模块代码分析完后本节FLASH读写实验的程序设计部分就结束了,至于顶层模块和LED灯模块非常简单,就不再讲解了。
1.5下载验证
弄懂了代码之后接下来我们就来下板子验证一下,首先将开发板电源线掺入,然后将下载线JTAG一端插入板子的JTAG接口,USB端插入电脑USB接口,如下图所示:
图 45.5.1 硬件连接图
连接好硬件后打开板子电源开关,将sof文件下载到板子上去,可以看到LED0常亮,说明FLASH读写数据正确,如下图所示:
图 45.5.2 读写成功现象
到这里就说明本次FLASH读写实验成功了,有的同学可能会问第一次写入并读出是成功的,那么之后的区块擦除是否成功呢?我们可以打开SignalTap观察最后区块擦除后还能不能读到数据。下面给出第一次读取数据波形图和区块擦除后波形图:
图 45.5.3 第一次读取数据正确
图 45.5.4 区块擦除后再次读取数据
从上面两幅图中可以看到第一次读取数据是正确的,区块擦除后整个MISO处于高电平,说明数据被成功擦除。
|
阿莫论坛20周年了!感谢大家的支持与爱护!!
曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
|