IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> RT-Thread 内核线程切换原理 -> 正文阅读

[嵌入式]RT-Thread 内核线程切换原理

1、背景

????????本文章主要说明 rtthread 内核线程是如何切换的,初学者刚从裸机开发接触 RTOS 时难免会有些不适应,明白这部分原理之后就会对 RTOS 有更深的理解。在学习内核线程切换原理之前需要有以下基础知识铺垫。本文以 arm 公司的 Cortex-M3 内核为例。

2、基础知识

  • CM3 拥有通用寄存器 R0-R15 以及一些特殊功能寄存器(中断屏蔽寄存器等等)

  • R0-R12 都是通用寄存器,用来临时存储程序运行时产生的数据

  • R13 这个寄存器存储堆栈指针,在 CM3 内核中一共有两个堆栈指针(MSP、PSP),于是 CM3 支持两个堆栈。在启动文件中定义的那个栈空间属于主栈,还有一个在我们创建线程时的栈属于线程栈。这两个栈空间不是同一个空间。

    主堆栈指针(MSP),这是默认的堆栈指针,在裸机开发中只是用这一个指针,由 OS 内核、中断服务程序以及所有需要特权访问的应用程序代码使用。

    进程堆栈指针(PSP),用于常规的应用程序代码,比如线程。

  • R14 也叫做连接寄存器LR,在调用子程序时存储返回地址

  • R15 也叫做程序计数器 (PC,program counter),因为 CM3 内部使用了指令流水线,PC 中存放的是当前指令的地址+4,也就是下一条指令的地址。

  • 栈空间的定义 : 向下生长的栈。也就是说每次执行一个 push(压栈)命令,栈指针向下减小一个单元,每次执行pop命令,栈指针增加一个单元。如下图所示

3、代码分析

3.1 内核寄存器结构体定义

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;
?};

3.2 初始化线程栈

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);//栈地址 + 4 个字节
? ? ?stk ?= (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);//向下8个字节对齐
? ? ?stk -= sizeof(struct stack_frame);//偏移16个字(16*4个字节)
??
? ? ?stack_frame = (struct stack_frame *)stk;//强制转换为 struct stack_frame 类型
??
? ? ?/* init all register */
? ? ?for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
? ?  {
? ? ? ?  ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;//初始化这16个字的空间为 0xdeadbeef
? ?  }
?    /* 初始化高8个字的内存空间 */
? ? ?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 */
??
?#if USE_FPU
? ? ?stack_frame->flag = 0;
?#endif /* USE_FPU */
??
? ? ?/* return task's current stack address */
? ? ?return stk;
?}
  • stack_addr 这个参数为当前线程栈的结束地址,也就是最高的地址。为什么是最高地址?原因是上面说过的栈空间的定义。

  • struct stack_frame 这个结构体的定义可不是胡乱定义的,里面是有顺序要求的。

  • stk -= sizeof(struct stack_frame);//偏移16个字(16*4个字节) 为何偏移这么多字节,因为这16个字的空间的每个地址要按照结构体成员变量的地址去存放,即 psr 要放到这个栈的最高地址,r4 在最低的地址。如图所示,此图出自野火。

3.3 执行线程切换

????????阅读这段代码之前得知道,cm3 内核执行中断或异常时,r0、r1、r2、r3、r12、lr、pc、psr,这些寄存器是自动压栈的。

?rt_hw_context_switch ? ?PROC
? ? ?EXPORT rt_hw_context_switch ;导出函数,此操作能够让C侧代码调用,C侧的第一个参数为当前线程栈sp的指针,第二个
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?;为将要执行的线程栈 sp 的指针
? ? ?; set rt_thread_switch_interrupt_flag to 1
? ? ?LDR ? ? r2, =rt_thread_switch_interrupt_flag;中断标志位  L2 = &rt_thread_switch_interrupt_flag
? ? ?LDR ? ? r3, [r2];r3 = *r2也就是 r3 = rt_thread_switch_interrupt_flag
? ? ?CMP ? ? r3, #1  ;判断rt_thread_switch_interrupt_flag 与 1是否相等
? ? ?BEQ ? ? _reswitch ;相等跳转 _reswitch,当第2次执行线程切换时,rt_thread_switch_interrupt_flag被pendsv置0
? ?                   ?;既然是第二次,所以当前线程具有上文所以要把sp存到rt_interrupt_from_thread,直接跳转_reswitch
? ?                   ?;表示的是第一次切换线程,因为没有上文,所以直接跳到 _reswitch
? ? ?MOV ? ? r3, #1  ;不等则置1
? ? ?STR ? ? r3, [r2] ;rt_thread_switch_interrupt_flag = 1
??
? ? ?LDR ? ? r2, =rt_interrupt_from_thread ? ; set rt_interrupt_from_thread
? ? ?STR ? ? r0, [r2] ? ? ? ? ? ? ? ? ? ? ? ?;rt_interrupt_from_thread = r0,&sp,当前线程sp的地址
??
?_reswitch
? ? ?LDR ? ? r2, =rt_interrupt_to_thread ? ? ; set rt_interrupt_to_thread
? ? ?STR ? ? r1, [r2] ? ? ? ? ? ? ? ? ? ? ? ?;rt_interrupt_to_thread = r1,&sp,将要只要的线程的sp的地址
?    ;触发 pendsv 中断,线程切换的核心
? ? ?LDR ? ? r0, =NVIC_INT_CTRL ? ? ? ? ? ? ?; trigger the PendSV exception (causes context switch)
? ? ?LDR ? ? r1, =NVIC_PENDSVSET
? ? ?STR ? ? r1, [r0]
? ? ?BX ? ? ?LR
? ? ?ENDP
??
?; 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
??
? ? ?; 关闭所有中断以保护这一过程不被打断
? ? ?MRS ? ? r2, PRIMASK
? ? ?CPSID ? I
??
? ? ?; rt_thread_switch_interrupt_flag 为 1时才继续接下来的操作,为0则跳转 pendsv_exit
? ? ?LDR ? ? r0, =rt_thread_switch_interrupt_flag
? ? ?LDR ? ? r1, [r0]
? ? ?CBZ ? ? r1, pendsv_exit ? ? ? ? ; pendsv already handled
??
? ? ?; 清楚中断标志位
? ? ?MOV ? ? r1, #0x00
? ? ?STR ? ? r1, [r0]
?    ;判断 rt_interrupt_from_thread 是否为0,即是否是第一次切换线程,是0则跳转至switch_to_thread
? ? ?LDR ? ? r0, =rt_interrupt_from_thread
? ? ?LDR ? ? r1, [r0]
? ? ?CBZ ? ? r1, switch_to_thread ? ?; skip register save at the first time
??
? ? ?MRS ? ? r1, psp ? ? ? ? ? ? ? ? ; 获取当前线程栈指针到r1中
? ? ?STMFD ? r1!, {r4 - r11} ? ? ? ? ; 将r4 - r11寄存器中的值压入当前栈空间中
? ? ?LDR ? ? r0, [r0]
? ? ?STR ? ? r1, [r0] ? ? ? ? ? ? ? ?; 把当前线程栈指针记录到 rt_interrupt_from_thread 中,即当前栈指针 sp 中
??
?switch_to_thread
? ? ?LDR ? ? r1, =rt_interrupt_to_thread;获取将要执行的栈的sp的地址
? ? ?LDR ? ? r1, [r1]                
? ? ?LDR ? ? r1, [r1] ? ? ? ? ? ? ? ?
??
? ? ?LDMFD ? r1!, {r4 - r11} ? ? ? ? ; 从将要执行的栈中弹出这个线程中的寄存器r4-r11
? ? ?MSR ? ? psp, r1 ? ? ? ? ? ? ? ? ; 并把要执行的线程的栈指针给到 psp
??
?pendsv_exit
? ? ?; 恢复中断
? ? ?MSR ? ? PRIMASK, r2
?    ;由于cm3 内核发生中断时,堆栈指针使用的是msp,因此退出中断时,确保使用psp指针,实际操作就是对,lr寄存的位3进行置1就控制  ? ; 退出中断后使用psp中断
? ? ?ORR ? ? lr, lr, #0x04
? ? ?BX ? ? ?lr ;退出中断时使用psp指针
? ? ?ENDP
  • 通过解读 pendsv 中断代码我们知道,在进入 pendsv 中断前,r0、r1、r2、r3、r12、lr、pc、psr 这些寄存器已经自动压入了当前栈中。

  • 当 pendsv 中断退出时,新的将要执行的线程的中断上下文(r0、r1、r2、r3、r12、lr、pc、ps)会自动的从这个线程栈中弹出,程序计数器 PC 就得到了这个将要执行的线程的pc值,这个线程中用到的其他寄存器的值也从这个新的线程栈中得到了(一部分手动pop,一部分自动pop)。

问题点一:我可以通过这个线程栈指针访问到R0~R15的值吗?

  • 答案是肯定的,因为我们传入的 sp 地址就指向了线程栈地址的偏移16个字处,而内核压栈时,先自动压入 r0、r1、r2、r3、r12、lr、pc、psr 这8个字的空间,按照顺序压,先压psr,然后我们手动压 r4 - r11 ,也是按照顺序压,先压r11。此时这16个字的空间就被填满了,这也是为什么线程栈结构体中的成员变量的顺序不是随便填的(个人理解)。

问题点二:当我进入hard_fault 异常时,我能否获取到当前线程栈指针,从而拿到 pc 指针来判断程序出错的位置?

  • 答案是可以的,rt-thread 已经帮我们重写了 hard_fault 服务程序,其原理请看下回分解.................

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-01-03 16:16:35  更:2022-01-03 16:16:41 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 12:28:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码