24. QSPI——读写外部FLASH

关于FLASH存储器,请参考“常用存储器介绍”章节,实验中FLASH芯片的具体参数,请参考其规格书《AT25SF321B》来了解或是参照《W25Q64》来了解。

我们在上一章 第23章 SPI——OLED屏幕显示 中已经学习过关于SPI协议的通讯实验,在本章我们将继续学习关于SPI协议的另一个分支——QSPI。

24.1. QSPI协议简介

QSPI是Queued SPI的简写,是Motorola公司推出的SPI接口的扩展,比SPI应用更加广泛。在SPI协议的基础上,Motorola公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即QSPI协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线) SPI Flash 存储介质。

该接口可以在以下三种模式下工作:

  1. 间接模式:使用 QSPI 寄存器执行全部操作

  2. 状态轮询模式:周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断)

  3. 内存映射模式:外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器

采用双闪存模式时,将同时访问两个 Quad-SPI Flash,吞吐量和容量均可提高二倍。

QSPI是Quad SPI的简写,表示6线SPI,是Motorola公司推出的SPI接口的扩展,比SPI应用更加广泛。

在SPI协议的基础上,Motorola公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即QSPI协议)。

使用该接口,用户可以一次性传输包含多达16个8位或16位数据的传输队列。一旦传输启动,直到传输结束,都不需要CPU干预,极大的提高了传输效率。该协议在ColdFire系列MCU得到广泛应用。

24.1.1. QSPI功能框图

QSPI是一个内存控制器,用于连接具有SPI兼容接口的串行ROM(非易失性存储器,如串行闪存、串行EEPROM或串行FeRAM)。

图 QUADSPI 功能框图

SFMSMD

传输模式控制寄存器

SFMSSC

芯片选择控制寄存器

SFMSKC

时钟控制寄存器

SFMSST

状态寄存器

SFMCOM

通信端口寄存器

SFMCMD

通信方式控制寄存器

SFMCST

通信状态寄存器

SFMSIC

指令码注册

SFMSAC

地址模式控制寄存器

SFMSDC

虚拟周期控制寄存器

SFMSPC

SPI协议控制寄存器

SFMPMD

端口控制寄存器

SFMCNT1

外部QSPI地址寄存器

注解

QSPI常用的寄存器有通信端口寄存器(SFMCOM)以及通信方式控制寄存器(SFMCMD)

24.1.2. QSPI引脚的定义

我们的开发板采用的是双闪存禁止的模式连接单片QSPI Flash。QSPI 使用 6 个信号连接Flash,分别是四个数据线QIO0~QIO3,一个时钟输出CLK,一个片选输出(低电平有效)QSSL,它们的作用介绍如下:

  1. QSSL:片选输出(低电平有效),适用于 FLASH 1。如果 QSPI 始终在双闪存模式下工作,则其也可用于 FLASH 2从设备选择信号线。QSPI通讯以QSSL线置低电平为开始信号,以QSSL线被拉高作为结束信号。

  2. CLK:时钟输出,适用于两个存储器,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率, 不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。

  3. QIO0:在双线 / 四线模式中为双向 IO,单线模式中为串行输出,适用于FLASH 1。

  4. QIO1:在双线 / 四线模式中为双向 IO,单线模式中为串行输入,适用于FLASH 1。

  5. QIO2:在四线模式中为双向 IO,适用于 FLASH 1。

  6. QIO3:在四线模式中为双向 IO,适用于 FLASH 1。

24.1.3. QSPI命令序列

QUADSPI 通过命令与 Flash 通信 每条命令包括指令、地址、交替字节、空指令和数据这五个阶段, 任一阶段均可跳过,但至少要包含指令、地址、交替字节或数据阶段之一。nCS 在每条指令开始前下降,在每条指令完成后再次上升。先看看QSPI四线模式下的读命令时序,见下图。

图 四线模式下的读命令时序

24.1.4. QSPI内存映射模式

外部QSPI设备空间映射到内部空间如图所示

外部总线的空间

图

串行闪存和控制寄存器在地址空间上的位置由配置中设置的区域的地址范围决定。 SPI空间具有32位地址宽度,用于引用串行闪存。当访问SPI空间进行读取时,将自动启动SPI总线周期,并返回从串行闪存中读取的数据。 SPI空间的地址宽度固定为32位。然而,SPI总线的地址宽度在地址模式控制寄存器(SFMSAC)寄存器的SFMAS[1:0]位中可选择为8、16、24或32位。如果选择8、16或24位作为SPI总线的地址宽度,则只有用于访问SPI空间的地址的较低部分被发布到串行闪存。

24.2. 控制FLASH的指令

对主机端(RA6M5)来说,只是它遵守最基本的QSPI通讯协议发送出的数据,但在设备端(FLASH芯片)把这些数据解释成不同的意义,所以才成为指令。 查看FLASH芯片的数据手册《AT25SF321B》,可了解各种它定义的各种指令的功能及指令格式,见表 23‑2。

表 23‑2 FLASH常用芯片指令表(摘自规格书《AT25SF321B》)

指令

第一字节(指令编码)

第二字节

第三字节

第四字节

第五字节

第六字节

第七-N字节

Write Enable

06h

Write Disable

04h

Read Status Register

05h

(S7–S0)

Write Status Register

01h

(S7–S0)

Read Data

03h

A23–A16

A15–A8

A7–A0

(D7–D0)

(Next byte)

continuous

Fast Read

0Bh

A23–A16

A15–A8

A7–A0

dummy

(D7–D0)

(Next Byte) continuous

Fast Read Dual Output

3Bh

A23–A16

A15–A8

A7–A0

dummy

I/O = (D6,D4,D2,D0) O = (D7,D5,D3,D1)

(one byte per 4 clocks, continuous)

Page Program

02h

A23–A16

A15–A8

A7–A0

D7–D0

Next byte

Up to 256 bytes

Block Erase(64KB)

D8h

A23–A16

A15–A8

A7–A0

Sector Erase(4KB)

20h

A23–A16

A15–A8

A7–A0

Chip Erase

C7h

Power-down

B9h

Release Power- down / Device ID

ABh

dummy

dummy

dummy

(ID7-ID0)

Manufacturer/ Device ID

90h

dummy

dummy

00h

(M7-M0)

(ID7-ID0)

JEDEC ID

9Fh

(M7-M0)

生产厂商

(ID15-ID8)

存储器类型

(ID7-ID0) 容量

该表中的第一列为指令名,第二列为指令编码,第三至第N列的具体内容根据指令的不同 而有不同的含义。其中带括号的字节参数,方向为FLASH向主机传输,即命令 响应,不带括号的则为主机向FLASH传输。表中“ A0~A23” 指FLASH芯片 内部存储器组织的地址;“ M0~M7” 为厂商号(MANUFACTURER ID);“ID0-ID15”为FLASH芯片的ID;“dummy”指该处可为任意数据;“ D0~D7” 为FLASH内部存储矩阵的内容。

在FLSAH芯片内部,存储有固定的厂商编号(M7-M0)和不同类型FLASH芯片独有的编号(ID15-ID0),见表 23‑3。

表 23‑3 FLASH数据手册的设备ID说明

FLASH型号

厂商号(M7-M0)

FLASH型号(ID15-ID0)

AT25SF321B

1F h

8701 h

W25Q32

EF h

4018 h

通过指令表中的读ID指令“JEDEC ID”可以获取这两个编号,该指令编码为“9F h”,其中“9F h”是指16进制数“9F” (相当于C语言中的0x9F)。紧跟指令编码的三个字节分别为FLASH芯片输出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。

此处我们以该指令为例,配合其指令时序图进行讲解,见下图。

图 FLASH读ID指令“JEDEC ID”的时序(摘自规格书《AT25SF321B》)

主机首先通过MOSI线向FLASH芯片发送第一个字节数据为“9Fh”,当FLASH芯片收到该数 据后,它会解读成主机向它发送了“JEDEC指令”,然后它就作出该命令的响应:通过MISO线把它的 厂商ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指令响应后可进行校验。常见的 应用是主机端通过读取设备ID来测试硬件是否连接正常,或用于识别设备。

对于FLASH芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多的数据。

实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么 通讯协议。如上一章的EEPROM使用的是I2C,本章的FLASH使用的是SPI。那么我们 就先根据它的通讯协议,选择好RA6M5的硬件模块,并进行相应的I2C或SPI模 块初始化。接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的 指令。如EEPROM中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而FLASH则定 义了更多的指令,有写指令、读指令、读ID指令等等。最后,我们根据这些指令的格式要求,使用 通讯协议向设备发送指令,达到控制设备的目标。

为了方便使用,我们把FLASH芯片的常用指令编码使用宏来封装起来, 后面需要发送指令编码的时候我们直接使用这些宏即可。

FLASH指令编码表
/*FLASH常用命令*/
#define WriteEnable                 0x06
#define WriteDisable                0x04
#define ReadStatusReg               0x05
#define WriteStatusReg              0x01
#define ReadData                    0x03
#define FastReadData                0x0B
#define FastReadDual                0x3B
#define PageProgram                 0x02
#define BlockErase                  0xD8
#define SectorErase                 0x20
#define ChipErase                   0xC7
#define PowerDown                   0xB9
#define ReleasePowerDown            0xAB
#define DeviceID                    0xAB
#define ManufactDeviceID            0x90
#define JedecDeviceID               0x9F

/*其它*/
#define sFLASH_ID                  0x1F8701
#define Dummy_Byte                 0xFF

24.3. 实验:读写外部Flash芯片

24.3.1. 硬件设计

野火启明6M5开发板的 QSPI FLASH 电路图如图所示:

图

野火启明4M2开发板的 QSPI FLASH 电路图如图所示:

图

野火启明2L1开发板的 SPI FLASH 电路图如图所示:

图

FLASH 芯片连接到 MCU 的引脚如下表所示。

连接的引脚

野火启明开发板

引脚连接

启明6M5(QSPI连接)

  • P305——QSPCLK(CLK)

  • P306——QSSL(CS)

  • P307——QIO0

  • P308——QIO1

  • P309——QIO2

  • P310——QIO3

启明4M2(QSPI连接)

  • P500——QSPCLK(CLK)

  • P501——QSSL(CS)

  • P502——QIO0

  • P503——QIO1

  • P504——QIO2

  • P505——QIO3

启明2L1(SPI连接)

  • P413——SSLA0(CS)

  • P412——RSPCKA(CLK)

  • P411——MOSIA

  • P410——MISOA

24.3.2. 软件设计

24.3.2.1. 新建工程

因为本章节的 QSPI Flash 相关实验例程需要用到板子上的串口功能,因此我们可以直接以前面的“19_UART_Receive_Send”工程为基础进行修改。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “QSPI_Flash”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “QSPI_Flash”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

注:对于野火启明2L1开发板,连接外部Flash使用的是普通SPI接口,工程文件夹可重命名为 “SPI_Flash”

工程新建好之后,在工程根目录的 “src” 文件夹下面新建 “qspi_flash” 或 “spi_flash” 文件夹, 再进入改文件夹里面新建源文件和头文件: “bsp_qspi_flash.c/.h”(启明6M5/启明4M2开发板) 或 “bsp_spi_flash.c/.h”(启明2L1开发板)。 工程文件结构如下。

文件结构
QSPI_Flash(启明6M5/启明4M2开发板)或 SPI_Flash(启明2L1开发板)
├─ ......
└─ src
   ├─ led
   │  ├─ bsp_led.c
   │  └─ bsp_led.h
   ├─ debug_uart
   │  ├─ bsp_debug_uart.c
   │  └─ bsp_debug_uart.h
   ├─ qspi_flash 或 spi_flash(启明2L1开发板)
   │  ├─ bsp_qspi_flash.c 或 bsp_spi_flash.c(启明2L1开发板)
   │  └─ bsp_qspi_flash.h 或 bsp_spi_flash.h(启明2L1开发板)
   └─ hal_entry.c

24.3.2.2. FSP配置

打开工程项目的 FSP 配置界面进行配置。 启明6M5/启明4M2开发板对比启明2L1开发板,由于前者使用QSPI外设连接Flash芯片,后者使用SPI外设连接Flash芯片, QSPI与SPI是两个不同的外设,因此它们的FSP配置方法有比较大的不同。

对于启明6M5/启明4M2开发板的 “QSPI_Flash” 工程:

打开工程项目的 FSP 配置界面之后,首先切到“Pins”页面,配置QSPI引脚。

启明6M5按照如下图所示配置:

图

启明4M2按照如下图所示配置:

图

接着在 FSP 配置界面里面依次点击 “Stacks” -> “New Stack” -> “Storage” -> “QSPI” 来添加QSPI模块。 如下图所示。

图

按照如下图所示对 QSPI 模块属性进行配置:

图

QSPI 模块的属性介绍如下:

QSPI 属性介绍

QSPI属性

描述

SPI Protocol

SPI协议。

Address Byte

地址的长度(字节)。

Read Mode

读取的模式。

Page Size Bytes

页写入长度(字节)。

Command Definitions

指令的定义。

QSPKCLK Divisor

CLK时钟设置

Minimum QSSL Deselect Cycles

QSSL保持周期

Pins

QSPI引脚配置

注解

当我们需要QSPI四根线进行四线快数读取数据的时候,我们只需要在Read Mode里选择 Fast Read Quad I/O 即可。

对于启明2L1开发板的 “SPI_Flash” 工程:

打开工程项目的 FSP 配置界面之后,首先切到“Pins”页面,配置SPI引脚。

启明2L1按照如下图所示配置:

图

接着在 FSP 配置界面里面依次点击 Stacks -> New Stack -> Connectivity -> SPI (r_spi) 来添加SPI模块。 如下图所示。

图

按照如下图所示对 SPI 模块属性进行配置:

图

SPI 模块的属性介绍如下:

SPI 属性介绍

SPI属性

描述

Name

模块实例名。设置为g_spi0_flash

Channel

通道。这里选择spi0

Receive Interrupt Priority

接收中断优先级

Transmit Buffer Empty Interrupt Priority

发送缓存区空中断优先级

Transfer Complete Interrupt Priority

发送完成中断优先级

Error Interrupt Priority

错误中断优先级

Operating Mode

操作模式。可选SPI主机或从机

Clock Phase

SPI时钟相位

Clock Polarity

SPI时钟极性

Mode Fault Error

模式错误检测。检测主从模式冲突

Bit Order

位时序。MSB或LSB

Callback

中断回调函数。设置为spi_flash_callback

SPI Mode

SPI 模式。设置为SPI Operation

Full or Transmit Only Mode

全双工或仅发送模式选择

Slave Select Polarity

从机选择引脚极性。一般是低电平有效

Select SSL(Slave Select)

从机选择信号

MOSI Idle State

总线空闲时 MOSI 电平

Parity Mode

极性模式

Byte Swapping

字节交换模式

Bitrate

比特率

Clock Delay

时钟延迟

SSL Negation Delay

SSL失效延迟

Next Access Delay

下一次访问延迟

配置完成之后可以按下快捷键“Ctrl + S”保存, 最后点右上角的 “Generate Project Content” 按钮,让软件自动生成配置代码即可。

接下来就可以为外部串行Flash编写操作代码了。 使用 QSPI 和使用 SPI 操作串行Flash其实是类似的,只是两者的通信接口不同而已。 下面以启明6M5开发板为例,对串行Flash芯片进行操作。启明4M2和启明2L1的代码读者可直接参考相应配套例程。

24.3.2.3. QSPI直接读写FLASH函数

当使用QSPI接口时,通过 R_QSPI_DirectWrite 和 R_QSPI_DirectRead 这两个函数,可以直接读写QSPI FLASH, 通过这种方式,用户需要写入FLASH芯片的控制指令进行相应操作。

R_QSPI_DirectWrite 的函数原型如下:

fsp_err_t R_QSPI_DirectWrite (spi_flash_ctrl_t  * p_ctrl,uint8_t const * const p_src,uint32_t const  bytes,bool const  read_after_write)

发送一个数组的数据,p_src需要发送的数组,bytes字节的长度,read_after_write是否发送数据的截止信号(意思是将QSSL拉高代表数据的截止),一般我们需要和R_QSPI_DirectRead进行组合发送数据。 在这个函数之后我们需要增加一定时间的延时,或者是通过中断来进行判断写入数据是否成功。

R_QSPI_DirectRead 的函数原型如下:

fsp_err_t R_QSPI_DirectRead (spi_flash_ctrl_t * p_ctrl, uint8_t * const p_dest, uint32_t const bytes)

接收一个数组的数据,p_dest需要接收到的数组,bytes需要接收数组的长度(字节)。在执行读取的函数命令之后我们需要增加一定时间的延时,或者是通过中断来进行判断读取数据是否成功。

在此之后,我们将使用R_QSPI_DirectWrite和R_QSPI_DirectRead的组合,来实现我们想要的一些功能。

24.3.2.4. 读取FLASH芯片ID

根据“JEDEC”指令的时序,我们把读取FLASH ID的过程编写成一个函数,见下

代码清单 读取FLASH芯片ID
/**
* @brief  读取FLASH ID
* @param  无
* @retval FLASH ID
*/
uint32_t QSPI_Flash_ReadID(void)
{
   unsigned char data[6] = {};
   uint32_t back;
   data[0] = JedecDeviceID;

   R_QSPI_DirectWrite(&g_qspi0_flash_ctrl, &data[0], 1, true);     //false: close the spi  true: go go go
   R_QSPI_DirectRead(&g_qspi0_flash_ctrl, &data[0], 3);

   /*把数据组合起来,作为函数的返回值*/
   back = (data[0] << 16) | (data[1] << 8) | (data[2]);

   return back;
}

这段代码利用FSP里的R_QSPI_DirectWrite函数发送JedecDeviceID指令,然后通过R_QSPI_DirectRead函数读取三个字节的函数,最后把读取到的这3个数据合并到一个变量(back)中,然后作为函数返回值,把该返回值与我们预先定义的ID进行对比,即可知道FLASH芯片是否正常。

24.3.2.5. FLASH写使能

在向FLASH芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能,见下。

代码清单 写使能命令
/**
* @brief  向FLASH发送 写使能 命令
* @param  none
* @retval none
*/
void QSPI_Flash_WriteEnable(void)
{
   unsigned char data[6] = {};
   data[0] = WriteEnable;
   R_QSPI_DirectWrite(&g_qspi0_flash_ctrl, &data[0], 1, false);
}

24.3.2.6. 读取当前FLASH状态

与EEPROM一样,由于FLASH芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的, 所以在写操作后需要确认FLASH芯片“空闲”时才能进行再次写入。为了表示自己的工作状态, FLASH芯片定义了一个状态寄存器,如下图所示。

图 FLASH芯片的状态寄存器

我们只关注这个状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。

利用指令表中的“Read Status Register”指令可以获取FLASH芯片状态寄存器的内容,其时序见下图。

图 读取状态寄存器的时序

只要向FLASH芯片发送了读状态寄存器的指令,FLASH芯片就会持续向主机返回最新的状态寄存器内容, 直到收到SPI通讯的停止信号。据此我们编写了具有等待FLASH芯片写入结束功能的函数,见下。

代码清单 通过读状态寄存器等待FLASH芯片空闲
/**
* @brief  等待WIP(BUSY)标志被置0,即等待FLASH内部数据写入完毕
* @param  无
*/
fsp_err_t QSPI_Flash_WaitForWriteEnd(void)
{
   spi_flash_status_t status = {.write_in_progress = true};
   int32_t time_out          = (INT32_MAX);
   fsp_err_t err             = FSP_SUCCESS;

   do
   {
      /* Get status from QSPI flash device */
      err = R_QSPI_StatusGet(&g_qspi0_flash_ctrl, &status);
      if (FSP_SUCCESS != err)
      {
            printf("R_QSPI_StatusGet Failed\r\n");
            return err;
      }

      /* Decrement time out to avoid infinite loop in case of consistent failure */
      --time_out;

      if (RESET_VALUE >= time_out)
      {
            printf("\r\n ** Timeout : No result from QSPI flash status register ** \r\n");
            return FSP_ERR_TIMEOUT;
      }

   }
   while (false != status.write_in_progress);

   return err;
}

这段代码发送R_QSPI_StatusGet函数获取当前的芯片是否在写入状态,并在while循环里持续获取寄存器的内容并检验它的标志位,一直等待到该标志表示写入结束时才退出本函数,以便继续后面与FLASH芯片的数据通讯。

24.3.2.7. FLASH扇区擦除

由于FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。

通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”,见下表。

本实验FLASH芯片的擦除单位

擦除单位

大小

指令

扇区擦除Sector Erase

4KB

20h

块擦除Block Erase

64KB

D8h

整片擦除Chip Erase

整个芯片完全擦除

60h

FLASH芯片的最小擦除单位为扇区(Sector),而一个块(Block)包含16个扇区,其内部存储矩阵分布见下图。

图 FLASH芯片的存储矩阵

使用扇区擦除指令“Sector Erase”可控制FLASH芯片开始擦写,其指令时序见下图。

图 扇区擦除时序

扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的存储矩阵地址。 要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后, 通过读取寄存器状态等待扇区擦除操作完毕,代码实现见下。

代码清单 擦除扇区
/**
* @brief  擦除FLASH扇区
* @param  SectorAddr:要擦除的扇区地址
* @retval 无
*/
void QSPI_Flash_SectorErase(uint32_t adress)
{
   unsigned char data[6] = {};

   data[0] = 0x06;     //write_enable_command
   data[1] = 0x20;     //erase_command
   data[2] = (uint8_t)(adress >> 16);
   data[3] = (uint8_t)(adress >> 8);
   data[4] = (uint8_t)(adress);
   R_QSPI->SFMCMD = 1U;
   R_QSPI->SFMCOM = data[0];
   R_QSPI_DirectWrite(&g_qspi0_flash_ctrl, &data[1], 4, false);

   QSPI_Flash_WaitForWriteEnd();
}

这段代码使用瑞萨FSP调用R_QSPI_DirectWrite()发送四个字节的数据, 先发送写使能0x06,之后通过R_QSPI_DirectWrite()发送扇区的删除命令0x02,以及三个字节的地址。调用扇区擦除指令时注意输入的地址要对齐到4KB。

24.3.2.8. FLASH的页写入

目标扇区被擦除完毕后,就可以向它写入数据了。与EEPROM类似,FLASH芯片也有页写入命令, 使用页写入命令最多可以一次向FLASH传输256个字节的数据,我们把这个单位为页大小。 FLASH页写入的时序见下图。

图 FLASH芯片页写入

从时序图可知,第1个字节为“页写入指令”编码,2-4字节为要写入的“地址Address”,接着的是要写入的内容,最多可以发送256字节数据,这些数据将会从“地址Address”开始,按顺序写入到FLASH的存储矩阵。若发送的数据超出256个,则会覆盖前面发送的数据。

与擦除指令不一样,页写入指令的地址并不要求按256字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址x”执行页写入指令后,发送了200个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入200个字节也是没有问题的(小于256均可)。 只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写,想深入了解,可学习我们的“FLASH文件系统”相关的例子。

把页写入时序封装成函数,其实现见下。

代码清单 FLASH的页写入
/**
* @brief  对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
* @param  pBuffer,要写入数据的指针
* @param  WriteAddr,写入地址
* @param  NumByteToWrite,写入数据长度,必须小于等于页大小
* @retval 无
*/
void QSPI_Flash_PageWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
   R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, NumByteToWrite);
   QSPI_Flash_WaitForWriteEnd();
}


static void qspi_d0_byte_write_standard(uint8_t byte)
{
   R_QSPI->SFMCOM = byte;
}

/**
* @brief  读取flash数据
* @param  p_ctrl
* @param  p_src     需要传回的数据
* @param  p_dest    数据地址
* @param  byte_count    数据长度
*/
fsp_err_t R_QSPI_Read(spi_flash_ctrl_t     *p_ctrl,
                     uint8_t              *p_src,
                     uint8_t *const       p_dest,
                     uint32_t              byte_count)
{
   qspi_instance_ctrl_t *p_instance_ctrl = (qspi_instance_ctrl_t *) p_ctrl;


   uint32_t chip_address = (uint32_t) p_dest - (uint32_t) QSPI_DEVICE_START_ADDRESS + R_QSPI->SFMCNT1;

   bool restore_spi_mode = false;
   void (* write_command)(uint8_t byte) = qspi_d0_byte_write_standard;
   void (* write_address)(uint8_t byte) = qspi_d0_byte_write_standard;

#if QSPI_CFG_SUPPORT_EXTENDED_SPI_MULTI_LINE_PROGRAM

   /* If the peripheral is in extended SPI mode, and the configuration provided in the BSP allows for programming on
   * multiple data lines, and a unique command is provided for the required mode, update the SPI protocol to send
   * data on multiple lines. */
   if ((SPI_FLASH_DATA_LINES_1 != p_instance_ctrl->data_lines) &&
            (SPI_FLASH_PROTOCOL_EXTENDED_SPI == R_QSPI->SFMSPC_b.SFMSPI))
   {
      R_QSPI->SFMSPC_b.SFMSPI = p_instance_ctrl->data_lines;

      restore_spi_mode = true;

      /* Write command in extended SPI mode on one line. */
      write_command = gp_qspi_prv_byte_write[p_instance_ctrl->data_lines];

      if (SPI_FLASH_DATA_LINES_1 == p_instance_ctrl->p_cfg->page_program_address_lines)
      {
            /* Write address in extended SPI mode on one line. */
            write_address = gp_qspi_prv_byte_write[p_instance_ctrl->data_lines];
      }
   }
#endif

   /* Enter Direct Communication mode */
   R_QSPI->SFMCMD = 1;

   /* Send command to enable writing */
   write_command(0x03);

   /* Write the address. */
   if ((p_instance_ctrl->p_cfg->address_bytes & R_QSPI_SFMSAC_SFMAS_Msk) == SPI_FLASH_ADDRESS_BYTES_4)
   {
      /* Send the most significant byte of the address */
      write_address((uint8_t)(chip_address >> 24));
   }

   /* Send the remaining bytes of the address */
   write_address((uint8_t)(chip_address >> 16));
   write_address((uint8_t)(chip_address >> 8));
   write_address((uint8_t)(chip_address));

   /* Write the data. */
   uint32_t index = 0;
   while (index < byte_count)
   {
      /* Read the device memory into the passed in buffer */
      *(p_src + index) = (uint8_t) R_QSPI->SFMCOM;
      index++;
   }

   /* Close the SPI bus cycle. Reference section 39.10.3 "Generating the SPI Bus Cycle during Direct Communication"
   * in the RA6M3 manual R01UH0886EJ0100. */
   R_QSPI->SFMCMD = 1;


   /* Return to ROM access mode */
   R_QSPI->SFMCMD = 0;

   return FSP_SUCCESS;
}

这段代码使用瑞萨FSP调用R_QSPI_Write()函数进行进行页写入, 先发送“写使能”命令,接着才开始页写入时序,然后发送指令编码、地址, 再把要写入的数据一个接一个地发送出去,发送完后结束通讯,通过get_flash_status()函数来 检查FLASH状态寄存器, 等待FLASH内部写入结束。

24.3.2.9. 不定量数据写入

应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便,所以我们在它的基础上编写了“不定量数据写入”的函数, 基实现见下。

代码清单 不定量数据写入
/**
* @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
* @param  pBuffer,要写入数据的指针
* @param  WriteAddr,写入地址
* @param  NumByteToWrite,写入数据长度
* @retval 无
*/
void QSPI_Flash_BufferWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
   uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;

   /*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
   Addr = WriteAddr % SPI_FLASH_PageSize;

   /*差count个数据值,刚好可以对齐到页地址*/
   count = SPI_FLASH_PageSize - Addr;
   /*计算出要写多少整数页*/
   NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
   /*mod运算求余,计算出剩余不满一页的字节数*/
   NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

   /* Addr=0,则WriteAddr 刚好按页对齐 aligned  */
   if (Addr == 0)
   {
      /* NumByteToWrite < SPI_FLASH_PageSize */
      if (NumOfPage == 0)
      {
            R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, NumByteToWrite);
            QSPI_Flash_WaitForWriteEnd();

      }
      else /* NumByteToWrite > SPI_FLASH_PageSize */
      {
            /*先把整数页都写了*/
            while (NumOfPage--)
            {
               R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, SPI_FLASH_PageSize);
               QSPI_Flash_WaitForWriteEnd();

               WriteAddr +=  SPI_FLASH_PageSize;
               pBuffer += SPI_FLASH_PageSize;
            }
            /*若有多余的不满一页的数据,把它写完*/
            R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, NumOfSingle);
            QSPI_Flash_WaitForWriteEnd();

      }
   }
   /* 若地址与 SPI_FLASH_PageSize 不对齐  */
   else
   {
      /* NumByteToWrite < SPI_FLASH_PageSize */
      if (NumOfPage == 0)
      {
            /*当前页剩余的count个位置比NumOfSingle小,一页写不完*/
            if (NumOfSingle > count)
            {
               temp = NumOfSingle - count;
               /*先写满当前页*/
               R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, count);
               QSPI_Flash_WaitForWriteEnd();


               WriteAddr +=  count;
               pBuffer += count;
               /*再写剩余的数据*/
               R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, temp);
               QSPI_Flash_WaitForWriteEnd();

            }
            else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
            {
               R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, NumByteToWrite);
               QSPI_Flash_WaitForWriteEnd();

            }
      }
      else /* NumByteToWrite > SPI_FLASH_PageSize */
      {
            /*地址不对齐多出的count分开处理,不加入这个运算*/
            NumByteToWrite -= count;
            NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
            NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

            /* 先写完count个数据,为的是让下一次要写的地址对齐 */
            R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, count);
            QSPI_Flash_WaitForWriteEnd();

            /* 接下来就重复地址对齐的情况 */
            WriteAddr +=  count;
            pBuffer += count;
            /*把整数页都写了*/
            while (NumOfPage--)
            {
               R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, SPI_FLASH_PageSize);
               QSPI_Flash_WaitForWriteEnd();

               WriteAddr +=  SPI_FLASH_PageSize;
               pBuffer += SPI_FLASH_PageSize;
            }
            /*若有多余的不满一页的数据,把它写完*/
            if (NumOfSingle != 0)
            {
               R_QSPI_Write(&g_qspi0_flash_ctrl, pBuffer, WriteAddr, NumOfSingle);
               QSPI_Flash_WaitForWriteEnd();

            }
      }
   }
}

这段代码与EEPROM章节中的“快速写入多字节”函数原理是一样的,运算过程在此不再赘述。区别是页的大小以及实际数据写入的时候,使用的是针对FLASH芯片的页写入函数,且在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态。

24.3.2.10. 从FLASH读取数据

相对于写入,FLASH芯片的数据读取要简单得多,使用读取指令“Read Data”即可,其指令时序见下图。

图 SPI FLASH读取数据时序

发送了指令编码及要读的起始地址后,FLASH芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制, 只要没有停止通讯,FLASH芯片就会一直返回数据。代码实现见下。

代码清单 从FLASH读取数据
/**
* @brief  读取FLASH数据,减少ctrl这个标志
* @param  pBuffer,存储读出数据的指针
* @param  ReadAddr,读取地址
* @param  NumByteToRead,读取数据长度
* @retval 无
*/
void QSPI_Flash_BufferRead(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
   R_QSPI_Read(&g_qspi0_flash_ctrl, pBuffer, ReadAddr, NumByteToRead);
}

/**
* @brief  读取flash数据
* @param  p_ctrl
* @param  p_src     需要传回的数据
* @param  p_dest    数据地址
* @param  byte_count    数据长度
*/
fsp_err_t R_QSPI_Read(spi_flash_ctrl_t     *p_ctrl,
                     uint8_t              *p_src,
                     uint8_t *const       p_dest,
                     uint32_t              byte_count)
{
   qspi_instance_ctrl_t *p_instance_ctrl = (qspi_instance_ctrl_t *) p_ctrl;


   uint32_t chip_address = (uint32_t) p_dest - (uint32_t) QSPI_DEVICE_START_ADDRESS + R_QSPI->SFMCNT1;

   bool restore_spi_mode = false;
   void (* write_command)(uint8_t byte) = qspi_d0_byte_write_standard;
   void (* write_address)(uint8_t byte) = qspi_d0_byte_write_standard;

#if QSPI_CFG_SUPPORT_EXTENDED_SPI_MULTI_LINE_PROGRAM

   /* If the peripheral is in extended SPI mode, and the configuration provided in the BSP allows for programming on
   * multiple data lines, and a unique command is provided for the required mode, update the SPI protocol to send
   * data on multiple lines. */
   if ((SPI_FLASH_DATA_LINES_1 != p_instance_ctrl->data_lines) &&
            (SPI_FLASH_PROTOCOL_EXTENDED_SPI == R_QSPI->SFMSPC_b.SFMSPI))
   {
      R_QSPI->SFMSPC_b.SFMSPI = p_instance_ctrl->data_lines;

      restore_spi_mode = true;

      /* Write command in extended SPI mode on one line. */
      write_command = gp_qspi_prv_byte_write[p_instance_ctrl->data_lines];

      if (SPI_FLASH_DATA_LINES_1 == p_instance_ctrl->p_cfg->page_program_address_lines)
      {
            /* Write address in extended SPI mode on one line. */
            write_address = gp_qspi_prv_byte_write[p_instance_ctrl->data_lines];
      }
   }
#endif

   /* Enter Direct Communication mode */
   R_QSPI->SFMCMD = 1;

   /* Send command to enable writing */
   write_command(0x03);

   /* Write the address. */
   if ((p_instance_ctrl->p_cfg->address_bytes & R_QSPI_SFMSAC_SFMAS_Msk) == SPI_FLASH_ADDRESS_BYTES_4)
   {
      /* Send the most significant byte of the address */
      write_address((uint8_t)(chip_address >> 24));
   }

   /* Send the remaining bytes of the address */
   write_address((uint8_t)(chip_address >> 16));
   write_address((uint8_t)(chip_address >> 8));
   write_address((uint8_t)(chip_address));

   /* Write the data. */
   uint32_t index = 0;
   while (index < byte_count)
   {
      /* Read the device memory into the passed in buffer */
      *(p_src + index) = (uint8_t) R_QSPI->SFMCOM;
      index++;
   }

   /* Close the SPI bus cycle. Reference section 39.10.3 "Generating the SPI Bus Cycle during Direct Communication"
   * in the RA6M3 manual R01UH0886EJ0100. */
   R_QSPI->SFMCMD = 1;


   /* Return to ROM access mode */
   R_QSPI->SFMCMD = 0;

   return FSP_SUCCESS;
}

由于读取的数据量没有限制,所以发送读命令后一直接收NumByteToRead个数据到结束即可。

24.3.2.11. hal_entry入口函数

最后我们来编写 hal_entry 入口函数,进行FLASH芯片读写校验,代码见下。

代码清单 hal_entry 入口函数
/* 用户头文件包含 */
#include "led/bsp_led.h"
#include "debug_uart/bsp_debug_uart.h"
#include "qspi_flash/bsp_qspi_flash.h"


#define  FLASH_WriteAddress     0x00000
#define  FLASH_ReadAddress      FLASH_WriteAddress
#define  FLASH_SectorToErase    FLASH_WriteAddress

/* 发送缓冲区初始化 */
uint8_t Tx_Buffer[] = "感谢您选用野火启明瑞萨RA开发板";
uint8_t Rx_Buffer[sizeof(Tx_Buffer)];


/*
* 函数名:Buffercmp
* 描述  :比较两个缓冲区中的数据是否相等
* 输入  :pBuffer1     src缓冲区指针
*         pBuffer2     dst缓冲区指针
*         BufferLength 缓冲区长度
* 输出  :无
* 返回  :0 pBuffer1 等于   pBuffer2
*         1 pBuffer1 不等于 pBuffer2
*/
int Buffercmp(uint8_t *pBuffer1, uint8_t *pBuffer2, uint16_t BufferLength)
{
   while (BufferLength--)
   {
      if (*pBuffer1 != *pBuffer2)
      {
            return 1;
      }

      pBuffer1++;
      pBuffer2++;
   }
   return 0;
}


void hal_entry(void)
{
   /* TODO: add your own code here */
   uint32_t FlashID = 0;
   uint32_t FlashDeviceID = 0;

   LED_Init();         // LED 初始化
   Debug_UART4_Init(); // SCI4 UART 调试串口初始化
   QSPI_Flash_Init();  // 串行FLASH初始化

   printf("这是一个串行FLASH的读写例程\r\n");
   printf("打开串口助手查看打印的信息\r\n\r\n");


   /* 获取 SPI g_qspi0_flash ID */
   FlashID = QSPI_Flash_ReadID();
   FlashDeviceID = QSPI_Flash_ReadDeviceID();

   if ((FlashID == FLASH_ID_W25Q32JV) || (FlashID == FLASH_ID_AT25SF321B))
   {
      if(FlashID == FLASH_ID_W25Q32JV)
      {
            printf("检测到串行FLASH:W25Q32 !\r\n");
      }
      else
      {
            printf("检测到串行FLASH:AT25SF32 !\r\n");
      }
      printf("FlashID is 0x%X, Manufacturer Device ID is 0x%X.\r\n", FlashID, FlashDeviceID);


      /* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
      // 这里擦除4K,即一个扇区,擦除的最小单位是扇区
      QSPI_Flash_SectorErase(FLASH_SectorToErase);

      /* 将发送缓冲区的数据写到flash中 */
      // 这里写一页,一页的大小为256个字节
      QSPI_Flash_BufferWrite(Tx_Buffer, FLASH_WriteAddress, sizeof(Tx_Buffer));
      printf("写入的数据为:%s \r\n", Tx_Buffer);

      /* 将刚刚写入的数据读出来放到接收缓冲区中 */
      QSPI_Flash_BufferRead(Rx_Buffer, FLASH_ReadAddress, sizeof(Tx_Buffer));
      printf("读出的数据为:%s \r\n", Rx_Buffer);

      if (Buffercmp(Tx_Buffer, Rx_Buffer, sizeof(Tx_Buffer)) == 0)
      {
            printf("\r\n32Mbit串行Flash测试成功!\r\n");
            LED3_ON;
      }
      else
      {
            printf("\r\n32Mbit串行Flash测试失败!\r\n");
            LED1_ON;
      }


      printf("\r\n测试存储浮点数和整数示例\r\n");
      /* 存储小数和整数的数组,各7个 */
      long double double_buffer[7] = {0};
      int int_buffer[7] = {0};

      /*生成要写入的数据*/
      for (uint8_t k = 0; k < 7; k++)
      {
            double_buffer[k] = k + 0.1;
            int_buffer[k] = k * 500 + 1 ;
      }
      printf("向芯片写入数据:");
      /*打印到串口*/
      printf("\r\n小数 tx = ");
      for (uint8_t k = 0; k < 7; k++)
      {
            printf("%LF, ", double_buffer[k]);
      }
      printf("\r\n整数 tx = ");
      for (uint8_t k = 0; k < 7; k++)
      {
            printf("%d, ", int_buffer[k]);
      }

      /* 前面已擦除整个扇区和写入第0页,现继续写入第1页和第2页 */
      /*写入小数数据到第一页*/
      QSPI_Flash_BufferWrite((void *)double_buffer, SPI_FLASH_PageSize * 1, sizeof(double_buffer));
      /*写入整数数据到第二页*/
      QSPI_Flash_BufferWrite((void *)int_buffer, SPI_FLASH_PageSize * 2, sizeof(int_buffer));

      /*读取小数数据*/
      QSPI_Flash_BufferRead((void *)double_buffer, SPI_FLASH_PageSize * 1, sizeof(double_buffer));
      /*读取整数数据*/
      QSPI_Flash_BufferRead((void *)int_buffer, SPI_FLASH_PageSize * 2, sizeof(int_buffer));


      printf("\r\n\r\n从芯片读到数据:");
      printf("\r\n小数 rx = ");
      for (uint8_t k = 0; k < 7; k++)
      {
            printf("%LF, ", double_buffer[k]);
      }
      printf("\r\n整数 rx = ");
      for (uint8_t k = 0; k < 7; k++)
      {
            printf("%d, ", int_buffer[k]);
      }
   }
   else
   {
      printf("\tFLASH_ID 错误:0x%X", FlashID);
      LED1_ON;
   }


   while(1);


#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

24.3.3. 下载验证

用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到FLASH测试的调试信息。

图