13. FSP库启动文件详解

本章配套视频介绍:

../../_images/video.png

《15-FSP库启动文件详解》

https://www.bilibili.com/video/BV1C34y137uf/

13.1. 什么是启动文件

启动文件是系统上电复位后执行的第一个程序。主要做了以下工作:

  1. 初始化堆栈。

  2. 使能FPU(float-point unit,即浮点单元)。

  3. 定位中断向量表。

  4. 配置系统时钟。

  5. 启用CORTEX-M33栈监视器。

  6. 初始化C语言运行环境。

  7. 初始化变量SystemCoreClock,这个变量存放的是处理器时钟的频率。

  8. 初始化用于触发NVIC中断的ELC(Event Link Controller)事件。

  9. 初始化IO口

13.2. 启动文件代码讲解

13.2.1. 复位程序

13-1:复位程序
void Reset_Handler (void)
{
    /* 使用BSP对系统进行初始化. */
    SystemInit();

    /* Call user application. */
    main();

    while (1)
    {
        /* Infinite Loop. */
    }
}

这是系统上电或复位后执行的第一个程序,使用BSP对系统进行初始化,随后通过main函数进入用户代码。 BSP负责使MCU从复位状态进入到用户的应用程序。在到达用户的应用程序之前,BSP设置栈、堆、时钟、中断、C语言运行环境和堆栈监视器。

13.2.2. 栈区初始化

13-2:栈区初始化
/* Main stack */
static uint8_t g_main_stack[BSP_CFG_STACK_MAIN_BYTES + BSP_TZ_STACK_SEAL_SIZE]
BSP_ALIGN_VARIABLE(BSP_STACK_ALIGNMENT)   //宏展开后为“__attribute__((aligned(8)))”
BSP_PLACE_IN_SECTION(BSP_SECTION_STACK);  //宏展开后为“__attribute__((section( ".stack"))) __attribute__((__used__))”

栈是一种先进后出的内存结构,存放函数的参数值、返回值、局部变量等,在程序运行过程中实时加载和释放。 如果代码中使用的局部变量和函数嵌套较多,则需要增加栈区的大小,需要注意的是, 栈区分配大小不能超过RAM的大小。 宏“BSP_CFG_STACK_MAIN_BYTES”可以在FSP Configuration的“BSP”属性栏中的“RA Common”中通过修改“Main stack size”设置,默认为1KB(0x400 Byte)。

宏“BSP_TZ_STACK_SEAL_SIZE”用于封装栈顶,便于检测并阻止攻击者对栈的攻击。若使用TrustZone,则该宏为8,反之为0。

代码中的“BSP_ALIGN_VARIABLE(BSP_STACK_ALIGNMENT)” ,宏展开后为“__attribute__((aligned(8)))”,

“BSP_PLACE_IN_SECTION(BSP_SECTION_STACK)”,宏展开后为“__attribute__((section( “.stack”))) __attribute__((__used__))”

__attribute__可以设置类型,变量或函数的属性,下面将逐一解释修饰“g_main_stack”的属性。

  • __attribute__((aligned(8))):参数“aligned”指定被修饰对象的对齐方式(以字节为单位)。“aligned(8)”则意为栈区在分配时将 采用8字节对齐方式。

  • __attribute__((section( “.stack”))):参数“section”可以将变量定义到指定的输入段中。“section( “.stack”)”则意为将栈 放到名为“.stack”的输入段中。

  • __attribute__((__used__)):参数“__used__”告诉编译器,这个变量会被使用,即使没被调用也不会被编译器警告, 之所以要用这个属性修饰变量,是因为使用C或C++语言的用户一般不会直接操作栈, 绝大部分时候这个变量是不会被用户直接调用的。

13.2.3. 堆区初始化

13-3:堆区初始化
/* Heap */
#if (BSP_CFG_HEAP_BYTES > 0)  //若分配堆区大小为0则不进行初始化

BSP_DONT_REMOVE static uint8_t g_heap[BSP_CFG_HEAP_BYTES]
BSP_ALIGN_VARIABLE(BSP_STACK_ALIGNMENT)  //宏展开后为“__attribute__((aligned(8)))”
BSP_PLACE_IN_SECTION(BSP_SECTION_HEAP);  //宏展开后为“__attribute__((section(".heap"))) __attribute__((__used__))”
#endif

堆没有栈那样先进后出的顺序,用于动态内存分配,一般由程序员使用malloc和free进行分配和释放。 BSP_CFG_HEAP_BYTES用于配置堆区大小,当这个宏定义为0,则不对堆区进行初始化。 由于MCU中可用的片上SRAM相对较少,且缺乏内存保护,这意味着必须非常小心地控制堆的使用,以避免内存泄漏、溢出和试图过度分配。 因此默认堆区大小被设置为0。如果用户需要(例如一些C标准库函数需要使用堆), 可以在FSP Configuration中“BSP”属性栏的“RA Common”中通过修改“Heap size”来设置堆区大小。

代码中的BSP_DONT_REMOVE只有在使用IAR编译器的情况下才会被宏展开为“__root”,意为强制编译, 保证没有使用的函数或者变量也能够包含在目标代码中。其他环境下这个宏没有宏展开,可以忽略。

宏“BSP_ALIGN_VARIABLE(BSP_STACK_ALIGNMENT)”同样也修饰栈,这里的作用和上文的修饰栈的宏相同。

宏“BSP_PLACE_IN_SECTION(BSP_SECTION_HEAP)”展开后为“__attribute__((section(“.heap”))) __attribute__((__used__))”, 这个宏与上文的BSP_PLACE_IN_SECTION(BSP_SECTION_STACK)作用差不多,只是参数“section”不同,这里的“section(“.heap”)” 意为将堆放到名为“.heap”的输入段中。

13.2.4. 中断向量表初始化

13-4:中断向量表初始化
/* Vector table. */
BSP_DONT_REMOVE const exc_ptr_t __Vectors[BSP_CORTEX_VECTOR_TABLE_ENTRIES]
BSP_PLACE_IN_SECTION(BSP_SECTION_FIXED_VECTORS) =
{
    (exc_ptr_t) (&g_main_stack[0] + BSP_CFG_STACK_MAIN_BYTES), /*      Initial Stack Pointer     */
    Reset_Handler,                                             /*      Reset Handler             */
    NMI_Handler,                                               /*      NMI Handler               */
    HardFault_Handler,                                         /*      Hard Fault Handler        */
    MemManage_Handler,                                         /*      MPU Fault Handler         */
    BusFault_Handler,                                          /*      Bus Fault Handler         */
    UsageFault_Handler,                                        /*      Usage Fault Handler       */
    SecureFault_Handler,                                       /*      Secure Fault Handler      */
    0,                                                         /*      Reserved                  */
    0,                                                         /*      Reserved                  */
    0,                                                         /*      Reserved                  */
    SVC_Handler,                                               /*      SVCall Handler            */
    DebugMon_Handler,                                          /*      Debug Monitor Handler     */
    0,                                                         /*      Reserved                  */
    PendSV_Handler,                                            /*      PendSV Handler            */
    SysTick_Handler,                                           /*      SysTick Handler           */
};

宏“BSP_PLACE_IN_SECTION(BSP_SECTION_FIXED_VECTORS)”展开后为“__attribute__((section(“.fixed_vectors”))) __attribute__((__used__))”。 意为将表放到名为“.fixed_vectors”的输入段中,并且即使不被使用,编译器也不会警告。

13.2.5. SystemInit()

13-5:SystemInit()
void SystemInit (void)
{
#if __FPU_USED

    /* Enable the FPU only when it is used.
     * Code taken from Section 7.1, Cortex-M4 TRM (DDI0439C) */

    /* Set bits 20-23 (CP10 and CP11) to enable FPU. */
    SCB->CPACR = (uint32_t) CP_MASK;
#endif

#if BSP_TZ_SECURE_BUILD
    uint32_t * p_main_stack_top = (uint32_t *) __Vectors[0];
    *p_main_stack_top = BSP_TZ_STACK_SEAL_VALUE;
#endif

    .............. //由于篇幅所限,省略中间代码

    /* Call Post C runtime initialization hook. */
    R_BSP_WarmStart(BSP_WARM_START_POST_C);

    /* Initialize ELC events that will be used to trigger NVIC interrupts. */
    bsp_irq_cfg();

    /* Call any BSP specific code. No arguments are needed so NULL is sent. */
    bsp_init(NULL);
}

这是MCU进入Reset_Handler后执行的第一个函数,正如函数的字面意思,用于初始化MCU和运行环境, 运行完这段代码后将由main进入用户的hal_entry函数,由于代码较长,下面将分为几个部分对代码进行分析。

13.2.5.1. 使能FPU

13-6:使能 FPU
#if __FPU_USED

    /* Enable the FPU only when it is used.
     * Code taken from Section 7.1, Cortex-M4 TRM (DDI0439C) */

    /* Set bits 20-23 (CP10 and CP11) to enable FPU. */
    SCB->CPACR = (uint32_t) CP_MASK;
#endif

FPU(Float-Point Unit)支持单精度加、减、乘、除、乘、累加、平方根运算。它还提供了定点和浮点数据格式以及浮点常量之间的转换的命令。

13.2.5.2. 封装栈顶

13-7:封装栈顶
#if BSP_TZ_SECURE_BUILD
    uint32_t * p_main_stack_top = (uint32_t *) __Vectors[0];
    *p_main_stack_top = BSP_TZ_STACK_SEAL_VALUE;
#endif

这里获取栈顶指针,并将栈顶赋值为“BSP_TZ_STACK_SEAL_VALUE”,它的宏展开为0xFEF5EDA5,不能用作程序地址,因为地址范围从 0xE0000000到0xFFFFFFFF是不可执行的。这个过程被称为封栈(Sealing Stack),过程如图。如果有人针对栈进行攻击,那么这个地址 的值会被覆盖掉,这会被检测并阻止。

图

13.2.5.3. 设置中断向量表的基地址

13-8:设置中断向量表的基地址
#if !BSP_TZ_NONSECURE_BUILD
    SCB->VTOR = (uint32_t) &__Vectors;
#endif

这里通过直接设置SCB->VTOR的值来设置中断向量表的基地址。在非安全项目中,这一步会被跳过。

13.2.5.4. 热启动回调函数

13-9:热启动回调函数
void R_BSP_WarmStart(bsp_warm_start_event_t event) __attribute__((weak));


void R_BSP_WarmStart (bsp_warm_start_event_t event)
{
    if (BSP_WARM_START_RESET == event)
    {
        /* C runtime environment has not been setup so you cannot use globals. System clocks are not setup. */
    }

    if (BSP_WARM_START_POST_CLOCK == event)
    {
        /* C runtime environment has not been setup so you cannot use globals. Clocks have been initialized. */
    }
    else if (BSP_WARM_START_POST_C == event)
    {
        /* C runtime environment, system clocks, and pins are all setup. */
    }
    else
    {
        /* Do nothing */
    }
}

这个函数会被调用三次,分别在时钟初始化前,时钟初始化后和C语言运行环境初始化后被调用,这个函数被声明了“weak”属性,因此其可以被用户重写。 在默认情况下,这个函数会在hal_entry.c中被重写。

13-10:hal_entry中被重写的函数
void R_BSP_WarmStart(bsp_warm_start_event_t event)
{
    if (BSP_WARM_START_RESET == event)
    {
#if BSP_FEATURE_FLASH_LP_VERSION != 0  //因为RA6M5上没有这个功能,因此可以忽略这部分

        /* Enable reading from data flash. */
        R_FACI_LP->DFLCTL = 1U;

        /* Would normally have to wait tDSTOP(6us) for data flash recovery. Placing the enable here, before clock and
         * C runtime initialization, should negate the need for a delay since the initialization will typically take more than 6us. */
#endif
    }

    if (BSP_WARM_START_POST_C == event)
    {
        /* C runtime environment and system clocks are setup. */

        /* Configure pins. */
        R_IOPORT_Open (&g_ioport_ctrl, g_ioport.p_cfg);
    }
}

默认情况下,这里只有在C语言运行环境初始化后,也就是函数传入参数为“BSP_WARM_START_POST_C”时,才会对用户在FSP Configuration中配置的引脚进行初始化,其他时候这里不会进行操作。

13.2.5.5. 时钟初始化

13-11:时钟初始化
bsp_clock_init();

根据bsp_clock_cfg.h中的设置来设置所有系统时钟,这些配置来自于FSP Configuration中的Clocks选项卡。

13.2.5.6. 启用CORTEX-M33栈监视器

13-12:启用CORTEX-M33栈监视器
/* Use CM33 stack monitor. */
__set_MSPLIM(BSP_PRV_STACK_LIMIT);

设置主堆栈指针限制。没有ARMv8-M主扩展的设备(即Cortex-M23)缺乏不安全的堆栈指针限制寄存器,因此在非安全模式下此操作被忽略。

13.2.5.7. 初始化C语言运行环境

13.2.5.7.1. 初始化BSS区
13-13:BSS区初始化
    /* Zero out BSS */
 #if defined(__ARMCC_VERSION)
    memset((uint8_t *) &Image$$BSS$$ZI$$Base, 0U, (uint32_t) &Image$$BSS$$ZI$$Length);
 #elif defined(__GNUC__)
    memset(&__bss_start__, 0U, ((uint32_t) &__bss_end__ - (uint32_t) &__bss_start__));
 #elif defined(__ICCARM__)
    memset((uint32_t *) __section_begin(".bss"), 0U, (uint32_t) __section_size(".bss"));
 #endif

BSS(Block Start by Symbol)指用来存放程序中未初始化的全局变量和静态变量的一块内存区域,在这里BSS区所有数据都会被初始化为0。

13.2.5.7.2. 初始化data区
13-14:DATA区初始化
    /* Copy initialized RAM data from ROM to RAM. */
 #if defined(__ARMCC_VERSION)
    memcpy((uint8_t *) &Image$$DATA$$Base, (uint8_t *) &Load$$DATA$$Base, (uint32_t) &Image$$DATA$$Length);
 #elif defined(__GNUC__)
    memcpy(&__data_start__, &__etext, ((uint32_t) &__data_end__ - (uint32_t) &__data_start__));
 #elif defined(__ICCARM__)
    memcpy((uint32_t *) __section_begin(".data"), (uint32_t *) __section_begin(".data_init"),
           (uint32_t) __section_size(".data"));

data区是用来存放已初始化的全局变量,静态变量和常量的一块内存区域。在这段代码,数据会从ROM被拷贝到RAM的data区。

13.2.5.7.3. 调用全局对象的构造函数
13-15:调用全局或静态对象的构造函数
 #if defined(__ARMCC_VERSION)
int32_t count = Image$$INIT_ARRAY$$Limit - Image$$INIT_ARRAY$$Base;
for (int32_t i = 0; i < count; i++)
{
    void (* p_init_func)(void) =
        (void (*)(void))((uint32_t) &Image$$INIT_ARRAY$$Base + (uint32_t) Image$$INIT_ARRAY$$Base[i]);
    p_init_func();
}

#elif defined(__GNUC__)
    int32_t count = __init_array_end - __init_array_start;
    for (int32_t i = 0; i < count; i++)
    {
        __init_array_start[i]();
    }

#elif defined(__ICCARM__)
    void const * pibase = __section_begin("SHT$$PREINIT_ARRAY");
    void const * ilimit = __section_end("SHT$$INIT_ARRAY");
    __call_ctors(pibase, ilimit);
#endif

RA系列MCU是支持使用C++语言进行开发的,这段代码用于调用C++的全局对象的构造函数。

13.2.5.8. 初始化SystemCoreClock的值

13-16:初始化SystemCoreClock的值
/* Initialize SystemCoreClock variable. */
SystemCoreClockUpdate();

初始化SystemCoreClock的值,这个值表示处理器时钟的频率,默认为200MHz。

13.2.5.9. 初始化ICU

13-17:初始化 ICU
/* Initialize ELC events that will be used to trigger NVIC interrupts. */
bsp_irq_cfg();

这个函数将初始化ICU(Interrupt Control Unit),即中断控制器,以便配置的ELC事件触发NVIC中断。