接着面的文章内容: RT-Thread 系统分析(3)-线程管理之线程切换(系统移植基础篇一).的内容,接下来我们继续分析剩余的函数。
函数和变量描述rt_base_t rt_hw_interrupt_disable(void);关闭全局中断void rt_hw_interrupt_enable(rt_base_t level);打开全局中断rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit);线程栈的初始化,内核在线程创建和线程初始化里面会调用这个函数void rt_hw_context_switch_to(rt_uint32 to);没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);从 from 线程切换到 to 线程,用于线程和线程之间的切换void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to);从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用rt_uint32_t rt_thread_switch_interrupt_flag;表示需要在中断里进行切换的标志rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread;在线程进行上下文切换时候,用来保存 from 和 to 线程PendSV_Handler函数(在下一篇内容中)在 Cortex-M3 里,PendSV 中断处理函数是 PendSV_Handler()。在 PendSV_Handler() 里完成线程切换的实际工作没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用。
/* * void rt_hw_context_switch_to(rt_uint32 to); * R0 --> to */ .global rt_hw_context_switch_to /*声明一个全局可调用变量*/ .type rt_hw_context_switch_to, %function/*将 rt_hw_context_switch_to 设置为函数类型*/ rt_hw_context_switch_to: LDR R1, =rt_interrupt_to_thread /*r1 的值是一个指针,该指针指向 rt_interrupt_to_thread 的 SP 成员*/ STR R0, [R1] /*将 r0 寄存器的值保存到 rt_interrupt_to_thread 变量里*/ /* set from thread to 0 设置 from 线程为空,表示不需要从保存 from 的上下文*/ LDR R1, =rt_interrupt_from_thread/*r1 的值是一个指针,该指针指rt_interrupt_from_thread 的 SP 成员*/ MOV R0, #0 /*将 立即数0 赋值给r0寄存器*/ STR R0, [R1] /*将 r0 寄存器的值保存到 rt_interrupt_from_thread 变量里*/ /* set interrupt flag to 1 设置标志为 1,表示需要切换,这个变量将在 PendSV 异常处理函数里切换的时被清零*/ LDR R1, =rt_thread_switch_interrupt_flag MOV R0, #1 /*将 立即数1 赋值给r0寄存器*/ STR R0, [R1] /*将 r0 寄存器的值保存到 rt_thread_switch_interrupt_flag 变量里*/ /* set the PendSV exception priority */ LDR R0, =SHPR3 /*加载SHPR3(系统优先寄存器)地址到R0*/ LDR R1, =PENDSV_PRI_LOWEST /*从PENDSV_PRI_LOWEST(PendSV优先级值(最低))地址处加载值到R1*/ LDR.W R2, [R0,#0] /* read 将R0寄存器处的地址偏移0个字节处的值加载到R2寄存器*/ ORR R1, R1, R2 /* modify R1值=R1值|R2值*/ STR R1, [R0] /* write-back 将R0的值写入SHPR3寄存器中 */ /*触发PendSV异常(导致上下文切换) trigger the PendSV exception (causes context switch) */ LDR R0, =ICSR /*将ICSR寄存器地址载入R0寄存器*/ LDR R1, =PENDSVSET_BIT /*PENDSVSET_BIT值为0x10000000 用于触发PendSV异常的值*/ STR R1, [R0] /*将PENDSVSET_BIT值写入到R0指向的ICSR寄存器处*/ /* restore MSP 还原MSP */ LDR r0, =SCB_VTOR /* 加载 SCB_VTOR(向量表偏移寄存器 )地址到r0*/ LDR r0, [r0] /*获取向量表的首地址值*/ LDR r0, [r0] /*获取MSP初始值到r0*/ NOP MSR msp, r0 /*写入 R0 的值到主堆栈中*/ /* enable interrupts at processor level 在处理器级别启用中断*/ CPSIE F /*开 异常*/ CPSIE I /*开 中断*/ /* never reach here! */说明:伪指令的使用请看下面知识点讲解,其中设置PendSV异常优先级,触发PendSV中断是一种固定的用法,即将固定的值写入ICSR寄存器即可触发PendSV异常,此时可以产生异常中断,CPU会自动进行压栈,此时可以进行上下文切换。
这一段的代码比较绕,蕴含的知识点比较复杂琐碎,下面开始专门讲解分析。
在离开复位状态后, CM3 做的第一件事就是读取下列两个 32 位整数的值: z 从地址 0x0000,0000 处取出 MSP 的初始值。 z 从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量, LSB 必须是 1。然后从这个值所对应的地址处取指。 图 3.17 复位序列 请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。在 CM3 中, 0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。 向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行 的第一条指令。 因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。 举例来说,如果你的堆栈区域在 0x20007C00‐0x20007FFF 之间,那么 MSP 的初始值就必须是0x20008000。向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。要注意因为 CM3 是在 Thumb态下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为这个原因, 图 中使用 0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式开始了程序的执行。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。 MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
SCB_VTOR 的值在context_gcc.S文件内容的开头有定义:
.equ SCB_VTOR, 0xE000ED08 /* Vector Table Offset Register 向量表偏移寄存器*/这是向量表偏移寄存器的地址,因此LDR r0, =SCB_VTOR 这段代码表示:加载SCB_VTOR 寄存器的地址到r0寄存器
这段表示:加载指向SCB_VTOR 寄存器的地址处的值到r0寄存器,这个值是多少呢?是中断向量偏移地址的值。接下来我们想中断向量偏移地址的值是多少呢?请看:
这是定义了一个对象,名字是:g_pfnVectors ,而这个对象的第一个元素是_estack,这个_estack的值就是MSP (主堆栈指针)的初始值。原因是:
因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。举例来说,如果你的堆栈区域在 0x20007C00‐0x20007FFF 之间,那么 MSP 的初始值就必须是 0x20008000。
这段代码表示:代码段(text段)4字节对齐后所加载的第一个对象是.isr_vector,而.isr_vector的内容是栈顶地址值和中断向量表。
从这里map映射文件可以验证前面的说法, .isr_vector是代码段第一个加载的对象,起始地址是0x08000000,而0x08000000地址指向的值第一个值就是MSP主堆栈指针值,就是_estack代表的值,也是MSP主堆栈指针初始值。
可以看到map文件_estack的值为:0x20000998,那么是不是确实是栈顶地址值呢?从下面的程序编译出的**.bin文件**的内容可以看到,bin文件的开始前4个字节就是:0x20000998
因此第一次的 LDR r0, [r0] 表示:加载中断向量表偏移地址的值 第二次 LDR r0, [r0] 表示: 加载 中断向量表偏移地址的值指向第一个条目的值,即栈顶地址值,也是MSP主堆栈指针初始值。
从上面的分析和描述中我们可以了解以下结论:
CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。这也是上面反复强调的关键点。 向量表跟随在 MSP 的初始值之后从加载的顺序可以看出CPU必须要先加载MSP的值才能够正常的执行Reset_Handler函数。
Both LDR 和 ADR 都有能力产生一个地址,但是语法和行为不同。对于 LDR,如果汇编器发现要产生立即数是一个程序地址,它会自动地把 LSB 置位,例如:
LDR r0, =address1 ; R0= 0x4000 | 1 … address1 0x4000: MOV R0, R1在这个例子中,汇编器会认出 address1 是一个程序地址,所以自动置位 LSB。另一方面,如果汇编器发现要加载的是数据地址,则不会自作聪明,多机灵啊!看:
LDR R0, =address1 ; R0= 0x4000 … address1 0x4000: DCD 0x0 ;0x4000 处记录的是一个数据ADR 指令则是“厚道人”,它决不会修改 LSB。例如:
ADR r0, address1 ; R0= 0x4000。注意: 没有“=”号 … address1 0x4000: MOV R0, R1ADR 将如实地加载 0x4000。注意,语法略有不同,没有“=”号。 前面已经提到, LDR 通常是把要加载的数值预先定义,再使用一条 PC 相对加载指令来取出。而 ADR 则尝试对 PC 作算术加法或减法来取得立即数。因此 ADR 未必总能求出需要的立即数。其实顾名思义, ADR 是为了取出附近某条指令或者变量的地址,而 LDR 则是取出一个通用的 32 位整数。因为 ADR 更专一,所以得到了优化,故而它的代码效率常常比 LDR的要高。
用于访问存储器的基础指令是“加载(Load)”和“存储(Store)”。加载指令 LDR 把存储器中的内容加载到寄存器中,存储指令 STR 则把寄存器的内容存储至存储器中,传送过程中数据类型也可以变通,最常使用的格式有: 表 4.14 常用的存储器访问指令
示例功能描述LDRB Rd, [Rn, #offset]从地址 Rn+offset 处读取一个字节到 RdLDRH Rd, [Rn, #offset]从地址 Rn+offset 处读取一个半字到 RdLDR Rd, [Rn, #offset]从地址 Rn+offset 处读取一个字到 RdLDRD Rd1, Rd2, [Rn, #offset]从地址 Rn+offset 处读取一个双字(64 位整数)到 Rd1(低32 位)和 Rd2(高 32 位)中。STRB Rd, [Rn, #offset]把 Rd 中的低字节存储到地址 Rn+offset 处STRH Rd, [Rn, #offset]把 Rd 中的低半字存储到地址 Rn+offset 处STR Rd, [Rn, #offset]把 Rd 中的低字存储到地址 Rn+offset 处LDRD Rd1, Rd2, [Rn, #offset]把 Rd1(低 32 位)和 Rd2(高 32 位)表达的双字存储函数 rt_hw_context_switch() 和函数 rt_hw_context_switch_interrupt() 都有两个参数,分别是 from 线程和 to 线程。它们实现从 from 线程切换到 to 线程的功能。下图是具体的流程图: 函数源代码:
/* * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); * R0 --> from * R1 --> to */ .global rt_hw_context_switch_interrupt /*定义全局可调用变量 rt_hw_context_switch_interrupt*/ .type rt_hw_context_switch_interrupt, %function /*将 rt_hw_context_switch_interrupt 设置为函数类型*/ .global rt_hw_context_switch /*定义全局可调用变量 rt_hw_context_switch*/ .type rt_hw_context_switch, %function /*将 rt_hw_context_switch 设置为函数类型*/ rt_hw_context_switch_interrupt: rt_hw_context_switch: /* 检查 rt_thread_switch_interrupt_flag 变量是否为 1 set rt_thread_switch_interrupt_flag to 1 */ LDR R2, =rt_thread_switch_interrupt_flag LDR R3, [R2] /*将 rt_thread_switch_interrupt_flag地址处的值加载到R3中*/ CMP R3, #1 /*如果变量为 1 就跳过更新 from 线程的内容*/ BEQ _reswitch /*当R3寄存器值为1时执行_reswitch*/ MOV R3, #1 /*R3值不为1执行,此时将R3寄存器值设置为1*/ STR R3, [R2] /*设置 rt_thread_switch_interrupt_flag 变量为 1*/ LDR R2, =rt_interrupt_from_thread /* set rt_interrupt_from_thread */ STR R0, [R2] /*将R0的值赋值给 rt_interrupt_from_thread*/ _reswitch: /*R3值为1 执行*/ LDR R2, =rt_interrupt_to_thread /*从参数 r1 里更新 rt_interrupt_to_thread 变量 set rt_interrupt_to_thread */ STR R1, [R2] /*将r1的值写入到rt_interrupt_to_thread对应的地址 */ LDR R0, =ICSR /*触发 PendSV 异常,将进入 PendSV 异常处理函数里完成上下文切换 trigger the PendSV exception (causes context switch) */ LDR R1, =PENDSVSET_BIT STR R1, [R0] BX LR说明:
函数的输入参数:观察当前代码可以看出,在代码使用时,开始并没使用R0和R1寄存器,这是因为,rt_hw_context_switch函数是有输入参数的,ARM子函数定义中的参数放入寄存器的规则是遵循的是 ATPCS(ARM-Thumb Procedure Call Standard)。下面知识点简单介绍一下函数参数的存放规则。因此输入参数void rt_hw_context_switch(rt_uint32 from, rt_uint32 to)的对应关系是 R0 --> from 而R1 --> to。基于自动部分压栈和 PendSV 的特性,上下文切换可以实现地更加简洁,因此上面的切换线程的代码也是很简洁。补充: ATPCS建议函数的形参不超过4个,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递
子程序结果返回规则 1.结果为一个32位的整数时,可以通过寄存器R0返回. 2.结果为一个64位整数时,可以通过R0和R1返回,依此类推. 3.结果为一个浮点数时,可以通过浮点运算部件的寄存器f0,d0或者s0来返回. 4.结果为一个复合的浮点数时,可以通过寄存器f0-fN或者d0~dN来返回. 5.对于位数更多的结果,需要通过调用内存来传递. 举例: 输入参数通过r0-r3传递,多余的放入堆栈中;返回值放入r0,不够的话放入{r0,r1}或者{r0,r1,r2,r3},比如: int foo(int a, int b, int c, int d), 输入:r0 = a, r1 = b, r2 = c, r3 = d,返回:r0 = 类型为int的retvalue int *foo(char a, double b, int c, char d), 输入:r0 = a, r1用于对齐(double 要求8字节对齐), b = {r2, r3},c放在堆栈的sp[0]位置,d放在堆栈的sp[4]位置,这里的sp是指进入函数时的sp;返回:r0 = 类型为int *的retvalue
其实一开始就看这一段代码,我们可能是蒙的,因为为什么是这样的流程,这样的流程设计原因是什么? 原因就是:rt_hw_context_switch函数和rt_hw_context_switch_interrupt函数实现上下文切换的方式是很类似的,两个函数输入的参数(rt_uint32 from, rt_uint32 to)也相同,因此这两个函数写在了一起。
在不同的 CPU 架构里,线程之间的上下文切换和中断到线程的上下文切换,上下文的寄存器部分可能是有差异的,也可能是一样的。在 Cortex-M 里面上下文切换都是统一使用 PendSV 异常来完成,切换部分并没有差异。但是为了能适应不同的 CPU 架构,RT-Thread 的 libcpu 抽象层还是需要实现三个线程切换相关的函数: 1) rt_hw_context_switch_to():没有来源线程,切换到目标线程,在调度器启动第一个线程的时候被调用。 2) rt_hw_context_switch():在线程环境下,从当前线程切换到目标线程。 3) rt_hw_context_switch_interrupt ():在中断环境下,从当前线程切换到目标线程。 在线程环境下进行切换和在中断环境进行切换是存在差异的。线程环境下,如果调用 rt_hw_context_switch() 函数,那么可以马上进行上下文切换;而在中断环境下,需要等待中断处理函数完成之后才能进行切换。
由于这种差异,在 ARM9 等平台,rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 的实现并不一样。在中断处理程序里如果触发了线程的调度,调度函数里会调rt_hw_context_switch_interrupt() 触发上下文切换。中断处理程序里处理完中断事务之后,中断退出之前,检查 rt_thread_switch_interrupt_flag 变量,如果该变量的值为 1,就根据 rt_interrupt_from_thread 变量和 rt_interrupt_to_thread 变量,完成线程的上下文切换。
在 Cortex-M 处理器架构里,基于自动部分压栈和 PendSV 的特性,上下文切换可以实现地更加简洁。
硬件在进入 PendSV 中断之前自动保存了 from 线程的 PSR、PC、LR、R12、R3-R0 寄存器,然后 PendSV 里保存 from 线程的 R11-R4 寄存器,以及恢复 to 线程的 R4-R11 寄存器,最后硬件在退出 PendSV 中断之后,自动恢复 to 线程的 R0~R3、R12、LR、PC、PSR 寄存器。
硬件在进入中断之前自动保存了 from 线程的 PSR、PC、LR、R12、R3-R0 寄存器,然后触发了 PendSV 异常。在 PendSV 异常处理函数里保存 from 线程的 R11~R4 寄存器,以及恢复 to 线程的 R4~R11 寄存器,最后硬件在退出 PendSV 中断之后,自动恢复 to 线程的 R0~R3、R12、PSR、PC、LR 寄存器。
上面两篇文章已经把线程切换一些基本的函数分析完了,基本上了解了线程栈初始化和线程切换的内容,其中ARM子函数定义中的参数放入寄存器的规则是一个很复杂的内容,但是目前还没有遇到函数使用比较复杂的用法就不再深入研究这里面的用法了,后面还有一些与线程切换相关的函数,限于篇幅原因,放到第三篇中进行分析。