STM32F4的CAN升级方案 bootloader源代码,对应测试用app源代码,都是keil工程,代码有备注,也有使用说明。 带对应上位机可执行文件。 上位机vs2013开发(默认exe,源代码需要额外拿)

——从代码实现视角剖析 Bootloader 与 APP 协同工作机制


引言

在嵌入式系统开发中,远程固件升级(Firmware Over-The-Air, FOTA)能力是产品可维护性与生命周期管理的关键。本文聚焦于一套基于 STM32F407(正点原子探索者开发板)与 CAN 总线 的完整固件升级方案,深入剖析其 Bootloader 与 APP 双区协同架构 的代码实现细节。该方案不仅逻辑清晰、可靠性高,且具备良好的工程可移植性,适用于工业控制、车载设备等对稳定性要求严苛的场景。

STM32F4的CAN升级方案 bootloader源代码,对应测试用app源代码,都是keil工程,代码有备注,也有使用说明。 带对应上位机可执行文件。 上位机vs2013开发(默认exe,源代码需要额外拿)

本文将严格依据所提供的 说明文档.txt说明文档2.txt 以及 A049STM32F4的CAN升级方案merged.txt 中的代码片段,逐层解读其核心机制,重点落在 代码逻辑、内存布局、协议交互与状态控制 上。


一、系统整体架构与内存规划

1.1 Flash 地址空间划分

系统采用 双区架构,将内部 Flash 划分为两个逻辑区域:

  • Bootloader 区:起始地址 0x08000000(MCU 默认复位向量地址)
  • APP 区:起始地址 0x08008000(偏移 32KB)
  • 标志位区:关键控制地址 APPEXEFLAG_ADDR = 0x08007800

该标志位位于 Bootloader 与 APP 之间的 **保留扇区**(通常为 Sector 3),用于指示 APP 是否处于“可执行”状态。

main.h 中明确定义:

#define APP_START_ADDR      ((uint32_t)0x08008000)
#define APP_EXE_FLAG_ADDR   ((uint32_t)0x08007800)

1.2 标志位状态机设计

标志位采用 32 位魔数 实现状态控制:

含义 行为
0xFFFFFFFF Flash 擦除态(初始/无效) APP 首次运行时写入有效标志
0x78564312 APP 可执行标志 Bootloader 跳转至 APP
其他值(如 0x00000000 升级中或无效 Bootloader 不跳转,进入升级监听

该设计确保 无论 APP 是否存在或损坏,系统始终可进入 Bootloader 接收升级指令,这是本方案的核心鲁棒性保障。


二、Bootloader 启动与跳转逻辑

2.1 复位入口行为

Bootloader 的 main() 函数(或等效启动逻辑)首先检查标志位:

// 伪代码,源自说明文档
if (*(uint32_t*)APP_EXE_FLAG_ADDR == 0x78564312) {
    CAN_BOOT_JumpToApplication(APP_START_ADDR);
} else {
    // 进入 CAN 升级监听主循环
    CAN_Bootloader_Main();
}

2.2 跳转函数实现细节

A049...merged.txt 中可见跳转函数原型:

void CAN_BOOT_JumpToApplication(__IO uint32_t Addr)
{
    pFunction Jump_To_Application;
    __IO uint32_t JumpAddress;

    // 校验栈顶地址是否合法(位于 SRAM)
    if (((*(__IO uint32_t*)Addr) & 0x2FFE0000) == 0x20000000) {
        JumpAddress = *(__IO uint32_t*)(Addr + 4); // 复位向量 = 第二个字
        Jump_To_Application = (pFunction)JumpAddress;
        
        // 重定向中断向量表
        SCB->VTOR = Addr;
        
        // 设置主栈指针(MSP)
        __set_MSP(*(__IO uint32_t*)Addr);
        
        // 跳转
        Jump_To_Application();
    }
}

关键点

  • 检查 APP 起始地址的 栈顶值 是否落在 SRAM 区(0x20000000~0x2001FFFF),防止跳转到非法地址。
  • 显式设置 SCB->VTOR 以重定向中断向量表,避免中断服务例程错乱。
  • 使用函数指针跳转,实现干净的上下文切换。

三、APP 的自举与标志位管理

3.1 APP 启动流程

APP 的 main() 函数(见 app\User\main.c)首先重定向向量表,并检查标志位:

int main(void)
{
    SCB->VTOR = FLASH_BASE | 0x8000; // 0x08008000

    // 首次运行:写入执行标志
    if (*((uint32_t *)APP_EXE_FLAG_ADDR) == 0xFFFFFFFF) {
        Write_Flash(APP_EXE_FLAG_ADDR, 0x78564312);
    }

    // 正常业务逻辑 + CAN 指令监听
}

此处 `Write_Flash()` 应封装对内部 Flash 的编程操作(需先解锁、擦除扇区、写入、再上锁)。

3.2 APP 必须响应的指令

尽管 APP 是业务主体,但仍需实现两个关键 CAN 指令(见 can_app.c):

  • CMD_List.Check:返回固件版本与类型,供上位机识别设备状态。
  • CMD_List.Excute:收到后擦除标志位并软件复位,强制下次启动进入 Bootloader。
// APP 中的 CMD_List.Excute 处理逻辑(伪代码)
if (can_cmd == CMD_List.Excute) {
    Erase_Flash_Sector(APP_EXE_FLAG_ADDR); // 擦除标志位所在扇区
    NVIC_SystemReset(); // 软件复位
}

这使得上位机可在 APP 运行时发起升级流程,实现 无缝切换至 Bootloader


四、CAN 通信协议与指令处理

4.1 设备地址生成机制

设备 CAN ID 由芯片唯一 ID 动态计算(见 ReadCANAddress()):

uint16_t Read_CAN_Address(void)
{
    uint32_t sn = *(__IO uint32_t*)(0x1FFF7A10) +
                  *(__IO uint32_t*)(0x1FFF7A14) +
                  *(__IO uint32_t*)(0x1FFF7A18);
    uint32_t addr = (sn>>24) + (sn>>16) + (sn>>8) + sn;
    addr = 0x0134; // 实际使用中可固定或保留计算
    return addr;
}

该函数返回 16 位地址,用于构建 **29 位扩展帧 ID**。

4.2 扩展帧 ID 编码规则

CAN 报文使用 29 位扩展 ID,格式如下:

[31:16] = 设备地址(16位)
[15:4]  = 保留
[3:0]   = 命令码(4位)

在代码中体现为:

#define CMD_WIDTH 4
TxMessage.ExtId = (CAN_BOOT_GetAddrData() << CMD_WIDTH) | CMD_List.Write;

4.3 核心指令集(CBL_CMD_LIST)

can_app.h 中定义结构体:

typedef struct _CBL_CMD_LIST {
    unsigned char Erase;        // 0x00
    unsigned char WriteInfo;    // 0x01
    unsigned char Write;        // 0x02
    unsigned char Check;        // 0x03
    unsigned char SetBaudRate;  // 0x04
    unsigned char Excute;       // 0x05
    unsigned char CmdSuccess;   // 0x08
    unsigned char CmdFaild;     // 0x09
} CBL_CMD_LIST;

4.4 关键指令处理逻辑(Bootloader 侧)

(1)`Erase` 指令
// 擦除 APP 区域(0x08008000 起始的多个扇区)
CAN_BOOT_ErasePage(APP_START_ADDR, APP_END_ADDR);
(2)`WriteInfo` 指令
// 解析起始偏移与总长度
addr_offset = (Data[0]<<24) | ... | Data[3];
data_size   = (Data[4]<<24) | ... | Data[7];
start_addr  = APP_START_ADDR + addr_offset;
(3)`Write` 指令(带 CRC 校验)
// 累积数据至缓冲区 data_temp[1026]
for(i=0; i<pRxMessage->DLC; i++) {
    data_temp[data_index++] = pRxMessage->Data[i];
}

// 数据收满后校验并写入 Flash
if (data_index >= data_size) {
    crc_data = crc16_ccitt(data_temp, data_size - 2);
    if (crc_data == ((data_temp[data_size-2]<<8) | data_temp[data_size-1])) {
        FLASH_Unlock();
        ret = CAN_BOOT_ProgramDatatoFlash(start_addr, data_temp, data_size-2);
        FLASH_Lock();
        // 再次 CRC 校验写入结果
    }
}

**注意**:每包数据包含 **2 字节 CRC16-CCITT 校验码**,确保传输与写入双重可靠性。

(4)`Excute` 指令
// 擦除标志位所在扇区(0x08007800)
Erase_Flash_Sector(APP_EXE_FLAG_ADDR);
// 软件复位
NVIC_SystemReset();

五、应答机制与通信优化

文档明确指出:

“引导程序中的所有的应答报文,都是为了测试加的,完全可以不用应答报文,直接用 ID 区别正确和错误的应答,即就是 `TxMessage.DLC = 0;`”

这意味着:

  • 成功应答:发送 ExtId = (addr << 4) | CmdSuccessDLC = 0
  • 失败应答:发送 ExtId = (addr << 4) | CmdFaildDLC = 0

优势

  • 无数据负载,节省总线带宽
  • 通过 ID 直接判断结果,简化上位机解析逻辑

六、工程配置要点

6.1 APP 工程链接脚本(`.sct` 文件)

app\Project\Output\Project.sct 中:

LR_IROM1 0x08008000 0x00100000 {
    ER_IROM1 0x08008000 0x00100000 {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
        .ANY (+XO)
    }
}

确保代码从 0x08008000 开始链接。

6.2 Bootloader 工程

  • 默认从 0x08000000 链接
  • 实现 Flash 擦写、CAN 驱动、跳转逻辑
  • 不包含复杂业务,保持精简

七、总结:代码设计的工程智慧

本方案的代码实现体现了以下工程思想:

  1. 状态驱动:通过单一标志位控制启动路径,逻辑清晰无歧义。
  2. 双重校验:传输 CRC + 写入后 CRC,确保固件完整性。
  3. 协议极简:利用 CAN ID 编码命令与状态,无冗余数据。
  4. 双向控制:Bootloader 与 APP 均响应关键指令,支持运行时升级。
  5. 安全跳转:校验栈顶地址、重定向 VTOR、设置 MSP,防止跳转崩溃。

该代码不仅是一套功能完整的升级方案,更是一份优秀的嵌入式系统设计范本,值得在实际项目中借鉴与复用。

Logo

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

更多推荐