利用rt-Thread构建STM32运行环境

硬件:正点原子精英版STM32F103ZET6

软件:RT-Thread-v4.0.1

一、搭建环境

(1)进入到rt-thread\bsp\stm32f10x-HAL目录下,在此处打开ComEmu命令行界面(不会的同学请参考官网提供的示例:Env的使用方法),并输入menuconfig命令,然后有如下界面:利用rt-Thread构建STM32运行环境_第1张图片

配置选项如图所示,这里主要介绍一下RT-Thread Components选项的配置:进入到RT-Thread Components选项,选择了The main() function as user entry function;这里的意思是将main()作为用户的入口函数,稍后会讲到这里的具体作用;

利用rt-Thread构建STM32运行环境_第2张图片

(2)保存配置,退出;

之后编译有多种方法可选:

①使用命令行scons命令编译,其中编译器是ARM-GCC,编译结果是bin文件;

②使用scons –target=mdk5命令,构建keil5的开发环境,然后打开project.uvprojx工程,这里告诉大家一个小技巧,因为每次修改配置后,这个project.uvprojx工程都会根据template.uvprojx工程重新生成一遍,然后在keil界面里设置有关硬件参数,再编译烧写,所以,可以根据自己的板子,把template.uvprojx利用文本工具打开,修改里面的硬件参数,例如:我的板子的主芯片是STM32F103ZET6,而template.uvprojx中设置的是

$$Device:STM32F103RC$Device\Include\stm32f10x.h

直接把STM32F103RC用STM32F103ZE替换掉,另外包括时钟,RAM ROM设置这里也可以改:

IRAM(0x20000000,0xC000) IROM(0x08000000,0x40000) CPUTYPE("Cortex-M3") CLOCK(8000000) ELITTLE

根据自己需要做修改,就省得每次都要进keil重新配置了;keil4工程类似如此;

③IAR编译环境,没有尝试过编译STM32,就不多说了;

(3)构建环境完成,然后留给我们的main()函数里面啥都没有,但是恭喜你,你可以烧写到你的板子上验证一下,RT-Thread是可以跑起来的,把串口打开可以看到一堆打印在刷屏,就这么简单?中间到底发生了什么?

二、启动分析

接下来,根据之前的配置简要分析RT-Thread的启动过程了;

在startup_stm32f103xe.s有下面一段汇编:

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

由此处进入到C语言的main函数;但这里进入到的并不是那个什么都没的main()函数中,而是在rt-thread/src/components.c中,有这么一段代码:

#ifdef RT_USING_USER_MAIN

void rt_application_init(void);
void rt_hw_board_init(void);
int rtthread_startup(void);

#if defined(__CC_ARM) || defined(__CLANG_ARM)    //这个宏表示采用ARM-GCC编译器编译的
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
    rt_hw_interrupt_disable();
    rtthread_startup();
    return 0;
}
#elif defined(__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();
    rt_hw_interrupt_disable();
    rtthread_startup();
    return 0;
}
#elif defined(__GNUC__)        //这个宏表示采用arm-none-eabi-gcc编译器编译的
extern int main(void);
/* Add -eentry to arm-none-eabi-gcc argument */
int entry(void)
{
    rt_hw_interrupt_disable();
    rtthread_startup();
    return 0;
}
#endif

这里是预编译宏,根据不同编译器选择对应启动入参,这里不作详细介绍,有兴趣可以了解一下;所以我们用得到的代码就是:

extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
    rt_hw_interrupt_disable();
    rtthread_startup();
    return 0;
}

关键在于$Sub$$main和$Super$$main;查相关资料这里简单解释一下:

这是一种特殊模式:用于有一个已经存在且不能被改变的函数的情况,使用这两个模式可以帮原函数打补丁,如存在一个函数foo():

$Sub$$foo:定义的新功能函数,原先的foo()的入口变为$Sub$$foo(),在$Sub$$foo里面可以对foo()函数功能重写;

$Super$$foo:指的是原始的foo()函数,使用$Super$$foo时,用户调用的实际上就是foo();

因此,那段汇编执行完成后调到的main()是这个$Sub$$main;然后往下执行,注意函数rtthread_startup();里面才是主要的:

int rtthread_startup(void)
{
    rt_hw_interrupt_disable();	//关中断

    /* board level initialization
     * NOTE: please initialize heap inside board initialization.
     */
    rt_hw_board_init();		//板级初始化

    /* show RT-Thread version */
    rt_show_version();		//显示内核版本信息

    /* timer system initialization */
    rt_system_timer_init();	//系统时钟初始化

    /* scheduler system initialization */
    rt_system_scheduler_init();		//任务调度器初始化

#ifdef RT_USING_SIGNALS		//如果定义了信号
    /* signal system initialization */
    rt_system_signal_init();		//信号初始化
#endif

    /* create init_thread */
    rt_application_init();		//创建应用初始化

    /* timer thread initialization */
    rt_system_timer_thread_init();	//系统定时器线程初始化

    /* idle thread initialization */
    rt_thread_idle_init();		//空闲线程初始化

#ifdef RT_USING_SMP	//如果定义了多核处理器,这里肯定不支持
    rt_hw_spin_lock(&_cpus_lock);	
#endif /*RT_USING_SMP*/

    /* start scheduler */
    rt_system_scheduler_start();	//启动任务调度器,永不返回了

    /* never reach here */
    return 0;
}

都是一堆初始化的操作,在这里我们最起码能知道:启动任务调度器的时候,已经有两个线程可以跑起来了:(1)系统软定时器线程;(2)空闲线程;分别进入到其中看下就可以明白之前不断打印刷屏的是啥玩意了,所以即使外面的main()我们啥都没写,系统起来还是没问题的;

再关注一下rt_application_init()函数,同样是在这个文件里的,看下它的实现:

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);
}

这里很简单,就是创建一个叫”main”的线程,然后启动它,线程入口是main_thread_entry,继续看:

void main_thread_entry(void *parameter)
{
    extern int main(void);
    extern int $Super$$main(void);

    /* RT-Thread components initialization */
    rt_components_init();

#ifdef RT_USING_SMP
    rt_hw_secondary_cpu_up();
#endif
    /* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
    $Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
    main();
#endif
}

关键的东东来了,首先声明了main()和$Super$$main();再有一个初始化,然后去调用$Super$$main();,按照之前的解释(忘记了回过头再看下),这里的调用就是那个什么都没有的main()函数了,然后吭哧吭哧的就可以执行用户的业务,所以我们自己的应用程序就是完善那个什么都没有的main函数,在main里面创建一个线程跑下,看是不是这样,这里提供一个点灯的demo程序:https://download.csdn.net/download/kuangzuxiaon/11390602

 

三、总结

RT-Thread给我们的嵌入式环境搭建提供了极大便利,官方能想得到的几乎都给你干了,关键在于他们提供的Env工具,按照其它嵌入式系统的做法,没有有Env工具,必须从源码着手去撘环境,移植RTT的内核,这种做法我尝试过,总会有一些莫名的报错,估计还是系统架构不太熟悉,不过最终也是可以跑起来的,之前也出过一个例程可以参考:https://download.csdn.net/download/kuangzuxiaon/11008669,当然这种做法不会受限于Env工具,对于工程文件组织可以随心所欲,而不用去修改配置脚本,还是不能忘本;系统环境和启动流程搞清楚了,然后就是对接业务需求,驱动和应用开发;

你可能感兴趣的:(物联网,C语言,ARM,+,Linux)