给STM32F103瘦身:手把手教你打造一个12KB以内的轻量级BootLoader
2026/4/30 14:44:41 网站建设 项目流程

给STM32F103瘦身:手把手教你打造一个12KB以内的轻量级BootLoader

在嵌入式开发中,资源受限的MCU(如STM32F103系列)常常面临内存紧张的挑战。当Flash空间仅有64KB或128KB时,BootLoader的体积优化就显得尤为重要。本文将深入探讨如何通过代码精简、分区策略优化等手段,将BootLoader压缩至12KB以内,同时确保其功能的完整性和可靠性。

1. BootLoader的核心功能与设计考量

BootLoader作为MCU启动的第一段代码,承担着硬件初始化、固件升级和应用程序跳转等关键任务。在资源受限的环境中,我们需要在功能完整性和代码体积之间找到平衡点。

核心功能模块

  • 硬件初始化(时钟、GPIO、通信接口等)
  • 固件验证与升级逻辑
  • 应用程序跳转机制
  • 错误处理与恢复

在设计轻量级BootLoader时,需要特别注意以下几点:

  1. 最小化依赖:仅包含必要的驱动和库
  2. 优化算法:选择空间效率高的实现方式
  3. 精简错误处理:保留关键错误检测,简化非关键路径
  4. 避免浮点运算:使用定点数替代浮点运算

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进行开发时,需要进行以下优化配置:

  1. 编译器优化选项

    • 优化级别设置为-Os(优化大小)
    • 启用链接时间优化(LTO)
    • 禁用未使用的函数和数据消除
  2. 链接器配置

    • 设置ROM起始地址为0x08000000
    • 限制代码大小为0x3000(12KB)
  3. 库配置

    • 使用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大小:

  1. 查看生成的.map文件,确认代码段和数据段大小
  2. 检查生成的.bin文件实际大小
  3. 使用size工具查看各段占用情况
$ arm-none-eabi-size bootloader.elf text data bss dec hex filename 11012 128 1024 12164 2f84 bootloader.elf

5.2 功能测试

测试用例

  1. 正常启动跳转测试
  2. 固件升级流程测试
  3. 断电恢复测试
  4. 错误固件处理测试

测试脚本示例

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过大问题

可能原因

  1. 使用了大型库函数(如printf)
  2. 编译器优化不足
  3. 包含了不必要的驱动代码

解决方案

  • 使用简化版的打印函数:
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 跳转失败问题

调试步骤

  1. 检查应用程序的向量表地址是否正确
  2. 验证栈指针是否有效
  3. 检查应用程序的编译选项是否正确

应用程序需要做的修改

// 在应用程序的中断向量表中设置正确的偏移量 SCB->VTOR = FLASH_BASE | 0x4000; // 假设App起始地址为0x08004000

7.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操作后添加适当的延迟,虽然这会稍微增加升级时间,但能显著提高可靠性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询