RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)

目录

异常处理代码分析

 系统定时器到期切换任务

使用gdb跟踪任务切换与恢复过程


上一篇文章RISC-V FreeRTOS启动过程分析(基于qume+gdb调试)_Dingjun798077632的博客-CSDN博客分析了RISC-V+ FreeRTOS的启动过程,文章中提到配置的异常入口地址为freertos_risc_v_trap_handler,本文接着从该函数开始分析异常处理过程。

异常处理代码分析

freertos_risc_v_trap_handler函数代码如下:

FreeRTOS\Source\portable\GCC\RISC-V\portASM.S

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第1张图片

宏portcontextSAVE_CONTEXT_INTERNAL代码如下:

FreeRTOS\Source\portable\GCC\RISC-V\portContext.h

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第2张图片

        异常入口首先通过宏portcontextSAVE_CONTEXT_INTERNAL,保存CONTEXT上下文到当前任务的栈中(竟然异常时的sp指针还是指向当前Task的任务栈的栈顶,所以上面是直接压栈),最后sp保存到pxCurrentTCB->pxTopOfStack(退出异常恢复/切换任务时,需要使用pxCurrentTCB->pxTopOfStack先恢复sp,之后通过sp恢复任务的CONTEXT、这里不保存x3(gp不会修改)、x4(tp没有用到),至于原因上一篇文章中分析过。整个压栈处理和上文中的初始化任务栈很像,保存顺序基本一致,这里注意一点,portcontextSAVE_CONTEXT_INTERNAL中sp偏移0位置(栈顶)还空着,这个位置是留着保存cpu返回地址的。

        这里保存上下文的代码,实际上是有一点点漏洞的,在创建任务和vTaskStartScheduler()之前,pxCurrentTCB一定是个空指针,代码task.c中也可以看到,此时触发异常保存sp到pxCurrentTCB->pxTopOfStack肯定有问题。当然调用vTaskStartScheduler()之前异步中断是被屏蔽的,但是同步异常没法屏蔽。

        portcontextSAVE_CONTEXT_INTERNAL之后,接着读取了mcause(异常原因寄存器,功能类似armv8的ESR_ELx)和mepc(异常返回地址寄存器,功能类似armv8的ELR_ELx)到a0和a1(a0和a1原来的值以及压栈了)。之后判断是异常还是中断分别跳转到对应的Label中处理。mcause>=0为异常,mcause<0为中断(interrupt最高位为1即为负数)。

mcause寄存器

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第3张图片

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第4张图片

        异步中断处理asynchronous_interrupt中,将mepc中断返回地址直接保存到了sp偏移0的位置。同步异常处理synchronous_exception中将mepc+4作为中断返回地址直接保存到了sp偏移0的位置。

        原因:异步中断会等当前指令退休(执行完)后响应中断,mepc自动保存下一条需要执行的指令;而同步异常,触发异常的就是当前执行的指令(也就没法等当前指令退休),比如ecall指令或者非法指令触发的异常,如果继续回到当前指令执行还是会继续触发异常,就陷入了死循环(当然也可以在异常处理代码中修复触发异常的指令,然后回到mepc处继续执行,本文不讨论。另外,mepc+4是不是有可能是mepc+2有点疑问,如果采用压缩指令集)。

        保存好mepc返回地址到任务栈后,就可以切换sp到中断栈了,对应代码上面load_x sp, xISRStackTop。之后跳转handle_interrupt或者handle_exception。相关代码如下:

FreeRTOS\Source\portable\GCC\RISC-V\portASM.S

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第5张图片

 系统定时器到期切换任务

        这里重点分析timer定时器中断处理过程,也是任务切换过程。timer中断mcause的值为0x8000[]0007(中间加工[],是应为RV64系统mcause值0x8000 0000 0000 0007)对应上文mcause描述。判断时定时器中断,调用函数xTaskIncrementTick,改函数freeRTOS的核心函数,平台无关,主要作用更新systick,检查是否需要切换任务(有同优先级或者更高优先级的任务ready),OS软件定时器是否到期,调用vApplicationTickHook...。

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第6张图片

该函数返回值xSwitchRequired,就是表示是否需要切换任务。

函数退出后判断a0(函数返回值)是否为0,为0不需要切换任务直接跳转processed_source,否则继续执行call vTaskSwitchContext。

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第7张图片

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第8张图片

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第9张图片

vTaskSwitchContext函数通过宏taskSELECT_HIGHEST_PRIORITY_TASK修改了pxCurrentTCB指针,之后退出异常时,通过pxCurrentTCB恢复任务就完成了任务的切换。

接下来回到processed_source,也就是恢复任务上下文,退出异常处理。

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第10张图片

恢复上下文portcontextRESTORE_CONTEXT和之前的保存上下文portcontextSAVE_CONTEXT_INTERNAL对应。通过pxCurrentTCB恢复sp,将sp偏移0的值恢复到了mepc,其它寄存器也一并恢复,如果切换了pxCurrentTCB,自然恢复的就是另一个任务的上下文。最后调用mert退出异常处理。执行mret(这里时异常退出使用mret,和上一篇文章使用ret切换到任务中入口函数运行不一样,切换到任务入口相当于函数调用),cpu使用mstatus.mpp恢复处理器工作模式(这里恢复前后都是M模式),自动使用mepc恢复pc指针。也就回到了进入异常前的任务,或者完成任务切换。

        这里FreeRTOS是实时操作系统,并不是只有在timer中断时才切换任务,比如高优先级任务被唤醒而发生抢占,或者当前运行的任务主动挂起都会触发任务切换。此时RTOS检测到需要切换任务,调用portYIELD_WITHIN_API() -> portYIELD -> __asm volatile( "ecall" ) 进入异常处理,在异常处理handle_exception中同样是调用vTaskSwitchContext,

接下来的流程就和timer一样了。(实际上,对于arm cortex-m系列处理也是类似的,主动触发PendSV,PendSV中调用vTaskSwitchContext)

使用gdb跟踪任务切换与恢复过程

上述任务切换与恢复过程,同样可以使用gdb跟踪分析。

修改FreeRTOS/Demo/RISC-V-Qemu-virt_GCC/main_blinky.c,将prvQueueReceiveTask和prvQueueSendTask改为两个相同优先级的任务,任务中避免主动挂起,这样两个任务就会一直被轮流调度,每个systick切换一次。

main_blinky.c修改后的代码:

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第11张图片为了一个任务每个systick多循环几次,本人修改了FreeRTOS/Demo/RISC-V-Qemu-virt_GCC/FreeRTOSConfig.h中configTICK_RATE_HZ的值为20。

编译并反汇编,启动gdb调试后,对照反汇编文件,在异常函数入口处设置断点,如下图所示:

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第12张图片

下面两张图片的日志,下半部分有点重叠,分两次截的图

RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第13张图片RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_第14张图片

        上面日志,先运行prvQueueSendTask,运行一段时间定时器触发中断,在异常入口处断点打印mepc、sp等寄存器值(就是该task的上下文)。继续运行切换到任务prvQueueReceiveTask,再次定时器触发中断停在中断入口断点处,此时继续的话应该会切换回prvQueueSendTask。

为了观察prvQueueSendTask上下文的恢复情况,我们先在processed_source最后的mret指令处增加断点,继续运行到新增加断点处,此时理应恢复了prvQueueSendTask的上下文,那我们就再打印一次相关寄存器,与前一次打印的对比,可以看到是一模一样的。也就是prvQueueSendTask -> prvQueueReceiveTask -> prvQueueSendTask,整个过程完美恢复了Task的上下文。

        任务切换的跟踪调试演示就到此为止,可以自行修改代码,加入更多断点,做更细致的跟踪分析。中断或者异常处理后恢复现场的过程,比上述切换任务恢复现场过程简单,这里不再演示。

你可能感兴趣的:(RISC-V,FreeRTOS,risc-v)