Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
Kernel
想深入了解RTOS内核线程切换原理--请看此文
发布于 2020-02-23 22:35:07 浏览:2964
订阅该版
**本文转自我的博客csdn fhqlongteng**,转载过来,代码的格式发生混乱,你可以到我的博客中查看原版。 **1、简介 ** 本文主要介绍RT Thread操作系统在cortex-m3内核上的移植接口文件,通过本篇博客你将深入了解RTOS操作系统是怎么通过触发软中断实现任务切换的,怎么实现内核异常信息的打印功能。 **2、移植的接口文件** RT-Thread操作系统的移植接口文件主要用cpuport.c,context_rvds.s,backtrace.c,div0.c,showmem.c。其中最重要的文件是cpuport.c和context_rvds.s这两个文件,其他三个文件在cortex-M3内核移植时没有实际的应用,这三个文件实际一些辅助的功能,打印内存,除数为0,后台跟踪等操作,内容很简单,可以自行查看。 **3、任务切换context_rvds.s** 这是一个汇编语言的文件,这个文件实现了任务切换,触发软件中断,硬件异常错误处理等操作,是操作系统移植时要实现的最重要的功能。程序的内部逻辑根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``` ![30cb5d44e276505b1d4c053c8b25525da228db30.gif](https://oss-club.rt-thread.org/uploads/data-image-base64/30cb5d44e276505b1d4c053c8b25525da228db30.gif) ** 线程切换的核心就在上面的代码注释中,不懂的话要多看几次同时参考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是实现开中断和关中断,功能很简单,不再详细描述。 ```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``` ** 4、cpu接口程序cpuport.c** 这个程序主要有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
", context->exception_stack_frame.psr); rt_kprintf("r00: 0x%08x
", context->exception_stack_frame.r0); rt_kprintf("r01: 0x%08x
", context->exception_stack_frame.r1); rt_kprintf("r02: 0x%08x
", context->exception_stack_frame.r2); rt_kprintf("r03: 0x%08x
", context->exception_stack_frame.r3); rt_kprintf("r04: 0x%08x
", context->r4); rt_kprintf("r05: 0x%08x
", context->r5); rt_kprintf("r06: 0x%08x
", context->r6); rt_kprintf("r07: 0x%08x
", context->r7); rt_kprintf("r08: 0x%08x
", context->r8); rt_kprintf("r09: 0x%08x
", context->r9); rt_kprintf("r10: 0x%08x
", context->r10); rt_kprintf("r11: 0x%08x
", context->r11); rt_kprintf("r12: 0x%08x
", context->exception_stack_frame.r12); rt_kprintf(" lr: 0x%08x
", context->exception_stack_frame.lr); rt_kprintf(" pc: 0x%08x
", context->exception_stack_frame.pc); if(exception_info->exc_return & (1 << 2) ) { rt_kprintf("hard fault on thread: %s
", rt_thread_self()->name); #ifdef RT_USING_FINSH list_thread(); #endif /* RT_USING_FINSH */ } else { rt_kprintf("hard fault on handler
"); } #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)* = 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内核移植部分的关键代码的讲解,如有不懂的地方可以在下面留言联楼主 *
查看更多
3
个回答
默认排序
按发布时间排序
andychen
2020-02-24
这家伙很懒,什么也没写!
帮你重新排了下版,这样阅读效果不比博客差了
fhqmcu
认证专家
2020-02-24
个人博客:https://blog.csdn.net/fhqlongteng
>帮你重新排了下版,这样阅读效果不比博客差了 --- 谢谢的,我正要手工排版来的,昨天太晚就没有整了
EINTR
2022-01-02
这家伙很懒,什么也没写!
关于通过结构体能够访问内核寄存器的问题我可否这样理解? 看源码发现: `rt_hw_stack_init` 返回的是栈顶地址,而栈顶向下的16个字 的空间是一个16个字的结构体,结构体成员变量对应的是内核寄存器,按照压栈顺序排放。 1、压栈时,先从当前线程栈的最高地址压栈(栈向下生长,进入 pendsv 自动将一些寄存器的值压入栈中),将16个字的寄存器的值压入了线程栈空间的高16个字的内存中( stack_frame 这个结构体)。 2、进入 hardfault 前,自动将一些寄存器压栈,然后压入 r4-r11,然后压手动再次压lr,lr中存储的是中断返回动作(返回后使用 psp,还是 msp),这个lr是最后手动入栈的,所以这个lr在最低的地址,所以,这个 exc_return 放在结构体的最开始。然后在 `rt_hw_hard_fault_exception` 中通过这个结构体就能拿到进入 hardfault 前的任务的栈中各个寄存器的信息,通过 exc_return 判断是线程环境还是中断环境引起的异常。
撰写答案
登录
注册新账号
关注者
1
被浏览
3k
关于作者
fhqmcu
个人博客:https://blog.csdn.net/fhqlongteng
提问
16
回答
60
被采纳
1
关注TA
发私信
相关问题
1
请教cpu使用率分析
2
选择FreeRTOS, 还是RT-Thread。
3
thread heap stack overflow ?
4
rtt消息队列delay问题
5
释放被删除线程的内存地方在哪里啊
6
请教:各线程结束后,释放其中的内存的连续性问题
7
STM32F103中断关于信号量、邮箱问题
8
RTT中的线程栈大小如何控制
9
关于线程由执行态变为挂起态的代码实现,,,
10
rt_malloc(rt_size_t size)内存分配函数最小分配尺寸问题
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
最新文章
1
使用百度AI助手辅助编写一个rt-thread下的ONVIF设备发现功能的功能代码
2
RT-Thread 发布 EtherKit开源以太网硬件!
3
rt-thread使用cherryusb实现虚拟串口
4
《C++20 图形界面程序:速度与渲染效率的双重优化秘籍》
5
《原子操作:程序世界里的“最小魔法单位”解析》
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
ulog
C++_cpp
at_device
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
8
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
3
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
分享
好友
手机
浏览
扫码手机浏览
投诉
建议
回到
底部