前言

在嵌入式系统开发中,Bootloader 是一个关键的底层软件组件。它在系统启动时执行硬件初始化和配置,并负责将控制权移交给主应用程序。在案例的项目架构中,采用了 Bootloader 和应用程序分离的设计方案,两者具有独立的内存分配。

具体的内存规划如下:

  1. Bootloader 内存布局
  • 起始地址:0x8000000
  • 配置详情:
MEMORY
{
	RAM    (xrw)    : ORIGIN = 0x24000000,   LENGTH = 512K
	FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 128K
}
  • 这段 128K 的 FLASH 空间专门用于存储 Bootloader 的代码和执行。
  1. 应用程序内存布局
  • 起始地址:0x8020000(位于 Bootloader 区域的末尾之后)
  • 配置详情:
MEMORY
{
	RAM    (xrw)    : ORIGIN = 0x24000000,   LENGTH = 512K
	FLASH    (rx)    : ORIGIN = 0x8020000,   LENGTH = 256K
}
  • 应用程序拥有独立的 256K FLASH 空间,用于存储其代码和数据。

问题

在系统启动过程中,可以观察到 Bootloader 成功跳转到应用程序后,应用程序的初始化流程似乎正常执行 - EasyLogger 的初始化信息已经明确输出,这表明应用程序的代码确实开始运行了。然而令人困惑的是,系统随后却意外崩溃,并产生了详细的寄存器转储信息。

Jump to application running ... 
I/elog            EasyLogger V2.2.99 is initialize success.
I/NO_TAG          app_start
Crash, dump regs:
R0    = 0x00000000
R1    = 0xA5A5A5A5
R2    = 0x00000000
R3    = 0x24002398
R12   = 0x00000024
LR    = 0xFFFFFFF9
PC    = 0x0800C7E4
PSR   = 0x210F000B
BFAR  = 0xA5A5A5A5
CFSR  = 0x00000082
HFSR  = 0x40000000
DFSR  = 0x0000000B
AFSR  = 0x00000000
SHCSR = 0x00010080
R4    = 0x08000000
R5    = 0x00000000
R6    = 0x080289A5
R7    = 0x24080000
R8    = 0x00000000
R9    = 0x00000000
R10   = 0x00000000
R11   = 0x00000000
dump sp stack[sp sp-512]:
0x00000000,0xA5A5A5A5,0x00000000,0x24002398,
0x00000024,0xFFFFFFF9,0x0800C7E4,0x210F000B,
0x00000000,0x00000040,0x00000000,0x12000000,
0x00000024,0x0802D6F3,0x0802D762,0x210F0000,

从崩溃日志中,发现几个关键异常点:

  1. 程序计数器位置异常
    PC 寄存器值为 0x0800C7E4,这个地址明确位于 Bootloader 的存储区域(0x08000000 - 0x0801FFFF)。这非常不符合预期,因为此时系统应该正在执行应用程序的代码。
  2. 栈指针异常
    SP 寄存器值为 0x00000000,这是一个无效的栈地址。同时,栈内存转储显示前两个值也是 0x000000000xA5A5A5A5(典型的未初始化内存标记),表明栈可能已被破坏。

问题排查

为了揪出问题的根源,需要对应用程序的代码进行的排查。经过逐行分析,发现了一个关键线索:当应用程序执行到 prvPortStartFirstTask 函数时,就会触发崩溃。这个函数在 FreeRTOS 中的主要职责是启动第一个任务。


static void prvPortStartFirstTask( void )
{
    __asm volatile (
        " ldr r0, =0xE000ED08   \n" /* Use the NVIC offset register to locate the stack. */
        " ldr r0, [r0]          \n"
        " ldr r0, [r0]          \n"
        " msr msp, r0           \n" /* Set the msp back to the start of the stack. */
        " mov r0, #0            \n" /* Clear the bit that indicates the FPU is in use, see comment above. */
        " msr control, r0       \n"
        " cpsie i               \n" /* Globally enable interrupts. */
        " cpsie f               \n"
        " dsb                   \n"
        " isb                   \n"
        " svc 0                 \n" /* System call to start first task. */
        " nop                   \n"
        " .ltorg                \n"
        );
}

这个函数的工作流程可以概括为以下几个关键步骤:

  • 向量表处理:通过 Cortex-M7VTOR 寄存器(地址为 0xE000ED08)来获取向量表的首地址。这个地址在链接脚本中被定义为 FLASH 的起始地址。
  • 堆栈初始化:从向量表的第一个条目中加载初始主堆栈指针(MSP)。这个指针对应于链接脚本中定义的 _estack,也就是 RAM 的结束地址。
  • 上下文切换:通过执行 svc 0 触发 SVC 异常。这一操作会使得程序跳转到启动文件(startup_stm32xxx.s)中定义的 SVC_Handler。SVC_Handler 的任务是加载第一个任务的上下文,从而使处理器能够切换到第一个任务的上下文并开始运行。

简而言之,prvPortStartFirstTask 函数通过 Cortex-M7VTOR 寄存器获取向量表的首地址,进而加载初始主堆栈指针(MSP),并通过触发 SVC 异常来切换到第一个任务的上下文并启动它。

调试

在排查问题的过程中,首先尝试在 SVC_Handler 中添加打印语句,但发现没有任何输出,这表明 SVC_Handler 根本没有被调用。为了进一步了解问题所在,决定在 prvPortStartFirstTask 函数中加入一些调试代码,以便查看向量表的内容。

// 打印寄存器值的辅助函数
void print_register(const char *name, uint32_t value) {
    char buf[64];
    snprintf(buf, sizeof(buf), "%s = 0x%08lX\r\n", name, value);
    printf(buf);
}

// 打印向量表内容
void print_vector_table(uint32_t *vtor) {
    char buf[128];
    
    // 打印向量表地址
    snprintf(buf, sizeof(buf), "\nVector Table @ 0x%08lX\r\n", (uint32_t)vtor);
    printf(buf);
    
    // 打印前16个向量(覆盖所有核心异常)
    for(int i = 0; i < 16; i++) {
        uint32_t vector_addr = vtor[i];
        snprintf(buf, sizeof(buf), "Vec[%2d] @ 0x%08lX = 0x%08lX %s\r\n", 
                 i, (uint32_t)&vtor[i], vector_addr,
                 (i == 1) ? "(Reset_Handler)" : 
                 (i == 11) ? "(SVC_Handler)" : "");
        printf(buf);
    }
}
static void prvPortStartFirstTask( void )
{
    /* 添加调试输出 */
    volatile uint32_t *vtor = (uint32_t *)0xE000ED08;
    uint32_t vector_table_addr = *vtor;
    
    print_register("VTOR Register", (uint32_t)vtor);
    print_register("Vector Table Addr", vector_table_addr);
    print_vector_table((uint32_t *)vector_table_addr);
	xxxxxxx

通过这些调试代码,系统打印出了向量表的内容,结果如下:

VTOR Register = 0xE000ED08
Vector Table Addr = 0x08000000

Vector Table @ 0x08000000
Vec[ 0] @ 0x08000000 = 0x24080000 
Vec[ 1] @ 0x08000004 = 0x080075D5 (Reset_Handler)
Vec[ 2] @ 0x08000008 = 0x08007625 
Vec[ 3] @ 0x0800000C = 0x08006B0D 
Vec[ 4] @ 0x08000010 = 0x08006B2F 
Vec[ 5] @ 0x08000014 = 0x08007625 
Vec[ 6] @ 0x08000018 = 0x08007625 
Vec[ 7] @ 0x0800001C = 0x00000000 
Vec[ 8] @ 0x08000020 = 0x00000000 
Vec[ 9] @ 0x08000024 = 0x00000000 
Vec[10] @ 0x08000028 = 0x00000000 
Vec[11] @ 0x0800002C = 0x0800C7E1 (SVC_Handler)
Vec[12] @ 0x08000030 = 0x08006B51 
Vec[13] @ 0x08000034 = 0x00000000 
Vec[14] @ 0x08000038 = 0x0800C811 
Vec[15] @ 0x0800003C = 0x08006B53 

从打印结果可以看出,向量表的地址仍然是 Bootloader 的起始地址 0x08000000,而没有被更新到应用程序的向量表地址 0x8020000

这就意味着,当 prvPortStartFirstTask 函数通过 VTOR 寄存器获取向量表时,它获取到的仍然是 Bootloader 的向量表,而不是应用程序的向量表。因此,当它尝试通过向量表调用 SVC_Handler 时,实际上调用的是 Bootloader 中的 SVC_Handler,而不是应用程序中的 SVC_Handler,这直接导致了崩溃的发生。

进一步分析,问题的根本原因在于 VTOR 寄存器的设置错误。具体来说:

  • VTOR 设置错误VTOR 寄存器仍然指向 Bootloader 区域(0x08000000),而不是应用程序的向量表(0x8020000)。
  • SVC_Handler 地址错误:向量表中 SVC_Handler 的地址是 0x0800C7E1,而崩溃时的程序计数器(PC)值为 0x0800C7E4,这表明程序试图执行 Bootloader 中的代码,而不是应用程序中的代码。

bootloader代码

这段代码负责将控制权从 Bootloader 转移到应用程序。代码本身能够顺利地跳转到应用程序的入口点,但问题在于它没有正确地更新向量表的指向,导致应用程序运行时仍然在使用 Bootloader 的向量表,这就引发了后续的崩溃问题。

void qbt_jump_to_app(void)
{
    typedef void (*app_func_t)(void);
    uint32_t app_addr = QBOOT_APP_ADDR;
    uint32_t stk_addr = *((__IO uint32_t *)app_addr);
    app_func_t app_func = (app_func_t)(*((__IO uint32_t *)(app_addr + 4)));

    if ((((uint32_t)app_func & 0xff000000) != 0x08000000) || (((stk_addr & 0x2ff00000) != 0x20000000) && ((stk_addr & 0x2ff00000) != 0x24000000)))
    {
        printf("No legitimate application.\r\n");
        return;
    }

    printf("Jump to application running ... \r\n");
    HAL_Delay(200);
    
    __disable_irq();
    HAL_DeInit();
    HAL_RCC_DeInit();
    
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL = 0;

    for(int i=0; i<128; i++)
    {
        HAL_NVIC_DisableIRQ(i);
        HAL_NVIC_ClearPendingIRQ(i);
    }
    
    __set_CONTROL(0);
    __set_MSP(stk_addr);
    
    app_func();//Jump to application running
    
    printf("Qboot jump to application fail.\r\n");
}

在这段代码中,Bootloader 在跳转到应用程序之前,执行了一系列的初始化操作,包括关闭中断、初始化硬件、清除 SysTick 等,这些操作都是为了确保应用程序能够在干净的环境中启动。然而,代码中缺少了一个关键步骤——更新 VTOR 寄存器的值。

VTOR 寄存器用于指定向量表的地址,而向量表是中断和异常处理的关键。由于没有更新 VTOR 寄存器,当应用程序运行时,它仍然使用 Bootloader 的向量表,这就导致了应用程序在尝试调用 SVC_Handler 时,实际上调用的是 Bootloader 中的 SVC_Handler,而不是应用程序中的 SVC_Handler,从而引发了崩溃。

解决方案

找到问题所在后,解决方法就显得相对直接了。关键在于,在 Bootloader 准备把控制权交给应用程序之前,得确保 VTOR 寄存器的值被正确更新,让它指向应用程序的向量表地址,也就是 0x8020000。这样,应用程序运行时就能用上属于自己的向量表了。修改后的 Bootloader 代码片段如下:

void qbt_jump_to_app(void)
{
	xxxxxx
    SCB->VTOR = app_addr; // 设置向量表偏移寄存器
    app_func(); // Jump to application running
    
    printf("Qboot jump to application fail.\r\n");
}

在代码里,SCB->VTOR = app_addr; 这一行就是解决问题的关键操作。它把 VTOR 寄存器的值更新为应用程序的向量表地址,确保应用程序在运行时能正确地使用自己的向量表。

验证修复

为了验证这个修改是否真的解决了问题,重新运行了应用程序,并且在 prvPortStartFirstTask 函数里打印出了向量表的内容。打印结果如下:

VTOR Register = 0xE000ED08
Vector Table Addr = 0x08020000

Vector Table @ 0x08020000
Vec[ 0] @ 0x08020000 = 0x24080000 
Vec[ 1] @ 0x08020004 = 0x080289B5 (Reset_Handler)
Vec[ 2] @ 0x08020008 = 0x08028A05 
Vec[ 3] @ 0x0802000C = 0x0802815D 
Vec[ 4] @ 0x08020010 = 0x0802817F 
Vec[ 5] @ 0x08020014 = 0x08028A05 
Vec[ 6] @ 0x08020018 = 0x08028A05 
Vec[ 7] @ 0x0802001C = 0x00000000 
Vec[ 8] @ 0x08020020 = 0x00000000 
Vec[ 9] @ 0x08020024 = 0x00000000 
Vec[10] @ 0x08020028 = 0x00000000 
Vec[11] @ 0x0802002C = 0x0802D681 (SVC_Handler)
Vec[12] @ 0x08020030 = 0x080281A1 
Vec[13] @ 0x08020034 = 0x00000000 
Vec[14] @ 0x08020038 = 0x0802D7D1 
Vec[15] @ 0x0802003C = 0x080281A3 

从打印结果可以看出,向量表的地址已经成功更新为应用程序的向量表地址 0x8020000,而且 SVC_Handler 的地址也指向了应用程序中的正确地址 0x0802D681。这说明 VTOR 寄存器已经被正确设置,应用程序能够正常调用它自己的 SVC_Handler,而不是 Bootloader 中的 SVC_Handler

其他解决方案

除了在 Bootloader 里更新 VTOR 寄存器这一招之外,还有别的办法能在应用程序这边确保向量表用得对。这些方法有时候能当成替代方案使,或者当成多一层保障也不错。

在启动代码中设置 VTOR(推荐)

可以在应用程序的启动文件里(通常叫 startup_stm32*.s),在 Reset_Handler 的开头加几行代码,直接把 VTOR 寄存器设置好。这招的好处是,它能在应用程序刚启动的时候,就把向量表的事儿给安排妥当,避免后面再出岔子。代码改起来也简单,像这样:

Reset_Handler:
  /* 设置 VTOR 到应用程序向量表 */
  ldr r0, =0xE000ED08    /* SCB->VTOR 寄存器地址 */
  ldr r1, =0x8020000     /* 应用程序向量表地址 */
  str r1, [r0]           /* 设置 VTOR = 0x8020000 */
  
  /* 继续原有初始化代码 */
  ldr sp, =_estack

不过,要是项目里 Bootloader 和应用程序共用一个启动文件,那这个方案就不适用了。

在 SystemInit() 函数中设置

要是项目里有 SystemInit() 函数,用来初始化系统时钟或者别的硬件资源,那也可以在这个函数里顺手把 VTOR 寄存器的设置代码加上。这样做的好处是,它和启动代码分开,以后维护或者管理起来都方便不少。代码改起来也很简单:

void SystemInit(void)
{
  /* 设置向量表偏移 */
  SCB->VTOR = 0x8020000;
  
  /* 原有时钟初始化代码 */
  /* ... */
}

在 main() 函数开头设置

main() 函数的开头设置 VTOR 寄存器,也是个可行的办法。虽然相比前面两种方法,这个设置得稍微晚一点,但它还是能在应用程序的主要逻辑跑起来之前,把向量表的事儿给搞定。代码改起来也很直观:

int main(void)
{
  // 尽早设置向量表
  SCB->VTOR = 0x8020000;
  
  // 其他初始化
  HAL_Init();
  SystemClock_Config();
  
  // FreeRTOS 初始化
  xTaskCreate(/* ... */);
  vTaskStartScheduler();
}

总结

在嵌入式开发里,Bootloader 和应用程序之间的切换特别关键,就像接力赛里交接棒的瞬间。要是这一棒没接好,整个系统可能就跑不起来了。本文通过分析一个带 Bootloader 的 FreeRTOS 项目启动失败的问题,把问题的来龙去脉都梳理清楚了,也找到了解决办法。

问题的根源在于 Bootloader 跳转到应用程序后,VTOR 寄存器的值没更新,导致应用程序还在用 Bootloader 的向量表。这就像是应用程序在找自己的工具箱,结果找错了地方,自然就乱套了。最后通过在 Bootloader 中更新 VTOR 寄存器的值,或者在应用程序启动时显式设置 VTOR 寄存器,解决了这个问题。这样一来,应用程序就能顺利找到自己的向量表,正常运行了。

除了直接在 Bootloader 里解决问题,本文还提供了几种在应用程序中设置 VTOR 寄存器的方法。比如在启动代码里设置、在 SystemInit() 函数里设置,或者在 main() 函数开头设置。这些方法各有优势,开发者可以根据项目的实际情况和个人习惯,选择最适合的方式。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐