STM32F1 HAL库IAP实战:向量表重定向与Flash分区精解
发布时间:2026/9/17 4:29:50 作者:尧图编辑部 阅读量:1,286

简介本资源是一套基于HAL库开发的STM32F1系列微控制器IAP在应用编程升级解决方案面向嵌入式开发工程师及物联网固件升级实践者解决远程OTA升级中Bootloader与Application协同设计、内存分区管理、固件安全校验与回退机制等核心难题。压缩包含1843个文件主体为1116个C源码与484个头文件实现HAL驱动、IAP协议栈及应用逻辑辅以102个汇编启动文件、56个IAR链接脚本.icf及2个Keil工程.uvprojx、2个STM32CubeMX配置.ioc完整覆盖从BootloaderSTM32F1_Boot到应用程序STM32F1_App的双区部署架构包体大小37.47MB。已有344人学习下载。读者可直接复用经过验证的IAP通信协议框架、BIN固件生成流程、跳转执行机制及配套MCU手册与原理图参考显著降低远程升级功能开发门槛与调试风险。1. STM32F1 HAL库IAP不是“换固件”那么简单而是内存布局、向量表偏移、中断重定向三重校准的现场手术你手头有一块STM32F103C8T6板子串口接上电脑想用IAP升级App——结果烧完Bootloader后新App一运行就HardFault或者跳转过去但LED不闪、串口没响应。这不是代码写错了而是你没动过SCB-VTOR、没清过__HAL_FLASH_INSTRUCTION_CACHE_DISABLE()、也没把App的中断向量表从0x08000000挪到0x08004000。IAP在HAL库环境下不是“复制粘贴bin文件”就能跑通的流程它是一次对Flash分区、向量表映射、中断上下文切换的精准外科操作。本资源包含STM32F1_Boot.uvguix、STM32F1_App.uvguix、IAP_App_V2.bin等已按STM32F1系列典型48KB Flash容量如F103C8完成地址空间硬编码Bootloader固定占用前16KB0x08000000–0x08003FFFApp区起始地址设为0x08004000且所有HAL初始化函数均通过HAL_RCC_DeInit()HAL_Init()做二次复位清理。适合已掌握CubeMX基础配置、但尚未实操过双Bank跳转的嵌入式工程师尤其适用于工业传感器节点、远程PLC模块等无法开盖刷机的部署场景。2. Bootloader与App的内存分区与链接脚本强制对齐为什么.bin文件不能直接memcpy到0x080040002.1 Flash地址空间划分必须由ld脚本与KEIL/IAR工程设置双重锁定IAP成败的第一道门槛是Flash物理地址的不可协商性。STM32F1系列无内置EEPROM所有固件更新必须写入主Flash而Bootloader和App必须严格隔离——否则新App覆盖Bootloader会导致设备永久变砖。本资源采用经典双区布局区域起始地址大小用途关键约束Bootloader0x0800000016KB (0x4000)永驻程序负责接收、校验、写入、跳转必须禁用该区域写保护且FLASH_BASE宏需指向此地址App0x08004000剩余Flash如F103C8为32KB用户业务逻辑App的VECT_TAB_OFFSET必须设为0x4000否则中断向量读取错位提示KEIL中需在Options for Target → Linker → Use Memory Layout from Target Dialog勾选并手动编辑STM32F103C8Tx_FLASH.ldIAR对应lnkstm32f103xb.xcl确保MEMORY段定义与实际硬件一致/* STM32F103C8Tx_FLASH.ld */ MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K /* 全Flash空间 */ } SECTIONS { .bootloader : { *(.bootloader) } FLASH AT FLASH .text_app : { *(.text) *(.rodata) } FLASH AT FLASH .data : { *(.data) } RAM AT FLASH }若使用CubeMX生成工程必须在Project Manager → Code Generator → Advanced Settings中将Bootloader工程的Startup file设为startup_stm32f103xb.s并取消勾选Generate peripheral initialization code——Bootloader无需初始化全部外设仅需启用SYSCFG、FLASH、GPIOA用于串口TX/RX。2.2 App的向量表偏移不是改一个宏就够SCB-VTOR必须在跳转前重置HAL库默认将向量表加载到0x08000000但App位于0x08004000若不重定向任何中断如SysTick、USART都会跳转到Bootloader区执行非法指令。本资源在Bootloader跳转函数中强制重置VTOR// bootloader_main.c void Jump_To_Application(uint32_t ApplicationAddress) { typedef void (*pFunction)(void); pFunction Jump_To_Application; // 1. 禁用所有中断防止跳转过程中被抢占 __disable_irq(); // 2. 清空指令/数据缓存HAL库关键 __HAL_FLASH_INSTRUCTION_CACHE_DISABLE(); __HAL_FLASH_DATA_CACHE_DISABLE(); __HAL_FLASH_INSTRUCTION_CACHE_RESET(); __HAL_FLASH_DATA_CACHE_RESET(); __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); __HAL_FLASH_DATA_CACHE_ENABLE(); // 3. 重设向量表基址核心 SCB-VTOR ApplicationAddress; // 传入0x08004000 // 4. 设置MSP为主堆栈指针App的初始栈顶 __set_MSP(*(uint32_t*)ApplicationAddress); // 5. 获取App复位向量并跳转 Jump_To_Application (pFunction)(*(uint32_t*)(ApplicationAddress 4)); Jump_To_Application(); }参数说明ApplicationAddress 4是ARM Cortex-M3的复位向量地址向量表第1项为初始MSP第2项为复位入口。__set_MSP()必须在跳转前调用否则App启动时堆栈溢出。若此处遗漏现象为跳转后立即HardFault且SCB-CFSR显示INVPC无效PC。2.3 Bin文件写入Flash前必须按页擦除且校验逻辑需覆盖整个App区STM32F1 Flash擦除最小单位为页1KB写入前必须擦除目标页。本资源Bootloader中FLASH_Program函数封装了页擦除-写入-校验闭环// flash_if.c HAL_StatusTypeDef FLASH_Write_Bin(uint32_t WriteAddr, uint8_t *pBuffer, uint32_t Size) { uint32_t i 0; uint32_t PageError 0; FLASH_EraseInitTypeDef EraseInitStruct; __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR); // 1. 计算需擦除的页范围STM32F1每页1KB EraseInitStruct.TypeErase TYPEERASE_PAGES; EraseInitStruct.PageAddress WriteAddr; EraseInitStruct.NbPages (Size % 1024 0) ? (Size / 1024) : (Size / 1024 1); if (HAL_FLASHEx_Erase(EraseInitStruct, PageError) ! HAL_OK) { return HAL_ERROR; // 擦除失败如写保护未解除 } // 2. 逐字写入HAL要求32位对齐 for(i 0; i Size; i 4) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, WriteAddr i, *(uint32_t*)(pBuffer i)) ! HAL_OK) { return HAL_ERROR; } } // 3. 校验读回比对关键防错步骤 uint8_t read_buf[1024]; for(i 0; i Size; i 1024) { uint32_t len (Size - i 1024) ? 1024 : (Size - i); memcpy(read_buf, (uint8_t*)(WriteAddr i), len); if (memcmp(pBuffer i, read_buf, len) ! 0) { return HAL_ERROR; // 写入数据错误 } } return HAL_OK; }注意HAL_FLASH_Program()要求地址和数据均为32位对齐pBuffer若为uint8_t*需强制转换为uint32_t*。若App二进制长度非4字节整除末尾需补0填充否则HAL_FLASH_Program会触发PGSERR编程序列错误。3. 串口IAP协议设计与HAL驱动层适配如何让Bootloader可靠接收128KB固件而不丢包3.1 自定义轻量级IAP协议帧结构解决HAL_UART_Receive_IT丢包的根本原因HAL库默认HAL_UART_Receive_IT()在接收缓冲区满或超时才触发回调但IAP固件传输长达数秒期间若发生中断嵌套或高优先级任务抢占极易丢失字节。本资源采用带校验和的定长帧协议彻底规避中断接收缺陷字段长度说明SOF1字节固定值0xAACMD1字节0x01请求固件大小0x02发送固件块0x03校验确认LEN2字节数据长度小端序DATALEN字节有效载荷固件块或命令参数CRC81字节整帧CRC8校验多项式0x07Bootloader主循环中不依赖IT接收而是用HAL_UART_Receive()阻塞式读取完整帧// iap_protocol.c typedef struct { uint8_t sof; uint8_t cmd; uint16_t len; uint8_t data[256]; // 最大帧256字节 uint8_t crc; } IAP_FrameTypeDef; HAL_StatusTypeDef IAP_Receive_Frame(UART_HandleTypeDef *huart, IAP_FrameTypeDef *frame) { uint8_t buf[8]; // SOFCMDLEN(2)CRC 5字节头部 uint32_t timeout HAL_GetTick() 1000; // 1秒超时 // 1. 同步SOF while(HAL_UART_Receive(huart, buf[0], 1, 10) ! HAL_OK) { if(HAL_GetTick() timeout) return HAL_TIMEOUT; } if(buf[0] ! 0xAA) return HAL_ERROR; // 2. 读取剩余头部 if(HAL_UART_Receive(huart, buf[1], 4, 100) ! HAL_OK) return HAL_ERROR; frame-sof buf[0]; frame-cmd buf[1]; frame-len (buf[3] 8) | buf[2]; // 小端转主机序 frame-crc buf[4]; // 3. 读取DATA if(frame-len 0) { if(HAL_UART_Receive(huart, frame-data, frame-len, 500) ! HAL_OK) return HAL_ERROR; } // 4. CRC校验 if(IAP_CRC8(buf, 5 frame-len) ! frame-crc) return HAL_ERROR; return HAL_OK; }逻辑说明HAL_UART_Receive()在huart-gState HAL_UART_STATE_READY时才执行避免了IT模式下huart-RxXferCount被意外修改的风险。每帧最大256字节兼顾传输效率与RAM占用F103C8仅有20KB RAM。3.2 App固件完整性校验不只是MD5而是结合CRC32与签名验证的双保险OTA升级最怕固件被篡改。本资源在Bootloader中集成CRC32校验ISO 3309标准与RSA签名验证可选// firmware_verify.c uint32_t CRC32_Calculate(uint8_t *data, uint32_t size) { const uint32_t CRC32_POLY 0xEDB88320; uint32_t crc 0xFFFFFFFF; for(uint32_t i 0; i size; i) { crc ^ data[i]; for(uint8_t j 0; j 8; j) { if(crc 1) crc (crc 1) ^ CRC32_POLY; else crc 1; } } return crc ^ 0xFFFFFFFF; } // 校验流程 HAL_StatusTypeDef IAP_Verify_Firmware(uint32_t app_addr, uint32_t app_size) { uint8_t *app_ptr (uint8_t*)app_addr; uint32_t calc_crc CRC32_Calculate(app_ptr, app_size); uint32_t stored_crc *(uint32_t*)(app_ptr app_size - 4); // CRC存于固件末尾4字节 if(calc_crc ! stored_crc) { return HAL_ERROR; // CRC不匹配 } // 可选RSA公钥验证需移植mbed TLS // if(RSA_Verify(app_ptr, app_size - 256, app_ptr app_size - 256) ! 0) { // return HAL_ERROR; // } return HAL_OK; }参数说明app_size必须包含CRC字段本身即固件总长业务代码4字节CRC。生成IAP_App_V2.bin时构建脚本需在末尾追加CRC32值例如Linux下# 生成带CRC的固件 arm-none-eabi-objcopy -O binary STM32F1_App.axf app_temp.bin ./crc32_append app_temp.bin # 此脚本计算CRC并追加4字节 mv app_temp.bin_with_crc IAP_App_V2.bin4. HAL库IAP实战从CubeMX配置到Keil下载的7步闭环操作4.1 CubeMX工程配置清单Bootloader与App必须差异化设置配置项Bootloader工程App工程差异原因SYS → SysTick不使能使能App需HAL_Delay()Bootloader仅用HAL_GetTick()RCC → HSE使能8MHz晶振使能同Bootloader保证时钟一致性避免跳转后SysTick异常USART1 → ModeAsynchronousAsynchronous通信接口统一USART1 → NVIC仅使能Global Interrupt使能Global Interrupt RXNEIEBootloader用轮询App可用中断Project Manager → ToolchainARM GCC / KEILARM GCC / KEIL编译器需一致Code Generator → Generate peripheral initialization asSeparate filesSeparate files避免HAL_MspInit()冲突Advanced Settings → HAL driver仅勾选HAL_FLASH_MODULE_ENABLEDHAL_GPIO_MODULE_ENABLEDHAL_RCC_MODULE_ENABLED勾选全部所需外设Bootloader精简App全功能注意App工程的main.c中HAL_Init()后必须插入__HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH();——这是关键否则App启动时仍从0x08000000读取向量表。该函数将系统存储器重映射到0x00000000使CPU从App区取指令。4.2 Keil MDK编译与Bin生成全流程命令行本资源提供自动化脚本build_iap.bat确保Bootloader与App的地址空间绝对隔离:: build_iap.bat echo off :: 编译Bootloader C:\Keil_v5\UV4\UV4.exe -b STM32F1_Boot.uvprojx -t Target 1 -o boot_log.txt if errorlevel 1 goto :error :: 编译App强制指定分散加载 C:\Keil_v5\UV4\UV4.exe -b STM32F1_App.uvprojx -t Target 1 -o app_log.txt if errorlevel 1 goto :error :: 生成App bin剔除Bootloader占用区 C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output IAP_App_V2.bin STM32F1_App.axf :: 追加CRC32调用Python脚本 python crc32_append.py IAP_App_V2.bin echo IAP固件生成成功 exit /b 0 :error echo 编译失败请检查log.txt pause其中crc32_append.py内容为import sys import zlib def append_crc32(filename): with open(filename, rb) as f: data f.read() crc zlib.crc32(data) 0xFFFFFFFF with open(filename, ab) as f: f.write(crc.to_bytes(4, little)) if __name__ __main__: append_crc32(sys.argv[1])4.3 实机验证四步法用串口助手完成端到端测试烧录Bootloader用ST-Link Utility将STM32F1_Boot.hex烧录至0x08000000验证串口输出IAP Bootloader Ready发送固件请求用XCOM发送AA 01 00 00 00请求固件大小Bootloader返回AA 01 00 00 [SIZE_L][SIZE_H] [CRC]分块上传固件将IAP_App_V2.bin按256字节分块每块构造帧AA 02 [LEN_L][LEN_H] [DATA...] [CRC]发送触发跳转发送AA 03 00 00 00Bootloader校验后跳转观察App的LED闪烁或串口打印App Running v2.0。排错关键点若第3步卡住用逻辑分析仪抓取USART波形确认是否因波特率误差导致帧同步失败F103C8在72MHz下115200bps误差1%但若用内部RC时钟则需校准。5. IAP回滚与安全加固当新固件启动失败时如何让设备自动恢复出厂版本5.1 双App备份区设计用最后2KB Flash实现原子化切换为防升级失败导致设备宕机本资源预留最后2KB0x0800F000–0x0800FFFF作为备份区。Bootloader在写入新App前先将当前App完整备份至此// backup_manager.c #define BACKUP_AREA_START 0x0800F000 #define APP_SIZE_MAX 0x0000C000 // 48KB HAL_StatusTypeDef IAP_Backup_Current_App(void) { uint32_t src 0x08004000; uint32_t dst BACKUP_AREA_START; uint32_t size APP_SIZE_MAX; // 1. 擦除备份区 FLASH_EraseInitTypeDef erase; erase.TypeErase TYPEERASE_PAGES; erase.PageAddress dst; erase.NbPages (size % 1024 0) ? size / 1024 : size / 1024 1; if(HAL_FLASHEx_Erase(erase, PageError) ! HAL_OK) return HAL_ERROR; // 2. 逐页复制避免RAM不足 for(uint32_t i 0; i size; i 1024) { uint8_t page_buf[1024]; memcpy(page_buf, (uint8_t*)(src i), 1024); if(FLASH_Write_Bin(dst i, page_buf, 1024) ! HAL_OK) return HAL_ERROR; } return HAL_OK; }5.2 启动时自检机制通过标志位CRC双重判定是否回滚App启动时在RAM中写入0xDEADBEEF标志Bootloader上电后检查// bootloader_start.c #define BOOT_FLAG_ADDR 0x20004FFC // SRAM末尾4字节 #define BOOT_FLAG_VAL 0xDEADBEEF void System_Start_Check(void) { uint32_t *flag_ptr (uint32_t*)BOOT_FLAG_ADDR; uint32_t flag *flag_ptr; if(flag ! BOOT_FLAG_VAL) { // 标志不存在可能是首次启动或App崩溃 if(IAP_Verify_Firmware(BACKUP_AREA_START, APP_SIZE_MAX) HAL_OK) { // 备份区有效回滚 FLASH_EraseInitTypeDef erase; erase.TypeErase TYPEERASE_PAGES; erase.PageAddress 0x08004000; erase.NbPages APP_SIZE_MAX / 1024; HAL_FLASHEx_Erase(erase, PageError); // 从备份区恢复 uint8_t buf[1024]; for(uint32_t i 0; i APP_SIZE_MAX; i 1024) { memcpy(buf, (uint8_t*)(BACKUP_AREA_START i), 1024); FLASH_Write_Bin(0x08004000 i, buf, 1024); } *flag_ptr BOOT_FLAG_VAL; // 设置标志 HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, BOOT_FLAG_ADDR, BOOT_FLAG_VAL); } } }技巧BOOT_FLAG_ADDR选在SRAM末尾而非Flash避免频繁擦写Flash降低寿命。每次App正常退出前执行*flag_ptr 0清除标志确保下次启动重新校验。5.3 安全启动增强基于公钥的固件签名验证mbed TLS最小化移植若需更高安全等级可启用RSA-2048签名。本资源提供mbedtls_config.h裁剪版仅启用必要模块// mbedtls_config.h精简版 #define MBEDTLS_AES_C #define MBEDTLS_RSA_C #define MBEDTLS_PK_C #define MBEDTLS_SHA256_C #define MBEDTLS_BIGNUM_C #define MBEDTLS_ENTROPY_C #define MBEDTLS_CTR_DRBG_C /* 禁用SSL/TLS、X509等无关模块 */ #undef MBEDTLS_SSL_TLS_C #undef MBEDTLS_X509_CRT_PARSE_C签名验证核心代码#include mbedtls/rsa.h #include mbedtls/sha256.h int verify_signature(uint8_t *firmware, size_t len, uint8_t *signature) { mbedtls_rsa_context rsa; mbedtls_rsa_init(rsa, MBEDTLS_RSA_PKCS_V15, 0); // 加载公钥硬编码在代码中或从OTP读取 const unsigned char pubkey[] { /* 2048-bit DER格式公钥 */ }; mbedtls_mpi_read_binary(rsa.N, pubkey10, 256); // 示例偏移 mbedtls_mpi_read_binary(rsa.E, pubkey266, 3); // e65537 uint8_t hash[32]; mbedtls_sha256(firmware, len, hash, 0); int ret mbedtls_rsa_pkcs1_verify(rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, 0, hash, signature); mbedtls_rsa_free(rsa); return ret; }部署要点公钥必须固化在Bootloader Flash中不可被App修改私钥由产线服务器保管。每次固件发布前用OpenSSL生成签名openssl dgst -sha256 -sign private_key.pem -out app.sig app.bin。本文还有配套的精品资源点击获取