基于SPI协议的flash驱动控制

发布时间 2023-08-27 17:58:19作者: jasonlee~

第46章、基于SPI协议的flash驱动控制

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

【理论】

【SPI协议】

【1 概念】

SPI(Serial Peripheral Interface,串行外围设备接口)通讯协议,是Motorola公司提出的一种 同步串行 接口技术,是一种高速、全双工同步通信总线,在芯片中只占用 四根管脚 用来 控制 及 数据传输;

【2 用途】

广泛用于 EEPROM、Flash、RTC(实时时钟)、ADC(数模转换器)、DSP(数字信号处理器)以及 数字信号解码器上;

【3 特点】

优点:支持全双工通信,通讯方式简单,且相对数据传输速率较快;

缺点:没有指定的 流控制,没有 应答机制 确认数据是否接收,与 IIC总线通讯协议 相比,在数据 可靠性 上有一定缺陷;

 

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

【SPI 物理层】

连接方式和设备引脚的功能描述:

SPI通讯设备的通讯模式是 主从通讯模式,通讯双方有主从之分,根据从机设备的个数,SPI通讯设备之间的连接方式可分为 一主一从一主多从

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

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

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

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

(4) CS' (Chip Select):片选信号线,也称为CS_N,低电平有效。当有多个 SPI从设备 与 SPI主机 相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的 SPI总线 上,即无论有多少个从设备,都共同使用这3条总线;而每个从设备都有独立的这一条CS_N信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。

【注】

  1. I2C协议中通过 设备地址 寻址、选中总线上的某个设备并与其进行通讯;
  2. SPI协议中没有设备地址,它使用 片选CS_N信号线 来寻址,当主机要选择从设备时,把该从设备的 CS_N 信号线设置为 低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯 以CS_N线置低电平 为开始信号,以 CS_N线被拉高 作为结束信号。

【SPI 协议层】

【CPOL/CPHA 及 通讯模式】

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

SPI通讯协议的4种模式如下:

模式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时钟的奇数边沿,本模式中,偶数边沿为下降沿。

CPOL(时钟极性) CPHA(时钟相位) 时钟电平 数据采样边沿 数据更新

0 0 低 奇(上 ) 偶

0 1 低 偶 奇(上)

1 0 高 奇(下) 偶

1 1 高 偶 奇(下)

CPOL(时钟极性):取值 0,数据采样边沿在 上升沿;取值 1,数据采样边沿在 下降沿; (0上,1下)

CPHA(时钟相位):取值 0,数据采样边沿在 奇数边沿;取值1,数据采样边沿在 偶数边沿;(0奇,1偶)

  1. CPOL 表示设备未被选中的空闲状态时,串行时钟SCK的电平状态:CPOL = 0,空闲状态时SCK为低电平,CPOL = 1,空闲状态时SCK为高电平;
  2. CPHA的不同参数则规定了 数据采样 是在SCK时钟的 奇数边沿 还是 偶数边沿:CPHA = 0,数据采样是在SCK时钟的 奇数边沿,CPHA = 1,数据采样是在SCK时钟的 偶数边沿。

这里不使用上升沿或下降沿表示,是因为不同模式下,奇数边沿或偶数边沿与上升沿或下降沿的对应不是固定的,

根据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的 偶数边沿被采样。

【SPI 基本通讯过程】

SPI通讯协议的4中通讯模式,模式0(00)和模式3(11)比较常用,下面以模式0为例,讲解SPI基本的通讯过程:

下图表示的是 主机视角 的通讯时序。SCK、MOSI、CS_N信号均由主机控制产生:

SCK :时钟信号,用以同步数据;

MOSI:主机输出从机输入信号,主机通过此信号线传输数据给从机;

MISO:由从机产生,主机通过该信号线读取从机的数据。

CS_N:片选信号,用以选定从机设备,低电平有效;

MOSI 与 MISO 的信号只在 CS_N 为 低电平 的时候才有效,在SCK的每个时钟周期 MOSI 和 MISO 传输一位数据。

  1. 标号①处:CS_N 拉低,是SPI通讯的 起始信号。CS_N是每个从机 各自独占 的信号线,当从机在自己的CS_N线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。

标号⑥处:CS_N 拉高,是SPI通讯的 停止信号,表示本次通讯结束,从机的选中状态被取消。

  1. SPI使用 MOSI及MISO 信号线来传输数据,使用 SCK信号线进行数据同步。MOSI及MISO数据线在SCK的 每个时钟周期传输 一位数据,且数据输入输出是 同时进行的。数据传输时,MSB先行 或 LSB先行 并没有作硬性规定,但要保证两个SPI通讯设备之间使用同样的协定,一般都会上图中的 MSB先行模式。
  2. 观察图中的 ②③④⑤ 标号处,MOSI 及 MISO的数据在 SCK下降沿 变化输出,在 SCK上升沿 被采样。即在SCK的上升沿时刻,MOSI及MISO的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及MISO为下一次表示数据做准备。
  3. SPI每次数据传输可以 8位或16位为单位,每次传输的 单位数 不受限制。

【实验1 SPI-Flash 全擦除 实验】

1、在对工程上板验证时,可以通过两种方式烧录程序:

(1)将程序(sof文件) 下载 到FPGA内部的 SRAM 之中,这种方式烧录过程耗时较短,但掉电后程序会丢失,再次上电后要重新烧录程序;

(2)将程序(jic文件) 固化 到FPGA外部挂载的 Flash芯片中,Flash芯片是 非易失性存储器,程序掉电后不会丢失,重新上电后会执行掉电前 烧录到Flash中的程序,但是烧录程序耗时较长。

【由 sof文件 生成 jic文件】

S1.

S2.设置好后选择生成

2、针对方式2,若想要将 固化到Flash 中的程序删除,可以通过两种方式,分别是 全擦除扇区擦除

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

方式一:利用FPGA编译软件,通过Quartus软件的“programmer”窗口,将烧录到Flash的*.jic文件擦除,具体见下图;

方式二:编写全擦除程序,实现Flash芯片的全擦除,就是下面要进行的实验。

【Flash芯片介绍】

Flash芯片,是一种 非易失性存储芯片,掉电后数据不会丢失。在FPGA工程的设计中,Flash主要用作 外接芯片 来 存储FPGA程序,使FPGA在上电后可以立即执行程序。SPI-Flash芯片就是支持SPI通讯协议的Flash芯片。

征途Pro开发板使用的Flash型号为W25Q16存储容量为16Mbit(2M字节)。

Flash芯片数据手册:w25q16_datasheet.pdf

【全擦除(Bulk Erase)操作】

简称BE,操作指令为8’b 1100_0111 (C7h);

全擦除操作:将 Flash芯片中的所有 存储单元 设置为 全1,实现步骤如下:

  1. 先写入 写使能(WREN) 指令,将芯片设置为 写使能锁存(WEL) 状态;
  2. 拉低片选信号(S'),写入全擦除指令。在指令写入过程中,片选信号(S')保持低电平,待指令被芯片 锁存 后,将片选信号拉高;
  3. 全擦除指令被锁存并执行后,等待一个完整的 全擦除周期(tBE),完成Flash芯片的全擦除操作。

全擦除操作的详细介绍及时序图,

【写使能指令(Write Enable)】

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

接下来详细说明 写使能指令 相关内容:

写使能(Write Enable)指令,简称WREN,操作指令为8'b 0000_0110(06h)。

写使能指令(WREN) ,可将 Flash芯片 设置为 写使能锁存(WEL) 状态;在每一次页 写操作(PP)、扇区擦除(SE)、全擦除(BE) 和 写状态寄存器(WRSR)操作 之前,都需要先进行 写使能指令写入 操作。

操作时序为先拉低片选信号,写入写使能指令,在指令写入过程中,片选信号始终保持低电平,指令写入完成后,将片选信号拉高。

写使能指令的详细介绍及时序图:

【串行输入时序】

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

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

图中定义了许多时间参数,其中有三个需要格外注意,分别是tSLCH、tCHSH和tSHSL。时间参数参考数值如图:

片选信号下降沿 始到 第一个有效数据写入 时止,这一时间段必须 大于等于5ns;

片选信号 最后一个有效数据写入 时始到 片选信号上升沿 止,这一时间段必须 大于等于5ns;

片选信号 自上一个上升沿 始到 下一个下降沿 止,这一时间段必须大于等于100ns(2指令间的间隔时间)。

综上述对操作时序、全擦除指令、写使能指令、Flash芯片的串行输入时序 的介绍,绘制完整全擦除操作时序图如图:

【串行输入时序时钟要求】

选择低于20MHZ时钟,对系统时钟进行4分频,得12.5MHZ

【程序设计】

整个 全擦除工程 调用3个模块,按键消抖模块(key_filter), Flash全擦除模块(flash_be_ctrl) 和 顶层模块(spi_flash_be):

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

【全擦除控制模块(flash_be_ctrl)】

生成并输出 小于20M时钟(sck)、片选(s_n)、数据信号(mosi),向Flash芯片发送 全擦除指令,控制Flash芯片实现全擦除。

整个过程分为:IDEL、WREN(写使能)、两指令间等待状态(DELAY)、BE(擦除),用状态机编写

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

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

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

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

对于这一流程操作,使用状态机来实现。在模块内部声明状态机状态变量state,定义状态机各状态分别为:

初始状态(IDLE)、写使能状态(WR_EN)、两指令间等待状态(DELAY)、全擦除状态(BE)。

状态机状态跳转流程如下:

  1. 系统上电后,状态机状态变量state处于

     

    1. 当 全擦除触发信号key 有效时,表示开始对Flash芯片的 全擦除操作,状态机跳转到 s_n)拉低
    2. 等待 tSLCH ≥5ns 时间,等待时间过后对 主输出从输入信号(mosi) 写入 写使能指令写使能指令赋值给mosi)
    3. 等待 tCHSH ≥5ns 时间,等待时间过后 片选信号(s_n)拉高,取消对Flash芯片的选择,同时状态机跳转到两指令间

       

      1. 等待 tSHSL≥ 100ns 时间,状态机跳转到 (s_n)拉低,选中已写入 写使能指令的 Flash芯片;
      2. 等待 tSLCH≥5ns 时间,等待时间过后对 主输出从输入信号(mosi) 写入全擦除指令(BE)
      3. 等待 tCHSH≥5ns 时间,等待时间过后 片选信号(s_n)拉高,取消对Flash芯片的选择,同时状态机跳回

         

        【问题1】片选信号的等待时间tSLCH、 tCHSH、tSHSL的参数确定:

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

        上述过程中 tSLCH、 tCHSH、tSHSL等待时间分别是至少 5ns、5ns、100ns,将8bit写使能指令(0000_0110)和全擦除指令(1100_0111)写入mosi信号需要640ns(80ns*8bit)。故可声明一个通用计数器cnt_clk,以640ns(32个计数单位)为下限进行等待时间的计数,满足所有参数的等待要求。cnt_clk计数器 初值为0,在0-31计数范围内循环计数。在状态机处于初始状态时,始终保持为0;在状态机处于初始状态之外的其他状态时,每个系统时钟周期自加1,计到最大值清0,重新计数。

         

        【问题2】状态机状态跳转约束条件的确定:

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

        计数器cnt_byte:

        声明计数器 cnt_byte 对计数器 cnt_clk 的计数周期进行计数,cnt_byte初值为0:当状态机处于 初始状态(IDLE)时,计数器cnt_byte始终 保持初值0;当状态机处于 除初始状态外的其他状态时,计数器cnt_byte开始对计数器cnt_clk的计数周期进行计数,cnt_clk每完成一个完整的循环计数,即cnt_clk = 31时,计数器cnt_byte自加1,其他时刻保持当前值不变。(cnt_clk记满32次即最大延迟时间,可写入1byte=8bit数据)。

        状态跳转:

        1. 当状态机跳转到 写使能状态(WR_EN),同时 片选信号(s_n)拉低,
        2. 在计数器cnt_clk的第1个计数周期(cnt_byte = 0),是对 片选信号等待时间 tSLCH = 640ns 的计数;
        3. 在计数器cnt_clk的第2个计数周期(cnt_byte = 1),是对 写使能指令 写入时间(在640ns内进行 写入8bit 写使能指令) 进行计数;
        4. 在计数器cnt_clk的第3个计数周期(cnt_byte = 2),是对 片选信号等待时间 tCHSH = 640ns 的计数;
        5. 状态机跳转到 两指令间等待状态(DELAY),同时 片选信号(s_n)拉高;
        6. 在计数器cnt_clk的第4个计数周期(cnt_byte = 3),是对 片选信号 两指令之间的等待时间 tSHSL = 640ns 的计数;
        7. 状态机跳转到 全擦除状态(BE),片选信号再次拉低;
        8. 在计数器cnt_clk的第5个计数周期(cnt_byte = 4),是对 片选信号等待时间 tSLCH = 640ns 的计数;
        9. 在计数器cnt_clk的第6个计数周期(cnt_byte = 5),是对 全擦除指令写入时间 (在640ns内进行 写入8bit 全擦除指令) 进行计数;
        10. 在计数器cnt_clk的第7个计数周期(cnt_byte = 6),是对 片选信号等待时间 tCHSH = 640ns 的计数;
        11. 状态机跳转到 初始状态(IDLE),Flash芯片的全擦除操作完成。

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

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

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

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

        声明四分频计数器cnt_sck,赋初值为0,只有在cnt_byte计算值为1或5时,即输出写使能指令或全擦除指令时,计数器 cnt_sck 在0-3范围内循环计数,计数周期为系统时钟周期,每个时钟周期自加1;使用四分频计数器cnt_sck作为约束条件,生成串行时钟sck,频率为12.5MHz。

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

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

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

        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 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 ; //比特计数器

        //cnt_clk:系统时钟计数器,计数0-31,共640ns,用于表示延迟时间和8bit数据写入时间。
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_clk <= 5'd0;
        // else if(state != IDLE)
        // cnt_clk <= cnt_clk + 1'b1; //cnt_clk位宽为5,此处计数值为5位宽最大值,可自动清零
        else if((state == IDLE) || (cnt_clk == 5'd31))
        cnt_clk <= 5'd0 ;
        else
        cnt_clk <= cnt_clk + 1'b1 ;
        end

        //cnt_byte:计数0-6,隔出7段32(640ns),每计数1个32用于时间延迟以及写入指令
        //读使能状态WR_EN(等待5ns、写入读指令、等待5ns);3
        //间隔等待时间DEALY(100ns); 1
        //全擦除指令(等待5ns、写入擦除指令、等待5ns); 3
        always@(posedge sys_clk or negedge sys_rst_n)begin
        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 == 5'd31)
        cnt_byte <= cnt_byte + 1'b1;
        end

        /***sck:串行时钟信号/
        //cnt_sck:串行时钟计数器:在7段32中的第1段、第5段计数0-3,进行4分频
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_sck <= 2'd0;
        else if((state == WR_EN) && (cnt_byte == 1'b1)//第1段,读指令写入
        ||(state == BE) && (cnt_byte == 3'd5))//第5段,全擦除指令写入
        cnt_sck <= cnt_sck + 1'b1;
        end

        //sck:生成 4分频 串行时钟
        always@(posedge sys_clk or negedge sys_rst_n)begin
        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;
        end

        /***cs_n:片选信号/
        //cs_n:片选信号
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cs_n <= 1'b1; //初始状态片选信号为高,不工作
        else if(key == 1'b1)
        cs_n <= 1'b0; //按键输入,拉低片选信号,选中芯片
        else if((state == WR_EN) && (cnt_byte == 3'd2) && (cnt_clk == 5'd31))//(0-2)3段,WR_EN状态结束
        cs_n <= 1'b1; //
        else if((state == DELAY) && (cnt_byte == 3'd3) && (cnt_clk == 5'd31))//第4段,间隔等待状态结束
        cs_n <= 1'b0; //片选信号再次选中,开始全擦除状态
        else if((state == BE) && (cnt_byte == 3'd6) && (cnt_clk == 5'd31)) //(4-6)3段,BE状态结束
        cs_n <= 1'b1;
        end

        /***mosi:主出从入信号/
        //cnt_bit:高低位对调,控制mosi输出
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_bit <= 3'd0;
        else if(cnt_bit == 3'd7 && cnt_sck == 2'd2)
        cnt_bit <= 3'd0;
        else if(cnt_sck == 2'd2) // 与cnt_clk对齐
        cnt_bit <= cnt_bit + 1'b1;
        end

        //state:两段式状态机第一段,状态跳转
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        state <= IDLE;
        else begin
        case(state)
        IDLE: if(key == 1'b1)
        state <= WR_EN;
        WR_EN: if((cnt_byte == 3'd2) && (cnt_clk == 5'd31))//(0-2)3段,WR_EN状态结束
        state <= DELAY;
        DELAY: if((cnt_byte == 3'd3) && (cnt_clk == 5'd31))//第4段,间隔等待状态结束
        state <= BE;
        BE: if((cnt_byte == 3'd6) && (cnt_clk == 5'd31))//(4-6)3段,BE状态结束
        state <= IDLE;
        default: state <= IDLE;
        endcase
        end
        end

        //mosi:两段式状态机第二段,逻辑输出
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        mosi <= 1'b0;
        // else if((state == WR_EN) && (cnt_byte == 3'd2)
        // ||(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]; //全擦除指令
        end

        endmodule

        【按键消抖模块(key_filter)】

        module  key_filter(
            input   wire    sys_clk     ,   //系统时钟50Mhz
            input   wire    sys_rst_n   ,   //全局复位
            input   wire    key_in      ,   //按键输入信号
        
        output  reg     key_out        //key_out为1时表示消抖后检测到按键被按下
                                        //key_out为0时表示没有检测到按键被按下
        

        );

        parameter CNT_MAX = 20'd1_000_000; //计数器计数最大值,计数20ms

        //reg define
        reg [19:0] cnt_20ms ; //计数器

        //cnt_20ms:时钟上升沿检测到按键输入为低电平,计数器开始计数
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        cnt_20ms <= 20'b0;
        else if(key_in == 1'b1) //按键信号拉高,清零计数
        cnt_20ms <= 20'b0;
        else if(cnt_20ms == CNT_MAX-1'b1 && key_in == 1'b0) //计数满,但按键仍为低则保持计数值
        cnt_20ms <= cnt_20ms;
        else
        cnt_20ms <= cnt_20ms + 1'b1;

        //key_out:当计数满20ms后产生按键有效标志位,且key_out在999_999时拉高(触发信号为999_998),维持一个时钟的高电平
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        key_out <= 1'b0;
        else if(cnt_20ms == CNT_MAX - 2'd2)//当计数到999_998时,产生按键输出标志信号
        key_out <= 1'b1;
        else
        key_out <= 1'b0;

        endmodule

        【顶层模块(top_spi_flash_be)】

        module  top_spi_flash_be(
            input   wire    sys_clk     ,   //系统时钟,频率50MHz
            input   wire    sys_rst_n   ,   //复位信号,低电平有效
            input   wire    key_in     ,   //按键输入信号
        
        output  wire    cs_n        ,   //片选信号
        output  wire    sck         ,   //串行时钟
        output  wire    mosi            //主输出从输入数据
        

        );

        //parameter define
        // parameter CNT_MAX = 20'd1_000_000; //计数器计数最大值,20ms

        //wire define
        wire key_flag ;

        //------------- key_filter_inst -------------
        key_filter #(.CNT_MAX(1_00))key_filter_inst01(
        .sys_clk (sys_clk ), //系统时钟,频率50MHz
        .sys_rst_n (sys_rst_n ), //复位信号,低电平有效
        .key_in (key_in ), //按键输入信号

        .key_out    (key_flag     )   //消抖后信号
        

        );

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

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

        );

        endmodule

        `timescale  1ns/1ns
        module  tb_top_spi_flash_be();
        //reg   define
        reg     sys_clk    ;
        reg     sys_rst_n  ;
        reg     key_in     ;
        //wire  define
        wire    cs_n ;
        wire    sck  ;
        wire    mosi ;
        
        //时钟、复位信号、模拟按键信号
        initial
            begin
                sys_clk = 0;
                sys_rst_n <= 0;
                key_in <=  0; //初始化key_in为低电平
                #100
                sys_rst_n <= 1;
                #1000
        
                key_in <=  1;//产生1个按键激励,模拟抖动
                #20
                key_in <=  0;
                #200
                key_in <=  1;//再产生1个按键激励,模拟抖动
                #20
                key_in <=  0;
            end
        always  #10 sys_clk <=  ~sys_clk;
        
        //在testbench.v文件中复制
        defparam memory.mem_access.initfile = "initmemory.txt";
        
        //-------------spi_flash_erase-------------//
        //模拟master芯片
        top_spi_flash_be  top_spi_flash_be_inst01(
            .sys_clk    (sys_clk    ),  //系统时钟,频率50MHz
            .sys_rst_n  (sys_rst_n  ),  //复位信号,低电平有效
            .key_in     (key_in    ),  //按键输入信号
        
            .sck        (sck    ),  //串行时钟
            .cs_n       (cs_n   ),  //片选信号
            .mosi       (mosi   )   //主输出从输入数据
        );
        
        //模拟slave芯片,直接复制不能改例化名称
        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

         

         

         

         

         

         

         

         

         

         

        【实验2 SPI-Flash 扇区擦除 实验】

        扇区擦除可以对Flash芯片中的 某一扇区 进行擦除而不改变其他扇区中的存储数据,要擦除 扇区的选择 通过 扇区擦除地址 来表示。

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

        【falsh存储结构】

        M25P16:

        16 Mbit = 16 * 1024*1024 bit = 2_097_152 byte --> 2_097_152 byte /32/256

        sector(扇区):32扇区,每扇区256个 page

        page(页):每页256个 byte(共8192页, 2_097_152 byte)

        【地址构成】

        扇区地址(2位)+页地址(2位)+字节地址(2位)

        【扇区擦除(Sector Erase)操作】

        简称SE,操作指令为8’b1101_0000(D8h) 。

        【扇区擦除步骤】

        扇区擦除指令 是将Flash芯片中的 被选中扇区 的所有存储单元设置为 全1。

        1. 在Flash芯片写入扇区擦出指令之前,需要先写入 写使能(WREN)指令,将芯片设置为 写使能锁存(WEL)状态;
        2. 随后要 拉低片选信号,写入 扇区擦除指令、扇区地址、页地址 和 字节地址,在指令、地址写入过程中,片选信号始终保持低电平,待指令、地址被芯片锁存后,将片选信号拉高;
        3. 扇区擦除指令、地址被锁存并执行后,需要等待一个完整的扇区擦除周期(tSE),才能完成Flash芯片的扇区擦除操作。

        【注】(1)全擦除可擦除数据 16Mbit(整个存储空间);

        (2)扇区擦除可擦除数据 512Kbit(1个扇区);

        【程序设计】

        整个扇区擦除工程也分为3个模块,按键消抖模块(key_filter)、扇区擦除模块(flash_se_ctrl)、顶层模块(spi_flash_se):

        【扇区擦除控制模块( flash_se_ctrl)】

        外部按键产生擦除标志信号,经消抖处理后输出进入 扇区擦除模块(flash_se_ctrl),此信号作为触发条件触发Flash扇区擦除模块工作后, 扇区擦除模块输出串行时钟信号(sck)、片选信号(cs_n)和主输出从输入信号(mosi),3路信号通过顶层模块输入外部挂载的Flash芯片,实现扇区擦除功能。Flash扇区擦除整体波形图如下:

        对比扇区擦除波形图与全擦除波形图可以看出,两波形图中各信号波形变化类似,唯一区别就是,相对全擦除而言,扇区擦除在写入扇区擦除指令后还需要写入3字节的地址信息。

        全擦除操作共分为(0-6)7段32,扇区擦除多了3byte地址增加为(0-9)10段32;

        段1:写使能指令

        段5:扇区擦除指令

        段6:扇区地址

        段7:页地址

        段8:字节地址

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

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

        第二部分:状态机相关信号的波形

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

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

        【状态机状态跳转流程】:

        s1. 系统上电后,状态机状态变量state一直处于初始状态(IDLE);

        s2. 当传入的扇区擦除触发信号key有效时,表示实验工程开始执行对Flash芯片的扇区擦除操作,状态机跳转到写使能状态(WR_EN),同时片选信号拉低,选中要进行扇区擦除操作的Flash芯片;

        s3. 状态跳转到写使能状态且片选信号拉低后,要进行tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入写使能指令,指令写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳转到两指令间等待状态(DELAY);

        s4. 在此状态等待时间tSHSL≥ 100ns后,状态机跳转到扇区擦除状态(SE),同时片选信号拉低,选中已写入写使能指令的Flash芯片;

        s5. 状态机跳转到扇区擦除状态且片选信号拉低后,要进行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芯片的扇区擦除操作完成。

        第三部分:输出相关信号的波形

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

        模块输出的 串行时钟(sck)为12.5MHz,为系统时钟50MHz通过 四分频 得到。声明一个四分频计数器,对系统时钟进行四分频。

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

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

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

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

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

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

        module  flash_se_ctrl(
            input   wire    sys_clk     ,   //系统时钟,频率50MHz
            input   wire    sys_rst_n   ,   //复位信号,低电平有效
            input   wire    key_flash   ,   //消抖后的按键输入标志信号,高有效
        
        output  reg     cs_n        ,   //片选信号,低有效
        output  reg     sck         ,   //串行时钟
        output  reg     mosi            //主输出从输入数据
        

        );

        //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_10; //字节计数器
        reg [3:0] state ; //状态机状态
        reg [4:0] cnt_clk_32 ; //系统时钟计数器
        reg [1:0] cnt_sck ; //串行时钟计数器
        reg [2:0] cnt_bit ; //比特计数器

        /******计数器将 写指令周期 和 扇区擦除周期 分为10个部分,每个部分32小段/
        //cnt_clk_32:系统时钟计数器,用以记录单个字节
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        cnt_clk_32 <= 5'd0;
        // else if(state != IDLE)
        // cnt_clk_32 <= cnt_clk_32 + 1'b1;//cnt_clk_32位宽为5,此处计数值为5位宽最大值,可自动清零
        else if((state == IDLE) || (cnt_clk_32 == 5'd31))
        cnt_clk_32 <= 5'd0 ;
        else
        cnt_clk_32 <= cnt_clk_32 + 1'b1 ;

        //cnt_byte_10:计数0-9,隔出10段32(640ns),每计数1个32用于时间延迟以及写入指令
        //读使能状态WR_EN(等待5ns、写入读指令、等待5ns); 3(0、1、2)
        //间隔等待时间DEALY(100ns); 1(3)
        //扇区擦除状态(等待5ns,扇区擦除指令,扇区地址,页地址,字节地址,等待5ns):6(4,5,6,7,8,9)
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_byte_10 <= 4'd0;
        else if(cnt_clk_32 == 5'd31) begin
        if (cnt_byte_10 == 4'd9)
        cnt_byte_10 <= 4'd0;
        else
        cnt_byte_10 <= cnt_byte_10 + 1'b1; //每当计数32则记为1段,计10段清零
        end
        end

        /***cs_n:片选信号/
        //cs_n:片选信号
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        cs_n <= 1'b1;
        else if(key_flash == 1'b1)
        cs_n <= 1'b0;
        else if((state == WR_EN) && (cnt_byte_10 == 4'd2) && (cnt_clk_32 == 5'd31))//0、1、2
        cs_n <= 1'b1;
        else if((state == DELAY) && (cnt_byte_10 == 4'd3) && (cnt_clk_32 == 5'd31))//3
        cs_n <= 1'b0;
        else if((state == SE) && (cnt_byte_10 == 4'd9) && (cnt_clk_32 == 5'd31))//4、5、6、7、8、9
        cs_n <= 1'b1;

        /******sck:串行时钟信号,4分频系统时钟信号/
        //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_10 == 1'b1))
        cnt_sck <= cnt_sck + 1'b1;
        else if((state == SE) && (cnt_byte_10 >= 4'd5) && (cnt_byte_10 <= 4'd8))//4、5,6,7,8、9
        cnt_sck <= cnt_sck + 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;

        /***mosi:主出从入信号/
        //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_flash == 1'b1) //按键按下,开始擦除
        state <= WR_EN;
        WR_EN:if((cnt_byte_10 == 4'd2) && (cnt_clk_32 == 5'd31))
        state <= DELAY; //3,第4段
        DELAY:if((cnt_byte_10 == 4'd3) && (cnt_clk_32 == 5'd31))
        state <= SE; //4,(5,6,7,8),9,第5-9段
        SE: if((cnt_byte_10 == 4'd9) && (cnt_clk_32 == 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_10 == 4'd2))
        // mosi <= 1'b0;
        // else if((state == SE) && (cnt_byte_10 == 4'd9))
        // mosi <= 1'b0;
        else if((state == WR_EN) && (cnt_byte_10 == 4'd1) && (cnt_sck == 5'd0))
        mosi <= WR_EN_INST[7 - cnt_bit]; //写使能指令
        else if((state == SE) && (cnt_byte_10 == 4'd5) && (cnt_sck == 5'd0))
        mosi <= SE_INST[7 - cnt_bit]; //扇区擦除指令
        else if((state == SE) && (cnt_byte_10 == 4'd6) && (cnt_sck == 5'd0))
        mosi <= SECTOR_ADDR[7 - cnt_bit]; //扇区地址
        else if((state == SE) && (cnt_byte_10 == 4'd7) && (cnt_sck == 5'd0))
        mosi <= PAGE_ADDR[7 - cnt_bit]; //页地址
        else if((state == SE) && (cnt_byte_10 == 4'd8) && (cnt_sck == 5'd0))
        mosi <= BYTE_ADDR[7 - cnt_bit]; //字节地址

        endmodule

        `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

        【按键消抖模块(key_filter)】

        module  key_filter(
            input   wire    sys_clk     ,   //系统时钟50Mhz
            input   wire    sys_rst_n   ,   //全局复位
            input   wire    key_n_in      ,   //按键输入信号
        
        output  reg     key_flag        //key_flag为1时表示消抖后检测到按键被按下
                                         //key_flag为0时表示没有检测到按键被按下
        

        );

        parameter CNT_MAX = 20'd1_000_000; //计数器计数最大值,计数20ms

        //reg define
        reg [19:0] cnt_20ms ; //计数器

        //cnt_20ms:时钟上升沿检测到按键输入为低电平,计数器开始计数
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        cnt_20ms <= 20'b0;
        else if(key_n_in == 1'b1) //按键信号拉高,清零计数
        cnt_20ms <= 20'b0;
        else if(cnt_20ms == CNT_MAX-1'b1 && key_n_in == 1'b0) //计数满,但按键仍为低则保持计数值
        cnt_20ms <= cnt_20ms;
        else
        cnt_20ms <= cnt_20ms + 1'b1;

        //key_flag:当计数满20ms后产生按键有效标志位,且key_flag在999_999时拉高(触发信号为999_998),维持一个时钟的高电平
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        key_flag <= 1'b0;
        else if(cnt_20ms == CNT_MAX - 2'd2)//当计数到999_998时,产生按键输出标志信号
        key_flag <= 1'b1;
        else
        key_flag <= 1'b0;

        endmodule

        【顶层模块(top_spi_flash_se)】

        module  top_spi_flash_se(
            input   wire    sys_clk     ,   //系统时钟,频率50MHz
            input   wire    sys_rst_n   ,   //复位信号,低电平有效
            input   wire    key_n_in      , //按键输入信号
        
        output  wire    cs_n        ,   //片选信号
        output  wire    sck         ,   //串行时钟
        output  wire    mosi            //主输出从输入数据
        

        );

        //parameter define
        parameter CNT_MAX = 20'd999_999; //计数器计数最大值

        //wire define
        wire key_flag ;

        ////
        //
        Instantiation //
        //
        *//
        //------------- key_filter_inst -------------
        key_filter key_filter_inst01(
        .sys_clk (sys_clk ), //系统时钟,频率50MHz
        .sys_rst_n (sys_rst_n ), //复位信号,低电平有效
        .key_n_in (key_n_in ), //按键输入信号

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

        );

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

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

        );

        endmodule

        【仿真】

        【注】

        1、仿真程序中要初始化flash,直接从testbench文件中复制:

        defparam memory.mem_access.initfile = "initmemory.txt";

        2、要例化m25p16,模拟slave接收数据,直接从testbench文件中复制,不要修改名称:

        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   (       )   //输出串行数据
        

        );

        3、仿真设置时要添加M25P16_VG_V12文件夹中的6个文件:

        `timescale  1ns/1ns
        module  tb_top_spi_flash_se();
        //reg   define
        reg     sys_clk    ;
        reg     sys_rst_n  ;
        reg     key_n_in     ;
        //wire  define
        wire    cs_n ;
        wire    sck  ;
        wire    mosi ;
        

        //时钟、复位信号、模拟按键信号
        initial
        begin
        sys_clk = 0;
        sys_rst_n <= 0;
        key_n_in <= 0; //初始化key_n_in为低电平
        #100
        sys_rst_n <= 1;
        #1000

            #200
            key_n_in &lt;=  1;//产生1个按键激励,模拟抖动
            #20
            key_n_in &lt;=  0;
        
            #200
            key_n_in &lt;=  1;//再产生1个按键激励,模拟抖动
            #20
            key_n_in &lt;=  0;
        end
        

        always #10 sys_clk <= ~sys_clk;

        //在testbench.v文件中复制
        defparam memory.mem_access.initfile = "initmemory.txt";

        //-------------spi_flash_erase-------------//
        //模拟master芯片
        top_spi_flash_be top_spi_flash_be_inst01(
        .sys_clk (sys_clk ), //系统时钟,频率50MHz
        .sys_rst_n (sys_rst_n ), //复位信号,低电平有效
        .key_n_in (key_n_in ), //按键输入信号

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

        );

        //模拟slave芯片,直接复制不能改例化名称
        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

        本实验中,SPI工作于 模式0(00),Flash芯片在串行时钟sck的 上升沿 进行 数据采样,在sck的 下降沿 进行传输 数据更新,即在sck的下降沿对mosi信号 写入 写使能指令 和 全擦除指令。

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

        【实验3 SPI-Flash 页写 实验】

        使用页写指令,向Flash中写入N字节数据,0<N<256。本实验中向Flash芯片中写入0-99,共100字节数据,数据初始地址为:24’h00_04_25。

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

        【页写操作(Page Program)】

        简称PP,操作指令为8’b0000_0010 (02h):

        1、页写指令是根据写入数据 将存储单元中的“1”置为“0”,实现数据的写入。具体步骤:

        S1、在写入页写指令之前,需要先写入 写使能(WREN)指令,将芯片设置为 写使能锁存(WEL)状态;

        S2、随后要拉低 片选信号,写入 页写指令、扇区地址、页地址、字节地址,紧跟地址写入要存储在Flash的 字节数据,在指令、地址以及数 据写入过程中,片选信号始终保持低电平,待指令、地址、数据被芯片锁存后,将片选信号拉高;

        S3、片选信号拉高后,等待一个完整的页写周期(tPP),才能完成Flash芯片的页写操作。

        2、Flash芯片中一页最多可以 存储256字节数据,表示页写操作一次最多向Flash芯片 写入256字节数据。

        3、页写指令写入后,随即写入 3字节 数据写入首地址扇区地址页地址字节地址),扇区地址与页地址是确定数据写入Flash的 特定扇区 的特定页,字节地址位为在该页数据写入的 字节首地址。

        【写入情况】:写入数据个数超过存储数量,从头写,覆盖之前的数据。

        1. 字节首地址为 该页的首地址,即 字节首地址为8’b0000_0000:

        (1)数据写入个数 N 小于 256 字节,数据可以被正确写入Flash芯片;

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

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

        1. 字节首地址 非该页的首地址,即 字节首地址不是8’b0000_0000,数据写入个数为 N (0<N<256):

        (1)若数据写入个数 N 少于 字节首地址地址到末地址之间的 存储单元个数,数据可以被正确写入Flash芯片;

        (2)若数据写入个数 N 多于 字节首地址地址到末地址之间的 存储单元个数,等于字节首地址地址到末地址之间的存储单元个数的数据可以被正 确写入Flash芯片,超出的那部分数据重新以8’b0000_0000为字节首地址顺序写入本页,覆盖改地址之前存储的数据。

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

        【程序设计】

        页写工程也分为3个模块,按键消抖模块(key_filter)、页写模块(flash_pp_ctrl)和包含各模块实例化的顶层模块(spi_flash_pp)。

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

        【页写控制模块(flash_pp_ctrl)】

        当页写触发信号输入页写模块时,页写模块执行页写操作,输出串行时钟信号(sck)、片选信号(cs_n)和主输出从输入信号(mosi)。

        主输出从输入信号输出数据包括 页写指令(8'b0000_0010)、地址(8bit,扇区地址、页地址、字节地址) 和 待写入数据。

        Flash页写模块整体波形图如下:

        对比 页写模块波形图 与 扇区擦除模块波形图 可以看出,两波形图中各信号波形变化类似,存在区别就是,相对扇区擦除而言,页写操作在 写入页写指令和 数据写入首地址后还需要写入 n字节数据 (n为整数,0 < n ≤ 256)

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

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

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

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

        使用状态机来实现。在模块内部声明状态机状态变量state,定义状态机各状态分别为:初始状态(IDLE)、写使能状态(WR_EN)、两指令间等待状态(DELAY)、页写状态(PP)。

        【状态机状态跳转流程】:

        系统上电后,状态机状态变量state一直处于初始状态(IDLE);

        当传入的扇区擦除触发信号key有效时,状态机跳转到写使能状态(WR_EN),同时片选信号拉低,选中要进行扇区擦除操作的Flash芯片;

        进行tSLCH≥5ns的等待时间,等待时间过后对写入写使能指令(赋值给mosi),指令写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择;

        状态机跳转到两指令间等待状态(DELAY);在此状态等待时间tSHSL≥ 100ns后,状态机跳转到页写状态(PP),同时片选信号拉低,选中已写入写使能指令的Flash芯片;

        状态机跳转到 页写状态 且片选信号拉低后,进行 tSLCH≥5ns的等待时间,等待时间过后对主输出从输入信号写入页写指令、3字节的数据写入首地址地址和 待写入数据,数据写入完成后需要进行tCHSH≥5ns的等待时间,等待时间过后拉高片选信号,取消对Flash芯片的选择,同时状态机跳回初始状态(IDLE),一次完整的页写操作完成。

        使用这两个计数器作为约束条件就可以实现状态机的状态跳转,当状态机跳转到写使能状态时,同时片选信号拉低,在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芯片的页写操作完成。

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

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

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

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

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

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

        module  flash_pp_ctrl(
        
        input   wire            sys_clk     ,   //系统时钟,频率50MHz
        input   wire            sys_rst_n   ,   //复位信号,低电平有效
        input   wire            key_flash   ,   //按键输入信号
        
        output  reg             cs_n        ,   //片选信号
        output  reg             sck         ,   //串行时钟
        output  reg             mosi            //主输出从输入数据
        

        );

        //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; //字节地址:37
        parameter NUM_DATA = 8'd100 ; //页写数据个数(0-99),N=100

        //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 ; //页写入数据

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

        //cnt_byte_10:计数0-9,隔出10段32(640ns),每计数1个32用于时间延迟以及写入指令
        //读使能状态WR_EN(等待5ns、写入读指令、等待5ns); 3(0、1、2)
        //间隔等待时间DEALY(100ns); 1(3)
        //扇区擦除状态(等待5ns,扇区擦除指令,扇区地址,页地址,字节地址,等待5ns):6(4,5,6,7,8,9)
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_byte <= 8'd0;
        else if((cnt_clk == 5'd31) && (cnt_byte == 8'd9 + NUM_DATA ))
        cnt_byte <= 8'd0; //计数109段32,清零
        else if(cnt_clk == 5'd31)
        cnt_byte <= cnt_byte + 1'b1;
        end

        /***cs_n:片选信号/
        //cs_n:片选信号
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cs_n <= 1'b1; //初始高电平begin
        else if(key_flash == 1'b1)
        cs_n <= 1'b0; //按键信号到来,拉低片选信号,写使能状态
        else if((cnt_byte == 8'd2) && (cnt_clk == 5'd31) && (state == WR_EN))
        cs_n <= 1'b1; //第3段等待时间后,拉高片选信号,进入间隔状态
        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; //在109个32后,数据写入,拉高片选信号,进入空闲状态
        end

        /******sck:串行时钟信号,4分频系统时钟信号/
        //cnt_sck:串行时钟计数器,用以生成串行时钟
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_sck <= 2'd0;
        else if(((state == WR_EN) && (cnt_byte == 8'd1)) //一共计数(0,NUM_DATA + 8'd9 - 1'b1)个32
        || (state == PP) && (cnt_byte >= 8'd5) && (cnt_byte <= NUM_DATA + 8'd9 - 1'b1))
        cnt_sck <= cnt_sck + 1'b1;
        end
        //sck:输出串行时钟
        always@(posedge sys_clk or negedge sys_rst_n)begin
        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;
        end

        /***mosi:主出从入信号/
        //cnt_bit:高低位对调,控制mosi输出
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        cnt_bit <= 3'd0;
        else if(cnt_sck == 2'd2)
        cnt_bit <= cnt_bit + 1'b1;
        end
        //data:页写入数据(0-99)
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        data <= 8'd0;
        else if(((cnt_byte >= 8'd9) && (cnt_byte < NUM_DATA + 8'd9 - 1'b1)) && (cnt_clk == 5'd31))
        data <= data + 1'b1;
        end
        //state:两段式状态机第一段,状态跳转
        always@(posedge sys_clk or negedge sys_rst_n)begin
        if(sys_rst_n == 1'b0)
        state <= IDLE;
        else
        case(state)
        IDLE: if(key_flash == 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
        end
        //mosi:两段式状态机第二段,逻辑输出
        always@(posedge sys_clk or negedge sys_rst_n)begin
        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]; //页写入数据
        end

        endmodule

        `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
        

        【按键消抖模块】

        module  key_filter(
            input   wire    sys_clk     ,   //系统时钟50Mhz
            input   wire    sys_rst_n   ,   //全局复位
            input   wire    key_n_in      ,   //按键输入信号
        
        output  reg     key_flag        //key_flag为1时表示消抖后检测到按键被按下
                                         //key_flag为0时表示没有检测到按键被按下
        

        );

        parameter CNT_MAX = 20'd1_000_000; //计数器计数最大值,计数20ms

        //reg define
        reg [19:0] cnt_20ms ; //计数器

        //cnt_20ms:时钟上升沿检测到按键输入为低电平,计数器开始计数
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        cnt_20ms <= 20'b0;
        else if(key_n_in == 1'b1) //按键信号拉高,清零计数
        cnt_20ms <= 20'b0;
        else if(cnt_20ms == CNT_MAX-1'b1 && key_n_in == 1'b0) //计数满,但按键仍为低则保持计数值
        cnt_20ms <= cnt_20ms;
        else
        cnt_20ms <= cnt_20ms + 1'b1;

        //key_flag:当计数满20ms后产生按键有效标志位,且key_flag在999_999时拉高(触发信号为999_998),维持一个时钟的高电平
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        key_flag <= 1'b0;
        else if(cnt_20ms == CNT_MAX - 2'd2)//当计数到999_998时,产生按键输出标志信号
        key_flag <= 1'b1;
        else
        key_flag <= 1'b0;

        endmodule

        【顶层模块】

        module  top_spi_flash_pp(
            input   wire    sys_clk     ,   //系统时钟,频率50MHz
            input   wire    sys_rst_n   ,   //复位信号,低电平有效
            input   wire    key_n_in      ,   //按键输入信号
        
        output  wire    cs_n        ,   //片选信号
        output  wire    sck         ,   //串行时钟
        output  wire    mosi            //主输出从输入数据
        

        );

        //wire define
        wire key_flag ;

        //------------- key_filter_inst -------------//
        key_filter #(.CNT_MAX(100)) key_filter_inst01(
        .sys_clk (sys_clk ), //系统时钟,频率50MHz
        .sys_rst_n (sys_rst_n ), //复位信号,低电平有效
        .key_n_in (key_n_in ), //按键输入信号

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

        );

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

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

        );

        endmodule

        【仿真】

        `timescale  1ns/1ns
        module  tb_top_spi_flash_pp();
        //reg   define
        reg        sys_clk   ;
        reg        sys_rst_n ;
        reg        key_n_in  ;
        //wire  define
        wire       cs_n      ;
        wire       sck       ;
        wire       mosi      ;
        wire [7:0] cmd_data  ;
        

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

        initial begin
        $timeformat(-9, 0, "ns", 16);
        $monitor("@time %t: key_n_in=%b ", $time, key_n_in );
        $monitor("@time %t: cs_n=%b", $time, cs_n);
        $monitor("@time %t: cmd_data=%d", $time, cmd_data);
        end

        defparam memory.mem_access.initfile = "initmemory.txt";

        //-------------spi_flash_pp-------------//
        top_spi_flash_pp top_spi_flash_pp_inst01(
        .sys_clk (sys_clk ), //input sys_clk
        .sys_rst_n (sys_rst_n), //input sys_rst
        .key_n_in (key_n_in ), //input key_n_in

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

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

        endmodule

         

        【实验4 SPI_Flash 连续写 实验】

        通过页写指令实现数据的 连续写操作,方法有二。

        其一,每次 页写入 只写入 单字节数据,若想连续写入N字节数据,需要连续执行N次页写操作;

        其二,先通过 页写指令 一次性 写满第一页数据,计算 剩余数据 所占 整数页,通过 页写指令 写满整数页,每次页写指令一次写满一页,剩余不足一页的数据,再通过一次页写指令一次性写入。

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

        本实验实现第一种方法。

        【实验目标】

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

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

        【连续写操作时序】

        本实验实现的连续写操作是采用上述方式一,每次使用 页写操作 只向Flash芯片写入 单字节数据,连续执行多次单字节写入 来 实现连续写操作。其操作时序与页写时序相同,

        【程序设计】

        本实验与 页写工程 有所区别,使用 串口RS232 向 Flash芯片 连续写入数据(非按键控制)。

        连续写工程包括4个模块,串口接收模块(uart_rx)、连续写模块(flash_seq_wr_ctrl)、串口发送模块(uart_tx) 以及 顶层模块(spi_flash_seq_wr):

        1. PC端使用 串口助手 向FPGA 连续发送 100字节 串行数据;
        2. 串口接收模块(uart_rx) 接收 100字节 串行数据,并将 串行数据 拼接成 单字节数据 ,再发送到 连续写控制模块 以及 串口发送模块;
        3. 连续写模块(flash_seq_wr_ctrl) 按 SPI时序 输出 串行时钟信号(sck)、片选信号(cs_n) 和 主输出从输入信号(mosi),并通过 3路信号 将 单字节数据 写入 Flash芯片;
        4. 串口发送模块(uart_tx) 将 单字节数据 传回PC端,并通过 串口助手 打印出来。

        【连续写控制模块(flash_seq_wr_ctrl)】

        串口接收模块 将数据完成拼接后,将拼接后的 单字节数据 输入 连续写模块,与数据同步传入的还有 数据标志信号;

        连续写模块 接收到 有效的 数据标志信号 后,使用 页写指令 完成一次 单字节数据 的 写操作,输出 串行时钟、片选信号 和 主输出从输入信号。主输出从输入信号输出的数据包括指令、数据地址和与数据标志信号同步传入的单字节数据。

        对比 连续写模块 与 页写模块 波形图可以看出,两波形图中各信号波形变化类似,区别就是,相对页写模块,连续写操作 将 数据标志信号 作为触发信号 ,每次只写入 单字节数据,且每次数据写入都需要 更新存储地址。

        第一部分:输入信号波形

        系统上电之后,连续写模块一直处于初始状态,数据标志信号(pi_flag) 有效时,开始执行 页写相关操作。

        输入信号还包含 时钟信号sys_clk(50MHz)、复位信号sys_rst_n(低电平有效),输入信号波形图如下:

        第二部分:存储地址addr的波形

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

        声明 地址寄存器(addr_reg)和 存储地址(addr)实现 存储地址更新:

        1. 先将数据的 初始地址(ADDR) 赋值给 地址寄存器addr_reg,每接收到 标志信号 pi_flag后,则addr_reg自加1。
        2. 声明 存储地址(addr),初值为0,pi_data有效 时将 addr_reg值 赋给 addr。解决了 直接使用 addr_reg 作存储地址,会使 首地址无数据写入的问题,

        两地址信号波形如下:

        其他信号的波形设计与页写模块的相关信号相同。

        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             sck         ,   //串行时钟
        output  reg             cs_n        ,   //片选信号
        output  reg             mosi            //主输出从输入数据
        

        );

        //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 [23:0] addr_reg; //数据写入地址寄存器
        reg [23:0] addr ; //数据写入地址

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

        //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;

        //cs_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

        【串口输入模块(uart_rx)】
        module  uart_rx(
            input   wire            sys_clk     ,   //系统时钟50MHz
            input   wire            sys_rst_n   ,   //全局复位
            input   wire            rx          ,   //串口接收数据
        
        output  reg     [7:0]   po_data     ,   //串转并后的8bit数据
        output  reg             po_flag         //串转并后的数据有效标志信号
        

        );

        //localparam define
        parameter UART_BPS = 'd9600, //串口波特率
        CLK_FREQ = 'd50_000_000; //时钟频率
        localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;

        //reg define
        reg rx_reg1 ;
        reg rx_reg2 ;
        reg rx_reg3 ;
        reg start_nedge ;
        reg work_en ;
        reg [12:0] baud_cnt ;
        reg bit_flag ;
        reg [3:0] bit_cnt ;
        reg [7:0] rx_data ;
        reg rx_flag ;

        //插入两级寄存器进行数据同步,用来消除亚稳态
        //rx_reg1:第一级寄存器,寄存器空闲状态复位为1
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        rx_reg1 <= 1'b1;
        else
        rx_reg1 <= rx;

        //rx_reg2:第二级寄存器,寄存器空闲状态复位为1
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        rx_reg2 <= 1'b1;
        else
        rx_reg2 <= rx_reg1;

        //rx_reg3:第三级寄存器和第二级寄存器共同构成下降沿检测
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        rx_reg3 <= 1'b1;
        else
        rx_reg3 <= rx_reg2;

        //start_nedge:检测到下降沿时start_nedge产生一个时钟的高电平
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        start_nedge <= 1'b0;
        else if((~rx_reg2) && (rx_reg3))
        start_nedge <= 1'b1;
        else
        start_nedge <= 1'b0;

        //work_en:接收数据工作使能信号
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        work_en <= 1'b0;
        else if(start_nedge == 1'b1)
        work_en <= 1'b1;
        else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
        work_en <= 1'b0;

        //baud_cnt:波特率计数器计数,从0计数到BAUD_CNT_MAX - 1
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        baud_cnt <= 13'b0;
        else if((baud_cnt == BAUD_CNT_MAX - 1) || (work_en == 1'b0))
        baud_cnt <= 13'b0;
        else if(work_en == 1'b1)
        baud_cnt <= baud_cnt + 1'b1;

        //bit_flag:当baud_cnt计数器计数到中间数时采样的数据最稳定,
        //此时拉高一个标志信号表示数据可以被取走
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        bit_flag <= 1'b0;
        else if(baud_cnt == BAUD_CNT_MAX/2 - 1)
        bit_flag <= 1'b1;
        else
        bit_flag <= 1'b0;

        //bit_cnt:有效数据个数计数器,当8个有效数据(不含起始位和停止位)
        //都接收完成后计数器清零
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        bit_cnt <= 4'b0;
        else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
        bit_cnt <= 4'b0;
        else if(bit_flag ==1'b1)
        bit_cnt <= bit_cnt + 1'b1;

        //rx_data:输入数据进行移位
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        rx_data <= 8'b0;
        else if((bit_cnt >= 4'd1)&&(bit_cnt <= 4'd8)&&(bit_flag == 1'b1))
        rx_data <= {rx_reg3, rx_data[7:1]};

        //rx_flag:输入数据移位完成时rx_flag拉高一个时钟的高电平
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        rx_flag <= 1'b0;
        else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
        rx_flag <= 1'b1;
        else
        rx_flag <= 1'b0;

        //po_data:输出完整的8位有效数据
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        po_data <= 8'b0;
        else if(rx_flag == 1'b1)
        po_data <= rx_data;

        //po_flag:输出数据有效标志(比rx_flag延后一个时钟周期,为了和po_data同步)
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        po_flag <= 1'b0;
        else
        po_flag <= rx_flag;

        endmodule

        【串口输出模块(uart_tx)】
        module  uart_tx(
             input   wire            sys_clk     ,   //系统时钟50MHz
             input   wire            sys_rst_n   ,   //全局复位
             input   wire    [7:0]   pi_data     ,   //模块输入的8bit数据
             input   wire            pi_flag     ,   //并行数据有效标志信号
        
         output  reg             tx              //串转并后的1bit数据
        

        );

        //localparam define
        parameter UART_BPS = 'd9600 , //串口波特率
        CLK_FREQ = 'd50_000_000 ; //时钟频率
        localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;

        //reg define
        reg [12:0] baud_cnt;
        reg bit_flag;
        reg [3:0] bit_cnt ;
        reg work_en ;

        //work_en:接收数据工作使能信号
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        work_en <= 1'b0;
        else if(pi_flag == 1'b1)
        work_en <= 1'b1;
        else if((bit_flag == 1'b1) && (bit_cnt == 4'd9))
        work_en <= 1'b0;

        //baud_cnt:波特率计数器计数,从0计数到BAUD_CNT_MAX - 1
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        baud_cnt <= 13'b0;
        else if((baud_cnt == BAUD_CNT_MAX - 1) || (work_en == 1'b0))
        baud_cnt <= 13'b0;
        else if(work_en == 1'b1)
        baud_cnt <= baud_cnt + 1'b1;

        //bit_flag:当baud_cnt计数器计数到1时让bit_flag拉高一个时钟的高电平
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        bit_flag <= 1'b0;
        else if(baud_cnt == 13'd1)
        bit_flag <= 1'b1;
        else
        bit_flag <= 1'b0;

        //bit_cnt:数据位数个数计数,10个有效数据(含起始位和停止位)到来后计数器清零
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        bit_cnt <= 4'b0;
        else if((bit_flag == 1'b1) && (bit_cnt == 4'd9))
        bit_cnt <= 4'b0;
        else if((bit_flag == 1'b1) && (work_en == 1'b1))
        bit_cnt <= bit_cnt + 1'b1;

        //tx:输出数据在满足rs232协议(起始位为0,停止位为1)的情况下一位一位输出
        always@(posedge sys_clk or negedge sys_rst_n)
        if(sys_rst_n == 1'b0)
        tx <= 1'b1; //空闲状态时为高电平
        else if(bit_flag == 1'b1)
        case(bit_cnt)
        0 : tx <= 1'b0;
        1 : tx <= pi_data[0];
        2 : tx <= pi_data[1];
        3 : tx <= pi_data[2];
        4 : tx <= pi_data[3];
        5 : tx <= pi_data[4];
        6 : tx <= pi_data[5];
        7 : tx <= pi_data[6];
        8 : tx <= pi_data[7];
        9 : tx <= 1'b1;
        default : tx <= 1'b1;
        endcase

        endmodule

        【顶层模块(top_spi_flash_seq_wr)】

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

        module  top_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 define
        parameter UART_BPS = 14'd9600 , //比特率
        CLK_FREQ = 26'd50_000_000 ; //时钟频率

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

        //-------------uart_rx_inst-------------//
        uart_rx 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_tx_inst(
        .sys_clk (sys_clk ), //系统时钟50Mhz
        .sys_rst_n (sys_rst_n), //全局复位
        .pi_data (po_data ), //并行数据
        .pi_flag (po_flag ), //并行数据有效标志信号

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

        );

        endmodule

        【仿真】
        `timescale  1ns/1ns
        module  tb_top_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:/Desktop/Share_docs/1.FPGA/Project_Documents/quartus_files/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.CLK_FREQ = 500000;
        // defparam spi_flash_seq_wr_inst.uart_tx_inst.CLK_FREQ = 500000;

        defparam memory.mem_access.initfile = "initmemory.txt";

        //-------------spi_flash_seq_wr_inst-------------//
        top_spi_flash_seq_wr top_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

        【实验5 SPI_Flash 读数据 实验】