一、SPI Flash与QSPI FLASH
1、首先说说FLASH,不管是QSPI Flash还是SPI Flash说的实际上是一种闪存芯片,比如最常见的W25Q128(下图),真正不同的是SPI协议与QSPI协议罢了。可以看到下图的芯片,2、3、5、7这四个引脚是可以复用的,根据不同的通信协议变换功能。
W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64 等。
W25Q128 将 16M 的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为16 个扇区(Sector),每个扇区 4K 个字节。W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作(SRAM:静态随机存取存储器(Static Random-Access Memory,SRAM)是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。)。
W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为2.7~ 3.6V,W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。
![](https://i-blog.csdnimg.cn/blog_migrate/63eca8a44c16a81fbf7de29cfca0ba87.png)
SPI与FLASH的通信方式:
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
如下图所示,标椎的SPI接口一般使用四条信号线通信:
SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)
MISO: 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
MOSI: 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
SCLK:串行时钟信号,由主设备产生。
CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。
![](https://i-blog.csdnimg.cn/blog_migrate/d864b04b54e90df7fafe9e5e0b5dc3d2.png)
QSPI与FLASH的通信方式:
所谓QSPI是Queued SPI的简写,是Motorola公司推出的SPI接口的扩展,比SPI应用更加广泛。在SPI协议的基础上,Motorola公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即QSPI协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线)SPI Flash 存储介质。
该接口可以在以下三种模式下工作:
① 间接模式:使用 QSPI 寄存器执行全部操作
② 状态轮询模式:周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断)
③ 内存映射模式:外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器
采用双闪存模式时,将同时访问两个 Quad-SPI Flash,吞吐量和容量均可提高二倍。
![](https://i-blog.csdnimg.cn/blog_migrate/aa92dbbdf5be058b85095b81fb622bbb.png)
二、QSPI协议功能框图
![](https://i-blog.csdnimg.cn/blog_migrate/3f38b3febfbfb02b139c6e667cbd2a44.png)
QSPI 使用 6 个信号连接Flash,分别是四个数据线BK1_IO0~BK1_IO3,一个时钟输出CLK,一个片选输出(低电平有效)BK1_nCS,它们的作用介绍如下:
(1) BK1_nCS:片选输出(低电平有效),适用于 FLASH 1。如果 QSPI 始终在双闪存模式下工作,则其也可用于 FLASH 2从设备选择信号线。QSPI通讯以BK1_nCS线置低电平为开始信号,以BK1_nCS线被拉高作为结束信号。
(2) CLK:时钟输出,适用于两个存储器,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如STM32的QSPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。
(3) BK1_IO0:在双线 / 四线模式中为双向 IO,单线模式中为串行输出。
(4) BK1_IO1:在双线 / 四线模式中为双向 IO,单线模式中为串行输入。
(5) BK1_IO2:在四线模式中为双向 IO。
(6) BK1_IO3:在四线模式中为双向 IO。
二、QSPI初始化结构体详解
跟其它外设一样,STM32 HAL库提供了QSPI初始化结构体及初始化函数来配置SPI外设。初始化结构体及函数定义在库文件“stm32f7xx_hal_spi.h”及“stm32f7xx_hal _spi.c”中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就能对SPI外设运用自如了,见代码清单 241。
a、 QSPI_InitTypeDef初始化结构体
1 typedef struct {
2 uint32_t ClockPrescaler; //预分频因子
3 uint32_t FifoThreshold; //FIFO中的阈值
4 uint32_t SampleShifting; //采样移位
5 uint32_t FlashSize; //Flash大小
6 uint32_t ChipSelectHighTime; //片选高电平时间
7 uint32_t ClockMode; //时钟模式
8 uint32_t FlashID; //Flash ID
9 uint32_t DualFlash; //双闪存模式
10 } QSPI_InitTypeDef;
这些结构体成员说明如下,其中括号内的文字是对应参数在STM32 HAL库中定义的宏:
(1) ClockPrescaler
本成员设置预分频因子,对应寄存器QUADSPI_CR [31:24]即PRESCALER[7:0],取值范围是0—255,可以实现1—256级别的分频。仅可在 BUSY = 0 时修改该字段。
(2) FifoThreshold
本成员设置FIFO 阈值级别,对应寄存器QUADSPI_CR [12:8]即FTHRES[4:0],定义在间接模式下 FIFO 中将导致 FIFO 阈值标志(FTF,QUADSPI_SR[2])置 1 的字节数阈值。
(3) SampleShifting
本成员设置采样,对应寄存器QUADSPI_CR [4],默认情况下,QUADSPI 在 Flash 驱动数据后过半个 CLK 周期开始采集数据。使用该位,可考虑外部信号延迟,推迟数据采集。可以取值0:不发生移位;1:移位半个周期。在 DDR 模式下 (DDRM = 1),固件必须确保 SSHIFT = 0。
(4) FlashSize
本成员设置FLASH大小,对应寄存器QUADSPI_CCR [20:16]的FSIZE[4:0]位。定义外部存储器的大小,简介模式Flash容量最高可达4GB(32位寻址),但是在内存映射模式下限制为256MB,如果是双闪存则可以达到512MB。
(5) ChipSelectHighTime
本成员设置片选高电平时间,对应寄存器QUADSPI_CR [10:8]的CSHT[2:0]位,定义片选 (nCS) 在发送至 Flash 的命令之间必须保持高电平的最少 CLK 周期数。可以取值1~8个周期。
(6) ClockMode
本成员设置时钟模式,对应寄存器QUADSPI_CR [0]位,指示CLK在命令之间的电平,可以选模式0,1: nCS 为高电平(片选释放)时,CLK 必须保持低电平;或者模式3 ,1:nCS 为高电平(片选释放)时,CLK 必须保持高电平。
(7) FlashID
本成员用于选择Flash1或者Flash2,单闪存模式下选择需要访问的flash。
(8) DualFlash
本成员用于激活双闪存模式,0:禁止双闪存模式;1:使能双闪存模式。双闪存模式可以使系统吞吐量和容量扩大一倍。
b、QSPI_CommandTypeDe通信配置命令结构体
1 typedef struct {
2 uint32_t Instruction; //指令
3 uint32_t Address; //地址
4 uint32_t AlternateBytes; //交替字节
5 uint32_t AddressSize; //地址长度
6 uint32_t AlternateBytesSize; //交替字节长度
7 uint32_t DummyCycles; //空指令周期
8 uint32_t InstructionMode; //指令模式
9 uint32_t AddressMode; //地址模式
10 uint32_t AlternateByteMode; //交替字节模式
11 uint32_t DataMode; //数据模式
12 uint32_t NbData; //数据长度
13 uint32_t DdrMode; //双倍数据速率模式
14 uint32_t DdrHoldHalfCycle; //DDR保持周期
15 uint32_t SIOOMode; //仅发送指令一次模式
16 } QSPI_CommandTypeDef;
这些结构体成员说明如下,其中括号内的文字是对应参数在STM32 HAL库中定义的宏:
(1) Instruction
本成员设置通信指令,指定要发送到外部 SPI 设备的指令。仅可在 BUSY = 0 时修改该字段。
(2) Address
本成员指定要发送到外部 Flash 的地址,BUSY = 0 或 FMODE = 11(内存映射模式)时,将忽略写入该字段。在双闪存模式下,由于地址始终为偶地址,ADDRESS[0] 自动保持为“0”。
(3) AlternateBytes
本成员指定要在地址后立即发送到外部 SPI 设备的可选数据,仅可在 BUSY = 0 时修改该字段。
(4) AddressSize
本成员定义地址长度,可以是8位,16位,24位或者32位。
(5) AlternateBytesSize
本成员定义交替字节长度,可以是8位,16位,24位或者32位。
(6) DummyCycles
本成员定义空指令阶段的持续时间,在 SDR 和 DDR 模式下,它指定 CLK 周期数 (0-31)。
(7) InstructionMode
本成员定义指令阶段的操作模式,00:无指令;01:单线传输指令;10:双线传输指令;11:四线传输指令。
(8) AddressMode
本成员定义地址阶段的操作模式,00:无地址;01:单线传输地址;10:双线传输地址;11:四线传输地址。
(9) AlternateByteMode
本成员定义交替字节阶段的操作模式00:无交替字节;01:单线传输交替字节;10:双线传输交替字节;11:四线传输交替字节。
(10) DataMode
本成员定义数据阶段的操作模式,00:无数据;01:单线传输数据;10:双线传输数据;11:四线传输数据。该字段还定义空指令阶段的操作模式。
(11) NbData
本成员设置数据长度,在间接模式和状态轮询模式下待检索的数据数量(值 + 1)。对状态轮询模式应使用不大于 3 的值(表示 4 字节)。
(12) DdrMode
本成员为地址、交替字节和数据阶段设置 DDR 模式,0:禁止 DDR 模式;1:使能 DDR 模式。
(13) DdrHoldHalfCycle
本成员设置DDR 模式下数据输出延迟 1/4 个 QUADSPI 输出时钟周期,0:使用模拟延迟来延迟数据输出;1:数据输出延迟 1/4 个 QUADSPI 输出时钟周期。仅在 DDR 模式下激活。
(14) SIOOMode
本成员设置仅发送指令一次模式,。IMODE = 00 时,该位不起作用。0:在每个事务中发送指令;1:仅为第一条命令发送指令。
三、QSPI—读写串行FLASH实验
![](https://i-blog.csdnimg.cn/blog_migrate/eb618c423cb72944481306b47a0d22f6.png)
(1) 初始化通讯使用的目标引脚及端口时钟;
GPIO_InitTypeDef GPIO_InitStruct;
/* 使能 QSPI 及 GPIO 时钟 */
QSPI_FLASH_CLK_ENABLE();
QSPI_FLASH_CLK_GPIO_ENABLE();
QSPI_FLASH_BK1_IO0_CLK_ENABLE();
QSPI_FLASH_BK1_IO1_CLK_ENABLE();
QSPI_FLASH_BK1_IO2_CLK_ENABLE();
QSPI_FLASH_BK1_IO3_CLK_ENABLE();
QSPI_FLASH_CS_GPIO_CLK_ENABLE();
(2) 配置SPI外设的模式、地址、速率等参数并使能SPI外设;
/* QSPI_FLASH 模式配置 */
QSPIHandle.Instance = QUADSPI;
QSPIHandle.Init.ClockPrescaler = 2;
QSPIHandle.Init.FifoThreshold = 4;
QSPIHandle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;
QSPIHandle.Init.FlashSize = 23;
QSPIHandle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_8_CYCLE;
QSPIHandle.Init.ClockMode = QSPI_CLOCK_MODE_0;
HAL_QSPI_Init(&QSPIHandle);
(3)初始化QSPI接口;
uint8_t BSP_QSPI_Init(void)
{
QSPI_CommandTypeDef s_command;
uint8_t value = W25Q128FV_FSR_QE;
/* QSPI存储器复位 */
if (QSPI_ResetMemory() != QSPI_OK) {
return QSPI_NOT_SUPPORTED;
}
/* 使能写操作 */
if (QSPI_WriteEnable() != QSPI_OK) {
return QSPI_ERROR;
}
/* 设置四路使能的状态寄存器,使能四通道IO2和IO3引脚 */
s_command.InstructionMode = QSPI_INSTRUCTION_1_LINE;
s_command.Instruction = WRITE_STATUS_REG2_CMD;
s_command.AddressMode = QSPI_ADDRESS_NONE;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_1_LINE;
s_command.DummyCycles = 0;
s_command.NbData = 1;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* 配置命令 */
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){
return QSPI_ERROR;
}
/* 传输数据 */
if (HAL_QSPI_Transmit(&QSPIHandle, &value, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){
return QSPI_ERROR;
}
/* 自动轮询模式等待存储器就绪 */
if (QSPI_AutoPollingMemReady(W25Q128FV_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK) {
return QSPI_ERROR;
}
return QSPI_OK;
}
(4) 编写基本SPI按字节收发的函数;
/**接收函数
* @brief 从QSPI存储器中读取大量数据.
* @param pData: 指向要读取的数据的指针
* @param ReadAddr: 读取起始地址
* @param Size: 要读取的数据大小
* @retval QSPI存储器状态
*/
uint8_t BSP_QSPI_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)
{
QSPI_CommandTypeDef s_command;
/* 初始化读命令 */
s_command.InstructionMode = QSPI_INSTRUCTION_1_LINE;
s_command.Instruction = READ_CMD;
s_command.AddressMode = QSPI_ADDRESS_1_LINE;
s_command.AddressSize = QSPI_ADDRESS_24_BITS;
s_command.Address = ReadAddr;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_1_LINE;
s_command.DummyCycles = 0;
s_command.NbData = Size;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* 配置命令 */
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK){
return QSPI_ERROR;
}
/* 接收数据 */
if(HAL_QSPI_Receive(&QSPIHandle, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE)!= HAL_OK) {
return QSPI_ERROR;
}
return QSPI_OK;
}
发送函数
/**
* @brief 将大量数据写入QSPI存储器
* @param pData: 指向要写入数据的指针
* @param WriteAddr: 写起始地址
* @param Size: 要写入的数据大小
* @retval QSPI存储器状态
*/
uint8_t BSP_QSPI_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size)
{
QSPI_CommandTypeDef s_command;
uint32_t end_addr, current_size, current_addr;
/* 计算写入地址和页面末尾之间的大小 */
current_addr = 0;
while (current_addr <= WriteAddr) {
current_addr += W25Q128FV_PAGE_SIZE;
}
current_size = current_addr - WriteAddr;
/* 检查数据的大小是否小于页面中的剩余位置 */
if (current_size > Size) {
current_size = Size;
}
/* 初始化地址变量 */
current_addr = WriteAddr;
end_addr = WriteAddr + Size;
/* 初始化程序命令 */
s_command.InstructionMode = QSPI_INSTRUCTION_1_LINE;
s_command.Instruction = QUAD_INPUT_PAGE_PROG_CMD;
s_command.AddressMode = QSPI_ADDRESS_1_LINE;
s_command.AddressSize = QSPI_ADDRESS_24_BITS;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_4_LINES;
s_command.DummyCycles = 0;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* 逐页执行写入 */
do {
s_command.Address = current_addr;
s_command.NbData = current_size;
/* 启用写操作 */
if (QSPI_WriteEnable() != QSPI_OK) {
return QSPI_ERROR;
}
/* 配置命令 */
if(HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
return QSPI_ERROR;
}
/* 传输数据 */
if(HAL_QSPI_Transmit(&QSPIHandle, pData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
return QSPI_ERROR;
}
/* 配置自动轮询模式等待程序结束 */
if(QSPI_AutoPollingMemReady(HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != QSPI_OK) {
return QSPI_ERROR;
}
/* 更新下一页编程的地址和大小变量 */
current_addr += current_size;
pData += current_size;
current_size = ((current_addr + W25Q128FV_PAGE_SIZE) > end_addr) ? (end_addr-current_addr) : W25Q128FV_PAGE_SIZE;
} while (current_addr < end_addr);
return QSPI_OK;
}
(5) 编写对FLASH擦除及读写操作的的函数;
由于FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。
通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”,
![](https://i-blog.csdnimg.cn/blog_migrate/6ae8927fbf41014b8bc9f3239447258f.png)
扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。
/**
* @brief 擦除QSPI存储器的指定块
* @param BlockAddress: 需要擦除的块地址
* @retval QSPI存储器状态
*/
uint8_t BSP_QSPI_Erase_Block(uint32_t BlockAddress)
{
QSPI_CommandTypeDef s_command;
/* 初始化擦除命令 */
s_command.InstructionMode = QSPI_INSTRUCTION_1_LINE;
s_command.Instruction = SECTOR_ERASE_CMD;
s_command.AddressMode = QSPI_ADDRESS_1_LINE;
s_command.AddressSize = QSPI_ADDRESS_24_BITS;
s_command.Address = BlockAddress;
s_command.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
s_command.DataMode = QSPI_DATA_NONE;
s_command.DummyCycles = 0;
s_command.DdrMode = QSPI_DDR_MODE_DISABLE;
s_command.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
s_command.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
/* 启用写操作 */
if (QSPI_WriteEnable() != QSPI_OK) {
return QSPI_ERROR;
}
/* 发送命令 */
if(HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) {
return QSPI_ERROR;
}
/* 配置自动轮询模式等待擦除结束 */
if (QSPI_AutoPollingMemReady(W25Q128FV_SUBSECTOR_ERASE_MAX_TIME) != QSPI_OK) {
return QSPI_ERROR;
}
return QSPI_OK;
}