FreeRTOS 在带 Bootloader 的项目中启动失败问题分析与解决
嵌入式系统开发中,Bootloader与应用程序分离设计导致启动异常。Bootloader(0x8000000-128K)成功跳转至应用程序(0x8020000-256K)后,系统崩溃并显示PC指针异常地位于Bootloader区域(0x0800C7E4)。调试发现程序在FreeRTOS的prvPortStartFirstTask函数中崩溃,向量表地址(0x08000000)仍指向Bootload
前言
在嵌入式系统开发中,Bootloader 是一个关键的底层软件组件。它在系统启动时执行硬件初始化和配置,并负责将控制权移交给主应用程序。在案例的项目架构中,采用了 Bootloader 和应用程序分离的设计方案,两者具有独立的内存分配。
具体的内存规划如下:
- Bootloader 内存布局:
- 起始地址:
0x8000000 - 配置详情:
MEMORY
{
RAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K
}
- 这段 128K 的 FLASH 空间专门用于存储 Bootloader 的代码和执行。
- 应用程序内存布局:
- 起始地址:
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,
从崩溃日志中,发现几个关键异常点:
- 程序计数器位置异常
PC 寄存器值为0x0800C7E4,这个地址明确位于Bootloader的存储区域(0x08000000 - 0x0801FFFF)。这非常不符合预期,因为此时系统应该正在执行应用程序的代码。 - 栈指针异常
SP 寄存器值为0x00000000,这是一个无效的栈地址。同时,栈内存转储显示前两个值也是0x00000000和0xA5A5A5A5(典型的未初始化内存标记),表明栈可能已被破坏。
问题排查
为了揪出问题的根源,需要对应用程序的代码进行的排查。经过逐行分析,发现了一个关键线索:当应用程序执行到 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-M7的VTOR寄存器(地址为0xE000ED08)来获取向量表的首地址。这个地址在链接脚本中被定义为 FLASH 的起始地址。 - 堆栈初始化:从向量表的第一个条目中加载初始主堆栈指针(MSP)。这个指针对应于链接脚本中定义的
_estack,也就是 RAM 的结束地址。 - 上下文切换:通过执行 svc 0 触发 SVC 异常。这一操作会使得程序跳转到启动文件(
startup_stm32xxx.s)中定义的SVC_Handler。SVC_Handler 的任务是加载第一个任务的上下文,从而使处理器能够切换到第一个任务的上下文并开始运行。
简而言之,prvPortStartFirstTask 函数通过 Cortex-M7 的 VTOR 寄存器获取向量表的首地址,进而加载初始主堆栈指针(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() 函数开头设置。这些方法各有优势,开发者可以根据项目的实际情况和个人习惯,选择最适合的方式。
更多推荐



所有评论(0)