大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是 IVT 里的不同 entry 设置可能会造成 i.MXRT1xxx 系列启动 App 后发生异常跑飞问题的分析解决经验。
事情缘起恩智浦官方论坛上的一个疑问帖 《RT1015 dev_cdc_vcom_freertos reset entry failed》,这是客户 QISDA 遇到的问题,由痞子衡的同事 - 非常细心负责的 Kerry 小姐姐将问题整理出来并发了贴,帖子里做了详尽的问题描述以及各种测试结果。看完长帖后,痞子衡第一猜想就是跟 App 栈设置有关,最终也确实是这个原因。那么为什么栈设置会出问题呢?且听痞子衡细聊:
一、问题描述
让我们先来整理一下帖子里的问题现象,客户在 RT1015-EVK 上测试了恩智浦官方 SDK 里的两个例程,一个是简单的 hello_world,另一个是复杂的 dev_cdc_vcom_freertos,这两个例程在不同 IDE、IVT 中 entry 值组合下现象不一致:
测试 App | IVT 中 entry | 测试 IDE | App 运行结果 |
---|---|---|---|
hello_world | 中断向量表起始地址 / 复位向量函数地址 |
IAR EWARM/ MCUXpresso IDE |
正常 |
dev_cdc_vcom_freertos | 中断向量表起始地址 | IAR EWARM/ MCUXpresso IDE |
正常 |
dev_cdc_vcom_freertos | 复位向量函数地址 | IAR EWARM | 正常 |
dev_cdc_vcom_freertos | 复位向量函数地址 | MCUXpresso IDE | 异常跑飞 |
根据上表结果,其实我们很难得出一个有效推论,只能说这个异常结果在特定的 App, entry 值, MCUXpresso IDE 下才能复现。
二、原因探究
既然暂时看不出原因,那我们先做一些准备工作吧。我们把三个影响因子(App, entry 值, IDE)的差异先整理出来:
2.1 两个 App 的不同链接分配
两个 App 都来自 SDK,是经过官方详尽测试的,所以我们不去怀疑 App 本身的功能异常。它们的差异主要在链接分配上。以 IAR 为例,我们只看 flexspi_nor build,在链接文件中默认分配的堆、栈大小均为 1KB:
/* Sizes */
if (isdefinedsymbol(__stack_size__)) {
define symbol __size_cstack__ = __stack_size__;
} else {
define symbol __size_cstack__ = 0x0400;
}
if (isdefinedsymbol(__heap_size__)) {
define symbol __size_heap__ = __heap_size__;
} else {
define symbol __size_heap__ = 0x0400;
}
hello_world 例程因为比较简单,所以用直接用了默认的堆栈大小,而 dev_cdc_vcom_freertos 例程比较复杂,堆栈做了额外调整,栈增大到了 8KB。
此外我们还注意到 hello_world 例程将其 RW, ZI, 堆栈全部放进了 32KB 的 DTCM;而 dev_cdc_vcom_freertos 例程则将 RW, ZI 放入了 64KB OCRAM,只将堆栈放进了 DTCM:
define symbol m_data_start = 0x20000000;
define symbol m_data_end = 0x20007FFF;
define symbol m_data2_start = 0x20200000;
define symbol m_data2_end = 0x2020FFFF;
define region DATA_region = mem:[from m_data_start to m_data_end-__size_cstack__];
define region DATA2_region = mem:[from m_data2_start to m_data2_end];
define region CSTACK_region = mem:[from m_data_end-__size_cstack__+1 to m_data_end];
// 适用 hello_world 例程
place in DATA_region { block RW };
place in DATA_region { block ZI };
place in DATA_region { last block HEAP };
place in DATA_region { block NCACHE_VAR };
place in CSTACK_region { block CSTACK };
// 适用 dev_cdc_vcom_freertos 例程
place in DATA2_region { block RW };
place in DATA2_region { block ZI };
place in DATA_region { last block HEAP };
place in DATA_region { block NCACHE_VAR };
place in CSTACK_region { block CSTACK };
2.2 entry 值在 BootROM 中的使用
再说 IVT 中的 entry,痞子衡在 i.MXRT1xxx 系列启动那些事系列文章中的 《Bootable image 格式与加载》 的 3.2 节介绍过 IVT 结构以及其作用,IVT 是关键启动头,指导了 BootROM 去搬移 App 以及加载执行,其中 entry 成员主要用于跳转执行。
为什么这个 entry 值既可以是中断向量表(Vector Table)起始地址也可以是复位向量(Reset_Handler)函数地址呢?这取决于 BootROM 中是怎么利用这个 entry 值的,下面函数即是 BootROM 中最终跳转函数:
void jump_to_entry(uint32_t entry)
{
typedef void (*application_callback_t)(void);
static application_callback_t s_app_callback;
pu_irom_mpu_disable();
__DMB();
__DSB();
__ISB();
// The entry point is the absolute address of the call back function
if ((uint32_t)entry & 1)
{
s_app_callback = (application_callback_t)entry;
}
// The entry point is the base address of vector table
else
{
static uint32_t s_stack_pointer;
// Ensure Core read vector table for destination instead of register
volatile uint32_t *vector_table = (volatile uint32_t *)entry;
s_stack_pointer = vector_table[0];
s_app_callback = (application_callback_t)vector_table[1];
// Update Stack pointer
__set_MSP(s_stack_pointer);
__set_PSP(s_stack_pointer);
}
__DSB();
__ISB();
// Jump to user application in the end
s_app_callback();
// Should never reach here
__NOP();
__NOP();
}
从上面的跳转函数 jump_to_entry()实现可以看出,entry 值如果是复位函数地址(即奇地址),那么 BootROM 直接跳转到复位函数执行;如果 entry 值是中断向量表首地址(即偶地址),BootROM 会先将当前 SP 重设到 App 指定的栈顶,然后再跳转到复位函数。
好的,现在我们知道了 IVT 中不同的 entry 值差异在哪了。
2.3 不同 IDE 下 startup 流程
因为涉及到两个不同 IDE,即 IAR 和 MCUXpresso IDE,所以我们分别看一下这两个 IDE 下的 startup 实现。我们知道 main 函数之后的代码基本是 IDE 无关的,而 startup 却是因编译器而异。
痞子衡以 i.MXRT1010 的 SDK2.8.2 包里的例程为例,先用 IAR 打开其中的 dev_cdc_vcom_freertos 例程,找到工程下的 startup_MIMXRT1011.s 文件,看它的 Reset_Handler 实现:
__vector_table
DCD sfe(CSTACK)
DCD Reset_Handler
DCD NMI_Handler ;NMI Handler
DCD HardFault_Handler ;Hard Fault Handler
; ...
__Vectors_End
THUMB
PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER:NOROOT(2)
Reset_Handler
CPSID I ; Mask interrupts
LDR R0, =0xE000ED08
LDR R1, =__vector_table
STR R1, [R0]
LDR R2, [R1]
MSR MSP, R2
LDR R0, =SystemInit
BLX R0
CPSIE I ; Unmask interrupts
LDR R0, =__iar_program_start
BX R0
IAR 版本 Reset_Handler 主要分四步: 重设 VTOR、重设 SP、执行 SystemInit(关看门狗,关 Systick,处理 Cache)、执行 IAR 库函数 __iar_program_start(data/bss/ramfunc 段初始化,跳转到 main)。
再用 MCUXpresso IDE 打开同样的 dev_cdc_vcom_freertos 例程,找到工程下的 startup_mimxrt1011.c 文件,看它的 ResetISR 实现:
extern void _vStackTop(void);
__attribute__ ((used, section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
// Core Level - CM7
&_vStackTop, // The initial stack pointer
ResetISR, // The reset handler
NMI_Handler, // The NMI handler
HardFault_Handler, // The hard fault handler
// ...
}; /* End of g_pfnVectors */
__attribute__ ((section(".after_vectors.reset")))
void ResetISR(void) {
__asm volatile ("cpsid i");
SystemInit();
// Copy the data sections from flash to SRAM.
unsigned int LoadAddr, ExeAddr, SectionLen;
unsigned int *SectionTableAddr;
// Load base address of Global Section Table
SectionTableAddr = &__data_section_table;
// Copy the data sections from flash to SRAM.
while (SectionTableAddr < &__data_section_table_end) {
LoadAddr = *SectionTableAddr++;
ExeAddr = *SectionTableAddr++;
SectionLen = *SectionTableAddr++;
data_init(LoadAddr, ExeAddr, SectionLen);
}
// At this point, SectionTableAddr = &__bss_section_table;
// Zero fill the bss segment
while (SectionTableAddr < &__bss_section_table_end) {
ExeAddr = *SectionTableAddr++;
SectionLen = *SectionTableAddr++;
bss_init(ExeAddr, SectionLen);
}
__asm volatile ("cpsie i");
// Call the Redlib library, which in turn calls main()
__main();
while (1);
}
MCUXpresso IDE 版本 ResetISR 主要分三步: 执行 SystemInit(重设 VTOR,关看门狗,关 Systick,处理 Cache)、data/bss/ramfunc 段初始化、跳转到 main。
经过上面对比,看出差异没有?MCUXpresso IDE 相比 IAR 的 startup 少了一步重设 SP 的动作。
2.4 导致异常跑飞的栈错误
有了前面三节的分析基础,我们基本可以得出 dev_cdc_vcom_freertos 例程异常跑飞的原因是发生了栈错误。为什么会发生栈错误?这是由于 MCUXpresso 下的 startup 中没有重设 SP 操作,所以当 IVT 中的 entry 是复位向量时,BootROM 跳转到 App 后依旧延用 BootROM 中的栈,根据芯片参考手册 System Boot 章节里的信息,BootROM 的栈放在了 OCRAM 空间(0x20200000 - 0x202057FF),但是 dev_cdc_vcom_freertos 例程又把 RW, ZI 段也放进了 OCRAM 中,因此随着 App 的运行对栈的利用(函数调用、局部变量定义)有可能与 App 中的 RW, ZI 段数据(全局变量)互相破坏,程序发生未知跑飞也在意料之中。
解决问题的方法是什么?当然是在 MCUXpresso IDE 的 startup 流程中加入重设 SP 操作,保持与 IAR startup 流程一致。
__attribute__ ((section(".after_vectors.reset")))
void ResetISR(void) {
__asm volatile ("cpsid i");
/* 新增 SP 重设代码 */
__asm volatile ("MSR msp, %0" : : "r" (&_vStackTop) : );
__asm volatile ("MSR psp, %0" : : "r" (&_vStackTop) : );
SystemInit();
// ...
}
至此,IVT 里的不同 entry 设置可能会造成 i.MXRT1xxx 系列启动 App 后发生异常跑飞问题的分析解决经验痞子衡便介绍完毕了,掌声在哪里~~~