STM32 RT-Thread 系统分析(3)-线程管理之线程切换(系统移植基础篇三)

前言

基本信息

名称描述说明
RT-Thread Studio 软件版本版本: 1.1.3
RT-Thread 系统版本4.0.2
STM32CubeIDE 软件版本1.4.0
STM32芯片型号STM32F013VG

前言说明

前面已经分析了一部分与线程切换相关的函数,其中最关键的函数PendSV_Handler函数在本篇进行分析,由于这个函数相关的知识点篇幅较长,因此专门来单独进行分析。

函数和变量描述
PendSV_HandlerPendSV 中断处理函数是 PendSV_Handler(),线程切换的实际工作在 PendSV_Handler() 里完成

PendSV_Handler函数

从前面的函数来看,线程栈初始化函数、线程切换函数都要触发PendSV_Handler中断函数执行,RT-thread系统在PendSV_Handler() 里完成线程切换的实际工作。
在这里插入图片描述

说明

rt_interrupt_to_thread变量中存储值的含义

rt_hw_context_switch函数部分代码如下:

_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对应的地址 */

R1的值是rt_hw_context_switch函数的输入参数 rt_uint32 to,而to这个变量输入的值示例代如下:

rt_hw_context_switch((rt_ubase_t)&from_thread->sp,
                            (rt_ubase_t)&to_thread->sp);

因此R1的值实际上是to_thread线程结构体sp栈指针的地址,特别注意 R1的值是栈指针的地址

因此rt_interrupt_to_thread存储的是 要跳转的线程栈指针的地址

关键代码1:

PendSV_Handler函数部分代码如下:

switch_to_thread: /**/
    LDR     R1, =rt_interrupt_to_thread /*加载 rt_interrupt_to_thread变量 的地址 的值到R1*/
    LDR     R1, [R1]                 /*加载 rt_interrupt_to_thread 的值(to thread线程栈的地址 )到R1*/
    LDR     R1, [R1]                /*加载to thread线程栈的地址处 的值到R1  load thread stack pointer */

从rt_hw_context_switch函数代码内容可以看到rt_interrupt_to_thread 变量在被赋值的时候实际上是将输入的要跳转的线程栈指针的地址。所以可以理解下面的代码含义:

  1. LDR R1, =rt_interrupt_to_thread 代码表示:rt_interrupt_to_thread 变量也是存在地址的,因此将rt_interrupt_to_thread 变量的地址加载到R1中。
  2. LDR R1, [R1] 代码表示:加载rt_interrupt_to_thread 变量的地址处的的值到R1中,相当于取到了rt_interrupt_to_thread 变量的值到R1中。
  3. LDR R1, [R1] 代码表示: 将rt_interrupt_to_thread 变量的值作为地址,加载这个地址处的值到R1中,而这个地址的值就是要切换线程的栈指针的地址

关键代码2:

	/*下面是保存 from线程的上下文*/
    MRS     R1, PSP                 /* 获取 from 线程的栈指针 get from thread stack pointer */
    STMFD   R1!, {R4 - R11}         /* 将 r4~r11 保存到线程的PSP栈里(from线程栈数组中) push R4 - R11 register */
    LDR     R0, [R0]
    STR     R1, [R0]                /* 更新线程的控制块的 SP 指针 update from thread stack pointer */
    LDMFD   R1!, {R4 - R11}         /* pop R4 - R11 register */
    MSR     PSP, R1                 /* update stack pointer */

这两段代码是保存from线程的上下文切换到to线程的上下文
STMFD和LDMFD指令的用法在下面的知识点里面进行讲解,这里不赘述。
下面抛出疑问点:

为什么使用PSP进程堆栈指针而不是用MSP主堆栈指针进行线程切换?
操作模式

Cortex‐M3 支持 2 个模式和两个特权等级
在这里插入图片描述

  • 当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面, handler模式总是特权级的。在复位后,处理器进入线程模式+特权级。
  • 在线程模式+用户级下,对系统控制空间( SCS)的访问将被阻止——该空间包含了配置寄存器 s 以及调试组件的寄存器 s。除此之外,还禁止使用 MSR 访问刚才讲到的特殊功能寄存器——除了 APSR 有例外。谁若是以身试法,则将 fault 伺候。
  • 在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级
    在这里插入图片描述
    如前所述,特权等级和堆栈指针的选择均由 CONTROL 负责。当 CONTROL[0]=0 时,在异常处理的始末,只发生了处理器模式的转换,如下图所示。
    中断前后的状态转换:
    在这里插入图片描述
    但若 CONTROL[0]=1(线程模式+用户级),则在中断响应的始末, both 处理器模式和特权等极都要发生变化,如下图所示。
    中断前后的状态转换+特权等级切换
    在这里插入图片描述
    CONTROL[ 0]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使用一条“系统服务呼叫指令( SVC)”来触发“SVC 异常”,该异常的服务例程可以选择修改CONTROL[0]。
Cortex-M3 的双堆栈机制

我们已经知道了 CM3 的堆栈是分为两个:主堆栈进程堆栈, CONTROL[1]决定如何选择。
CONTROL[1]=0 时的堆栈使用情况:
当 CONTROL[1]=0 时,只使用 MSP,此时用户程序和异常 handler 共享同一个堆栈。这也是复位后的缺省使用方式。
在这里插入图片描述
CONTROL[1]=1 时的堆栈切换情况
当 CONTROL[1]=1 时,线程模式将不再使用 PSP,而改用 MSP( handler 模式永远使用MSP)。此时,进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP退出异常时切换回 PSP,并且从进程堆栈上弹出数据

在这里插入图片描述
在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下:

MRS R0, MSP ; 读取主堆栈指针到 R0
MSR MSP, R0 ; 写入 R0 的值到主堆栈中
MRS R0, PSP ; 读取进程堆栈指针到 R0
MSR PSP, R0 ; 写入 R0 的值到进程堆栈中

通过读取 PSP 的值, OS 就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(使用STMDB和LDMIA的书写形式)。 OS 还可以修改 PSP,用于实现多任务中的任务上下文切换。

问题说明

通过上面的解释我们可以总结以下关键语句

  1. 当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面, handler模式总是特权级的.
  2. 不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级
  3. 进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP
  4. 使用双堆栈机制,在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制

PendSV_Handler函数的执行是异常中断的执行,因此PendSV_Handler函数执行时,当前处于特权级下,此时可以切换PSP进程堆栈指针。因为异常时,自动压栈用的是PSP,此时在PendSV_Handler函数中的任何代码执行都是使用MSP主堆栈指针,不会影响到PSP进程堆栈指针。

为什么出栈和入栈都是采用R4-R11寄存器进行保存和切换的?
  1. R4-R11寄存器不能在异常发生时自动入栈
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;
};

异常发生时 exception_stack_frame结构体内的寄存器是能够自动入栈的,而stack_frame结构体内的寄存器内容不能自动入栈,这是硬件设计导致的。这个栈是线程初始化时分配给当前线程的栈数组的地址段。为了完整保存整个线程的所有数据,其它不能自动入栈的寄存器则需要手动PUSH入栈。当前出栈也是手动操作。

知识点

常用的多重存储器访问方式

示例功能描述
LDMIA Rd!, {寄存器列表}从 Rd 处读取多个字。每读一个字后 Rd 自增一次, 16位宽度
STMIA Rd!, {寄存器列表}存储多个字到 Rd 处。每存一个字后 Rd 自增一次, 16位宽度
LDMIA.W Rd!, {寄存器列表}从 Rd 处读取多个字。每读一个字后 Rd 自增一次, 32位宽度
LDMDB.W Rd!, {寄存器列表}从 Rd 处读取多个字。每读一个字前 Rd 自减一次, 32位宽度
STMIA.W Rd!, {寄存器列表}存储多个字到 Rd 处。每存一个字后 Rd 自增一次, 32位宽度
STMDB.W Rd!, {寄存器列表}存储多个字到 Rd 处。每存一个字前 Rd 自减一次, 32位宽度

上表中,加粗的是符合 CM3 堆栈操作的 LDM/STM 使用方式。并且,如果 Rd 是 R13(即 SP),则与
POP/PUSH 指令等效。 (LDMIA‐>POP, STMDB ‐> PUSH)

STMDB SP!, {R0-R3, LR} 等效于 PUSH {R0-R3, LR}
LDMIA SP!, {R0-R3, PC} 等效于 PUSH {R0-R3, PC}

Rd 后面的“!”是什么意思?它表示要自增(Increment)或自减(Decrement)基址寄存器 Rd
的值,时机是在每次访问前(Before)或访问后(After)。增/减单位:字(4字节)。例如,记R8=0x8000,
则下面两条指令:

STMIA.W R8!, {r0-R3} ; R8 值变为 0x8010,每存一次曾一次,先存储后自增
STMDB.W R8, {R0-R3} ; R8 值的“一个内部复本”先自减后存储,但是 R8 的
值不变

感叹号还可以用于单一加载与存储指令——LDR/STR。这也就是所谓的 “带预索引”
(Pre‐indexing)的 LDR 和 STR。例如:

LDR.W R0, [R1, #20]! ;预索引

该指令先把地址 R1+offset 处的值加载到 R0,然后, R1 <- R1+ 20(offset 也可以是负数)。这里的“!”就是指在传送后更新基址寄存器 R1 的值。“!”是可选的。如果没有“!”,则该指令就是普通的带偏移量加载指令。带预索引的数据传送可以用在多种数据类型上,并且既可用于加载,又可用于存储。
预索引数据传送的常见用法

示例功能描述
LDR.W Rd, [Rn, #offset]!
LDRB.W Rd, [Rn, #offset]!
LDRH.W Rd, [Rn, #offset]!
LDRD.W Rd1, Rd2, [Rn, #offset]!
字/字节/半字/双字的带预索引加载(不做带符号扩展,没有用到的高位全清 0)
LDRSB.W Rd, [Rn, #offset]!
LDRSH.W Rd, [Rn, #offset]!
字节/半字的带预索引加载,并且在加载后执行带符号扩展成 32 位整数
STR.W Rd, [Rn, #offset]!
STRB.W Rd, [Rn, #offset]!
STRH.W Rd, [Rn, #offset]!
STRD.W Rd1, Rd2, [Rn, #offset]!

CM3 除了支持“预索引”,还支持“后索引” (Post‐indexing)。后索引也要使用一个立即数 offset,但与预索引不同的是,后索引是忠实使用基址寄存器 Rd 的值作为数据传送的地址的。待到数据传送后,再执行 Rd Å Rd+offset(offset 可以是负数)。如:

STR.W R0, [R1], #-12 ;后索引

该指令是把 R0 的值存储到地址 R1 处的。在存储完毕后, R1 Å R1+(‐12)
注意, [R1]后面是没有“! ”的。可见, 在后索引中,基址寄存器是无条件被更新的——相当于有一个“隐藏”的“!
后索引的常见用法

示例功能描述
LDR.W Rd, [Rn], #offset
LDRB.W Rd, [Rn], #offset
LDRH.W Rd, [Rn], #offset
LDRD.W Rd1, Rd2, [Rn], #offset
字/字节/半字/双字的带预索引加载(不做带符号扩展,没有用到的高位全清 0)
LDRSB.W Rd, [Rn], #offset]
LDRSH.W Rd, [Rn], #offset]
字节/半字的带预索引加载,并且在加载后执行带符号扩展成 32 位整数
STR.W Rd, [Rn], #offset
STRB.W Rd, [Rn], #offset
STRH.W Rd, [Rn], #offset
STRD.W Rd1, Rd2, [Rn], #offset

指令STMFD和LDMFD分析

指令说明
LDM从一片连续的地址空间中加载多个字到若干寄存器
STM存储若干寄存器中的字到一片连续的地址空间中
LDMIA加载多个字,并且在加载后自增基址寄存器
STMIA加载多个字,并且在加载后自增基址寄存器

栈指针指向栈顶元素(即最后一个入栈的数据元素)时称为FULL栈;
栈指针指向与栈顶元素相邻的一个可用书局单元时称为EMPTY栈。

根据数据栈的增长方向不同:
当数据栈向内存地址减小的方向增长时,称为Descending栈;
当数据栈向内存地址增加的方向增长时,称为 Ascending栈

综合上面两点,可以存在以下四种数据栈:

指令说明
FD - Full Descending入栈,数据栈向内存地址减小的方向增长
ED - Empty Descending出栈,数据栈向内存地址减小的方向增长
FA - Full Ascending入栈,数据栈向内存地址增加的方向增长
EA - Empty Ascending出栈,数据栈向内存地址增加的方向增长

因此实际上存在下面这些批量load/save指令:

LDMFA, LDMFD, LDMEA, LDMED
STMED, STMEA, STMFD, STMFA

给定数据栈对应着的特定批量load/save指令,也决定了地址变化方式:

比如FD栈,对应的批量传送指令是LDMFD/STMFD,对应的地址变化方式是IA(事后递增方式), DB(事先递减方式)

STMFD SP!, {R0~R7, LR}

start_address = sp - 9 * 4

end_address = sp - 4
把寄存器r0~r7和LR共9个寄存器,存储到start_address开始, 到end_address结束的栈中,并且修改SP的值(SP变小),相当于压栈。

LDMFD SP!, {R0~R7, LR}
start_address = SP

end_address = SP + 9 * 4

把堆栈从start_address开始,到end_address内的值恢复到寄存器R0, R1… R7和LR中,并修改SP的值(SP变大),相当于出栈。

16 位数据操作指令

名字功能
ADC带进位加法
ADD加法
AND按位与(原文为逻辑与,有误——译注)。这里的按位与和 C 的”&”功能相同
ASR算术右移
BIC按位清 0(把一个数跟另一个无符号数的反码按位与)
CMN负向比较(把一个数跟另一个数据的二进制补码相比较)
CMP比较(比较两个数并且更新标志)
CPY把一个寄存器的值拷贝到另一个寄存器中
EOR近位异或
LSL逻辑左移(如无其它说明,所有移位操作都可以一次移动多格——译注)
LSR逻辑右移
MOV寄存器加载数据,既能用于寄存器间的传输,也能用于加载立即数
MUL乘法
MVN加载一个数的 NOT 值(取到逻辑反的值)
NEG取二进制补码
ORR按位或(原文为逻辑或,有误——译注)
ROR圆圈右移
SBC带借位的减法
SUB减法
TST测试(执行按位与操作,并且根据结果更新 Z)
REV在一个 32 位寄存器中反转字节序
REVH把一个 32 位寄存器分成两个 16 位数,在每个 16 位数中反转字节序
REVSH把一个 32 位寄存器的低 16 位半字进行字节反转,然后带符号扩展到 32 位
SXTB带符号扩展一个字节到 32 位
SXTH带符号扩展一个半字到 32 位
UXTB无符号扩展一个字节到 32 位
UXTH无符号扩展一个半字到 32 位

16 位转移指令

名字功能
B无条件转移
B条件转移
BL转移并连接。用于呼叫一个子程序,返回地址被存储在 LR 中
BLX #im使用立即数的 BLX 不要在 CM3 中使用
CBZ比较,如果结果为 0 就转移(只能跳到后面的指令——译注)
CBNZ比较,如果结果非 0 就转移(只能跳到后面的指令——译注)
ITIf‐Then
CBZ 和 CBNZ

比较并条件跳转指令专为循环结构的优化而设,它只能做前向跳转。语法格式为:

CBZ <Rn>, <label>
CBNZ <Rn>, <label>

它们的跳转范围较窄,只有 0‐126。
典型范围如下所示:

while (R0!=0)
{
Function1();
}

变成

Loop
CBZ R0, LoopExit
BL Function1
B Loop
LoopExit:

与其它的比较指令不同, CBZ/CBNZ 不会更新标志位

总结

线程切换相关的基础函数已经基本介绍完毕,线程切换相关的基础函数都与硬件相关,但是确是移植系统的基础。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值