STM32F1的串口在线升级方案

项目背景

使用在线升级的原因在于,当单片机固定于设备内部且不具备使用编程器直接烧录程序的情况时,发挥着巨大的作用。在线升级方案也有很多种比如通过串口、蓝牙、CAN总线等等,通过不同的通信方式将需要的APP程序传输到引导程序进行处理(简称IAP)。IAP收到数据过后将数据存入制定位置的内存中进行保存。本项目采用标准库函数进行开发,适配于库函数的项目使用。

IAP和APP程序的区别

IAP就是引导程序,用于跳转到APP程序和接收APP程序bin文件实现在线升级,本次项目IAP地址分区为0x08000000 到 0x08004000 处于IAP程序存储地址,APP地址分区为0x08004000 到 更具单片机型号确定。

设置现有APP工程适配升级使用

1.在APP程序主函数开始加入如下这段代码

SCB->VTOR = FLASH_BASE | 0x4000; //设置中断向量表偏移量 __enable_irq();

2.在软件上勾选C99

3.如果编译代码报错是因为勾选了C99 C99要求函数前要void ,因为这里采用的是正点原子的工程制作的。

4.输入如下指令 支持bin文件生成 fromelf --bin -o "$L@L.bin" "#L"

5.设置偏移地址如下


6.对仿真调试进行设计

IAP程序介绍

IAP升级的原理是采用串口调试助手,每次发送1KB升级程序然后延时1500ms继续发送数据,当发送的最后一段数据不足1KB,最后在发送一条结束升级指令即可完成整个升级流程。

代码如下

#include "led.h" #include "delay.h" #include "sys.h" #include "usart.h" #include "stmflash.h" /* 2026.7.9 基于串口的简易程序在线升级(IAP)代码 功能: 1. 串口1(PA9-TX, PA10-RX) 波特率9600 2. 启动升级指令: BE 53 54 41 52 54 66 ED 3. 结束信号指令: AE A3 A4 A1 A2 A4 A6 AD 4. APP起始地址: 0x08004000 5. 上电后10秒倒计时, 每秒上报剩余时间; 期间收到启动升级指令进入升级模式, 否则倒计时结束后跳转APP 6. 升级模式: 每帧接收1024字节写入Flash, 收到结束指令时 把最后一段(不足1024字节)补齐到1024字节写入, 随后跳转APP */ //---------------------- 参数定义 ---------------------- #define APP_ADDR 0x08004000 // APP程序起始地址 #define BLOCK_SIZE 1024 // 每帧数据长度(字节) #define HALF_PER_BLOCK (BLOCK_SIZE/2) // STMFLASH_Write按半字写入 #define BLOCK_PAUSE_MS 800 // 传输暂停判定(小于PC端包间隔1500ms, 用于识别"最后一段") // 启动升级指令 static const u8 START_CMD[8] = {0xBE,0x53,0x54,0x41,0x52,0x54,0x66,0xED}; // 结束信号指令 static const u8 END_CMD[8] = {0xAE,0xA3,0xA4,0xA1,0xA2,0xA4,0xA6,0xAD}; //---------------------- 局部函数声明 ---------------------- static u8 start_match_feed(u8 b); static void jump_to_app(u32 app_addr); static void upgrade_mode(void); //---------------------- 启动指令滑动匹配 ---------------------- // 每收到1字节喂入, 若最近8字节与START_CMD完全一致则返回1 static u8 sb[8]; static u8 start_match_feed(u8 b) { u8 i; for (i = 0; i < 7; i++) sb[i] = sb[i + 1]; sb[7] = b; for (i = 0; i < 8; i++) if (sb[i] != START_CMD[i]) return 0; return 1; } //---------------------- 结束指令匹配 ---------------------- // p 指向8字节数据, 与结束信号指令 END_CMD 完全一致则返回1 static u8 is_end_cmd(u8 *p) { u8 i; for (i = 0; i < 8; i++) if (p[i] != END_CMD[i]) return 0; return 1; } //---------------------- 跳转到APP ---------------------- typedef void (*pFunction)(void); static void jump_to_app(u32 app_addr) { u32 stack_top = *(vu32 *)app_addr; // 校验栈顶指针是否落在SRAM地址范围(0x20000000 ~ 0x2001FFFF 附近) if ((stack_top & 0x2FFE0000) == 0x20000000) { pFunction jump = (pFunction)(*(vu32 *)(app_addr + 4)); __disable_irq(); // 跳转前关闭中断, 避免中途响应 SCB->VTOR = app_addr; // 重定位中断向量表到APP __set_MSP(stack_top); // 设置主堆栈指针 __enable_irq(); // 重新开启全局中断, 保证APP能响应中断 jump(); // 跳转 } // APP无效则不跳转, 卡死等待 while (1); } //---------------------- 升级模式 ---------------------- // 策略: 从中断环形缓冲持续取字节。 // - 每凑满 1024 字节立即写入Flash并回送反馈(不受包间隔影响, 每块都有反馈) // - 出现传输暂停(>BLOCK_PAUSE_MS)视为"最后一段"已传完, 置等待END标志 // - 结束指令(8字节)按内容匹配; 收到后把最后一段补齐到1024字节写入, 然后跳转 static void upgrade_mode(void) { u32 addr = APP_ADDR; static u16 frame16[HALF_PER_BLOCK]; // 接收缓冲(1024字节, u16对齐, 满足STMFLASH_Write要求) u8 *frame = (u8 *)frame16; // 用static避免占用栈, 防止栈溢出 u16 frame_len = 0; // 当前已缓存(尚未成块)的字节数 u8 pend_state = 0; // 1: 已收到最后一段(传输暂停), 等待END指令 u8 b; u16 i; u32 waited; Uart1_FlushRx(); // 清空启动指令后残留的字节 printf("已进入升级模式, 请发送bin文件(每包1024字节 间隔1500ms)\r\n"); while (1) { waited = 0; while (!Uart1_GetByte(&b)) // 缓冲区无数据时等待 { delay_ms(1); waited++; if (frame_len > 0 && waited >= BLOCK_PAUSE_MS) { // 传输暂停: 当前frame为最后一段(不足1024字节) if (frame_len >= 8 && is_end_cmd(frame + frame_len - 8)) { // 暂停时的内容本身就是结束指令(无数据残留或残留已处理) frame_len -= 8; if (frame_len > 0) { for (i = frame_len; i < BLOCK_SIZE; i++) frame[i] = 0xFF; STMFLASH_Write(addr, frame16, HALF_PER_BLOCK); addr += BLOCK_SIZE; } printf("升级完成! 共写入 %lu 字节, 即将跳转到APP。\r\n", (unsigned long)(addr - APP_ADDR)); delay_ms(200); jump_to_app(APP_ADDR); return; } pend_state = 1; // 是最后一段数据, 暂存, 等待END指令 break; // 跳出取字节循环, 回到while(1)等待END } if (frame_len == 0 && waited >= 3000) waited = 0; // 首字节等待较长, 不退出升级 } if (frame_len > 0 && waited >= BLOCK_PAUSE_MS) { continue; // 已置等待END标志, 继续循环等待END到达 } frame[frame_len++] = b; // 收到一个字节, 存入缓冲 if (frame_len == BLOCK_SIZE) // 满1024字节: 完整数据块, 立即写入 { STMFLASH_Write(addr, frame16, HALF_PER_BLOCK); addr += BLOCK_SIZE; frame_len = 0; pend_state = 0; printf("已写入 %lu 字节 @0x%08X (本次写入 %u 字节)\r\n", (unsigned long)addr, (unsigned int)(addr - BLOCK_SIZE), (unsigned int)BLOCK_SIZE); continue; } // 不足1024字节: 若已处于等待END状态且末尾8字节为结束指令, 则结束升级 if (pend_state && frame_len >= 8 && is_end_cmd(frame + frame_len - 8)) { frame_len -= 8; // 去掉结束指令 if (frame_len > 0) { for (i = frame_len; i < BLOCK_SIZE; i++) frame[i] = 0xFF; STMFLASH_Write(addr, frame16, HALF_PER_BLOCK); addr += BLOCK_SIZE; } printf("升级完成! 共写入 %lu 字节, 即将跳转到APP。\r\n", (unsigned long)(addr - APP_ADDR)); delay_ms(200); jump_to_app(APP_ADDR); return; } } } //---------------------- 主函数 ---------------------- int main(void) { u8 upgrade = 0; u8 sec; // 系统时钟在启动文件SystemInit中已完成配置 delay_init(); uart1_init(9600); // PA9/PA10, 波特率9600, 已开启RX中断 Uart1_FlushRx(); // 清空接收环形缓冲 printf("IAP引导程序已启动, APP起始地址 = 0x%08X\r\n", APP_ADDR); // 10秒倒计时, 每秒上报剩余时间, 期间检测启动升级指令 for (sec = 5; sec > 0; sec--) { printf("倒计时: %d 秒 (发送指令进入升级模式)\r\n", sec); { u32 t; u8 b; for (t = 0; t < 1000; t++) // 约等待1秒, 期间持续检测启动指令 { while (Uart1_GetByte(&b)) // 取出中断缓冲中的所有字节逐个匹配 { if (start_match_feed(b)) { upgrade = 1; break; } } if (upgrade) break; delay_ms(1); } } if (upgrade) break; } if (upgrade) { upgrade_mode(); } else { printf("倒计时结束, 未收到升级指令, 直接跳转到APP。\r\n"); delay_ms(200); jump_to_app(APP_ADDR); } while (1); }

升级流程操作

1.首先单片机重新上电,这时会通过串口调试助手输出倒计时信息

2.在5秒倒计时内发送进入升级指令,使其系统升级模式,系统回复如下指令。

3.选择APP的bin文件进行传输,在传输之前需要按照如下进行设置

4.等待文件传输完成,最后在发送结束升级指令就完成操作了