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