操作方法为右键目录或文件,点击Include/Exclude From Build菜单项恢复编译,同样的方法再选一遍即可。
下面说些移植操作系统的注意事项
ARM上移植实时操作系统大家可能比较熟悉,对于RISC-V内核的MCU,可能相对比较陌生。下面结合WCH的CH32V103和CH32V307两款芯片来详细说下针对RISC-V平台,移植实时操作系统的注意点。
在移植前,有必要对RISC-V的一些基本知识点有一定的了解,这里对RISC-V的概况,发展,指令集,特权模式等不作详述,仅结合WCH的RISC-V内核的MCU,简单介绍我们移植实时操作系统有可能遇到的关键点做一下描述。这里之所以选取V103和V307两款芯片,主要其极具代表性:
首先,直观上其外设的使用方法和我们之前熟悉的F103,F107等是兼容的,这样降低了我们使用和移植时的难度,基于WCH提供的外设库,我们以前上层的代码甚至于不用修改可直接使用。其次,V103是WCH RISC-V内核家族中的V3内核,V307为V4内核,V3内核支持RV32IMAC指令集,即除支持RISC-V基本的32位整数指令集外,还支持硬件乘除法,原子指令,压缩指令。V4在V3的基础上增加了单精度硬件浮点,并且其性能也比V3高。
除上述之外,虽然两者的中断控制器(PFIC)相较于现行的PLIC均不同,均不是统一入口,而是采用中断向量表寻址的方式,但是V3的中断向量表处存放是一条指令,而V4的向量表既可以存放指令,也可以存放中断处理函数的地址。两者均支持中断嵌套和硬件压栈,区别在于V3最大嵌套两级,V4最大可达八级,同时V3的硬件压栈深度两级,V4的硬件压栈深度为三级。这里需要注意的是,移植实时操作系统时需要关闭硬件压栈,在切换任务时所有寄存器,我们希望是由我们自己控制其压栈和出栈的内容。
RISC-V寄存器如下图所示,其中x0-x31为整形寄存器,f0-f31为浮点寄存器(V3没有浮点寄存器)。所有带caller的寄存器,当发生中断时需要保存,值得注意的是,WCH的硬件压栈保存的寄存器仅仅保存整数的16个caller saved 寄存器。正常一个中断函数的寄存器保存我们不用关心,编译器会帮我们做的很好。但是当我们从一个汇编入口进中断函数的时候这些过程就不得不由我们自己来实现。寄存器中几个相对特殊的x0恒为0,x1是返回地址寄存器ra,函数调用时用来存放返回地址,x2为堆栈指针sp,x3为gp全局指针,用来寻址全局变量。对于一个正常运行的程序,除了x0,gp两个初始值固定的外,其余的均会是不确定的,所有在进行上下文保护时,均需要保存。用到硬件浮点的时候,更是要保存32个浮点寄存器。
除了上述的寄存器,移植还要关心的是几个csr寄存器mstatus,mepc。正常情况下大部分csr只能在机器模式下操作(WCH的v3和v4内核支持机器模式和用户模式)。mstatus中,MIE为中断使能,当进中断时MPIE更新为MIE,返回时MIE更新为MPIE。MPP用于保存进中断之前的特权模式,如果我们设置其为MPP=0b11,那么将一直处于机器模式,其mret返回后还是处于机器模式。mepc是机器模式下异常程序指针,其只会在发生异常是被更新(中断也是一类异常),进异常时我们可以从另外两个csr寄存器mcause来看引起异常原因通过mtval查看引起异常时的值。当从异常返回时mepc的值被更新给pc。我们正是通过进中断修改mepc来实现任务的切换的,后面会详细说明这个过程。
实时操作系统大家应该不陌生,常见的uCOS,FreeRTOS,RT-Thread,LiteOS-M等等,其基本的思路都是一样的,需要一个定时器用于系统时间片的实现,一个中断用于任务切换。想要其能够在一个MCU上成功的跑起来,需要弄清除一下几个事情:
(1)进中断需要保存哪些内容。
从之前的描述中,应该知道,对于risc-v内核来说其进中断压栈的是caller saved的寄存器。从下图一可以看出,进Sys
tick中断函数,先进行寄存器保存,退出中断时进行寄存器恢复,如果开启硬件浮点,同时还会对浮点寄存器进行保存和恢复。这个过程是编译器帮我们实现,有一点需要注意的是我们移植的代码里面进中断后获取了中断的堆栈“csrrw sp,mscratch,sp”,返回时恢复了线程的堆栈指针“csrrw sp,mscratch,sp”中断堆栈指针初始值是在任务开始时存入mscratch寄存器的,如果采用C形式中断函数,中断堆栈的获取会在压栈操作之后,中断压入的堆栈是当前运行任务的任务堆栈区域,如果想要中断函数压栈时压入的自己的堆栈区域,可以使用汇编入口,进中断后先修改sp,然后压栈,再调用中断处理函数,如图二所示。
图1
图2
(2)任务栈需要保存哪些内容。
前文说过对于一个正常运行的程序,切换任务前,除了x0恒0,x3 gp指针外,其余的寄存器均需要保存,每个RTOS中都会定义一个上下文保存相关的结构体,这里我们以华为鸿蒙LiteOS_M为例,看一下这个结构体:
图3
在创建任务的时候均会为一个任务分配一个id和堆栈大小并对这个堆栈做初始化:
图4
图5
任务创建好了后会关联一个根据任务id关联一个任务控制块taskCB,总的任务个数是在头文件中配置的(target_config.h)总的任务块的初始化也是在LOS_KernelInit被初始化。
图6
从上面可以看出来,task--->taskCB--->sp指针--->memory这样的路线,而这片memory开始位置用于上下文保存。
这样的方式在其他RTOS中也可以看到,例如RT-Thread中用于上下文保存的结构体rt_hw_stack_frame,和taskCB类似的结构体rt_thread等。
图7
图8
(3)如何开启任务调度。
前面看了每个任务上下文保存位置,注意到堆栈初始化的时候把任务的入口地址给了context->epc。同时LiteOS_M源码中定义了一个LosTask类型的全局变量g_losTask,其内部只有两个任务控制块指针,一个指向当前运行的任务,一个指向新任务,即要切换至的任务。
图9
当做好一系列初始化后,LiteOS会调用HalStartSchedule来初始化系统节拍定时器,并注册系统定时器的中断处理函数,然后开始转向执行第一个任务,如下图所示:
图10
其中OsSchedStart函数从任务列表中获取第一个任务,并赋值给g_losTask里面的runTask和newTask。然后调用HalStartToRun转向执行runTask所指示的任务。HalStartToRun是一段汇编代码,下面就具体看其如何切换至runTask,具体如下图的注释:
图11
这样mret之后就转向去执行第一个任务,并且不会再有返回,因为每个任务本身会是个循环,这里也就能理解其源码注释 never return的含义。
图12
其他操作系统中也有类似的操作,例如RT-Thread中有个rt_hw_context_switch_to函数,其也是汇编代码实现,它是一个带参数的函数,其传入的参数为(&to_thread->sp),如下图:
图13
从名字就可以看出,传递的参数为启动执行的第一个线程的控制块的堆栈指针sp的值,后面赋值mepc,mstatus,其他寄存器等等都是和LiteOS_M一致的。
(4)如何进行任务切换。
了解了如何切换至第一个任务,那么如何实现不同任务之间的切换呢,在这之前我们应该都有了解,RTOS是根据任务的优先级和时间片进行轮转的,每个任务执行一段时间,然后切换至下一个任务执行。每次切换前我们需要把当前任务的运行状态进行保存,然后切换至新任务,对其运行状态进行恢复,如此循环反复,实现任务调度。时间片实现使用的是内核的SysTick定时器,LiteOS_M是在los_timer.c中实现的,这个只需要根据实际硬件的进行初始化就行。其他操作系统也是类似,像RT-Thread源码中我们根据硬件完成board.c。对于任务切换,我们利用内核的软中断,只要使能该中断,并且当需要切换任务时,把中断控制器的对应的pendset位置1,即可触发该中断进行任务切换。下图是liteOS_M切换过程:
图14
图15
其他操作系统也是大同小异,具体的区别仅仅是在切换新任务时,新任务如何获取的问题,上图可以看到LiteOS_M是通过g_losTask来管理,RT-Thread中定义了from_thread,to_thread,顾名思义从一个线程切换至另外一个线程。
弄清楚以上的问题,对于某一个RTOS的基本移植来说应该就比较明了。
最后移植好的鸿蒙os,RT-Thread等实时操作系统的代码均已在MRS上线,可以直接创建,开发相关应用