问题现象

用户报告了一个诡异的现象:在 Bootloader 模式下刷固件时:

  • App_B.bin:成功,设备正常进入 App_B
  • App_A.bin:上位机显示”升级传输完成”,但设备实际没有进入 App,仍停留在 Bootloader

同时,上位机界面的”当前运行槽位”和”固件”两项都显示为”未知”。

日志回放

以下是一次刷 App_A.bin 失败的真实日志(关键片段):

[18:26:43] 准备升级,模式 本地升级。
[18:26:43] 警告: 未读取到当前运行槽位,将按所选镜像继续升级。
[18:26:43] 所选镜像槽:A。
[18:26:43] 镜像复位向量:0x08008145。
[18:26:43] 固件: App_A.bin
[18:26:43] 固件大小: 26952 bytes (0x00006948)
[18:26:44] 打开串口 COM5,波特率 115200。
[18:26:44] TX 解锁: 0A060030A55A73D5
[18:26:49] 警告: 解锁 未在 5 秒内收到回包,继续尝试后续流程。
[18:26:49] TX 进入 Bootloader: 0A0600310005197D
[18:26:54] 警告: 进入 Bootloader 未在 5 秒内收到回包,继续尝试后续流程。
[18:26:56] 等待 Bootloader YMODEM 握手 ('C')...
[18:26:57] 已收到 Bootloader 握手字符 'C'。
[18:26:57] YMODEM 头包已确认。
...(传输过程正常,100% 完成)...
[18:27:01] 最终空包已确认。
[18:27:01] 升级传输完成,设备应完成校验并自动复位回 App。
[18:27:01] 流程结束。

从日志看,YMODEM 传输 100% 完成,头包、数据包、EOT、最终空包全部确认。但设备没有复位进入 App。

定位分析

分析一:为什么运行槽位显示”未知”

上位机通过 Modbus 读取寄存器 0x005AREG_DIAG_RUNNING_SLOT)获取当前运行槽位。该寄存器由 App 层的 G780s Modbus 从站提供:

// G780s.c — App 运行态
case REG_DIAG_RUNNING_SLOT:  // 0x005A
    return Upgrade_GetRunningSlot();

Upgrade_GetRunningSlot() 根据 SCB->VTOR 判断当前运行在 A 槽还是 B 槽。

但当设备进入 Bootloader 后,USART3 不再运行 App 的 Modbus 从站逻辑,而是被 Bootloader 接管用于 YMODEM 收发。此时上位机去读 0x005A 不会得到正常回包,因此显示为”未读取”或”未知”。

这不是 Bug,而是 Bootloader 态的预期行为——设备不在 App 中,自然无法响应 App 层的 Modbus 查询。

分析二:为什么 App_A 传输成功但设备不启动

这是真正的 Bug 所在。问题出在 Bootloader 和上位机对”目标槽位”的理解不一致。

上位机侧:YMODEM 头包中传递的信息是:

  • 文件名
  • 文件大小
  • CRC32
  • target_fw_version
  • SHA256

没有”目标槽位 A/B”这个字段。 上位机虽然允许用户选择 App_A.binApp_B.bin,但这个选择只影响”推荐哪个镜像文件”,不会通过 YMODEM 协议传给 Bootloader。

Bootloader 侧:在 BootFlash_BeginImage() 中,BootFlash_SelectDownloadSlot() 自行决定写入目标:

// boot_flash.c
runtime->transfer_slot = BootFlash_SelectDownloadSlot(runtime);

选择策略是:

  1. 读取 boot_control.confirmed_slotboot_control.active_slot
  2. 找到当前认为”稳定可运行”的槽位
  3. 选择另一个槽位作为下载目标

也就是说,Bootloader 的槽位选择与上位机选择了哪个镜像完全无关

根因链条

假设当前 Bootloader 认为”稳定槽”为 A,则下载目标固定为 B:

上位机选择 App_A.bin → Bootloader 写入 B 槽 → 向量表校验失败 → 停留 Bootloader
上位机选择 App_B.bin → Bootloader 写入 B 槽 → 向量表校验通过 → 进入 App_B

App_A.bin 的复位向量位于 A 槽地址范围(如 0x08008145),但实际被写入了 B 槽。Bootloader 最终校验时,按”B 槽镜像”去校验向量表合法性:

// boot_verify.c
uint8_t Upgrade_IsAppVectorValid(uint32_t app_base_addr)
{
    uint32_t app_reset = *(__IO uint32_t *)(app_base_addr + 4);
    // 复位向量必须指向 App 区域内
    if (app_reset < app_base_addr ||
        app_reset >= (app_base_addr + UPGRADE_APP_MAX_SIZE))
        return 0u;
    return 1u;
}

App_A.bin 的 ResetHandler 地址在 A 槽范围,不在 B 槽范围,因此校验失败,Bootloader 不放行,设备继续停留。

代码对应关系

上位机侧

文件关键逻辑
LocalUpgradeViewModel.cs准备升级、读取运行槽位、输出日志
RunningSlotProtocol.cs0x005A 寄存器,Bootloader 态失败
YModemProtocol.cs发送 YMODEM 头包/数据包,不传目标槽位

设备 App 侧

文件关键逻辑
G780s.cApp 态提供 0x005A 运行槽位寄存器
Upgrade.cUpgrade_GetRunningSlot() 根据 VTOR 判断

Bootloader 侧

文件关键逻辑
boot_flash.cBootFlash_SelectDownloadSlot() 决定写入槽位
boot_main.c升级完成后最终校验和跳转
boot_verify.cCRC32/SHA256/向量表放行校验
ymodem.c解析 YMODEM 头包,不解析目标槽位

最终结论

  1. “运行槽位显示未知”是 Bootloader 态的预期行为,设备不在 App 中自然无法响应
  2. 真正的问题是上位机和 Bootloader 对”目标槽位”的理解不一致
  3. 上位机允许用户选择 App_A.bin / App_B.bin,但这个选择不影响 Bootloader 的写入目标
  4. Bootloader 自行决定写入哪个槽位,与镜像文件名无关
  5. 当 Bootloader 决定写入 B 槽时,发送 App_B.bin 可以通过校验,发送 App_A.bin 会因向量表不匹配而失败

处理状态

本次仅完成根因定位与分析,未修改任何代码。后续修复方案待定,可选方向包括:

  • 方案 A:在 YMODEM 头包中增加目标槽位字段,让 Bootloader 按上位机指令写入
  • 方案 B:在 Bootloader 校验阶段,允许向量表落在任一槽位范围(放宽校验,但风险增加)
  • 方案 C:上位机在 Bootloader 模式下先通过其他方式获取 Bootloader 的下载目标槽位,再自动匹配对应镜像文件