本文主要介绍RT Thread操作系统在cortex-m3内核上的移植接口文件,通过本篇博客你将深入了解RTOS操作系统是怎么通过触发软中断实现任务切换的,怎么实现内核异常信息的打印功能。
RT-Thread操作系统的移植接口文件主要用cpuport.c,context_rvds.s,backtrace.c,div0.c,showmem.c。其中最重要的文件是cpuport.c和context_rvds.s这两个文件,其他三个文件在cortex-M3内核移植时没有实际的应用,这三个文件实际一些辅助的功能,打印内存,除数为0,后台跟踪等操作,内容很简单,可以自行查看。
这是一个汇编语言的文件,这个文件实现了任务切换,触发软件中断,硬件异常错误处理等操作,是操作系统移植时要实现的最重要的功能。程序的内部逻辑根cortex-m3内核的编程模型有关,想了解此程序逻辑,需要对cortex-m3内核的编程模块有一定的了解。
操作系统进行初始化芯片的时钟,必要的外设后,开始进行第一个任务/线程调度时,会调用rt_hw_context_switch_to,函数的输入参数是进行切换的任务(线程)的堆栈指针。这个函数的具体功能如下:
请详细查看我增加的中文注释。
请详细查看我增加的中文注释,有你不会的干货。
请详细查看我增加的中文注释。
;/*
; * void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * this fucntion is used to perform the first thread switch
; */
rt_hw_context_switch_to PROC
EXPORT rt_hw_context_switch_to
; set to thread
; 把要切换到的线程的堆栈指针记录到变量rt_interrupt_to_thread中来
LDR r1, =rt_interrupt_to_thread
STR r0, [r1]
; set from thread to 0
; 第一次进行线程切换,没有上一次切换所以把rt_interrupt_from_thread设置为0
LDR r1, =rt_interrupt_from_thread
MOV r0, #0x0
STR r0, [r1]
; set interrupt flag to 1
; 进行线程切换,把线程切换标志变量rt_thread_switch_interrupt_flag设置为1
LDR r1, =rt_thread_switch_interrupt_flag
MOV r0, #1
STR r0, [r1]
; set the PendSV exception priority
; 设置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
; trigger the PendSV exception (causes context switch)
;触发pendsv软件中断,此时中断关闭,并不会产生中断
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
; restore MSP
;这段代码实际是可以没有的,移植时这样做有了一个好处就是增加了MSP堆栈的使用空间
;cortex-m3内核复位时使用msp堆栈,从复位到进行初始化操作时会调用很多函数,会进行一些压栈操作
;占用一部分msp堆栈,由于程序不会退出到复位的位置,压栈占用的msp空间永远不会释放,产生了堆栈的
;的空间浪费一小部分。
; 下面的代码实现的功能是读取SCB_VTOR寄存器,这个寄存器保存了中断向量表的起始位置,此位置的字
; 就是MSP堆栈的指针,即启动代码里面分配出来的堆栈的栈顶。经过2次 LDR r0, [r0]就是相当于取到
;堆栈的栈顶,最后设置msp为栈顶
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
MSR msp, r0
; enable interrupts at processor level
; 打开中断
CPSIE F
CPSIE I
; never reach here!
ENDP
在已经进行了一次线程切换后,再次进行线程切换时会调用void rt_hw_context_switch(rt_uint32 from, rt_uint32 to)这个函数,这个函数与上面rt_hw_context_switch_to函数的功能相比大同小异。
;/*
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; */
rt_hw_context_switch_interrupt
EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch
; set rt_thread_switch_interrupt_flag to 1
; 判断线程切换标志rt_thread_switch_interrupt_flag是否为1
LDR r2, =rt_thread_switch_interrupt_flag
LDR r3, [r2]
CMP r3, #1
BEQ _reswitch
;不为1时,设置为1,把from的线程的堆栈指针记录到rt_interrupt_from_thread中
MOV r3, #1
STR r3, [r2]
LDR r2, =rt_interrupt_from_thread ; set rt_interrupt_from_thread
STR r0, [r2]
_reswitch
;为1时,把to的线程的堆栈指针记录到rt_interrupt_to_thread中来
LDR r2, =rt_interrupt_to_thread ; set rt_interrupt_to_thread
STR r1, [r2]
;触发pendsv中断
LDR r0, =NVIC_INT_CTRL ; trigger the PendSV exception (causes context switch)
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
ENDP
pendsv中断是真正进行了线程切换操作的,前面介绍的2个函数主要在进行线程切换前,把要切换的线程的堆栈指针记录到这个汇编的程序的变量中,在pendsv中断中进行线程切换时使用,并且触发中断。下面介绍pendsv中断内部实现线程切换的原理,一定要仔细看呀。
; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
PendSV_Handler PROC
EXPORT PendSV_Handler
;根据cortext-m3内核的编程模型,进行pendsv中断前,内核已经自动的psr, pc, lr, r12, r3, r2, r1, r0把这些寄存器压入到发生切换的线程的堆栈psp中的去了,跳到中断程序,使用的堆栈自动切换成msp
; disable interrupt to protect context switch
; 记录primask中断开关寄存器的值到r2寄存器中,用于退出中断后再打开中断用
MRS r2, PRIMASK
;关闭中断
CPSID I
; get rt_thread_switch_interrupt_flag
; 判断rt_thread_switch_interrupt_flag标志为1时,才进行线程切换,为0时直接退出中断
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
; clear rt_thread_switch_interrupt_flag to 0
; 进行线程切换,清除rt_thread_switch_interrupt_flag变量
MOV r1, #0x00
STR r1, [r0]
;判断从哪个线程切换出去,要切换到第一个线程时,rt_interrupt_from_thread变量为0
;判断此变量为0表示切入第一个线程
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread ; skip register save at the first time
;不为0时,把r4-r11这8个寄存器保存到当前要切换出去的线程堆栈psp中去,并且把当前线程的堆栈指针psp记录到rt_interrupt_from_thread变量中来
MRS r1, psp ; get from thread stack pointer
STMFD r1!, {r4 - r11} ; push r4 - r11 register
LDR r0, [r0]
STR r1, [r0] ; update from thread stack pointer
switch_to_thread
;把要切入的线程的堆栈指针取出到r1寄存器中
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] ; load thread stack pointer
;从要切入线程堆栈中弹出这个线程中的寄存器r4-r11,把线程堆栈指针赋值到psp中。
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
MSR psp, r1 ; update stack pointer
pendsv_exit
; restore interrupt
; 打开中断
MSR PRIMASK, r2
;cortex-m3内核中发生中断时,在中断程序中使用的堆栈是msp,操作系统线程设计使用的是psp线程,所以上面的线程切换就是实现是两个线程的堆栈指针的切换,即把当前线程的堆栈psp保存到rt_interrupt_from_thread
为量中,把要切入的堆栈赋值到psp中去。
;由于中断中使用的msp堆栈,退出中断是如果不做任何操作还是使用msp堆栈,而线程使用的是psp堆栈,所以对lr寄存的位3进行置1就控制退出中断后使用psp中断。
ORR lr, lr, #0x04
BX lr
ENDP
还有两个函数rt_hw_interrupt_enable和rt_hw_interrupt_disable是实现开中断和关中断,功能很简单,不再详细描述。
harfault_Handler中断中当发生了硬件错误中断时,比如非法内存访问,外设初始化,操作非法会发生。这个中断函数中实现了出打印出发生中断异常点的函数的指针。
HardFault_Handler PROC
; get current context
;中断程序中lr表示的是EXC_RETURN寄存器的状态,这个寄存器的位2表示进入中断前使用的是psp还是
;msp堆栈,在rt-thread中,如果硬件错误中断发生在线程中使用的是psp,如果是从另外一个中断发生
;硬件故障产生的中断,使用的是msp
TST lr, #0x04 ; if(!EXC_RETURN[2])
ITE EQ
;把发生中断前的堆栈指针赋值到r0寄存器中去
MRSEQ r0, msp ; [2]=0 ==> Z=1, get fault context from handler.
MRSNE r0, psp ; [2]=1 ==> Z=0, get fault context from thread.
;手动把r4-r11压入堆栈中,再压入lr寄存器,记住这里多压入了9个寄存器的值
; 这里这样操作的原因是为了rt_hw_hard_fault_exception函数中定义的结构体能对齐访问到全部寄存器
STMFD r0!, {r4 - r11} ; push r4 - r11 register
STMFD r0!, {lr} ; push exec_return register
TST lr, #0x04 ; if(!EXC_RETURN[2])
ITE EQ
;上面压完堆栈后,把更新后的堆栈指针重新写入到psp或msp中去,r0寄存器保存的是发生hardfault中
; 断前使用的堆栈指针,做为参数会传入函数rt_hw_hard_fault_exception中去
MSREQ msp, r0 ; [2]=0 ==> Z=1, update stack pointer to MSP.
MSRNE psp, r0 ; [2]=1 ==> Z=0, update stack pointer to PSP.
PUSH {lr}
BL rt_hw_hard_fault_exception
POP {lr}
ORR lr, lr, #0x04
BX lr
ENDP
ALIGN 4
END
这个程序主要有2个函数void rt_hw_hard_fault_exception(struct exception_info * exception_info),rt_uint8_t *rt_hw_stack_init(void *tentry,void *parameter, rt_uint8_t *stack_addr,void *texit)比较重要,另外几个函数的功能都很简单不做详细介绍。
rt_hw_hard_fault_exception函数中实现打印发生错误中断前的程序的位置的上下位,即发生中断时程序的出现故障的位置。还记得上面的程序段中如下的这些操作,这些操作是向堆中多压入了9个寄存器,进入此函数中使用结构体来struct exception_info来进行访问使用的。
;手动把r4-r11压入堆栈中,再压入lr寄存器,记住这里多压入了9个寄存器的值
; 这里这样操作的原因是为了rt_hw_hard_fault_exception函数中定义的结构体能对齐访问到全部寄存器
STMFD r0!, {r4 - r11} ; push r4 - r11 register
STMFD r0!, {lr} ; push exec_return register
结构体struct exception_info的定义如下,
struct exception_stack_frame
{
rt_uint32_t r0;
rt_uint32_t r1;
rt_uint32_t r2;
rt_uint32_t r3;
rt_uint32_t r12;
rt_uint32_t lr;
rt_uint32_t pc;
rt_uint32_t psr;
};
struct stack_frame
{
/* r4 ~ r11 register */
rt_uint32_t r4;
rt_uint32_t r5;
rt_uint32_t r6;
rt_uint32_t r7;
rt_uint32_t r8;
rt_uint32_t r9;
rt_uint32_t r10;
rt_uint32_t r11;
struct exception_stack_frame exception_stack_frame;
};
struct exception_info
{
rt_uint32_t exc_return;
struct stack_frame stack_frame;
};
从结构体的定义可以看出r0成员变量前面还有exc_return,r4-r11这9个成员变量,所以手动向堆栈中压入9个寄存器,使用这个结构体来访问发生中断前的程序位置的pc,通过pc值就能找到哪段程序发生了错误中断。
void rt_hw_hard_fault_exception(struct exception_info * exception_info)
{
extern long list_thread(void);
struct stack_frame* context = &exception_info->stack_frame;
if (rt_exception_hook != RT_NULL)
{
rt_err_t result;
result = rt_exception_hook(exception_info);
if (result == RT_EOK)
return;
}
rt_kprintf("psr: 0x%08x\n", context->exception_stack_frame.psr);
rt_kprintf("r00: 0x%08x\n", context->exception_stack_frame.r0);
rt_kprintf("r01: 0x%08x\n", context->exception_stack_frame.r1);
rt_kprintf("r02: 0x%08x\n", context->exception_stack_frame.r2);
rt_kprintf("r03: 0x%08x\n", context->exception_stack_frame.r3);
rt_kprintf("r04: 0x%08x\n", context->r4);
rt_kprintf("r05: 0x%08x\n", context->r5);
rt_kprintf("r06: 0x%08x\n", context->r6);
rt_kprintf("r07: 0x%08x\n", context->r7);
rt_kprintf("r08: 0x%08x\n", context->r8);
rt_kprintf("r09: 0x%08x\n", context->r9);
rt_kprintf("r10: 0x%08x\n", context->r10);
rt_kprintf("r11: 0x%08x\n", context->r11);
rt_kprintf("r12: 0x%08x\n", context->exception_stack_frame.r12);
rt_kprintf(" lr: 0x%08x\n", context->exception_stack_frame.lr);
rt_kprintf(" pc: 0x%08x\n", context->exception_stack_frame.pc);
if(exception_info->exc_return & (1 << 2) )
{
rt_kprintf("hard fault on thread: %s\r\n\r\n", rt_thread_self()->name);
#ifdef RT_USING_FINSH
list_thread();
#endif /* RT_USING_FINSH */
}
else
{
rt_kprintf("hard fault on handler\r\n\r\n");
}
#ifdef RT_USING_FINSH
hard_fault_track();
#endif /* RT_USING_FINSH */
while (1);
}
rt_hw_stack_init函数在创建线程时,对分配的线程的堆栈进行初始化,一个线程中使用全部的cortex-m3的16个寄存器,所以这个函数在线程的堆栈的栈顶位置向下的16个字进行初始化,按照内核进入中断时压入堆栈的寄存器顺序排列进行初始化,特别说明一下lr是返回地址,即线程退出后返回到rt_thread_exit函数中,pc是线程的入口函数地址。
/**
* This function will initialize thread stack
*
* @param tentry the entry of thread
* @param parameter the parameter of entry
* @param stack_addr the beginning stack address
* @param texit the function will be called when thread exit
*
* @return stack address
*/
rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr,
void *texit)
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
stk = stack_addr + sizeof(rt_uint32_t);
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
stk -= sizeof(struct stack_frame);
stack_frame = (struct stack_frame *)stk;
/* init all register */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
}
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
stack_frame->exception_stack_frame.r1 = 0; /* r1 */
stack_frame->exception_stack_frame.r2 = 0; /* r2 */
stack_frame->exception_stack_frame.r3 = 0; /* r3 */
stack_frame->exception_stack_frame.r12 = 0; /* r12 */
stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
/* return task's current stack address */
return stk;
}
至此已经完成了全部cortext-m3内核移植部分的关键代码的讲解,如有不懂的地方可以在下面留言联楼主