8. 基于SPI协议的Flash驱动控制

8.1. 章节导读

本章节读者要学习掌握SPI通讯协议的基本知识和概念,理解掌握基于SPI总线的Flash驱动控制的相关内容,熟悉 FPGA与SPI器件之间数据通信流程。根据所学知识设计一个基于SPI总线的Flash驱动控制器,实现FPGA对 Flash存储器的数据写入、数据读取以及扇区擦除和全擦除操作,并上板验证。

8.2. 理论学习

在进行控制器的设计之前,我们先对涉及到的理论知识做一下讲解。既然是要设计基于SPI 总线的 Flash驱动控制器,首先要讲解的就是重要的通讯协议之一:SPI通讯协议。

SPI(Serial Peripheral Interface,串行外围设备接口)通讯协议,是Motorola公司提出的一种同步串行接口技术,是一种高速、全双工、同步通信总线,在芯片中只占用四根管脚用来控制及数据传输,广泛用于EEPROM、Flash、ADC(数模转换器)、DSP(数字信号处理器)以 及数字信号解码器上,是常用的也是较为重要的通讯协议之一。

SPI通讯协议的优点是支持全双工通信,通讯方式较为简单,且相对数据传输速率较快;缺点是没有指定的流控制,没有应答机制确认数据是否接收,与IIC总线通讯协议相比,在数据可靠性上有一定缺陷,IIC总线通讯协议的相关内容会在后面章节进行讲解。

对于SPI通讯协议的相关内容我们分为物理层、协议层两部分进行讲解,具体内容如下。

8.2.1. SPI物理层

对于SPI协议的物理层,需要讲解的就是SPI通讯设备的连接方式和设备引脚的功能描述。

SPI通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分,根据从机设备的个数,SPI通讯设备之间的连接方式可分为一主一从和一主多从,具体见图 38‑1、图 38‑2。

SPI002

图 38‑1 一主一从SPI通讯设备连接图

SPI003

图 38‑2 一主多从SPI通讯设备连接图

SPI通讯协议包含1条时钟信号线、2条数据总线和1条片选信号线, 时钟信号线为SCK,2条数据总线分别为MOSI(主输出从输入)、MISO(主输入从输出),片选信号线为,它们的作用介绍如下:

  1. SCK (Serial Clock):时钟信号线,用于同步通讯数据。由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不同,两个设备之间通讯时,通讯速率受限于低速设备。

  2. MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,数据方向由主机到从机。

  3. MISO (Master Input,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,数据方向由从机到主机。

(4) (Chip Select):片选信号线,也称为CS_N,以下用CS_N表示。当有多个SPI从设备与SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同使用这3条总线;而每个从设备都有独立的这一条CS_N信号线,本信号线独占主机 的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用CS_N信号线来寻址,当主机要选择从设备时,把该从设备的CS_N信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI通讯。所以SPI通讯以CS_N线置低电平为开始信号,以CS_N线被拉高作为结束信号。

8.2.2. SPI协议层

在这一小结我们主要说明一下SPI通讯协议的通讯过程,在此之前先来介绍一下,SPI通信协议的通讯模式。

8.2.2.1. CPOL/CPHA及通讯模式

SPI通讯协议一共有四种通讯模式,模式0、模式1、模式2以及模式3,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了空闲状态(CS_N为高电平,设备未被选中)时SCK时钟信号的电平状态,CPHA规定了数据采样是在SCK时钟的奇数边沿还是偶数边沿。

SPI通讯协议的4种模式如下,通讯模式时序图,具体见图 38‑3。

模式0:CPOL= 0,CPHA=0。空闲状态时SCK串行时钟为低电平;数据采样在SCK时钟的奇数边沿,本模式中,奇数边沿为上升沿;数据更新在SCK时钟的偶数边沿,本模式中,偶数边沿为下降沿。

模式1:CPOL= 0,CPHA=1。空闲状态时SCK串行时钟为低电平;数据采样在SCK时钟的偶数边沿,本模式中,偶数边沿为下降沿;数据更新在SCK时钟的奇数边沿,本模式中,偶数边沿为上升沿。

模式2:CPOL= 1,CPHA=0。空闲状态时SCK串行时钟为高电平;数据采样在SCK时钟的奇数边沿,本模式中,奇数边沿为下降沿;数据更新在SCK时钟的偶数边沿,本模式中,偶数边沿为上升沿。

模式3:CPOL= 1,CPHA=1。空闲状态时SCK串行时钟为高电平;数据采样在SCK时钟的偶数边沿,本模式中,偶数边沿为上升沿;数据更新在SCK时钟的奇数边沿,本模式中,偶数边沿为下降沿。

SPI004

图 38‑3 SPI通讯模式时序图

对于4种通讯模式中,CPOL比较好理解,就是表示设备未被选中的空闲状态时,串行时钟SCK的电平状态,CPOL = 0,空闲状态时SCK为低电平,CPOL = 1,空闲状态时SCK为高电平;CPHA的不同参数则规定了数据采样是在SCK时钟的奇数边沿还是偶数边沿,CPHA = 0,数据采样是在SCK时钟的奇数边沿,CPHA = 1,数据采样是在SCK时钟的偶数边沿,这里不使用上升沿或下降沿表示,是因为不同模式下,奇数边沿或偶数边沿与上升沿或下降沿的对应不是固定的,为了便于读者理解,此处做一下说明。

SPI005

图 38‑4 CPHA=0时的SPI通讯模式

结合图 38‑4,我们进行一下简要分析。首先,根据SCK在空闲状态时的电平,分为两种情况。CPOL = 0,SCK信号线在空闲状态为低电平; CPOL = 1,SCK信号线在空闲状态为高电平。

无论CPOL = 0还是1,我们配置的时钟相位CPHA = 0,在图中可以看到,采样时刻都是在SCK的奇数边沿。注意当CPOL=0的时候,时钟的奇数边沿是上升沿,而CPOL=1的时候,时钟的奇数边沿是下降沿。所以SPI的采样时刻不是由上升/下降沿决定的。MOSI和MISO数据线的有效信号在SCK的奇 数边沿保持不变,数据信号将在SCK奇数边沿时被采样,在非采样时刻,MOSI和MISO的有效信号才发生切换。

类似地,当CPHA=1时,不受CPOL的影响,数据信号在SCK的偶数边沿被采样,具体见图 38‑5。

SPI006

图 38‑5 CPHA=1时的SPI通讯模式

8.2.2.2. SPI基本通讯过程

上文中,我们详细介绍了SPI通讯协议的4中通讯模式,其中模式0和模式3比较常用,下面我们以模式0为例,为大家讲解一下SPI基本的通讯过程。SPI模式0通讯时序图,具体见图 38‑6。

SPI007

图 38‑6 SPI模式0通讯时序图

此图表示的是主机视角的通讯时序。SCK、MOSI、CS_N信号均由主机控制产生,SCK是时钟信号,用以同步数据,MOSI是主机输出从机输入信号,主机通过此信号线传输数据给从机,CS_N为片选信号,用以选定从机设备,低电平有效;而MISO的信号由从机产生,主机通过该信号线读取从机的数据。MOSI与MI SO的信号只在CS_N为低电平的时候才有效,在SCK的每个时钟周期MOSI和MISO传输一位数据。

8.2.2.3. 通讯的起始和停止信号

在图 38‑6中的标号处,CS_N信号线由高变低,是SPI通讯的起始信号。CS_N是每个从机各自独占的信号线,当从机在自己的CS_N线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号处,CS_N信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

8.2.2.4. 数据有效性

SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行并没有作硬性规定,但要保证两个SPI通讯设备之间使用同样的协定,一般都会采图 38‑6中的MSB先行模式。

观察图中的标号处,MOSI及MISO的数据在SCK的上升沿期间变化输出,在SCK的下降沿时被采样。即在SCK的下降沿时刻,MOSI及MISO的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及MISO为下一次表示数据做准备。

SPI每次数据传输可以8位或16位为单位,每次传输的单位数不受限制。

8.3. 实战演练

对于Flash芯片,相信大家都不陌生,它是一种非易失性储存芯片,在进行单片机、STM32等MCU的学习时,估计大家都用到过,那么在学习FPGA时,我们也会用到它,因为它是非易失性存储器,掉电后数据不会丢失,在FPGA工程的设计中,Flash主要用作外接芯片来存储FPGA程序,使FPGA在上电后可以立 即执行我们想要执行的程序。SPI-Flash芯片就是支持SPI通讯协议的Flash芯片。

在上一部分,我们讲解了SPI通讯协议的相关知识,为读者更好的掌握SPI通讯协议,为加深读者对SPI通讯协议的理解,在后文中我们会加入几个有关SPI-Flash芯片读写操作的实验来加深读者对SPI通讯协议的理解。

8.3.1. SPI-Flash全擦除实验

我们在平时对工程进行上板验证的时候,可以通过两种方式烧录程序:一种是将程序下载到FPGA内部的SRAM之中,这种方式烧录过程耗时较短,但缺点是掉电后程序会丢失,再次上电后要重新烧录程序;另外一种就是将程序固化到FPGA外部挂载的Flash芯片中,Flash芯片是非易失性存储器,程序掉电后不会丢失,重 新上电后会执行掉电前烧录到Flash中的程序,但是烧录程序耗时较长。

如果我们对程序验证完成后,想要将固化到Flash中的程序删除时,可以通过两种方式,分别是全擦除和扇区擦除。

在本小节中我们将带领读者分别编写全擦除工程和扇区擦除工程,使读者对这两种擦除方式有清醒的认识,让读者掌握两种擦除方式的实现方法。

Flash的全擦除,顾名思义就是将Flash所有的存储空间都进行擦除操作,使各存储空间内存储数据恢复到初始值。FPGA要实现Flash的全擦除也有有两种方式。

方式一:利用FPGA编译软件,通过Quartus软件的“programmer”窗口,将烧录到Flash的*.jic文件擦除,具体见图 38‑7;方式二:编写全擦除程序,实现Flash芯片的全擦除,就是我们下面要进行的实验。

SPI008

图 38‑7 Quartus软件实现Flash芯片全擦除

8.3.1.1. 实验目标

事先向Flash芯片中烧录流水灯程序,FPGA上电执行流水灯程序,下载Flash芯片全擦除程序到FPGA内部SRAM并执行,擦除Flash芯片中烧录的流水灯程序,FPGA重新上电后,无程序执行。

8.3.1.2. 硬件资源

征途Mini开发板使用的Flash型号为W25Q16存储容量为16Mbit(2M字节)。Flash实物图与原理图,如图 38‑8、图 38‑9所示。

SPI009

图 38‑8 板载Flash实物图

SPI010

图 38‑9 板载Flash原理图

8.3.1.3. 操作时序

在本小节中,我们结合数据手册来详细说明一下SPI-Flash芯片全擦除操作的相关内容。

全擦除(Bulk Erase)操作,简称BE,操作指令为8’b1100_0111(C7h),具体见图 38‑10。

SPI011

图 38‑10 全擦除操作指令

由数据手册中全擦除介绍部分可知,全擦除指令是将Flash芯片中的所有存储单元设置为全1,在Flash芯片写入全擦出指令之前,需要先写入写使能(WREN)指令,将芯片设置为写使能锁存(WEL)状态;随后要拉低片选信号,写入全擦除指令,在指令写入过程中,片选信号始终保持低电平,待指令被芯片锁存后,将片选 信号拉高;全擦除指令被锁存并执行后,需要等待一个完整的全擦除周期(tBE),才能完成Flash芯片的全擦除操作。全擦除操作的详细介绍及时序图,具体见图 38‑11。

SPI012

图 38‑11 全擦除操作详细介绍及操作时序

上文全擦除操作中我们提到,全擦除(BE)指令写入前必须先对Flash芯片写入写使能(WREN)指令,使芯片处于写使能锁存(WEL)状态。此状态下写入全擦除指令才会被Flash芯片响应,否则,全擦除指令无效。

所以,接下来我们要详细说明一下写使能指令的相关内容。

写使能(Write Enable)指令,简称WREN,操作指令为8’b0000_0110(06h),具体见图 38‑12。

SPI013

图 38‑12 写使能指令

由数据手册中写使能介绍部分可知,写使能指令可将Flash芯片设置为写使能锁存(WEL)状态;在每一次页写操作(PP)、扇区擦除(SE)、全擦除(BE)和写状态寄存器(WRSR)操作之前,都需要先进行写使能指令写入操作。操作时序为先拉低片选信号,写入写使能指令,在指令写入过程中,片选信号始终保持低电平 ,指令写入完成后,将片选信号拉高。写使能指令的详细介绍及时序图,具体见图 38‑13。

SPI014

图 38‑13 写使能指令详细介绍及操作时序

虽然全擦除操作和写使能操作的相关内容介绍完毕,但我们还不能开始实验工程的设计,因为我们还有十分重要的知识点需要详细说明,那就是Flash芯片的串行输入时序。

写使能指令、全擦除指令以及其它操作指令在写入Flash芯片时要严格遵循芯片的串行输入时序。串行输入时序图,具体见图 38‑14。

SPI015

图 38‑14 串行输入时序图

如图所示,相关操作指令在写入芯片之前需要先拉低片选信号,在片选信号保持低电平时将指令写入数据输入端口,指令写入完毕,拉高片选信号,数据输出端口在指令写入过程中始终保持高阻态。

图中定义了许多时间参数,其中有三个我们需要格外注意,分别是tSLCH、tCHSH和tSHSL。时间参数参考数值,具体见图 38‑15。

SPI016

图 38‑15 时间参数参照表

由图 38‑14可知,片选信号自下降沿始到第一个有效数据写入时止,这一段等待时间定义为片选信号有效建立时间 tSLCH,由图 38‑15可知,这一时间段必须大于等于5ns;片选信号自最后一个有效数据写入时始到片选信号上升沿止,这一段等待时间定义为片选信号有效保持时间tCHSH,由图 38‑15可知,这一时间段必须大于等于5ns;片选信号自上一个上升沿始到下一个下降沿止,这一段等待时间定义为片选信号高电平等待时间tSHSL,由图 38‑15可知,这一时间段必须大于等于100ns。

到这里我们已经讲解了写使能指令、全擦除指令的相关内容和操作时序,对Flash芯片的串行输入时序也做了说明。综上所述,绘制完整全擦除操作时序图如图 38‑16。

SPI017

图 38‑16 完整全擦除操作时序图

8.3.1.4. 程序设计

在操作时序小节,我们对全擦除的相关知识和操作时序做了详细讲解,接下来我们开始实验工程的程序设计。

整体说明

整个全擦除工程调用3个模块,按键消抖模块(key_filter), Flash全擦除模块(flash_be_ctrl)和顶层模块(spi_flash_be)。模块框图,具体见图 38‑17;模块简介,具体见表格 38‑1。

SPI018

图 38‑17 全擦除工程整体框图

表格 38‑1 全擦除工程模块简介

模块名称

功能描述

spi_flsah_be

全擦除工程顶层模块

key_filter

按键消抖模块

flash_be_ctrl

全擦除模块

在整个系统工程中,外部按键负责产生全擦除触发信号,信号由外部进入FPGA,经顶层模块(spi_flash_be)进入按键消抖模块(key_filter),触发信号经消抖处理后输出进入Flash全擦除模块(spi_flash_be),触发信号有效,Flash全擦除模块工作, 生成并输出串行时钟信号(sck)、片选信号(cs_n)和主输出从输入信号(mosi),3路信号输入外部挂载的Flash芯片,Flash芯片接收到全擦除指令,实现Flash芯片全擦除。

整个系统工程包含3个模块,对于按键消抖模块(key_filter)的相关知识在之前的章节已经做过详细介绍,此处不再赘述;顶层模块(spi_flash_be)在后文中会略作说明;对于实验工程 的核心Flash全擦除模块(flash_be_ctrl),我们进行一下详细说明,方便读者理解。

全擦除模块

模块框图

Flash全擦除模块是本实验工程的核心模块,其生成并输出时钟、片选和数据信号,向Flash芯片发送全擦除指令,控制Flash芯片实现全擦除,本模块的模块框图,具体见图 38‑18;模块端口的相关描述,具体见表格 38‑2。

SPI019

图 38‑18 全擦除模块框图

表格 38‑2 模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

key

1Bit

Input

全擦除触发信号

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

波形图绘制

模块框图部分已经对模块功能和输入输出端口功能做了简要说明,那么如何利用输入信号实现模块功能,得到正确的输出信号是本小节将要讲解的内容。在本小节,我们通过波形图的绘制,对各信号波形进行详细讲解,带领读者掌握模块功能的实现方法。

Flash全擦除波形图,具体见图 38‑19、图 38‑20、图 38‑21。

SPI020

图 38‑19 Flash全擦除整体波形图

SPI021

图 38‑20 Flash全擦除局部波形(一)

SPI022

图 38‑21 Flash全擦除局部波形(二)

对于模块波形图的设计与绘制,我们针对各信号进行分部分讲解。

第一部分:输入信号波形绘制

系统上电之后,全擦除模块一直处于初始状态,只有当输入的全擦除触发信号key有效时,模块才会开始执行全擦除的相关操作,触发信号是由外部物理按键生成,经由按键消抖模块做消抖处理后传入。除此之外,输入信号还包含时钟信号sys_clk(50MHz)、复位信号sys_rst_n(低电平有效),输入信号波形图如 下。

SPI023

图 38‑22 输入信号波形图

第二部分:状态机相关信号的波形设计与实现

由前文可知,一个完整的全擦除操作需要对Flash芯片执行两次指令的写入,分别为写使能指令和全擦除指令,而且在片选信号拉低后指令写入前、指令写入完成后片选信号拉高前,以及两指令写入之间都需要做规定时间的等待。

对于这一流程操作,我们可以使用状态机来实现。在模块内部声明状态机状态变量state,定义状态机各状态分别为:初始状态(IDLE)、写使能状态(WR_EN)、两指令间等待状态(DELAY)、全擦除状态(BE)。

状态机状态跳转流程如下:系统上电后,状态机状态变量state一直处于初始状态(IDLE);当传入的全擦除触发信号key有效时,表示实验工程开始执行对Flash芯片的全擦除操作,状态机跳转到写使能状态(WR_EN),同时片选信号拉低,选中要进行全擦除操作的Flash芯片;状态跳转到写使能状态且片选信号 拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入写使能指令,指令写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳转到两指令间等待状态(DELAY);在此状态等待时间tSHSL≥ 100ns后,状态机跳转到全擦除状态(BE),同时片选信号拉低,选中已写入写使能指令的Flash芯片;状态机跳转到全擦除状态且片选信号拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入全擦除指令,指令写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳回初始状态(IDLE),一次完整的全擦除操作完成。

状态机的状态跳转的流程确定了,模块功能实现的整体思路也就确定了。接下来要做的就是状态机的实现,这一过程要解决诸多问题。

第一个问题:片选信号的等待时间tSHSL、 tCHSH、tSHSL的参数确定。

在状态机的状态跳转过程中我们提到,片选信号在某些位置需要做规定时间的等待,但在各状态的等待时间参数是不同的,如果声明多个计数器对各等待时间分别计数或者使用一个计数器、多个等待结束标志的话,虽然能够实现,但较为麻烦,且声明信号较多。不如声明一个通用计数器,以最长等待时间为下限进行等待时间的计数,且各等 待时间没有上限约束,更容易实现。

那么这个等待时间设置多长最为合适呢?我们想到了指令写入过程所需时间。

由Flash芯片数据手册可知,Flash芯片数据读操作的时钟频率(SCK)上限为20MHz,除数据读操作之外的其他操作频率上限为50MHz,为了后续数据读操作不再进行时钟的更改,本实验工程的所有实验的时钟均使用12.5MHz,因为晶振传入时钟为50MHz,通过四分频生成12.5MHz较为方便,且满足 Flash芯片时钟要求。

Flash芯片的指令为串行传输,每个时钟周期只能写入1比特数据,要写入一个完整的单字节指令需要8个完整的SCK时钟周期,即32个完整的系统时钟,系统时钟频率为50MHz,完整指令的写入需要640ns。

这个时间大于片选信号最长等待时间的下限100ns。所以将片选信号的各等待时间的时间参数统一设置为640ns,即32个系统时钟周期。这样声明的计数器不仅可以用作片选信号等待时间计数,也可以用做指令信号写入时间计数,可节省寄存器资源。

所以声明计数器cnt_clk,初值为0,在0-31计数范围内循环计数,在状态机处于初始状态时,始终保持为0;在状态机处于初始状态之外的其他状态时,每个系统时钟周期自加1,计到最大值清0,重新计数。

第二个问题:状态机状态跳转约束条件的确定。

状态机在系统上电之后处于初始状态(IDLE),待输入的全擦除触发信号有效时,状态机由初始状态跳转到写使能状态(WR_EN),但写使能状态后的各状态跳转应该如何进行,跳转条件又是什么?

我们想到可以使用刚刚声明的计数器cnt_clk作为状态跳转的约束条件,但条件并不充分,因为计数器cnt_clk为0-31循环计数,使用其单独作为约束条件的话,状态机在每个cnt_clk的计数周期都会存在满足跳转条件的计数值,所以我们需要声明一个新的计数器来对计数器cnt_clk的计数周期进行计数,使 用两个计数器作为约束条件可以实现状态机的状态跳转。

声明计数器cnt_byte对计数器cnt_clk的计数周期进行计数。对cnt_byte赋初值为0,当状态机处于初始状态(IDLE)时,计数器cnt_byte始终保持初值0;当状态机处于除初始状态外的其他状态时,计数器cnt_byte开始对计数器cnt_clk的计数周期进行计数,cnt_clk每完成一 个完整的循环计数,即cnt_clk = 31时,计数器cnt_byte自加1,其他时刻保持当前值不变。

使用这两个计数器作为约束条件就可以实现状态机的状态跳转,当状态机跳转到写使能状态时,同时片选信号拉低,在cnt_byte = 0、计数器cnt_clk的第1个计数周期,是对片选信号等待时间tSLCH = 640ns的计数;在cnt_byte = 1、计数器cnt_clk的第2个计数周期,是对写使能指令写入时间进行计数;在cnt_byte = 2、计数器cnt_clk的第3个计数周期,是对片选信号等待时间tCHSH = 640ns的计数,第3个周期的计数完成后状态机跳转到两指令间等待状态(DELAY),同时片选信号拉高,计数器开始进行第4个计数周期的计数;此时cnt_byte = 3,这一计数周期是对片选信号两指令之间的等待时间tSHSL = 640ns的计数,计数完成后状态机跳转到全擦除状态(BE),片选信号再次拉低;在cnt_byte = 4、计数器cnt_clk的第5个计数周期,是对片选信号等待时间tSLCH = 640ns的计数;在cnt_byte = 5、计数器cnt_clk的第6个计数周期,是对全擦除指令写入时间进行计数;在cnt_byte = 6、计数器cnt_clk的第7个计数周期,是对片选信号等待时间tCHSH = 640ns的计数,第7个周期的计数完成后状态机跳回到初始状态(IDLE),Flash芯片的全擦除操作完成。

综上所述,绘制上述各信号波形图如下。

SPI024

图 38‑23 状态机相关信号波形图

第三部分:输出相关信号的波形设计与实现

本模块输出信号有3路,分别为片选信号cs_n、串行时钟信号sck和主输出从输入信号mosi。对于片选信号的波形设计与实现在第二部分已经做了详细说明,本部分重点讲解一下串行时钟信号sck、主输出从输入信号mosi以及与其相关信号的波形设计与实现。

在前文中我们提到模块输出的串行时钟为12.5MHz,为系统时钟50MHz通过四分频得到。所以在这里需要声明一个四分频计数器,对系统时钟进行四分频,产生串行时钟信号sck。

本实验使用的Flash芯片使用的是SPI通讯协议的模式0,即CPOL= 0,CPHA=0。空闲状态时SCK串行时钟为低电平;数据采样在SCK时钟的奇数边沿,本模式中,奇数边沿为上升沿;数据更新在SCK时钟的偶数边沿,本模式中,偶数边沿为下降沿,模式0时序图如下图所示。

SPI007

图 38‑24 SPI模式0通讯时序图

由于Flash芯片使用的为模式0的通讯模式,所以串行时钟信号sck在空闲状态保持低电平,在数据传输过程输出12.5MHz频率的时钟信号。在这里我们声明四分频计数器cnt_sck,赋初值为0,只有在cnt_byte计算值为1或5时,即输出写使能指令或全擦除指令时,计数器cnt_sck在0-3范围内循环 计数,计数周期为系统时钟周期,每个时钟周期自加1;使用四分频计数器cnt_sck作为约束条件,生成串行时钟sck,频率为12.5MHz。

四分频计数器cnt_sck、串行时钟信号sck波形图如下图所示。

SPI025

图 38‑25 cnt_sck、sck信号波形图(一)

SPI026

图 38‑26 cnt_sck、sck信号波形图(二)

串行时钟信号sck生成后,根据SPI模式0通讯时序图,本实验中Flash芯片在串行时钟sck的上升沿进行数据采样,我们需要在sck的下降沿进行传输数据的更新,在sck的下降沿对mosi信号写入写使能指令和全擦除指令。

有一点读者还需要注意,Flash芯片的指令或数据的写入要满足高位在前的要求,我们声明一个计数器cnt_bit,左右时实现指令或数据的高低位对调,计数器初值为0,在0-7范围内循环计数,计数时钟为串行时钟sck,每个时钟周期自加1,其他时刻恒为0。

绘制miso、cnt_sck信号波形图如下。

SPI027

图 38‑27 mosi、cnt_sck信号波形图(一)

SPI028

图 38‑28 mosi、cnt_sck信号波形图(二)

讲到这里,模块涉及到的所有信号都已将讲解完毕,将各信号波形进行整合后,就得到了本小节开头处的模块整体波形图。

代码编写

波形图绘制完成后,根据绘制的波形图开始代码编写, Flash全擦除参考代码,具体见代码清单 38‑1。

代码清单 38‑1 Flash全擦除参考代码(flash_be_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
module flash_be_ctrl
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire key , //按键输入信号

output reg cs_n , //片选信号
output reg sck , //串行时钟
output reg mosi //主输出从输入数据
);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter IDLE = 4'b0001 , //初始状态
WR_EN = 4'b0010 , //写状态
DELAY = 4'b0100 , //等待状态
BE = 4'b1000 ; //全擦除状态
parameter WR_EN_INST = 8'b0000_0110, //写使能指令
BE_INST = 8'b1100_0111; //全擦除指令

//reg define
reg [2:0] cnt_byte; //字节计数器
reg [3:0] state ; //状态机状态
reg [4:0] cnt_clk ; //系统时钟计数器
reg [1:0] cnt_sck ; //串行时钟计数器
reg [2:0] cnt_bit ; //比特计数器

////
//\* Main Code \//
////

//cnt_clk:系统时钟计数器,用以记录单个字节
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 5'd0;
else if(state != IDLE)
cnt_clk <= cnt_clk + 1'b1;

//cnt_byte:记录输出字节个数和等待时间
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_byte <= 3'd0;
else if((cnt_clk == 5'd31) && (cnt_byte == 3'd6))
cnt_byte <= 3'd0;
else if(cnt_clk == 31)
cnt_byte <= cnt_byte + 1'b1;

//cnt_sck:串行时钟计数器,用以生成串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sck <= 2'd0;
else if((state == WR_EN) && (cnt_byte == 1'b1))
cnt_sck <= cnt_sck + 1'b1;
else if((state == BE) && (cnt_byte == 3'd5))
cnt_sck <= cnt_sck + 1'b1;

//cs_n:片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(key == 1'b1)
cs_n <= 1'b0;
else if((cnt_byte == 3'd2)&&(cnt_clk == 5'd31)&&(state == WR_EN))
cs_n <= 1'b1;
else if((cnt_byte == 3'd3)&&(cnt_clk == 5'd31)&&(state == DELAY))
cs_n <= 1'b0;
else if((cnt_byte == 3'd6)&&(cnt_clk == 5'd31)&&(state == BE))
cs_n <= 1'b1;

//sck:输出串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sck <= 1'b0;
else if(cnt_sck == 2'd0)
sck <= 1'b0;
else if(cnt_sck == 2'd2)
sck <= 1'b1;

//cnt_bit:高低位对调,控制mosi输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 3'd0;
else if(cnt_sck == 2'd2)
cnt_bit <= cnt_bit + 1'b1;

//state:两段式状态机第一段,状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE: if(key == 1'b1)
state <= WR_EN;
WR_EN: if((cnt_byte == 3'd2) && (cnt_clk == 5'd31))
state <= DELAY;
DELAY: if((cnt_byte == 3'd3) && (cnt_clk == 5'd31))

state <= BE;
BE: if((cnt_byte == 3'd6) && (cnt_clk == 5'd31))
state <= IDLE;
default: state <= IDLE;
endcase

//mosi:两段式状态机第二段,逻辑输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
mosi <= 1'b0;
else if((state == WR_EN) && (cnt_byte == 3'd2))
mosi <= 1'b0;
else if((state == BE) && (cnt_byte == 3'd6))
mosi <= 1'b0;
else if((state == WR_EN)&&(cnt_byte == 3'd1)&&(cnt_sck == 5'd0))
mosi <= WR_EN_INST[7 - cnt_bit]; //写使能指令
else if((state == BE) && (cnt_byte == 3'd5) && (cnt_sck == 5'd0))
mosi <= BE_INST[7 - cnt_bit]; //全擦除指令

endmodule

模块参考代码是参照绘制波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,本小节不再过多叙述。

仿真代码编写

参考代码编写完成后,为检验代码是否够实现预期功能,我们要对代码进行仿真,观察各信号波形是否按照预期规律变化。

在仿真参考代码中,我们模拟生成了频率为50MHz的系统时钟信号、低有效的复位信号和全擦除触发信号,将全擦除模块中3路输出信号连接到Flash仿真模型上,将仿真模型的保持信号hold_n和写保护信号we_n强制置为高电平,主输入从输出信号miso悬空不接。仿真参考代码,具体见代码清单 38‑2。

代码清单 38‑2 全擦除模块仿真参考代码(tb_flash_be_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
\`timescale 1ns/1ns
module tb_flash_be_ctrl();

//wire define
wire cs_n ; //Flash片选信号
wire sck ; //Flash串行时钟
wire mosi ; //Flash主输出从输入信号

//reg define
reg sys_clk ; //模拟时钟信号
reg sys_rst_n ; //模拟复位信号
reg key ; //模拟全擦除触发信号

//时钟、复位信号、模拟按键信号
initial
begin
sys_clk = 1’b1;
sys_rst_n <= 1’b0;
key <= 1’b0;
#100
sys_rst_n <= 1’b1;
#1000
key <= 1’b1;
#20
key <= 1’b0;
end

always #10 sys_clk <= ~sys_clk; //模拟时钟,频率50MHz

//写入Flash仿真模型初始值(全F)
defparam memory.mem_access.initfile = "initmemory.txt";

//------------- flash_be_ctrl_inst -------------
flash_be_ctrl flash_be_ctrl_inst
(
.sys_clk (sys_clk ), //输入系统时钟,频率50MHz,1bit
.sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效,1bit
.key (key ), //按键输入信号,1bit

.sck (sck ), //输出串行时钟,1bit
.cs_n (cs_n ), //输出片选信号,1bit
.mosi (mosi ) //输出主输出从输入数据,1bit
);

//------------- memory -------------
m25p16 memory
(
.c (sck ), //输入串行时钟,频率12.5Mhz,1bit
.data_in (mosi ), //输入串行指令或数据,1bit
.s (cs_n ), //输入片选信号,1bit
.w (1'b1 ), //输入写保护信号,低有效,1bit
.hold (1'b1 ), //输入hold信号,低有效,1bit

.data_out ( ) //输出串行数据
);

endmodule

仿真波形分析

仿真参考代码编写完毕后,我们使用Modelsim进行代码仿真,仿真过后查看仿真波形图,由图可知,输入输出信号以及内部变量的仿真波形图与绘制的波形图时序相同,具体见图 38‑29、图 38‑30以及图 38‑31。

SPI029

图 38‑29 全擦除整体仿真波形图

SPI030

图 38‑30 全擦除局部仿真波形图(一)

SPI031

图 38‑31 全擦除局部仿真波形图(二)

仿真结果显示,接收到擦除指令的仿真模型进行了一个完整的全擦除循环,标志着Flash的全擦除成功,具体见图 38‑32。

SPI032

图 38‑32 全擦除循环结束

注:读者在此要注意的是,芯片数据手册表明,Flash芯片完成一个完整的全擦除周期最少需要40秒,且仿真模型中定义的全擦除周期参数也为40秒,Modelsim若完成40秒仿真,耗费时间较长,为缩短仿真时间,可更改仿真文件夹(M25P16_VG_V12)中的参数定义文件(parameter.v)中的全擦 除周期参数(TBE),具体见图 38‑33、图 38‑34。

SPI033

图 38‑33 芯片数据手册中全擦除周期参数

SPI034

图 38‑34 仿真模型中定义的全擦除周期参数

顶层模块

模块框图

顶层模块的功能是实例化各子模块,对外引入工程输入信号、引出工程输出信号,对内将各子模块对应信号相互连接。顶层模块框图,具体见图 38‑35;顶层模块端口功能描述,具体见表格 38‑3。

SPI035

图 38‑35 顶层模块框图

表格 38‑3 顶层模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

pi_key

1Bit

Input

全擦除触发信号

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

代码编写

顶层模块作用为实例化各个子模块,将子模块各对应信号之间相互连接,无需绘制波形图,在此只列出顶层模块参考代码,代码比较简单,不再做相应讲解和功能仿真。顶层模块参考代码,具体见代码清单 38‑3。

代码清单 38‑3 顶层模块代码(spi_flash_be.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
module spi_flash_be
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire pi_key , //按键输入信号

output wire cs_n , //片选信号
output wire sck , //串行时钟
output wire mosi //主输出从输入数据
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter CNT_MAX = 20'd999_999; //计数器计数最大值

//wire define
wire po_key ;

////
//\* Instantiation \//
////
//------------- key_filter_inst -------------
key_filter
#(
.CNT_MAX (CNT_MAX ) //计数器计数最大值
)
key_filter_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key_in (pi_key ), //按键输入信号

.key_flag (po_key ) //消抖后信号
);

//------------- flash_be_ctrl_inst -------------
flash_be_ctrl flash_be_ctrl_inst
(

.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key (po_key ), //按键输入信号

.sck (sck ), //片选信号
.cs_n (cs_n ), //串行时钟
.mosi (mosi ) //主输出从输入数据
);

endmodule

RTL视图

至此全擦除工程基本完成,在Quartus中对代码进行编译,编译若有错误,请读者根据错误提示信息作出更改,直至编译通过,编译通过后查看RTL视图,与顶层模块框图对比,两者一致,各信号连接正确。RTL视图,具体见图 38‑36。

SPI036

图 38‑36 RTL视图

8.3.1.5. 上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 38‑4所示。

表格 38‑4 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

输入系统时钟

sys_rst_n

Input

M15

复位信号

pi_key

Input

M2

按键KEY1

sck

Output

H1

串行时钟

cs_n

Output

D2

Flash片选信号

mosi

Output

C1

主输出从输入数据

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 38‑37所示。

SPI037

图 38‑37 管脚分配

结果验证

如图 38‑28所示,开发板连接5V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。

SPI038

图 38‑38 程序下载连线图

如图 38‑39所示,使用“Programmer”为开发板下载程序,将流水灯的JIC文件固化到Flash;程序固化完成后,对板卡断电并重新上电,板卡自动执行流水灯程序。

SPI039

图 38‑39 程序下载窗口

如图 38‑40所示,使用“Programmer”将Flash全擦除程序下载到板卡。

SPI040

图 38‑40 程序下载窗口

程序下载完成后,流水灯程序不再执行;按下按键KEY1,对板卡重新上电,无程序执行。全擦除成功,上板验证通过。

8.3.2. SPI-Flash扇区擦除实验

在上文中我们提到,Flash芯片的擦除方式分为全擦除和扇区擦除两种,全擦除的实现方法文门已经通过实验讲解过了,下面我们同样通过实验为大家讲解SPI-Flash擦除中的另外一种方式:扇区擦除。

读者看到这里肯定心中存有疑问,扇区擦除和全擦除有什么区别?既然能够实现全擦除,为什么还要做扇区擦除?

这两种方式擦除方式均可实现Flash芯片的擦除,但两种擦除方式的作用范围不同。试想一下,当一块Flash芯片中的不同的扇区烧录了不同的程序,而我们只想擦除某个扇区的程序保留其他程序时,Flash的全擦除是不能满足要求的,这时候就需要扇区擦除来实现这一功能。扇区擦除可以对Flash芯片中的某一扇区进行 擦除而不改变其他扇区中的存储数据,要擦除扇区的选择通过扇区擦除地址来表示,具体实现方法,我们在下文会具体说明。

为了方便读者理解,我们举个例子来说明一下。假设一个Flash芯片有N个扇区,我们在扇区0-扇区n烧录了程序A,在扇区n+1-扇区m烧录了B,因为某些原因我们需要擦除程序A保留程序B,这时我们可以利用扇区擦除指令,擦除扇区0-扇区n中的所有扇区或其中任意扇区,程序A被擦除或不完整,程序均不能执行,Fl ash中只剩下可执行的程序B。

下面我们依然通过实验的方式,带领读者熟悉并掌握扇区擦除的实现方法。

8.3.2.1. 实验目标

编写扇区擦除工程,擦除事先烧录到Flash中的流水灯程序所占的某个扇区,使流水灯程序不能正常工作。在此次实验工程,我们选择擦除第0个扇区,擦除地址为24’h00_04_25。

8.3.2.2. 硬件资源

参照SPI-Flash全擦除实验中的硬件资源小节。

8.3.2.3. 操作时序

在本小节中,我们结合数据手册来详细说明一下SPI-Flash芯片扇区擦除操作的相关内容。

扇区擦除(Sector Erase)操作,简称SE,操作指令为8’b1101_0000(D8h),具体见图 38‑41。

SPI041

图 38‑41 扇区擦除操作指令

由数据手册中扇区擦除介绍部分可知,扇区擦除指令是将Flash芯片中的被选中扇区的所有存储单元设置为全1,在Flash芯片写入扇区擦出指令之前,需要先写入写使能(WREN)指令,将芯片设置为写使能锁存(WEL)状态;随后要拉低片选信号,写入扇区擦除指令、扇区地址、页地址和字节地址,在指令、地址写入过程 中,片选信号始终保持低电平,待指令、地址被芯片锁存后,将片选信号拉高;扇区擦除指令、地址被锁存并执行后,需要等待一个完整的扇区擦除周期(tSE),才能完成Flash芯片的扇区擦除操作。扇区擦除操作的详细介绍及时序图,具体见图 38‑42。

SPI042

图 38‑42 扇区擦除操作详细介绍及操作时序

上文全擦除操作中我们提到,扇区擦除(SE)指令写入前必须先对Flash芯片写入写使能(WREN)指令。对于写使能指令和串行输入时序的相关内容,在全擦除实验的操作时序小节已经做了详细介绍,在此不再赘述。

结合写使能指令、扇区擦除指令的相关内容和操作时序,绘制完整扇区擦除操作时序图如图 38‑43。

SPI043

图 38‑43 完整扇区擦除操作时序图

8.3.2.4. 程序设计

整体说明

与全擦除工程类似,整个扇区擦除工程也分为3个模块,按键消抖模块(key_filter)扇区擦除模块(flash_se_ctrl)和包含各模块实例化的顶层模块(spi_flash_se),模块框图,具体见图 38‑44;模块简介具体见表格 38‑5。

SPI044

图 38‑44 扇区擦除工程整体框图

表格 38‑5 扇区擦除工程模块简介

模块名称

功能描述

spi_flsah_se

扇区擦除工程顶层模块

key_filter

按键消抖模块

flash_se_ctrl

扇区擦除模块

在整个系统工程中,外部按键负责产生擦除信号,信号由外部进入FPGA,经顶层模块(spi_flash_se)进入按键消抖模块(key_filter),触发信号经消抖处理后输出进入工程核心模块扇区擦除模块(flash_se_ctrl),此信号作为触发条件触发Flash扇区擦除模块工作后, 扇区擦除模块输出串行时钟信号(sck)、片选信号(cs_n)和主输出从输入信号(mosi),3路信号通过顶层模块输入外部挂载的Flash芯片,实现扇区擦除功能。

系统整体包含3个模块,按键消抖的相关知识在之前的章节已经做过详细介绍,此处不再赘述;在下文中,我们对Flash扇区擦除模块(flash_se_ctrl)和顶层模块(spi_flash_se)进行一下说明,方便读者理解。

扇区擦除模块

模块框图

Flash扇区擦除模块是本实验工程的核心模块,其生成并输出串行时钟、片选信号和主输出从输入数据信号,控制Flash芯片实现扇区擦除,模块框图,具体见图 38‑45;模块端口描述,具体见表格 38‑6。

SPI045

图 38‑45 扇区擦除模块框图

表格 38‑6 模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

key

1Bit

Input

扇区擦除触发信号

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

波形图绘制

模块框图部分已经对模块功能和输入输出端口功能做了简要说明,在本小节我们开始绘制波形图。通过波形图的绘制,对各信号波形进行详细讲解,带领读者掌握模块功能的实现方法。

Flash扇区擦除模块波形图,具体见图 38‑46、图 38‑47。

SPI046

图 38‑46 Flash扇区擦除整体波形图

SPI047

图 38‑47 Flash扇区擦除局部波形图

对比扇区擦除波形图与全擦除波形图可以看出,两波形图中各信号波形变化类似,唯一区别就是,相对全擦除而言,扇区擦除在写入扇区擦除指令后还需要写入3字节的地址信息。对于模块波形图的设计与绘制,我们针对各信号进行分部分讲解。

第一部分:输入信号波形绘制

系统上电之后,扇区擦除模块一直处于初始状态,只有当输入的扇区擦除触发信号key有效时,模块才会开始执行扇区擦除的相关操作,触发信号是由外部物理按键生成,经由按键消抖模块做消抖处理后传入。除此之外,输入信号还包含时钟信号sys_clk(50MHz)、复位信号sys_rst_n(低电平有效),输入信号波 形图如下。

SPI023

图 38‑48 输入信号波形图

第二部分:状态机相关信号的波形设计与实现

由前文可知,一个完整的扇区擦除操作需要对Flash芯片执行两次指令的写入,分别为写使能指令和扇区擦除指令,扇区擦除指令写入后还需要写入要执行扇区擦除操作的扇区地址,而且在片选信号拉低后指令写入前、指令或地址写入完成后片选信号拉高前,以及两指令写入之间都需要做规定时间的等待。

对于这一流程操作,我们参照全擦除模块,使用状态机来实现。在模块内部声明状态机状态变量state,定义状态机各状态分别为:初始状态(IDLE)、写使能状态(WR_EN)、两指令间等待状态(DELAY)、扇区擦除状态(SE)。

状态机状态跳转流程如下:系统上电后,状态机状态变量state一直处于初始状态(IDLE);当传入的扇区擦除触发信号key有效时,表示实验工程开始执行对Flash芯片的扇区擦除操作,状态机跳转到写使能状态(WR_EN),同时片选信号拉低,选中要进行扇区擦除操作的Flash芯片;状态跳转到写使能状态且片 选信号拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入写使能指令,指令写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳转到两指令间等待状态(DELAY);在此状态等待时间tSHSL≥ 100ns后,状态机跳转到扇区擦除状态(SE),同时片选信号拉低,选中已写入写使能指令的Flash芯片;状态机跳转到扇区擦除状态且片选信号拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入扇区擦除指令和3字节的扇区擦除地址,地址写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳回初始状态(IDLE),一次完整的扇区擦除操作完成。

对于片选信号等待时间tSHSL、 tCHSH、tSHSL的参数确定,我们参照全擦除模块的方法,将片选信号的各等待时间的时间参数统一设置为640ns,即32个系统时钟周期。这样声明的计数器不仅可以用作片选信号等待时间计数,也可以用做指令 信号写入时间计数,可节省寄存器资源。声明计数器cnt_clk,初值为0,在0-31计数范围内循环计数,在状态机处于初始状态时,始终保持为0;在状态机处于初始状态之外的其他状态时,每个系统时钟周期自加1,计到最大值清0,重新计数。

对于状态机状态跳转约束条件的确定,我们同样参照全擦除模块的处理方法。声明计数器cnt_byte对计数器cnt_clk的计数周期进行计数。对cnt_byte赋初值为0,当状态机处于初始状态(IDLE)时,计数器cnt_byte始终保持初值0;当状态机处于除初始状态外的其他状态时,计数器cnt_byte 开始对计数器cnt_clk的计数周期进行计数,cnt_clk每完成一个完整的循环计数,即cnt_clk = 31时,计数器cnt_byte自加1,其他时刻保持当前值不变。

使用这两个计数器作为约束条件就可以实现状态机的状态跳转,当状态机跳转到写使能状态时,同时片选信号拉低,在cnt_byte = 0、计数器cnt_clk的第1个计数周期,是对片选信号等待时间tSLCH = 640ns的计数;在cnt_byte = 1、计数器cnt_clk的第2个计数周期,是对写使能指令写入时间进行计数;在cnt_byte = 2、计数器cnt_clk的第3个计数周期,是对片选信号等待时间tCHSH = 640ns的计数,第3个周期的计数完成后状态机跳转到两指令间等待状态(DELAY),同时片选信号拉高,计数器开始进行第4个计数周期的计数;此时cnt_byte = 3,这一计数周期是对片选信号两指令之间的等待时间tSHSL = 640ns的计数,计数完成后状态机跳转到全擦除状态(BE),片选信号再次拉低;在cnt_byte = 4、计数器cnt_clk的第5个计数周期,是对片选信号等待时间tSLCH = 640ns的计数;在cnt_byte = 5、计数器cnt_clk的第6个计数周期,是对扇区擦除指令写入时间进行计数;在cnt_byte = 6、7、8,计数器cnt_clk的第7、8、9个计数周期,是对扇区擦除地址写入时间进行计数;在cnt_byte = 9、计数器cnt_clk的第10个计数周期,是对片选信号等待时间tCHSH = 640ns的计数,第10个周期的计数完成后状态机跳回到初始状态(IDLE),Flash芯片的扇区擦除操作完成。

综上所述,绘制上述各信号波形图如下。

SPI048

图 38‑49 状态机相关信号波形图

第三部分:输出相关信号的波形设计与实现

本模块输出信号有3路,分别为片选信号cs_n、串行时钟信号sck和主输出从输入信号mosi。对于片选信号的波形设计与实现在第二部分已经做了详细说明,本部分重点讲解一下串行时钟信号sck、主输出从输入信号mosi以及与其相关信号的波形设计与实现。

模块输出的串行时钟为12.5MHz,为系统时钟50MHz通过四分频得到。所以在这里需要声明一个四分频计数器,对系统时钟进行四分频,产生串行时钟信号sck。

本实验使用的Flash芯片使用的是SPI通讯协议的模式0,即CPOL= 0,CPHA=0。空闲状态时SCK串行时钟为低电平;数据采样在SCK时钟的奇数边沿,本模式中,奇数边沿为上升沿;数据更新在SCK时钟的偶数边沿,本模式中,偶数边沿为下降沿,模式0时序图如下图所示。

SPI007

图 38‑50 SPI模式0通讯时序图

由于Flash芯片使用的为模式0的通讯模式,所以串行时钟信号sck在空闲状态保持低电平,在数据传输过程输出12.5MHz频率的时钟信号。在这里我们声明四分频计数器cnt_sck,赋初值为0,只有在cnt_byte计算值为1、5、6、7、8时,即输出写使能指令、扇区擦除指令以及扇区存储地址时,计数器c nt_sck在0-3范围内循环计数,计数周期为系统时钟周期,每个时钟周期自加1;使用四分频计数器cnt_sck作为约束条件,生成串行时钟sck,频率为12.5MHz。

四分频计数器cnt_sck、串行时钟信号sck波形图如下图所示。

SPI049

图 38‑51 cnt_sck、sck信号波形图(一)

SPI050

图 38‑52 cnt_sck、sck信号波形图(二)

串行时钟信号sck生成后,根据SPI模式0通讯时序图,本实验中Flash芯片在串行时钟sck的上升沿进行数据采样,我们需要在sck的下降沿进行传输数据的更新,在sck的下降沿对mosi信号写入写使能指令和全擦除指令。

有一点读者还需要注意,Flash芯片的指令或数据的写入要满足高位在前的要求,我们声明一个计数器cnt_bit,左右时实现指令或数据的高低位对调,计数器初值为0,在0-7范围内循环计数,计数时钟为串行时钟sck,每个时钟周期自加1,其他时刻恒为0。

绘制miso、cnt_sck信号波形图如下。

SPI051

图 38‑53 mosi、cnt_sck信号波形图(一)

SPI052

图 38‑54 mosi、cnt_sck信号波形图(二)

讲到这里,模块涉及到的所有信号都已将讲解完毕,将各信号波形进行整合后,就得到了本小节开头处的模块整体波形图。

代码编写

参照波形图我们可以快速写出本模块的参考代码,Flash扇区擦除参考代码,具体见代码清单 38‑4。

代码清单 38‑4 Flash扇区擦除参考代码(flash_se_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
module flash_se_ctrl
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire key , //按键输入信号

output reg cs_n , //片选信号
output reg sck , //串行时钟
output reg mosi //主输出从输入数据
);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter IDLE = 4'b0001 , //初始状态
WR_EN = 4'b0010 , //写状态
DELAY = 4'b0100 , //等待状态
SE = 4'b1000 ; //扇区擦除状态
parameter WR_EN_INST = 8'b0000_0110, //写使能指令
SE_INST = 8'b1101_1000; //扇区擦除指令
parameter SECTOR_ADDR = 8'b0000_0000, //扇区地址
PAGE_ADDR = 8'b0000_0100, //页地址
BYTE_ADDR = 8'b0010_0101; //字节地址

//reg define
reg [3:0] cnt_byte; //字节计数器
reg [3:0] state ; //状态机状态
reg [4:0] cnt_clk ; //系统时钟计数器
reg [1:0] cnt_sck ; //串行时钟计数器
reg [2:0] cnt_bit ; //比特计数器

////
//\* Main Code \//
////

//cnt_clk:系统时钟计数器,用以记录单个字节
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 5'd0;
else if(state != IDLE)
cnt_clk <= cnt_clk + 1'b1;

//cnt_byte:记录输出字节个数和等待时间
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_byte <= 4'd0;
else if((cnt_clk == 5'd31) && (cnt_byte == 4'd9))
cnt_byte <= 4'd0;
else if(cnt_clk == 31)
cnt_byte <= cnt_byte + 1'b1;

//cnt_sck:串行时钟计数器,用以生成串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sck <= 2'd0;
else if((state == WR_EN) && (cnt_byte == 1'b1))
cnt_sck <= cnt_sck + 1'b1;
else if((state == SE) && (cnt_byte >= 4'd5) && (cnt_byte <= 4'd8))
cnt_sck <= cnt_sck + 1'b1;

//cs_n:片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(key == 1'b1)
cs_n <= 1'b0;
else if((cnt_byte == 4'd2)&&(cnt_clk == 5'd31)&&(state == WR_EN))
cs_n <= 1'b1;
else if((cnt_byte == 4'd3)&&(cnt_clk == 5'd31)&&(state == DELAY))
cs_n <= 1'b0;
else if((cnt_byte == 4'd9)&&(cnt_clk == 5'd31)&&(state == SE))
cs_n <= 1'b1;

//sck:输出串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sck <= 1'b0;
else if(cnt_sck == 2'd0)
sck <= 1'b0;
else if(cnt_sck == 2'd2)
sck <= 1'b1;

//cnt_bit:高低位对调,控制mosi输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 3'd0;
else if(cnt_sck == 2'd2)
cnt_bit <= cnt_bit + 1'b1;

//state:两段式状态机第一段,状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE: if(key == 1'b1)
state <= WR_EN;

WR_EN: if((cnt_byte == 4'd2) && (cnt_clk == 5'd31))
state <= DELAY;
DELAY: if((cnt_byte == 4'd3) && (cnt_clk == 5'd31))
state <= SE;
SE: if((cnt_byte == 4'd9) && (cnt_clk == 5'd31))
state <= IDLE;
default: state <= IDLE;
endcase

//mosi:两段式状态机第二段,逻辑输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
mosi <= 1'b0;
else if((state == WR_EN)&& (cnt_byte == 4'd2))
mosi <= 1'b0;
else if((state == SE)&&(cnt_byte == 4'd9))
mosi <= 1'b0;
else if((state == WR_EN)&&(cnt_byte == 4'd1)&&(cnt_sck == 5'd0))
mosi <= WR_EN_INST[7 - cnt_bit]; //写使能指令
else if((state == SE) && (cnt_byte == 4'd5) && (cnt_sck == 5'd0))
mosi <= SE_INST[7 - cnt_bit]; //扇区擦除指令
else if((state == SE) && (cnt_byte == 4'd6) && (cnt_sck == 5'd0))
mosi <= SECTOR_ADDR[7 - cnt_bit]; //扇区地址
else if((state == SE) && (cnt_byte == 4'd7) && (cnt_sck == 5'd0))
mosi <= PAGE_ADDR[7 - cnt_bit]; //页地址
else if((state == SE) && (cnt_byte == 4'd8) && (cnt_sck == 5'd0))
mosi <= BYTE_ADDR[7 - cnt_bit]; //字节地址

endmodule

模块参考代码是参照绘制波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,本小节不再过多叙述。

仿真代码编写

参考代码编写完成后,为检验代码是否够实现预期功能,我们要对代码进行仿真,观察各信号波形是否按照预期规律变化。仿真参考代码,具体见代码清单 38‑5。

代码清单 38‑5 扇区擦除模块仿真参考代码(tb_flash_se_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
\`timescale 1ns/1ns
module tb_flash_se_ctrl();

//wire define
wire cs_n;
wire sck ;
wire mosi ;

//reg define
reg sys_clk ;
reg sys_rst_n ;
reg key ;

//时钟、复位信号、模拟按键信号
initial
begin
sys_clk = 0;
sys_rst_n <= 0;
key <= 0;
#100
sys_rst_n <= 1;
#1000
key <= 1;
#20
key <= 0;
end

always #10 sys_clk <= ~sys_clk;

//写入Flash仿真模型初始值(全F)
defparam memory.mem_access.initfile = "initmemory.txt";

//------------- flash_se_ctrl_inst -------------
flash_se_ctrl flash_se_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key (key ), //按键输入信号

.sck (sck ), //串行时钟
.cs_n (cs_n ), //片选信号
.mosi (mosi ) //主输出从输入数据
);

//------------- memory -------------
m25p16 memory
(
.c (sck ), //输入串行时钟,频率12.5Mhz,1bit
.data_in (mosi ), //输入串行指令或数据,1bit
.s (cs_n ), //输入片选信号,1bit
.w (1'b1 ), //输入写保护信号,低有效,1bit
.hold (1'b1 ), //输入hold信号,低有效,1bit

.data_out ( ) //输出串行数据
);

endmodule

仿真波形分析

仿真参考代码编写完毕后,我们进行仿真,Modelsim仿真过后查看仿真波形图,由图可知仿真波形图与绘制的波形图时序相同,具体见图 38‑55、图 38‑56、图 38‑57;仿真结果显示完成一个完整的扇区擦除循环,标志着Flash的扇区擦除成功,具体见图 38‑58。

SPI053

图 38‑55 扇区擦除整体仿真波形图

SPI054

图 38‑56 扇区擦除局部仿真波形图(一)

SPI055

图 38‑57 扇区擦除局部仿真波形图(二)

SPI056

图 38‑58 扇区擦除循环结束

注:读者在此要注意的是,芯片数据手册表明,Flash芯片完成一个完整的扇区擦除周期最大需要3秒,且仿真模型中定义的扇区擦除周期参数也为3秒,Modelsim若完成3秒仿真,耗费时间较长,为缩短仿真时间,可更改仿真文件夹(M25P16_VG_V12)中的参数定义文件(parameter.v)中的全擦除 周期参数(TSE),具体见图 38‑59、图 38‑60。

SPI057

图 38‑59 芯片数据手册中扇区擦除周期参数

SPI058

图 38‑60 仿真模型中定义的扇区擦除周期参数

顶层模块

模块框图

顶层模块的功能是实例化各子模块,对外引入工程输入信号、引出工程输出信号,对内将各子模块对应信号相互连接。顶层模块框图,具体见图 38‑61;顶层模块端口功能描述,具体见表格 38‑7。

SPI059

图 38‑61 顶层模块框图

表格 38‑7 顶层模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

pi_key

1Bit

Input

扇区擦除触发信号

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

代码编写

顶层模块作用为实例化各个子模块,将子模块各对应信号之间相互连接,无需绘制波形图,在此只列出顶层模块参考代码,代码比较简单,不再做相应讲解和功能仿真。顶层模块参考代码,具体见代码清单 38‑6。

代码清单 38‑6 顶层模块代码(spi_flash_se.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module spi_flash_se
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire pi_key , //按键输入信号

output wire cs_n , //片选信号
output wire sck , //串行时钟
output wire mosi //主输出从输入数据
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter CNT_MAX = 20'd999_999; //计数器计数最大值

//wire define
wire po_key ;

////
//\* Instantiation \//
////
//------------- key_filter_inst -------------
key_filter
#(
.CNT_MAX (CNT_MAX ) //计数器计数最大值
)
key_filter_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key_in (pi_key ), //按键输入信号

.key_flag (po_key ) //消抖后信号
);

//------------- flash_se_ctrl_inst -------------
flash_se_ctrl flash_se_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key (po_key ), //按键输入信号

.sck (sck ), //片选信号
.cs_n (cs_n ), //串行时钟
.mosi (mosi ) //主输出从输入数据
);

endmodule

RTL视图

至此扇区擦除工程基本完成,在Quartus中对代码进行编译,编译若有错误,请读者根据错误提示信息作出更改,直至编译通过,编译通过后查看RTL视图,与顶层模块框图对比,两者一致。RTL视图,具体见图 38‑62。

SPI060

图 38‑62 RTL视图

8.3.2.5. 上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 38‑8所示。

表格 38‑8 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

输入系统时钟

sys_rst_n

Input

M15

复位信号

pi_key

Input

M2

按键KEY1

sck

Output

H1

串行时钟

cs_n

Output

D2

Flash片选信号

mosi

Output

C1

主输出从输入数据

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 38‑63所示。

SPI037

图 38‑63 管脚分配

结果验证

如图 38‑64所示,开发板连接5V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。

SPI038

图 38‑64 程序下载连线图

如图 38‑65所示,使用“Programmer”为开发板下载程序,将流水灯的JIC文件固化到Flash;程序固化完成后,对板卡断电并重新上电,板卡自动执行流水灯程序。

SPI061

图 38‑65 程序下载窗口

如图 38‑66所示,使用“Programmer”将Flash扇区擦除程序下载到板卡。

SPI062

图 38‑66 程序下载窗口

程序下载完成后,流水灯程序不再执行;按下按键KEY1,对板卡重新上电,无程序执行。扇区擦除成功,上板验证通过。

8.3.3. SPI-Flash页写实验

上面小节中我们学习了SPI-Flash的两种擦除方式,接下来,我们依然通过实验的方式,为大家讲解一下SPI-Flash的数据页写入的实现。

8.3.3.1. 实验目标

使用页写指令,向Flash中写入N字节数据,N为整数,且大于0小于等于256。在本本实验中我们向Flash芯片中写入0-99,共100字节数据,数据初始地址为24’h00_04_25。

注意:在向Flash芯片写入数据之前,先要对芯片执行全擦除操作。

8.3.3.2. 硬件资源

参照SPI-Flash全擦除实验中的硬件资源小节。

8.3.3.3. 操作时序

在本小节中,我们结合数据手册来详细说明一下SPI-Flash芯片页写操作的相关内容。

页写(Page Program)操作,简称PP,操作指令为8’b0000_0010(02h),具体见图 38‑67。

SPI063

图 38‑67 页写操作指令

由数据手册中页写操作介绍部分可知,页写指令是根据写入数据将存储单元中的“1”置为“0”,实现数据的写入。在写入页写指令之前,需要先写入写使能(WREN)指令,将芯片设置为写使能锁存(WEL)状态;随后要拉低片选信号,写入页写指令、扇区地址、页地址、字节地址,紧跟地址写入要存储在Flash的字节数据, 在指令、地址以及数据写入过程中,片选信号始终保持低电平,待指令、地址、数据被芯片锁存后,将片选信号拉高;片选信号拉高后,等待一个完整的页写周期(tPP),才能完成Flash芯片的页写操作。

读者还要注意的是,Flash芯片中一页最多可以存储256字节数据,这也表示页写操作一次最多向Flash芯片写入256字节数据。页写指令写入后,随即写入3字节数据写入首地址,首地址为扇区地址、页地址、字节地址组成,扇区地址与页地址是确定数据写入Flash的特定扇区的特定页,字节地址位再该页数据写入的字 节首地址。

当数据写入的字节首地址为该页的首地址,及字节首地址为8’b0000_0000,数据写入个数为0-256字节,数据可以被正确写入Flash芯片;

当数据写入的字节首地址不是该页的首地址,及字节首地址不是8’b0000_0000,数据写入个数为0-256字节,若数据写入个数少于字节首地址地址到末地址之间的存储单元个数,数据可以被正确写入Flash芯片;若数据写入个数多于字节首地址地址到末地址之间的存储单元个数,等于字节首地址地址到末地址之间的存 储单元个数的数据可以被正确写入Flash芯片,超出的那部分数据,会以8’b0000_0000为字节首地址顺序写入本页,覆盖改地址之前存储的数据。

例如字节首地址为8’0000_1111,字节首地址地址到末地址之间的存储单元个数为241个,即本页最多可写入241字节数据,若写入数据为200个字节,数据可以被正确写入;若写入数据为256个字节,前241个字节的数据可以正确写入Flash芯片,而超出的15个字节就以本页的首地址8’b0000_000 0为数据写入首地址顺序写入,覆盖本页原有的前15个字节的数据。

当数据写入的字节首地址为该页的首地址,及字节首地址为8’b0000_0000,数据写入个数超出256个字节,前256个字节会按照时序顺序写入256个存储单元,超出部分以本页的首地址8’b0000_0000为数据写入首地址顺序写入,覆盖本页已写入的新数据。

例如写入字节首地址为8’b0000_0000,写入字节数为300个,前256个字节数据按照时序写入存储单元,超出的44个数据会覆盖刚刚写入的前44个数据。

页写操作的详细介绍及时序图,具体见图 38‑68。

SPI064

图 38‑68 页写操作详细介绍及操作时序

对于写使能指令和串行输入时序的相关内容,在全擦除实验的操作时序小节已经做了详细介绍,在此不再赘述。

结合写使能指令、页写指令的相关内容和操作时序,绘制完整页写操作时序图如图 38‑69。

SPI065

图 38‑69 完整页写操作时序图

8.3.3.4. 程序设计

整体说明

与全擦除工程类似,整个页写工程也分为3个模块,按键消抖模块(key_filter)、页写模块(flash_pp_ctrl)和包含各模块实例化的顶层模块(spi_flash_pp),模块框图,具体见图 38‑70;模块简介具体见表格 38‑9。

SPI066

图 38‑70 页写工程整体框图

表格 38‑9 页写工程模块简介

模块名称

功能描述

spi_flsah_pp

页写工程顶层模块

key_filter

按键消抖模块

flash_pp_ctrl

页写模块

在整个系统工程中,外部按键负责产生页写信号,信号由外部进入FPGA,经顶层模块(spi_flash_pp)进入按键消抖模块(key_unshake),触发信号经消抖处理后输出进入工程核心模块页写模块(flash_pp_ctrl),此信号作为触发条件触发Flash页写模块工作后, 页写模块输出串行时钟信号(sck)、片选信号(cs_n)和主输出从输入信号(mosi),3路信号通过顶层模块输入外部挂载的Flash芯片,将数据写入Flash芯片。

系统整体包含3个模块,按键消抖的相关知识在之前的章节已经做过详细介绍,此处不再赘述;在下文中,我们对Flash页写模块(flash_pp_ctrl)和顶层模块(spi_flash_pp)进行一下说明,方便读者理解。

页写模块

模块框图

当有效页写触发信号输入页写模块时,页写模块执行页写操作,输出串行时钟、片选信号和主输出从输入信号。主输出从输入信号输出的数据包括指令、数据地址和待写入数据。页写模块框图,具体见图 38‑71;页写模块输入输出端口简介,具体见表格 38‑10。

SPI067

图 38‑71 Flash页写模块框图

表格 38‑10 模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

key

1Bit

Input

页写触发信号

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

波形图绘制

模块框图部分已经对模块功能和输入输出端口功能做了简要说明,在本小节我们开始绘制波形图。通过波形图的绘制,对各信号波形进行详细讲解,带领读者掌握模块功能的实现方法。

Flash页写模块波形图,具体见图 38‑72、图 38‑73。

SPI068

图 38‑72 Flash页写模块整体波形图

SPI069

图 38‑73 Flash页写模块局部波形图

对比页写模块波形图与扇区擦除模块波形图可以看出,两波形图中各信号波形变化类似,存在区别就是,相对扇区擦除而言,页写操作在写入页写指令和数据写入首地址后还需要写入n(n为整数,0 < n ≤ 256)字节的待写入数据。对于模块波形图的设计与绘制,我们针对各信号进行分部分讲解。

第一部分:输入信号波形绘制

系统上电之后,页写模块一直处于初始状态,只有当输入的页写触发信号key有效时,模块才会开始执行页写相关操作,触发信号是由外部物理按键生成,经由按键消抖模块做消抖处理后传入。除此之外,输入信号还包含时钟信号sys_clk(50MHz)、复位信号sys_rst_n(低电平有效),输入信号波形图如下。

SPI023

图 38‑74 输入信号波形图

第二部分:状态机相关信号的波形设计与实现

由前文可知,一个完整的页写操作需要对Flash芯片执行两次指令的写入,分别为写使能指令和页写指令,扇区擦除指令写入后还需要写入要执行数据写入首地址地址以及待写入数据,而且在片选信号拉低后指令写入前、指令或数据写入完成后片选信号拉高前,以及两指令写入之间都需要做规定时间的等待。

对于这一流程操作,我们参照全擦除模块,使用状态机来实现。在模块内部声明状态机状态变量state,定义状态机各状态分别为:初始状态(IDLE)、写使能状态(WR_EN)、两指令间等待状态(DELAY)、页写状态(PP)。

状态机状态跳转流程如下:系统上电后,状态机状态变量state一直处于初始状态(IDLE);当传入的扇区擦除触发信号key有效时,表示实验工程开始执行对Flash芯片的扇区擦除操作,状态机跳转到写使能状态(WR_EN),同时片选信号拉低,选中要进行扇区擦除操作的Flash芯片;状态跳转到写使能状态且片 选信号拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入写使能指令,指令写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳转到两指令间等待状态(DELAY);在此状态等待时间tSHSL≥ 100ns后,状态机跳转到页写状态(PP),同时片选信号拉低,选中已写入写使能指令的Flash芯片;状态机跳转到扇区擦除状态且片选信号拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入页写指令、3字节的数据写入首地址地址和待写入数据,数据写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳回初始状态(IDLE),一次完整的页写操作完成。

对于片选信号等待时间tSHSL、 tCHSH、tSHSL的参数确定,我们参照全擦除模块的方法,将片选信号的各等待时间的时间参数统一设置为640ns,即32个系统时钟周期。这样声明的计数器不仅可以用作片选信号等待时间计数,也可以用做指令 信号写入时间计数,可节省寄存器资源。声明计数器cnt_clk,初值为0,在0-31计数范围内循环计数,在状态机处于初始状态时,始终保持为0;在状态机处于初始状态之外的其他状态时,每个系统时钟周期自加1,计到最大值清0,重新计数。

对于状态机状态跳转约束条件的确定,我们同样参照全擦除模块的处理方法。声明计数器cnt_byte对计数器cnt_clk的计数周期进行计数。对cnt_byte赋初值为0,当状态机处于初始状态(IDLE)时,计数器cnt_byte始终保持初值0;当状态机处于除初始状态外的其他状态时,计数器cnt_byte 开始对计数器cnt_clk的计数周期进行计数,cnt_clk每完成一个完整的循环计数,即cnt_clk = 31时,计数器cnt_byte自加1,其他时刻保持当前值不变。

使用这两个计数器作为约束条件就可以实现状态机的状态跳转,当状态机跳转到写使能状态时,同时片选信号拉低,在cnt_byte = 0、计数器cnt_clk的第1个计数周期,是对片选信号等待时间tSLCH = 640ns的计数;在cnt_byte = 1、计数器cnt_clk的第2个计数周期,是对写使能指令写入时间进行计数;在cnt_byte = 2、计数器cnt_clk的第3个计数周期,是对片选信号等待时间tCHSH = 640ns的计数,第3个周期的计数完成后状态机跳转到两指令间等待状态(DELAY),同时片选信号拉高,计数器开始进行第4个计数周期的计数;此时cnt_byte = 3,这一计数周期是对片选信号两指令之间的等待时间tSHSL = 640ns的计数,计数完成后状态机跳转到全擦除状态(BE),片选信号再次拉低;在cnt_byte = 4、计数器cnt_clk的第5个计数周期,是对片选信号等待时间tSLCH = 640ns的计数;在cnt_byte = 5、计数器cnt_clk的第6个计数周期,是对页写指令写入时间进行计数;在cnt_byte = 6、7、8,计数器cnt_clk的第7、8、9个计数周期,是对数据写入首地址写入时间进行计数;在cnt_byte = 9 – (n+9-1)、计数器cnt_clk的第10 – (n+10-1)个计数周期,是对nnijzie的写入数据的时间计数;在cnt_byte = (n+9)、计数器cnt_clk的第(n+10)个计数周期,是对片选信号等待时间tCHSH = 640ns的计数,第(n+10)个周期的计数完成后状态机跳回到初始状态(IDLE),Flash芯片的页写操作完成。

综上所述,绘制上述各信号波形图如下。

SPI070

图 38‑75 状态机相关信号波形图

第三部分:输出相关信号的波形设计与实现

本模块输出信号有3路,分别为片选信号cs_n、串行时钟信号sck和主输出从输入信号mosi。对于片选信号的波形设计与实现在第二部分已经做了详细说明,本部分重点讲解一下串行时钟信号sck、主输出从输入信号mosi以及与其相关信号的波形设计与实现。

模块输出的串行时钟为12.5MHz,为系统时钟50MHz通过四分频得到。所以在这里需要声明一个四分频计数器,对系统时钟进行四分频,产生串行时钟信号sck。

本实验使用的Flash芯片使用的是SPI通讯协议的模式0,即CPOL= 0,CPHA=0。空闲状态时SCK串行时钟为低电平;数据采样在SCK时钟的奇数边沿,本模式中,奇数边沿为上升沿;数据更新在SCK时钟的偶数边沿,本模式中,偶数边沿为下降沿,模式0时序图如下图所示。

SPI007

图 38‑76 SPI模式0通讯时序图

由于Flash芯片使用的为模式0的通讯模式,所以串行时钟信号sck在空闲状态保持低电平,在数据传输过程输出12.5MHz频率的时钟信号。在这里我们声明四分频计数器cnt_sck,赋初值为0,只有在cnt_byte计数值为1、5、6、7、8、9-(n+9-1)时,即输出写使能指令、页写指令、数据写入首 地址以及写入数据时,计数器cnt_sck在0-3范围内循环计数,计数周期为系统时钟周期,每个时钟周期自加1;使用四分频计数器cnt_sck作为约束条件,生成串行时钟sck,频率为12.5MHz。

四分频计数器cnt_sck、串行时钟信号sck波形图如下图所示。

SPI071

图 38‑77 cnt_sck、sck信号波形图(一)

SPI050

图 38‑78 cnt_sck、sck信号波形图(二)

串行时钟信号sck生成后,根据SPI模式0通讯时序图,本实验中Flash芯片在串行时钟sck的上升沿进行数据采样,我们需要在sck的下降沿进行传输数据的更新,在sck的下降沿对mosi信号写入写使能指令和全擦除指令。

有一点读者还需要注意,Flash芯片的指令或数据的写入要满足高位在前的要求,沃恩生意一个计数器cnt_bit,左右时实现指令或数据的高低位对调,计数器初值为0,在0-7范围内循环计数,计数时钟为串行时钟sck,每个时钟周期自加1,其他时刻恒为0。

绘制miso、cnt_sck信号波形图如下。

SPI051

图 38‑79 mosi、cnt_sck信号波形图(一)

SPI052

图 38‑80 mosi、cnt_sck信号波形图(二)

讲到这里,模块涉及到的所有信号都已将讲解完毕,将各信号波形进行整合后,就得到了本小节开头处的模块整体波形图。

页写模块波形图与扇区擦除波形图类似,对于各信号的波形设计与实现,读者可参阅全擦除实验的波形图绘制小节,在此不再对页写波形图各信号进行讲解。

代码编写

参照页写模块波形图编写模块参考代码,页写模块的参考代码,具体见代码清单 38‑7。

代码清单 38‑7 页写模块参考代码(flash_pp_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
module flash_pp_ctrl(

input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire key , //按键输入信号

output reg cs_n , //片选信号
output reg sck , //串行时钟
output reg mosi //主输出从输入数据

);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter IDLE = 4'b0001 , //初始状态
WR_EN = 4'b0010 , //写状态
DELAY = 4'b0100 , //等待状态
PP = 4'b1000 ; //页写状态
parameter WR_EN_INST = 8'b0000_0110, //写使能指令
PP_INST = 8'b0000_0010; //页写指令
parameter SECTOR_ADDR = 8'b0000_0000, //扇区地址
PAGE_ADDR = 8'b0000_0100, //页地址
BYTE_ADDR = 8'b0010_0101; //字节地址
parameter NUM_DATA = 8'd100 ; //页写数据个数(1-256)

//reg define
reg [7:0] cnt_byte ; //字节计数器
reg [3:0] state ; //状态机状态
reg [4:0] cnt_clk ; //系统时钟计数器
reg [1:0] cnt_sck ; //串行时钟计数器
reg [2:0] cnt_bit ; //比特计数器
reg [7:0] data ; //页写入数据

////
//\* Main Code \//
////

//cnt_clk:系统时钟计数器,用以记录单个字节
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 5'd0;
else if(state != IDLE)
cnt_clk <= cnt_clk + 1'b1;

//cnt_byte:记录输出字节个数和等待时间
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_byte <= 8'd0;
else if((cnt_clk == 5'd31) && (cnt_byte == NUM_DATA + 8'd9))
cnt_byte <= 8'd0;
else if(cnt_clk == 5'd31)
cnt_byte <= cnt_byte + 1'b1;

//cnt_sck:串行时钟计数器,用以生成串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sck <= 2'd0;
else if((state == WR_EN) && (cnt_byte == 8'd1))
cnt_sck <= cnt_sck + 1'b1;
else if((state == PP) && (cnt_byte >= 8'd5)
&& (cnt_byte <= NUM_DATA + 8'd9 - 1'b1))
cnt_sck <= cnt_sck + 1'b1;

//cs_n:片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(key == 1'b1)
cs_n <= 1'b0;
else if((cnt_byte == 8'd2)&&(cnt_clk == 5'd31)&&(state == WR_EN))
cs_n <= 1'b1;
else if((cnt_byte == 8'd3)&&(cnt_clk == 5'd31)&&(state == DELAY))
cs_n <= 1'b0;
else if((cnt_byte == NUM_DATA + 8'd9)&&(cnt_clk == 5'd31)&&(state == PP))
cs_n <= 1'b1;

//sck:输出串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sck <= 1'b0;
else if(cnt_sck == 2'd0)
sck <= 1'b0;
else if(cnt_sck == 2'd2)
sck <= 1'b1;

//cnt_bit:高低位对调,控制mosi输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 3'd0;
else if(cnt_sck == 2'd2)
cnt_bit <= cnt_bit + 1'b1;

//data:页写入数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data <= 8'd0;

else if((cnt_clk == 5'd31) && ((cnt_byte >= 8'd9)
&& (cnt_byte < NUM_DATA + 8'd9 - 1'b1)))
data <= data + 1'b1;

//state:两段式状态机第一段,状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE: if(key == 1'b1)
state <= WR_EN;
WR_EN: if((cnt_byte == 8'd2) && (cnt_clk == 5'd31))
state <= DELAY;
DELAY: if((cnt_byte == 8'd3) && (cnt_clk == 5'd31))
state <= PP;
PP: if((cnt_byte == NUM_DATA + 8'd9) && (cnt_clk == 5'd31))
state <= IDLE;
default: state <= IDLE;
endcase

//mosi:两段式状态机第二段,逻辑输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
mosi <= 1'b0;
else if((state == WR_EN) && (cnt_byte== 8'd2))
mosi <= 1'b0;
else if((state == PP) && (cnt_byte == NUM_DATA + 8'd9))
mosi <= 1'b0;
else if((state == WR_EN)&&(cnt_byte == 8'd1)&&(cnt_sck == 5'd0))
mosi <= WR_EN_INST[7 - cnt_bit]; //写使能指令
else if((state == PP) && (cnt_byte == 8'd5) && (cnt_sck == 5'd0))
mosi <= PP_INST[7 - cnt_bit]; //页写指令
else if((state == PP) && (cnt_byte == 8'd6) && (cnt_sck == 5'd0))
mosi <= SECTOR_ADDR[7 - cnt_bit]; //扇区地址
else if((state == PP) && (cnt_byte == 8'd7) && (cnt_sck == 5'd0))
mosi <= PAGE_ADDR[7 - cnt_bit]; //页地址
else if((state == PP) && (cnt_byte == 8'd8) && (cnt_sck == 5'd0))
mosi <= BYTE_ADDR[7 - cnt_bit]; //字节地址
else if((state == PP) && ((cnt_byte >= 8'd9)
&&(cnt_byte <= NUM_DATA + 8'd9 - 1'b1))&&(cnt_sck == 5'd0))
mosi <= data[7 - cnt_bit]; //页写入数据

endmodule

仿真代码编写

参考代码编写完成后,为检验代码是否够实现预期功能,我们要对代码进行仿真,观察各信号波形是否按照预期规律变化。仿真参考代码,具体见代码清单 38‑8。

代码清单 38‑8 页写模块仿真参考代码(tb_flash_pp_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
\`timescale 1ns/1ns
module tb_flash_pp_ctrl();

//wire define
wire cs_n;
wire sck ;
wire mosi;

//reg define
reg sys_clk ;
reg sys_rst_n ;
reg key ;

//时钟、复位信号、模拟按键信号
initial
begin
sys_clk = 0;
sys_rst_n <= 0;
key <= 0;
#100
sys_rst_n <= 1;
#1000
key <= 1;
#20
key <= 0;
end

always #10 sys_clk <= ~sys_clk;

//写入Flash仿真模型初始值(全F)
defparam memory.mem_access.initfile = "initmemory.txt";

//------------- flash_pp_ctrl_inst -------------
flash_pp_ctrl flash_pp_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key (key ), //按键输入信号

.sck (sck ), //串行时钟
.cs_n (cs_n ), //片选信号
.mosi (mosi ) //主输出从输入数据
);

//------------- memory -------------
m25p16 memory
(
.c (sck ), //输入串行时钟,频率12.5Mhz,1bit
.data_in (mosi ), //输入串行指令或数据,1bit
.s (cs_n ), //输入片选信号,1bit
.w (1'b1 ), //输入写保护信号,低有效,1bit
.hold (1'b1 ), //输入hold信号,低有效,1bit

.data_out ( ) //输出串行数据
);

endmodule

仿真波形分析

仿真代码编写完成,使用Quartus软件和Modelsim可联合仿真,自有效页写触发信号传入,页写模块开始进入页写操作;到数据写入完成,片选信号拉高,页写操作完成,仿真波形与绘制波形图,各信号变化波形一致。仿真波形图具体见图 38‑81至图 38‑86。

SPI072

图 38‑81 页写模块整体仿真波形图

SPI073

图 38‑82 页写模块局部仿真波形图(一)

SPI074

图 38‑83 页写模块局部仿真波形图(二)

SPI075

图 38‑84 页写模块局部仿真波形图(三)

SPI076

图 38‑85 页写模块局部仿真波形图(四)

SPI077

图 38‑86 页写模块局部仿真波形图(五)

在前文中,我们提到,在片选信号二次拉高后,Flash芯片会进入页写循环周期,循环周期完毕后,数据才算真正写入Flash芯片,此时页写才算真正完成。页写模块的仿真文件中假如了仿真模型,我们可以通过仿真模型的波形和输出信息查看Flash芯片的页写循环周期,由图可知,片选信号二次拉高后,完成一个页写循环周 期,周期为5ms,具体见图 38‑87、图 38‑88;数据手册中,标注页写循环周期最大参数为5ms,仿真模型中,声明的页写循环周期为最大参数5ms,与仿真波形一致,具体见图 38‑89、图 38‑90。

SPI078

图 38‑87 Flash芯片的页写循环仿真波形

SPI079

图 38‑88 Flash芯片的页写循环

SPI080

图 38‑89 数据手册标注页写循环周期参数

SPI081

图 38‑90 页写循环周期参数

顶层模块

代码编写

扇区擦除工程与全擦除工程只在擦除功能模块存在差异,其他部分相同,顶层模块的整体框架也相同,在此我们不再对扇区擦除工程的顶层模块做讲解,只列出参考代码,读者可参阅参阅全擦除工程顶层模块小节的介绍。顶层模块参考代码,具体见代码清单 38‑9。

代码清单 38‑9 顶层模块参考代码(spi_falsh_pp.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module spi_flash_pp
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire pi_key , //按键输入信号

output wire cs_n , //片选信号
output wire sck , //串行时钟
output wire mosi //主输出从输入数据
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter CNT_MAX = 20'd999_999; //计数器计数最大值

//wire define
wire po_key ;

////
//\* Instantiation \//
////
//------------- key_filter_inst -------------
key_filter
#(
.CNT_MAX (CNT_MAX ) //计数器计数最大值
)
key_filter_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key_in (pi_key ), //按键输入信号

.key_flag (po_key ) //消抖后信号
);

//------------- flash_pp_ctrl_inst -------------
flash_pp_ctrl flash_pp_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key (po_key ), //按键输入信号

.sck (sck ), //片选信号
.cs_n (cs_n ), //串行时钟
.mosi (mosi ) //主输出从输入数据
);

endmodule

RTL视图

顶层代码编写完成。工程进行到这里已基本完成,在Quartus软件中进行工程编译,编译通过后,查看RTL视图,与顶层模块一致,各信号连接正确。RTL视图,具体见图 38‑91。

SPI082

图 38‑91 RTL视图

8.3.3.5. 上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 38‑11所示。

表格 38‑11 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

输入系统时钟

sys_rst_n

Input

M15

复位信号

pi_key

Input

M2

按键KEY1

sck

Output

H1

串行时钟

cs_n

Output

D2

Flash片选信号

mosi

Output

C1

主输出从输入数据

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 38‑92所示。

SPI037

图 38‑92 管脚分配

结果验证

待SPI-Flash读操作讲解完毕后,联合上板验证。

8.3.4. SPI_Flash连续写实验

Flash芯片在使用的时候,常常要连续写入不定量的数据,这时使用页写并不是特别方便, 但我们可以在页写指令的基础上,实现连续写入数据的操作。

通过页写指令实现数据的连续写操作,方法有二。其一,每次页写入只写入单字节数据,若想连续写入N字节数据,需要连续执行N次页写操作,;其二,先通过页写指令一次性写满第一页数据,计算剩余数据所占整数页,通过页写指令写满整数页,每次页写指令一次写满一页,剩余不足一页的数据,再通过一次页写指令一次性写入。

第一种方法实现起来较为简单,但数据写入所需时间较长;第二种方法与第一种相比,数据写入时间大大缩短,但实现起来比第一种困难。

在本小节中,我们将通过实验的方式,带领读者学习一下SPI-Flash连续数据写入的第一种实现方法。

8.3.4.1. 实验目标

使用页写指令,将串口发送过来的连续不定量数据写入Flash。本实验中,我们发送数据为100字节,串口波特率位9600。

注意:在向Flash芯片写入数据之前,先要对芯片执行全擦除操作。

8.3.4.2. 硬件资源

参照SPI-Flash全擦除实验中的硬件资源小节。

8.3.4.3. 操作时序

本实验实现的连续写操作是采用刚刚提到的方式一来实现,每次使用页写操作只向Flash芯片写入单字节数据,连续执行多次单字节写入来实现连续写操作。其操作时序与页写时序相同,读者可回顾参阅,此处不再赘述。

8.3.4.4. 程序设计

整体说明

与页写工程有所区别,Flash芯片的连续写操作不再使用按键控制,我们使用串口RS232向其写入数据。连续写工程包括4个模块,串口接收模块(uart_rx)、连续写模块(flash_seq_wr_ctrl)、串口发送模块(uart_tx)以及顶层模块(spi_flash_seq_wr),工程整体框图具 体见图 38‑93;模块简介具体见表格 38‑12。

SPI083

图 38‑93 连续写工程整体框图

表格 38‑12 连续写工程模块简介

模块名称

功能描述

spi_flsah_seq_wr

连续写工程顶层模块

uart_rx

串口接收模块

uart_tx

串口发送模块

flash_seq_wr_ctrl

连续写模块

在PC端使用串口助手向FPGA连续发送100字节数据,串行数据输入FPGA后,由串口接收模块(uart_rx)将串行数据拼接成单字节数据传入连续写模块(flash_seq_wr_ctrl),连续写模块(flash_seq_wr_ctrl)按时序输出串行时钟信号(sck)、片选信号(cs_n)和主输出 从输入信号(mosi),3路信号通过顶层模块输入外部挂载的Flash芯片,将数据写入Flash芯片,传入连续写模块的字节数据同时传入串口发送模块(uart_tx),该模块将拼接后的字节数据传回PC端,并通过串口助手打印出来。

系统整体包含4个模块,串口收发模块的相关知识在之前的章节已经做过详细介绍,此处不再赘述;在下文中,我们对Flash连续写模块(flash_seq_wr_ctrl)和顶层模块(spi_flash_seq_wr)进行一下说明,方便读者理解。

连续写模块

模块框图

串口接收模块将数据完成拼接后,将拼接过后的是单字节数据输入连续写模块,与数据同步传入的还有数据标志信号;连续写模块接收到有效的数据标志信号后,使用页写指令完成一次单字节数据的写操作,输出串行时钟、片选信号和主输出从输入信号。主输出从输入信号输出的数据包括指令、数据地址和与数据标志信号同步窜入的单字节 数据。连续写模块框图,具体见图 38‑94;连续写模块输入输出端口简介,具体见表格 38‑13。

SPI084

图 38‑94 Flash连续写模块框图

表格 38‑13 模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

pi_flag

1Bit

Input

数据标志信号

pi_data

8Bit

Input

待写入字节数据

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

波形图绘制

模块框图部分已经对模块功能和输入输出端口功能做了简要说明,在本小节我们开始绘制波形图。

Flash连续写模块波形图,具体见图 38‑95。

SPI085

图 38‑95 Flash连续写模块整体波形图

对比连续写模块波形图与页写模块波形图可以看出,两波形图中各信号波形变化类似,存在区别就是,相对页写模块而言,连续写操作将数据标志信号作为数据写操作触发信号使用页写将传入的字节数据写入Flash芯片,每次只写入单字节数据,且每次数据写入都需要更新存储地址。对于模块波形图的设计与绘制,我们针对各信号进行 分部分讲解。

第一部分:输入信号波形绘制

系统上电之后,连续写模块一直处于初始状态,只有当输入的数据标志信号有效时,模块才会开始执行页写相关操作,将与标准信号同时传入的单字节数据写入Flash。除此之外,输入信号还包含时钟信号sys_clk(50MHz)、复位信号sys_rst_n(低电平有效),输入信号波形图如下。

SPI086

图 38‑96 输入信号波形图

第二部分:存储地址addr的波形设计与实现

连续写操作是通过连续执行多次页写操作实现,每次只写入单字节数据,所以每次单字节数据写入完成都需要更新数据写入地址。

声明两信号变量实现存储地址更新,地址寄存器addr_reg和存储地址addr,先将舍弟的初始地址赋值给地址寄存器addr_reg,每次接收到有效数据标志信号pi_flag,addr_reg自加1,若直接使用addr_reg作出存储地址,会使首地址无数据写入,真正的存储首地址变为首地址的下一个存储地址 ,为了解决这一问题,声明了存储地址addr,初值为0,pi_data有效时将addr_reg赋值给addr。两地址信号波形如下。

SPI087

图 38‑97 地址信号波形图

其他信号的波形设计与页写模块的相关信号相同,这里不再赘述,读者可回顾查阅页写模块相关内容。

代码编写

参照连续写模块波形图编写模块参考代码,连续写模块的参考代码,具体见代码清单 38‑10。

代码清单 38‑10 连续写模块参考代码(flash_pp_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
module flash_seq_wr_ctrl(

input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire pi_flag , //数据标志信号
input wire [7:0] pi_data , //写入数据

output reg cs_n , //片选信号
output reg sck , //串行时钟
output reg mosi //主输出从输入数据

);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter IDLE = 4'b0001 , //初始状态
WR_EN = 4'b0010 , //写状态
DELAY = 4'b0100 , //等待状态
PP = 4'b1000 ; //扇区擦除状态
parameter WR_EN_INST = 8'b0000_0110, //写使能指令
PP_INST = 8'b0000_0010; //扇区擦除指令
parameter ADDR = 24'h00_04_25; //数据写入地址

//reg define
reg [3:0] cnt_byte; //字节计数器
reg [3:0] state ; //状态机状态
reg [4:0] cnt_clk ; //系统时钟计数器
reg [1:0] cnt_sck ; //串行时钟计数器
reg [2:0] cnt_bit ; //比特计数器
reg [23:0] addr_reg; //数据写入地址寄存器
reg [23:0] addr ; //数据写入地址
reg [15:0] data_num; //记录写入数据个数

////
//\* Main Code \//
////

//data_num:记录写入数据个数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_num <= 16'd0;
else if(pi_flag == 1'b1)
data_num <= data_num + 1'b1;
else
data_num <= data_num;

//cnt_clk:系统时钟计数器,用以记录单个字节
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 5'd0;
else if(state != IDLE)
cnt_clk <= cnt_clk + 1'b1;

//cnt_byte:记录输出字节个数和等待时间
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_byte <= 4'd0;
else if((cnt_clk == 5'd31) && (cnt_byte == 4'd10))
cnt_byte <= 4'd0;
else if(cnt_clk == 31)
cnt_byte <= cnt_byte + 1'b1;

//cnt_sck:串行时钟计数器,用以生成串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sck <= 2'd0;
else if((state == WR_EN) && (cnt_byte == 1'b1))
cnt_sck <= cnt_sck + 1'b1;
else if((state == PP) && (cnt_byte >= 4'd5) && (cnt_byte <= 4'd9))
cnt_sck <= cnt_sck + 1'b1;

//addr_reg:数据写入地址寄存器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
addr_reg <= ADDR;
else if(pi_flag == 1'b1)
addr_reg <= addr_reg + 1'b1 ;

//addr:数据写入地址
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
addr <= 24'd0;
else if(pi_flag == 1'b1)
addr <= addr_reg;

//ss_n:片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(pi_flag == 1'b1)
cs_n <= 1'b0;
else if((cnt_byte == 4'd2)&&(cnt_clk == 5'd31)&&(state == WR_EN))
cs_n <= 1'b1;
else if((cnt_byte == 4'd3)&&(cnt_clk == 5'd31)&&(state == DELAY))
cs_n <= 1'b0;
else if((cnt_byte == 4'd10)&&(cnt_clk == 5'd31)&&(state == PP))

cs_n <= 1’b1;

//sck:输出串行时钟 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1’b0) sck <= 1’b0; else if(cnt_sck == 2’d0) sck <= 1’b0; else if(cnt_sck == 2’d2) sck <= 1’b1;

//cnt_bit:高低位对调,控制mosi输出 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1’b0) cnt_bit <= 3’d0; else if(cnt_sck == 2’d2) cnt_bit <= cnt_bit + 1’b1;

//state:两段式状态机第一段,状态跳转 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1’b0) state <= IDLE; else case(state) IDLE: if(pi_flag == 1’b1) state <= WR_EN; WR_EN: if((cnt_byte == 4’d2) && (cnt_clk == 5’d31)) state <= DELAY; DELAY: if((cnt_byte == 4’d3) && (cnt_clk == 5’d31)) state <= PP; PP: if((cnt_byte == 4’d10) && (cnt_clk == 5’d31)) state <= IDLE; default: state <= IDLE; endcase

//mosi:两段式状态机第二段,逻辑输出 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1’b0) mosi <= 1’b0; else if((state == WR_EN) && (cnt_byte == 4’d2)) mosi <= 1’b0; else if((state == PP) && (cnt_byte == 4’d10)) mosi <= 1’b0; else if((state == WR_EN)&&(cnt_byte == 4’d1) && (cnt_sck == 5’d0)) mosi <= WR_EN_INST[7 - cnt_bit]; //写使能指令 else if((state == PP) && (cnt_byte == 4’d5) && (cnt_sck == 5’d0)) mosi <= PP_INST[7 - cnt_bit]; //扇区擦除指令 else if((state == PP) && (cnt_byte == 4’d6) && (cnt_sck == 5’d0)) mosi <= addr[23 - cnt_bit]; //扇区地址 else if((state == PP) && (cnt_byte == 4’d7) && (cnt_sck == 5’d0)) mosi <= addr[15 - cnt_bit]; //页地址 else if((state == PP) && (cnt_byte == 4’d8) && (cnt_sck == 5’d0)) mosi <= addr[7 - cnt_bit]; //字节地址 else if((state == PP) && (cnt_byte == 4’d9) && (cnt_sck == 5’d0)) mosi <= pi_data[7 - cnt_bit]; //写入数据

endmodule

对于本模块的仿真验证步骤单独进行,待顶层模块说明完毕,对整个工程进行整体仿真,在对本模块仿真波形做查看。

顶层模块

模块设计

顶层模块框图如图所示,输入时钟复位信号,以及通过端口rx输入待写入串行数据,输出tx信号将数据回传给PC端,输出sck、cs_n、mosi将数据写入Flash芯片。内部实例化各子功能模块,连接各自对应信号。

SPI088

图 38‑98 顶层模块框图

代码编写

顶层模块较为简单,无需绘制波形图,直接编写参考代码。顶层模块参考代码,具体见代码清单 38‑11。

代码清单 38‑11 顶层模块参考代码(spi_falsh_ seq_wr.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
module spi_flash_seq_wr(

input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire rx , //串口接收数据

output wire cs_n , //片选信号
output wire sck , //串行时钟
output wire mosi , //主输出从输入数据
output wire tx //串口发送数据

);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter UART_BPS = 14'd9600 , //比特率
CLK_FREQ = 26'd50_000_000 ; //时钟频率

//wire define
wire po_flag ;
wire [7:0] po_data ;

////
//\* Instantiation \//
////

//-------------uart_rx_inst-------------
uart_rx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_rx_inst(
.sys_clk (sys_clk ), //系统时钟50Mhz
.sys_rst_n (sys_rst_n), //全局复位
.rx (rx ), //串口接收数据

.po_data (po_data ), //串转并后的数据
.po_flag (po_flag ) //串转并后的数据有效标志信号
);

//-------------flash_seq_wr_ctrl_inst-------------
flash_seq_wr_ctrl flash_seq_wr_ctrl_inst(

.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.pi_flag (po_flag ), //数据标志信号
.pi_data (po_data ), //写入数据

.sck (sck ), //片选信号
.cs_n (cs_n ), //串行时钟
.mosi (mosi ) //主输出从输入数据

);

//-------------uart_tx_inst-------------
uart_tx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_tx_inst
(
.sys_clk (sys_clk ), //系统时钟50Mhz
.sys_rst_n (sys_rst_n), //全局复位
.pi_data (po_data ), //并行数据
.pi_flag (po_flag ), //并行数据有效标志信号

.tx (tx ) //串口发送数据
);

endmodule

RTL视图

顶层代码编写完成。工程进行到这里已基本完成,在Quartus软件中进行工程编译,编译通过后,查看RTL视图,具体见图 38‑99。

SPI089

图 38‑99 RTL视图

仿真验证

仿真代码编写

对顶层代码进行仿真,实现工程整体仿真。仿真参考代码,具体见代码清单 38‑12。

代码清单 38‑12 顶层模块仿真参考代码(tb_ spi_flash_seq_wr.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
\`timescale 1ns/1ns
module tb_spi_flash_seq_wr();

//wire define
wire tx ;
wire cs_n;
wire sck ;
wire mosi;
wire miso;

//reg define
reg clk ;
reg rst_n ;
reg rx ;
reg [7:0] data_mem [299:0] ; //data_mem是一个存储器,相当于一个ram

//读取sim文件夹下面的data.txt文件,并把读出的数据定义为data_mem
initial
$readmemh("E:/GitLib/Altera/EP4CE10/base_code/10_spi_flash/spi_flash_write/spi_flash_seq_wr/sim/spi_flash.txt",data_mem);


//时钟、复位信号
initial
begin
clk = 1'b1 ;
rst_n <= 1'b0 ;
#200
rst_n <= 1'b1 ;
end

always #10 clk = ~clk;


initial
begin
rx <= 1'b1;
#200
rx_byte();
end

task rx_byte();
integer j;
for(j=0;j<300;j=j+1)
rx_bit(data_mem[j]);
endtask

task rx_bit(input[7:0] data); //data是data_mem[j]的值。
integer i;
for(i=0;i<10;i=i+1)
begin
case(i)
0: rx <= 1'b0 ; //起始位
1: rx <= data[0];
2: rx <= data[1];
3: rx <= data[2];
4: rx <= data[3];
5: rx <= data[4];
6: rx <= data[5];
7: rx <= data[6];
8: rx <= data[7]; //上面8个发送的是数据位
9: rx <= 1'b1 ; //停止位
endcase
#1040; //一个波特时间=sclk周期*波特计数器
end
endtask

//重定义defparam,用于修改参数,缩短仿真时间
defparam spi_flash_seq_wr_inst.uart_rx_inst.BAUD_CNT_END = 52;
defparam spi_flash_seq_wr_inst.uart_rx_inst.BAUD_CNT_END_HALF = 26;
defparam spi_flash_seq_wr_inst.uart_tx_inst.BAUD_CNT_END = 52;
defparam memory.mem_access.initfile = "initmemory.txt";

//-------------spi_flash_seq_wr_inst-------------
spi_flash_seq_wr spi_flash_seq_wr_inst(
.sys_clk (clk ), //input sys_clk
.sys_rst_n (rst_n ), //input sys_rst_n
.rx (rx ), //input rx

.cs_n (cs_n ), //output cs_n
.sck (sck ), //output sck
.mosi (mosi ), //output mosi
.tx (tx ) //output tx

);

m25p16 memory (
.c (sck ),
.data_in (mosi ),
.s (cs_n ),
.w (1'b1 ),
.hold (1'b1 ),
.data_out (miso )
);

endmodule

仿真波形分析

仿真代码编写完成,使用Quartus软件和Modelsim可联合仿真,查看连续写模块仿真波形。连续写模块仿真波形图具体见图 38‑100、图 38‑101、图 38‑102。

SPI090

图 38‑100 连续写模块整体仿真波形图

SPI091

图 38‑101 连续写模块局部仿真波形图(一)

SPI092

图 38‑102 连续写模块局部仿真波形图(二)

8.3.4.5. 上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 38‑14所示。

表格 38‑14 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

输入系统时钟

sys_rst_n

Input

M15

复位信号

pi_key

Input

M2

按键KEY1

rx

Input

N6

串口接收数据

tx

Output

N5

串口发送数据

sck

Output

H1

串行时钟

cs_n

Output

D2

Flash片选信号

mosi

Output

C1

主输出从输入数据

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 38‑103 管脚分配所示。

SPI093

图 38‑103 管脚分配

结果验证

待SPI-Flash读操作讲解完毕后,联合上板验证。

8.3.5. SPI_Flash读数据实验

在前面两个实验中,我们分别使用页写和连续写的方式向Flash芯片写入数据,在本小节的实验中我们进行Flash芯片的数据读取操作,将之前使用页写或连续写操作写入Flash的数据读出来。

8.3.5.1. 实验目标

使用页写或连续写操作向Flash芯片写入数据,再使用数据读操作读取之前写入数据,将读取的数据使用串口传回PC机,使用串口助手传回数据并与之前写入数据比较,判断正误。

注意:在向Flash芯片写入数据之前,先要对芯片执行全擦除操作。

8.3.5.2. 硬件资源

参照SPI-Flash全擦除实验中的硬件资源小节。

8.3.5.3. 操作时序

在本小节中,我们结合数据手册来详细说明一下SPI-Flash芯片数据读操作的相关内容。数据读操作,操作指令为8’b0000_0011(03h),具体见图 38‑104。

SPI094

图 38‑104 数据读操作指令

要执行数据读指令,首先拉低片选信号选中Flash芯片,随后写入数据读(READ)指令,紧跟指令写入3字节的数据读取首地址,指令和地址会在串行时钟上升沿被芯片锁存。随后存储地址对应存储单元中的数据在串行时钟下降沿通过串行数据总线输出。

数据读取首地址可以为芯片中的任何一个有效地址,使用数据读(READ)指令可以对芯片内数据连续读取,当首地址数据读取完成,会自动对首地址的下一个地址进行数据读取。若最高位地址内数据读取完成,会自动跳转到芯片首地址继续进行数据读取,只有再次拉高片选信号,才能停止数据读操作,否者会对芯片执行无线循环读操作 。

数据读操作的详细介绍及时序图,具体见图 38‑105。

SPI095

图 38‑105 数据读操作详细介绍及操作时序

数据读操作指令写入之前无需先写入写使能指令,且执行数据读操作过程中,片选信号拉低后和拉高前无需做规定时间等待,上图中的时序图就是完整的数据读操作时序。

8.3.5.4. 程序设计

整体说明

在整个数据读操作实验工程中共包括4个模块,

与全擦除工程类似,整个页写工程也分为3个模块,按键消抖模块(key_filter)、数据读模块(flash_read_ctrl)、串口数据发送模块(uart_tx)和包含各模块实例化的顶层模块(spi_flash_read),模块框图,具体见图 38‑106;模块简介具体见表格 38‑15。

SPI096

图 38‑106 数据读工程整体框图

表格 38‑15 数据读工程模块简介

模块名称

功能描述

spi_flsah_read

数据读工程顶层模块

key_filter

按键消抖模块

flash_read _ctrl

数据读模块

uart_tx

串口数据发送模块

在整个系统工程中,外部按键负责产生数据读触发信号,信号由外部进入FPGA,经顶层模块(spi_flash_read)进入按键消抖模块(key_filter),触发信号经消抖处理后输出进入工程核心模块数据读模块(flash_read_ctrl),此信号作为触发条件触发数据读模块工作后,模块输出串行时钟 信号(sck)、片选信号(cs_n)和主输出从输入信号(mosi),3路信号通过顶层模块输入外部挂载的Flash芯片,写入数据读指令和读数据首地址;Flash芯片通过主输入从输出信号(mosi)传回读取数据,数据暂存在数据读模块内部内部实例化的FIFO中,待数据读取完成后,通过串口数据发送模块将数据 回传给PC端。

系统整体包含4个模块,按键消抖和串口发送模块的相关知识在之前的章节已经做过详细介绍,此处不再赘述;在下文中,我们对数据读模块(flash_read_ctrl)和顶层模块(spi_flash_read)进行一下说明,方便读者理解。

数据读模块

模块框图

当有效读触发信号输入数据读模块时,数据读模块执行数据读操作,输出串行时钟、片选信号和主输出从输入信号,向Flash芯片写入数据读指令和数据读首地址,通过主输入从输出信号接收读取数据,读取数据暂存到内部实例化FIFO中,待数据读取完毕,将FIFO中数据传出至串口数据发送模块。数据读模块框图,具体见图 38‑107;数据读模块输入输出端口简介,具体见表格 38‑16。

SPI097

图 38‑107 数据读模块框图

表格 38‑16 模块输入输出端口功能描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

key

1Bit

Input

页写触发信号

miso

1Bit

Input

Flash主输入从输出信号

sck

1Bit

Output

Flash串行时钟

cs_n

1Bit

Output

Flash片选信号

mosi

1Bit

Output

Flash主输出从输入信号

tx_flag

1Bit

Output

读出数据标志信号

tx_data

8Bit

Output

读出Flash的字节数据

波形图绘制

模块框图部分已经对模块功能和输入输出端口功能做了简要说明,在本小节我们开始绘制波形图。通过波形图的绘制,对各信号波形进行详细讲解,带领读者掌握模块功能的实现方法。

数据读模块波形图,具体见图 38‑108至图 38‑112。

SPI098

图 38‑108 数据读模块整体波形图(一)

SPI099

图 38‑109 数据读模块整体波形图(二)

SPI100

图 38‑110 数据读模块整体波形图(三)

SPI101

图 38‑111 数据读模块整体波形图(四)

SPI102

图 38‑112 数据读模块整体波形图(五)

由于篇幅原因,我们将模块整体波形图分为了5个部分,图 38‑108、图 38‑109绘制的是数据读指令和数据读取首地址写入过程各信号波形图;图 38‑110、图 38‑111绘制的是100字节数据读出过程各信号波形图;图 38‑112则是绘制的数据读取完成后字节数据发送至串口数据收发模块工程的各信号波形变化。对于模块波形图的设计与绘制,我们针对各信号进行详细讲解。

第一部分:输入信号波形绘制

模块的输入信号共有4路,除去必不可少的时钟和复位信号,还有经消抖处理后的数据读触发信号key和Flash存储芯片传入的主输入从输出信号miso。

系统上电之后,数据读模块一直处于初始状态,只有当输入的数据读触发信号key有效时,模块才会开始执行数据读相关操作,输出有效串行时钟和片选信号,通过mosi写入数据读指令和数据读取首地址到Flash芯片,这一过程miso始终保持高阻态,待指令和地址写入完成,Flash芯片通过miso回传地址对应存储单 元数据,待数据读取完成,miso信号继续保持高阻态。相关信号波形如下。

SPI103

图 38‑113 指令、地址写入过程各输入信号波形图

SPI104

图 38‑114 数据读取工程各输入信号波形图(部分)

SPI105

图 38‑115 读出数据发送过程各输入信号波形图

第二部分:状态机相关信号的波形设计与实现

沿用之前几个实验工程的设计方法,数据读模块也使用状态机的设计思路,不过本模块状态机只有三个状态:初始状态(IDLE)、数据读状态(READ)和数据发送状态(SEND)。

系统上电后,状态机一直处于初始状态(IDLE),当传入的数据读触发信号有效时,表示实验工程开始对Flash芯片执行数据读取操作,状态机跳转到数据读状态(READ),同时片选信号拉低,选中要进行扇区擦除操作的Flash芯片;在数据读状态,模块向Flash芯片写入数据读指令和3字节数据读取首地址,随后接 收Flash芯片传回的读出数据,将串行数据拼接为字节数据后,暂存到FIFO中;待数据读取完成后,拉高片选信号结束数据读操作,状态机跳转到数据发送状态(SEND),将FIOF数据读出并发送到串行数据发送模块;读出数据均发送完成后,状态机跳回初始状态,等待下一次有效读触发信号。

对于状态机状态跳转约束条件的确定,我们同样参照全擦除模块的处理方法。声明计数器cnt_byte对计数器cnt_clk的计数周期进行计数。对cnt_byte赋初值为0,当状态机处于初始状态(IDLE)时,计数器cnt_byte始终保持初值0;当状态机处于数据读状态时,计数器cnt_byte开始对计数器 cnt_clk的计数周期进行计数,cnt_clk每完成一个完整的循环计数,即cnt_clk = 31时,计数器cnt_byte自加1,其他时刻保持当前值不变。

使用这两个计数器作为约束条件就可以实现状态机的状态跳转,当状态机跳转到写使能状态时,同时片选信号拉低,在cnt_byte = 0、计数器cnt_clk的第1个计数周期,是对数据读指令写入时间进行计数;在cnt_byte = 1、2、3,计数器cnt_clk的第2、3、4个计数周期,是对数据读取首地址写入时间进行计数;在cnt_byte = 4 –103、计数器cnt_clk的第5 – 104个计数周期,是对100字节数据的读取时间计数;将读取的100字节数据暂存到FIFO中,100字节数据读取完成后,即cnt_byte = 103、计数器cnt_clk = 31,状态机跳转到数据发送状态(SEND),拉高片选信号。

对于状态机由数据发送状态如何跳转回到初始状态,我们需要声明信号变量作为约束条件。

在数据发送状态,我们需要将FIFO中暂存的100字节数据发送串口发送模块,由其将数据传回PC机。串口发送红模块需要将接收到的字节数据转换为单比特数据进行数据回传,转换过程需要时间,所以我们向其发送字节数据要考虑到数据转换时间要求,传入串口发送模块的字节数据之间要有一定的时间间隔。由串口RS232章节 可知,在50MHz时钟下、串口波特率位9600时,串口接收或发送单比特数据需要5208个时钟周期,8位数据加起始位和结束位,共计需要52080个时钟周期。我们声明计数器cnt_wait对数据读模块发送阶段数据发送时间间隔进行循环计数,计数范围为0-59999,计数周期为50MHz系统时钟。计数器cn t_wait每计数到最大值,FIFO读使能信号fifo_read_en拉高一个时钟周期,读取并输出读出的字节数据。

因为要实现100字节的数据读取,需要声明一个读出数据计数器read_data_num对读出FIFO的数据个数进行计数,初值为0,FIFO读使能信号每拉高一个时钟周期,计数器read_data_num自加1。

同时,还需要一个读有效信号fifo_read_valid,约束上述3个信号的有效范围,信号赋初值为低电平,当FIFO内写入数据为100 字节时,拉高读有效信号,只有信号为高电平时,计数器cnt_wait、读出字节计数器read_data_num进行计数、读使能信号fifo_read_en有效,待100 字节数据读取完成,即cnt_wait计数到最大值、read_data_num计数到最大值,读有效信号拉低,同样以此信号为条件,实现状态机由数据发送状态(SEND)向初始状态(IDLE)跳转。

综上所述,绘制上述各信号波形图如下。

SPI106

图 38‑116 状态机相关信号波形图(指令、地址写入部分)

SPI107

图 38‑117 状态机相关信号波形图(数据读出部分)

SPI108

图 38‑118 状态机相关信号波形图(数据发送部分)

第三部分:Flash芯片相关信号的波形设计与实现

要想将数据读指令和读数据首地址写入Flash芯片,需要输出3路信号到Flash芯片,分别为片选信号cs_n、串行时钟信号sck和主输出从输入信号mosi。对于片选信号的波形设计与实现在第二部分已经做了详细说明,串行时钟信号sck、主输出从输入信号mosi以及与其相关信号的波形设计与实现与页写模块相似 。

Flash芯片使用的是SPI通讯协议的模式0,即CPOL= 0,CPHA=0。声明四分频计数器cnt_sck,赋初值为0,只有在cnt_byte计数值为0-103时,即输出数据读指令、数据读取首地址和输入读取数据时,计数器cnt_sck在0-3范围内循环计数,计数周期为系统时钟周期,每个时钟周期自加 1;使用四分频计数器cnt_sck作为约束条件,生成串行时钟sck,频率为12.5MHz。

四分频计数器cnt_sck、串行时钟信号sck波形图如下图所示。

SPI109

图 38‑119 cnt_sck、sck信号波形图

串行时钟信号sck生成后,根据SPI模式0通讯时序图,本实验中Flash芯片在串行时钟sck的上升沿进行数据采样,我们需要在sck的下降沿进行传输数据的更新,当cnt_byte计数范围为0-3时,在sck的下降沿对mosi信号写入数据读指令和数据读取首地址。

Flash芯片的指令或数据的写入要满足高位在前的要求,声明一个计数器cnt_bit,实现指令或数据的高低位对调,计数器初值为0,在0-7范围内循环计数,计数时钟为串行时钟sck,每个时钟周期自加1,其他时刻恒为0。

绘制miso、cnt_sck信号波形图如下。

SPI110

图 38‑120 mosi、cnt_sck信号波形图

数据读指令和数据读取首地址均写入Flash芯片后,Flash芯片通过miso信号发送地址对应数据给FPGA,FPGA在时钟scl的上升沿采集miso信号线上的数据,由于miso为串行数据线,每次只能传输单比特数据,而我们需要将单比特数据拼接为字节数据存储到FIFO中。

声明标志信号miso_flag和数据变量data,使用之前声明的计数器cnt_byte和cnt_sck作为约束条件控制miso_flag信号电平变化。当cnt_byte计数在4-103范围内、cnt_sck为1是,赋值miso_flag信号为有效的高电平,miso_flag有效时,读取miso中数据 信号赋值给数据变量data的最低位;当计数器cnt_bit计数到7且miso_flag信号有效时,完成一个字节数据的拼接。

声明信号po_flag_reg和数据变量po_data,在计数器cnt_bit计数到7且miso_flag信号有效时,信号po_flag_reg赋值为有效高电平;po_flag_reg信号有效时,将data赋值给po_data。此时数据变量数据变量po_data为一个完整的单字节数据。

为了将字节数据po_data写入FIFO,需要写使能信号,声明标志信号po_flag,信号滞后po_flag_reg信号一个时钟周期,信号po_flag与字节数据po_data同步,将po_data写入FIFO。上述各信号波形图如下。

SPI111

图 38‑121 信号miso及相关信号波形图(一)

SPI112

图 38‑122 信号miso及相关信号波形图(二)

第四部分:发送数据tx_data、发送数据标志信号tx_flag信号波形设计与实现

将100字节数据均写入FIFO后,开始向串口发送模块发送字节数据,使用fifo_read_en信号作FIFO读使能,将读出的字节数据赋值给tx_data;将fifo_read_en信号打一拍生成与tx_data同步的数据标志信号tx_flag,两信号输出至串口发送模块,波形图如下。

SPI113

图 38‑123 tx_data、tx_flag信号波形图

代码编写

参照数据读模块波形图编写模块参考代码,数据读模块参考代码,具体见代码清单 38‑13。

代码清单 38‑13 数据读模块参考代码(flash_read_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
module flash_read_ctrl(

input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire key , //按键输入信号
input wire miso , //读出flash数据

output reg sck , //串行时钟
output reg cs_n , //片选信号
output reg mosi , //主输出从输入数据
output reg tx_flag , //输出数据标志信号
output wire [7:0] tx_data //输出数据

);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter IDLE = 3'b001 , //初始状态
READ = 3'b010 , //数据读状态
SEND = 3'b100 ; //数据发送状态

parameter READ_INST = 8'b0000_0011; //读指令
parameter NUM_DATA = 16'd100 ; //读出数据个数
parameter SECTOR_ADDR = 8'b0000_0000, //扇区地址
PAGE_ADDR = 8'b0000_0100, //页地址
BYTE_ADDR = 8'b0010_0101; //字节地址
parameter CNT_WAIT_MAX= 16'd6_00_00 ;

//wire define
wire [7:0] fifo_data_num ; //fifo内数据个数
//reg define
reg [4:0] cnt_clk ; //系统时钟计数器
reg [2:0] state ; //状态机状态
reg [15:0] cnt_byte ; //字节计数器
reg [1:0] cnt_sck ; //串行时钟计数器
reg [2:0] cnt_bit ; //比特计数器
reg miso_flag ; //miso提取标志信号
reg [7:0] data ; //拼接数据
reg po_flag_reg ; //输出数据标志信号
reg po_flag ; //输出数据
reg [7:0] po_data ; //输出数据
reg fifo_read_valid ; //fifo读有效信号
reg [15:0] cnt_wait ; //等待计数器
reg fifo_read_en ; //fifo读使能
reg [7:0] read_data_num ; //读出fifo数据个数

////
//\* Main Code \//
////
//cnt_clk:系统时钟计数器,用以记录单个字节
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 5'd0;
else if(state == READ)
cnt_clk <= cnt_clk + 1'b1;

//cnt_byte:记录输出字节个数和等待时间
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_byte <= 16'd0;
else if((cnt_clk == 5'd31) && (cnt_byte == NUM_DATA + 16'd3))
cnt_byte <= 16'd0;
else if(cnt_clk == 5'd31)
cnt_byte <= cnt_byte + 1'b1;

//cnt_sck:串行时钟计数器,用以生成串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sck <= 2'd0;
else if(state == READ)
cnt_sck <= cnt_sck + 1'b1;

//cs_n:片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(key == 1'b1)
cs_n <= 1'b0;
else if((cnt_byte == NUM_DATA + 16'd3) && (cnt_clk == 5'd31) && (state == READ))
cs_n <= 1'b1;

//sck:输出串行时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sck <= 1'b0;
else if(cnt_sck == 2'd0)
sck <= 1'b0;
else if(cnt_sck == 2'd2)
sck <= 1'b1;

//cnt_bit:高低位对调,控制mosi输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 3'd0;
else if(cnt_sck == 2'd2)
cnt_bit <= cnt_bit + 1'b1;


//state:两段式状态机第一段,状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE: if(key == 1'b1)
state <= READ;
READ: if((cnt_byte == NUM_DATA + 16'd3) && (cnt_clk == 5'd31))
state <= SEND;
SEND: if((read_data_num == NUM_DATA)
&& ((cnt_wait == (CNT_WAIT_MAX - 1'b1))))
state <= IDLE;
default: state <= IDLE;
endcase

//mosi:两段式状态机第二段,逻辑输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
mosi <= 1'b0;
else if((state == READ)&&(cnt_byte>= 16'd4))
mosi <= 1'b0;
else if((state == READ)&&(cnt_byte == 16'd0)&&(cnt_sck == 2'd0))
mosi <= READ_INST[7 - cnt_bit]; //读指令
else if((state == READ)&&(cnt_byte == 16'd1)&&(cnt_sck == 2'd0))
mosi <= SECTOR_ADDR[7 - cnt_bit]; //扇区地址
else if((state == READ)&&(cnt_byte == 16'd2)&&(cnt_sck == 2'd0))
mosi <= PAGE_ADDR[7 - cnt_bit]; //页地址
else if((state == READ)&&(cnt_byte == 16'd3)&&(cnt_sck == 2'd0))
mosi <= BYTE_ADDR[7 - cnt_bit]; //字节地址

//miso_flag:miso提取标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
miso_flag <= 1'b0;
else if((cnt_byte >= 16'd4) && (cnt_sck == 2'd1))
miso_flag <= 1'b1;
else
miso_flag <= 1'b0;

//data:拼接数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data <= 8'd0;
else if(miso_flag == 1'b1)
data <= {data[6:0],miso};

//po_flag_reg:输出数据标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_flag_reg <= 1'b0;
else if((cnt_bit == 3'd7) && (miso_flag == 1'b1))
po_flag_reg <= 1'b1;
else
po_flag_reg <= 1'b0;

//po_flag:输出数据标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_flag <= 1'b0;
else
po_flag <= po_flag_reg;

//po_data:输出数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_data <= 8'd0;
else if(po_flag_reg == 1'b1)
po_data <= data;
else
po_data <= po_data;

//fifo_read_valid:fifo读有效信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
fifo_read_valid <= 1'b0;
else if((read_data_num == NUM_DATA)
&& ((cnt_wait == (CNT_WAIT_MAX - 1'b1))))
fifo_read_valid <= 1'b0;
else if(fifo_data_num == NUM_DATA)
fifo_read_valid <= 1'b1;

//cnt_wait:两数据读取时间间隔
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_wait <= 16'd0;
else if(fifo_read_valid == 1'b0)
cnt_wait <= 16'd0;
else if(cnt_wait == (CNT_WAIT_MAX - 1'b1))
cnt_wait <= 16'd0;
else if(fifo_read_valid == 1'b1)
cnt_wait <= cnt_wait + 1'b1;

//fifo_read_en:fifo读使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
fifo_read_en <= 1'b0;
else if((cnt_wait == (CNT_WAIT_MAX - 1'b1))
&& (read_data_num < NUM_DATA))
fifo_read_en <= 1'b1;
else
fifo_read_en <= 1'b0;

//read_data_num:自fifo中读出数据个数计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
read_data_num <= 8'd0;
else if(fifo_read_valid == 1'b0)
read_data_num <= 8'd0;
else if(fifo_read_en == 1'b1)
read_data_num <= read_data_num + 1'b1;

//tx_flag
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx_flag <= 1'b0;
else
tx_flag <= fifo_read_en;

////
//\* Instantiation \//
////
//-------------fifo_data_inst--------------
fifo_data fifo_data_inst(
.clock (sys_clk ), //时钟信号
.data (po_data ), //写数据,8bit
.wrreq (po_flag ), //写请求
.rdreq (fifo_read_en ), //读请求

.q (tx_data ), //数据读出,8bit
.usedw (fifo_data_num) //fifo内数据个数
);

endmodule

代码中的各信号在波形图绘制小节已经做了详细讲解,此处不再赘述。对于参考代码的仿真验证不再单独进行,待顶层模块介绍完毕,对工程进行整体仿真,再查看本模块仿真波形。

顶层模块

模块设计

顶层模块框图如图所示,输入时钟复位信号,通过端口miso输入自Flash读取的串行数据,接收数据读操作触发信号pi_key;输出sck、cs_n、mosi将数据写入Flash芯片,通过串口tx将读出Flash数据发送到PC机。内部实例化各子功能模块,连接各自对应信号。

SPI114

图 38‑124 顶层模块框图

代码编写

顶层模块参考代码,具体见代码清单 38‑14。

代码清单 38‑14 顶层模块参考代码(spi_flash_read.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
module spi_flash_read(

input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire pi_key , //按键输入信号
input wire miso , //读出flash数据

output wire cs_n , //片选信号
output wire sck , //串行时钟
output wire mosi , //主输出从输入数据
output wire tx

);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter CNT_MAX = 20'd999_999 ; //计数器计数最大值
parameter UART_BPS = 14'd9600 , //比特率
CLK_FREQ = 26'd50_000_000 ; //时钟频率


//wire define
wire po_key ; //消抖处理后的按键信号
wire tx_flag ; //输入串口发送模块数据标志信号
wire [7:0] tx_data ; //输入串口发送模块数据

////
//\* Instantiation \//
////
//------------- key_filter_inst -------------
key_filter
#(
.CNT_MAX (CNT_MAX ) //计数器计数最大值
)
key_filter_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key_in (pi_key ), //按键输入信号

.key_flag (po_key ) //消抖后信号
);

//-------------flash_read_ctrl_inst-------------
flash_read_ctrl flash_read_ctrl_inst(

.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.key (po_key ), //按键输入信号
.miso (miso ), //读出flash数据

.sck (sck ), //片选信号
.cs_n (cs_n ), //串行时钟
.mosi (mosi ), //主输出从输入数据
.tx_flag (tx_flag ), //输出数据标志信号
.tx_data (tx_data ) //输出数据

);

//-------------uart_tx_inst-------------
uart_tx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_tx_inst(
.sys_clk (sys_clk ), //系统时钟50Mhz
.sys_rst_n (sys_rst_n), //全局复位
.pi_data (tx_data ), //并行数据
.pi_flag (tx_flag ), //并行数据有效标志信号

.tx (tx ) //串口发送数据
);

endmodule

RTL视图

顶层代码编写完成。工程进行到这里已基本完成,在Quartus软件中进行工程编译,编译通过后,查看RTL视图,各信号连接正确。RTL视图,具体见图 38‑125。

SPI115

图 38‑125 RTL视图

仿真代码编写

仿真顶层模块,实现对实验工程的整体仿真。顶层仿真参考代码,具体见代码清单 38‑15。

代码清单 38‑15 数据读实验顶层模块仿真参考代码(tb_flash_read_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
\`timescale 1ns/1ns
module tb_spi_flash_read();

//wire define
wire cs_n;
wire sck ;
wire mosi;
wire miso;
wire tx ;

//reg define
reg clk ;
reg rst_n ;
reg key ;

//时钟、复位信号、模拟按键信号
initial
begin
clk = 0;
rst_n <= 0;
key <= 0;
#100
rst_n <= 1;
#1000
key <= 1;
#20
key <= 0;
end

always #10 clk <= ~clk;

defparam memory.mem_access.initfile = "initM25P16_test.txt";
defparam spi_flash_read_inst.flash_read_ctrl_inst.CNT_WAIT_MAX = 1000;
defparam spi_flash_read_inst.uart_tx_inst.BAUD_CNT_END = 10;
//------------- spi_flash_read -------------
spi_flash_read spi_flash_read_inst(
.sys_clk (clk ), //input sys_clk
.sys_rst_n (rst_n ), //input sys_rst
.pi_key (key ), //input key
.miso (miso ),

.sck (sck ), //output sck
.cs_n (cs_n ), //output cs_n
.mosi (mosi ), //output mosi
.tx (tx )

);

//------------- memory -------------
m25p16 memory (
.c (sck ),
.data_in (mosi ),
.s (cs_n ),
.w (1'b1 ),
.hold (1'b1 ),
.data_out (miso )
);

endmodule

仿真波形分析

仿真代码编写完成,使用Quartus软件和Modelsim实现联合仿真,实现数据读模块仿真波形图具体见图 38‑126至图 38‑131。

SPI116

图 38‑126 指令、地址写入过程仿真波形图(一)

SPI117

图 38‑127 指令、地址写入过程仿真波形图(二)

SPI118

图 38‑128 数据读取过程仿真波形图(一)

SPI119

图 38‑129 数据读取过程仿真波形图(二)

SPI120

图 38‑130 数据发送过程仿真波形图(一)

SPI121

图 38‑131 数据发送过程仿真波形图(二)

8.3.5.5. 上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 38‑17所示。

表格 38‑17 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

输入系统时钟

sys_rst_n

Input

M15

复位信号

pi_key

Input

M2

按键KEY1

tx

Output

N5

串口发送数据

sck

Output

H1

串行时钟

cs_n

Output

D2

Flash片选信号

mosi

Output

C1

主输出从输入数据

miso

Input

H2

主输入从输出数据

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 38‑132所示。

SPI122

图 38‑132 管脚分配

结果验证

按照如图 38‑133所示连接5V电源、下载器、USB数据线以及短路帽,连接完成后,板卡上电。

SPI123

图 38‑133 程序下载连接线

Flash页写验证

使用“Programmer”为开发板下载Flash全擦除程序,按下按键KEY1对Flash芯片进行全擦除;随后向开发板下载Flash读数据程序,打开串口助手,按下按键KEY1,串口助手会连续接收并打印自Flash读取的100字节数据,如图 38‑134所示。由于对Flash芯片进行过全擦除,读取的100字节数据均为FF。

SPI124

图 38‑134 页写验证(一)

接着向开发板下载Flash页写程序,按下按键KEY1对Flash芯片写入0-99共100字节数据;再次向开发板下载Flash读数据程序,按下按键KEY1,读取页写程序写入Flash的100字节数据,并通过串口助手打印出来,如图 38‑135所示。写入数据与读取数据相同,页写程序与读程序验证通过。

SPI125

图 38‑135 页写验证(二)

Flash连续写验证

使用“Programmer”为开发板下载Flash全擦除程序,按下按键KEY1对Flash芯片进行全擦除;随后向开发板下载Flash读数据程序,打开串口助手,按下按键KEY1,串口助手会连续接收并打印自Flash读取的100字节数据,如图 38‑136所示。由于对Flash芯片进行过全擦除,读取的100字节数据均为FF。

SPI124

图 38‑136 连续写验证(一)

接着向开发板下载Flash连续写程序,通过串口助手向Flash写入100字节数据,如图 38‑137所示。

SPI126

图 38‑137 连续写验证(二)

再次向开发板下载Flash读数据程序,按下按键KEY1,读取连续写程序写入Flash的100字节数据,并通过串口助手打印出来,如图 38‑138所示。写入数据与读取数据相同,连续写程序验证通过。

SPI127

图 38‑138 连续写验证(三)

8.4. 章末总结

到这里,本章节的内容讲解完毕,对于SPI通讯协议和SPI-Flash芯片的擦除和数据读写读者务必理解掌握。

8.5. 拓展训练

更改代码,将Flash芯片的擦除、读、写工程合为一个工程,实现Flash芯片的擦除、数据读/写。