项目场景:
在使用STM32G0B1这款MCU时,使用FLASH模拟片内EEPROM的功能,选择了一个2k的页存储一些系统参数。
问题描述
在调试功能时,操作FLASH经常失败,更有时会导致HardFault。废话不说直接上代码。
擦除代码:
/**
* @brief 擦除页
*
* @param address 起始地址
* @param addr_len 地址长度,注:单位为字节
* @return uint8_t FLASH_RTN_OK:擦除成功;
* 其他值:擦除失败
*/
uint8_t BSP_EraseFlashPages(uint32_t address, uint32_t addr_len)
{
FLASH_EraseInitTypeDef FlashEraseInit;
uint32_t SectorError, addr_end;
uint8_t rtn = FLASH_RTN_OK;
addr_end = address + addr_len - 1;
if (address < ADDR_FLASH_BANK_1 || address > ADDR_FLASH_END ||
addr_end < ADDR_FLASH_BANK_1 || addr_end > ADDR_FLASH_END)
{
return FLASH_RTN_ERROR; // 地址不在规定范围内
}
if(address < ADDR_FLASH_BANK_2 && addr_end >= ADDR_FLASH_BANK_2) // 跨bank擦除
{
FlashEraseInit.Banks = FLASH_BANK_1;
FlashEraseInit.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
FlashEraseInit.Page = ((address - ADDR_FLASH_BANK_1) / 2048); // 起始页
FlashEraseInit.NbPages = 127 - FlashEraseInit.Page + 1;
HAL_FLASH_Unlock(); //解锁
if (HAL_FLASHEx_Erase(&FlashEraseInit, &SectorError) != HAL_OK)
{
rtn = FLASH_RTN_ERASE_ERROR;
//发生错误了
HAL_FLASH_Lock(); //上锁
return rtn;
}
FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
FlashEraseInit.Banks = FLASH_BANK_2;
FlashEraseInit.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
FlashEraseInit.Page = 0; // 起始页
FlashEraseInit.NbPages = ((addr_end - ADDR_FLASH_BANK_1) / 2048) - 127;
if (HAL_FLASHEx_Erase(&FlashEraseInit, &SectorError) != HAL_OK)
{
rtn = FLASH_RTN_ERASE_ERROR;
//发生错误了
}
FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
HAL_FLASH_Lock(); //上锁
return rtn;
}
else // 同BANK擦除
{
// 判断地址所在bank
if(address >= ADDR_FLASH_BANK_2)
{
FlashEraseInit.Banks = FLASH_BANK_2; // 要擦除的块
}
else
{
FlashEraseInit.Banks = FLASH_BANK_1;
}
FlashEraseInit.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
FlashEraseInit.Page = ((address - ADDR_FLASH_BANK_1) / 2048); // 起始页
FlashEraseInit.NbPages = ((addr_end - ADDR_FLASH_BANK_1) / 2048) - FlashEraseInit.Page + 1;
HAL_FLASH_Unlock(); //解锁
if (HAL_FLASHEx_Erase(&FlashEraseInit, &SectorError) != HAL_OK)
{
rtn = FLASH_RTN_ERASE_ERROR;
//发生错误了
}
FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
HAL_FLASH_Lock(); //上锁
return rtn;
}
}
写入代码:
/**
* @brief 从指定的Flash地址写入数据,按双字写入
*
* @param writeAddr Flash写入起始地址
* @param pBuffer 写入缓冲区首指针,内存地址必须8字节对其
* @param ByteToWrite 要写的字节个数,注意是字节个数
* @return uint8_t FLASH_RTN_OK:写入成功; 其他值:写入失败
*/
uint8_t BSP_WriteFlashDoubleWord(uint32_t writeAddr, uint64_t *pBuffer, uint32_t ByteToWrite)
{
HAL_StatusTypeDef FlashStatus = HAL_OK;
uint32_t addrx = 0;
uint32_t endaddr = 0;
uint8_t rtn = FLASH_RTN_OK;
//非法地址
if (writeAddr < STM32_FLASH_BASE || writeAddr % 4)
{
return FLASH_RTN_ERROR;
}
if (((uint32_t)pBuffer & 7) != 0)
{
return FLASH_RTN_ERROR;
}
addrx = writeAddr; //写入的起始地址
endaddr = writeAddr + ByteToWrite; //写入的结束地址
FlashStatus = FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
HAL_FLASH_Unlock(); //解锁
if (FlashStatus == HAL_OK)
{
addrx = writeAddr;
while (addrx < endaddr) //写数据
{
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addrx, *pBuffer) != HAL_OK) //写入数据
{
HAL_FLASH_Lock(); //上锁
return FLASH_RTN_WRITE_ERROR;//写入异常
}
addrx += sizeof(typeof(*pBuffer));
pBuffer += 1;
}
}
HAL_FLASH_Lock(); //上锁
return rtn;
}
原因分析:
这段代码是参考之前的STM32F4的代码,怀疑是F4和G0的flash操作有差异,可能有些时序有误,导致了hardfault。于是翻出了G0的手册阅读了flash章节,发现确实是由于操作差异导致了问题,下面就详细展开说明。
- G0只提供了两种写入方式:双字写入和快速写入(32 double words = 256 bytes),除此之外的写入都被视为非法操作,会引起SR寄存器错误标志位置位。
- 由于写入方式限制,导致SR寄存器可能在当前操作前已经出现错误标志位,手册里也对时序做出明确规定,先清除由于之前操作引起的错误标志位,再执行本次flash操作。
基于以上两点,手册对flash操作进行了详细说明:
页擦除:
擦除步骤:
1)解锁FLASH
2)等待SR->BSY1 和 SR->BSY2为0
3)检查并清除所有的错误标志位,尤其是SR->PGSERR
4)以上步骤都通过后,才可以进行擦除操作
HAL库的源码已经对以上的操作进行了一些封装:
1、擦除前先判断BSY1和BSY2, 如果置位,直接返回HAL_TIMEOUT。程序应对返回值进行判断,返回值是HAL_TIMEOUT并且*PageError未被修改,表示flash正在操作,需等待。
2、判断错误标志位,有则清除并返回HAL_ERROR,程序应对返回值进行判断,返回值是HAL_ERROR并且*PageError未被修改,表示之前flash操作有误,但已经将错误标志位清除,可以重新进行擦除操作。
前面判断无问题,flash空闲并且也无错误发生。执行擦除操作,返回值是HAL_ERROR或者HAL_TIMEOUT,并且*PageError不是0xFFFFFFFF则代表擦除失败的页标号,反之则擦除成功。
写入:
写入步骤:
1)写过的地址不可以写入非0数据。
(全写0代表此区域废除->flash均衡算法,写满一页再擦除)
2)字节或半字写入会导致SIZERR,地址未8字节对齐导致PGAERR
3)等待SR->BSY1 和 SR->BSY2为0
4)检查并清除所有的SR寄存器内的错误标志位
5)执行写操作
源码:
1、先判断BSY1和BSY2, 如果置位,直接返回HAL_TIMEOUT。程序应对返回值进行判断,返回值是HAL_TIMEOUT,表示flash正在操作,需等待。
2、判断错误标志位,有则清除并返回HAL_ERROR,程序应对返回值进行判断,返回值是HAL_ERROR,表示之前flash操作有误,但已经将错误标志位清除,可以重新进行写入操作。
FLASH操作时间
另外查看datasheet还可以知道页擦除的时间最长可以去到40ms,时间上也要注意。
由此已经解释清除了G0片内flash的操作时序,参照正确的时序修改代码应该就可以解决问题。
解决方案:
按照上述时序优化flash驱动代码。
定义返回值,可以精准分析具体的出错原因。
// 返回值
#define FLASH_RTN_OK 0x00 // OK
#define FLASH_RTN_PARA_ERROR 0x01 // 参数错误
#define FLASH_RTN_UNLOCK_ERROR 0x02 // 解锁失败
#define FLASH_RTN_LOCK_ERROR 0x04 // 上锁失败
#define FLASH_RTN_ERASE_ERROR 0x08 // 擦除错误
#define FLASH_RTN_WRITE_ERROR 0x10 // 写错误
#define FLASH_RTN_BSY 0x20 // flash busy
#define FLASH_RTN_SR_ERROR 0x40 // SR错误标志置位
擦除:
/**
* @brief 锁定flash
*
*/
static uint8_t flash_lock_private(void)
{
if(HAL_FLASH_Lock() != HAL_OK) //上锁
{
return FLASH_RTN_LOCK_ERROR;
}
return FLASH_RTN_OK;
}
/**
* @brief 擦除flash
*
*/
static uint8_t flash_erase_private(FLASH_EraseInitTypeDef *herase, uint32_t *hpage)
{
uint8_t i, rtn = FLASH_RTN_OK;
HAL_StatusTypeDef ret;
*hpage = 0xFFFFFFFF;
for(i = 0; i < 2; i++) // 操作两次,防止第一次由于之前的flash操作失败导致报错
{
ret = HAL_FLASHEx_Erase(herase, hpage);
if(ret == HAL_OK)
{
rtn |= FLASH_RTN_OK;
break;
}
else if(i == 0 && ret == HAL_ERROR && *hpage == 0xFFFFFFFF)
{
// 首次出现ERROR,由于之前的操作失败导致报错
// HAL_FLASHEx_Erase()会清除SR寄存器的错误标志,重试第二次
continue;
}
else
{
if(ret == HAL_TIMEOUT)
{
rtn |= FLASH_RTN_BSY;
}
else if(ret == HAL_ERROR)
{
rtn |= FLASH_RTN_SR_ERROR;
}
if(*hpage != 0xFFFFFFFF)
rtn |= FLASH_RTN_ERASE_ERROR;
break;
}
}
return rtn;
}
/**
* @brief 擦除页
*
* @param address 起始地址
* @param addr_len 地址长度,注:单位为字节
* @return uint8_t FLASH_RTN_OK:擦除成功;
* 其他值:擦除失败
*/
uint8_t BSP_EraseFlashPages(uint32_t address, uint32_t addr_len)
{
FLASH_EraseInitTypeDef FlashEraseInit;
uint32_t SectorError, addr_end;
uint8_t rtn = FLASH_RTN_OK;
addr_end = address + addr_len - 1;
if (address < ADDR_FLASH_BANK_1 || address > ADDR_FLASH_END ||
addr_end < ADDR_FLASH_BANK_1 || addr_end > ADDR_FLASH_END)
{
return FLASH_RTN_PARA_ERROR; // 地址不在规定范围内
}
if(HAL_FLASH_Unlock() != HAL_OK) //解锁
{
return FLASH_RTN_UNLOCK_ERROR;
}
if(address < ADDR_FLASH_BANK_2 && addr_end >= ADDR_FLASH_BANK_2) // 跨bank擦除
{
FlashEraseInit.Banks = FLASH_BANK_1;
FlashEraseInit.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
FlashEraseInit.Page = ((address - ADDR_FLASH_BANK_1) / 2048); // 起始页
FlashEraseInit.NbPages = 127 - FlashEraseInit.Page + 1;
if((rtn = flash_erase_private(&FlashEraseInit, &SectorError)) != FLASH_RTN_OK)
{
rtn |= flash_lock_private(); // 上锁
return rtn;
}
FlashEraseInit.Banks = FLASH_BANK_2;
FlashEraseInit.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
FlashEraseInit.Page = 0; // 起始页
FlashEraseInit.NbPages = ((addr_end - ADDR_FLASH_BANK_1) / 2048) - 127;
rtn = flash_erase_private(&FlashEraseInit, &SectorError);
}
else // 同BANK擦除
{
// 判断地址所在bank
if(address >= ADDR_FLASH_BANK_2)
{
FlashEraseInit.Banks = FLASH_BANK_2; // 要擦除的块
}
else
{
FlashEraseInit.Banks = FLASH_BANK_1;
}
FlashEraseInit.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
FlashEraseInit.Page = ((address - ADDR_FLASH_BANK_1) / 2048); // 起始页
FlashEraseInit.NbPages = ((addr_end - ADDR_FLASH_BANK_1) / 2048) - FlashEraseInit.Page + 1;
rtn = flash_erase_private(&FlashEraseInit, &SectorError);
}
rtn |= flash_lock_private(); // 上锁
return rtn;
}
写入:
/**
* @brief 从指定的Flash地址写入数据,按双字写入
*
* @param writeAddr Flash写入起始地址,写入起始地址必须8字节对齐
* @param pBuffer 写入缓冲区首指针
* @param ByteToWrite 要写的字节个数,注意是字节个数,必须是8的整倍数
* @return uint8_t FLASH_RTN_OK:写入成功; 其他值:写入失败
*/
uint8_t BSP_WriteFlashDoubleWord(uint32_t writeAddr, uint64_t *pBuffer, uint32_t ByteToWrite)
{
HAL_StatusTypeDef ret;
uint32_t addrx = 0, endaddr = 0, cnt = 0;
uint8_t rtn = FLASH_RTN_OK;
//非法地址
if (writeAddr < STM32_FLASH_BASE || writeAddr % 8)
{
return FLASH_RTN_PARA_ERROR;
}
// if (((uint32_t)pBuffer & 7) != 0)
// {
// return FLASH_RTN_PARA_ERROR;
// }
addrx = writeAddr; //写入的起始地址
endaddr = writeAddr + ByteToWrite; //写入的结束地址
if(HAL_FLASH_Unlock() != HAL_OK) //解锁
{
return FLASH_RTN_UNLOCK_ERROR;
}
addrx = writeAddr;
while (addrx < endaddr) //写数据
{
if((ret = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addrx, *pBuffer)) != HAL_OK) //写入数据
{
if(cnt == 0 && ret == HAL_ERROR)
{
// 首次出现ERROR,由于之前的操作失败导致报错
// HAL_FLASH_Program()会清除SR寄存器的错误标志,重试第二次
cnt++;
continue;
}
else
{
rtn |= FLASH_RTN_WRITE_ERROR;
break;
}
}
addrx += sizeof(typeof(*pBuffer));
pBuffer += 1;
cnt++;
}
rtn |= flash_lock_private(); // 上锁
return rtn;
}
完善驱动后,问题得到解决,flash操作不再失败,也不会进入hardfault。
总结:
总的来说,在使用MCU的时候,还是得查看官方的手册,一切操作以手册为准。本以为ST的外设驱动兼容性会很高,一开始就图省事直接把F4的代码搬过来用了,后面还是得老老实实地读手册扣代码。