2. Flash分区规划与链接脚本配置

任务目标

在Bootloader差分OTA方案中,应用程序需要从固定地址启动(而非默认的0x08000000)。本案例将完成:修改链接脚本使应用程序链接到指定Flash地址,编写启动代码重定位中断向量表,并通过Bootloader跳转验证应用能否正确运行。

环境与依赖

  • MCU:STM32F103系列(Flash基址0x08000000,SRAM基址0x20000000)
  • 工具链:arm-none-eabi-gcc + Makefile(或STM32CubeIDE)
  • Bootloader:位于0x08000000,跳转地址为0x08008000(即偏移32KB)

实现步骤

1. 链接脚本修改(app.ld)

将应用程序的Flash起始地址改为0x08008000,并保留中断向量表在段开头:

/* app.ld - 应用程序链接脚本 */
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08008000, LENGTH = 96K  /* 偏移32KB,剩余96KB */
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector))    /* 中断向量表必须位于起始地址 */
    . = ALIGN(4);
  } > FLASH

  .text :
  {
    . = ALIGN(4);
    *(.text*)
    *(.rodata*)
    . = ALIGN(4);
    _etext = .;
  } > FLASH

  .data :
  {
    . = ALIGN(4);
    _sdata = .;
    *(.data*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT> FLASH

  .bss :
  {
    . = ALIGN(4);
    _sbss = .;
    *(.bss*)
    . = ALIGN(4);
    _ebss = .;
  } > RAM
}

2. 启动代码(startup.c)

在进入main前完成向量表重定位和必要的外设时钟使能:

/* startup.c - 应用程序启动代码 */
#include <stdint.h>

extern uint32_t _sdata, _edata, _sbss, _ebss;
extern int main(void);

/* 中断向量表 - 由链接脚本放置在0x08008000 */
extern void Reset_Handler(void);
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    (void (*)(void))0x20005000,  /* 初始SP,指向RAM顶端 */
    Reset_Handler,               /* Reset */
    /* 其他异常/中断可在此添加,本例省略 */
};

void Reset_Handler(void) {
    /* 1. 拷贝.data段 */
    uint32_t *src = &_sdata;
    uint32_t *dst = (uint32_t*)&_sdata;
    /* 注意:_sdata在RAM中,_etext在Flash中,需用__etext */
    extern uint32_t __etext;
    src = &__etext;
    dst = &_sdata;
    while (dst < &_edata) *dst++ = *src++;

    /* 2. 清零.bss段 */
    dst = &_sbss;
    while (dst < &_ebss) *dst++ = 0;

    /* 3. 重定位中断向量表到SRAM(可选,但推荐) */
    /* 若直接运行在Flash,可跳过此步;此处演示SRAM重定位 */
    /* 实际工程中需将0x08008000处的向量表复制到SRAM并设置VTOR */
    /* 本最小示例直接使用Flash中的向量表,不执行重定位 */

    /* 4. 跳转main */
    main();
    while (1);
}

3. 主程序(main.c)

/* main.c - 验证应用程序运行 */
#include <stdint.h>

/* 简单延时 */
static void delay(volatile uint32_t count) {
    while (count--) {}
}

int main(void) {
    /* 使能GPIOC时钟(RCC_APB2ENR 地址0x40021018) */
    volatile uint32_t *RCC_APB2ENR = (uint32_t*)0x40021018;
    *RCC_APB2ENR |= (1 << 4);  /* IOPCEN */

    /* PC13配置为推挽输出(CRH 地址0x40011004) */
    volatile uint32_t *GPIOC_CRH = (uint32_t*)0x40011004;
    *GPIOC_CRH = (*GPIOC_CRH & ~(0xF << 20)) | (0x2 << 20); /* 输出50MHz */

    volatile uint32_t *GPIOC_ODR = (uint32_t*)0x4001100C;
    while (1) {
        *GPIOC_ODR ^= (1 << 13);  /* 翻转PC13 */
        delay(2000000);
    }
}

4. 编译与链接命令

arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb startup.c -o startup.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb main.c -o main.o
arm-none-eabi-ld -T app.ld startup.o main.o -o app.elf
arm-none-eabi-objcopy -O binary app.elf app.bin

Bootloader跳转验证

Bootloader侧跳转代码(供参考,不属于本案例编译范围):

/* Bootloader跳转函数 - 位于0x08000000的Bootloader中 */
void jump_to_app(void) {
    uint32_t app_addr = 0x08008000;
    void (*app_reset)(void) = (void (*)(void))(*(uint32_t*)(app_addr + 4));
    __set_MSP(*(uint32_t*)app_addr);  /* 设置主栈指针 */
    app_reset();                       /* 跳转 */
}

验证方法与判定标准

  1. 构建检查:编译链接无错误,app.elf生成成功。
  2. 地址验证:执行arm-none-eabi-nm app.elf | grep Reset_Handler,确认Reset_Handler地址为0x08008004(向量表偏移4字节处)。
  3. 烧录与运行:将Bootloader烧录至0x08000000,将app.bin烧录至0x08008000。复位后观察PC13引脚LED以约1Hz频率闪烁。
  4. 跳转成功标志:若LED闪烁,说明Bootloader成功跳转且应用程序在0x08008000正常运行;若LED不亮或跑飞(进入HardFault),检查向量表首字是否为有效栈顶地址(0x20005000)及链接地址是否正确。

常见问题

  • 跳转后无反应:确认app.bin确实烧录在0x08008000,且Bootloader跳转代码中读取的向量表地址正确。
  • 中断不工作:若应用程序使用中断,需在启动代码中设置SCB->VTOR = 0x08008000(Cortex-M3),本最小示例未使用中断故省略。
  • 链接报错region overflow:检查app.ld中FLASH长度是否足够,当前配置96KB适用于较小应用。