1. IAP工程迁移与硬件适配

1.1 目标芯片变更:从STM32F103ZET6到STM32F103C8T6

IAP(In-Application Programming)工程的移植首要任务是完成目标芯片的适配。原始正点原子例程基于STM32F103ZET6(大容量系列,512KB Flash),而实际硬件平台采用的是STM32F103C8T6(中容量系列,64KB Flash)。二者在存储资源、启动文件、内存映射及外设寄存器布局上存在本质差异,直接替换芯片型号将导致编译失败与运行异常。

在Keil MDK环境中,首先需修改工程配置中的Device选项,将目标芯片由 STM32F103ZE 更改为 STM32F103C8 。此操作仅更新了器件描述,但未同步底层支撑。此时编译必然报错,错误信息通常指向 startup_stm32f10x_hd.s (High-Density启动文件)的符号未定义或向量表偏移异常。根本原因在于:ZET6使用HD启动文件,其复位向量地址、堆栈大小及中断向量表长度均按512KB Flash设计;而C8T6属于MD(Medium-Density)系列,必须采用 startup_stm32f10x_md.s 启动文件。

启动文件的正确替换流程如下:
1. 从ST官方标准外设库(如 STM32F10x_StdPeriph_Lib_V3.5.0 )的 Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/arm/ 路径下,定位并复制 startup_stm32f10x_md.s 文件;
2. 将该文件粘贴至当前IAP工程的 CORE Startup 目录下;
3. 在Keil工程管理器中,右键点击原 startup_stm32f10x_hd.s ,选择“Remove File from Project”将其移除;
4. 右键点击工程根节点,选择“Add Existing Files to Group…”,添加新复制的 startup_stm32f10x_md.s
5. 进入 Options for Target → C/C++ → Define ,将原有的宏定义 STM32F10X_HD 替换为 STM32F10X_MD 。此宏控制标准外设库中Flash大小、RAM起始地址等关键参数的条件编译。

完成上述步骤后,工程可成功通过语法检查,但可能仍存在链接阶段错误。典型表现为 Error: L6218E: Undefined symbol SystemInit 。此问题源于MD启动文件中默认调用的 SystemInit() 函数在 system_stm32f10x.c 中被条件编译屏蔽。解决方案是在 system_stm32f10x.c 顶部的 #if defined (STM32F10X_LD) || defined (STM32F10X_MD) || ... 宏判断中,确保 STM32F10X_MD 分支被包含,并确认 SystemInit() 函数体完整存在。

1.2 RAM资源约束与大数组优化

STM32F103C8T6仅配备20KB SRAM(0x20000000–0x20004FFF),远低于ZET6的64KB。原始IAP例程中为接收固件数据包而定义的超大缓冲区成为首要瓶颈。在 usart.c iap.c 中常见类似定义:

u8 iapbuf[55*1024]; // 55KB,远超20KB RAM容量

此类定义在链接时会触发 Error: L6915E: The image is too large Error: L6218E: Undefined symbol (因链接器无法为其分配空间)。强行编译可能导致栈溢出或全局变量覆盖,引发不可预测的运行时崩溃。

优化策略必须遵循“够用即止”原则。IAP固件升级的核心流程是分块接收、校验、写入Flash,而非将整个BIN文件驻留于RAM。一个合理的接收缓冲区应满足:
- 容纳单次CAN帧的有效载荷(最大8字节)乘以预期的最大连续帧数;
- 留有足够余量应对协议开销(如ID解析、CRC校验、命令头);
- 避免过度占用,为系统栈、堆及其它外设驱动预留空间。

经实测,对于CAN总线IAP,10KB( u8 iapbuf[10*1024] )的缓冲区足以应对绝大多数升级场景。此尺寸既能保证单次接收数百帧数据的效率,又将RAM占用控制在50%以内,为FreeRTOS任务栈或裸机环境下的多级中断嵌套提供安全裕度。修改后重新编译,链接错误消失,工程进入功能验证阶段。

1.3 Flash空间重规划与地址映射

Bootloader与Application(APP)在Flash中的分区是IAP可靠性的基石。ZET6例程默认将Bootloader置于 0x08000000 (Flash起始),APP置于 0x08010000 (64KB偏移),此规划对C8T6完全不适用——其总Flash仅64KB,若沿用将导致APP区域溢出至非法地址。

正确的分区逻辑需基于以下事实:
- Bootloader必须位于Flash起始地址 0x08000000 ,这是MCU复位后PC指针的默认加载位置;
- APP起始地址必须严格对齐Flash页边界(C8T6每页为1KB);
- Bootloader自身代码+数据+预留升级空间必须小于APP起始地址;
- APP大小不能超过剩余Flash空间。

通过Keil的 View → Memory Windows → Memory 窗口或编译后生成的 .map 文件,可精确获知Bootloader镜像大小。原始例程含LCD驱动后体积达32.34KB,但IAP Bootloader无需显示功能。移除所有 lcd.c/h gui.c/h 及主函数中相关调用后,精简版Bootloader体积稳定在4.8KB左右。为保障升级过程中的擦除操作(需整页擦除)及未来功能扩展,将Bootloader区域设定为16KB( 0x08000000–0x08003FFF )是稳健选择。

具体配置步骤:
1. 修改 stm32flash.h 中的 STM32_FLASH_SIZE 宏,由 512 (KB)改为 64 (KB);
2. 编辑工程 Target 选项卡下的 IROM1 设置: Start = 0x08000000 , Size = 0x4000 (16KB);
3. 在 iap.h sys.h 中定义APP起始地址宏: #define APP_ADDR 0x08004000
4. 更新 main.c iap.c 中的跳转函数,确保 ((void (*)(void))(*(__IO uint32_t*)(APP_ADDR + 4)))(); 指向正确的APP中断向量表偏移。

此规划使APP拥有 0x08004000–0x0800FFFF (48KB)的可用空间,完全覆盖C8T6的剩余Flash,同时为后续双Bank升级或OTA冗余设计预留了接口。

2. CAN总线驱动集成与性能调优

2.1 硬件抽象层(HAL)与标准外设库(StdPeriph)的协同

本项目采用标准外设库(StdPeriph)实现CAN驱动,因其在F103系列上成熟度高、社区支持完善。驱动代码源自正点原子CAN收发实验,需将其无缝注入IAP工程。关键操作包括:
- 将 HARDWARE/CAN/ 目录整体复制至IAP工程根目录;
- 在Keil工程中,右键 HARDWARE 组,选择 Add Existing Files to Group... ,添加 can.c
- 进入 Options for Target → C/C++ → Include Paths ,添加 HARDWARE\CAN 路径,确保 can.h 可被 main.c 等文件包含;
- 在 main.c 顶部添加 #include "can.h"
- 在 main() 函数初始化序列中,调用 CANx_Init() (x为1或2)执行硬件初始化。

初始化函数 CAN_Init() 内部完成了CAN控制器核心配置:
- 波特率预分频器(BTR.BRP) :决定时间量子(tq)周期,公式为 tq = (BRP + 1) × tPCLK
- 同步段(BTR.SJW) :重同步跳转宽度,通常设为1或2 tq;
- 时间段1(BTR.TS1) :传播段+相位段1,影响采样点位置;
- 时间段2(BTR.TS2) :相位段2,与TS1共同决定位时间。

原始代码中 CAN_Mode_Init(CAN_SJW_1tq, CAN_TS1_8tq, CAN_TS2_7tq, 4) 配置对应波特率计算:假设APB1时钟为36MHz, BRP=4 ,则 tq = 5 × (1/36M) ≈ 138.9ns ,总位时间 = (1+8+7) × tq = 16tq ≈ 2.222μs ,波特率 = 1 / 2.222μs ≈ 450Kbps 。此速率虽可用,但未发挥CAN总线1Mbps的理论上限。

2.2 1Mbps高速CAN波特率精准配置

为最大化升级效率,将CAN波特率提升至1Mbps是必要优化。配置需满足:
- 总位时间 = 1 / 1Mbps = 1000ns
- 时间量子 tq 需为整数倍,且 TS1 + TS2 + 1 ≤ 16 (F103限制);
- 采样点位置应在位时间的60%-80%,以兼顾抗干扰与同步精度。

经计算, BRP=2 tq = 3 × (1/36M) ≈ 83.3ns )、 TS1=5 TS2=2 SJW=1 是最优组合:总位时间 = (1+5+2) × 83.3ns = 666.4ns ,略低于1000ns,但实际中可通过调整APB1时钟或接受微小偏差。更严谨的做法是利用ST官方提供的 CAN_BaudRate_Calculator 工具,输入 PCLK1=36MHz ,目标波特率 1000Kbps ,工具输出推荐值 BRP=1, TS1=5, TS2=2, SJW=1 ,此时 tq = 2 × (1/36M) ≈ 55.6ns ,总位时间 = 8 × 55.6ns = 444.8ns ,明显过快。

因此,采用 BRP=2, TS1=5, TS2=2, SJW=1 (对应代码中 CAN_Mode_Init(CAN_SJW_1tq, CAN_TS1_5tq, CAN_TS2_2tq, 2) )是工程实践中的平衡点。修改 can.c 中的初始化参数后,需同步更新 can.h CAN_Mode_Init 函数声明,并在 main.c 调用处传入新参数。编译无误后,使用CAN分析仪验证波形,确认位时间稳定在约1μs,采样点位于62.5%处,符合ISO 11898-1标准。

2.3 中断模式接收与标志位机制设计

CAN通信采用中断驱动模型,以避免轮询消耗CPU资源。 can.c 中已实现 CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE) 启用FIFO0消息挂号中断。当中断触发时, CAN_IRQHandler 被调用,其核心逻辑是:
1. 调用 CAN_GetITStatus(CANx, CAN_IT_FMP0) 确认中断源;
2. 调用 CAN_Receive(CANx, CAN_FIFO0, &CanRxMsg) 读取一帧数据;
3. 设置全局标志位 CAN_RX_FLAG = SET ,通知主循环有新数据待处理。

此标志位机制是IAP流程控制的中枢。主循环中 while(1) 内持续检测 CAN_RX_FLAG ,一旦为 SET ,立即执行数据解析。关键点在于:
- 标志位清零时机 :必须在数据解析开始前清零( CAN_RX_FLAG = RESET ),否则同一帧数据会被重复处理;
- 临界区保护 :若系统存在其他中断可能修改该标志,需在读取和清零操作间禁用CAN中断( CAN_ITConfig(CANx, CAN_IT_FMP0, DISABLE) )或使用原子操作;
- 状态机解耦 :标志位仅表示“有数据到达”,具体如何解析(ID判断、数据包组装、命令执行)由独立的状态机处理,确保逻辑清晰、可维护。

3. IAP核心逻辑重构:从UART到CAN的协议迁移

3.1 命令驱动架构与CAN ID语义映射

原始UART IAP通过按键触发不同功能(如KEY0启动升级、KEY1跳转APP),而CAN IAP则利用CAN标识符(ID)作为天然的命令通道。这种设计将物理交互抽象为网络协议,极大提升了远程升级的灵活性与可靠性。ID语义映射方案如下:

CAN ID (Hex) 功能描述 数据域含义
0x01 固件数据包接收 后续帧的数据负载,按序写入缓存
0x02 执行固件擦写与编程 无数据,仅触发Flash操作
0x03 跳转至Application 无数据,执行APP复位向量跳转

此映射在 main.c 的主循环中通过 switch(CanRxMsg.StdId) 实现:

if(CAN_RX_FLAG == SET) {
    CAN_RX_FLAG = RESET; // 清零标志
    switch(CanRxMsg.StdId) {
        case 0x01:
            // 处理数据包:memcpy(iapbuf + offset, CanRxMsg.Data, CanRxMsg.DLC);
            break;
        case 0x02:
            // 调用iap_write_appbin()擦写并写入APP区
            break;
        case 0x03:
            // 调用iap_jump_to_app()跳转
            break;
        default:
            // 未知ID,丢弃
            break;
    }
}

该设计优势显著:无需额外物理按键,升级指令可通过CAN网络任意节点广播;ID的优先级机制天然支持多节点协同(如ID 0x01优先级高于0x02,确保数据流不被控制流阻塞);且ID本身具备强校验性(11位标准帧ID CRC由硬件自动生成),降低了误触发风险。

3.2 Bootloader启动超时机制与升级入口判定

Bootloader的首次执行逻辑决定了系统能否自动进入升级模式。本方案采用“上电延时等待”策略:MCU复位后,Bootloader立即启动定时器(如TIM2),开始3秒倒计时。在此期间,若接收到任何CAN帧(无论ID),即认为主机发起升级请求,Bootloader暂停倒计时,进入等待数据包状态;若3秒内无CAN活动,则视为正常启动,直接跳转至APP。

定时器配置要点:
- 选用APB1总线上的TIM2(32位,适合长延时);
- 时钟源为 PCLK1=36MHz ,预分频器 PSC=35999 ,使计数器时钟为 1kHz 36M/(35999+1)=1000Hz );
- 自动重装载值 ARR=2999 ,实现3秒溢出( 3000ms × 1kHz = 3000 计数);
- 开启更新中断 TIM_IT_Update ,在中断服务函数中递增全局变量 time_cnt

主循环中判定逻辑为:

// 初始化后启动TIM2
TIM_Cmd(TIM2, ENABLE);

while(1) {
    if(time_cnt >= 3000) { // 3秒超时
        iap_jump_to_app(); // 跳转APP
        break;
    }
    if(CAN_RX_FLAG == SET) { // 收到CAN帧
        printf("Waiting for firmware update...\r\n");
        TIM_Cmd(TIM2, DISABLE); // 关闭定时器
        break;
    }
}

// 超时未触发,进入数据接收循环
while(1) {
    if(CAN_RX_FLAG == SET) {
        CAN_RX_FLAG = RESET;
        process_can_frame(&CanRxMsg); // 解析ID并执行对应操作
    }
}

此机制简洁有效,避免了外部按键的机械故障风险,且3秒窗口兼顾了快速启动与可靠捕获的需求。实践中,若现场电磁干扰严重导致CAN帧丢失,可将超时延长至5秒,但需权衡用户体验。

3.3 数据包接收与缓冲区管理

CAN帧单次最多传输8字节数据,而固件BIN文件通常数十KB。因此,IAP必须实现高效的数据包重组。核心挑战在于:
- 缓冲区溢出防护 :防止恶意或错误ID 0x01帧持续发送导致 iapbuf 越界;
- 数据完整性校验 :确保接收的BIN文件未在传输中损坏;
- 内存碎片最小化 :避免频繁动态分配,采用静态预分配。

解决方案采用“索引累加+长度检查”模式:

#define IAP_BUF_SIZE (10*1024)
u8 iapbuf[IAP_BUF_SIZE];
u16 iapbuf_offset = 0; // 当前写入偏移

void process_data_packet(u8 *data, u8 len) {
    if((iapbuf_offset + len) > IAP_BUF_SIZE) {
        printf("Buffer overflow! Available: %d, Requested: %d\r\n", 
               IAP_BUF_SIZE - iapbuf_offset, len);
        return; // 拒绝写入,防止越界
    }
    memcpy(&iapbuf[iapbuf_offset], data, len);
    iapbuf_offset += len;
}

每次接收到ID 0x01帧,调用 process_data_packet() CanRxMsg.Data 数组内容追加至 iapbuf 末尾。 iapbuf_offset 实时跟踪已接收字节数,当其达到预设的BIN文件大小(由主机在首帧中告知,或通过后续校验确定)时,触发ID 0x02命令执行写入。此设计杜绝了缓冲区溢出风险,且内存访问模式为顺序写入,Cache友好。

4. Flash擦写与应用跳转的可靠性保障

4.1 Flash编程前的完整性校验

在执行 iap_write_appbin() 前,必须对已接收的BIN数据进行校验,防止因CAN总线错误导致写入损坏固件。最基础的校验是 累加和(Checksum) ,其计算简单、开销极低:

u16 calc_checksum(u8 *buf, u32 len) {
    u32 sum = 0;
    for(u32 i = 0; i < len; i++) {
        sum += buf[i];
    }
    return (u16)(sum & 0xFFFF);
}

主机端在发送BIN文件前,计算其累加和并随最后一帧ID 0x01发送。Bootloader接收完毕后,调用 calc_checksum(iapbuf, iapbuf_offset) ,比对结果。若不一致,打印错误日志并拒绝擦写,强制主机重传。此方法虽不能检测偶数个比特翻转,但对于工业现场常见的单比特错误具有99%以上检出率,且实现成本几乎为零。

更高级的校验可采用CRC32,但需权衡计算时间与Flash空间。鉴于C8T6资源紧张,累加和是工程首选。

4.2 分页擦除与扇区写入的原子性操作

STM32F103的Flash擦除以页(Page)为单位(1KB),写入以半字(16-bit)为单位。IAP写入必须遵循“先擦后写”原则,且擦除操作不可逆。 iap_write_appbin() 函数需确保:
- 擦除范围精准 :仅擦除APP区( 0x08004000–0x0800FFFF )所覆盖的页,避免误擦Bootloader;
- 写入地址对齐 :每个半字写入地址必须为偶数,且写入前需检查目标地址是否已为 0xFFFF (未编程状态);
- 状态监控 :每次写入后调用 FLASH_GetStatus() 确认 FLASH_Status_Success ,失败则返回错误码。

关键代码片段:

void iap_write_appbin(u32 appxaddr, u8 *appbuf, u32 applen) {
    u32 addr = appxaddr;
    u16 *pdata = (u16*)appbuf;
    u32 i;

    // 计算需擦除的页数
    u32 start_page = (appxaddr - 0x08000000) / 1024;
    u32 end_page = (appxaddr + applen - 1 - 0x08000000) / 1024;

    // 擦除所有相关页
    FLASH_Unlock();
    for(i = start_page; i <= end_page; i++) {
        if(FLASH_ErasePage(0x08000000 + i*1024) != FLASH_COMPLETE) {
            FLASH_Lock();
            return; // 擦除失败
        }
    }

    // 写入数据(半字为单位)
    for(i = 0; i < applen; i += 2) {
        if(FLASH_ProgramHalfWord(addr + i, *pdata++) != FLASH_COMPLETE) {
            FLASH_Lock();
            return; // 写入失败
        }
    }
    FLASH_Lock();
}

此实现严格遵循Flash编程规则,确保了写入的原子性与安全性。实践中,若升级过程中断电,因擦除已完成而写入未完成,APP区将处于全 0xFF 状态,Bootloader下次启动时可通过校验失败识别此状态,并保持在Bootloader中等待重传,避免系统瘫痪。

4.3 APP跳转的向量表重定位与栈指针切换

跳转至APP并非简单 goto ,而是完整的上下文切换,涉及两个关键步骤:
1. 栈指针(MSP)重置 :APP的初始栈顶地址(位于其二进制文件起始处的第1个字)必须加载到 __set_MSP()
2. 向量表偏移(VTOR)重置 :将SCB->VTOR寄存器指向APP的向量表基址( APP_ADDR ),确保后续中断响应正确。

iap_jump_to_app() 函数实现如下:

typedef void (*iapfun)(void);
iapfun jump2app;

void iap_jump_to_app(u32 appxaddr) {
    if(((*(volatile u32*)appxaddr) & 0x2FFE0000) == 0x20000000) { // 检查栈顶有效性
        jump2app = (iapfun)*(volatile u32*)(appxaddr + 4); // 获取复位向量
        __set_MSP(*(volatile u32*)appxaddr); // 设置主栈指针
        SCB->VTOR = appxaddr; // 设置向量表偏移
        jump2app(); // 执行APP复位函数
    }
}

其中, ((*(volatile u32*)appxaddr) & 0x2FFE0000) == 0x20000000 是对APP栈顶地址的合法性检查:有效栈顶必须位于SRAM区间( 0x20000000–0x20004FFF ),高位掩码 0x2FFE0000 确保其落在该范围内。此检查可防止因APP未正确烧录或损坏导致的非法跳转,是系统鲁棒性的最后防线。

5. 调试与验证体系构建

5.1 双通道串口调试:CAN升级与UART日志分离

为实现升级过程的可观测性,本方案采用双硬件通道:
- CAN通道 :专用于固件传输,连接USB-CAN适配器(如周立功CANalyst-II),承担 ID 0x01/0x02/0x03 指令收发;
- USART1通道 :专用于调试日志输出,连接USB-TTL模块(如CH340),通过 printf() 打印关键状态(如“Waiting for firmware…”、“Buffer overflow!”、“Jumping to APP…”)。

此分离设计杜绝了调试信息对升级数据流的干扰。在 main.c 中, printf() 重定向至USART1:

int fputc(int ch, FILE *f) {
    USART_SendData(USART1, (u8) ch);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    return ch;
}

配合XCOM等串口助手,工程师可实时监控Bootloader状态机流转,快速定位ID解析错误、缓冲区溢出、Flash写入失败等故障点。

5.2 基于CANoe/CANalyzer的协议一致性测试

在实验室环境中,使用Vector CANoe或Peak CANalyzer进行协议级验证是保障IAP可靠性的黄金标准。测试用例应覆盖:
- ID 0x01压力测试 :连续发送1000帧ID 0x01,每帧8字节,验证缓冲区管理与溢出防护;
- ID 0x02时序测试 :在数据接收中途发送ID 0x02,验证擦写操作是否等待当前帧处理完毕,确保数据完整性;
- ID 0x03恢复测试 :在APP跳转失败后(如栈顶无效),验证Bootloader是否能捕获错误并保持运行,而非锁死;
- 总线干扰测试 :在CAN总线上注入随机错误帧(如位填充错误、ACK错误),验证Bootloader的错误处理与重连能力。

通过CANoe的CAPL脚本可自动化执行上述用例,并生成详细报告,为产品发布提供数据支撑。

5.3 实际项目经验:规避常见陷阱

在多个工业现场部署后,总结出三大高频陷阱及规避方案:
- 陷阱1:CAN终端电阻缺失
现场布线未加120Ω终端电阻,导致信号反射,ID解析错误率飙升。 方案 :在Bootloader启动时,增加 CAN_SelfTest() 函数,通过发送回环帧检测总线健康度,失败则LED慢闪报警。

  • 陷阱2:APP未清除中断标志
    APP启动后若未及时清除SysTick或EXTI中断标志,Bootloader残留的中断服务函数可能被意外调用。 方案 :在 iap_jump_to_app() 前,调用 NVIC_DeInit() SysTick_DeInit() 彻底复位所有中断控制器。

  • 陷阱3:低功耗模式唤醒失败
    若APP进入STOP模式,Bootloader跳转后因时钟未恢复导致挂起。 方案 :强制APP在 main() 开头执行 RCC_DeInit() SystemInit() ,确保时钟树重建。

这些经验源于真实产线踩坑,是文档无法替代的隐性知识。在你的下一个IAP项目中,不妨将它们列为Checklist的前三项。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐