本例程基于STM32F103ZET6
FLASH大小为512K。
介绍FLASH
不同型号的 STM32,其 FLASH 容量也有所不同,最小的只有 16K 字节,最大的则达到了
1024K 字节。我们的精英 STM32 开发板选择的是 STM32F103ZET6 的 FLASH 容量为 512K 字节,属于大容量产品,大容量产品的闪存模块组织如表 43.1.1 所示:
主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。
平时,我们烧写的代码就存放在主存储器部分,
闪存的写入步骤(写入用户数据数据)
写操作有四步:
解锁——>擦除——>写数据—---->上锁
解锁:将两个特定的解锁序列号(KEY1:0x45670123 KEY2:0xCDEF89AB)依次写入FLASH_KEYR寄存器
擦除:FLASH物理特性(只能写0,不能写1),所以写FLASH之前需要擦除,将要写入的区域变为0xFFFF.擦除操作分为:页擦除和批量擦除
写数据:擦除完成,可以向FLASH写数据,每次只能以16位方式写入。
上锁:写入数据完成,需要设置FLASH_CR[LOCK]位为1,重新上锁,以防数据不小心被修改。
代码分析
写入数据
可以看到当数据写入时,会存在两种情况。
情况1:要写入的地址范围都在一个扇区(页),不跨扇区
情况2:要写入的地址范围不在一个扇区(页),跨扇区
编写代码的核心重点:FLASH物理特性(只能写0,不能写1),如果待写入地址的数据不是0xFFFF(16字节写入),那么就要把它擦除为0xFFFF,且擦除的时候要按扇区为单位来擦。
这里擦除数据,按一个一个扇区的擦除。那么我们要先获取要被写入数据的当前扇区的全部数据(不写入的部分也要保存,以免数据被误改),然后其中要写入的范围里的数据进行判断,是否都为0xFFFF,如果不是,那么进行擦除整个扇区,再重新写入整个扇区数据。如果都为0xFFFF,那么直接写入待写入的数据即可。
对应代码如下
stmflash_write_nocheck函数在stmflash_write函数里调用
**
* @brief 不检查的写入
这个函数的假设已经把原来的扇区擦除过再写入
* @param waddr : 起始地址 (此地址必须为2的倍数!!,否则写入出错!)
* @param pbuf : 数据指针
* @param length : 要写入的 半字(16位)数
* @retval 无
*/
void stmflash_write_nocheck(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
uint16_t i;
for (i = 0; i < length; i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, waddr, pbuf[i]);
waddr += 2; /* 指向下一个半字 */
}
}
/**
* @brief 在FLASH 指定位置, 写入指定长度的数据(自动擦除)
* @note 该函数往 STM32 内部 FLASH 指定位置写入指定长度的数据
* 该函数会先检测要写入的扇区是否是空(全0XFFFF)的?, 如果
* 不是, 则先擦除, 如果是, 则直接往扇区里面写入数据.
* 数据长度不足扇区时,自动被回擦除前的数据
* @param waddr : 起始地址 (此地址必须为2的倍数!!,否则写入出错!)
* @param pbuf : 数据指针
* @param length : 要写入的 半字(16位)数
* @retval 无
*/
uint16_t g_flashbuf[STM32_SECTOR_SIZE / 2]; /* 最多是2K字节 */
void stmflash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
uint32_t secpos; /* 扇区地址 */
uint16_t secoff; /* 扇区内偏移地址(16位字计算) */
uint16_t secremain; /* 扇区内剩余地址(16位字计算) */
uint16_t i;
uint32_t offaddr; /* 去掉0X08000000后的地址 */
FLASH_EraseInitTypeDef flash_eraseop;
uint32_t erase_addr; /* 擦除错误,这个值为发生错误的扇区地址 */
if (waddr < STM32_FLASH_BASE || (waddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE)))
{
return; /* 非法地址 */
}
HAL_FLASH_Unlock(); /* FLASH解锁 */
offaddr = waddr - STM32_FLASH_BASE; /* 实际偏移地址. */
secpos = offaddr / STM32_SECTOR_SIZE; /* 扇区地址 0~255 for STM32F103ZET6 */
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; /* 在扇区内的偏移(2个字节为基本单位.) */
secremain = STM32_SECTOR_SIZE / 2 - secoff; /* 扇区剩余空间大小 */
if (length <= secremain)
{
secremain = length; /* 不大于该扇区范围 */
}
while (1)
{
stmflash_read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE, g_flashbuf, STM32_SECTOR_SIZE / 2); /* 读出整个扇区的内容 */
for (i = 0; i < secremain; i++) /* 校验数据 */
{
if (g_flashbuf[secoff + i] != 0XFFFF)
{
break; /* 需要擦除 */
}
}
if (i < secremain) /* 需要擦除 */
{
flash_eraseop.TypeErase = FLASH_TYPEERASE_PAGES; /* 选择页擦除 */
flash_eraseop.Banks = FLASH_BANK_1;
flash_eraseop.NbPages = 1;
flash_eraseop.PageAddress = secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE; /* 要擦除的扇区 */
HAL_FLASHEx_Erase( &flash_eraseop, &erase_addr);
for (i = 0; i < secremain; i++) /* 复制 */
{
g_flashbuf[i + secoff] = pbuf[i];
}
stmflash_write_nocheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE, g_flashbuf, STM32_SECTOR_SIZE / 2); /* 写入整个扇区 */
}
else
{
stmflash_write_nocheck(waddr, pbuf, secremain); /* 写已经擦除了的,直接写入扇区剩余区间. */
}
if (length == secremain)
{
break; /* 写入结束了 */
}
else /* 写入未结束 */
{
secpos++; /* 扇区地址增1 */
secoff = 0; /* 偏移位置为0 */
pbuf += secremain; /* 指针偏移 */
waddr += secremain * 2; /* 写地址偏移(16位数据地址,需要*2) */
length -= secremain; /* 字节(16位)数递减 */
if (length > (STM32_SECTOR_SIZE / 2))
{
secremain = STM32_SECTOR_SIZE / 2; /* 下一个扇区还是写不完 */
}
else
{
secremain = length; /* 下一个扇区可以写完了 */
}
}
}
HAL_FLASH_Lock(); /* 上锁 */
}
注意事项
!!! 注意,如果要在这部分存储用户数据的话,不要把用户数据地址和存放代码和数据常数的地址重合
占用flash大小 = Code段+RO-data+RW-data.
占用SRAM大小 = RW-data + ZI-data
这里计算出占用FLASH大小为40588,16进制为9E8C,实际上可以.map文件,实际生成的占用FLASH大小会小于计算出来大小,这里为40332,16进制为9D8C,这是因为,未使用变量被优化掉了。
因为起始地址为0x8000 0000,那么用户存储数据的地址只能在0x8000 9D8C之后。
这里就定义为0X0807 0000。