IAP-应用编程
更新于 2026年9月23日
本篇总结
使用 IAP,通过板载按键实现固件在线升级与跳转运行。
硬件平台:本例程基于 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 即可。
| 引脚 | 功能 | 方向 | 模式 |
|---|---|---|---|
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
| PG6 | USER 按键 | 输入 | 上拉输入 (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!

后操作按键:
- 长按 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!。




常见问题
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 观察接收与升级过程