USB升级器Bootloader代码流程

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

240.5.4 USB升级器Bootloader代码流程

if (HAL_UART_Receive(&huart2, pData, 4, 0xFF) == HAL_OK) /* 31usb_tool:USART2 在 255 ms 超时内收到完整 4 字节命令才继续。 */
{ /* 开始串口命令处理。 */
  if ((pData[0] == 0xAF) && (pData[3] == 0xFA)) /* 只校验帧头 AF 和帧尾 FA;当前代码不检查第 2 字节。 */
  { /* 开始帧边界校验分支。 */
    if (pData[2] == 0x01) /* 第 3 字节为 01 表示请求升级,对应 AF 00 01 FA。 */
    { /* 开始升级请求分支。 */
        printf("Upgrade will start...\r\n"); /* 在 C8T6 的 USART1 输出升级开始提示。 */
        uint8_t send_msg[8] = {0xBE, 0xAD, 0xBE, 0xEF, 0x00, 0xAA, 0xAA, 0xAA}; /* 准备 8 字节 CAN 控制帧。 */
        send_msg[4] = 0x01; /* 把命令字改为 01,表示请求当前 APP 进入升级模式。 */
        CAN1_Send_Msg(CAN_ID_CALL_OTA, (uint8_t *)send_msg, 8); /* 以 CAN ID 0x0B3 把请求发给 ZET6 当前 APP。 */
        ota_call_flag = 1; /* 标记 C8T6 正在等待 Bootloader 的升级确认。 */
        ota_call_tick = OTA_CALL_MAX_WAIT + HAL_GetTick(); /* 保存等待确认的截止时刻。 */
    } /* 结束升级请求分支。 */
  } /* 结束帧边界校验分支。 */
} /* 结束串口命令处理。 */

if(ota_call_flag == 1) /* 已向 APP 发出请求后,轮询 Bootloader 的确认帧。 */
{ /* 开始确认等待状态。 */
    if((ota_call_tick >=  HAL_GetTick()) && (ota_call_tick != 0)) /* 当前 tick 尚未超过截止时刻。 */
    { /* 开始未超时分支。 */
      uint32_t recv_id = 0; /* 保存接收到的 CAN 标准 ID。 */
      uint8_t recv_len = 0; /* 保存 CAN 数据长度。 */
      uint8_t rxdata[8] = {0}; /* 保存一帧最多 8 字节的经典 CAN 数据。 */
      recv_len = CAN1_Recv_Msg(&recv_id, rxdata); /* 从软件接收接口轮询一帧。 */
      if(recv_len > 0) /* 长度大于 0 表示本轮确实取到报文。 */
      { /* 开始有效报文分支。 */
        if(recv_id == CAN_ID_CALL_OTA_ACK) /* ID 为 0x0D3 时视为 Bootloader 的升级确认。 */
        { /* 开始确认帧分支。 */
            printf("Upgrade ack received...\r\n"); /* 输出已收到确认。 */
            ota_call_tick = 0; /* 清除截止时刻。 */
            ota_call_flag = 0; /* 结束确认等待状态。 */
            ymodule_run_flag = 1; /* 允许后面的 YMODEM 接收流程运行。 */
        } /* 结束确认帧分支。 */
      } /* 结束有效报文分支。 */
    } /* 结束未超时分支。 */
    else if((ota_call_tick <  HAL_GetTick()) && (ota_call_tick != 0)) /* 超过截止时刻且截止值有效。 */
    { /* 开始超时分支。 */
        ota_call_flag = 0; /* 取消等待状态。 */
        ota_call_tick = 0; /* 清除截止时刻。 */
        printf("Upgrade timeout...\r\n"); /* 记录升级握手超时。 */
    } /* 结束超时分支。 */
} /* 结束确认等待状态。 */

这段代码位于 C8T6 的 31usb_tool/Core/Src/main.c。它完成“PC 请求→当前 APP→软件复位→Bootloader 确认”这一段控制握手。当前实现收到 0x0D3 只检查 CAN ID,没有继续校验 8 字节确认载荷,这是课堂工程的实际行为。

34Bootloader/Core/Src/main.c:按 Flash 标志选择升级或启动 APP。

int8_t ret = inter_flash_cfg_get_app_update_flag(); /* 从 0x08006000 配置区读取并返回持久化升级标志。 */

if (ret == 1) /* 标志为 1 表示 APP 已请求升级。 */
{ /* 开始升级模式分支。 */
    printf("wait ota server\r\n"); /* 输出 Bootloader 正在等待升级服务端。 */
    uint8_t send_msg[8] = {0xBE, 0xAD, 0xBE, 0xEF, 0x00, 0xAA, 0xAA, 0xAA}; /* 构造确认控制帧。 */
    send_msg[4] = 0x01; /* 命令字 01 表示确认进入升级模式。 */
    CAN1_Send_Msg(CAN_ID_CALL_OTA_ACK, (uint8_t *)send_msg, 8); /* 以 CAN ID 0x0D3 回复 C8T6 升级工具。 */
    cantp_run_flag = 1; /* 允许 Bootloader 主循环运行 ISO-TP 服务。 */
} /* 结束升级模式分支。 */
else if (ret == 0) /* 标志为 0 表示没有升级请求。 */
{ /* 开始正常启动分支。 */
    printf("[BOOT]--->jump app\r\n\r\n"); /* 输出即将跳转 APP 的日志。 */
    boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* 校验并跳转到 0x08008000 的 APP。 */
    cantp_run_flag = 0; /* 若跳转函数返回,则保持 TP 服务关闭。 */
} /* 结束正常启动分支。 */

if (cantp_run_flag == 1) /* Bootloader 已进入升级模式时持续服务 ISO-TP。 */
{ /* 开始 TP 服务分支。 */
    iso_tp_server(); /* 轮询 CAN、推进 ISO-TP 状态机并处理完整升级数据块。 */
} /* 结束 TP 服务分支。 */

boot_check_stack2jump_app() 先检查 APP 起始处的初始栈指针是否落在 SRAM 地址范围,再从 APP+4 读取复位入口,设置 MSP、关闭可屏蔽中断并跳转。因此前两课的 APP 必须链接到 0x08008000、把 VTOR 指向同一地址,并在启动后执行 __set_PRIMASK(0)。

31usb_tool/ymodem/ymodem.c:把 128 字节 YMODEM 数据积累成 2 KiB 后交给 ISO-TP。

memcpy(&flash_buf[flash_buf_rx_cnt], &aPacketData[PACKET_DATA_INDEX], PACKET_SIZE); /* 每次只复制 PACKET_SIZE=128 字节到 2 KiB 缓冲区。 */
flash_buf_rx_cnt = flash_buf_rx_cnt + PACKET_SIZE; /* 把缓冲区写入位置向后移动 128 字节。 */

if (((packets_received % 16) == 0) || (packets_received == file_all_num)) /* 每 16 个数据包或判断为最后一包时发送一批。 */
{ /* 开始 2 KiB 批处理。 */
    flash_buf_rx_cnt = 0; /* 下一批重新从缓冲区起点累计。 */
    uint32_t offest_addr  = page_cnt * 2048; /* 计算相对 APP 起点的页偏移;变量名保留源码拼写。 */
    uint32_t data_len     = 2048; /* 当前实现无论末批有效字节多少,都固定发送 2048 字节。 */
    iso_tp_send_ota_data(offest_addr, flash_buf, data_len); /* 阻塞地把带偏移与长度的数据块交给 ISO-TP。 */
    HAL_Delay(100); /* 每批额外等待 100 ms,保留课程节流设置。 */
    printf("cnt:%d addr:%x\r\n", page_cnt, INTER_FLASH_APP_ADDR + (page_cnt * 2048)); /* 打印页号和目标绝对地址。 */
    page_cnt++; /* 指向下一 2 KiB 页。 */
    memset(flash_buf, 0, 2048); /* 清零缓冲区,使最后不足 2 KiB 的尾部以 0 填充。 */
} /* 结束 2 KiB 批处理。 */

uint8_t send_msg[8] = {0xBE, 0xAD, 0xBE, 0xEF, 0x00, 0xAA, 0xAA, 0xAA}; /* YMODEM 会话结束时构造控制帧。 */
send_msg[4] = 0x02; /* 命令字 02 表示固件传输结束。 */
CAN1_Send_Msg(CAN_ID_CALL_OTA, (uint8_t *)send_msg, 8); /* 以 CAN ID 0x0B3 通知 Bootloader 清标志并启动新 APP。 */

这里的 16 × 128 = 2048 与 STM32F103ZET6 的 2 KiB Flash 页大小相配。本项目的桥接代码明确按 128 字节复制,因此依赖发送端实际采用 128 字节数据包;通用 YMODEM 也常见 1024 字节数据块,ST 的 AN4657 示例就说明其 YMODEM 按 1024 字节块发送。若直接换一个发送端产生 1 KiB 包,当前桥接逻辑不能原样适用。

34Bootloader/Core/Src/iso_tp_cfg.c:重组完整数据块、写 Flash,并处理结束命令。

if (recv_id == CAN_ID_OTA_SEND) /* 收到 C8T6→ZET6 的 ISO-TP 数据 ID 0x0B2。 */
{ /* 开始 TP 数据帧分支。 */
    isotp_on_can_message(&g_link, rxdata, recv_len); /* 把本帧交给 ISO-TP 状态机解析和重组。 */
} /* 结束 TP 数据帧分支。 */
else if (recv_id == CAN_ID_CALL_OTA) /* 收到 CAN ID 0x0B3 的升级控制帧。 */
{ /* 开始升级控制帧分支。 */
    if ((rxdata[0] == 0xBE) && /* 校验控制魔数第 1 字节。 */
        (rxdata[1] == 0xAD) && /* 校验控制魔数第 2 字节。 */
        (rxdata[2] == 0xBE) && /* 校验控制魔数第 3 字节。 */
        (rxdata[3] == 0xEF)) /* 校验控制魔数第 4 字节。 */
    { /* 魔数正确后判断命令字。 */
        if (rxdata[4] == 0x02) /* 02 表示 C8T6 已结束 YMODEM 文件传输。 */
        { /* 开始升级结束分支。 */
            printf("[tp server] ota end\r\n"); /* 打印升级结束日志。 */
            inter_flash_cfg_set_app_update_flag(0); /* 清除持久化升级标志,避免下次复位再次等待升级。 */
            printf("--->jump app\r\n"); /* 打印跳转提示。 */
            boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* 跳到 0x08008000 运行新 APP。 */
        } /* 结束升级结束分支。 */
    } /* 结束控制魔数校验。 */
} /* 结束升级控制帧分支。 */

ret = isotp_receive(&g_link, payload, payload_size, &out_size); /* 尝试从 ISO-TP 状态机取出一个完整 PDU。 */
if (ISOTP_RET_OK == ret) /* 返回 OK 表示一整个上层数据块已经重组完成。 */
{ /* 开始完整数据块处理。 */
    if(payload[0] == 0xBF) /* 自定义负载头 BF 表示这是固件数据块。 */
    { /* 开始固件数据块分支。 */
        uint32_t pack_offset = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 8) | ((uint32_t)payload[4] << 0); /* 按大端字段还原 32 位相对偏移。 */
        uint32_t pack_size = ((uint32_t)payload[5] << 24) | ((uint32_t)payload[6] << 16) | ((uint32_t)payload[7] << 8) | ((uint32_t)payload[8] << 0); /* 按大端字段还原数据长度。 */
        memcpy(ota_recv_data, &payload[9], pack_size); /* 把九字节自定义头后的固件内容复制到写入缓冲区。 */
        printf("\r\npack_offset:%d, pack_size:%d\r\n", pack_offset, pack_size); /* 打印本批偏移和长度。 */
        inter_flashif_erase_page(INTER_FLASH_APP_ADDR + pack_offset); /* 擦除目标 APP 页。 */
        inter_flashif_write_page(INTER_FLASH_APP_ADDR + pack_offset, (uint32_t *)ota_recv_data, pack_size/4); /* 以 32 位字写入本批数据。 */
        printf("write end\r\n"); /* 记录本页写入流程结束。 */
    } /* 结束固件数据块分支。 */
} /* 结束完整数据块处理。 */

上述四段均取自课程配套 31usb_tool 与 34Bootloader,只补充逐行注释,没有改写业务逻辑或抄入 HAL/ISO-TP 库内部实现。

一、Bootloader代码和USB升级器代码

系统里有三个 MCU 节点:一个 C8T6 作为 VCU/数据采集控制器,一个 C8T6 作为升级桥接器,一个 ZET6 运行屏幕 APP 和 Bootloader。正常业务时 VCU 通过 CAN ID 0x0B4 更新仪表盘;升级时桥接器把 PC 固件转换成 CAN TP 数据送给 ZET6。

两个文件传输层分别覆盖不同链路:

链路协议职责
PC ↔ C8T6 USART2YMODEM文件名、大小、数据包、CRC、ACK/NAK 和结束会话
C8T6 ↔ ZET6 CAN项目集成的 ISO-TP 实现把 2057 字节自定义 PDU 分成经典 CAN 帧,进行序号、流控和重组

控制帧不走上述大文件载荷:0x0B3 用于请求/结束,0x0D3 用于 Bootloader 确认;ISO-TP 数据发送 ID 为 0x0B2,反向流控 ID 为 0x0B1。

1.1 CAN-TP协议

课程口述把 CAN TP 类比为 TCP“三次握手”,这个类比不准确。ISO-TP(ISO 15765-2)在经典 CAN 的 8 字节载荷之上定义四类帧:Single Frame、First Frame、Consecutive Frame 和 Flow Control。多帧发送时,首帧声明总长度,接收端用流控帧给出块大小 BS 和最小帧间隔 STmin,发送端再依次发送连续帧。可参考 Linux 内核的 ISO-TP 文档。

它解决的是分段、重组、顺序和流控,不等同于 TCP,也不自动提供固件签名、版本授权或整包哈希。本工程还在 ISO-TP PDU 外加了 9 字节自定义头:0xBF + 4 字节偏移 + 4 字节长度。

配套配置中 ISO_TP_DEFAULT_BLOCK_SIZE=128、ISO_TP_DEFAULT_ST_MIN=0、响应超时 1000 ms。iso_tp_send_ota_data() 调用的是阻塞发送,因此返回时 TP 状态机已经完成本批发送或报错;外层再固定延时 100 ms 是课程额外加入的节流。

1.2 USB升级器代码

这两个工程可以作为学习样例和实验基线,但“已经稳定”不等于适合不经审查地直接用于产品。至少要核对目标芯片、分区大小、CAN ID、Flash 页大小、串口参数、YMODEM 包型、镜像最大长度和失败恢复策略。

完整状态机如下:

  1. PC 发 AF 00 01 FA;C8T6 向当前 APP 发 0x0B3 / BE AD BE EF 01 AA AA AA。
  2. APP 写 ota_flag=1 并系统复位。
  3. Bootloader 读到标志 1,向 C8T6 发 0x0D3 确认并开始 TP 服务。
  4. C8T6 收到确认后启动 YMODEM,向 PC 发字符 C。
  5. PC 发送文件;C8T6 累计 16×128 B,封装成 2057 B PDU,再经 ISO-TP 发给 Bootloader。
  6. Bootloader 重组 PDU,取出偏移和 2048 B 数据,擦除并写入对应 APP 页。
  7. YMODEM 完成后 C8T6 发命令字 0x02;Bootloader 清除升级标志并跳转新 APP。

1.2.1 实例项目分析

第 31 个工程 31usb_tool 运行在 C8T6,第 34 个工程 34Bootloader 运行在 ZET6。阅读时应同时打开两个 main.c,沿着同一个标志或 CAN ID 在两端来回追踪,而不是从头阅读协议库的每一行。

先追四个控制状态:ota_call_flag 表示桥接器等待 Bootloader 确认,ymodule_run_flag 表示允许进入 YMODEM 接收,Flash 中的 ota_flag 表示 ZET6 下次启动要升级,cantp_run_flag 表示 Bootloader 正在运行 ISO-TP 服务。四者位于不同 MCU 或不同存储介质,不能混为一个“升级标志”。

1.2.1.1 USB升级器代码分析

USART2 的 HAL_UART_Receive(..., 4, 0xFF) 是阻塞式接收,完整收到四字节才进入判断。当前代码验证 pData[0]、pData[2] 和 pData[3],没有验证 pData[1]==0x00;协议约定仍是 AF 00 01 FA。

C8T6 发出的第一条 CAN 命令目标是当前正在运行的 APP,因为此时 Bootloader 尚未接管。APP 并非“跳转到 Bootloader 地址”,而是写 Flash 标志后执行 HAL_NVIC_SystemReset();复位自然从 0x08000000 开始运行 Bootloader。

收到 Bootloader 的 0x0D3 后,桥接器只按 ID 判定确认并启动 YMODEM,没有核对载荷 BE AD BE EF 01。这能完成课堂实验,但更严谨的状态机应同时检查 DLC、魔数、命令字和会话状态。

YMODEM 接收逻辑还有三个值得看懂的边界:

  • 源码定义了 128 B 与 1024 B 两种包缓存,但桥接分支固定 memcpy(..., PACKET_SIZE),其中 PACKET_SIZE=128,所以本实验链路实际依赖 128 B 包。
  • file_all_num = filesize / PACKET_SIZE 后,源码用 file_all_num % PACKET_SIZE 判断是否加一;按含义本应检查文件大小对 128 的余数。对本课 310,132 B 文件恰好仍得到 2,423 个包,但这是偶然满足,不是通用正确写法。
  • 最后一批仍固定发送 2,048 B,缓冲区预先清零,所以文件末尾不足一页的部分会补 0 写入。Bootloader 没有记录真实镜像长度。

源码的镜像大小检查也在给 *p_size 赋 filesize 之前检查 *p_size,调用者初值为 0,因此当前检查不能真正阻止超大镜像。学习流程时必须知道这些限制,不能据“演示成功”推断所有边界都已处理。

1.2.1.2 Bootloader代码分析

Bootloader 的 ret==1 和 ret==0 分别对应等待升级与正常跳 APP;配置读取错误由初始化函数提前卡在错误循环,不会进入这两个分支。

ISO-TP 完成一批重组后,Bootloader 读取 4 字节偏移和 4 字节长度,把目标地址计算为 0x08008000 + pack_offset。课堂每批 pack_size=2048,正好是一页,并能被 /4 转为 512 个 32 位字。当前代码没有在写前检查 pack_offset + pack_size 是否越过 480 KiB APP 分区,也没有在整包完成后计算固件哈希或验证签名。

课程把慢速主要归因于每批 HAL_Delay(100)。对 310,132 B 文件共有约 ceil(310132/2048)=152 批,这些固定延时合计约 15.2 秒;而视频实测约 9 分钟,因此 100 ms 延时只解释一小部分。其余耗时来自 YMODEM 往返、阻塞式 ISO-TP 分帧与流控、CAN 软件轮询、逐页擦写、大量串口日志及实现细节,不能只归因于这一个延时。

传输结束控制帧到达后,Bootloader 立即清 ota_flag 并跳 APP。更完整的产品实现应先验证镜像总长度、目标范围和整体完整性;若校验失败则保留可恢复状态,不能启动半写入或损坏的 APP。本课先用于理解双 MCU 桥接和 Bootloader 状态流转,后续复用时应把这些边界检查列为必做项。