资料下载
RT-Thread Simulator 例程
操作流程
- 将上面的仿真例程下载并解压,通过
MDK
打开,编译,调试,并打开串口点击运行,就可以看到如下输出了:
- 添加自己的
thread
:在main()
函数中添加即可,如下图:
启动流程
- 首先是通过
startup_stm32f103xe.s
启动文件调用SystemInit()
,系统初始化完成后,调用C库
函数__main()
,然后由__main()
调用用户的main()
函数。但是,由于ARMCC
编译器的特性,可以在调用main()
函数前插入一个$Sub$$main()
函数(其他编译器也有类似特性)。rt-thread
就是利用了这个特性,使所有的硬件、系统初始化都在$Sub$$main()
函数完成,而不需要用户在main()
中调用。
- 接下来看看
rtthread_startup()
函数:
- 我们进入到 rt_application_init() 函数去看一下:
- 以上就是系统的启动即初始化流程,以及实现自己的
thread
的操作流程。但是我们明明还有其他的thread
在运行啊,比如上面图一
中的tshell
和tidle
线程,这两个线程又是从哪里启动的呢?
- 首先,我们通过全局搜索tshell,看下这个这个thread是在哪里创建的:
#define FINSH_THREAD_NAME "tshell"
原来是在 shell.c
中的finsh_system_init()
函数中创建了tshell
线程,那理论上是不是就应该全局搜索在哪里调用了finsh_system_init()
函数呢?确实是,但搜索后发现,这个函数没有被调用的记录,这又是为啥呢?
- 重点就在
finsh_system_init()
函数后面的一句:
INIT_APP_EXPORT(finsh_system_init);
查看定义如下(C
语言中##
起连接作用):
那么INIT_APP_EXPORT(finsh_system_init);
这句翻译一下,就是:
INIT_EXPORT(finsh_system_init, "6");
再翻译一下,就是:
RT_USED const init_fn_t __rt_init_finsh_system_init SECTION(".rti_fn.6") = finsh_system_init;
这句就已经很清晰了吧,定义了一个变量__rt_init_finsh_system_init
,变量类型为const init_fn_t
(其中init_fn_t
为函数指针),这个变量的值就是finsh_system_init()
这个函数的起始地址,重点是:将这个函数起始地址放在了.rti_fn.6
的段中!
分析到这里,就很容易再分析出另外几个宏定义的作用了:
INIT_BOARD_EXPORT(fn) : 将板子初始化函数起始地址放到
.rti_fn.1
的段中
INIT_PREV_EXPORT(fn) :将预初始化函数起始地址放到.rti_fn.2
的段中
INIT_DEVICE_EXPORT(fn) :将设备初始化函数起始地址放到.rti_fn.3
的段中
INIT_COMPONENT_EXPORT(fn) :将组件初始化函数起始地址放到.rti_fn.4
的段中
INIT_ENV_EXPORT(fn) :将环境初始化函数起始地址放到.rti_fn.5
的段中
这里再提醒一下,当在同一个段中放入多个变量时,这些变量会按照时间顺序依次往后排。
- 现在我们知道,
tshell
线程创建函数没有被直接调用,而是放在了一个特色的段中,那么这个段中的函数什么时候被执行的呢?这个我也没找到什么好的定位方法,只能从初始化函数依次看下来,然后发现,有这么一条函数调用链:
$Sub$$main()
-->rtthread_startup()
-->rt_application_init()
-->main_thread_entry()
-->rt_components_init()
通过以上代码,我们看到通过INIT_EXPORT()宏又定义了几个函数并放入了相应的段:
rti_start()
-->.rti_fn.0
rti_board_start()
-->.rti_fn.0.end
rti_board_end()
-->.rti_fn.1.end
rti_end()
-->.rti_fn.6.end
根据上面所定义段的命名,猜测一下这些段的地址关系应该是:
.rti_fn.0
-->.rti_fn.0.end
-->.rti_fn.1
-->.rti_fn.1.end
-->.rti_fn.2
-->.rti_fn.3
-->.rti_fn.4
-->.rti_fn.5
-->.rti_fn.6
-->.rti_fn.6.end
打开map
文件看一下各个段的地址验证一下(由于我们例程中并没有使用到2-5
这几个段,所以map
中没有记录):
这里有个还有个疑问:各个段的地址是怎么确定的呢?由编译器指定还是由开发人员指定呢?
如果由编译器指定,那编译器又怎么知道哪个段需要放前面呢?如果由开发人员指定,又是在哪里指定的呢?
自问自答一下:我暂时没有找到什么资料,但是通过在代码中添加其他的段名进行各种尝试,发现段地址的由编译器决定的,而段的存放顺序是由段的名字按字母排序决定的!如下图:
接下来终于可以看看这些初始化函数是怎么调用的了:
/**
* @brief RT-Thread Components Initialization.
*/
void rt_components_init(void)
{
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
}
通过以上函数中的for
循环,直接将__rt_init_rti_board_end
(即.rti_fn.1.end)
到__rt_init_rti_end
(即.rti_fn.6.end
)之间的所有初始化函数执行了一遍。当然,也就包括了在.rti_fn.6
段中的finsh_system_init()
函数了。
- 同理,通过以下的函数调用流程,可以将
.rti_fn.1
中的初始化函数也执行一遍:
$Sub$$main()
-->rtthread_startup()
-->rt_hw_board_init()
-->rt_components_board_init()
/**
* @brief Onboard components initialization. In this function, the board-level
* initialization function will be called to complete the initialization
* of the on-board peripherals.
*/
void rt_components_board_init(void)
{
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
}
通过以上函数中的for
循环,直接将__rt_init_rti_board_start
(即.rti_fn.0.end)
到__rt_init_rti_board_end
(即.rti_fn.1.end
)之间的所有初始化函数执行了一遍。而且,在rtthread_startup()
函数中,rt_hw_board_init()
函数是比rt_application_init()
函数更早调用,这就保证了.rti_fn.1
段中的函数要早于其他段中函数的执行。
- rt-thread的启动流程这就讲完啦,大家现在应该知道为什么要把这些初始化函数放在这么多个段中了吧?
因为在同一个段的函数执行顺序是由编译器决定的,如果我们需要指定顺序,只能通过在不同段中实现。