全部例程

FLASH-内部读写

W55MH32

更新于 2026年9月23日

本篇总结

把程序烧录后剩余的内部 FLASH 空间当掉电存储器用,实现数据的擦除、写入与读取。

🔑 KEIL MDK、UART调试、FLASH

硬件平台:本例程基于 W55MH32L-EVB 开发板运行。

采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB

本示例演示 W55MH32 内部 FLASH 的读写操作。W55MH32 芯片内部有一个 FLASH 存储器,主要用于存储代码。编译好的应用程序通过下载器烧录到内部 FLASH 中,由于 FLASH 掉电不丢失,芯片重新上电后内核从内部 FLASH 加载代码运行。
除了用外部下载器读写,W55MH32 在运行时也能对自身内部 FLASH 读写。若程序烧录后还有剩余空间,可像外部 SPI-FLASH 那样利用起来,存储运行时产生的需要掉电保存的数据。内部 FLASH 访问速度远快于外部 SPI-FLASH,常用于紧急状态存储关键记录。

前置工具准备

开始本例程前,请确认以下工具已安装:

硬件连接

内部 FLASH 为片内资源,无需外部连线。测试结果通过 USART1 串口打印,仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC。

引脚功能方向模式
PA9USART1_TX开发板 → PC复用推挽输出 (AF_PP)
PA10USART1_RXPC → 开发板浮空输入 (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 07FF2KB
主存储块(页 1)0x0800 0800 - 0x0800 0FFF2KB
...(页 511)0x080F F800 - 0x080F FFFF2KB
系统存储器0x1FFF F000 - 0x1FFF F7FF2KB
选项字节0x1FFF F800 - 0x1FFF F80F16B

对内部 FLASH 的写入过程

写入内部 FLASH 需经过解锁→擦除→写入三个阶段,每个阶段操作不同的寄存器位:

三阶段操作流程

  1. 解锁:复位后 FLASH_CR 默认上锁。向 FLASH_KEYR 依次写入 KEY1(0x45670123) 和 KEY2(0xCDEF89AB) 解锁
  2. 页擦除:检查 BSY 位空闲 → 置 PER 位选页擦除 → 向 FLASH_AR 写入页地址 → 置 STRT 位开始 → 等待 BSY 清零
  3. 写入数据:检查 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 一样存取掉电保存数据,替代外部存储

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板

Andy

W5500 技术支持专家