|
最近项目需要,搞双QSPI FLASH操作,包括BOOT和应用程序都使用双FLASH。网上的资料比较少,只得自己研究,现做个总结,希望对朋友们有用。
程序:BOOT
功能: DUAL QSPI操作,U盘启动升级,带LCD显示(升级进度)
升级:两部分。1:内部FLASH为主程序,2:QSPI FLASH存的是LCD显示资源
背景
硬件: 底板自制,核心板H743XI(隔壁XX火的进单位已成型,不然真不推荐),屏幕800*480(自购)
软件:根据需求,采用硬汉哥3个例程的代码,分别是:
V7-029_QSPI读写例程(四线DMA方式,读每秒48MB)
V7-026_FatFS文件系统例子(外挂U盘)
V7-049_内部Flash模拟EEPROM
屏幕驱动参考XX火的代码。
开发思路:代码不可能从0开始写,不然要吐血。因此考虑选取硬汉哥的一个例程作为基础,然后添加其他的驱动进去,这样最省事。目测
选取V7-026 这个最合适,因为U盘和FATFS都已具备,代码几乎不用修改直接就可以用。(这里要吐槽XX火的代码,很多代码感觉没怎么测试就放出来了,完全经不起折腾,代码里有些很明显的BUG,代码组织上也是有些山寨的感觉,出了问题基本只能自已摸索,因为技术支持=0,这也是前面说的不推荐的原因---个人看法不代表单位意见)。
开始动手了。。
U盘程序先下进去,啥也不用改,不得不说,硬汉哥的程序就是稳,直接启动,按照DEMO走一遍,功能都OK。
接下来就是重点搞双QSPI了,这个要单独测,基于V7-029_QSPI读写例程。网上关于H7 双QSPI操作的例子不多,最后还是在官方的软件包里的QSPI 例程
里找到两个关于双QSPI的代码,但官方的代码都比较简单,只能参考。
目录:STM32Cube_FW_H7_V1.7.0\Projects\STM32H743I-EVAL\Examples\QSPI
硬件设计框图:
如果对QSPI不是太熟,要先对H7手册上的QSPI模块部分要仔细看一看,寄存器不是太多,CCR寄存器里的东西要了解下,因为程序里发命令的参数
都是在配置这个寄存器。
这里有一段描述,感觉比较有价值,因为将直接影响后面的部分程序设计:
从上可知两片FLASH获取指令都是一样的,只是数据阶段分成两半写入或读取,其中FLASH BK1是偶地址数,BK2是奇地址数据。
之前有一个疑惑:单个FLASH时,一个扇区4K,写入4K数据,OK没问题,那双片FLASH时,一次多传输一半的数据,那这个4K数据是不是要变成8K或2k?
其实不动变,该怎么操作还怎么操作,这个是自动完成的。如下面这段码,单片或双片时都一样,只是在FLASH中占用的空间减小了一半,因为分成
两个来存了。
for(i = 0; i< TEST_SIZE; i += QSPI_PAGE_SIZE)
{
if (QSPI_WriteBuffer(buf, TEST_ADDR + i, QSPI_PAGE_SIZE) == 0)
{
printf("写串行Flash出错!\r\n");
return;
}
}
一定要把手册或相关教程看一下,然后开始改代码了。。在改代码前一定要注意板载的是什么型号的FLASH,切记。像我板上挂的是W25Q256JVEQ,
事实上还有一个型号W25Q256FV,而这两款芯片的手册不完一样,有些命令不相同。我前面看了好一会,感觉怎么总是怪怪的,再一看娘的,型号不对。
没什么特别的捷径,从初始化函数看起:
void bsp_InitQSPI_W25Q256(void)
{
/* 复位QSPI */
QSPIHandle.Instance = QUADSPI;
if (HAL_QSPI_DeInit(&QSPIHandle) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
/* 设置时钟速度,QSPI clock = 200MHz / (ClockPrescaler+1) = 100MHz */
QSPIHandle.Init.ClockPrescaler = 1;
/* 设置FIFO阀值,范围1 - 32 */
QSPIHandle.Init.FifoThreshold = 32;
/*
QUADSPI在FLASH驱动信号后过半个CLK周期才对FLASH驱动的数据采样。
在外部信号延迟时,这有利于推迟数据采样。DDR模式必须设置为0
*/
QSPIHandle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE;
/*Flash大小是2^(FlashSize + 1) = 2^25 = 32MB */
QSPIHandle.Init.FlashSize = QSPI_FLASH_SIZE - 1; // QSPI_FLASH_SIZE =26
/* 命令之间的CS片选至少保持1个时钟周期的高电平 */
QSPIHandle.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_3_CYCLE;
/*
MODE0: 表示片选信号空闲期间,CLK时钟信号是低电平
MODE3: 表示片选信号空闲期间,CLK时钟信号是高电平
*/
QSPIHandle.Init.ClockMode = QSPI_CLOCK_MODE_0;
/* QSPI有两个BANK,这里使用的BANK1 . 双QSPI无所谓*/
QSPIHandle.Init.FlashID = QSPI_FLASH_ID_2;
/* V7开发板仅使用了BANK1,这里是禁止双BANK */
//QSPIHandle.Init.DualFlash = QSPI_DUALFLASH_DISABLE;
QSPIHandle.Init.DualFlash = QSPI_DUALFLASH_ENABLE;
/* 初始化配置QSPI 控制器*/
if (HAL_QSPI_Init(&QSPIHandle) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
if(QSPI_ResetMemory() != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
HAL_Delay(50);
QSPI_EnterFourBytesAddress(&QSPIHandle);//add by bwu
HAL_Delay(10);
}
要知道这些配置是什么意思,一定要进到HAL_QSPI_Init函数里去看,这就要求我们对QSPI的寄存器比较了解,所以这也是为什么前面我说要看手册的原因
不然根本不明白这些配置是怎么来的,会产生什么影响。
接下来,再了解一下怎么发命令的,以复位函数为例:
/**
* @brief 复位QSPI存储器。
* @param QSPIHandle: QSPI句柄
* @retval 无
*/
static uint8_t QSPI_ResetMemory(void)
{
QSPI_CommandTypeDef s_command={0};
/* 初始化复位使能命令 */
s_command.InstructionMode = QSPI_INSTRUCTION_1_LINE;
s_command.Instruction = RESET_ENABLE_CMD;
s_command.AddressMode = QSPI_ADDRESS_NONE;
s_command.AddressSize = QSPI_ADDRESS_32_BITS; /* 32位地址 */
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 (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
/* 发送复位存储器命令 ,需紧接66H指令,不然将disable reset*/
s_command.Instruction = RESET_MEMORY_CMD;
if (HAL_QSPI_Command(&QSPIHandle, &s_command, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
/* 配置自动轮询模式等待存储器就绪 */
QSPI_AutoPollingMemReady(&QSPIHandle);
return QSPI_OK;
}
这里就要求我们对这句话有一定了解:
QUADSPI 通过命令与 FLASH 通信 每条命令包括指令、地址、交替字节、空指令和数据这 五个阶段 任一阶段均可跳过,但至少要包含指令、地址、交替字节或数据阶段之一。nCS 在每条指令开始前下降,在每条指令完成后再次上升。 ----- 手册原话
以及这些参数主要在配置的寄存器: 通信配置寄存器 (QUADSPI_CCR),
同时还要求对所用的FLASH要了解,明白发出的是什么命令,有什么作用,为什么要这么发。看下面红框里的说明 :
下面这个函数是查询FLASH 忙状态,
/*
*********************************************************************************************************
* 函 数 名: QSPI_AutoPollingMemReady
* 功能说明: 等待QSPI Flash就绪,主要用于Flash擦除和页编程时使用
* 形 参: hqspi QSPI_HandleTypeDef句柄
* 返 回 值: 无
*********************************************************************************************************
*/
static void QSPI_AutoPollingMemReady(QSPI_HandleTypeDef *hqspi)
{
QSPI_CommandTypeDef sCommand = {0};
QSPI_AutoPollingTypeDef sConfig = {0};
/* 基本配置 */
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 1线方式发送指令 */
sCommand.AddressSize = QSPI_ADDRESS_32_BITS; /* 32位地址 */
sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
sCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* W25Q256JV不支持DDR */
sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式,数据输出延迟 */
sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输都发指令 */
/* 读取状态*/
sCommand.Instruction = READ_STATUS_REG_CMD; /* 读取状态命令 */
sCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无需地址 */
sCommand.DataMode = QSPI_DATA_1_LINE; /* 1线数据 */
sCommand.DummyCycles = 0; /* 无需空周期 */
/* 屏蔽位设置的bit0,匹配位等待bit0为0,即不断查询状态寄存器bit0,等待其为0 */
sConfig.Mask = 0x0101;
sConfig.Match = 0x00;
sConfig.MatchMode = QSPI_MATCH_MODE_AND;
sConfig.StatusBytesSize = 2;//1;
sConfig.Interval = 0x10;
sConfig.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE;
if (HAL_QSPI_AutoPolling_IT(&QSPIHandle, &sCommand, &sConfig) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
}
注意这两句代码:
sConfig.Mask = 0x0101;
sConfig.Match = 0x00;
Mask 表示要匹配返回的状态字的bit位(为“1”的bit位有效),Match表示要匹配的值(对应mask的为‘1’bit位)。
这两句就是表示匹配返回的状态字的bit0和bit3位要等于0,即状态字的bit0位=0,即BUSY状态。
MASK在单片FLASH时是一个字节,现在是双FLASH,每个FLASH返回一个字节,共两字节,这个要注意。
再来看看FLASH的状态寄存器。
W25Q256JV FLASH有3个状态寄存器,每个8位,含义:
下面是写使能函数:
/*
*********************************************************************************************************
* 函 数 名: QSPI_WriteEnable
* 功能说明: 写使能
* 形 参: hqspi QSPI_HandleTypeDef句柄。
* 返 回 值: 无
*********************************************************************************************************
*/
static void QSPI_WriteEnable(QSPI_HandleTypeDef *hqspi)
{
QSPI_CommandTypeDef sCommand = {0};
QSPI_AutoPollingTypeDef s_config = {0};
/* 基本配置 */
sCommand.InstructionMode = QSPI_INSTRUCTION_1_LINE; /* 1线方式发送指令 */
sCommand.AddressSize = QSPI_ADDRESS_32_BITS; /* 32位地址 */
sCommand.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; /* 无交替字节 */
sCommand.DdrMode = QSPI_DDR_MODE_DISABLE; /* W25Q256JV不支持DDR */
sCommand.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY; /* DDR模式,数据输出延迟 */
sCommand.SIOOMode = QSPI_SIOO_INST_EVERY_CMD; /* 每次传输都发指令 */
/* 写使能 */
sCommand.Instruction = WRITE_ENABLE_CMD; /* 写使能指令 */
sCommand.AddressMode = QSPI_ADDRESS_NONE; /* 无需地址 */
sCommand.DataMode = QSPI_DATA_NONE; /* 无需数据 */
sCommand.DummyCycles = 0; /* 空周期 */
if (HAL_QSPI_Command(&QSPIHandle, &sCommand, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
/* Configure automatic polling mode to wait for write enabling */
s_config.Match = 0x0202;
s_config.Mask = 0x0202;
s_config.MatchMode = QSPI_MATCH_MODE_AND;
s_config.StatusBytesSize = 2;
s_config.Interval = 0x10;
s_config.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE;
sCommand.Instruction = READ_STATUS_REG_CMD;
sCommand.DataMode = QSPI_DATA_1_LINE;
if (HAL_QSPI_AutoPolling(hqspi, &sCommand, &s_config, HAL_QPSI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
/* 等待写使能完成 */
/*StatusMatch = 0;
QSPI_AutoPollingMemReady(&QSPIHandle);
while(StatusMatch == 0);
StatusMatch = 0;*/
}
s_config.Match = 0x0202;
s_config.Mask = 0x0202;
Write Enable Latch (WEL) is a read only bit in the status register (S1) that is set to 1 after executing a
Write Enable Instruction. The WEL status bit is cleared to 0 when the device is write disabled.
这两句代码检测状态寄存器1的WEL位,通过上在描述,在执行写使能指令时设置为1,当设备写禁止时此状态位清0.
双片FLASH写入数据时,偶地址的数据写入到FLASH1中,奇地址的数据写入到FLASH2中,用下面代码进行测试:
/* 填充测试缓冲区 */
for (i = 0; i < 1024; i++)
{
if(i%2==0)
buf = 0x22;//写到FLASH1
else
buf = 0x44;//写到FLASH2
}
写入后,再读出来数据应该是22 44 22 44....这样的,切换到单片时,FLASH1 中读出来的全部是22 22 22 ...,
在使用过程中遇到一个问题,当时想验证一下写入的值是否正常,双片写入后,切换到单片模式,FLASH BK1 驱动正常中,读写正常。但FLASH BK2 单片怎么也
不好使,不知道是哪里没搞对还是咋的。感觉FLASH BK2的启用必须是两片同时启用时才好用。简单说就是要么单片BK1, 要么双片。
另外要说明的一点是,硬汉哥代码中给出的全片擦除函数是通过一个扇区一个扇区的擦除,64MB擦下来真得太久,而W25Q256JV是有全片擦除指令的(0xC7),
一条指令就可以全片擦除,时间大幅缩短,亲测好用。
了解上面的知识点,基本可以正常驱动DUAL QSPI FLASH了。
将修改测试好的QSPI FLASH 驱动文件移植到USB 的工程中去,再测一把,测试OK。
接下来,测试内部FLASH读写擦除。直接上硬汉的例程,测试没问题,移植到USB工程中去不行,写入一部分数据后就报错,折腾了半天没折,后来还是在硬汉的提醒
下修改正确:FLASH的HAL库中的驱动老版本有BUG,需要用新版本的(HAL 1.7 或者 1.8版本),修改方法:
1. 将stm32h7xx_hal_flash.c,stm32h7xx_hal_flash_ex.c stm32h7xx_hal_flash.h, stm32h7xx_hal_flash_ex.h 四个文件分别替换掉原来的文件。
2. 修改stm32h743xx.h 关于FALSH的那部分定义,将HAL 1.7.0版本的stm32h743xx.h 中的FLASH部分复制过来(直接替换整个文件会带来别的问题):
老版本的:
/******************************************************************************/
/* */
/* FLASH */
/* */
/******************************************************************************/
/*
* @brief FLASH Total Sectors Number
*/
#if 0
#define FLASH_SECTOR_TOTAL 16
/******************* Bits definition for FLASH_ACR register **********************/
#define FLASH_ACR_LATENCY_Pos (0U)
#define FLASH_ACR_LATENCY_Msk (0x7U << FLASH_ACR_LATENCY_Pos) /*!< 0x00000007 */
省略。。。。
新版本的:
/*
* @brief FLASH Total Sectors Number
*/
#define FLASH_SECTOR_TOTAL 8U
#define FLASH_NB_32BITWORD_IN_FLASHWORD 8U
/******************* Bits definition for FLASH_ACR register **********************/
#define FLASH_ACR_LATENCY_Pos (0U)
#define FLASH_ACR_LATENCY_Msk (0xFUL << FLASH_ACR_LATENCY_Pos) /*!< 0x0000000F */
#define FLASH_ACR_LATENCY FLASH_ACR_LATENCY_Msk /*!< Read Latency */
#define FLASH_ACR_LATENCY_0WS (0x00000000UL)
#define FLASH_ACR_LATENCY_1WS (0x00000001UL)
#define FLASH_ACR_LATENCY_2WS (0x00000002UL)
#define FLASH_ACR_LATENCY_3WS (0x00000003UL)
。。。。
/******************* Bits definition for FLASH_ECC_FA register *******************/
#define FLASH_ECC_FA_FAIL_ECC_ADDR_Pos (0U)
#define FLASH_ECC_FA_FAIL_ECC_ADDR_Msk (0x7FFFUL << FLASH_ECC_FA_FAIL_ECC_ADDR_Pos) /*!< 0x00007FFF */
#define FLASH_ECC_FA_FAIL_ECC_ADDR FLASH_ECC_FA_FAIL_ECC_ADDR_Msk /*!< ECC error address */
===============================================END=================================================================
做完上面修改就可以测试一下全片擦除了(除BOOT区外)
在原DEMO上加了一个例子:
case '3':
/* 擦除扇区 */
{
uint32_t i=1, err=0;
uint8_t buf[4096]={0};
uint32_t sector[16]={
ADDR_FLASH_SECTOR_0_BANK1,
ADDR_FLASH_SECTOR_1_BANK1,
ADDR_FLASH_SECTOR_2_BANK1,
ADDR_FLASH_SECTOR_3_BANK1,
ADDR_FLASH_SECTOR_4_BANK1,
ADDR_FLASH_SECTOR_5_BANK1,
ADDR_FLASH_SECTOR_6_BANK1,
ADDR_FLASH_SECTOR_7_BANK1,
ADDR_FLASH_SECTOR_0_BANK2,
ADDR_FLASH_SECTOR_1_BANK2,
ADDR_FLASH_SECTOR_2_BANK2,
ADDR_FLASH_SECTOR_3_BANK2,
ADDR_FLASH_SECTOR_4_BANK2,
ADDR_FLASH_SECTOR_5_BANK2,
ADDR_FLASH_SECTOR_6_BANK2,
ADDR_FLASH_SECTOR_7_BANK2,
};
printf("擦除FLASH....\r\n");
for(i=3;i<16;i++) // boot占用前两个扇区
bsp_EraseCpuFlash(sector);
printf("擦除FLASH结束. 开始写入...\r\n");
memset(buf, 0xaa, 4096);
/* 扇区写入数据 */
for(i=0; i< ADDR_FLASH_SECTOR_7_BANK2+128*1024; i+=4096)//820 0000‬
{
err = bsp_WriteCpuFlash((uint32_t)ADDR_FLASH_SECTOR_3_BANK1+i, (uint8_t *)buf, sizeof(buf));
if (err){
printf("写入失败,err=%d, 位置: %ld\r\n", err, i);
break;
}
}
printf("写入结束,大小: %ld\r\n", i);
}
break;
到这里,基本BOOT就可以了,其他的都常规操作了,从U盘中检测文件是否存在,存在读取,写入FLASH。先升级内部FLASH,再升级QSPI FLASH。最后跳转。
不得不说,8线QSPI的速度比4线还是快多了,同样的文件,下载明显示快了一倍。比起MDK下载那根本不可同日而语。
走了一遍,确定没问题,跟领导说事情搞定了。领导看完后,淡淡的说句,再加了LCD显示一下升级进度。我说:给了个灯指示,下载的时候灯闪得很快。领导说,不够
人性。我:...
屏幕驱动搞了半天显示不正常,以过往走弯路的直觉,感觉方向不对,果断干掉屏幕程序,把XX火的驱动代码移植过来,一断操作,好了。
这里要注意一点:LCD与USB 的时钟冲突问题,LTDC外设时钟是挂在PLL3上的,所以USB的时钟源不能选PLL3了,因为两个外设的时钟不一样,切记
H7的时钟这块感觉还是比较复杂的,要好好看看时钟树的图。
泣血总结。
如果需要代码的可以Email: 624394687@qq.com
|
|