1. STM32 IAP升级核心原理剖析
IAP(In-Application Programming)技术是嵌入式设备实现固件远程更新的关键技术。对于STM32C8T6这类资源受限的MCU,通过串口+YModem协议实现IAP是最经济实用的方案。其核心原理是将Flash存储器划分为Bootloader和APP两个独立区域,Bootloader负责接收新固件并写入APP区域,完成后再跳转到新程序执行。
关键点:Bootloader必须足够可靠,因为它承担着整个升级过程的管理工作。建议Bootloader代码尽可能精简,只保留最必要的功能。
1.1 存储器布局设计
STM32C8T6的Flash总容量为64KB,按照1KB页大小进行管理。典型的分配方案如下:
| 区域 | 起始地址 | 大小 | 用途说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 8KB | 存放引导程序 |
| APP | 0x08002000 | 56KB | 存放用户应用程序 |
| 参数区 | 0x0800E000 | 2KB | 可选,存放升级参数等 |
这种分配方式确保了:
- Bootloader有足够空间实现基本功能
- APP区域可以容纳中等复杂度的应用程序
- 保留了最后2KB作为参数存储区(可选)
1.2 中断向量表重映射
当程序从Bootloader跳转到APP时,必须正确处理中断向量表偏移。APP工程中需要做以下配置:
- 在system_stm32f10x.c中设置VECT_TAB_OFFSET:
c复制#define VECT_TAB_OFFSET 0x2000
- 在main函数初始化时调用:
c复制SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
- MDK用户需要在Options->Target中设置IROM1起始地址为0x08002000
2. Bootloader实现细节
2.1 跳转函数实现
跳转到APP的代码是Bootloader最核心的部分,必须确保安全可靠:
c复制void jump_to_app(uint32_t app_addr)
{
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
/* 检查栈顶地址是否合法(在RAM范围内) */
uint32_t stack_pointer = *(__IO uint32_t*)app_addr;
if((stack_pointer & 0x2FFE0000) != 0x20000000)
return;
/* 设置主堆栈指针 */
__set_MSP(stack_pointer);
/* 获取复位中断服务程序地址 */
uint32_t reset_handler = *(__IO uint32_t*)(app_addr + 4);
Jump_To_Application = (pFunction)reset_handler;
/* 关闭所有中断 */
__disable_irq();
/* 跳转到APP */
Jump_To_Application();
}
注意事项:跳转前务必关闭所有中断,否则可能导致程序跑飞。同时建议在跳转前执行一次完整的外设复位。
2.2 Flash操作关键点
Flash编程是IAP的核心操作,必须严格遵循时序要求:
c复制void flash_erase(uint32_t start_addr, uint32_t end_addr)
{
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
FLASH_Status status;
for(uint32_t addr = start_addr; addr < end_addr; addr += FLASH_PAGE_SIZE){
status = FLASH_ErasePage(addr);
if(status != FLASH_COMPLETE){
// 错误处理
break;
}
}
FLASH_Lock();
}
void flash_write(uint32_t addr, uint8_t *data, uint32_t len)
{
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
uint32_t *p_data = (uint32_t*)data;
for(uint32_t i = 0; i < len; i += 4){
if(FLASH_ProgramWord(addr + i, *p_data++) != FLASH_COMPLETE){
// 错误处理
break;
}
}
FLASH_Lock();
}
3. YModem协议实现
3.1 协议状态机设计
YModem协议采用128字节数据包传输,每个包包含以下字段:
- 起始字符(SOH/STX)
- 包序号(1字节)
- 包序号补码(1字节)
- 数据(128/1024字节)
- CRC16校验(2字节)
状态机实现框架:
c复制typedef enum {
YMODEM_IDLE,
YMODEM_START,
YMODEM_FILENAME,
YMODEM_DATA,
YMODEM_END
} YModemState;
typedef struct {
YModemState state;
uint8_t packetIndex;
uint32_t fileSize;
uint32_t bytesReceived;
uint8_t buffer[1024];
} YModemHandler;
void ymodem_process(uint8_t data)
{
static YModemHandler handler;
switch(handler.state){
case YMODEM_IDLE:
if(data == 'C'){ // CRC模式
handler.state = YMODEM_START;
send_ack();
}
break;
case YMODEM_FILENAME:
// 解析文件名和文件大小
if(parse_filename(data)){
handler.state = YMODEM_DATA;
send_ack();
}
break;
case YMODEM_DATA:
// 存储数据到缓冲区
handler.buffer[handler.bytesReceived++] = data;
if(handler.bytesReceived == 128){
// 写入Flash
flash_write(APP_ADDRESS + handler.fileSize,
handler.buffer, 128);
handler.fileSize += 128;
handler.bytesReceived = 0;
send_ack();
}
break;
case YMODEM_END:
// 处理结束包
jump_to_app(APP_ADDRESS);
break;
}
}
3.2 CRC校验实现
可靠的校验是保证固件完整性的关键:
c复制uint16_t crc16_update(uint16_t crc, uint8_t data)
{
crc ^= (uint16_t)data << 8;
for(uint8_t i = 0; i < 8; i++){
if(crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
return crc;
}
uint16_t crc16_calculate(uint8_t *data, uint32_t len)
{
uint16_t crc = 0;
while(len--)
crc = crc16_update(crc, *data++);
return crc;
}
4. 上位机配合与调试技巧
4.1 固件文件准备
使用JFlash工具将HEX转换为BIN文件时,需要注意:
- 确保输出BIN文件不包含非数据段
- 检查文件大小不超过APP区域容量
- 建议添加版本信息到文件头
Python转换脚本增强版:
python复制def hex2bin(hex_file, bin_file):
with open(hex_file, 'r') as f_hex:
with open(bin_file, 'wb') as f_bin:
ext_addr = 0
for line in f_hex:
if line[0] != ':':
continue
byte_count = int(line[1:3], 16)
addr = int(line[3:7], 16) + ext_addr
rec_type = int(line[7:9], 16)
if rec_type == 0x04: # 扩展地址记录
ext_addr = int(line[9:13], 16) << 16
elif rec_type == 0x00: # 数据记录
data = bytes.fromhex(line[9:9+byte_count*2])
f_bin.seek(addr)
f_bin.write(data)
4.2 常见问题排查
-
升级后程序不运行
- 检查APP工程的ROM起始地址设置
- 验证中断向量表重映射代码
- 确认跳转前关闭了所有中断
-
传输卡在某个进度
- 检查Flash写保护状态
- 验证CRC校验算法
- 调整串口波特率(建议不超过115200)
-
文件传输不完整
- 检查YModem协议实现是否正确
- 确保上位机使用正确的YModem模式
- 增加超时重传机制
5. 进阶优化建议
5.1 双备份机制
为提高可靠性,可以实现双备份机制:
c复制#define APP1_ADDR 0x08002000
#define APP2_ADDR 0x08005000
void bootloader_main()
{
if(check_app(APP1_ADDR)){
jump_to_app(APP1_ADDR);
}else if(check_app(APP2_ADDR)){
jump_to_app(APP2_ADDR);
}else{
// 进入升级模式
start_ymodem();
}
}
5.2 差分升级
为减少传输数据量,可以实现差分升级:
- 上位机计算新旧固件差异
- 只传输差异部分
- Bootloader在设备端合并差异
5.3 安全加固
- 添加固件签名验证
- 实现加密传输
- 加入防回滚机制
在实际项目中,我通常会先在RAM中完全接收整个固件,验证通过后再写入Flash。这种方式虽然需要更多RAM,但大大提高了升级的可靠性。另外,建议在Bootloader中加入看门狗处理,防止升级过程中出现死锁。