QSIP驱动W25Q256调试记录

记录调试QSPI方式驱动W25Q256,其中以正点原子和野火的STM32F767系列的例程作为参考。

发现异常

  1. 正点原子 & 野火 例程中有水分,不适合参考指令。
  2. W25Q256JV 数据手册中部分指令的图表与时序图不符。

初始化配置

  • 引脚初始化
/**
  * @brief  QSPI引脚初始化
  * @param  None
  * @retval None
  */
void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi)
{
//忽略
}
  • QSPI模式初始化
/**
  * @brief  QSPI模式初始化
  * @param  None
  * @retval 失败回复QSPI_ERROR ,成功回复QSPI_OK
  */
uint8_t QSPI_Init(void)
{
    QSPI_Handler.Instance = QUADSPI;                                  //QSPI
    QSPI_Handler.Init.ClockPrescaler   = 1;                       
    QSPI_Handler.Init.FifoThreshold    = 4;                          
    QSPI_Handler.Init.SampleShifting   = QSPI_SAMPLE_SHIFTING_HALFCYCLE;
    QSPI_Handler.Init.FlashSize      = 25 - 1;                        
    QSPI_Handler.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_6_CYCLE;
    QSPI_Handler.Init.ClockMode      = QSPI_CLOCK_MODE_0;            
    QSPI_Handler.Init.FlashID       = QSPI_FLASH_ID_1;               
    QSPI_Handler.Init.DualFlash      = QSPI_DUALFLASH_DISABLE;     
    HAL_QSPI_Init(&QSPI_Handler);

   /* 设置QSPI存储器为4字节地址模式 */
 if (QSPI_SetMode_4ByteAddr() != QSPI_OK)
 {
  printf("QSPI_ERROR \r\n");
  return QSPI_ERROR;
 }
 return QSPI_OK;

  • 初始化要点
    初始化后,应该马上将地址模式设置为“4字节模式”。
    指令:0xB7
    检验是否初始化成功,可以使用读ID指令检验。后述有驱动配置细讲,不再贴代码。
/**
 * @brief  设置QSPI存储器为4字节地址模式(该函数只能在单线模式下)
 * @param  None
 * @retval 失败回复QSPI_ERROR ,成功回复QSPI_OK
 */
uint8_t QSPI_SetMode_4ByteAddr(void)
{
QSPI_CommandTypeDef s_cmd;
 /*可以通过读SR3来查看ADS位确定是否需要继续*/
if(W25Q_ReadSR(3) & W25Q_FSR3_ADS)
{
 printf("4ByteAddr already set! \r\n");
 return QSPI_OK;
}
s_cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;        //指令模式 :单线
s_cmd.Instruction       = W25X_Enable4ByteAddr;              //指令0xB7 
s_cmd.AddressMode       = QSPI_ADDRESS_NONE;               //地址模式 :无
s_cmd.Address      = 0;                         					    //地址   :无
s_cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;    //交替字节 :无
s_cmd.DataMode          = QSPI_DATA_NONE;     	   	 //数据   :无        
s_cmd.NbData       = 0;            			  			  //数据长度 :0      
s_cmd.DummyCycles       = 0;            		  			  //空周期  :0
s_cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;      //DDR   :禁止
s_cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;    //DDR延迟 :模拟延迟
s_cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;    //SIO0模式:不起作用
if (HAL_QSPI_Command(&QSPI_Handler, &s_cmd, QPSI_TIMEOUT) != HAL_OK)
{
return QSPI_ERROR;
}
  • 到此处,QSPI初始化已经完成。但在正点原子的例程中,初始化有进入QSPI模式的指令操作,指令0x38,但在W25Q256JV手册搜索并未发现该指令。查找发现 W25Q256的其他系列(非JV)有出现过0x38的指令。
  • 经测试,使用EnterQPIMode指令后,指令模式只能使用4线,无法再用单线指令,而数据手册时序图皆以单线指令作为指引。
    所以此处不建议继续参考正点原子的例程,会带来不必要的麻烦。
#define W25X_EnterQPIMode       0x38 //正点原子的非jv系列指令,不建议再使用

指令时序分析与驱动函数

W25Q256JV 数据手册

数据手册Instruction Set Table四个图表需要重点理解,分别是

8.1.2 Instruction Set Table 1 (Standard/Dual/Quad SPI, 3-Byte Address Mode)
8.1.3 Instruction Set Table 2 (Dual/Quad SPI Instructions,3-Byte Address Mode)
8.1.4 Instruction Set Table 3 (Standard SPI, 4-Byte Address Mode)
8.1.5 Instruction Set Table 4 (Dual/Quad SPI Instructions, 4-Byte Address Mode)

可以见到,除了Table3 不是QSPI的指令,其他3个图表指令皆可以当QSPI下使用,但因为地址模式已经设置为 4-Byte Address Mode,所以我们要用的就应该是 Table 4的指令表格。

8.1.5 Instruction Set Table 4 (Dual/Quad SPI Instructions, 4-Byte Address Mode)
这里我曾出现一个误区,认为如果使用QSPI指令的话,那么Table 3 的 4地址_SPI指令就不能再使用。其实这样的思维是错误的!在没有使用正点原子中的0x38-QSPImode指令时,QSPI和SPI指令都是可以照常使用的,也就是在进行一些简单的读写寄存器,可以继续使用SPI指令,如TABLE 3中的05h-读寄存器01h-写寄存器

在这里插入图片描述

纠错

正点原子中,完全没有使用Table 4的 Dual/Quad SPI Instructions, 4-Byte Address Mode 指令,所以此处第二次建议不要参考正点原子代码中的指令,容易误入思维误区!


驱动函数编程

读flash写flash作为举例。

读Flash:0x6C

因为读Flash包含大量的数据,建议使用QSPI指令,也就是Table 4的指令

在这里插入图片描述

  • 快速读QSPI-4字节地址-6Ch
    在这里插入图片描述
    根据上面的时序图分析:
    指令模式:单线 8位(1字节)
    地址模式:单线 32位(4字节)
    交替: 0
    空周期:单线 8位(1字节)
    数据:4线 (QSPI)

读FLASH驱动如下:

/**
 * @brief  快速读数据 (4线4地址指令)
 * @param 	pBuffer:输入读缓冲buf;Address:读开始flash地址;NumByte :读的字节量
 * @retval 空闲回复QSPI_OK,忙回复QSPI_BUSY
 */
 void W25Q_FastReadData(uint8_t *pBuffer,uint32_t Address,uint16_t NumByte)
{

   QSPI_CommandTypeDef s_cmd;
   W25Q_BusyWait();                     //等待空闲
   s_cmd.Instruction       = 0x6C;   		    //指令     :6C
   s_cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;        //指令模式 :1线
   
   s_cmd.Address					  = Address;									    
   s_cmd.AddressMode       = QSPI_ADDRESS_1_LINE;             //地址模式 :1线
   s_cmd.AddressSize		  	= QSPI_ADDRESS_32_BITS;		 //地址大小 :32bit
   
   s_cmd.DataMode          = QSPI_DATA_4_LINES;               //数据模式 :4线
   s_cmd.NbData						= NumByte;												 //数据长度	:NumByte			
   
   s_cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;			  //交替字节 :无
   s_cmd.DummyCycles       = 8;								//空周期		:8位 (重点)
   s_cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;					  //DDR			:禁止
   s_cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;			  //DDR延迟 :模拟延迟
   s_cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;			    //SIO0模式:不起作用
if (HAL_QSPI_Command(&QSPI_Handler, &s_cmd, QPSI_TIMEOUT) != HAL_OK)
   	printf("W25Q_FastReadData Time out \r\n");
   	
 if(HAL_QSPI_Receive(&QSPI_Handler,pBuffer,5000)!=HAL_OK)
   	printf("W25Q_FastReadData receive fault \r\n");;
   W25Q_BusyWait();                     //等待空闲
}
纠错

W25Q datasheet 的图表和时序图空周期长度不符。
野火并未使用QSPI指令读数据。

  • 手册上 指令6Ch 的时序图的空周期是8位的,而Table 4上标的是2个字节(16位)的空周期,因为当时测试没仔细对比两个图的异样,填的是16位,导致读Flash测试时前4字的数据丢失,无法被读取。遇到同样故障的小伙伴可以留意下空周期数和地址长度是否不符问题

  • 野火的例程虽然在写FLASH操作上使用Table 4指令,但读FLASH仍然用的SPI 03h指令,所以建议也不要参考野火例程的指令,自行编写更好。

以下是野火例程(不要参考):

/* 读操作 */
#define READ_CMD                             0x03           //野火例程中,读操作只使用了0X03
#define FAST_READ_CMD                        0x0B
#define DUAL_OUT_FAST_READ_CMD               0x3B
#define DUAL_INOUT_FAST_READ_CMD             0xBB
#define QUAD_OUT_FAST_READ_CMD               0x6B
#define QUAD_INOUT_FAST_READ_CMD             0xEB
/**
 * @brief  从QSPI存储器中读取大量数据.
 * @param  pData: 指向要读取的数据的指针
 * @param  ReadAddr: 读取起始地址
 * @param  Size: 要读取的数据大小    
 * @retval QSPI存储器状态
 */
 uint8_t BSP_QSPI_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)
{
/*其他省略*/
s_command.Instruction       = READ_CMD;  //0x03
}


扇区擦除:0x21

  • 因为擦除没有数据交互,可以使用TABLE 3的 SPI指令
    在这里插入图片描述
    根据上面的时序图分析:
    指令模式:单线 8位(1字节)
    地址模式:单线 32位(4字节)
    交替: 0
    空周期:0
    数据:0
/**
  * @brief  扇区擦除
  * @param 	Page:扇区页码
  * @retval 
  */
  void W25Q_Erase_Sector(uint16_t Page)
{
	QSPI_CommandTypeDef s_cmd;
	uint32_t Addr;
	Addr = Page * W25Q_SECTORSBYTE;
	
	W25Q_Write_Enable();                 //写使能
	W25Q_BusyWait();                     //等待空闲
	
	s_cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;        //指令模式 :1线
	s_cmd.Instruction       = 0x21;                           //指令 :SPI    
	s_cmd.Address					  = Addr;						   //地址			
	s_cmd.AddressSize			  = QSPI_ADDRESS_32_BITS;           //地址长度:4字节
	s_cmd.AddressMode       = QSPI_ADDRESS_1_LINE;            //地址模式 :1线
	s_cmd.DataMode          = QSPI_DATA_NONE;                  //指令模式 :0
	s_cmd.NbData						= 0;															 //数据长度	:0			
	s_cmd.DummyCycles       = 0;										//空周期		:0
	s_cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;			 //交替字节 :无
	s_cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;					 //DDR			:禁止
	s_cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;			 //DDR延迟 :模拟延迟
	s_cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;			   //SIO0模式:不起作用
		
	if (HAL_QSPI_Command(&QSPI_Handler, &s_cmd, QPSI_TIMEOUT) != HAL_OK)
	{
		printf("W25Q_Erase_Sector Time out \r\n");
	}
	W25Q_BusyWait();                     //等待空闲
}

写FLASH(最大256字节):0x34

  • 因为写Flash包含大量的数据,建议使用QSPI指令,也就是Table 4的指令
    在这里插入图片描述
    根据上面的时序图分析:
    指令模式:单线 8位(1字节)
    地址模式:单线 32位(4字节)
    交替: 0
    空周期:0
    数据:4线 (QSPI)
 /**
  * @brief  FLASH页编程。每次最大可写256BYTE
  * @param 	Page:扇区页码
  * @retval 
  */
void W25Q_Program_Page(uint8_t *pBuff,uint32_t Addr,uint16_t NumByte)
{
	QSPI_CommandTypeDef s_cmd;
	
	W25Q_Write_Enable();                 //写使能
	W25Q_BusyWait();                     //等待空闲
	printf("start program\r\n");
	s_cmd.InstructionMode   = QSPI_INSTRUCTION_1_LINE;       //指令模式 :1线
	s_cmd.Instruction       = 0x34;                          //指令     :QSPI
	
  s_cmd.AddressMode       = QSPI_ADDRESS_1_LINE;           //地址模式 :1线
	s_cmd.AddressSize			  = QSPI_ADDRESS_32_BITS;           //地址长度:4字
	s_cmd.Address					  = Addr;					//地址			Addr
	
	s_cmd.DataMode          = QSPI_DATA_4_LINES;               //指令模式 :0
	CmdHandler.NbData			  = NumByte;				   //数据长度	:						
	s_cmd.DummyCycles       = 0;							 //空周期		:0
	
	s_cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;			 //交替字节 :无
	s_cmd.DdrMode           = QSPI_DDR_MODE_DISABLE;					 //DDR			:禁止
	s_cmd.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;			 //DDR延迟 :模拟延迟
	s_cmd.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;			   //SIO0模式:不起作用
		
	if (HAL_QSPI_Command(&QSPI_Handler, &s_cmd, QPSI_TIMEOUT) != HAL_OK)
		printf("W25Q_Program_Page Time out \r\n");

	HAL_QSPI_Transmit(&QSPI_Handler,pBuff,5000);
	W25Q_BusyWait();                     //等待空闲
	
}  

总结:

1.正点原子和野火的例程都存在不严谨的问题,虽然结果能实现,但不适合初学者去区分QSPI和SPI的运用。
2.W25Q数据手册图表也存在空周期数不符问题,大家在使用数据手册过程中,一定要确保数据手册与IC对上,如测试过程中出现问题,第一时间翻查时序图确保各项参数正常。

补充说明(等待FLASH空闲)

野火例程中有状态轮询操作,而正点原子是使用读SR1的0位(busy)来等待完成,两者都可以完成等待Flash空闲,但测试时,轮询功能因失败导致QSPI进入错误故障中,无法继续正常读写,对后续程序影响较大,推荐直接使用读busy位作为判断。

  • 推荐循环读SR1
/*推荐使用*/
#define 			W25Q_FSR_WRIEN  							((uint8_t)(1 << 1))					/*!< write enable */
#define 			W25Q_FSR_BUSY              				   ((uint8_t)(1 << 0))   		  /*!< busy */
#define			  W25Q_FSR2_QE									((uint8_t)(1 << 1))				  /*!< quad enable */
#define				W25Q_FSR3_ADS							    ((uint8_t)(1 << 0))					/*!< 4address enable */
 /**
  * @brief  FLASH 空闲状态检测(循环等待空闲)
  * @param 	无
  * @retval 
  */
void W25Q_BusyWait(void)
{
	
	while( (W25Q_ReadSR(1) & W25Q_FSR_BUSY) ==  W25Q_FSR_BUSY);
	printf("SR 1 : %d\r\n",W25Q_ReadSR(1));
}

  • HAL_QSPI_AutoPolling,需要重新配置CmdHandler和PollingHandler,如掩码错误会引起不必要的故障,故不推荐轮询模式。
/**
  * @brief  QSPI_AutoPollingReady 读取存储器的SR并等待EOP
  * @param  None
  * @retval 失败回复QSPI_ERROR ,成功回复QSPI_OK
  */
uint8_t QSPI_AutoPollingReady(uint32_t timeout)
{
	/* 配置自动轮询模式等待存储器准备就绪 */  
	CmdHandler.InstructionMode   = QSPI_INSTRUCTION_1_LINE;        //指令模式 :单线
	CmdHandler.Instruction       = W25X_ReadStatusReg1;					   //指令		  :读状态寄存器
	CmdHandler.AddressMode       = QSPI_ADDRESS_NONE;						   //地址模式	:无
	CmdHandler.Address					 = 0;															 //地址			:无
	CmdHandler.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;			 //交替字节 :无
	CmdHandler.DataMode          = QSPI_DATA_1_LINE;							 //数据模式	:单线
	CmdHandler.NbData						 = 1;															 //数据长度	:1BYTE							
	CmdHandler.DummyCycles       = 0;															 //空周期		:0
	CmdHandler.DdrMode           = QSPI_DDR_MODE_DISABLE;					 //DDR			:禁止
	CmdHandler.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;			 //DDR延迟 :模拟延迟
	CmdHandler.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;			 //SIO0模式:不起作用
	
	PollingHandler.Match           = 0x00;												
	PollingHandler.Mask            = W25Q_FSR_BUSY;								 //掩码:		((uint8_t)(1 << 0)) 
	PollingHandler.MatchMode       = QSPI_MATCH_MODE_AND;
	PollingHandler.StatusBytesSize = 1;
	PollingHandler.Interval        = 0x10;
	PollingHandler.AutomaticStop   = QSPI_AUTOMATIC_STOP_ENABLE;
	
	if (HAL_QSPI_AutoPolling(&QSPI_Handler, &CmdHandler, &PollingHandler, timeout) != HAL_OK)  //自动轮询
		return QSPI_ERROR;
	else
    return QSPI_OK;
}


此记录用于本人留底翻查,并提供给其他与我一样的新用户参考,如有其他意见,可以留言或发邮箱到 xiaojianjiande@163.com 纠错。