摘要

前面几篇文章主要站在 STM32 BootLoader 这一侧讲:

  • APP 地址偏移和跳转
  • 串口 YMODEM 升级
  • W5500 网口 YMODEM 升级
  • F103、F407、G473 的 Flash 擦写
  • 固件头、CRC、版本号、防回滚
  • 升级状态机、错误码、异常恢复
  • A/B 分区和失败回滚
  • 固件加密、签名校验、防篡改

这些逻辑都在下位机里。

但真正做项目时,只写 BootLoader 还不够。现场升级一定要有一个上位机工具,负责选择固件、连接设备、进入升级模式、发送固件、显示进度、显示错误码、保存日志。

很多 BootLoader 调试卡住,并不是 Flash 写错了,而是上位机和 BootLoader 的节奏没对上:

BootLoader 在等 C,上位机在等 ACK
上位机发的是 app.bin,BootLoader 要的是带固件头的包
TCP 一次 send 1029 字节,下位机 recv 分成几段
升级失败只显示一句失败,没有错误码
设备复位后端口重连没处理
固件已经写完,但上位机太早关闭连接

这篇专门把上位机升级工具的联调流程拆开讲。重点不是做一个漂亮界面,而是把升级链路跑稳。

目录

1. 上位机工具到底负责什么

BootLoader 负责接收、校验、擦写和跳转。

上位机负责把升级动作组织起来。

一个能用于现场的上位机工具,至少要做这些事:

选择固件文件
解析固件头
显示固件版本和目标硬件
连接设备
读取设备当前版本
让设备进入 BootLoader
等待设备进入升级状态
发送固件
显示传输进度
显示 BootLoader 返回的阶段和错误码
升级完成后等待设备复位
重新连接 APP
确认新版本是否运行
保存升级日志

调试阶段可以先做得简单:

选择串口
选择固件
点击开始升级
打印日志

但产品阶段不能只剩一个发送按钮。

用户升级失败时,必须能知道失败位置:

固件头检查失败
目标板卡不匹配
版本号过低
擦除 Flash 失败
第 128 包发送失败
CRC 校验失败
签名校验失败
设备复位后没有上线

这些信息不一定都要给最终用户看,但上位机日志里必须有。

2. 推荐的工具界面结构

上位机工具不用一开始就做得很花。

推荐先按功能分成几个区域。

+--------------------------------------------------+
| 连接设置                                         |
| 串口号 波特率  /  IP 端口  连接  断开            |
+--------------------------------------------------+
| 固件信息                                         |
| 文件路径  选择固件                               |
| 版本号    目标芯片  目标板卡  APP 地址  大小     |
+--------------------------------------------------+
| 设备信息                                         |
| 当前模式  当前版本  BootLoader版本  设备ID       |
+--------------------------------------------------+
| 升级控制                                         |
| 进入BootLoader  开始升级  取消升级               |
| 进度条  当前阶段  错误码                         |
+--------------------------------------------------+
| 日志                                             |
| 时间 方向 内容                                   |
+--------------------------------------------------+

最重要的是:

固件信息要在发送前显示出来
设备信息要在升级前读取出来
错误信息要能对应到 BootLoader 的错误码

不要让用户盲发。

一个最小可用界面可以只有这些按钮:

连接
选择固件
进入升级
开始升级
保存日志

调试阶段先把这几个按钮跑稳,再考虑批量升级、自动扫描设备、权限管理、云端固件等功能。

3. 固件选择和本地校验

上位机发送固件前,必须先检查固件文件。

不要让 BootLoader 成为第一道检查。

3.1 不要直接发送任意 bin

如果前面已经设计了固件头,那么上位机选择的文件应该是打包后的固件包:

app.bin          原始 APP 镜像
app_secure.fw    带固件头、CRC、版本、签名的升级包

现场升级时应该发送:

app_secure.fw

不是直接发送:

app.bin

因为 BootLoader 需要从固件头里知道:

目标芯片
目标板卡
APP 起始地址
APP 大小
版本号
CRC 或 Hash
签名信息

3.2 上位机先解析固件头

选择文件后,上位机先读取前面的固件头。

示例字段:

typedef struct
{
    uint32_t magic;
    uint16_t header_size;
    uint16_t header_version;
    uint32_t target_chip;
    uint32_t target_board;
    uint32_t app_base;
    uint32_t app_size;
    uint32_t version_code;
    uint32_t app_crc32;
} fw_header_t;

上位机显示:

固件类型:STM32 BootLoader Package
目标芯片:STM32F407VET6
目标板卡:YD_AUTO_V1
APP地址 :0x08020000
APP大小 :182736 bytes
版本号  :1.2.3
版本码  :10203

如果 magic 不对,直接提示:

固件格式错误,请选择打包后的升级文件

不要进入发送流程。

3.3 上位机也可以计算一次 CRC

虽然 BootLoader 最终还会校验,但上位机本地先检查可以提前发现文件损坏。

流程:

读取固件头
读取 APP payload
计算 CRC32 或 SHA256
和固件头字段比较

如果本地校验失败:

固件文件可能损坏,禁止发送

这样可以减少现场调试时间。

3.4 文件大小也要检查

上位机应检查:

文件总大小 >= 固件头大小
payload 大小 == 固件头中的 payload_size
app_size 不超过目标芯片 APP 分区

如果做 A/B 分区,还要显示:

该固件适用于 APP_A
该固件适用于 APP_B
该固件支持 A/B 自动选择

不要让用户猜。

4. 串口升级的联调流程

串口升级最适合先调通 BootLoader。

它的优点是:

连接简单
日志方便
不受网络配置影响
问题容易定位

4.1 串口参数

推荐先固定参数:

波特率:115200 或 921600
数据位:8
停止位:1
校验位:None
流控:None

调试早期建议先用 115200。

等流程稳定后,再提高波特率。

如果一开始就用 921600,遇到失败时很难判断是协议问题还是串口质量问题。

4.2 串口升级基本流程

上位机打开串口
APP 收到进入升级命令
APP 写升级标志
APP 复位
BootLoader 启动
BootLoader 检测升级标志
BootLoader 周期发送字符 C
上位机收到 C
上位机开始发送 YMODEM 第 0 包
BootLoader ACK
上位机发送数据包
BootLoader 写 Flash
全部发送完成
BootLoader 校验固件
BootLoader 返回成功状态
设备复位或跳转 APP

4.3 上位机不要一连接就发送

YMODEM 的发送端要等 BootLoader 发起。

BootLoader 通常会周期发送:

C

表示:

我已经准备好,请用 CRC 模式发送 YMODEM

如果上位机没等到 C 就开始发送第 0 包,BootLoader 可能还没准备好。

典型现象:

上位机显示正在发送
设备没有反应
第 0 包超时

正确做法:

进入升级模式后,等待 C
收到 C 后再发第 0 包
等待 ACK 后再发下一包

4.4 串口接收线程

上位机串口接收建议单独线程或异步任务处理。

不要在 UI 线程里阻塞等待。

伪代码:

while (_serialPort.IsOpen)
{
    int b = _serialPort.ReadByte();
    _ymodemSender.OnByteReceived((byte)b);
}

UI 线程只负责显示状态:

等待 BootLoader
发送第 0 包
发送数据包 128/500
等待最终 ACK
升级完成

4.5 串口日志要区分方向

日志建议这样显示:

[10:21:03.120] TX  EnterBoot
[10:21:03.328] RX  BootAck
[10:21:04.012] RX  C
[10:21:04.015] TX  YMODEM block 0
[10:21:04.066] RX  ACK
[10:21:04.067] RX  C
[10:21:04.070] TX  YMODEM block 1

只打印一句“发送中”不够。

后面出问题,根本不知道卡在谁等谁。

5. W5500 TCP 升级的联调流程

W5500 网口升级和串口升级的整体逻辑类似。

区别是通信链路换成 TCP。

5.1 推荐连接方式

前面 W5500 那篇建议:

设备作为 TCP Server
上位机作为 TCP Client

设备固定监听端口,例如:

IP   : 192.168.1.50
Port : 5000

上位机连接:

192.168.1.50:5000

这样做的好处是:

设备不需要知道 PC IP
上位机可以主动发起连接
调试时抓包方便

5.2 TCP 连接成功不等于可以发固件

TCP 连接成功,只说明 Socket 通了。

还要等 BootLoader 进入升级状态。

推荐流程:

上位机连接 TCP
BootLoader 发送 HELLO 或 C
上位机识别设备状态
确认可以升级
开始 YMODEM over TCP

不要一 connect() 成功就立即发送整个文件。

5.3 TCP 分包一定要处理

这是网口升级最容易踩的坑。

上位机一次发送:

1029 字节

下位机可能收到:

500 字节
300 字节
229 字节

也可能收到:

1029 字节

TCP 是字节流,不保留你的 send() 边界。

所以上位机和 BootLoader 都不能假设:

一次 send 对应一次 recv

BootLoader 要把 TCP 接收缓存适配成“读一个字节”的接口。

上位机发送端也要严格等 ACK,不能疯狂连续写。

5.4 TCP 超时和断线

上位机要区分几种情况:

连接超时
连接被拒绝
传输中断开
长时间没有收到 ACK
设备复位导致连接断开

这些提示不能都叫“网络错误”。

推荐提示:

场景 上位机提示
IP 不通 无法连接设备,请检查 IP 和网段
端口未开 设备未进入 BootLoader 或端口未监听
传输中断开 升级过程中连接断开,请重新连接 BootLoader
最终阶段断开 设备可能正在复位,等待重新上线
ACK 超时 BootLoader 未响应当前数据包

5.5 升级成功后 TCP 断开是正常的

很多设备升级完成后会复位。

复位时 W5500 会重新初始化,TCP 连接一定断开。

上位机不能把这个断开一律当失败。

如果 BootLoader 已经返回:

UPGRADE_OK

随后 TCP 断开,可以显示:

升级成功,等待设备重启

然后进入重新连接流程。

6. 进入 BootLoader 的几种方式

上位机升级前,要让设备进入 BootLoader。

常见方式有四种。

6.1 上电按键进入

设备上电时检测按键。

按键按下 -> 停留 BootLoader
按键未按 -> 检查 APP 并跳转

优点:

简单可靠
适合救砖

缺点:

现场需要人工操作
远程升级不方便

6.2 APP 命令进入

APP 正常运行时,上位机发送命令:

EnterBootloader

APP 收到后:

写升级标志
延时应答
系统复位

BootLoader 上电后检测升级标志,停留升级模式。

这是最常用的方式。

6.3 空 APP 自动进入

BootLoader 检查 APP 无效:

栈顶地址非法
Reset_Handler 非法
CRC 失败
APP_VALID 标志不存在

就停留 BootLoader。

这是必须要有的兜底逻辑。

6.4 上位机控制复位脚

有些生产工具会通过 USB 转串口模块的 DTR/RTS,或者外部下载器控制复位脚。

流程:

拉 Boot 管脚或升级选择脚
复位设备
设备进入 BootLoader

这种方式适合产线,不一定适合现场。

7. 握手协议怎么设计

不要让上位机只靠猜。

建议 APP 和 BootLoader 都支持简单握手命令。

7.1 APP 模式握手

上位机连接正常 APP 后,先发送:

CMD_GET_INFO

APP 返回:

mode        = APP
app_version = 1.2.0
boot_version = 1.0.3
board_id    = YD_AUTO_V1
chip_id     = STM32F407VET6
device_id   = 12345678

上位机显示当前设备信息。

然后比较固件包:

固件目标板卡是否匹配
固件版本是否允许升级
APP 地址是否匹配

7.2 进入升级命令

上位机发送:

CMD_ENTER_BOOT

APP 返回:

ACK_ENTER_BOOT

然后 APP 写升级标志并复位。

注意顺序:

先应答上位机
再复位

如果 APP 收到命令后马上复位,上位机可能还没收到应答,会误判失败。

7.3 BootLoader 模式握手

BootLoader 启动后,可以返回:

BOOT_HELLO
boot_version
upgrade_state
last_error

如果继续用标准 YMODEM,也可以周期发送 C

更稳的做法是:

先返回 BootLoader 状态帧
确认后再进入 YMODEM 收包

例如:

上位机 -> CMD_PREPARE_UPGRADE
Boot   -> ACK_READY
Boot   -> C
上位机 -> YMODEM block 0

这样上位机能知道设备确实已经进入 BootLoader。

7.4 最小命令帧格式

调试阶段可以用简单帧:

SOF   CMD   LEN   DATA   CRC16
0x55  0x10  0x04  ....   ....

示例:

typedef struct
{
    uint8_t  sof;
    uint8_t  cmd;
    uint16_t len;
    uint8_t  data[128];
    uint16_t crc16;
} proto_frame_t;

不要把普通命令帧和 YMODEM 数据包混在一个解析状态里。

推荐分阶段:

命令模式:解析 CMD_GET_INFO / CMD_ENTER_BOOT
升级模式:解析 YMODEM

8. YMODEM 发送端状态机

上位机也应该有状态机。

不要用一大段阻塞函数从头发到尾。

8.1 发送端状态

typedef enum
{
    YM_TX_IDLE = 0,
    YM_TX_WAIT_C,
    YM_TX_SEND_HEADER,
    YM_TX_WAIT_HEADER_ACK,
    YM_TX_SEND_DATA,
    YM_TX_WAIT_DATA_ACK,
    YM_TX_SEND_EOT,
    YM_TX_WAIT_EOT_ACK,
    YM_TX_SEND_EMPTY,
    YM_TX_WAIT_EMPTY_ACK,
    YM_TX_DONE,
    YM_TX_ERROR
} ymodem_tx_state_t;

每个状态只做一件事。

8.2 等待 C

状态:YM_TX_WAIT_C
输入:收到字符 C
动作:发送第 0 包
下一个状态:YM_TX_WAIT_HEADER_ACK

超时:

提示:等待 BootLoader 请求超时

8.3 发送第 0 包

YMODEM 第 0 包包含文件名和文件大小。

如果发送的是固件包:

filename = app_secure.fw
filesize = 固件包总大小

注意,YMODEM 的文件大小是传输文件大小,不一定等于 APP 明文大小。

如果固件包包含固件头和签名:

YMODEM filesize = fw 文件大小
固件头 app_size = APP 明文大小

这两个不要混淆。

8.4 发送数据包

每个数据包发送后,等待 BootLoader 返回:

ACK

如果返回:

NAK

可以重发当前包。

如果连续失败超过次数:

升级失败

示例:

#define YM_MAX_RETRY  10

if (rx == ACK)
{
    block_index++;
    retry_count = 0;
    SendNextBlock();
}
else if (rx == NAK)
{
    retry_count++;
    if (retry_count > YM_MAX_RETRY)
    {
        SetError(YM_ERR_RETRY_OVER);
    }
    else
    {
        ResendCurrentBlock();
    }
}

8.5 不要连续无等待发送

上位机不能这样写:

foreach (var block in blocks)
{
    port.Write(block);
}

这样容易把 BootLoader 接收缓存打爆。

正确做法是:

发一包
等 ACK
再发下一包

串口和 TCP 都建议保持这个节奏。

9. 进度条应该怎么算

进度条看起来简单,实际很容易误导用户。

9.1 传输进度

最简单的进度:

已发送字节 / 固件包总字节

例如:

progress = sentBytes * 100 / firmwarePackageSize;

这是传输进度。

它不等于升级完成进度。

因为 BootLoader 后面还要:

写 Flash
计算 CRC
验签
写升级记录
复位

9.2 阶段进度

更适合现场的是阶段加进度:

连接设备
进入 BootLoader
发送固件
Flash 写入
CRC 校验
签名校验
写入升级记录
设备重启
版本确认

界面可以显示:

当前阶段:发送固件
传输进度:68%
当前包号:347 / 512

9.3 BootLoader 返回阶段

如果 BootLoader 支持状态码,可以周期返回:

BL_STATE_ERASE_APP
BL_STATE_RECV_DATA
BL_STATE_WRITE_DATA
BL_STATE_VERIFY_APP
BL_STATE_MARK_VALID

上位机显示:

设备正在擦除 APP 区
设备正在写入 Flash
设备正在校验固件

这比单纯的百分比更有用。

9.4 不要 100% 后还卡着

很多工具会出现:

进度条 100%
界面还在等待
用户不知道成功还是失败

原因是传输完成不等于升级完成。

可以把显示拆成:

传输:100%
设备校验中...
等待设备重启...
升级完成

这样用户不会误解。

10. 错误码和提示文案

第七篇已经讲过 BootLoader 错误码。

上位机要把错误码翻译成能理解的提示。

10.1 错误码分层

建议分三层:

通信错误
协议错误
设备错误

通信错误:

串口打开失败
串口断开
TCP 连接超时
TCP 连接中断
ACK 超时

协议错误:

YMODEM 包号错误
CRC16 错误
连续重发超限
收到未知响应

设备错误:

固件头错误
目标芯片不匹配
版本回滚
Flash 擦除失败
Flash 写入失败
APP CRC 失败
签名校验失败

10.2 错误码映射表

上位机可以维护一张表:

static readonly Dictionary<int, string> BootErrorText = new()
{
    { 0x00, "成功" },
    { 0x10, "固件头 magic 错误" },
    { 0x11, "目标芯片不匹配" },
    { 0x12, "目标板卡不匹配" },
    { 0x13, "APP 地址不允许写入" },
    { 0x14, "固件大小超过 APP 分区" },
    { 0x15, "固件版本低于设备允许版本" },
    { 0x20, "Flash 擦除失败" },
    { 0x21, "Flash 写入失败" },
    { 0x22, "Flash 读回校验失败" },
    { 0x30, "APP CRC 校验失败" },
    { 0x31, "APP Hash 校验失败" },
    { 0x32, "固件签名校验失败" },
};

界面显示:

升级失败:固件签名校验失败
错误码:0x32
建议:请确认固件是否由正式打包工具生成,是否选择了正确设备型号。

10.3 错误提示要带建议

只显示原因还不够。

最好附一条处理建议。

例如:

错误 建议
等待 C 超时 检查设备是否进入 BootLoader,串口是否接反
目标板卡不匹配 请确认固件包是否用于当前设备
Flash 写入失败 检查写保护、地址范围和供电
CRC 校验失败 重新发送固件,检查通信质量
签名失败 使用正式打包工具重新生成固件
复位后不上线 检查 APP 向量表、APP 地址、运行日志

这对售后很有用。

11. 设备复位和重新连接

升级完成后,设备通常会复位。

上位机必须处理复位带来的断开和重连。

11.1 串口设备复位

STM32 复位后,USB 转串口通常不会消失。

上位机可以保持串口打开。

但如果设备本身是 USB CDC,复位时 COM 口可能会短暂消失。

这时要做:

关闭旧串口
等待 COM 口重新出现
重新打开串口
发送 GET_INFO
读取新版本

11.2 TCP 设备复位

TCP 连接会断开。

上位机应进入等待重连:

升级完成
等待设备重启
每 1 秒尝试连接
最多等待 30 秒
连接成功后读取版本

示例:

for (int i = 0; i < 30; i++)
{
    if (await TryConnectAsync(ip, port))
    {
        var info = await ReadDeviceInfoAsync();
        return info;
    }

    await Task.Delay(1000);
}

throw new Exception("设备重启后未重新上线");

11.3 升级成功要以新版本确认为准

真正让用户放心的结果不是“固件发送完成”,而是:

设备已重新上线
当前版本 = 新固件版本
当前模式 = APP

界面可以显示:

升级完成,设备已运行新版本 1.2.3

如果设备没有重新上线:

固件发送成功,但未确认 APP 运行,请检查设备状态

这两种结果要区分。

12. 日志怎么打才方便排查

上位机日志是现场排查的核心。

12.1 日志字段

建议每条日志包含:

时间
方向
阶段
内容
耗时
错误码

例如:

[2026-06-08 14:20:01.120] [INFO] 选择固件 app_secure.fw
[2026-06-08 14:20:01.122] [INFO] 固件版本 1.2.3, APP大小 182736
[2026-06-08 14:20:05.300] [TX]   CMD_ENTER_BOOT
[2026-06-08 14:20:05.328] [RX]   ACK_ENTER_BOOT
[2026-06-08 14:20:06.012] [RX]   C
[2026-06-08 14:20:06.015] [TX]   YMODEM block 0, size=128
[2026-06-08 14:20:06.066] [RX]   ACK

12.2 保存完整升级记录

升级完成后保存:

设备ID
升级前版本
升级后版本
固件文件名
固件版本
开始时间
结束时间
结果
错误码
完整日志

文件名可以:

upgrade_20260608_142001_device12345678.log

12.3 不要只记录 UI 文案

日志里不要只写:

升级失败

要写底层信息:

YM_TX_WAIT_DATA_ACK timeout, block=128, retry=10
boot_error=0x21 Flash write failed, addr=0x08032000

这样回头看日志才能定位。

12.4 敏感信息不要写日志

如果固件包有加密和签名,不要记录:

AES key
HMAC key
私钥
完整签名输入

可以记录:

key_id
sign_algo
hash 前 4 字节

13. 批量升级时要注意什么

单台升级跑通后,很多项目会做批量升级。

批量升级不要简单地开很多线程同时发。

13.1 串口批量升级

串口批量通常是多台设备接多个 COM 口。

要注意:

每个 COM 口一个独立升级任务
每台设备独立日志
错误不能影响其他设备
UI 要能看到每台设备状态

状态表:

COM3  设备001  发送中  45%
COM4  设备002  校验中  100%
COM5  设备003  失败    等待 C 超时

13.2 TCP 批量升级

TCP 批量升级通常按 IP 列表升级。

不建议所有设备同时升级。

原因:

网络拥塞
设备同时复位
上位机日志混乱
供电系统瞬时变化
现场无法快速判断哪台失败

更稳的做法:

分批升级
每批 3 到 10 台
每台独立连接和日志
失败设备进入待处理列表

13.3 设备身份要确认

批量升级前,一定要读取设备身份:

device_id
board_id
current_version
ip

不要只按 IP 发固件。

IP 可能被改,设备可能被换。

13.4 失败重试要谨慎

可以自动重试连接失败、等待 C 超时这类前置错误。

但下面这些错误不要盲目自动重试:

目标板卡不匹配
版本回滚
签名校验失败
Flash 写入失败

这些问题需要人工确认。

14. C# 上位机代码结构示例

如果用 C# 写 WinForms 或 WPF,上位机代码可以分层。

14.1 推荐目录

FirmwareUpgradeTool
├─ Models
│  ├─ FirmwareInfo.cs
│  ├─ DeviceInfo.cs
│  └─ UpgradeResult.cs
├─ Protocol
│  ├─ IDeviceTransport.cs
│  ├─ SerialTransport.cs
│  ├─ TcpTransport.cs
│  └─ DeviceProtocol.cs
├─ YModem
│  ├─ YModemSender.cs
│  ├─ YModemPacket.cs
│  └─ YModemCrc.cs
├─ Firmware
│  ├─ FirmwarePackageReader.cs
│  └─ FirmwareValidator.cs
├─ Upgrade
│  ├─ UpgradeService.cs
│  └─ UpgradeState.cs
└─ UI
   └─ MainForm.cs

UI 不要直接拼 YMODEM 包。

UI 只调用:

await _upgradeService.StartUpgradeAsync(options, progress, cancellationToken);

14.2 通信接口

串口和 TCP 可以抽象成同一个接口:

public interface IDeviceTransport : IDisposable
{
    bool IsConnected { get; }

    Task ConnectAsync(CancellationToken ct);

    Task DisconnectAsync();

    Task WriteAsync(byte[] data, int offset, int count, CancellationToken ct);

    Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct);
}

这样 YMODEM 发送器不关心底层是串口还是 TCP。

14.3 固件读取

public sealed class FirmwareInfo
{
    public string FilePath { get; init; } = "";
    public string Version { get; init; } = "";
    public uint VersionCode { get; init; }
    public uint TargetChip { get; init; }
    public uint TargetBoard { get; init; }
    public uint AppBase { get; init; }
    public uint AppSize { get; init; }
    public long PackageSize { get; init; }
}

固件读取器负责:

打开文件
解析固件头
校验 magic
校验大小
计算 CRC 或 Hash
返回 FirmwareInfo

14.4 升级服务

public sealed class UpgradeService
{
    private readonly IDeviceTransport _transport;
    private readonly DeviceProtocol _protocol;
    private readonly YModemSender _ymodem;

    public async Task<UpgradeResult> StartUpgradeAsync(
        FirmwareInfo firmware,
        IProgress<UpgradeProgress> progress,
        CancellationToken ct)
    {
        progress.Report(UpgradeProgress.Stage("连接设备"));
        await _transport.ConnectAsync(ct);

        progress.Report(UpgradeProgress.Stage("读取设备信息"));
        DeviceInfo device = await _protocol.ReadDeviceInfoAsync(ct);

        ValidateDeviceAndFirmware(device, firmware);

        progress.Report(UpgradeProgress.Stage("进入 BootLoader"));
        await _protocol.EnterBootloaderAsync(ct);

        progress.Report(UpgradeProgress.Stage("等待 BootLoader"));
        await _protocol.WaitBootloaderReadyAsync(ct);

        progress.Report(UpgradeProgress.Stage("发送固件"));
        await _ymodem.SendFileAsync(firmware.FilePath, progress, ct);

        progress.Report(UpgradeProgress.Stage("等待设备重启"));
        DeviceInfo newInfo = await WaitAppOnlineAsync(firmware, ct);

        return UpgradeResult.Success(newInfo);
    }
}

这只是结构示例。

真正项目里要把串口断开、TCP 重连、取消升级、日志记录都补进去。

14.5 取消升级

上位机要支持取消。

但取消不是随便停止线程。

推荐:

还没擦 APP 前:可以直接取消
已经开始发送数据:发送 CAN,通知 BootLoader 取消
BootLoader 已经擦 APP:设备可能需要重新升级

YMODEM 取消通常发送:

CAN CAN

取消后界面提示:

升级已取消。设备可能停留在 BootLoader,请重新升级或重启设备。

不要让用户以为取消后旧 APP 一定还能运行。

15. 联调检查清单

真正联调时,建议按下面顺序。

15.1 先确认 APP 跳转

直接烧录 BootLoader
直接烧录 APP 到 APP_BASE
上电后 BootLoader 能跳 APP
APP 串口或网口能正常通信

如果 APP 跳转都没跑通,不要先调上位机升级。

15.2 再确认进入 BootLoader

APP 收到 EnterBoot 命令
APP 写升级标志
APP 应答上位机
APP 复位
BootLoader 检测升级标志
BootLoader 停留升级模式

串口日志应看到:

APP: enter boot request
BOOT: upgrade flag detected
BOOT: wait ymodem

15.3 再确认 YMODEM 第 0 包

只要第 0 包能通,说明大部分握手已经对上。

检查:

BootLoader 是否发送 C
上位机是否收到 C
上位机是否发送第 0 包
BootLoader 是否 ACK
BootLoader 是否再次发送 C

15.4 再确认 Flash 擦写

发送一个小固件。

检查:

BootLoader 擦除范围是否正确
APP_BASE 是否正确
写入地址是否递增
写入大小是否等于 app_size

用调试器或 STM32CubeProgrammer 查看 APP 起始地址:

APP_BASE + 0x00 栈顶
APP_BASE + 0x04 Reset_Handler

15.5 再确认校验和跳转

检查:

CRC 是否一致
固件头版本是否正确
APP_VALID 是否写入
升级记录区 CRC 是否正确
复位后是否跳 APP
APP 是否上报新版本

15.6 最后再测异常

正常流程跑通后,再测异常:

传错固件
传超大固件
传旧版本固件
传输中拔串口
传输中断网
写 Flash 时断电
CRC 故意改错
签名故意改错
升级完成后立即复位

异常测试一定要做。

否则现场第一次遇到,就是客户帮你测试。

16. 常见问题

16.1 上位机一直等待,BootLoader 没反应

先看 BootLoader 有没有进入升级模式。

检查:

APP 是否收到进入升级命令
升级标志是否写入成功
设备是否复位
BootLoader 是否检测到升级标志
串口号或 IP 是否连接正确

如果 BootLoader 没有发送 C,上位机不会开始 YMODEM。

16.2 BootLoader 一直发 C,上位机不发送

说明 BootLoader 已经准备好,但上位机没有进入发送状态。

检查:

上位机接收线程是否启动
收到的 C 是否被日志打印
YMODEM 状态机是否在 WAIT_C
是否被 UI 线程阻塞

16.3 第 0 包失败

常见原因:

上位机第 0 包格式错误
文件名和文件大小没有按 YMODEM 格式放
CRC16 算法不一致
BootLoader 期望 128 字节包,上位机发了 1024 字节包

第 0 包建议先用成熟串口工具对照验证。

16.4 数据包发送到一半失败

检查:

上位机是否等待 ACK
BootLoader 写 Flash 是否阻塞太久
串口波特率是否太高
TCP recv 是否正确处理分包
重发次数是否太少

如果 BootLoader 写 Flash 期间不能及时接收,可以降低发送节奏。

16.5 进度 100%,但设备不运行

传输 100% 只表示文件发完。

继续检查:

BootLoader 是否 CRC 通过
是否写 APP_VALID
是否复位
APP 向量表是否正确
APP 是否设置 VTOR
APP 是否上报新版本

16.6 TCP 升级成功后提示连接断开

如果 BootLoader 已经返回成功,然后设备复位导致断开,这是正常现象。

上位机应该显示:

升级成功,等待设备重启

不要直接报失败。

16.7 固件头检查失败

检查:

是否选择了打包后的 fw 文件
是否误选 app.bin
固件包 magic 是否正确
固件头版本是否兼容
大小端是否一致
结构体是否有对齐差异

C# 解析结构体时,不建议直接按本机结构体强转。

最好按字段逐个读取,并明确小端格式。

16.8 版本回滚失败

如果 BootLoader 返回版本回滚,上位机要显示当前设备允许的最低版本。

例如:

固件版本:1.0.5
设备允许最低版本:1.2.0
结果:拒绝降级

不要只显示“升级失败”。

16.9 签名校验失败

检查:

固件是否由正式打包工具生成
固件头关键字段是否被修改
key_id 是否匹配
BootLoader 公钥是否是当前版本
签名覆盖范围是否和打包工具一致

注意,上位机不能尝试自己重新签名客户选择的文件。

上位机只负责发送已经签名好的固件包。

16.10 取消升级后设备进不了 APP

如果已经擦除了单 APP 分区,取消升级后旧 APP 可能已经不完整。

这不是上位机错误。

界面要提示:

升级已取消,设备可能停留在 BootLoader,请重新发送完整固件。

如果希望取消后仍能运行旧 APP,需要 A/B 分区或外部 Flash 暂存。

16.11 多台设备升级时日志混在一起

每台设备必须独立日志。

日志前缀加:

COM口
IP
设备ID
任务ID

例如:

[COM3][DEV001] block=120 ACK
[COM4][DEV002] block=88 ACK

不要所有任务写同一个无前缀文本框。

16.12 上位机和 BootLoader 谁来判断版本

两边都要判断。

上位机先判断,可以提前提醒用户。

BootLoader 再判断,是最终防线。

不要只依赖上位机。

因为上位机可能被替换,也可能有人绕过上位机直接发数据。

17. 总结

BootLoader 升级不是单片机一个人的事。

真正稳定的升级链路,至少包含:

固件打包工具
上位机升级工具
BootLoader
APP
升级记录区
日志和错误码

这篇的几个重点:

上位机要先解析固件头,不要盲目发送
串口升级要等待 BootLoader 发送 C
TCP 升级要按字节流处理,不能假设一次 send 对应一次 recv
YMODEM 发送端也要有状态机
传输 100% 不等于升级完成
升级成功最好以设备重启后上报新版本为准
错误码要翻译成清楚的提示和处理建议
日志要保存阶段、方向、包号、错误码和设备信息
批量升级要独立任务、独立日志、分批执行
上位机只是第一道检查,BootLoader 才是最终防线

调试 BootLoader 时,建议先用串口跑通完整流程,再迁移到 W5500 TCP。

等单台设备稳定后,再做批量升级。

只要上位机和 BootLoader 的状态、错误码、日志能对应起来,现场问题就会好排查很多。

文章标签

STM32
BootLoader
IAP
YMODEM
W5500
上位机
固件升级
嵌入式
单片机
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐