全部例程

IAP-应用编程

W55MH32

更新于 2026年9月23日

本篇总结

使用 IAP,通过板载按键实现固件在线升级与跳转运行。

🔑 KEIL MDK、UART调试、IAP、FLASH

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

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

本示例演示实现 W55MH32 的 IAP 引导加载程序(Bootloader):通过 USART1 接收用户应用程序(APP)的 .bin 文件并写入 Flash,再跳转执行。
W55MH32 板载 1 个 USER 按键(PG6),本例程采用单键长短按识别:长按 PG6 >2s 触发固件更新(Copy To FLASH),短按 PG6 触发跳转运行 APP(Run FLASH APP)。

前置工具准备

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

  • WIZ UartTool V1.0:串口调试工具,用于发送 APP 的 .bin 文件。点击下载

硬件连接

USART1 的 TX(PA9) 和 RX(PA10) 通过板载 USB 转串口芯片连接到 DEBUG USB 口;USER 按键 PG6 一端接 GND,内部上拉,按下拉低。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC 即可。

引脚功能方向模式
PA9USART1_TX开发板 → PC复用推挽输出 (AF_PP)
PA10USART1_RXPC → 开发板浮空输入 (IN_FLOATING)
PG6USER 按键输入上拉输入 (IPU)

工程结构

工程位于 SDK 内 1.SDK\ModuleDemo\IAP\,核心文件如下:

IAP/
├── USER/
│   ├── main.c            ← 主流程:时钟/串口/NVIC初始化 + 接收检测 + 按键扫描 + IAP更新/跳转
│   ├── key.c/h           ← 单键长短按识别(PG6)
│   ├── iap.c/h           ← IAP 写 Flash 与跳转执行
│   ├── w55mh32_it.c/h    ← 中断服务函数(USART1_IRQHandler)
│   ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│   └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
    └── delay/
        └── delay.c/h      ← SysTick 延时函数

IAP 原理简述

W55MH32 的内部 Flash 起始于 0x08000000,程序文件从此地址开始写入。IAP 的核心思路是把 Flash 分成两部分:Bootloader(负责接收升级、写入、跳转)存放在低地址区,APP(真正的功能代码)存放在偏移 0x10000(64KB)之后的位置。上电后先运行 Bootloader,根据按键决定是写入新固件还是跳转到 APP 执行。

由于 APP 的中断向量表不再位于 0x08000000,需要将其偏移到 APP 起始地址(通过 VTOR 寄存器设置)。同时 APP 工程在 Keil 中要将 IROM1 的 Start 改为 0x08010000,并通过 fromelf 工具生成 .bin 文件供 IAP 升级使用。

IAP 程序的两条硬性要求

  • 新程序必须在 Bootloader 之后的某个偏移量为 x 的地址开始;
  • 必须将新程序的中断向量表相应移动,移动的偏移量为 x。

步骤1:按键模块 — key.h 引脚与宏定义

文件:USER/key.h — 将按键宏统一指向板载 USER 键 PG6,并定义长按/短按返回值。

#ifndef __KEY_H
#define __KEY_H

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "delay.h"
#include "w55mh32.h"

/* 板载 USER 按键:PG6(一端接 GND,内部上拉,按下拉低) */
#define KEY_PIN    GPIO_Pin_6
#define KEY_GROUP  GPIOG
#define KEY_PERIPH RCC_APB2Periph_GPIOG

#define KEY GPIO_ReadInputDataBit(KEY_GROUP, KEY_PIN)

#define KEY1_PRES 1   /* 长按 PG6 (>2s):更新固件(Copy To FLASH) */
#define KEY2_PRES 2   /* 短按 PG6:运行 APP(Run FLASH APP) */

void    KEY_Init(void);
uint8_t KEY_Scan(void);

#endif

步骤2:KEY_Init() 与 KEY_Scan() — 单键长短按识

文件:USER/key.c — KEY_Init() 将 PG6 配置为上拉输入;KEY_Scan() 检测按下后每 10ms 自增计时,累计到 200(2s)即判定为长按,释放后据此返回 KEY1_PRES 或 KEY2_PRES。

#include "key.h"

void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(KEY_PERIPH, ENABLE);

    GPIO_InitStructure.GPIO_Pin  = KEY_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(KEY_GROUP, &GPIO_InitStructure);
}

/*
 * 单按键(PG6)长短按识别:
 *   长按 (>2s) -> KEY1_PRES:更新固件(Copy To FLASH)
 *   短按       -> KEY2_PRES:运行 APP(Run FLASH APP)
 * 板载仅一个 USER 按键,故用长短按区分两种操作。
 */
uint8_t KEY_Scan(void)
{
    uint16_t t = 0;

    if (KEY == 0)
    {
        delay_ms(10);   /* de-dithering */
        if (KEY == 0)
        {
            /* 统计按键持续按下的时长,达到 2s 即可判定为长按 */
            while (KEY == 0)
            {
                delay_ms(10);
                t++;
                if (t >= 200)   /* 200 * 10ms = 2s */
                {
                    break;
                }
            }
            /* 等待按键释放 */
            while (KEY == 0);

            if (t >= 200)
            {
                return KEY1_PRES;   /* 长按:更新固件 */
            }
            else
            {
                return KEY2_PRES;   /* 短按:运行 APP */
            }
        }
    }
    return 0;
}

如果你想自定义

  • 调整长按阈值 → 修改 if (t >= 200) 中的数值,如改为 100 即为 1s 长按
  • 换用其他按键引脚 → 同步修改 key.h 中的 KEY_PIN / KEY_GROUP / KEY_PERIPH 三个宏

步骤3:NVIC 与串口配置

文件:USER/main.c — NVIC_Configuration() 使能 USART1 中断(抢占优先级 0、子优先级 0);UART_Configuration() 开启 USART1 与 GPIOA 时钟,PA9 复用推挽、PA10 浮空输入,配置 115200 / 8N1 / 无流控,并使能 RXNE 中断。

void NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;

    NVIC_InitStructure.NVIC_IRQChannel                   = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

void UART_Configuration(uint32_t bound)
{
    GPIO_InitTypeDef  GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    /* PA9: USART1_TX → 复用推挽输出 */
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* PA10: USART1_RX → 浮空输入 */
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* USART 参数:115200 / 8N1 / 全双工 */
    USART_InitStructure.USART_BaudRate            = bound;
    USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits            = USART_StopBits_1;
    USART_InitStructure.USART_Parity              = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;

    USART_Init(USART_TEST, &USART_InitStructure);
    USART_ITConfig(USART_TEST, USART_IT_RXNE, ENABLE);
    USART_Cmd(USART_TEST, ENABLE);
}

与普通阻塞收发不同,这里通过 USART_ITConfig(..., USART_IT_RXNE, ENABLE) 使能接收非空中断,实现非阻塞数据接收——CPU 在等待固件下发时仍可扫描按键。

步骤4:USART1_IRQHandler — 中断接收固件到 0x20001000

文件:USER/w55mh32_it.c — RXNE 标志置位时读取 DR 取得字节,若计数器未溢出(< 32KB)则写入缓冲区并自增 USART_RX_CNT,供主循环判断接收完成。接收缓冲区通过 __attribute__((at(0X20001000))) 固定到 SRAM 地址 0x20001000,容量 32KB。

void USART1_IRQHandler(void)
{
    uint8_t res;
    if (USART_GetFlagStatus(USART_TEST, USART_FLAG_RXNE))
    {
        res = USART_ReceiveData(USART_TEST);
        if (USART_RX_CNT < USART_REC_LEN)
        {
            USART_RX_BUF[USART_RX_CNT] = res;
            USART_RX_CNT++;
        }
    }
}

主循环通过对比 oldcount 与 USART_RX_CNT 判断数据是否接收完毕:若两次扫描值相同说明已停止接收,即可认定固件下发完成,避免使用固定超时。

步骤5:main.c 主流程

文件:USER/main.c | 函数:main() — 初始化延时/串口/NVIC → 打印时钟与操作提示 → 主循环:检测接收完成 → 每 100ms 扫描按键 → 长按校验复位向量后调用 IAP_Write_Appbin 写 Flash,短按校验后调用 IAP_Load_App 跳转执行。

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "delay.h"
#include "w55mh32.h"
#include "key.h"
#include "iap.h"

USART_TypeDef *USART_TEST = USART1;
#define USART_REC_LEN 32 * 1024 //Define the maximum number of received bytes 32K

void UART_Configuration(uint32_t bound);
void NVIC_Configuration(void);

uint16_t USART_RX_STA = 0; //receive status flag
uint16_t USART_RX_CNT = 0; //Number of bytes received
uint8_t  USART_RX_BUF[USART_REC_LEN] __attribute__((at(0X20001000)));

int main(void)
{
    uint8_t  key;
    uint16_t oldcount = 0; //The old serial port receives the data value.
    uint16_t applenth = 0; //The length of the received app code

    RCC_ClocksTypeDef clocks;

    delay_init();
    UART_Configuration(115200);
    NVIC_Configuration();
    RCC_GetClocksFreq(&clocks);

    printf("\n");
    printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
           (float)clocks.SYSCLK_Frequency / 1000000, (float)clocks.HCLK_Frequency / 1000000,
           (float)clocks.PCLK1_Frequency / 1000000, (float)clocks.PCLK2_Frequency / 1000000, (float)clocks.ADCCLK_Frequency / 1000000);

    printf("IAP Bootloader Test.\n");
    printf("Long  press PG6: Copy To FLASH (update firmware)\n");
    printf("Short press PG6: Run FLASH APP\n");

    KEY_Init();
    while (1)
    {
        if (USART_RX_CNT)
        {
            if (oldcount == USART_RX_CNT)
            {
                applenth     = USART_RX_CNT;
                oldcount     = 0;
                USART_RX_CNT = 0;
                printf("User program reception complete!\r\n");
                printf("code length:%dBytes\r\n", applenth);
            }
            else
            {
                oldcount = USART_RX_CNT;
            }
        }

        delay_ms(100);

        key = KEY_Scan();
        if (key == KEY1_PRES)
        {
            if (applenth)
            {
                printf("Start updating firmware...\r\n");
                printf("Copying To FLASH...\n");
                if (((*(vu32 *)(0X20001000 + 4)) & 0xFF000000) == 0x08000000)  //Determine whether it is 0X08XXXXXX.
                {
                    IAP_Write_Appbin(FLASH_APP1_ADDR, USART_RX_BUF, applenth); //Update Flash Code
                    printf("Copy APP Successed!!\n");
                    printf("Firmware update complete!\r\n");
                }
                else
                {
                    printf("Illegal FLASH APP!  \n");
                    printf("Non-flash applications!\r\n");
                }
            }
            else
            {
                printf("There is no firmware that can be updated!\r\n");
                printf("No APP!\n");
            }
        }

        if (key == KEY2_PRES)
        {
            printf("Start executing FLASH user code!!\r\n");
            if (((*(vu32 *)(FLASH_APP1_ADDR + 4)) & 0xFF000000) == 0x08000000) //Determine whether it is 0X08XXXXXX.
            {
                IAP_Load_App(FLASH_APP1_ADDR);                                 //Execute FLASH APP code
            }
            else
            {
                printf("Non-FLASH applications cannot be executed!\r\n");
                printf("Illegal FLASH APP!\n");
            }
        }
    }
}

两次校验逻辑:写入前检查缓冲区首字(复位向量)高 8 位是否为 0x08(Flash 区域),跳转前检查目标地址 FLASH_APP1_ADDR 处的复位向量是否合法,防止误写或跳飞。

如果你想自定义

  • 改 APP 起始地址 → 修改 iap.h 中的 FLASH_APP1_ADDR 宏,同时同步修改 APP 工程的 Keil Target 设置(IROM1 Start)和中断向量表偏移
  • 改缓冲区大小 → 修改 USART_REC_LEN 宏定义,注意不得超过 SRAM 容量
  • 换用其他通信口 → 同步修改 GPIO 引脚和对应的时钟使能宏,如改用 USART2 需注意其挂在 APB1

步骤6:printf 重定向

重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。

int SER_PutChar(int ch)
{
    while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
    USART_SendData(USART_TEST, (uint8_t)ch);

    return ch;
}

int fputc(int c, FILE *f)
{
    /* Place your implementation of fputc here */
    /* e.g. write a character to the USART */
    if (c == '\n')
    {
        SER_PutChar('\r');
    }
    return (SER_PutChar(c));
}

运行验证

打开 WIZ UartTool V1.0,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。开发板复位后串口应打印时钟信息与操作提示。通过串口发送 APP 的 .bin 文件,待出现 User program reception complete!

WIZ UartTool 串口输出 — IAP Bootloader 启动与接收提示

后操作按键:

  • 长按 PG6 >2s(KEY1_PRES):程序检查接收缓冲区数据。若复位向量合法(0x08XXXXXX),打印 Copy APP Successed!! 并写入 Flash;若非法打印 Illegal FLASH APP!;若未接收数据提示 No APP!。
  • 短按 PG6(KEY2_PRES):程序检查目标地址 FLASH_APP1_ADDR。若地址合法,跳转执行用户程序;否则提示 Non-FLASH applications cannot be executed!。
WIZ UartTool 串口输出 — KEY1 长按复制 APP 到 Flash
WIZ UartTool 串口输出 — Copy APP Successed 写入 Flash 成功
WIZ UartTool 串口输出 — KEY2 短按跳转执行用户程序
WIZ UartTool 串口输出 — 用户程序运行结果


常见问题

Q: 编译报错 "cannot open source file w55mh32.h"

检查 Project → Options → C/C++ → Include Paths,确保 SDK 的 Libraries 目录已添加。

Q: 长按 PG6 提示 "No APP!"

说明接收缓冲区为空,串口未成功下发 .bin 文件。确认 WIZ UartTool 已打开串口、文件发送完成且串口打印了 "User program reception complete!"。

Q: 长按 PG6 提示 "Illegal FLASH APP!"

下发的 .bin 文件复位向量地址不在 0x08XXXXXX 范围。检查 APP 工程的 IROM1 Start 是否设置为 0x08010000,以及 fromelf 是否正确生成了 .bin 文件(而非 .hex)。

Q: 短按 PG6 跳转后程序跑飞或无反应

APP 的中断向量表偏移未正确设置。在 APP 工程中需调用 sys_nvic_set_vector_table 或直接设置 SCB->VTOR,偏移量应与 APP 起始地址相对 0x08000000 的偏移一致(0x10000)。

Q: 串口输出乱码或无输出

先看串口打印的时钟频率是否正常(SYSCLK 应为 72.0Mhz)。若频率异常,检查 system_w55mh32.c 中 HSE_VALUE 是否与实际晶振一致;若无任何输出,确认 COM 口号正确、下载后已按 RESET 键。

总结

本文基于 W55MH32 实现了 IAP 在线升级 Bootloader:

  • Bootloader 通过 USART1(PA9/PA10,115200 / 8N1)以中断方式接收 APP 的 .bin 文件,存入 SRAM 0x20001000 处的 32KB 缓冲区
  • 单键长短按:长按 PG6 >2s 触发固件更新(校验复位向量后写 Flash),短按 PG6 触发跳转运行 APP(校验目标地址后执行)
  • 两次合法性校验:写入前检查缓冲区首字高 8 位是否为 0x08,跳转前检查目标地址复位向量是否合法,防止误写或跳飞
  • 重写 fputc() 实现 printf 重定向,借助 WIZ UartTool V1.0 观察接收与升级过程

下载本章例程

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

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