在有一些应用中,我们可能需要大一些容量的存储单元,而实现的形式多种多样,在这一篇中我们将来讨论怎么使用BY25QXXX系列NOR FLASH存储器的问题。
1、功能概述
在开始实现BY25QXXX系列NOR FLASH存储器的驱动之前,我们需要先了解一下它的基本情况。
1.1、QSPI接口
QSPI接口,是QueuedSPI的缩写。和之前谈到的SPI一样都是出自Motorola。QSPI在SPI基础上做了一些增强,且向下兼容SPI。QSPI相对SPI最显著的差异就是增加了发送接收数据队列,Queued的称呼就是这么来的。这样做的好处就是,无需每次数据传输都需要CPU参与,可以降低CPU的资源占用。
QSPI采用6先模式,同样也可以按Standard SPI、Dual SPI方式工作。我们引用STM32H7上一张QSPI接口与Flash的连接图来展示其连线方式。
QSPI接口可以在以下三种模式下工作:间接模式,使用 QSPI 寄存器执行全部操作;状态轮询模式,周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断);内存映射模式,外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器。我们在这里考虑BY25QXXX系列NOR FLASH存储器的驱动问题其实就是以间接模式访问的情况。
#1.2、BY25Q基本特点
BY25QXXX系列NOR FLASH存储器支持标准SPI模式、双线SPI模式、四线SPI模式。其封装级引脚定义如下:
这些引脚中,片选信号CS和始终信号SCLK在各种模式下是没有区别的。而SO(IO1)引脚在标准SPI模式下用作串行输出,在双线模式和四线模式下则是IO1。SI(IO0)引脚在标准SPI模式下用作串行输入,在双线模式和四线模式下则是IO0。WP(IO2)引脚在标准和双线模式下为写保护,在四线模式下为IO2。HOLD(IO3)引脚在标准和双线模式下为HOLD,在四线模式下为IO3。
#1.3、操作指令
BY25QXXX系列NOR FLASH存储器在间接访问模式下,主要有四类指令:配置与状态指令、读指令、ID和安全指令、编程和擦除指令。。
配置与状态指令,用于配置操作方式及获取工作状态,主要包括使能及状态操作,具体指令如下所示:
读指令,用于读取数据。读取数据支持在标准模式下、双线模式下、四线模式下进行操作,具体的指令如下所示:
ID和安全指令,用于读取或配置一些特定操作,如获取制造商编号以及设备编号等,具体的指令如下所示:
编程和擦除指令,用以实现对扇区、块以及整片的擦除以及指定的区域的编程等功能,具体的指令如下所示:
对于BY25QXXX系列NOR FLASH存储器,不管是读写操作还是其它操作在指令阶段都是标准的SPI操作方式。
2、驱动设计与实现
我们已经大致了解了BY25QXXX系列NOR FLASH存储器操作方式及指令,接下来我们就来考虑实现以间接模式访问它的驱动问题。
2.1、对象定义
我们依旧是基于对象的模式来考虑这一问题,所以我们首先需要定义BY25QXXX系列NOR FLASH存储器的对象类型。我们先来分析一下,作为对象BY25QXXX系列NOR FLASH存储器都有哪些必要的属性和操作。
先说一说属性问题,对于BY25QXXX系列NOR FLASH存储器对象来说可以标识器身份和状态的无非是ID和状态寄存器,而ID有包括制造商ID、设备ID、JEDEC ID和uniqueID等,我们可以将其作为对象的属性以标识不同的对象,当这些属性并不是必须的。
再来看一看操作问题,对于BY25QXXX系列NOR FLASH存储器,它的操作指令有很多,但我们通过分析他们的时序不难发现所有的指令都可归纳为:命令发送、数据发送、数据接收等内容。不同的指令包括不同的组合,所以我们只需要将命令发送、数据发送、数据接收作为对象的操作,通过组合就可以实现全部的操作指令。还有一点需要考虑的是,在写 数据或者擦除是需要等待是否完成,所以我们额外添加一个就绪检测操作。通过上述分析我们可以定义BY25QXXX系列NOR FLASH存储器对象类型如下:
/*定义BY25QXX对象类型 */
typedef struct BY25QObject{
uint8_t status[3];
uint8_t mfrID[2];
uint8_t jedecID[3];
uint8_t uniqueID[8];
void (*Write)(BY25QCommandConfigType config,uint8_t *wDatas); //写数据操作指针
void (*Read)(BY25QCommandConfigType config,uint8_t *rDatas); //读数据操作指针
void (*Command)(BY25QCommandConfigType config); //下发无数据操作命令
void (*Ready)(void); //检查Flash是否处于BUSY
}BY25QObjectType;
定义了对象类型后,我们便可以基于它得到对象变量,但对象变量必须实例化才可使用,我们我们来考虑BY25QXXX系列NOR FLASH存储器对象的初始化问题。
/*实现BY25Q初始化配置*/
void BY25QInitialization(BY25QObjectType *by250q, /*BY250Q存储器对象*/
BY25QWriteType write, /*写函数指针*/
BY25QReadType read, /*读函数指针*/
BY25QCommandType command, /*命令下发函数指针*/
BY25QReadyType ready /*就绪检测函数指针*/
)
{
if((by250q==NULL)||(write==NULL)||(read==NULL)||(command==NULL)||(ready==NULL))
{
return;
}
by250q->Write=write;
by250q->Read=read;
by250q->Command=command;
by250q->Ready=ready;
GetBy25qxxID(by250q);
ReadStatusRegister(by250q);
}
在这一初始化函数中,我们主要是配置了用于读写操作的函数指针,并读取了设备的各类ID以及状态寄存器的值。如果有其他需要在初始化是完成的工作也可以在此函数中实现。
2.2、对象操作
我们得到了对象类型,而且也可以为对象变量实现初始化配置。接下来我们看一看对象需要实现哪些操作。由于BY25QXXX系列NOR FLASH存储器的操作指令有很多,我们这里只是先几个必要的操作函数。
2.2.1、写使能
写使能操作需要在写入数据之前完成,不仅是写存储区域是需要操作,在写寄存器之前也需要先进行此操作。该操作只有一个0x06指令写入,存储器就会自己完成相应操作,并反应到状态寄存器上。其操作时序图如下:
此操作只占用标准SPI接口,事实上全部的命令都是如此。根据前述的描述及时序图,我们可以编写“写使能”的操作函数如下:
/* 写使能 */
static void WriteEnable(BY25QObjectType *by250q)
{
BY25QCommandConfigType config;
config.Instruction=WRITE_ENABLE; // 写使能指令0x06
config.DummyCycles=0; // 空指令周期数
config.AddressMode=0;
config.Address=0;
config.DataMode=0;
config.NbData=0;
by250q->Command(config);
}
2.2.2、读取数据
从BY25QXXX系列NOR FLASH存储器读取数据是必不可少的操作,而且有多个操作指令,这里我们实现Quad快速读指令。读取数据时,发送指令和地址都使用单线操作,获取数据则使用四线操作。其时序图如下:
根据前述的描述及时序图,我们可以编写“读取数据”的操作函数如下:
/*读取数据*/
static void QuadFastRead(BY25QObjectType *by250q,uint32_t readAddress,uint8_t *readBuffer,uint32_t readSize)
{
BY25QCommandConfigType config;
config.Instruction=0xEB; // 读ID指令0xEB
config.DummyCycles=6; // 空指令周期数
config.AddressMode=3;
config.Address=readAddress;
config.DataMode=3;
config.NbData=readSize;
by250q->Read(config,readBuffer);
}
2.2.3、擦除数据
擦除和编程就其本质是一样的。对于BY25QXXX系列NOR FLASH存储器,其擦除指令有扇区擦除、块擦除和整片擦除四种,我们这里实现常用的扇区擦除。只需要发送擦除指令和扇区首地址即可。其操作时序图如下:
根据前述的描述及时序图,我们可以编写“擦除数据”的操作函数如下:
/*擦除指定的扇区,扇区大小4KB*/
static void SectorErase(BY25QObjectType *by250q,uint32_t eraseAddress)
{
BY25QCommandConfigType config;
config.Instruction=0x20; // 读ID指令0x90
config.DummyCycles=0; // 空指令周期数
config.AddressMode=1;
config.Address=eraseAddress;
config.DataMode=0;
config.NbData=0;
by250q->Command(config);
}
2.2.4、编程数据
向存储器中写数据又称之为编程数据,而BY25QXXX系列NOR FLASH存储器有三种编程指令:页编程、Quad页编程以及快速页编程。这里我们实现Quad页编程。在编程时,发送指令和地址采用单线模式,发送数据则采用四线模式。其指令操作时序图如下:
根据前述的描述及时序图,我们可以编写“编程数据”的操作函数如下:
/*写数据*/
static void QuadPageProgram(BY25QObjectType *by250q,uint32_t writeAddress,uint8_t *writeBuffer,uint32_t writeSize)
{
BY25QCommandConfigType config;
config.Instruction=0x32; // 读ID指令0x32
config.DummyCycles=0; // 空指令周期数
config.AddressMode=1;
config.Address=writeAddress;
config.DataMode=3;
config.NbData=writeSize;
by250q->Write(config,writeBuffer);
}
3、驱动的使用
前述我们已经完成了BY25QXXX系列NOR FLASH存储器驱动的设计与实现。接下来我们需要具体使用这一驱动程序来操作BY25QXXX系列NOR FLASH存储器,以便验证驱动程序的正确性。
3.1、声明并初始化对象
我们先声明一个BY25QXXX系列NOR FLASH存储器对象变量,然后的操作都是基于这一对象变量来进行的。
BY25QObjectType by250q;
如我们前面所述,对象变量必须要初始化才能使用。所以我们先来使用前面定义的初始化函数BY25QInitialization对这个对象变量进行初始化。这个初始化函数拥有多个输入变量如下:
BY25QObjectType *by250q, /*BY250Q存储器对象*/
BY25QWriteType write, /*写函数指针*/
BY25QReadType read, /*读函数指针*/
BY25QCommandType command, /*命令下发函数指针*/
BY25QReadyType ready /*就绪检测函数指针*/
在这些参数中,第一个参数就是我们要初始化的队形变量。后面的4个参数则是需要定义的操作函数指针。与具体的应用有关,需要我们在特定的应用场景中定义并作为参数传递给对象变量初始化函数。在这里,我们使用的是STM32H750的硬件平台和ST的HAL库函数,具体的实现如下:
/*读操作*/
static void ReadActionForBY25Q(BY25QCommandConfigType config,uint8_t *readBuffer)
{
QSPI_CommandTypeDef sCommand;
uint32_t addressMode[4]={QSPI_ADDRESS_NONE,QSPI_ADDRESS_1_LINE,QSPI_ADDRESS_2_LINES,QSPI_ADDRESS_4_LINES};
uint32_t dataMode[4]={QSPI_DATA_NONE,QSPI_DATA_1_LINE,QSPI_DATA_2_LINES,QSPI_DATA_4_LINES};
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE;
sCommand.AddressSize = QSPI_ADDRESS_24_BITS;
sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
sCommand.DdrMode = QSPI_DDR_MODE_DISABLE;
sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
sCommand.Instruction = config.Instruction;
sCommand.DummyCycles= config.DummyCycles;
sCommand.AddressMode = addressMode[config.AddressMode];
sCommand.Address = config.Address;
sCommand.DataMode = dataMode[config.DataMode];
sCommand.NbData = config.NbData;
if (HAL_QSPI_Command(&hqspi, &sCommand,HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
if (HAL_QSPI_Receive(&hqspi, readBuffer,HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
}
/*写操作*/
static void WriteActionForBY25Q(BY25QCommandConfigType config,uint8_t *writeBuffer)
{
QSPI_CommandTypeDef sCommand;
uint32_t addressMode[4]={QSPI_ADDRESS_NONE,QSPI_ADDRESS_1_LINE,QSPI_ADDRESS_2_LINES,QSPI_ADDRESS_4_LINES};
uint32_t dataMode[4]={QSPI_DATA_NONE,QSPI_DATA_1_LINE,QSPI_DATA_2_LINES,QSPI_DATA_4_LINES};
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE;
sCommand.AddressSize = QSPI_ADDRESS_24_BITS;
sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
sCommand.DdrMode = QSPI_DDR_MODE_DISABLE;
sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
sCommand.Instruction = config.Instruction;
sCommand.DummyCycles= config.DummyCycles;
sCommand.AddressMode = addressMode[config.AddressMode];
sCommand.Address = config.Address;
sCommand.DataMode = dataMode[config.DataMode];
sCommand.NbData = config.NbData;
if (HAL_QSPI_Command(&hqspi, &sCommand,HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
if (HAL_QSPI_Transmit(&hqspi, writeBuffer,HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
}
/*配置命令*/
static void ConfigCommandForBY25Q(BY25QCommandConfigType config)
{
QSPI_CommandTypeDef sCommand;
uint32_t addressMode[4]={QSPI_ADDRESS_NONE,QSPI_ADDRESS_1_LINE,QSPI_ADDRESS_2_LINES,QSPI_ADDRESS_4_LINES};
uint32_t dataMode[4]={QSPI_DATA_NONE,QSPI_DATA_1_LINE,QSPI_DATA_2_LINES,QSPI_DATA_4_LINES};
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE;
sCommand.AddressSize = QSPI_ADDRESS_24_BITS;
sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
sCommand.DdrMode = QSPI_DDR_MODE_DISABLE;
sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
sCommand.Instruction = config.Instruction;
sCommand.DummyCycles= config.DummyCycles;
sCommand.AddressMode = addressMode[config.AddressMode];
sCommand.Address = config.Address;
sCommand.DataMode = dataMode[config.DataMode];
sCommand.NbData = config.NbData;
if (HAL_QSPI_Command(&hqspi, &sCommand,HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
}
/* 检测存储器是否就绪 */
static void QSPI_AutoPollingMemReady(void)
{
QSPI_CommandTypeDef sCommand;
QSPI_AutoPollingTypeDef sConfig;
/* Configure automatic polling mode to wait for memory ready ------ */
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE;
sCommand.AddressSize = QSPI_ADDRESS_24_BITS;
sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
sCommand.DdrMode = QSPI_DDR_MODE_DISABLE;
sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
sCommand.Instruction = 0x05;
sCommand.AddressMode = QSPI_ADDRESS_NONE;
sCommand.DataMode = QSPI_DATA_1_LINE;
sCommand.DummyCycles = 0;
sConfig.Match = 0x00;
sConfig.Mask = 0x01;
sConfig.MatchMode = QSPI_MATCH_MODE_AND;
sConfig.StatusBytesSize = 1;
sConfig.Interval = 0x10;
sConfig.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE;
if (HAL_QSPI_AutoPolling(&hqspi, &sCommand, &sConfig, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
}
有了这些参数后,我们就可以使用这些参数来初始化BY25QXXX系列NOR FLASH存储器的对象变量了。
/*实现BY25Q初始化配置*/
BY25QInitialization(&by250q, /*BY250Q存储器对象*/
WriteActionForBY25Q, /*写函数指针*/
ReadActionForBY25Q, /*读函数指针*/
ConfigCommandForBY25Q, /*命令下发函数指针*/
QSPI_AutoPollingMemReady /*就绪检测函数指针*/
);
3.2、基于对象进行操作
我们设计这样一个操作场景,我们更具一个变量的值来读写BY25QXXX系列NOR FLASH存储器。当我们为指定的变量赋值为1时,我们从指定的地址读取一定数量的数据出来,并复位变量。当我们为指定的变量赋值为2时,我们擦除指定的地址所在的扇区,然后在指定地址写入一定数量的数据,并复位变量。
/*程序存储器测试*/
void FlashOperation(void)
{
switch(swBY25Q)
{
case 1:
{
ReadDataFromBy25q(&by250q,QSPI_MEM_ADDRESS,readBuffer,READ_LENGTH);
swBY25Q=0;
break;
}
case 2:
{
pTimes++;
for(int i=0;i<WRITE_LENGTH;i++)
{
writeBuffer[i]=i+pTimes;
}
EraseSectorForBy25q(&by250q,QSPI_MEM_ADDRESS);
WriteDataToBy25q(&by250q,QSPI_MEM_ADDRESS,writeBuffer,256);
WriteDataToBy25q(&by250q,QSPI_MEM_ADDRESS+256,&writeBuffer[256],WRITE_LENGTH-256);
swBY25Q=0;
break;
}
default:
{
swBY25Q=0;
break;
}
}
}
我们同过在先修改变量swBY25Q的值,先修改为2以写入100个字节的数据;然后将变量赋值为1以读取先前写入的100个数据用以验证是否正确。测试结果发现,写入的数据和读出的数据是完全一致的,说明的们设计的驱动程序是正确的。
4、应用总结
在这一篇中,我们设计并实现了BY25QXXX系列NOR FLASH存储器在间接操作模式下的驱动程序。后续我们也同过简单的读写操作实例验证了驱动成需的正确性。至此,BY25QXXX系列NOR FLASH存储器驱动程序的设计工作就完成了。
在使用BY25QXXX系列NOR FLASH存储器驱动程序时需要注意,状态寄存的QE位非常重要。在使用Quad SPI时,该位必须置“1”,否则以Quad SPI模式写数据是不会成功的。而在SPI和Dual SPI方式时,该位最好置“0”,这样WP和HOLD操作才能有效。
在使用BY25QXXX系列NOR FLASH存储器驱动程序时需要注意,在使用QSPI接口时,需要尽可能将对应的GPIO速度配置的快一点,否则可能会不能操作。