目录
1.内部FLASH构成(F1)
2.FLASH读写过程(F1)
2.1内存的读取
2.2闪存的写入
2.3FLASH接口寄存器(写入 & 擦除相关)
3.FLASH相关HAL库函数简介(F1/F4/F7/H7)
4.编程实战
代码:
1.内部FLASH构成(F1)
闪存模块的子部分 | 作用 |
主存储器 | 用来存放代码和数据常数(如const类型到的数据) |
信息块 | 分为两个部分:系统存储(启动程序代码)、选项字节(用户选择字节) |
闪存存储器接口寄存器 | 用于控制闪存读写等,是整个闪存模块的控制结构 |
主存储器:地址范围为0x0800 0000~0x0807 FFFF,分为256页(代码中把页或扇区统称为扇区),每页2KB(小/中容量为1K)(F4/ F7/H7一个扇区为16K)。当BOOT0接地,系统将从0x0800 0000地址处开始读取代码(从主存储器启动)
信息块:
(1)系统存储大小为2KB,用来存储ST自带的启动程序,用来串口下载代码。
(2)选项字节大小为16B,一般用于设置内存的写保护、读保护当BOOT0接VCC,BOOT1接GND (串口下载程序) ,系统运行的就是这部分代码
2.FLASH读写过程(F1)
对FLASH的核心操作就是读和写。
FLASH的物理特性:只能写0,不能写1,写1靠擦除。
2.1内存的读取
CPU运行速度比FLASH快得多,STM32F103的FLASH最快访问速度≤24MHz,CPU频率超过这个速度,得加入等待时间,否则读写FLASH可能出错,导致死机等情况。
频率范围 | 等待周期数(LATENCY) |
0 < SYSCLK < 24MHz | 0个等待周期 |
24MHz < SYSCLK ≤ 48MHz | 1个等待周期 |
48MHz < SYSCLK ≤ 72MHz | 2个等待周期 |
正确设置好等待周期后,从地址addr,读取数据(字节为8位,半字为16位,字为32位)
data = *(volatile uint8_t *)addr; /* 读取一个字节数据 */
data = *(volatile uint16_t *)addr; /* 读取一个半字数据 */
data = *(volatile uint32_t *)addr; /* 读取一个字数据 */
将addr强制转换为uintx_t指针,然后取该指针所指向地址的值,即可获得addr地址的数据。
注意: 在进行写或擦除操作时,不能进行代码或数据的读取操作。
2.2闪存的写入
闪存编程是由FPEC(闪存编程和擦除控制器)模块处理的。
写操作有四步:解锁、擦除、写数据、上锁:
- 解锁:将两个特定的解锁序列号(KEY1:0x45670123、KEY2:0xCDEF89AB)依次写入FLASH_KEYR
- 擦除:FLASH物理特性(只能写0,不能写1),所以写FLASH之前需要擦除,将要写入的区域变为0xFFFF。擦除操作分为:页擦除和批量擦除
- 写数据:擦除完成,可以向FLASH写数据,每次只能以16位方式写入
- 上锁:写入数据完成,需要设置FLASH_CR[LOCK]位1,重新上锁,以防数据不小心被修改。
闪存的擦除:
闪存的写入:(注意是写半字)
2.3FLASH接口寄存器(写入 & 擦除相关)
FLASH寄存器 | 作用 |
FLASH_ACR | 用于使能/关闭加速功能(DCEN/ICEN/PRFTEN),且可根据fCPU控制FLASH访问时间(LATENCY) |
FLASH_KEYR | 用来解锁FLASH_CR,需依次写入特定序列(KEY1:0x45670123和KEY2:0xCDEF89AB) |
FLASH_SR | 用来提供正在执行的编程和擦除操作的相关信息(BSY位指示FLASH操作正在进行) |
FLASH_CR | 用于配置和启动FLASH操作(LOCK/STRT/PSIZE/SNB/SER/PG) |
对于H7还有FLASH_CCR 清除与控制寄存器用于清除相关错误。
FLASH_CR各位:
- LOCK:指示FLASH_CR寄存器是否被锁住(1锁 0未锁)
- STRT:用于开始一次擦除操作(1开始)
- PSIZE:用于设置编程宽度(3.3V PSIZE为2)
- SNB:用于选择要擦除的扇区编号 SER:用于选择扇区擦除操作(页擦除置1)
- PG:用于选择编程操作,往FLASH写数据需置1
3.FLASH相关HAL库函数简介(F1/F4/F7/H7)
驱动函数 | 关联寄存器 | 功能描述 |
HAL_FLASH_Unlock(…) | FLASH_KEYR | 用于解锁FLASH_CR的访问 |
HAL_FLASH_Lock(…) | FLASH_CR | 用于锁定FLASH_CR的访问 |
HAL_FLASH_Program(…) | FLASH_CR | 用于FLASH的写入 |
HAL_FLASHEx_Erase(…) | FLASH_CR/AR | 用于大量擦除或擦除指定的闪存扇区 |
FLASH_WaitForLastOperation(…) | FLASH_SR | 等待操作完成 |
FLASH相关结构体:FLASH_EraseInitTypeDef
F1:
uint32_t TypeErase /* 擦除类型 */
uint32_t Banks /* 擦除的bank编号(整片擦除) */
uint32_t PageAddress /* 擦除页面地址 */
uint32_t NbPages /* 擦除的页面数 */
F4/F7/H7:
uint32_t TypeErase
uint32_t Banks
uint32_t Sector
uint32_t NbSectors
uint32_t VoltageRange
4.编程实战
F103ZET6 stmflash_write思路:
每个扇区(页)是2KB,也就是2048个地址
写任何一个地址前,如果该地址的值并不是0xFF,需先擦除再写
- 根据w_addr,确定No.sector号以及w_addr在该sector的偏移
- 根据w_addr和length,确定写入的内容是否跨sector
- 确定好要操作的sector以及sector的地址范围
- 遍历要写的地址区域数据是否都是0xFF,如果都是不用擦除,否则需要先读出保存在buf后擦除
- 把该sector要操作的数据,也写到buf,最后一次性把buf写入到这个对应的sector即可
跨扇区:需要注意偏移(扇区地址 / 扇区中的偏移 / 写入数据的偏移 / 写地址偏移 / 写入长度)
代码:
/*** @brief 从指定地址读取一个半字 (16位数据)* @param faddr : 读取地址 (此地址必须为2的倍数!!)* @retval 读取到的数据 (16位)*/
uint16_t stmflash_read_halfword(uint32_t faddr)
{return *(volatile uint16_t *)faddr;
}
/*** @brief 从指定地址开始读出指定长度的数据* @param raddr : 起始地址* @param pbuf : 数据指针* @param length: 要读取的半字(16位)数,即2个字节的整数倍* @retval 无*/
void stmflash_read(uint32_t raddr, uint16_t *pbuf, uint16_t length)
{uint16_t i;for (i = 0; i < length; i++){pbuf[i] = stmflash_read_halfword(raddr); /* 读取2个字节 */raddr += 2; /* 偏移2个字节 */}
}
/*** @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.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(); /* 上锁 */
}