系统先从启动文件开始运行,然后进入 RT-Thread 的启动 rtthread_startup() ,最后进入用户入口 main(),如下图所示:
系统启动后先从汇编代码 startup_stm32f429xe.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统启动,最后进入用户程序入口 main()。
①从系统初始化开始执行,将函数地址赋给R0寄存器,跳转到R0地址执行并返回此处(BLX是带链接的跳转,即带返回的跳转)。
②将main函数地址给R0,将函数地址赋给R0,跳转到R0地址执行,不返回(BX是跳转,不返回)。
③跳转到了$Sub$$main
关于 $Sub$ $ 和 $Super$ $ 扩展功能的使用,详见:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0377g/pge1362065967698.html
$Sub$$main 中主要是一些系统启动代码(系统初始化)。
④在rtthread_startup中,主要实现了板级初始化(初始化外设和驱动);打印RT-Thread的logo和版本信息;初始化系统定时器;初始化调度器;创建application线程(这里将用户main函数作为一个线程,用户main里面是空的);初始化软件定时器;创建空闲线程;启动系统调度(启用调度后,main函数就会参与调度开始运行)。
【所以说 $Sub$$main在main之前干的活就是进行rt-thread系统初始化,为了让用户更方便的使用,让用户不要操心的太多】
⑤以下是在rt_application_init()函数中创建的main函数线程:
⑥$Super$$mian 可以直接跳到main()函数; 用户可以在main中写一些应用代码:
可以这样使用给main函数打补丁:
int $Sub$$main(void)
{
//添加补丁函数
$Super$$main(); //使用本句直接转到main()运行
}
当然,main()函数也可以是自己的其他函数,操作都是一样的,换一下函数名就好了
首先查看RT-Thread内核架构这一章节,明白RT-Thread链表及线程的实现方法。
线程创建好之后,我们需要把线程添加到就绪列表里面, 表示线程已经就绪,系统随时可以调度。
就绪列表实际上就是一个 rt_list_t 类型的数组,数组的大小由决定最大线程优先级的宏 RT_THREAD_PRIORITY_MAX 决 定 ,RT_THREAD_PRIORITY_MAX 在 rtconfig.h 中默认定义为 32。 数组的下标对应了线程的优先级,同一优先级的线程统一插入到就绪列表的同一条链表中。
定义就绪列表:
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
初始化就绪列表:
for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
{
rt_list_init(&rt_thread_priority_table[offset]);
}
rt_thread_startup()----->rt_thread_resume()----->rt_schedule_insert_thread()----->rt_list_insert_before()
void rt_schedule_insert_thread(struct rt_thread *thread)
{
register rt_base_t temp;
RT_ASSERT(thread != RT_NULL);
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* it's current thread, it should be RUNNING thread */
if (thread == rt_current_thread)
{
thread->stat = RT_THREAD_RUNNING | (thread->stat & ~RT_THREAD_STAT_MASK);
goto __exit;
}
/* READY thread, insert to ready queue */
thread->stat = RT_THREAD_READY | (thread->stat & ~RT_THREAD_STAT_MASK);
/* insert thread to ready list */
rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
&(thread->tlist));
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("insert thread[%.*s], the priority: %d\n",
RT_NAME_MAX, thread->name, thread->current_priority));
/* set priority mask */
#if RT_THREAD_PRIORITY_MAX > 32
rt_thread_ready_table[thread->number] |= thread->high_mask;
#endif
rt_thread_ready_priority_group |= thread->number_mask;
__exit:
/* enable interrupt */
rt_hw_interrupt_enable(temp);
}
调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。
rt_system_scheduler_init();
void rt_system_scheduler_start(void)
{
register struct rt_thread *to_thread;
rt_ubase_t highest_ready_priority;
to_thread = _get_highest_priority_thread(&highest_ready_priority);
#ifdef RT_USING_SMP
to_thread->oncpu = rt_hw_cpu_id();
#else
rt_current_thread = to_thread;
#endif /*RT_USING_SMP*/
rt_schedule_remove_thread(to_thread);
to_thread->stat = RT_THREAD_RUNNING;
/* switch to new thread */
#ifdef RT_USING_SMP
rt_hw_context_switch_to((rt_ubase_t)&to_thread->sp, to_thread);
#else
rt_hw_context_switch_to((rt_ubase_t)&to_thread->sp);
#endif /*RT_USING_SMP*/
/* never come back */
}
rt_hw_context_switch_to PROC
EXPORT rt_hw_context_switch_to
; r0 的值是一个指针,该指针指向 to 线程的线程控制块的 SP 成员
; 将 r0 寄存器的值保存到 rt_interrupt_to_thread 变量里
LDR r1, =rt_interrupt_to_thread
STR r0, [r1]
; 设置 from 线程为空,表示不需要从保存 from 的上下文
LDR r1, =rt_interrupt_from_thread
MOV r0, #0x0
STR r0, [r1]
; 设置标志为 1,表示需要切换,这个变量将在 PendSV 异常处理函数里切换的时被清零
LDR r1, =rt_thread_switch_interrupt_flag
MOV r0, #1
STR r0, [r1]
; 设置 PendSV 异常优先级为最低优先级
LDR r0, =NVIC_SYSPRI2
LDR r1, =NVIC_PENDSV_PRI
LDR.W r2, [r0,#0x00] ; read
ORR r1,r1,r2 ; modify
STR r1, [r0] ; write-back
; 触发 PendSV 异常 (将执行 PendSV 异常处理程序)
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
; 放弃芯片启动到第一次上下文切换之前的栈内容,将 MSP 设置启动时的值
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
MSR msp, r0
; 使能全局中断和全局异常,使能之后将进入 PendSV 异常处理函数
CPSIE F
CPSIE I
; 不会执行到这里
ENDP
rt_hw_context_switch_to() 只有目标线程,没有来源线程。这个函数里实现切换到指定线程的功能。
rt_hw_context_switch_to() ;
在 Cortex-M 处理器架构里,基于自动部分压栈和 PendSV 的特性,上下文切换可以实现地更加简洁。
线程之间的上下文切换,如下图表示:
中断到线程的上下文切换可以用下图表示:
系统调度就是在就绪列表中寻找优先级最高的就绪线程,然后去执行该线程。
rt_schedule();
使用rt_hw_context_switch/rt_hw_context_switch_interrupt() 进行线程的切换,函数 rt_hw_context_switch() 和函数 rt_hw_context_switch_interrupt() 都有两个参数,分别是 from 线程和 to 线程。它们实现从 from 线程切换到 to 线程的功能。下图是具体的流程图:
rt_hw_context_switch_interrupt
EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch
; 检查 rt_thread_switch_interrupt_flag 变量是否为 1
; 如果变量为 1 就跳过更新 from 线程的内容
LDR r2, =rt_thread_switch_interrupt_flag
LDR r3, [r2]
CMP r3, #1
BEQ _reswitch
; 设置 rt_thread_switch_interrupt_flag 变量为 1
MOV r3, #1
STR r3, [r2]
; 从参数 r0 里更新 rt_interrupt_from_thread 变量
LDR r2, =rt_interrupt_from_thread
STR r0, [r2]
_reswitch
; 从参数 r1 里更新 rt_interrupt_to_thread 变量
LDR r2, =rt_interrupt_to_thread
STR r1, [r2]
; 触发 PendSV 异常,将进入 PendSV 异常处理函数里完成上下文切换
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
PendSV 中断处理函数是 PendSV_Handler()。在 PendSV_Handler() 里完成线程切换的实际工作,下图是具体的流程图:
PendSV_Handler PROC
EXPORT PendSV_Handler
; 关闭全局中断
MRS r2, PRIMASK
CPSID I
; 检查 rt_thread_switch_interrupt_flag 变量是否为 0
; 如果为零就跳转到 pendsv_exit
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
; 清零 rt_thread_switch_interrupt_flag 变量
MOV r1, #0x00
STR r1, [r0]
; 检查 rt_thread_switch_interrupt_flag 变量
; 如果为 0,就不进行 from 线程的上下文保存
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread
; 保存 from 线程的上下文
MRS r1, psp ; 获取 from 线程的栈指针
STMFD r1!, {r4 - r11} ; 将 r4~r11 保存到线程的栈里
LDR r0, [r0]
STR r1, [r0] ; 更新线程的控制块的 SP 指针
switch_to_thread
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] ; 获取 to 线程的栈指针
LDMFD r1!, {r4 - r11} ; 从 to 线程的栈里恢复 to 线程的寄存器值
MSR psp, r1 ; 更新 r1 的值到 psp
pendsv_exit
; 恢复全局中断状态
MSR PRIMASK, r2
; 修改 lr 寄存器的 bit2,确保进程使用 PSP 堆栈指针
ORR lr, lr, #0x04
; 退出中断函数
BX lr
ENDP