STM32U3 FDCAN IAP 升级思路与实现
本教程说明 STM32U3C5ZITxQ 上基于 FDCAN1 的 IAP 升级方案。Bootloader 固定位于 0x08000000,APP 从 0x08010000 运行,上位机通过标准 CAN 帧完成固件下载、CRC 校验和跳转。
对于FDCAN配置使用以及性能测试 先前教程已经做过测试 bootloader工程前前期教程也已经讲过 可以参考先前测评
当前 FDCAN1 配置为 Classic CAN,1 Mbps。整条链路包含三部分:
bootloader:启动决策、APP 校验、CAN IAP 状态机、Flash 擦写。
APP:链接到 Bootloader 之后的测试应用,并响应上位机的“进入 Bootloader”命令。
tool:PyQt5 上位机,负责解析固件、生成 CRC32、按协议逐帧发送。
1. 工程结构
U3_FDCAN_IAP/
├─ bootloader/
│ ├─ Core/Inc/boot_config.h
│ ├─ Core/Src/main.c
│ ├─ Core/Src/boot_app.c
│ ├─ Core/Src/iap_can.c
│ └─ Core/Src/iap_flash.c
├─ APP/
│ ├─ Core/Src/main.c
│ ├─ Core/Src/boot_app.c
│ └─ Core/Src/system_stm32u3xx.c
└─ tool/
├─ can_iap_gui.py
├─ requirements.txt
└─ build_exe.bat
核心配置集中在 boot_config.h,Bootloader 和 APP 共用相同定义。
2. Flash 布局
| 区域 |
地址 |
说明 |
| Bootloader |
0x08000000 |
固定 64 KB |
| APP |
0x08010000 |
APP 链接地址和运行地址 |
| APP 元信息 |
0x081FF000 |
最后一个 4 KB Flash page |
关键宏:
#define BOOT_FLASH_BASE_ADDR FLASH_BASE
#define BOOT_FLASH_SIZE_BYTES FLASH_SIZE
#define BOOT_FLASH_END_ADDR (BOOT_FLASH_BASE_ADDR + BOOT_FLASH_SIZE_BYTES)
#define BOOTLOADER_SIZE_BYTES (64U * 1024U)
#define APP_FLASH_ADDR (BOOT_FLASH_BASE_ADDR + BOOTLOADER_SIZE_BYTES)
#define APP_METADATA_ADDR (BOOT_FLASH_END_ADDR - FLASH_PAGE_SIZE)
#define APP_MAX_SIZE_BYTES (APP_METADATA_ADDR - APP_FLASH_ADDR)
Bootloader scatter 文件将 IROM 限制为 0x08000000 ~ 0x0800FFFF。APP scatter 文件将 IROM 设置为 0x08010000,长度为 0x001EF000。按 2 MB Flash、4 KB page 计算,APP 可用区域约为 1980 KB。
元信息与 APP 代码分开存放,避免 Bootloader 通过扫描代码段判断 APP 有效性。最后一个 Flash page 用于保存:
typedef struct
{
uint32_t magic;
uint32_t app_addr;
uint32_t app_size;
uint32_t app_crc;
uint32_t app_version;
uint32_t valid;
uint32_t reserved0;
uint32_t reserved1;
} IapFlash_AppInfoTypeDef;
3. 升级链路
上位机
|
| USB-CAN Serial / python-can
v
FDCAN1 (PB8 RX, PB9 TX)
|
| 0x301 Host -> Boot
| 0x302 Boot -> Host
v
Bootloader IAP 状态机
|
| erase / write / verify
v
STM32U3 Flash
FDCAN1 当前参数:
| 参数 |
值 |
| Kernel clock |
96 MHz |
| NominalPrescaler |
4 |
| TSEG1 |
19 |
| TSEG2 |
4 |
| SJW |
4 |
| Bitrate |
1 Mbps |
Bitrate = 96 MHz / 4 / (1 + 19 + 4) = 1 Mbps。
FDCAN RX 过滤使用掩码模式接收所有标准数据帧,由上层根据 CAN ID 再过滤:
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterID1 = 0x000U;
sFilterConfig.FilterID2 = 0x000U;
发送时使用 Classic CAN 数据帧,FDFormat = FDCAN_CLASSIC_CAN,单帧最多 8 字节。
4. 启动决策
Bootloader 不使用“上电延时”策略,而是在复位后做确定性判断:
复位
├─ SRAM 魔术字存在?停留 Bootloader
├─ PC13 按下?停留 Bootloader
├─ APP 元信息和 CRC 有效?跳转 APP
└─ 否则初始化 CAN IAP
对应主流程:
if (Bootloader_ShouldStay() == 0U)
{
if (BOOT_APP_IsValid() != 0U)
{
BOOT_APP_Jump();
}
}
Bootloader_Init();
while (1)
{
Bootloader_Process();
}
Bootloader_ShouldStay():
static uint8_t Bootloader_ShouldStay(void)
{
if (Bootloader_TakeAppRequest() != 0U)
{
return 1U;
}
if (Bootloader_IsButtonPressed() != 0U)
{
return 1U;
}
return 0U;
}
SRAM 魔术字方案:
static uint8_t Bootloader_TakeAppRequest(void)
{
volatile uint32_t *magic = (volatile uint32_t *)IAP_BOOT_MAGIC_ADDR;
if (*magic == IAP_APP_BOOT_MAGIC)
{
*magic = 0U;
return 1U;
}
return 0U;
}
使用 SRAM 而非 Flash 标志位的好处是不增加 Flash 擦写磨损,且 Bootloader 读取后清零,不会让每次复位都重复进入升级模式。
5. APP 有效性校验与跳转
IAP_FLASH_IsAppValid() 的检查项:
- 元信息
magic 必须为 APP_INFO_MAGIC。
- 元信息
valid 必须为 APP_INFO_VALID。
app_addr 必须等于 APP_FLASH_ADDR。
app_size 必须大于 0 且不超过 APP_MAX_SIZE_BYTES。
- APP 初始 MSP 必须位于
0x20000000 ~ 0x20040000。
- APP ResetHandler 必须位于 APP 区。
- APP 区
app_size 字节的 CRC32 必须匹配元信息中的 app_crc。
跳转实现:
void BOOT_APP_Jump(void)
{
uint32_t appStack = *(uint32_t *)APP_FLASH_ADDR;
uint32_t appReset = *(uint32_t *)(APP_FLASH_ADDR + 4U);
AppEntryFunc appEntry = (AppEntryFunc)appReset;
__disable_irq();
HAL_DeInit();
SysTick->CTRL = 0U;
SysTick->LOAD = 0U;
SysTick->VAL = 0U;
for (uint32_t i = 0U; i < 8U; i++)
{
NVIC->ICER[i] = 0xFFFFFFFFU;
NVIC->ICPR[i] = 0xFFFFFFFFU;
}
SCB->VTOR = APP_FLASH_ADDR;
__set_MSP(appStack);
__enable_irq();
appEntry();
}
6. 进入升级模式
6.1 APP 远程唤醒
APP 正常运行时,上位机先发送 APP 唤醒命令:
| 方向 |
CAN ID |
数据 |
| 上位机 -> APP |
0x303 |
A5 |
| APP -> 上位机 |
0x304 |
ACK |
APP 收到命令后:
- 发送 ACK。
- 向
0x2003F000 写入魔术字 0x424C4455。
- 延时 50 ms,确保 ACK 有足够时间发出。
- 调用
NVIC_SystemReset()。
static void APP_RequestBootloader(void)
{
volatile uint32_t *magic = (volatile uint32_t *)IAP_BOOT_MAGIC_ADDR;
*magic = IAP_APP_BOOT_MAGIC;
NVIC_SystemReset();
}
6.2 PC13 强制进入
按住 PC13 上电或复位时,BSP_PB_GetState(BUTTON_USER) 返回高电平,Bootloader 直接停留升级模式。该方式用于 APP 损坏或无法响应唤醒命令的场景。
7. IAP 协议
7.1 CAN ID
| CAN ID |
方向 |
用途 |
0x301 |
上位机 -> Bootloader |
IAP 命令帧和 DATA 帧 |
0x302 |
Bootloader -> 上位机 |
ACK/NACK 响应 |
0x303 |
上位机 -> APP |
APP 进入 Bootloader 命令 |
0x304 |
APP -> 上位机 |
APP ACK |
7.2 命令定义
| 命令 |
值 |
说明 |
GET_INFO |
0x01 |
读取当前 APP 元信息 |
ENTER_BOOT |
0x02 |
初始化 IAP 会话 |
ERASE_APP |
0x03 |
擦除 APP 区 |
DOWNLOAD_START |
0x04 |
分三次下发 size、crc32、version |
DATA |
0x05 |
上传固件数据 |
DOWNLOAD_END |
0x06 |
结束下载 |
VERIFY |
0x07 |
计算 CRC 并保存有效元信息 |
RUN_APP |
0x08 |
校验通过后跳转 APP |
ABORT |
0x09 |
取消当前会话 |
7.3 响应帧
所有响应帧统一 8 字节:
Byte0: cmd
Byte1: rsp,ACK=0x79,NACK=0x1F,BUSY=0x7F
Byte2: status
Byte3: reserved
Byte4~7: value,小端
状态码:
| 状态 |
值 |
OK |
0x00 |
BAD_CMD |
0x01 |
BAD_STATE |
0x02 |
BAD_LENGTH |
0x03 |
BAD_ADDRESS |
0x04 |
SEQ_ERROR |
0x05 |
FLASH_ERROR |
0x06 |
CRC_ERROR |
0x07 |
APP_INVALID |
0x08 |
TIMEOUT |
0x09 |
7.4 DATA 帧
Classic CAN 单帧最多 8 字节,因此 DATA 帧固定占用 3 字节协议头,固件载荷为 5 字节:
Byte0: CMD_DATA
Byte1~2: seq,小端
Byte3~7: 固件数据
最后一包可以小于 5 字节。上位机必须使用真实 DLC,不能补齐到 8 字节,否则 Bootloader 按真实长度计算后会返回 BAD_LENGTH。
7.5 升级时序
上位机 Bootloader
| GET_INFO / ENTER_BOOT |
|------------------------------------->|
| ACK |
|<-------------------------------------|
| DOWNLOAD_START: size/crc/version |
|------------------------------------->|
| ACK |
|<-------------------------------------|
| ERASE_APP |
|------------------------------------->|
| ACK |
|<-------------------------------------|
| DATA seq=0..N |
|------------------------------------->|
| ACK value=next_seq |
|<-------------------------------------|
| DOWNLOAD_END |
|------------------------------------->|
| ACK |
|<-------------------------------------|
| VERIFY |
|------------------------------------->|
| ACK |
|<-------------------------------------|
| RUN_APP |
|------------------------------------->|
| 跳转 APP |
8. Bootloader 状态机
iap_can.c 使用状态机限制命令顺序:
typedef enum
{
IAP_STATE_IDLE = 0,
IAP_STATE_READY,
IAP_STATE_ERASED,
IAP_STATE_RECEIVING,
IAP_STATE_DONE,
IAP_STATE_VERIFIED
} IapStateTypeDef;
状态转换:
IDLE -> ENTER_BOOT -> READY
READY -> DOWNLOAD_START -> READY
READY -> ERASE_APP -> ERASED
ERASED -> DATA -> RECEIVING
RECEIVING -> DATA -> RECEIVING
RECEIVING -> DOWNLOAD_END -> DONE
DONE -> VERIFY -> VERIFIED
VERIFIED -> RUN_APP -> 跳转 APP
DATA 处理会同时校验状态、seq、DLC 和长度范围:
case IAP_CMD_DATA:
if ((iap_state != IAP_STATE_ERASED) && (iap_state != IAP_STATE_RECEIVING))
{
IAP_SendResponse(cmd, IAP_RSP_NACK, IAP_STATUS_BAD_STATE, expected_seq, received_bytes);
break;
}
if (seq != expected_seq)
{
IAP_SendResponse(cmd, IAP_RSP_NACK, IAP_STATUS_SEQ_ERROR, expected_seq, received_bytes);
break;
}
len = frame->dlc - 3U;
if ((frame->dlc < 4U) || (len > IAP_DATA_PAYLOAD_BYTES) ||
((received_bytes + len) > fw_size))
{
IAP_SendResponse(cmd, IAP_RSP_NACK, IAP_STATUS_BAD_LENGTH, expected_seq, received_bytes);
break;
}
if (IAP_FLASH_Write(received_bytes, &frame->data[3], len) != HAL_OK)
{
IAP_SendResponse(cmd, IAP_RSP_NACK, IAP_STATUS_FLASH_ERROR, expected_seq, HAL_FLASH_GetError());
break;
}
received_bytes += len;
expected_seq++;
iap_state = IAP_STATE_RECEIVING;
IAP_SendResponse(cmd, IAP_RSP_ACK, IAP_STATUS_OK, (uint16_t)(expected_seq - 1U), expected_seq);
break;
DATA ACK 的 value 返回 expected_seq,即下一包应使用的序号。上位机可以据此确认每一包已写入。
会话超过 IAP_SESSION_TIMEOUT_MS(10 秒)没有新帧时,IAP_CAN_Process() 会返回 TIMEOUT NACK,并把状态恢复为 IDLE。
9. Flash 写入实现
STM32U3 Flash 编程单位为 8 字节 double-word,因此 CAN 收到的数据先进入 8 字节缓存:
#define IAP_FLASH_WRITE_UNIT 8U
if (write_cache_len == IAP_FLASH_WRITE_UNIT)
{
HAL_FLASH_Unlock();
FLASH_WriteDoubleWord(write_cache_addr, write_cache, IAP_FLASH_WRITE_UNIT);
HAL_FLASH_Lock();
write_cache_addr += IAP_FLASH_WRITE_UNIT;
write_cache_len = 0U;
for (uint8_t i = 0U; i < IAP_FLASH_WRITE_UNIT; i++)
{
write_cache[i] = 0xFFU;
}
}
写入前先使 APP 元信息无效:
HAL_StatusTypeDef IAP_FLASH_Begin(uint32_t size, uint32_t crc, uint32_t version)
{
if ((size == 0U) || (size > APP_MAX_SIZE_BYTES))
{
return HAL_ERROR;
}
download_size = size;
download_crc = crc;
download_version = version;
return IAP_FLASH_SaveAppInfo(0U, 0U, 0U, APP_INFO_INVALID);
}
擦除范围是 APP_FLASH_ADDR 到 APP_METADATA_ADDR。擦除按页进行,并处理 Bank 边界:
static uint32_t FLASH_GetBank(uint32_t addr)
{
if (addr < (FLASH_BASE + FLASH_BANK_SIZE))
{
return FLASH_BANK_1;
}
return FLASH_BANK_2;
}
static uint32_t FLASH_GetPage(uint32_t addr)
{
if (addr < (FLASH_BASE + FLASH_BANK_SIZE))
{
return (addr - FLASH_BASE) / FLASH_PAGE_SIZE;
}
return (addr - (FLASH_BASE + FLASH_BANK_SIZE)) / FLASH_PAGE_SIZE;
}
下载结束时会补齐最后不足 8 字节的数据,重新计算 CRC32,只有 CRC 匹配才写入有效元信息:
crc = IAP_FLASH_CalcCrc32(APP_FLASH_ADDR, download_size);
if (crc != download_crc)
{
return HAL_ERROR;
}
return IAP_FLASH_SaveAppInfo(download_size, download_crc, download_version, APP_INFO_VALID);
CRC32 使用标准 IEEE CRC32 算法,与上位机 binascii.crc32() 一致。这样 Bootloader 与上位机不需要额外约定 CRC 变体。
升级可靠性来自以下顺序:
元信息标记无效
-> 擦除 APP 区
-> 逐包写入
-> 补齐最后 double-word
-> 计算 CRC
-> 校验通过后标记有效
升级中断时 APP 元信息不会变成有效状态,因此 Bootloader 不会跳转半包固件。
10. APP 工程要点
APP 必须单独链接到 0x08010000,并设置向量表偏移:
#define VECT_TAB_BASE_ADDRESS FLASH_BASE
#define VECT_TAB_OFFSET 0x00010000U
system_stm32u3xx.c 中的初始化代码会写入:
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
APP 启动后会打印 EnterAPP 和当前 VTOR,用于确认 Bootloader 跳转正确。测试 APP 同时初始化 FDCAN,用于接收 0x303/A5 唤醒命令。
11. 上位机实现
上位机核心流程:
- 加载
.bin 或解析 Intel HEX。
- 计算固件 size 和 CRC32。
- 可选发送
0x303/A5 唤醒 APP。
- 与 Bootloader 执行 IAP 协议。
Intel HEX 解析要求:
- 支持记录类型
00、01、02、04。
- 忽略
03、05。
- 校验每行 checksum。
- 地址必须从
0x08010000 或之后开始。
- 从
0x08010000 生成连续镜像,空洞用 0xFF 填充。
DATA 发送代码:
chunk = firmware[offset:offset + DATA_PAYLOAD_BYTES]
frame = bytes([CMD_DATA, seq & 0xFF, (seq >> 8) & 0xFF]) + chunk
rsp = self.command(CMD_DATA, frame, f"DATA seq={seq}", quiet=True)
next_seq = rsp["value"]
if next_seq != (seq + 1):
raise IapError(f"DATA ACK next_seq 异常: got {next_seq}, expect {seq + 1}")
上位机支持两种后端:
USB-CAN Serial:串口 921600,可自动设置 CAN 波特率 1000 kbps。
python-can:通过 interface、channel、bitrate 接入其他 CAN 设备。
运行上位机:
cd tool
py -m pip install -r requirements.txt
py can_iap_gui.py
12. 编译与部署
- 打开
bootloader/MDK-ARM/FDCAN_U3.uvprojx,target 为 FDCAN_U3。
- 编译并下载 Bootloader HEX。
- 打开
APP/MDK-ARM/FDCAN_U3.uvprojx,target 为 FDCAN_U3_APP。
- 编译 APP HEX。
- 第一次可通过 ST-Link 烧录 APP,之后通过 CAN IAP 升级。
命令行编译示例:
# 在 U3_FDCAN_IAP 工程根目录执行
$repo = (Get-Location).Path
& "C:\Keil_v5\UV4\UV4.exe" -j0 -b "$repo\bootloader\MDK-ARM\FDCAN_U3.uvprojx" -t "FDCAN_U3"
& "C:\Keil_v5\UV4\UV4.exe" -j0 -b "$repo\APP\MDK-ARM\FDCAN_U3.uvprojx" -t "FDCAN_U3_APP"
13. 验证项
| 验证项 |
操作 |
预期结果 |
| 强制进入 Bootloader |
按住 PC13 复位 |
串口显示Stay in bootloader |
| APP 正常启动 |
复位且 APP 有效 |
串口显示EnterAPP,VTOR 为 0x08010000 |
| APP 远程唤醒 |
上位机发送0x303/A5 |
APP ACK,随后复位进入 Bootloader |
| 正常升级 |
选择 APP HEX/BIN 后开始 |
所有命令 ACK,最终verify ok |
| 乱序 DATA |
修改 seq 后发送 |
返回SEQ_ERROR |
| 错误长度 |
最后一包补齐到 8 字节 |
返回BAD_LENGTH |
| 错误 CRC |
修改固件 CRC |
返回CRC_ERROR |
| 中断恢复 |
下载过程中断电 |
元信息无效,不会跳转损坏 APP |
预期日志:
[BOOT] CAN IAP Bootloader v1.0.0
[BOOT] Boot: 0x08000000 + 64 KB, App: 0x08010000, Meta: 0x081FF000
[BOOT] Stay in bootloader, waiting CAN IAP command
[IAP] enter boot
[IAP] erase app area...
[IAP] erase done
[IAP] download done bytes=...
[IAP] verify ok
[BOOT] Jump to app @0x08010000
EnterAPP
[APP] IAP test app, VTOR=0x08010000
14. 限制与扩展
当前实现的主要限制:
- Classic CAN 每帧只有 8 字节,DATA 帧实际固件载荷为 5 字节,吞吐偏低。
- 每个 DATA 帧都等待 ACK,可靠性高但会进一步降低吞吐。
- 没有固件加密和签名校验,不适合直接作为量产安全升级方案。
- 没有双 Bank 或回滚区,升级失败时需要重新下载或通过 PC13 恢复。
0x2003F000 魔术字地址依赖 APP 不覆盖该区域,产品化时应使用专用 RAM 或备份寄存器。
可扩展方向:
- 使用 FDCAN FD 模式,增大单帧数据载荷。
- 使用滑动窗口或连续下载模式,减少 ACK 往返。
- 增加 Bootloader 自升级、版本兼容检查和双 Bank 切换。
- 增加签名、加密和 Secure Boot 支持。