【保姆级·焚天教学】IAP升级实战:从原理到代码,中控与传感器Bootloader全解析
本文为《物联网实战课》IAP升级章节的终极复习笔记。结合6张核心原理图、10+张代码标注图,彻底搞懂中控和传感器如何协同完成固件升级。小白也能看懂,老手更能加深理解。
📚 目录
1. IAP升级核心概念 {#1}
IAP(In-Application Programming)就是在应用中进行编程。通俗点说,就是设备在运行过程中,自己给自己更新程序。
-
Bootloader:相当于设备的“保安室”。它是一个永远不会被刷掉的小程序,负责接收新固件并写入Flash,然后启动新固件。
-
APP:相当于设备的“居民楼”。这是我们真正要更新的业务代码。
-
配置信息:相当于“物业登记处”。记录下次启动是进保安室还是进居民楼。
[请在此处放置第4张图:中控/传感器Bootloader程序流程图]
图1:Bootloader的启动逻辑——根据配置信息选择去向
2. 内存划分:Flash就是楼盘 {#2}

中控(STM32H563)与传感器(STM32F030)的Flash分区
-
中控:2MB Flash
-
Bootloader:256KB(0x08000000 ~ 0x0803FFFF)
-
APP:1784KB(0x08040000 ~ 0x081FDFFF)
-
配置信息:最后8KB(0x081FE000 ~ 0x0801FFFF)
-
-
传感器:256KB Flash
-
Bootloader:128KB(0x08000000 ~ 0x0801FFFF)
-
APP:126KB(0x08020000 ~ 0x0803F7FF)
-
配置信息:最后2KB(0x0803F800 ~ 0x0803FFFF)
-
为什么这么分?
-
Bootloader和APP物理隔离,即使APP刷坏,Bootloader还在,可以重新刷机,防止变砖。
-
配置信息单独一个扇区,方便擦写,不影响代码区。
3. 升级流程三步骤(附时序图) {#3}
升级过程分为三步:① 让目标进入Bootloader → ② 发送固件 → ③ 让目标启动APP
场景A:升级中控(中控运行Bootloader)

升级中控的“二人转”
-
上位机发送“进入Bootloader”命令 → 中控Bootloader回复 → 中控保持在Bootloader。
-
上位机循环发送文件块 → 中控每收到一块立即烧录并回复。
-
发送完毕,上位机发送“进入APP”命令 → 中控写配置信息后复位,启动新APP。
场景B:升级传感器(中控运行APP,传感器运行Bootloader)

升级传感器的“三角恋”
-
上位机发送“进入Bootloader”命令(目标传感器)→ 中控APP转发 → 传感器Bootloader回复 → 中控APP回复上位机。
-
上位机发送文件块 → 中控APP立即转发 → 传感器烧录并回复 → 中控APP回复上位机。
-
发送完毕,上位机发送“进入APP”命令 → 中控转发 → 传感器写配置后复位,启动新APP → 中控回复上位机。
关键点:整个过程是同步阻塞的,每发一块必须等到回复才能发下一块,确保可靠性。
4. 中控Bootloader代码深度解析 {#4}
代码路径:3_程序源码\01_视频配套的源码\9-4-1_IAP升级上机演示\02_中控程序\h5_iap.7z
4.1 启动判断:岔路口的选择
文件:Core/Src/main.c
int main(void)
{
if (isBootloader() && !isNeedToUpdate())
{
extern void start_app(uint32_t vector);
start_app(get_app_vector());
}
// ... 其余初始化(Bootloader本身的初始化)
}

红色箭头指向 if (isBootloader() && !isNeedToUpdate()):
-
isBootloader():判断当前代码是否在Bootloader区域(通过比较函数自身的链接地址与APP起始地址)。 -
isNeedToUpdate():读取配置信息,检查是否需要留在Bootloader升级。-
如果配置信息无效,或
bEnterBootloader == 1,返回1(需要升级)。 -
如果
bEnterBootloader == 0,返回0(直接启动APP)。
-
辅助函数详解:
int isBootloader(void)
{
uint32_t link_addr = (uint32_t)isBootloader;
return (link_addr < APP_LOAD_ADDR); // APP_LOAD_ADDR = 0x08040000
}
int isNeedToUpdate(void)
{
FirmwareInfo tFirmwareInfo;
if (GetLocalFirmwareInfo(&tFirmwareInfo)) return 1;
if (tFirmwareInfo.bEnterBootloader == 1) return 1;
if (tFirmwareInfo.bEnterBootloader == 0) return 0;
return 1;
}
static int GetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
volatile PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
if (ptFlashInfo->file_len == 0xFFFFFFFF) return -1;
*ptFirmwareInfo = *ptFlashInfo;
return 0;
}
uint32_t get_app_vector(void)
{
PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
return ptFlashInfo->load_addr; // 从配置信息读取APP起始地址
}
4.2 跳转APP:汇编实现VTOR重定位
文件:Core/Src/jump.S
start_app PROC
; 设置VTOR = r0 (APP向量表基址)
LDR R1, =0xE000ED08
STR R0, [R1]
; 读取新向量表的第一个字作为MSP,设置主栈指针
LDR R1, [R0]
MOV SP, R1
; 读取新向量表的第二个字作为复位地址,跳转
LDR R1, [R0, #4]
BX R1
ENDP

汇编跳转——VTOR重定位、栈指针设置、跳转
注意:APP工程中必须注释掉默认设置VTOR的代码(如SystemInit中的VTOR赋值),否则APP启动后会再次覆盖VTOR,导致异常向量表混乱。
4.3 主循环:处理Modbus命令
如果启动判断决定留在Bootloader,则进入无限循环,不断接收上位机的Modbus请求。
文件:Core/Src/control.c
for (;;) {
do {
rc = modbus_receive(ctx, query);
} while (rc == 0);
if (rc < 0) continue;
// 1. 处理紧急命令(进入Bootloader/APP)
err = process_emergency_cmd(ctx, query, rc, mb_mapping);
if (err) {
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
continue;
}
// 2. 处理文件块(固件数据)
err = process_file_record(query, rc);
if (err) {
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
continue;
}
modbus_reply(ctx, query, rc, mb_mapping);
}
Bootloader主循环——先紧急命令,再文件块,最后回复
4.4 紧急命令:先回复再复位
函数:process_emergency_cmd(中控自身部分)
if (ptPointMap->channel == 0) // channel 0 表示中控自己
{
if (val == MODBUS_PRIVATE_CMD_ENTER_BOOT) // 0x55
{
if (!isBootloader())
{
// 先回复,再复位
modbus_reply(ctx, msg, msg_len, mb_mapping);
ResetToBootloader();
return -1;
}
return 0;
}
else if (val == MODBUS_PRIVATE_CMD_ENTER_APP) // 0xAA
{
modbus_reply(ctx, msg, msg_len, mb_mapping);
ResetToApplication();
return -1;
}
}
紧急命令处理——先回复上位机,再执行复位(否则无法回复)
复位函数(写配置信息 + 软复位):
void ResetToBootloader(void)
{
FirmwareInfo tFirmwareInfo;
GetLocalFirmwareInfo(&tFirmwareInfo);
tFirmwareInfo.bEnterBootloader = 1; // 标记下次启动进Bootloader
WriteFirmwareInfo(&tFirmwareInfo);
SoftReset();
}
void ResetToApplication(void)
{
FirmwareInfo tFirmwareInfo;
GetLocalFirmwareInfo(&tFirmwareInfo);
tFirmwareInfo.bEnterBootloader = 0; // 标记下次启动进APP
WriteFirmwareInfo(&tFirmwareInfo);
SoftReset();
}

写配置信息后软复位——下次启动根据配置决定去向
4.5 固件烧录:文件头+数据块
函数:process_file_record 中针对中控自身(channel == 0)的 burn_firmware。
if (record_no == 0)
{
// 文件头:解析文件信息,擦除Flash
memcpy(&tFileInfo, &msg[10], sizeof(tFileInfo));
tFileInfo.file_len = BE32toLE32(...);
recv_len = 0;
flash_addr = APP_LOAD_ADDR;
EraseFlash(APP_LOAD_ADDR, tFileInfo.file_len);
EraseFlash(CFG_OFFSET, SECTOR_SIZE);
}
else
{
// 文件块:写入Flash
cur_len = msg[2] - 7;
recv_len += cur_len;
WriteFirmware(&msg[10], cur_len, flash_addr);
flash_addr += cur_len;
}
if (recv_len >= tFileInfo.file_len)
{
// 接收完毕:更新配置信息(bEnterBootloader = 0)
tFirmwareInfo.bEnterBootloader = 0;
tFirmwareInfo.file_len = tFileInfo.file_len;
tFirmwareInfo.load_addr = APP_LOAD_ADDR;
WriteFirmwareInfo(&tFirmwareInfo);
}

固件烧录——文件头擦除,数据块写入,完成后更新配置
关于Modbus Write File Record格式(用于传输固件):

Modbus 0x15功能码格式——File Number区分文件,Record Number 0表示文件头
5. 传感器Bootloader代码深度解析 {#5}
5.1 传感器升级的特点
-
传感器无法直接连接电脑,必须通过中控APP转发命令和固件。
-
传感器作为Modbus从设备,地址为
01H(开关量)、02H(环境监测)、03H(温湿度)。 -
传感器的Bootloader需要响应两种Modbus请求:
-
写单个寄存器(功能码0x06)到升级命令寄存器(地址0x0000),用于触发进入Bootloader或APP。
-
写文件记录(功能码0x15)用于传输固件(File Number = 固件文件号)。
-
5.2 启动判断与配置信息
传感器的Flash划分(参考图2):
-
Bootloader起始:0x08000000
-
APP起始:0x08020000
-
配置信息起始:0x0803F800
启动判断逻辑与中控完全一致,只是地址宏不同:
#define APP_LOAD_ADDR 0x08020000
#define CFG_OFFSET 0x0803F800
int isBootloader(void)
{
uint32_t link_addr = (uint32_t)isBootloader;
return (link_addr < APP_LOAD_ADDR);
}
// isNeedToUpdate、GetLocalFirmwareInfo、get_app_vector 与中控完全相同
5.3 主循环:响应中控的命令
传感器的Bootloader主循环与中控类似,但没有点映射处理,只处理:
-
写寄存器命令(紧急命令)
-
写文件记录命令(固件数据)
伪代码:
for (;;) {
rc = modbus_receive(ctx, query);
if (rc < 0) continue;
// 1. 处理紧急命令(写升级寄存器)
if (is_write_single_register(query) &&
get_register_address(query) == CMD_REG_ADDR) // 0x0000
{
uint16_t val = get_register_value(query);
if (val == CMD_ENTER_BOOT) // 0x55
{
// 先回复,再复位进Bootloader
modbus_reply(ctx, query, rc, mb_mapping);
ResetToBootloader();
continue; // 不会执行到下面的回复
}
else if (val == CMD_ENTER_APP) // 0xAA
{
modbus_reply(ctx, query, rc, mb_mapping);
ResetToApplication();
continue;
}
}
// 2. 处理文件块(Write File Record)
if (is_write_file_record(query) && get_file_number(query) == FILE_NUMBER_FIRMWARE)
{
int err = burn_firmware(query, rc); // 烧录固件
if (err) {
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
} else {
modbus_reply(ctx, query, rc, mb_mapping);
}
continue;
}
// 其他请求按普通寄存器读写处理(但传感器Bootloader一般只处理上述两种)
modbus_reply(ctx, query, rc, mb_mapping);
}
关键点:传感器不需要处理点映射文件,因为点映射只存在于中控。
5.4 紧急命令:写寄存器触发复位
中控APP通过 modbus_write_point 向传感器的升级命令寄存器写入 0x55 或 0xAA。传感器的Bootloader收到后,执行与中控相同的逻辑:先回复,再复位。
if (val == CMD_ENTER_BOOT)
{
modbus_reply(ctx, msg, msg_len, mb_mapping); // 先回复
ResetToBootloader(); // 再复位
}
else if (val == CMD_ENTER_APP)
{
modbus_reply(ctx, msg, msg_len, mb_mapping);
ResetToApplication();
}
ResetToBootloader 和 ResetToApplication 的实现与中控完全一致,只是配置信息地址不同(CFG_OFFSET 已定义为传感器的配置区)。
5.5 固件烧录:只收数据,不解析映射
传感器的 burn_firmware 比中控简单,因为不需要处理点映射文件,只需要处理固件文件。核心逻辑:
static int burn_firmware(uint8_t *msg, uint16_t msg_len)
{
static uint32_t recv_len = 0;
static uint32_t flash_addr = APP_LOAD_ADDR;
static FileInfo tFileInfo;
uint16_t record_no = (msg[6] << 8) | msg[7]; // 记录号
if (record_no == 0)
{
// 文件头:解析文件信息,擦除Flash
memcpy(&tFileInfo, &msg[10], sizeof(tFileInfo));
tFileInfo.file_len = BE32toLE32((uint8_t*)&tFileInfo.file_len);
recv_len = 0;
flash_addr = APP_LOAD_ADDR;
EraseFlash(APP_LOAD_ADDR, tFileInfo.file_len);
EraseFlash(CFG_OFFSET, SECTOR_SIZE);
return 0;
}
else
{
// 文件块:写入Flash
uint16_t cur_len = msg[2] - 7; // 数据长度
recv_len += cur_len;
WriteFirmware(&msg[10], cur_len, flash_addr);
flash_addr += cur_len;
if (recv_len >= tFileInfo.file_len)
{
// 接收完毕:更新配置信息
FirmwareInfo tFirmwareInfo;
memset(&tFirmwareInfo, 0xff, sizeof(tFirmwareInfo));
tFirmwareInfo.bEnterBootloader = 0;
tFirmwareInfo.file_len = tFileInfo.file_len;
tFirmwareInfo.load_addr = APP_LOAD_ADDR;
strcpy((char*)tFirmwareInfo.file_name, (char*)tFileInfo.file_name);
WriteFirmwareInfo(&tFirmwareInfo);
}
return 0;
}
}
注意:传感器的Flash较小,擦除时需注意扇区大小(通常为2KB),但代码逻辑与中控相同。
6. 总结:一张图看懂整个升级流程 {#6}
[请在此处放置第3张图:中控APP任务调度图]
图13:中控APP日常任务——轮询传感器,同时作为升级的“二传手”
[请在此处放置第5张图:上位机程序流程图]
图14:上位机视角——简单三步走
核心要点回顾:
-
Bootloader的职责:启动引导 + 接收固件 + 烧录Flash + 更新配置。
-
中控APP的额外职责:在传感器升级时,透明转发命令和固件。
-
传感器的特殊性:无点映射,只处理固件文件和升级命令。
-
可靠性保障:每发一块必须等待回复,同步操作确保万无一失。
掌握了这些,你就能自己动手实现一套完整的IAP升级方案,无论是中控还是传感器,都能轻松应对。
更多推荐
所有评论(0)