本篇总结
把程序烧录后剩余的内部 FLASH 空间当掉电存储器用,实现数据的擦除、写入与读取。
🔑 KEIL MDK、UART调试、FLASH
硬件平台:本例程基于 W55MH32L-EVB 开发板运行。
采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB
本示例演示 W55MH32 内部 FLASH 的读写操作。W55MH32 芯片内部有一个 FLASH 存储器,主要用于存储代码。编译好的应用程序通过下载器烧录到内部 FLASH 中,由于 FLASH 掉电不丢失,芯片重新上电后内核从内部 FLASH 加载代码运行。
除了用外部下载器读写,W55MH32 在运行时也能对自身内部 FLASH 读写。若程序烧录后还有剩余空间,可像外部 SPI-FLASH 那样利用起来,存储运行时产生的需要掉电保存的数据。内部 FLASH 访问速度远快于外部 SPI-FLASH,常用于紧急状态存储关键记录。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口调试工具:点击下载
硬件连接
内部 FLASH 为片内资源,无需外部连线。测试结果通过 USART1 串口打印,仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC。
| 引脚 | 功能 | 方向 | 模式 |
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\ 下 FLASH 例程目录,核心文件如下:
读写内部FLASH例程/
├── USER/
│ ├── main.c ← FLASH 读写测试 + printf 重定向
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
├── delay/
│ └── delay.c/h ← SysTick 延时函数
└── eeprom/
└── eeprom.c/h ← FLASH 读写封装(WIZFLASH_Write/Read)
Keil MAP 文件中查看代码段和数据段的 FLASH 占用情况内部 FLASH 构成
W55MH32 的内部 FLASH 包含主存储器、系统存储器以及选项字节三个区域:
- 主存储块:存储应用程序代码,按页(Page)划分,每页 2KB。地址从
0x0800 0000 开始,共 512 页(1MB)
- 系统存储器:用户不可访问,出厂时固化了启动代码,负责实现串口/USB/CAN 等 ISP 烧录功能
- 选项字节:配置读写保护、看门狗硬件启动等选项,掉电不丢失
| 模块 | 地址范围 | 大小 |
| 主存储块(页 0) | 0x0800 0000 - 0x0800 07FF | 2KB |
| 主存储块(页 1) | 0x0800 0800 - 0x0800 0FFF | 2KB |
| ...(页 511) | 0x080F F800 - 0x080F FFFF | 2KB |
| 系统存储器 | 0x1FFF F000 - 0x1FFF F7FF | 2KB |
| 选项字节 | 0x1FFF F800 - 0x1FFF F80F | 16B |
对内部 FLASH 的写入过程
写入内部 FLASH 需经过解锁→擦除→写入三个阶段,每个阶段操作不同的寄存器位:
三阶段操作流程
- 解锁:复位后 FLASH_CR 默认上锁。向 FLASH_KEYR 依次写入 KEY1(0x45670123) 和 KEY2(0xCDEF89AB) 解锁
- 页擦除:检查 BSY 位空闲 → 置 PER 位选页擦除 → 向 FLASH_AR 写入页地址 → 置 STRT 位开始 → 等待 BSY 清零
- 写入数据:检查 BSY 位空闲 → 置 PG 位允许编程 → 向目标地址按 16 位写入 → 等待 BSY 清零
重要提示
FLASH 写入前必须先擦除整页,且只能将 1 写成 0,不能将 0 写成 1。每次只能按 16 位(半字)写入。写完后务必调用 FLASH_Lock() 上锁,防止误操作。
步骤1:FLASH 读写封装
本例程通过 eeprom.c/h 将 FLASH 操作封装为 WIZFLASH_Write() 和 WIZFLASH_Read() 两个函数,调用方式和 EEPROM 一样。写入函数内部自动完成解锁、擦除、写入、上锁全过程。
/* 写数据:从 WriteAddr 开始写入 NumToWrite 个半字 */
void WIZFLASH_Write(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)
{
u32 secpos; // 扇区地址
u16 secoff; // 扇区内偏移(半字)
u16 secremain; // 扇区剩余空间
u16 i;
u32 offaddr; // 去掉 0x08000000 后的偏移
if (WriteAddr < WIZ_FLASH_BASE || (WriteAddr >= (WIZ_FLASH_BASE + 1024 * WIZ_FLASH_SIZE))) return;
FLASH_Unlock(); // 解锁
offaddr = WriteAddr - WIZ_FLASH_BASE;
secpos = offaddr / WIZ_SECTOR_SIZE; // 扇区号
secoff = (offaddr % WIZ_SECTOR_SIZE) / 2; // 扇区内偏移
secremain = WIZ_SECTOR_SIZE / 2 - secoff; // 扇区剩余
if (NumToWrite <= secremain) secremain = NumToWrite;
while (1)
{
/* 读出整个扇区到缓存 */
WIZFLASH_Read(secpos * WIZ_SECTOR_SIZE + WIZ_FLASH_BASE, WIZFLASH_BUF, WIZ_SECTOR_SIZE / 2);
/* 检查待写区域是否全为 0xFFFF(擦除态) */
for (i = 0; i < secremain; i++)
{
if (WIZFLASH_BUF[secoff + i] != 0XFFFF) break;
}
if (i < secremain) // 需要擦除
{
FLASH_ErasePage(secpos * WIZ_SECTOR_SIZE + WIZ_FLASH_BASE);
for (i = 0; i < secremain; i++)
{
WIZFLASH_BUF[i + secoff] = pBuffer[i]; // 新数据替换
}
WIZFLASH_Write_NoCheck(secpos * WIZ_SECTOR_SIZE + WIZ_FLASH_BASE, WIZFLASH_BUF, WIZ_SECTOR_SIZE / 2);
}
else // 无需擦除,直接写
{
WIZFLASH_Write_NoCheck(WriteAddr, pBuffer, secremain);
}
if (NumToWrite == secremain)
break; // 写完
else // 跨扇区继续
{
secpos++;
secoff = 0;
pBuffer += secremain;
WriteAddr += secremain * 2;
NumToWrite -= secremain;
if (NumToWrite > (WIZ_SECTOR_SIZE / 2))
secremain = WIZ_SECTOR_SIZE / 2;
else
secremain = NumToWrite;
}
};
FLASH_Lock(); // 上锁
}
/* 读数据:从 ReadAddr 读出 NumToRead 个半字 */
void WIZFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead)
{
u16 i;
for (i = 0; i < NumToRead; i++)
{
pBuffer[i] = WIZFLASH_ReadHalfWord(ReadAddr);
ReadAddr += 2;
}
}
为什么写入要先读再擦
FLASH 擦除的最小单位是一整页(2KB),不能只擦几个字节。若目标地址所在页已有其他数据,直接擦除会丢失它们。所以 WIZFLASH_Write() 先把整页读到缓存,修改要写的位置后再整页写回,实现"按需修改"。
步骤2:写入与读回测试
主循环每 1 秒执行一次:把字符串 "WIZnet FLASH TEST" 写入 0x08008000,再读回并打印,验证读写一致。
/* 要写入 FLASH 的字符串 */
const u8 TEXT_Buffer[] = {"WIZnet FLASH TEST"};
#define SIZE sizeof(TEXT_Buffer) // 数组长度(含结束符)
#define FLASH_SAVE_ADDR 0X08008000 // 保存地址(避开程序占用区)
int main(void)
{
uint8_t datatemp[SIZE], i;
RCC_ClocksTypeDef clocks;
delay_init();
UART_Configuration(115200);
RCC_GetClocksFreq(&clocks);
printf("SYSCLK: ...\n");
printf("FLASH EEPROM Tset.\n");
while (1)
{
/* 写入字符串到 FLASH */
WIZFLASH_Write(FLASH_SAVE_ADDR, (u16 *)TEXT_Buffer, SIZE);
/* 从 FLASH 读回 */
WIZFLASH_Read(FLASH_SAVE_ADDR, (u16 *)datatemp, SIZE);
/* 打印读回的数据 */
for (i = 0; i < SIZE; i++)
{
printf("%s\n", datatemp);
}
memset(datatemp, 0x00, sizeof(datatemp));
delay_ms(1000); // 延时 1 秒后重复
}
}
如果你想自定义
- 更换保存地址 → 修改
FLASH_SAVE_ADDR,必须避开程序占用区域(通过 .map 文件确认程序结束地址)
- 更换写入内容 → 修改
TEXT_Buffer 数组内容,SIZE 会自动跟随
- 调整循环周期 → 修改
delay_ms(1000) 参数
运行验证
打开 WIZ UartTool V1.0,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。程序运行后串口打印时钟信息和测试提示,然后每秒循环写入并读回一次字符串。预期输出如下:
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
FLASH EEPROM Tset.
WIZnet FLASH TEST
WIZnet FLASH TEST
WIZnet FLASH TEST
...
说明:每次循环将 "WIZnet FLASH TEST" 写入 FLASH_SAVE_ADDR,再读回并打印。由于 printf 在 for 循环内,每次循环打印 SIZE(19)遍,之后延时 1 秒重复。若读回内容与写入一致,说明 FLASH 读写功能正常。
WIZ UartTool 串口输出 — 内部 FLASH 读写测试结果常见问题
Q: 擦除/写入后程序跑飞或复位
测试地址选在了程序代码区,擦除了正在运行的程序。通过 .map 文件确认程序结束地址,选择其后的空闲页。
Q: FLASH_ErasePage 返回 FLASH_BUSY 一直不完成
检查 FLASH 时钟是否使能。若之前操作异常导致控制器锁死,可尝试重新解锁(FLASH_Unlock())后再操作。
Q: 写入数据后读取为 0xFFFF
0xFFFF 是擦除后的默认值,说明写入未成功。确认擦除后未上锁就直接写入,且写入前调用了 FLASH_Unlock()。
Q: 写入 32 位数据只成功一半
FLASH 每次只能按 16 位写入。FLASH_ProgramWord() 内部分两次半字写入,若中途被中断打断可能导致只写了一半。确保写入过程不被中断,或关闭中断后操作。
Q: 选项字节区域无法修改
选项字节需用专用的 FLASH_UnlockOptionBytes() 解锁(写入 OPTKEY1/OPTKEY2),与主存储解锁的 KEY 不同。
总结
本文基于 W55MH32 实现了内部 FLASH 的读写操作:
- 三阶段写入:解锁(KEYR 写 KEY1/KEY2)→ 页擦除(PER+STRT)→ 按半字写入(PG),每步检查 BSY 位
- 读取零开销:读取无需解锁,直接指针访问目标地址,速度与访问 RAM 同级
- 空间规划:通过 .map 文件确认程序占用范围,选择空闲页存储数据,避免覆盖程序
- EEPROM 模拟:封装 WIZFLASH_Write/Read 后可像 EEPROM 一样存取掉电保存数据,替代外部存储