1. 什么是IAP和OTA?为什么需要它们?
大家好,我是老李,在嵌入式行业摸爬滚打十多年了,今天想和大家聊聊STM32F103的IAP远程升级实战。如果你曾经为设备固件更新需要拆机、接下载线而头疼,那么IAP技术绝对是你的救星。
IAP(In Application Programming)简单来说就是"在应用中编程",允许MCU在运行用户程序的同时对自身Flash进行编程。而OTA(Over The-Air)则是IAP的一种实现方式,通过无线通信进行远程升级。在实际项目中,我经常遇到设备安装在难以触及的位置,比如高空、密闭空间,这时候OTA就显得尤为重要。
STM32F103作为经典的Cortex-M3内核MCU,内置Flash支持自编程,这为IAP提供了硬件基础。通过串口结合YModem协议,我们可以实现稳定可靠的固件传输。我记得第一次实现这个功能时,那种"终于不用跑现场升级"的喜悦至今难忘。
2. 硬件准备与开发环境搭建
2.1 硬件选型与连接
要实现IAP功能,首先需要准备硬件平台。我推荐使用STM32F103C8T6最小系统板,也就是我们常说的"蓝板",它有64KB Flash和20KB RAM,完全足够IAP应用。
硬件连接很简单:
- 串口1(PA9/PA10)用于程序调试和YModem通信
- 预留一个GPIO(如PB0)作为进入Bootloader的触发引脚
- 如果使用无线模块,可以连接串口2或SPI接口
我在实际项目中发现,最好预留一个LED指示灯(如PC13)和按键(如PA0),这样在调试Bootloader时会方便很多。当设备无法启动时,可以通过按键强制进入Bootloader模式。
2.2 开发环境配置
开发工具我习惯用STM32CubeMX + Keil MDK的组合,这也是大多数STM32开发者常用的环境。首先用STM32CubeMX生成Bootloader和App两个工程的基础代码,这样能确保底层配置的一致性。
关键配置步骤:
- 在CubeMX中使能USART1,配置为115200波特率
- 开启Flash读写功能(默认已开启)
- 为Bootloader工程设置正确的Flash起始地址(0x08000000)
- 为App工程设置偏移地址(如0x08004000)
// Bootloader的链接脚本配置
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
// App工程的链接脚本配置
FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 48K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
3. Bootloader设计与实现详解
3.1 Bootloader的工作流程
Bootloader是IAP的核心,它需要完成以下几个关键任务:
- 上电后检查升级触发条件
- 与上位机建立通信,接收新固件
- 校验固件完整性
- 跳转到App程序执行
我设计的Bootloader流程是这样的:
void bootloader_main(void)
{
// 初始化外设
HAL_Init();
SystemClock_Config();
UART_Init();
LED_Init();
// 检查升级触发条件
if(check_update_flag() || GPIO_ReadPin(BOOT_KEY_PIN) == 0)
{
start_ymodem_receive();
}
else
{
jump_to_app();
}
}
在实际项目中,我建议为Bootloader预留16KB的Flash空间,这样既能满足功能需求,又不会占用太多应用空间。
3.2 YModem协议集成与优化
YModem协议是串口文件传输的经典协议,相比XModem,它支持批量传输和文件信息传递。我推荐使用开源ymodem.c/h,这个库稳定且易于集成。
YModem传输过程:
- 接收方发送'C'字符发起传输
- 发送方首先发送文件信息包(包含文件名和大小)
- 接收方确认后开始数据包传输
- 每个数据包1024字节,最后不足的用0x1A填充
- 传输完成后进行CRC校验
我在实现时增加了一些优化:
- 超时重传机制:防止传输卡死
- 断点续传:意外中断后可以从断点继续
- 进度显示:通过LED或串口输出传输进度
// YModem接收处理核心代码
int32_t ymodem_receive(uint8_t *buf)
{
uint8_t packet_data[PACKET_SIZE];
uint32_t file_size = 0;
uint32_t received_size = 0;
// 发送'C'开始传输
uart_send_byte('C');
while(1)
{
// 接收数据包
if(receive_packet(packet_data) == SUCCESS)
{
// 处理文件信息包
if(is_file_info_packet(packet_data))
{
file_size = extract_file_size(packet_data);
uart_send_byte(ACK);
continue;
}
// 处理数据包
memcpy(buf + received_size, packet_data, PACKET_1K_SIZE);
received_size += PACKET_1K_SIZE;
uart_send_byte(ACK);
// 更新进度显示
update_progress(received_size, file_size);
}
}
}
4. 应用程序设计与中断重映射
4.1 应用程序的地址配置
App程序需要做一些特殊配置才能与Bootloader协同工作。最重要的就是修改中断向量表偏移,否则中断无法正确响应。
在App工程的system_stm32f1xx.c中修改:
#define VECT_TAB_OFFSET 0x4000U // 16KB Bootloader偏移
// 在SystemInit函数中设置向量表偏移
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
在Keil中还需要修改链接配置:
- 打开Options for Target -> Linker
- 取消勾选"Use Memory Layout from Target Dialog"
- 编辑sct文件,修改Flash起始地址为0x08004000
4.2 中断处理注意事项
在IAP方案中,中断处理需要特别注意。Bootloader和App有各自的中断向量表,跳转时需要重新设置VTOR寄存器。
我遇到过一个坑:App中使能了某些外设中断,但在Bootloader中没有处理,导致从Bootloader跳转到App后中断无法正确响应。解决方案是在Bootloader中禁用所有外设中断:
void deinit_all_peripherals(void)
{
__HAL_RCC_GPIOA_CLK_DISABLE();
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
// 禁用其他外设时钟...
NVIC_DisableIRQ(USART1_IRQn);
NVIC_DisableIRQ(USART2_IRQn);
// 禁用其他中断...
}
5. 固件升级全流程实战
5.1 生成可升级的Bin文件
Keil默认生成的是hex文件,我们需要将其转换为bin文件才能用于OTA升级。在Keil中有两种方法:
方法一:使用fromelf工具
- 打开Options for Target -> User
- 在After Build/Rebuild中勾选Run #1
- 输入命令:
fromelf --bin --output=@L.bin !L
方法二:使用批处理文件 创建generate_bin.bat文件:
fromelf --bin --output=.\Output\app.bin .\Output\app.axf
我推荐第二种方法,因为更灵活,可以添加版本号等额外信息。
5.2 使用Xshell进行YModem升级
Xshell是常用的终端软件,内置YModem支持。升级步骤:
- 连接设备串口,波特率115200
- 设备启动时按下升级按键进入Bootloader模式
- 在Xshell中输入
ymodem命令 - 选择要传输的bin文件
- 等待传输完成,设备自动重启
如果传输中断,可以检查以下几点:
- 串口线是否接触良好
- 波特率是否匹配
- 是否有电磁干扰(建议使用屏蔽线)
5.3 升级验证与回退机制
升级完成后,Bootloader需要验证固件完整性才能跳转执行。我通常使用CRC32校验:
bool verify_firmware(uint32_t addr, uint32_t size)
{
uint32_t calculated_crc = calculate_crc(addr, size - 4);
uint32_t stored_crc = *(uint32_t*)(addr + size - 4);
return (calculated_crc == stored_crc);
}
为了安全起见,我还实现了回退机制:
- 保存两个版本的固件(当前和上一个)
- 如果新固件验证失败,自动回退到旧版本
- 记录升级日志,便于故障排查
6. 常见问题与调试技巧
6.1 内存分配问题
在IAP项目中,内存分配需要特别注意。Bootloader和App的RAM区域不能重叠,否则会导致数据损坏。
我建议的做法:
- Bootloader使用RAM前半部分(如0x20000000-0x20000FFF)
- App使用RAM后半部分(如0x20001000-0x20004FFF)
- 在链接脚本中明确指定RAM区域
6.2 中断向量表跳转问题
跳转到App前,需要确保中断处理正确:
void jump_to_app(void)
{
typedef void (*pFunction)(void);
pFunction jump_to_application;
uint32_t jump_address;
// 检查栈顶地址是否合法
jump_address = *(uint32_t*)(APP_ADDRESS + 4);
jump_to_application = (pFunction)jump_address;
// 禁用所有中断
__disable_irq();
// 设置主堆栈指针
__set_MSP(*(uint32_t*)APP_ADDRESS);
// 跳转到应用程序
jump_to_application();
}
6.3 电源稳定性问题
在Flash编程过程中,电源稳定性至关重要。我遇到过因为电源波动导致Flash写入失败的情况。建议:
- 增加电源滤波电容
- Flash写入前检查电压是否正常
- 写入失败时自动重试
7. 进阶优化与实战建议
7.1 增加安全加密功能
在生产环境中,固件安全非常重要。我建议增加以下安全措施:
- 固件加密:使用AES等算法加密固件
- 数字签名:验证固件来源合法性
- 防回滚:防止降级到有安全漏洞的版本
bool verify_signature(uint8_t *firmware, uint32_t size)
{
// 从固件末尾提取签名
uint8_t *signature = firmware + size - SIGNATURE_SIZE;
// 使用公钥验证签名
return crypto_verify(firmware, size - SIGNATURE_SIZE, signature);
}
7.2 无线OTA实现
虽然本文重点介绍串口OTA,但同样的原理也适用于无线OTA。只需要将串口传输替换为无线传输即可:
- 使用Wi-Fi模块:通过ESP8266/ESP32实现HTTP固件下载
- 使用4G模块:通过MQTT协议接收固件更新
- 使用蓝牙:适合短距离无线更新
我在实际项目中常用ESP8266+HTTP的方式,成本低且稳定性好。关键是设计好断点续传机制,避免无线传输中断导致升级失败。
7.3 量产测试建议
在大规模生产中,IAP功能需要充分测试:
- 模拟各种异常情况:断电、信号中断、数据错误等
- 测试边界情况:最大固件大小、最小固件大小等
- 长期稳定性测试:连续升级100次以上验证可靠性
我建议编写自动化测试脚本,模拟各种升级场景,确保每个出厂设备都经过充分测试。
记得第一次部署到现场时,最好预留本地升级接口,万一无线升级出现问题还可以通过串口补救。实际项目中我还会在设备信息中记录固件版本和升级历史,方便远程排查问题。

6952

被折叠的 条评论
为什么被折叠?



