1. 项目概述:STM32 IAP手写实现的核心价值
在嵌入式开发领域,IAP(In-Application Programming)技术一直是产品后期维护和功能升级的关键手段。不同于传统的JTAG/SWD烧录方式,基于串口的IAP方案允许设备在部署后通过通信接口实现固件更新,这对工业现场设备、物联网终端等应用场景具有重大意义。
我最近完成了一个完全手写的STM32 IAP实现方案,经过严格测试验证了其可靠性。这个方案的核心创新点在于:
- 采用DMA实现串口数据到内存的高效搬运
- 通过中断机制协调Flash写入操作
- 优化了传统方案中CPU资源占用过高的问题
- 实现了稳定的9600bps波特率通信
2. 硬件架构设计要点
2.1 存储空间规划
典型的STM32F103系列芯片存储结构如下:
| 存储区域 | 起始地址 | 典型大小 | 用途 |
|---|---|---|---|
| Flash | 0x08000000 | 64-512KB | 存放Bootloader和APP |
| SRAM | 0x20000000 | 20-64KB | 运行时内存 |
| 系统存储器 | 0x1FFFF000 | 2KB | 出厂Bootloader |
关键配置要点:
- Bootloader大小控制在8-16KB范围内
- APP起始地址需要按页对齐(STM32F103页大小为1KB)
- 中断向量表重映射必须正确处理
2.2 通信接口选择
本方案选用USART1作为通信接口,主要考虑:
- 稳定性优于USB方案
- 电路设计简单
- 兼容各种转换芯片(CH340/CP2102等)
配置参数:
- 波特率:9600bps(经测试最稳定)
- 数据位:8位
- 停止位:1位
- 无校验位
3. Bootloader实现细节
3.1 启动流程设计
void Bootloader_Init(void) { // 1. 初始化时钟系统 RCC_Configuration(); // 2. 初始化GPIO和USART GPIO_Configuration(); USART_Configuration(); // 3. 初始化DMA控制器 DMA_Configuration(); // 4. 检查升级标志 if(Check_Update_Flag()) { Start_Update_Process(); } else { Jump_To_App(); } }3.2 DMA数据搬运实现
关键配置结构体:
DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RxBuffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStructure);4. Flash编程关键操作
4.1 解锁Flash
void FLASH_Unlock(void) { FLASH->KEYR = FLASH_KEY1; FLASH->KEYR = FLASH_KEY2; }4.2 页擦除流程
FLASH_Status FLASH_ErasePage(uint32_t Page_Address) { FLASH_Status status = FLASH_COMPLETE; status = FLASH_WaitForLastOperation(EraseTimeout); if(status == FLASH_COMPLETE) { FLASH->CR |= CR_PER_Set; FLASH->AR = Page_Address; FLASH->CR |= CR_STRT_Set; status = FLASH_WaitForLastOperation(EraseTimeout); FLASH->CR &= CR_PER_Reset; } return status; }5. 应用跳转机制
5.1 中断向量表重映射
void NVIC_Configuration(void) { NVIC_SetVectorTable(NVIC_VectTab_FLASH, APP_START_ADDRESS); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); }5.2 跳转到APP的代码
void Jump_To_App(void) { typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t JumpAddress; JumpAddress = *(__IO uint32_t*)(APP_START_ADDRESS + 4); Jump_To_Application = (pFunction)JumpAddress; __set_MSP(*(__IO uint32_t*)APP_START_ADDRESS); Jump_To_Application(); }6. 通信协议设计
6.1 数据帧格式
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头 | 2 | 0xAA55 |
| 命令字 | 1 | 0x01:擦除 0x02:写入 |
| 地址 | 4 | 目标地址 |
| 长度 | 2 | 数据长度 |
| 数据 | N | 有效载荷 |
| 校验 | 2 | CRC16校验 |
6.2 状态机实现
typedef enum { STATE_IDLE, STATE_HEADER, STATE_CMD, STATE_ADDR, STATE_LEN, STATE_DATA, STATE_CRC } ProtocolState; void Process_Byte(uint8_t data) { static ProtocolState state = STATE_IDLE; static uint16_t data_index = 0; static uint16_t expected_len = 0; switch(state) { case STATE_IDLE: if(data == 0xAA) state = STATE_HEADER; break; // 其他状态处理... } }7. 实际测试中的经验总结
7.1 波特率选择考量
经过实测,不同波特率的稳定性表现:
| 波特率 | 传输速度 | 稳定性 | CPU负载 |
|---|---|---|---|
| 115200 | 快 | 差 | 低 |
| 57600 | 中 | 一般 | 中 |
| 9600 | 慢 | 优 | 高 |
最终选择9600bps的原因:
- 在Flash写入期间不会丢失数据
- 对时钟精度要求较低
- 适合长距离传输
7.2 Flash写入优化技巧
- 批量写入:每次写入尽可能多的数据(STM32F103建议每次写256字节)
- 交叉操作:当DMA在搬运下一批数据时,CPU处理当前数据的Flash写入
- 缓冲设计:采用双缓冲机制避免数据覆盖
#define BUF_SIZE 1024 uint8_t BufferA[BUF_SIZE]; uint8_t BufferB[BUF_SIZE]; uint8_t *ActiveBuffer = BufferA; uint8_t *ProcessBuffer = BufferB; void DMA1_Channel5_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC5)) { // 切换缓冲区 uint8_t *temp = ActiveBuffer; ActiveBuffer = ProcessBuffer; ProcessBuffer = temp; DMA_ClearITPendingBit(DMA1_IT_TC5); } }8. 常见问题解决方案
8.1 启动失败问题排查
检查向量表偏移:
readelf -S application.elf | grep Vector验证栈指针初始化:
uint32_t sp = *(uint32_t*)APP_START_ADDRESS; if((sp & 0x2FFE0000) != 0x20000000) { // 栈指针异常 }
8.2 通信异常处理
典型错误现象及解决方法:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据丢失 | DMA配置错误 | 检查DMA缓冲区和长度设置 |
| 校验失败 | 时钟不同步 | 调整波特率容差 |
| 无响应 | 波特率不匹配 | 确认双方波特率一致 |
9. 性能优化建议
内存使用优化:
- 将临时变量定义在SRAM的快速访问区域
- 对频繁访问的数据使用
__attribute__((section(".ramfunc")))
中断优先级配置:
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);
10. 完整实现流程示例
- 设备上电启动Bootloader
- 等待2秒接收升级指令(超时则跳转APP)
- 收到升级指令后进入升级模式
- 接收数据帧并校验
- 擦除目标Flash扇区
- 写入接收到的数据
- 完成所有数据写入后校验完整性
- 跳转到新的APP执行
void Start_Update_Process(void) { FLASH_Unlock(); while(1) { Packet packet = Receive_Packet(); if(packet.cmd == CMD_ERASE) { FLASH_ErasePage(packet.address); } else if(packet.cmd == CMD_WRITE) { for(int i=0; i<packet.length; i+=4) { uint32_t data = *(uint32_t*)&packet.data[i]; FLASH_ProgramWord(packet.address + i, data); } } else if(packet.cmd == CMD_JUMP) { FLASH_Lock(); Jump_To_App(); } } }在实际项目中,这个方案已经成功应用在多个工业设备上,实现了可靠的远程固件更新功能。通过DMA和中断的合理配合,即使在资源有限的STM32F103C8T6(64KB Flash,20KB RAM)上也能够稳定运行。