• RT-Thread Nano系统启动过程研究


    • 这里介绍的启动过程是以 STM32F407 芯片为例;

    • 使用的启动文件是适配 keil MDK 编译环境的,startup_stm32f407xx.S 启动文件;

    • RT-Thread 版本是 3.1.5 版本代码。

    总体启动流程介绍

    startup_xxx.S启动文件

    每款MCU芯片都有对应的官方使用汇编文件编写的启动文件 start_up_xxx.S ,这个启动文件主要是完成芯片时钟初始化,配置芯片的中断向量表、全局/静态变量初始化、设置栈顶指针等工作。

    在完成这些工作后,如果没有使用 RT-Thread 系统,那么就会跳转到用户的 main 函数,执行用户相关的代码了。下面是 startup_stm32f407xx.S 这个启动文件的 Resert_handler 的汇编代码。

    ; Reset handler
    Reset_Handler    PROC
                     EXPORT  Reset_Handler             [WEAK]
            IMPORT  SystemInit
            IMPORT  __main
    
                     LDR     R0, =SystemInit
                     BLX     R0
                     LDR     R0, =__main
                     BX      R0
                     ENDP
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中调用了 SystemInit 函数初始化了系统时钟之后,就跳转到了 __main 函数了,这个函数是 keil MDK 内置的一个函数,主要是完成数据段重定位、bss段清零等工作,然后最终跳转到用户 main 函数。

    加入RT-Thread的启动过程

    上面介绍的就是没有添加 RT-Thread 的芯片上电启动过程。当我们添加了 RT-Thread 之后,启动过程还是一样的,只是在调用 main 函数之前,调用了 RT-Thread 提供的一部分初始化代码,然后由 RT-Thread 启动 main 函数。即:

    在这里插入图片描述

    如下,是加入了 RT-Thread 之后的启动流程。

    在这里插入图片描述

    当运行完启动文件之后,就会运行 rtthread_startup(void) 这个RT-Thread 统一的入口函数,这个入口函数主要完成了以下工作:

    1. 全局关中断,初始化与系统相关的硬件。
    2. 打印系统版本信息,初始化系统内核对象(如定时器、调度器)。
    3. 初始化用户 main 线程(同时会初始化线程栈),在 main 线程中对各类模块依次进行初始化。
    4. 初始化软件定时器线程、初始化空闲线程。
    5. 启动调度器,系统切换到第一个线程开始运行(如 main 线程),并打开全局中断。

    为什么跳转到__main函数会先启动RT-Thread

    厂商提供的 startup_stm32f407xx.S 这个启动文件中,可以了解到最后是跳转到了 __main 函数运行的。跳转到这个函数之后,为什么会先运行 RT-Thread 提供的 rtthread_startup(void) 入口函数呢?

    这就涉及到 __CC_ARM 编译器(keil MDK就是用这个编译器的)环境下的 $Sub$$$Super$$ 补丁功能了,使用这个功能可以在不修改原函数的情况下在函数之前或者之后添加希望运行的代码。如对 main 函数的修饰作用:

    • $Sub$$main :新定义的main函数“补丁”,在运行 main 函数之前会先运行这个函数。
    • $Super$$main :调用这个函数将直接跳到原始 mian 函数。

    而RT-Thread 在 components.c 文件中使用上面介绍的两个修饰符,如下面一段代码所示:

    #if defined(__CC_ARM) || defined(__CLANG_ARM)     // 使用 __CC_ARM 或者 __CLANG_ARM 编译器,比如 keil MDK 使用的就是 __CC_ARM
    extern int $Super$$main(void);
    /* re-define main function */
    int $Sub$$main(void)		// 这个就是RT-Thread实现的main函数的补丁函数,运行main函数之前会先运行这个函数
    {
        rtthread_startup();
        return 0;
    }
    #elif defined(__ICCARM__)     // 使用 __ICCARM 编译器,比如 IAR
    extern int main(void);
    /* __low_level_init will auto called by IAR cstartup */
    extern void __iar_data_init3(void);
    int __low_level_init(void)
    {
        // call IAR table copy function.
        __iar_data_init3();
        rtthread_startup();
        return 0;
    }
    #elif defined(__GNUC__)
    /* Add -eentry to arm-none-eabi-gcc argument */
    int entry(void)				 // 使用 GCC 编译器
    {
        rtthread_startup();
        return 0;
    }
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    上面代码中,我们只看使用了 keil MDK 开发环境的那段代码。可以看出定义了一个 int $Sub$$main(void) 函数。

    也就是说,在启动文件最后跳转到 __main 函数后,这个函数想要运行 main 函数之前,会先调用 int $Sub$$main(void) 函数,而这个函数则调用了 rtthread_startup() 函数,所以才会先运行 rtthread_startup() 这个函数的。过程就是:

    在这里插入图片描述

    然后在 rtthread_startup() 函数内部,会调用一个 应用程序初始化函数 rt_application_init() ,下面是这个函数的定义,以及这个函数有关的代码。

    /* the system main thread */
    void main_thread_entry(void *parameter)
    {
        extern int main(void);
        extern int $Super$$main(void);
    
    #ifdef RT_USING_COMPONENTS_INIT
        /* RT-Thread components initialization */
        rt_components_init();
    #endif
        /* invoke system main function */
    #if defined(__CC_ARM) || defined(__CLANG_ARM)
        $Super$$main(); /* for ARMCC. */			// 这里实际上就是调用了用户实现的main函数
    #elif defined(__ICCARM__) || defined(__GNUC__)
        main();
    #endif
    }
    
    void rt_application_init(void)
    {
        rt_thread_t tid;
    
    #ifdef RT_USING_HEAP
        tid = rt_thread_create("main", main_thread_entry, RT_NULL,
                               RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
        RT_ASSERT(tid != RT_NULL);
    #else
        rt_err_t result;
    
        tid = &main_thread;
        result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
                                main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
        RT_ASSERT(result == RT_EOK);
    
        /* if not define RT_USING_HEAP, using to eliminate the warning */
        (void)result;
    #endif
    
        rt_thread_startup(tid);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    可以看出 rt_application_init() 这个函数内部创建了一个 main 线程,而这个线程函数最终调用了 $Super$$main() 函数,而对于 ARMCC 编译器语法,实际上就是相当于跳转到了用户定义的 main 函数了。

    在这里插入图片描述

  • 相关阅读:
    如何打造可视化警务巡防通信解决方案
    http状态,cookie、session、token的对比
    原版畅销36万册!世界级网工打造TCP/IP圣经级教材,第5版终现身
    java文件压缩加密,使用流的方式
    总结:从实模式到保护模式的流程和相关寄存器,相关数据结构之间的联系
    Java之JvisualVM简介
    JMeter —— 接口自动化测试(数据驱动)
    简述Linux磁盘IO
    166 个最常用的 Linux 命令汇总,总有你需要用到的...
    Linux、docker、kubernetes、MySql、Shell运维快餐
  • 原文地址:https://blog.csdn.net/luobeihai/article/details/126423286