给STM32F103瘦身:手把手教你打造一个12KB以内的轻量级BootLoader
在嵌入式开发中,资源受限的MCU(如STM32F103系列)常常面临内存紧张的挑战。当Flash空间仅有64KB或128KB时,BootLoader的体积优化就显得尤为重要。本文将深入探讨如何通过代码精简、分区策略优化等手段,将BootLoader压缩至12KB以内,同时确保其功能的完整性和可靠性。
1. BootLoader的核心功能与设计考量
BootLoader作为MCU启动的第一段代码,承担着硬件初始化、固件升级和应用程序跳转等关键任务。在资源受限的环境中,我们需要在功能完整性和代码体积之间找到平衡点。
核心功能模块:
- 硬件初始化(时钟、GPIO、通信接口等)
- 固件验证与升级逻辑
- 应用程序跳转机制
- 错误处理与恢复
在设计轻量级BootLoader时,需要特别注意以下几点:
- 最小化依赖:仅包含必要的驱动和库
- 优化算法:选择空间效率高的实现方式
- 精简错误处理:保留关键错误检测,简化非关键路径
- 避免浮点运算:使用定点数替代浮点运算
2. 内存分区策略优化
合理的Flash分区是BootLoader设计的关键。对于STM32F103这类资源受限的MCU,我们需要精心规划每个区域的大小和用途。
2.1 常见分区方案对比
| 分区方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Boot+App | 实现简单,App区空间大 | 升级失败风险高 | 对可靠性要求不高的场景 |
| Boot+App+Download | 升级安全,避免"变砖" | 需要额外存储空间 | 需要可靠升级的场景 |
| Boot+App1+App2 | 支持版本回退 | 需要双倍App空间 | 需要版本回退功能的场景 |
| Boot+Setting+App+Download | 支持配置存储 | 分区更复杂 | 需要保存配置参数的场景 |
2.2 推荐的分区方案
针对12KB BootLoader的限制,我们推荐以下分区方案:
#define FLASH_START_ADDR 0x08000000 #define BOOT_SIZE 0x3000 // 12KB #define SETTING_SIZE 0x1000 // 4KB #define APP_SIZE 0xE000 // 56KB #define DOWNLOAD_SIZE 0xE000 // 56KB const struct { uint32_t boot_start = FLASH_START_ADDR; uint32_t boot_end = FLASH_START_ADDR + BOOT_SIZE; uint32_t setting_start = FLASH_START_ADDR + BOOT_SIZE; uint32_t setting_end = FLASH_START_ADDR + BOOT_SIZE + SETTING_SIZE; uint32_t app_start = FLASH_START_ADDR + BOOT_SIZE + SETTING_SIZE; uint32_t app_end = FLASH_START_ADDR + BOOT_SIZE + SETTING_SIZE + APP_SIZE; uint32_t download_start= FLASH_START_ADDR + BOOT_SIZE + SETTING_SIZE + APP_SIZE; uint32_t download_end = FLASH_START_ADDR + BOOT_SIZE + SETTING_SIZE + APP_SIZE + DOWNLOAD_SIZE; } flash_layout;这种分区方案在128KB Flash的STM32F103上实现了:
- 12KB BootLoader空间
- 4KB配置存储区
- 56KB应用程序空间
- 56KB下载缓冲区
3. 代码优化技巧
3.1 链接脚本优化
通过精心设计的链接脚本,我们可以精确控制代码和数据的存放位置,避免空间浪费。
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 12K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .text : { *(.vectors) *(.text*) *(.rodata*) } > FLASH .data : { _sdata = .; *(.data*) _edata = .; } > RAM AT > FLASH .bss : { _sbss = .; *(.bss*) _ebss = .; } > RAM }3.2 关键代码优化
应用程序跳转函数优化:
__attribute__((naked)) void jump_to_app(uint32_t app_addr) { __asm volatile( "msr msp, r0\n" // 设置主堆栈指针 "ldr r0, [r0, #4]\n" // 加载复位地址 "bx r0" // 跳转到应用程序 ); } int validate_app(uint32_t app_addr) { // 检查栈指针是否在RAM范围内 if((*(volatile uint32_t*)app_addr & 0x2FFE0000) != 0x20000000) return 0; // 检查复位向量是否在Flash范围内 uint32_t reset_addr = *(volatile uint32_t*)(app_addr + 4); if(reset_addr < FLASH_START_ADDR || reset_addr >= (FLASH_START_ADDR + FLASH_SIZE)) return 0; return 1; }3.3 通信协议精简
对于轻量级BootLoader,建议使用简单的协议而非复杂的协议栈:
// 简化的XMODEM协议实现 #define SOH 0x01 #define EOT 0x04 #define ACK 0x06 #define NAK 0x15 void xmodem_receive(uint8_t *buffer) { uint8_t packet[132]; uint16_t packet_num = 1; while(1) { if(uart_receive_byte() == SOH) { uint8_t pkt_num = uart_receive_byte(); uint8_t pkt_num_c = uart_receive_byte(); if(pkt_num + pkt_num_c != 0xFF) { uart_send_byte(NAK); continue; } for(int i=0; i<128; i++) { packet[i] = uart_receive_byte(); } uint8_t checksum = uart_receive_byte(); // 简化的校验和验证 uint8_t calc_checksum = 0; for(int i=0; i<128; i++) { calc_checksum += packet[i]; } if(calc_checksum != checksum) { uart_send_byte(NAK); continue; } memcpy(buffer + (pkt_num-1)*128, packet, 128); uart_send_byte(ACK); packet_num++; } else if(uart_receive_byte() == EOT) { uart_send_byte(ACK); break; } } }4. 实战:构建12KB BootLoader
4.1 开发环境配置
使用Keil MDK进行开发时,需要进行以下优化配置:
编译器优化选项:
- 优化级别设置为-Os(优化大小)
- 启用链接时间优化(LTO)
- 禁用未使用的函数和数据消除
链接器配置:
- 设置ROM起始地址为0x08000000
- 限制代码大小为0x3000(12KB)
库配置:
- 使用MicroLIB减小标准库体积
- 避免使用printf等大型函数
4.2 关键组件实现
Flash操作函数:
void flash_erase_page(uint32_t page_address) { FLASH->CR |= FLASH_CR_PER; FLASH->AR = page_address; FLASH->CR |= FLASH_CR_STRT; while(FLASH->SR & FLASH_SR_BSY); FLASH->CR &= ~FLASH_CR_PER; } void flash_write(uint32_t address, uint32_t *data, uint32_t length) { FLASH->CR |= FLASH_CR_PG; for(uint32_t i=0; i<length; i+=4) { *(__IO uint32_t*)(address + i) = *data++; while(FLASH->SR & FLASH_SR_BSY); } FLASH->CR &= ~FLASH_CR_PG; }固件验证函数:
uint8_t verify_firmware(uint32_t src_addr, uint32_t dest_addr, uint32_t size) { uint32_t *src = (uint32_t*)src_addr; uint32_t *dest = (uint32_t*)dest_addr; for(uint32_t i=0; i<size/4; i++) { if(src[i] != dest[i]) { return 0; } } return 1; }4.3 启动流程优化
void bootloader_main(void) { // 最小化硬件初始化 clock_init(); gpio_init(); uart_init(115200); // 读取升级标志 uint8_t update_flag = read_update_flag(); if(update_flag == NEED_UPDATE) { // 执行固件升级流程 if(perform_update() == UPDATE_SUCCESS) { clear_update_flag(); } } // 验证应用程序 if(validate_app(APP_ADDRESS)) { jump_to_app(APP_ADDRESS); } // 应用程序无效,进入恢复模式 enter_recovery_mode(); }5. 测试与验证
5.1 尺寸验证
使用以下方法验证BootLoader大小:
- 查看生成的.map文件,确认代码段和数据段大小
- 检查生成的.bin文件实际大小
- 使用
size工具查看各段占用情况
$ arm-none-eabi-size bootloader.elf text data bss dec hex filename 11012 128 1024 12164 2f84 bootloader.elf5.2 功能测试
测试用例:
- 正常启动跳转测试
- 固件升级流程测试
- 断电恢复测试
- 错误固件处理测试
测试脚本示例:
import serial import time def send_firmware(port, filename): ser = serial.Serial(port, 115200, timeout=1) with open(filename, 'rb') as f: data = f.read() # 等待BootLoader准备好接收数据 while True: if ser.read(1) == b'C': break # 分块发送固件 block_size = 128 for i in range(0, len(data), block_size): block = data[i:i+block_size] # 发送XMODEM协议头 ser.write(bytes([0x01, (i//block_size+1) & 0xFF, 0xFF - ((i//block_size+1) & 0xFF)])) ser.write(block) # 填充不足的块 if len(block) < block_size: ser.write(bytes([0x1A]*(block_size - len(block)))) # 发送校验和 checksum = sum(block) & 0xFF ser.write(bytes([checksum])) # 等待ACK if ser.read(1) != b'\x06': print("传输错误") return False # 发送结束标志 ser.write(bytes([0x04])) return ser.read(1) == b'\x06'6. 性能优化进阶技巧
6.1 汇编级优化
对于性能关键的代码段,可以使用内联汇编进行优化:
void __attribute__((naked)) memcpy_fast(void *dest, const void *src, size_t n) { __asm volatile( "1: \n" "ldmia r1!, {r3} \n" "stmia r0!, {r3} \n" "subs r2, r2, #4 \n" "bne 1b \n" "bx lr \n" ); }6.2 中断处理优化
在BootLoader中,通常只需要少量关键中断:
void NMI_Handler(void) { while(1); } void HardFault_Handler(void) { while(1); } void SVC_Handler(void) { while(1); } void PendSV_Handler(void) { while(1); } void SysTick_Handler(void) { /* 可选 */ } // 在初始化时禁用所有不必要的中断 void disable_unused_interrupts(void) { for(int i=0; i<8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; // 禁用所有中断 } }6.3 通信协议加速
通过DMA加速数据传输:
void uart_dma_init(void) { // 配置DMA通道 DMA1_Channel4->CCR = DMA_CCR_MINC | DMA_CCR_DIR; DMA1_Channel4->CPAR = (uint32_t)&USART1->DR; // 启用DMA中断 NVIC_EnableIRQ(DMA1_Channel4_IRQn); } void uart_dma_receive(uint8_t *buffer, uint32_t length) { DMA1_Channel4->CCR &= ~DMA_CCR_EN; DMA1_Channel4->CMAR = (uint32_t)buffer; DMA1_Channel4->CNDTR = length; DMA1_Channel4->CCR |= DMA_CCR_EN; USART1->CR3 |= USART_CR3_DMAR; }7. 常见问题与解决方案
7.1 BootLoader过大问题
可能原因:
- 使用了大型库函数(如printf)
- 编译器优化不足
- 包含了不必要的驱动代码
解决方案:
- 使用简化版的打印函数:
void uart_putc(char c) { while(!(USART1->SR & USART_SR_TXE)); USART1->DR = c; } void uart_puts(const char *s) { while(*s) { uart_putc(*s++); } }7.2 跳转失败问题
调试步骤:
- 检查应用程序的向量表地址是否正确
- 验证栈指针是否有效
- 检查应用程序的编译选项是否正确
应用程序需要做的修改:
// 在应用程序的中断向量表中设置正确的偏移量 SCB->VTOR = FLASH_BASE | 0x4000; // 假设App起始地址为0x080040007.3 固件校验失败
增强校验方法:
uint32_t calculate_crc32(uint32_t *data, uint32_t length) { RCC->AHBENR |= RCC_AHBENR_CRCEN; CRC->CR = CRC_CR_RESET; for(uint32_t i=0; i<length/4; i++) { CRC->DR = data[i]; } return CRC->DR; }在实际项目中,我发现最容易被忽视的是Flash操作后的延迟问题。特别是在低端MCU上,Flash写入后需要足够的等待时间才能进行验证。一个实用的技巧是在关键Flash操作后添加适当的延迟,虽然这会稍微增加升级时间,但能显著提高可靠性。