软件平台:MounRiver工作室;硬件:CH32V307开发板
先去代码云克隆源代码。

创建一个新的CH32V307项目,将源代码直接拖动到项目中,添加它,然后添加头文件路径。
源代码比较全面,可以选择不需要的部分排除在编译之外。
方法是右键单击目录或文件,单击“包含/排除于生成”菜单项以恢复编译,然后再次选择相同的方法。
以下是移植操作系统的一些注意事项。
你可能对在ARM上移植实时操作系统比较熟悉,但对带有RISC-V内核的MCU可能比较陌生。下面结合WCH的两个芯片CH32V103和CH32V307,详细描述实时操作系统移植到RISC-V平台的注意事项。
在移植之前,有必要了解一下RISC-V的一些基础知识点,这里不详细介绍RISC-V的概况、发展、指令集、特权模式,只简单介绍一下我们在移植基于WCH MCU的实时操作系统时可能会遇到的要点。这里之所以选择V103和V307芯片,是因为它们很有代表性:
首先,直观来看,其外设的使用方法兼容F103、F107等。这是我们以前所熟悉的,从而降低了我们使用和移植的难度。基于WCH提供的外围库,我们之前的上层代码甚至可以不加修改直接使用。其次,V103是WCH RISC-V内核家族中的V3内核,V307是V4内核,V3内核支持RV32IMAC指令集,即除了RISC-V基本32位整数指令集外,还支持硬件乘除、原子指令和压缩指令。V4在V3的基础上增加了一个单精度硬件浮点,性能比V3更高。
除此之外,虽然两种设备的中断控制器与目前的PLIC不同,但都不是统一入口,而是由中断向量表寻址。然而,V3的中断向量表存储指令,而V4的向量表可以存储指令和中断处理函数的地址。两者都支持中断嵌套和硬件堆栈。不同的是V3的最大嵌套层数是二,V4的最大嵌套层数是八。同时,V3的硬件堆叠深度是2,V4的硬件堆叠深度是3。这里需要注意的是,移植实时操作系统时,硬件压栈需要关闭,切换任务时,所有寄存器,我们希望可以自己控制压栈和卸载的内容。
RISC-V寄存器如下图所示,其中x0-x31为整数寄存器,f0-f31为浮点寄存器。发生中断时,需要保存调用方的所有寄存器。值得注意的是,WCH的硬件堆栈保存的寄存器只保存整数16调用者保存的寄存器。我们不需要关心一个普通中断函数的寄存器保存,编译器会为我们做好的。但是当我们从一个汇编入口进入中断函数时,这些过程必须由我们自己实现。寄存器中几个比较特殊的x0寄存器常量为0,x1是返回地址寄存器ra,用来存储函数被调用时的返回地址,x2是堆栈指针sp,x3是全局指针gp,用来寻址全局变量。对于一个正常运行的程序,除了x0和gp有固定的初始值外,其余的都是不确定的,都需要在上下文保护时保存。使用硬件浮点时,需要保存32个浮点寄存器。
除了上述寄存器,移植时还需要注意几个csr寄存器mstatus、mepc。一般情况下,大部分csr只能在机器模式下运行。在mstatus中,启用MIE中断,当进入中断时,MPIE更新为MIE,当它返回时,MIE更新为MPIE。MPP用于在中断前保存特权模式。如果我们将其设置为MPP=0b11,它将始终处于机器模式,并且在其mret返回后仍将处于机器模式。Mepc是机器模式下的异常程序指针,只有在发生异常时才会更新。当异常发生时,我们可以从另外两个csr寄存器mcause中看到异常发生的原因,并通过mtval检查异常发生时的值。当mepc从异常返回时,它的值被更新到pc。正是通过修改mepc来切换任务,后面会详细说明。
实时操作系统应该是大家都熟悉的。常见的uCOS,FreeRTOS,RT-Thread,LiteOS-M等。,有相同的基本思想,都需要一个定时器用于系统时间片的实现,一个中断用于任务切换。如果您想在MCU上成功运行,您需要清除一些东西:
中断中需要保存的内容。
从前面的描述应该知道,对于risc-v内核来说,保存的调用者寄存器被压入中断栈。从下面的图1可以看出,当进入Systick中断功能时,寄存器首先被保存,当退出中断时,寄存器被恢复。如果硬件浮点开启,浮点寄存器将同时保存和恢复。这个过程是由编译器为我们实现的。需要注意的一点是,我们在移植的代码中得到了中断堆栈“csrrw sp,mscratch,sp”,并在返回时恢复了线程的堆栈指针“csrrw sp,mscratch,sp”。任务开始时,中断堆栈指针的初始值存储在mscratch寄存器中。如果中断函数是C形式的,那么在压栈操作之后会获取栈。中断推送的堆栈是当前运行任务的任务堆栈区。如果想在中断函数压栈时压入自己的栈区,可以使用汇编入口。进入中断后,先修改sp,然后按堆栈,再调用中断处理函数,如图2所示。
图1
图2
堆栈中需要保存的内容。
如前所述,对于一个正常运行的程序,在切换任务之前,除了x0常量0和x3 gp指针,其他所有寄存器都需要保存,每个RTOS中都会定义一个与上下文保存相关的结构。这里,我们以华为鸿蒙系统LiteOS_M为例,来看看这个结构:
图3
创建任务时,会为任务分配一个id和堆栈大小,并初始化堆栈:
图4
图5

任务创建后,将根据任务id关联一个任务控制块taskCB。任务总数在头文件中配置,总任务块的初始化也在LOS_KernelInit中初始化。
图6
从上面可以看出,task-> task CB-> SP指针->内存的路由,而这个内存的起始位置是用于上下文保存的。
这种方式在其他RTOS中也可以看到,比如rt_hw_stack_frame,一种在RT-Thread中用于上下文保存的结构,以及rt_thread,一种类似于taskCB的结构等。
图7
图8
如何打开任务计划?
查看每个任务上下文的存储位置,我注意到在初始化堆栈时,任务的入口地址被赋予了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函数,也是汇编代码实现。它是一个带参数的函数,传递的参数如下:
图13
从名字可以看出,传递的参数是开始执行的第一个线程的控制块的堆栈指针sp的值,以及后面的mepc、mstatus、其他寄存器等的值。都和LiteOS _ M一致。
如何切换任务?
知道如何切换到第一个任务,那么如何在不同的任务之间切换呢?在此之前,我们都应该知道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的基本移植应该是清楚的。
最后,Harmonyos OS、RT-Thread等实时操作系统的移植代码已经在MRS中上线,可以直接创建和开发相关应用。


