本文为《物联网实战课》IAP升级章节的终极复习笔记。结合6张核心原理图、10+张代码标注图,彻底搞懂中控和传感器如何协同完成固件升级。小白也能看懂,老手更能加深理解。


📚 目录

  1. 目录

    📚 目录

    1. IAP升级核心概念 {#1}

    2. 内存划分:Flash就是楼盘 {#2}

    3. 升级流程三步骤(附时序图) {#3}

    场景A:升级中控(中控运行Bootloader)

    场景B:升级传感器(中控运行APP,传感器运行Bootloader)

    4. 中控Bootloader代码深度解析 {#4}

    4.1 启动判断:岔路口的选择

    4.2 跳转APP:汇编实现VTOR重定位

    4.3 主循环:处理Modbus命令

    4.4 紧急命令:先回复再复位

    4.5 固件烧录:文件头+数据块

    5. 传感器Bootloader代码深度解析 {#5}

    5.1 传感器升级的特点

    5.2 启动判断与配置信息

    5.3 主循环:响应中控的命令

    5.4 紧急命令:写寄存器触发复位

    5.5 固件烧录:只收数据,不解析映射

    6. 总结:一张图看懂整个升级流程 {#6


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)


升级中控的“二人转”

  1. 上位机发送“进入Bootloader”命令 → 中控Bootloader回复 → 中控保持在Bootloader。

  2. 上位机循环发送文件块 → 中控每收到一块立即烧录并回复。

  3. 发送完毕,上位机发送“进入APP”命令 → 中控写配置信息后复位,启动新APP。

场景B:升级传感器(中控运行APP,传感器运行Bootloader)


升级传感器的“三角恋”

  1. 上位机发送“进入Bootloader”命令(目标传感器)→ 中控APP转发 → 传感器Bootloader回复 → 中控APP回复上位机。

  2. 上位机发送文件块 → 中控APP立即转发 → 传感器烧录并回复 → 中控APP回复上位机。

  3. 发送完毕,上位机发送“进入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请求:

    1. 写单个寄存器(功能码0x06)到升级命令寄存器(地址0x0000),用于触发进入Bootloader或APP。

    2. 写文件记录(功能码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升级方案,无论是中控还是传感器,都能轻松应对。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐