RISC-V架构下 FPU Context 的动态保存和恢复

本文由RT-Thread论坛用户@blta原创发布:https://club.rt-thread.org/ask/article/248051628070d52e.html
在 RISC-V移植那些事中 文章中提到了对 RISC-V架构FPU移植部分的优化,最近找工作,这件事做的断断续续,终于完成了。
开发环境 硬件
这次选用了Nuclei和中国移动芯昇科技合作的CM32M433R-START的RISC-V生态开发板,该开发板今年刚出来,比较新,采用芯来科技N308内核(RV32IMACFP)符合我们的FPU测试需求,99元价格也很便宜,果断入手测试,顺便支持一下!
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

more info , refer to https://www.rvmcu.com/quickst...
软件
由于rt-thread和rt-thread studio 目前还不支持该开发板,先使用官方Nuclei Studio测试
Nuclei Studio 2022.04
CM32M4xxR-Support-Pack-v1.0.2-win32-x32.zip
新建工程 1)基于CM32M433R_START开发板新建工程
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

2)基于RT-Thread新建工程
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

3)编译测试
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

这次使用的CMlink-OpenOCD 速度很给力啊,很快就加载成功!
新建测试线程 由于官方的例程切换时暂未对FPU部分上下文处理,所以一旦有多个牵涉到浮点寄存器操作的线程,就可能导致FPU上下文不一致。
.align 2 .global eclic_msip_handler eclic_msip_handler: addi sp, sp, -RT_CONTEXT_SIZE STORE x1,1* REGBYTES(sp)/* RA */ STORE x5,2* REGBYTES(sp) STORE x6,3* REGBYTES(sp) STORE x7,4* REGBYTES(sp) STORE x8,5* REGBYTES(sp) STORE x9,6* REGBYTES(sp) STORE x10, 7* REGBYTES(sp) STORE x11, 8* REGBYTES(sp) STORE x12, 9* REGBYTES(sp) STORE x13, 10 * REGBYTES(sp) STORE x14, 11 * REGBYTES(sp) STORE x15, 12 * REGBYTES(sp) #ifndef __riscv_32e STORE x16, 13 * REGBYTES(sp) STORE x17, 14 * REGBYTES(sp) STORE x18, 15 * REGBYTES(sp) STORE x19, 16 * REGBYTES(sp) STORE x20, 17 * REGBYTES(sp) STORE x21, 18 * REGBYTES(sp) STORE x22, 19 * REGBYTES(sp) STORE x23, 20 * REGBYTES(sp) STORE x24, 21 * REGBYTES(sp) STORE x25, 22 * REGBYTES(sp) STORE x26, 23 * REGBYTES(sp) STORE x27, 24 * REGBYTES(sp) STORE x28, 25 * REGBYTES(sp) STORE x29, 26 * REGBYTES(sp) STORE x30, 27 * REGBYTES(sp) STORE x31, 28 * REGBYTES(sp) #endif /* Push mstatus to stack */ csrr t0, CSR_MSTATUS STORE t0,(RT_SAVED_REGNUM - 1)* REGBYTES(sp).../* Restore Registers from Stack */ LOAD x1,1* REGBYTES(sp)/* RA */ LOAD x5,2* REGBYTES(sp) LOAD x6,3* REGBYTES(sp) LOAD x7,4* REGBYTES(sp) LOAD x8,5* REGBYTES(sp) LOAD x9,6* REGBYTES(sp) LOAD x10, 7* REGBYTES(sp) LOAD x11, 8* REGBYTES(sp) LOAD x12, 9* REGBYTES(sp) LOAD x13, 10 * REGBYTES(sp) LOAD x14, 11 * REGBYTES(sp) LOAD x15, 12 * REGBYTES(sp) #ifndef __riscv_32e LOAD x16, 13 * REGBYTES(sp) LOAD x17, 14 * REGBYTES(sp) LOAD x18, 15 * REGBYTES(sp) LOAD x19, 16 * REGBYTES(sp) LOAD x20, 17 * REGBYTES(sp) LOAD x21, 18 * REGBYTES(sp) LOAD x22, 19 * REGBYTES(sp) LOAD x23, 20 * REGBYTES(sp) LOAD x24, 21 * REGBYTES(sp) LOAD x25, 22 * REGBYTES(sp) LOAD x26, 23 * REGBYTES(sp) LOAD x27, 24 * REGBYTES(sp) LOAD x28, 25 * REGBYTES(sp) LOAD x29, 26 * REGBYTES(sp) LOAD x30, 27 * REGBYTES(sp) LOAD x31, 28 * REGBYTES(sp) #endifaddi sp, sp, RT_CONTEXT_SIZE mret

浮点线程1
thread1里加了一个稍复杂的浮点操作
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

浮点线程2
thread2简单些
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

线程优先级
thread1和thread2有相同优先级,时间片轮询执行。
thread1 = rt_thread_create("thread1",thread1_entry, NULL, 1024, 11, 1); if(thread1 != NULL) { rt_thread_startup(thread1); } else { printf("\r\n create thread1 failed\r\n"); }thread2 = rt_thread_create("thread2",thread2_entry, NULL, 1024, 11, 1); if(thread2 != NULL) { rt_thread_startup(thread2); } else { printf("\r\n create thread2 failed\r\n"); }

运行结果
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

线程1在第一次切换后,res部分就出现了异常,稍后我们观察一下改进FPU后能否解决该问题
FPU静态保存 参考rtthread ch32v307 BSP, 使用FPU静态保存,即不判断Mstatus.FS域直接全保存FPU部分的寄存器
Stack frame
首先,修改stack_frame, 增加32个float registers f0~f31
struct rt_hw_stack_frame { rt_ubase_t epc; /*!< epc - epc- program counter*/ rt_ubase_t ra; /*!< x1- ra- return address for jumps*/ rt_ubase_t t0; /*!< x5- t0- temporary register 0*/ rt_ubase_t t1; /*!< x6- t1- temporary register 1*/ rt_ubase_t t2; /*!< x7- t2- temporary register 2*/ rt_ubase_t s0_fp; /*!< x8- s0/fp- saved register 0 or frame pointer*/ rt_ubase_t s1; /*!< x9- s1- saved register 1*/ rt_ubase_t a0; /*!< x10 - a0- return value or function argument 0 */ rt_ubase_t a1; /*!< x11 - a1- return value or function argument 1 */ rt_ubase_t a2; /*!< x12 - a2- function argument 2*/ rt_ubase_t a3; /*!< x13 - a3- function argument 3*/ rt_ubase_t a4; /*!< x14 - a4- function argument 4*/ rt_ubase_t a5; /*!< x15 - a5- function argument 5*/ #ifndef __riscv_32e rt_ubase_t a6; /*!< x16 - a6- function argument 6*/ rt_ubase_t a7; /*!< x17 - s7- function argument 7*/ rt_ubase_t s2; /*!< x18 - s2- saved register 2*/ rt_ubase_t s3; /*!< x19 - s3- saved register 3*/ rt_ubase_t s4; /*!< x20 - s4- saved register 4*/ rt_ubase_t s5; /*!< x21 - s5- saved register 5*/ rt_ubase_t s6; /*!< x22 - s6- saved register 6*/ rt_ubase_t s7; /*!< x23 - s7- saved register 7*/ rt_ubase_t s8; /*!< x24 - s8- saved register 8*/ rt_ubase_t s9; /*!< x25 - s9- saved register 9*/ rt_ubase_t s10; /*!< x26 - s10- saved register 10*/ rt_ubase_t s11; /*!< x27 - s11- saved register 11*/ rt_ubase_t t3; /*!< x28 - t3- temporary register 3*/ rt_ubase_t t4; /*!< x29 - t4- temporary register 4*/ rt_ubase_t t5; /*!< x30 - t5- temporary register 5*/ rt_ubase_t t6; /*!< x31 - t6- temporary register 6*/ #endif rt_ubase_t mstatus; /*!<- machine status register*//* float register */ #ifdef ARCH_RISCV_FPU rv_floatreg_t f0; /* f0*/ rv_floatreg_t f1; /* f1*/ rv_floatreg_t f2; /* f2*/ rv_floatreg_t f3; /* f3*/ rv_floatreg_t f4; /* f4*/ rv_floatreg_t f5; /* f5*/ rv_floatreg_t f6; /* f6*/ rv_floatreg_t f7; /* f7*/ rv_floatreg_t f8; /* f8*/ rv_floatreg_t f9; /* f9*/ rv_floatreg_t f10; /* f10 */ rv_floatreg_t f11; /* f11 */ rv_floatreg_t f12; /* f12 */ rv_floatreg_t f13; /* f13 */ rv_floatreg_t f14; /* f14 */ rv_floatreg_t f15; /* f15 */ rv_floatreg_t f16; /* f16 */ rv_floatreg_t f17; /* f17 */ rv_floatreg_t f18; /* f18 */ rv_floatreg_t f19; /* f19 */ rv_floatreg_t f20; /* f20 */ rv_floatreg_t f21; /* f21 */ rv_floatreg_t f22; /* f22 */ rv_floatreg_t f23; /* f23 */ rv_floatreg_t f24; /* f24 */ rv_floatreg_t f25; /* f25 */ rv_floatreg_t f26; /* f26 */ rv_floatreg_t f27; /* f27 */ rv_floatreg_t f28; /* f28 */ rv_floatreg_t f29; /* f29 */ rv_floatreg_t f30; /* f30 */ rv_floatreg_t f31; /* f31 */ #endif };

至于另一个 FPU控制 寄存器 fcsr,只是一些异常标志和舍入模式,暂未放进栈里
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

Stack Init
把浮点寄存器初始值均设为0
rt_uint8_t *rt_hw_stack_init(void*tentry, void*parameter, rt_uint8_t *stack_addr, void*texit) { struct rt_hw_stack_frame *frame; rt_uint8_t*stk; inti; stk= stack_addr + sizeof(rt_ubase_t); stk= (rt_uint8_t *)RT_ALIGN_DOWN((rt_ubase_t)stk, REGBYTES); stk -= sizeof(struct rt_hw_stack_frame); frame = (struct rt_hw_stack_frame *)stk; for (i = 0; i < sizeof(struct rt_hw_stack_frame) / sizeof(rt_ubase_t); i++) { if(i < 30) ((rt_ubase_t *)frame)[i] = 0xdeadbeef; else ((rv_floatreg_t *)frame)[i] = 0x00; }frame->ra= (rt_ubase_t)texit; frame->a0= (rt_ubase_t)parameter; frame->epc= (rt_ubase_t)tentry; frame->mstatus = RT_INITIAL_MSTATUS; return stk; }

Save FPU context
根据栈帧顺序,在eclic_msip_handler 中断函数进入时先保存浮点寄存器组
eclic_msip_handler: #ifdef ARCH_RISCV_FPU addi sp, sp, -32*FREGBYTESFSTOREf0, 0 * FREGBYTES(sp) FSTOREf1, 1 * FREGBYTES(sp) FSTOREf2, 2 * FREGBYTES(sp) FSTOREf3, 3 * FREGBYTES(sp) FSTOREf4, 4 * FREGBYTES(sp) FSTOREf5, 5 * FREGBYTES(sp) FSTOREf6, 6 * FREGBYTES(sp) FSTOREf7, 7 * FREGBYTES(sp) FSTOREf8, 8 * FREGBYTES(sp) FSTOREf9, 9 * FREGBYTES(sp) FSTOREf10, 10 * FREGBYTES(sp) FSTOREf11, 11 * FREGBYTES(sp) FSTOREf12, 12 * FREGBYTES(sp) FSTOREf13, 13 * FREGBYTES(sp) FSTOREf14, 14 * FREGBYTES(sp) FSTOREf15, 15 * FREGBYTES(sp) FSTOREf16, 16 * FREGBYTES(sp) FSTOREf17, 17 * FREGBYTES(sp) FSTOREf18, 18 * FREGBYTES(sp) FSTOREf19, 19 * FREGBYTES(sp) FSTOREf20, 20 * FREGBYTES(sp) FSTOREf21, 21 * FREGBYTES(sp) FSTOREf22, 22 * FREGBYTES(sp) FSTOREf23, 23 * FREGBYTES(sp) FSTOREf24, 24 * FREGBYTES(sp) FSTOREf25, 25 * FREGBYTES(sp) FSTOREf26, 26 * FREGBYTES(sp) FSTOREf27, 27 * FREGBYTES(sp) FSTOREf28, 28 * FREGBYTES(sp) FSTOREf29, 29 * FREGBYTES(sp) FSTOREf30, 30 * FREGBYTES(sp) FSTOREf31, 31 * FREGBYTES(sp) #endif addi sp, sp, -RT_CONTEXT_SIZE STORE x1,1* REGBYTES(sp)/* RA */ STORE x5,2* REGBYTES(sp) STORE x6,3* REGBYTES(sp) STORE x7,4* REGBYTES(sp) .....

Restore FPU Context
Restore in rt_hw_context_switch_to
rt_hw_context_switch_to: /* Setup Interrupt Stack using The stack that was used by main() before the scheduler is started is no longer required after the scheduler is started. Interrupt stack pointer is stored in CSR_MSCRATCH */ la t0, _sp csrw CSR_MSCRATCH, t0 LOAD sp, 0x0(a0)/* Read sp from first TCB member(a0) *//* Pop PC from stack and set MEPC */ LOAD t0,0* REGBYTES(sp) csrw CSR_MEPC, t0 /* Pop mstatus from stack and set it */ LOAD t0,(RT_SAVED_REGNUM - 1)* REGBYTES(sp) csrw CSR_MSTATUS, t0 /* Interrupt still disable here */ /* Restore Registers from Stack */ LOAD x1,1* REGBYTES(sp)/* RA */ LOAD x5,2* REGBYTES(sp) LOAD x6,3* REGBYTES(sp) LOAD x7,4* REGBYTES(sp) LOAD x8,5* REGBYTES(sp) LOAD x9,6* REGBYTES(sp) LOAD x10, 7* REGBYTES(sp) LOAD x11, 8* REGBYTES(sp) LOAD x12, 9* REGBYTES(sp) LOAD x13, 10 * REGBYTES(sp) LOAD x14, 11 * REGBYTES(sp) LOAD x15, 12 * REGBYTES(sp) #ifndef __riscv_32e LOAD x16, 13 * REGBYTES(sp) LOAD x17, 14 * REGBYTES(sp) LOAD x18, 15 * REGBYTES(sp) LOAD x19, 16 * REGBYTES(sp) LOAD x20, 17 * REGBYTES(sp) LOAD x21, 18 * REGBYTES(sp) LOAD x22, 19 * REGBYTES(sp) LOAD x23, 20 * REGBYTES(sp) LOAD x24, 21 * REGBYTES(sp) LOAD x25, 22 * REGBYTES(sp) LOAD x26, 23 * REGBYTES(sp) LOAD x27, 24 * REGBYTES(sp) LOAD x28, 25 * REGBYTES(sp) LOAD x29, 26 * REGBYTES(sp) LOAD x30, 27 * REGBYTES(sp) LOAD x31, 28 * REGBYTES(sp) #endifaddi sp, sp, RT_CONTEXT_SIZE /* load float reg */ #ifdef ARCH_RISCV_FPUFLOADf0, 0 * FREGBYTES(sp) FLOADf1, 1 * FREGBYTES(sp) FLOADf2, 2 * FREGBYTES(sp) FLOADf3, 3 * FREGBYTES(sp) FLOADf4, 4 * FREGBYTES(sp) FLOADf5, 5 * FREGBYTES(sp) FLOADf6, 6 * FREGBYTES(sp) FLOADf7, 7 * FREGBYTES(sp) FLOADf8, 8 * FREGBYTES(sp) FLOADf9, 9 * FREGBYTES(sp) FLOADf10, 10 * FREGBYTES(sp) FLOADf11, 11 * FREGBYTES(sp) FLOADf12, 12 * FREGBYTES(sp) FLOADf13, 13 * FREGBYTES(sp) FLOADf14, 14 * FREGBYTES(sp) FLOADf15, 15 * FREGBYTES(sp) FLOADf16, 16 * FREGBYTES(sp) FLOADf17, 17 * FREGBYTES(sp) FLOADf18, 18 * FREGBYTES(sp) FLOADf19, 19 * FREGBYTES(sp) FLOADf20, 20 * FREGBYTES(sp) FLOADf21, 21 * FREGBYTES(sp) FLOADf22, 22 * FREGBYTES(sp) FLOADf23, 23 * FREGBYTES(sp) FLOADf24, 24 * FREGBYTES(sp) FLOADf25, 25 * FREGBYTES(sp) FLOADf26, 26 * FREGBYTES(sp) FLOADf27, 27 * FREGBYTES(sp) FLOADf28, 28 * FREGBYTES(sp) FLOADf29, 29 * FREGBYTES(sp) FLOADf30, 30 * FREGBYTES(sp) FLOADf31, 31 * FREGBYTES(sp) addisp, sp, 32 * FREGBYTES #endif mret

Restore in eclic_msip_handler
eclic_msip_handler: ..../* Pop additional registers *//* Pop mstatus from stack and set it */ LOAD t0,(RT_SAVED_REGNUM - 1)* REGBYTES(sp) csrw CSR_MSTATUS, t0 /* Interrupt still disable here */ /* Restore Registers from Stack */ LOAD x1,1* REGBYTES(sp)/* RA */ LOAD x5,2* REGBYTES(sp) LOAD x6,3* REGBYTES(sp) LOAD x7,4* REGBYTES(sp) LOAD x8,5* REGBYTES(sp) LOAD x9,6* REGBYTES(sp) LOAD x10, 7* REGBYTES(sp) LOAD x11, 8* REGBYTES(sp) LOAD x12, 9* REGBYTES(sp) LOAD x13, 10 * REGBYTES(sp) LOAD x14, 11 * REGBYTES(sp) LOAD x15, 12 * REGBYTES(sp) #ifndef __riscv_32e LOAD x16, 13 * REGBYTES(sp) LOAD x17, 14 * REGBYTES(sp) LOAD x18, 15 * REGBYTES(sp) LOAD x19, 16 * REGBYTES(sp) LOAD x20, 17 * REGBYTES(sp) LOAD x21, 18 * REGBYTES(sp) LOAD x22, 19 * REGBYTES(sp) LOAD x23, 20 * REGBYTES(sp) LOAD x24, 21 * REGBYTES(sp) LOAD x25, 22 * REGBYTES(sp) LOAD x26, 23 * REGBYTES(sp) LOAD x27, 24 * REGBYTES(sp) LOAD x28, 25 * REGBYTES(sp) LOAD x29, 26 * REGBYTES(sp) LOAD x30, 27 * REGBYTES(sp) LOAD x31, 28 * REGBYTES(sp) #endifaddi sp, sp, RT_CONTEXT_SIZE /* load float reg */ #ifdef ARCH_RISCV_FPUFLOADf0, 0 * FREGBYTES(sp) FLOADf1, 1 * FREGBYTES(sp) FLOADf2, 2 * FREGBYTES(sp) FLOADf3, 3 * FREGBYTES(sp) FLOADf4, 4 * FREGBYTES(sp) FLOADf5, 5 * FREGBYTES(sp) FLOADf6, 6 * FREGBYTES(sp) FLOADf7, 7 * FREGBYTES(sp) FLOADf8, 8 * FREGBYTES(sp) FLOADf9, 9 * FREGBYTES(sp) FLOADf10, 10 * FREGBYTES(sp) FLOADf11, 11 * FREGBYTES(sp) FLOADf12, 12 * FREGBYTES(sp) FLOADf13, 13 * FREGBYTES(sp) FLOADf14, 14 * FREGBYTES(sp) FLOADf15, 15 * FREGBYTES(sp) FLOADf16, 16 * FREGBYTES(sp) FLOADf17, 17 * FREGBYTES(sp) FLOADf18, 18 * FREGBYTES(sp) FLOADf19, 19 * FREGBYTES(sp) FLOADf20, 20 * FREGBYTES(sp) FLOADf21, 21 * FREGBYTES(sp) FLOADf22, 22 * FREGBYTES(sp) FLOADf23, 23 * FREGBYTES(sp) FLOADf24, 24 * FREGBYTES(sp) FLOADf25, 25 * FREGBYTES(sp) FLOADf26, 26 * FREGBYTES(sp) FLOADf27, 27 * FREGBYTES(sp) FLOADf28, 28 * FREGBYTES(sp) FLOADf29, 29 * FREGBYTES(sp) FLOADf30, 30 * FREGBYTES(sp) FLOADf31, 31 * FREGBYTES(sp) addisp, sp, 32 * FREGBYTES #endif mret

测试
同样的线程,运行后不会出现问题了
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

FPU动态保存 虽然当前的FPU静态保存方案可以解决FPU上下文问题,但是代价还是太大
一旦使能了FPU, 无论线程是否使用FPU,上下文切换时均会save , restore 32个 float registers
所以还是希望可以像ARM那样根据当前程序状态来决定是否保存浮点寄存器组,如下面PendSV:
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

在《riscv-privileged-20211203.pdf》文档 P26 中发现了mstatus FS域的定义。我们完全可以根据FS是否是Dirty状态,来决定是否保存寄存器组,这样只在需要的时候,保存额外的32的浮点寄存器,平时可以省下很多时间,提高系统切换效率。
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

Zephyr OS参考
目前只发现Zephyr OShttps://github.com/zephyrproj...
有代码似乎在做动态保存FPU的事情:
save:
#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) /* Assess whether floating-point registers need to be saved. */ lb t0, _thread_offset_to_user_options(a1) andi t0, t0, K_FP_REGS beqz t0, skip_store_fp_callee_savedfrcsr t0 sw t0, _thread_offset_to_fcsr(a1) DO_FP_CALLEE_SAVED(fsr, a1) skip_store_fp_callee_saved: #endif /* CONFIG_FPU && CONFIG_FPU_SHARING */

load:
#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) /* Determine if we need to restore floating-point registers. */ lb t0, _thread_offset_to_user_options(a0) li t1, MSTATUS_FS_INIT andi t0, t0, K_FP_REGS beqz t0, no_fp/* Enable floating point access */ csrs mstatus, t1/* Restore FP regs */ lw t1, _thread_offset_to_fcsr(a0) fscsr t1 DO_FP_CALLEE_SAVED(flr, a0) j 1fno_fp: /* Disable floating point access */ csrc mstatus, t1 1: #endif /* CONFIG_FPU && CONFIG_FPU_SHARING */

review代码发现 其实还是根据 k_thread->base->user_options来直接判断的
/* can be used for creating 'dummy' threads, e.g. for pending on objects */ struct _thread_base {/* this thread's entry in a ready/wait queue */ union { sys_dnode_t qnode_dlist; struct rbnode qnode_rb; }; /* wait queue on which the thread is pended (needed only for * trees, not dumb lists) */ _wait_q_t *pended_on; /* user facing 'thread options'; values defined in include/kernel.h */ uint8_t user_options; /* thread state */ uint8_t thread_state; ...

kernel.h的说明
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

FPU的测试代码
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

总结:
  1. Zephyr配置里有个CONFIG_FPU_SHARING,全局开关FPU
  2. 创建任务时,可以选择是否使用CPU的float registers
  3. 选择 K_FP_REGS后,调度时会增加额外的步骤去保存和恢复浮点寄存器上下文!
    RISC-V架构下 FPU Context 的动态保存和恢复
    文章图片
Zephyr这样处理,就需要任务创建时非常小心使用thread_option选项,如果未使能 K_FP_REGS 的任务中又涉及到浮点的操作,就很可能导致浮点上下文异常。
因为编译器是全局设置,一旦开启 硬件FPU, 发现了浮点运算,就可能使用浮点指令的(有时会使用lib库优化,不使用),它不会为你的失误买单。
mstatus.fs
虽然Zephyr的动态保存不是很完美,但也给我们提供了一个很好的参考,下面将根据mstatus.fs实现动态FPU保存与恢复
再次review 《riscv-privileged-20211203.pdf》文档,Table3.4 给出了FPU上下文在特权模式下的根据 mstatus.fs域保存和恢复的动作建议
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

P27 页有段很重要的解释说明
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

Context Save:
  • Dirty状态 : 保存FPU registers并在保存后切换到Clean状态,
  • Off,Init, Clean状态 : 均不需要保存FPU registers,同时状态保持不变
Context Restore:
  • Off 状态 :无动作
  • Init 状态 :可直接加载立即数0到FPU registers ,无需Memory加载访问
  • Clean 状态:从保持的内存栈中恢复
  • Dirty 状态 :在Context Save已经切到Clean状态了,所以不存在该状态
关于init状态的非memory 访问 和立即数问题
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

  1. 首先RV32F指令操作的rd ,rs1, rs2大部分均是浮点寄存器
  2. 浮点寄存器的直接赋值只有一个flw指令 : flw rd offset[11:0] (rs1)
    从下面的汇编代码可以看出浮点线程是很费线程栈的,寄存器的加载必须从内存中读取,不能使用立即数。
    RISC-V架构下 FPU Context 的动态保存和恢复
    文章图片

  3. 可以使用fmv.w.x ftx, zero 搬运指令完成,无需访问Memory
Mstatus.fs与FPU 上下文的疑惑
restore FPU Context(init) 按照riscv-privileged文档,rt_hw_context_switch_to需要init FPU registers
load前
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

load 后
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

可以看到一执行flw指令,mstaus.fs立即从 init 变成了 dirty状态,这显然是不对的:
只是把FPU registers 初始化为0, mstatus.fs就变成了 dirty, 线程里即使不涉及浮点操作,进入时就dirty了,天大的冤枉啊!
目前测试发现,只要执行浮点指令写 FPU registers(含 fscr),mstatus.fs就会变成dirty(off初始状态除外),所以riscv-privileged里restore context后的状态应该都是dirty。如果想继续保持init ,clean 状态就需要手动恢复一下
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

这样就有很多状态和特权文档有出入,所以打算基于实测结果简化代码
Init状态: 个人认为是不需要保存和恢复的,这样不会改变fs的状态.
dirty线程--->init线程(还未执行到浮点部分的线程或者就是个普通线程):
init线程无任何restore FPU动作,mstatus.fs保持在init状态,尽管 FPU registers残留了上一个浮点线程的现场,也不会有任何影响:
  • 本次执行碰到了浮点写入,编译器根据调用者原则,也会入栈,并且加载新的value到 FPU registers, mstatus.fs变成dirty
  • 本次未执行碰到了浮点写入,mstatus.fs保持init状态
init线程--->dirty线程:
切换过程中会restore dirty线程的 FPU registers
Clean 状态: 这个状态字面意思很好理解: 当前线程使用过FPU, 里面FPU registers部分已经被使用过了,但是使用的浮点寄存器生命周期已经结束了,FPU部分可以算是干净的状态,和init的差不多,只是FPU registers不是初始常量了。
但是测试中满足这些code都触发不了:
1) 线程主循环外调用浮点指令,主循环不涉及浮点指令
2) 线程调用浮点函数,本身不牵涉到浮点指令
测试中发现当前risc-v的 mstatus.fs是和 FPU registes写关联的,一旦write FPU registers(包含fscr), 均会导致 mstatus.fs 变成dirty(read 无影响), 这样硬件实现起来最简单。
问题来了,怎么知道浮点寄存器生命周期已经结束,编译器就是干这个的,它肯定知道,实现起来也简单
1) 线程主循环外调用浮点指令 这种情况编译器一般无能为力,它不能预测下面的指令。
2) 线程调用浮点函数,每一个牵涉到浮点指令的函数相当于一个临界区,临界区内继承mstatus.fs,只有在函数返回时恢复之前的mstatus.fs
其实直接在函数进入或者碰到浮点指令时压栈mstatus就行了,问题又来了
1) 如果调用深度过深,会浪费不少线程栈,ret前要判断一次,不太简洁。
2)最主要的是,如果线程处于用户模式,是无法访问 mstatus CSR 寄存器的。
或许基于上面原因,编译器并不会帮你保存恢复mstatus.fs状态,mstatus.fs的改变是纯指令触发的硬件行为。如果后期实现,估计也是通过ret指令触发的硬件行为。
所以这个clean 状态和让人疑惑,不排除我们目前没有抓到clean的特殊状态
Dirty状态: 基于前面的测试, 知道线程一旦写FPU registers就会触发dirty,然后一直保持该状态.
按照riscv-privileged Table 3.4 的描述,如果我们有一个浮点线程A,现在已经时dirty状态,正在执行:
时间点1:被抢占,切换到另一个线程,save FPU context后,先手动切换mstatus.fs = clean,
时间点2:发生调度, 浮点线程A优先级最高,restore FPU context后 , mstatus.fs 从clean变成了dirty, 再次手动切换mstatus.fs = clean
时间点3:浮点线程A刚切换过来,刚执行了非 FPU写几条指令 , mstatus.fs保持在clean 状态,又一个高优先级线程就绪,再次发生调度
? 如果遵循riscv-privileged Table 3.4,mstatus.fs = clean是不需要save FPU context。
出现的问题:
1)需要多次手动改变mstatus.fs = clean。恢复前后mstatus.fs保持一致的dirty,没啥问题啊,还符合逻辑,也不会出现问题。
2)时间点3发生的调度,应该save FPU context,此时和时间1比较只是多执行了几条指令,无法保证下文不会继续出现FPU访问
? 直接不保存很可能会导致FPU运算异常。
? 同样的道理, 如果存在Clean(None dirty, some clean)这个状态,它也应该被保存。
综上,如果尽量按照risc-v spec 文档,应该如下
方案一(手动切换状态)
current mstaus.fs off init clean dirty
save context NO NO Yes Yes
after save context off init clean clean
(switch to clean from dirty)
restore context NO NO Yes /
after save context off init clean
(switch to clean from dirty)
/
方案二(保留dirty 状态)
current mstaus.fs off init clean dirty
save context NO NO Yes Yes
after save context off init clean dirty
restore context NO NO Yes Yes
after save context off init dirty dirty
这两种方案我都测试过,均可正常运行, 后面会讲。
豁然开朗
如果clean就是一种软件定义呢 虽然两种方案都正常,暂未发现问题,但是总感觉怪怪的,按理说一个官方文档,不应该有这么明显的错误,网上看了一下也有网友对此疑问
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

百思不得其解,今天冲澡的时候豁然开朗:
如果这个clean 就是dirty 保存后的定义的一种的软件定义状态呢,毕竟在 STM32F407上也没有这个状态,也是只要有改变 FPU registers的行为均会触发 FPU Used(相当于dirty),然后一直保持。
再次参考 Zephyr OS 至于 clean 状态下的未保存 FPU context 导致的问题,参考 Zephyr OS的做法就没有任何问题,先看下它保存和恢复上下文的做法
重要的宏 kernel/include/gen_offset.h
* The macro "GEN_OFFSET_SYM(structure, member)" is used to generate a single * absolute symbol.The absolute symbol will appear in the object module * generated from the source file that utilizes the GEN_OFFSET_SYM() macro. * Absolute symbols representing a structure member offset have the following * form: * *____OFFSET * * The macro "GEN_NAMED_OFFSET_SYM(structure, member, name)" is also provided * to create the symbol with the following form: * *____OFFSET * * This header also defines the GEN_ABSOLUTE_SYM macro to simply define an * absolute symbol, irrespective of whether the value represents a structure * or offset.#ifndef ZEPHYR_KERNEL_INCLUDE_GEN_OFFSET_H_ #define ZEPHYR_KERNEL_INCLUDE_GEN_OFFSET_H_#include #include /* definition of the GEN_OFFSET_SYM() macros is toolchain independent*/#define GEN_OFFSET_SYM(S, M) \ GEN_ABSOLUTE_SYM(__##S##_##M##_##OFFSET, offsetof(S, M))#define GEN_NAMED_OFFSET_SYM(S, M, N) \ GEN_ABSOLUTE_SYM(__##S##_##N##_##OFFSET, offsetof(S, M))#endif /* ZEPHYR_KERNEL_INCLUDE_GEN_OFFSET_H_ */

GEN_OFFSET_SYM和GEN_NAMED_OFFSET_SYM宏会使用汇编语言定义一个__##S##_##M##_##OFFSET 和 _##S##_##N##_##OFFSET的两个symbol,声明后就可以在源文件中直接使用他们获取成员变量M相对与结构体S的偏移量 , 下面会多次出现,这个和rt_container_of一样重要。
riscv下的 异常caller栈z_arch_esf_t include/zephyr/arch/riscv/exp.h
struct __esf { ulong_t ra; /* return address */ulong_t t0; /* Caller-saved temporary register */ ulong_t t1; /* Caller-saved temporary register */ ulong_t t2; /* Caller-saved temporary register */ ulong_t t3; /* Caller-saved temporary register */ ulong_t t4; /* Caller-saved temporary register */ ulong_t t5; /* Caller-saved temporary register */ ulong_t t6; /* Caller-saved temporary register */ulong_t a0; /* function argument/return value */ ulong_t a1; /* function argument */ ulong_t a2; /* function argument */ ulong_t a3; /* function argument */ ulong_t a4; /* function argument */ ulong_t a5; /* function argument */ ulong_t a6; /* function argument */ ulong_t a7; /* function argument */ulong_t mepc; /* machine exception program counter */ ulong_t mstatus; /* machine status register */ulong_t s0; /* callee-saved s0 */#ifdef CONFIG_USERSPACE ulong_t sp; /* preserved (user or kernel) stack pointer */ #endif#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) RV_FP_TYPE ft0; /* Caller-saved temporary floating register */ RV_FP_TYPE ft1; /* Caller-saved temporary floating register */ RV_FP_TYPE ft2; /* Caller-saved temporary floating register */ RV_FP_TYPE ft3; /* Caller-saved temporary floating register */ RV_FP_TYPE ft4; /* Caller-saved temporary floating register */ RV_FP_TYPE ft5; /* Caller-saved temporary floating register */ RV_FP_TYPE ft6; /* Caller-saved temporary floating register */ RV_FP_TYPE ft7; /* Caller-saved temporary floating register */ RV_FP_TYPE ft8; /* Caller-saved temporary floating register */ RV_FP_TYPE ft9; /* Caller-saved temporary floating register */ RV_FP_TYPE ft10; /* Caller-saved temporary floating register */ RV_FP_TYPE ft11; /* Caller-saved temporary floating register */ RV_FP_TYPE fa0; /* function argument/return value */ RV_FP_TYPE fa1; /* function argument/return value */ RV_FP_TYPE fa2; /* function argument */ RV_FP_TYPE fa3; /* function argument */ RV_FP_TYPE fa4; /* function argument */ RV_FP_TYPE fa5; /* function argument */ RV_FP_TYPE fa6; /* function argument */ RV_FP_TYPE fa7; /* function argument */ #endif#ifdef CONFIG_RISCV_SOC_CONTEXT_SAVE struct soc_esf soc_context; #endif } __aligned(16); typedef struct __esf z_arch_esf_t;

Caller Registers的保存 __z_arch_esf_t_xxx_OFFSET 就是 z_arch_esf_t的成员xxx的相对于z_arch_esf_t基地址的偏移。DO_FP_CALLER_SAVED单独保存FPU context
arch/riscv/core/isr.S(下面的代码也在该文件中)
#define DO_FP_CALLER_SAVED(op, reg) \ op ft0, __z_arch_esf_t_ft0_OFFSET(reg); \ op ft1, __z_arch_esf_t_ft1_OFFSET(reg); \ op ft2, __z_arch_esf_t_ft2_OFFSET(reg); \ op ft3, __z_arch_esf_t_ft3_OFFSET(reg); \ op ft4, __z_arch_esf_t_ft4_OFFSET(reg); \ op ft5, __z_arch_esf_t_ft5_OFFSET(reg); \ op ft6, __z_arch_esf_t_ft6_OFFSET(reg); \ op ft7, __z_arch_esf_t_ft7_OFFSET(reg); \ op ft8, __z_arch_esf_t_ft8_OFFSET(reg); \ op ft9, __z_arch_esf_t_ft9_OFFSET(reg); \ op ft10, __z_arch_esf_t_ft10_OFFSET(reg) ; \ op ft11, __z_arch_esf_t_ft11_OFFSET(reg) ; \ op fa0, __z_arch_esf_t_fa0_OFFSET(reg); \ op fa1, __z_arch_esf_t_fa1_OFFSET(reg); \ op fa2, __z_arch_esf_t_fa2_OFFSET(reg); \ op fa3, __z_arch_esf_t_fa3_OFFSET(reg); \ op fa4, __z_arch_esf_t_fa4_OFFSET(reg); \ op fa5, __z_arch_esf_t_fa5_OFFSET(reg); \ op fa6, __z_arch_esf_t_fa6_OFFSET(reg); \ op fa7, __z_arch_esf_t_fa7_OFFSET(reg); #define DO_CALLER_SAVED_T0T1(op) \ op t0, __z_arch_esf_t_t0_OFFSET(sp); \ op t1, __z_arch_esf_t_t1_OFFSET(sp)#define DO_CALLER_SAVED_REST(op) \ op t2, __z_arch_esf_t_t2_OFFSET(sp); \ op t3, __z_arch_esf_t_t3_OFFSET(sp); \ op t4, __z_arch_esf_t_t4_OFFSET(sp); \ op t5, __z_arch_esf_t_t5_OFFSET(sp); \ op t6, __z_arch_esf_t_t6_OFFSET(sp); \ op a0, __z_arch_esf_t_a0_OFFSET(sp); \ op a1, __z_arch_esf_t_a1_OFFSET(sp); \ op a2, __z_arch_esf_t_a2_OFFSET(sp); \ op a3, __z_arch_esf_t_a3_OFFSET(sp); \ op a4, __z_arch_esf_t_a4_OFFSET(sp); \ op a5, __z_arch_esf_t_a5_OFFSET(sp); \ op a6, __z_arch_esf_t_a6_OFFSET(sp); \ op a7, __z_arch_esf_t_a7_OFFSET(sp); \ op ra, __z_arch_esf_t_ra_OFFSET(sp)

__irq_wrapper 中断函数 该中断处理包含了exception/interrupt/fault,比较复杂,我们只关心用于任务切换的system call 部分
/* * Handler called upon each exception/interrupt/fault * In this architecture, system call (ECALL) is used to perform context * switching or IRQ offloading (when enabled). */ SECTION_FUNC(exception.entry, __irq_wrapper)

1)z_arch_esf_t 的保存不区分中断源,但 FPU的caller在mstatus.fs 非零(未关闭)下才保存
/* Save caller-saved registers on current thread stack. */ addi sp, sp, -__z_arch_esf_t_SIZEOF DO_CALLER_SAVED_T0T1(sr); 3:DO_CALLER_SAVED_REST(sr); /* Save s0 in the esf and load it with &_current_cpu. */ sr s0, __z_arch_esf_t_s0_OFFSET(sp) GET_CURRENT_CPU(s0, t0)#ifdef CONFIG_USERSPACE /* * The scratch register now contains either the user mode stack * pointer, or 0 if entered from kernel mode. Retrieve that value * and zero the scratch register as we are in kernel mode now. */ csrrw t0, mscratch, zero bnez t0, 1f/* came from kernel mode: adjust stack value */ add t0, sp, __z_arch_esf_t_SIZEOF 1: /* save stack value to be restored later */ sr t0, __z_arch_esf_t_sp_OFFSET(sp)#if !defined(CONFIG_SMP) /* Clear user mode variable */ la t0, is_user_mode sw zero, 0(t0) #endif #endif/* Save MEPC register */ csrr t0, mepc sr t0, __z_arch_esf_t_mepc_OFFSET(sp)/* Save MSTATUS register */ csrr t4, mstatus sr t4, __z_arch_esf_t_mstatus_OFFSET(sp)#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) /* Assess whether floating-point registers need to be saved. */ li t1, MSTATUS_FS_INIT and t0, t4, t1 beqz t0, skip_store_fp_caller_saved DO_FP_CALLER_SAVED(fsr, sp) skip_store_fp_caller_saved: #endif /* CONFIG_FPU && CONFIG_FPU_SHARING */

2)z_riscv_switch 用于caller register的save , restore
reschedule:/* Get pointer to current thread on this CPU */ lr a1, ___cpu_t_current_OFFSET(s0)/* * Get next thread to schedule with z_get_next_switch_handle(). * We pass it a NULL as we didn't save the whole thread context yet. * If no scheduling is necessary then NULL will be returned. */ addi sp, sp, -16 sr a1, 0(sp) mv a0, zero call z_get_next_switch_handle lr a1, 0(sp) addi sp, sp, 16 beqz a0, no_reschedule/* * Perform context switch: * a0 = new thread * a1 = old thread */ call z_riscv_switch

z_riscv_switch之前说过了,根据_thread_t 的user_options是否使能了K_FP_REGS,决定是否保存 FPU caller部分
/* Convenience macros for loading/storing register states. */#define DO_CALLEE_SAVED(op, reg) \ op ra, _thread_offset_to_ra(reg); \ op tp, _thread_offset_to_tp(reg); \ op s0, _thread_offset_to_s0(reg); \ op s1, _thread_offset_to_s1(reg); \ op s2, _thread_offset_to_s2(reg); \ op s3, _thread_offset_to_s3(reg); \ op s4, _thread_offset_to_s4(reg); \ op s5, _thread_offset_to_s5(reg); \ op s6, _thread_offset_to_s6(reg); \ op s7, _thread_offset_to_s7(reg); \ op s8, _thread_offset_to_s8(reg); \ op s9, _thread_offset_to_s9(reg); \ op s10, _thread_offset_to_s10(reg); \ op s11, _thread_offset_to_s11(reg)#define DO_FP_CALLEE_SAVED(op, reg) \ op fs0, _thread_offset_to_fs0(reg); \ op fs1, _thread_offset_to_fs1(reg); \ op fs2, _thread_offset_to_fs2(reg); \ op fs3, _thread_offset_to_fs3(reg); \ op fs4, _thread_offset_to_fs4(reg); \ op fs5, _thread_offset_to_fs5(reg); \ op fs6, _thread_offset_to_fs6(reg); \ op fs7, _thread_offset_to_fs7(reg); \ op fs8, _thread_offset_to_fs8(reg); \ op fs9, _thread_offset_to_fs9(reg); \ op fs10, _thread_offset_to_fs10(reg); \ op fs11, _thread_offset_to_fs11(reg)GTEXT(z_riscv_switch) GTEXT(z_thread_mark_switched_in) GTEXT(z_riscv_configure_stack_guard)/* void z_riscv_switch(k_thread_t *switch_to, k_thread_t *switch_from) */ SECTION_FUNC(TEXT, z_riscv_switch)/* Save the old thread's callee-saved registers */ DO_CALLEE_SAVED(sr, a1)#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) /* Assess whether floating-point registers need to be saved. */ lb t0, _thread_offset_to_user_options(a1) andi t0, t0, K_FP_REGS beqz t0, skip_store_fp_callee_savedfrcsr t0 sw t0, _thread_offset_to_fcsr(a1) DO_FP_CALLEE_SAVED(fsr, a1) skip_store_fp_callee_saved: #endif /* CONFIG_FPU && CONFIG_FPU_SHARING *//* Save the old thread's stack pointer */ sr sp, _thread_offset_to_sp(a1)/* Set thread->switch_handle = thread to mark completion */ sr a1, ___thread_t_switch_handle_OFFSET(a1)/* Get the new thread's stack pointer */ lr sp, _thread_offset_to_sp(a0)#ifdef CONFIG_PMP_STACK_GUARD /* Preserve a0 across following call. s0 is not yet restored. */ mv s0, a0 call z_riscv_configure_stack_guard mv a0, s0 #endif#ifdef CONFIG_USERSPACE lb t0, _thread_offset_to_user_options(a0) andi t0, t0, K_USER beqz t0, not_user_task mv s0, a0 call z_riscv_configure_user_allowed_stack mv a0, s0 not_user_task: #endif#if CONFIG_INSTRUMENT_THREAD_SWITCHING mv s0, a0 call z_thread_mark_switched_in mv a0, s0 #endif/* Restore the new thread's callee-saved registers */ DO_CALLEE_SAVED(lr, a0)#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) /* Determine if we need to restore floating-point registers. */ lb t0, _thread_offset_to_user_options(a0) li t1, MSTATUS_FS_INIT andi t0, t0, K_FP_REGS beqz t0, no_fp/* Enable floating point access */ csrs mstatus, t1/* Restore FP regs */ lw t1, _thread_offset_to_fcsr(a0) fscsr t1 DO_FP_CALLEE_SAVED(flr, a0) j 1fno_fp: /* Disable floating point access */ csrc mstatus, t1 1: #endif /* CONFIG_FPU && CONFIG_FPU_SHARING */ret

_thread_t 会包含了一个struct _callee_saved 的成员 ,
struct _callee_saved { ulong_t sp; /* Stack pointer, (x2 register) */ ulong_t ra; /* return address */ ulong_t tp; /* thread pointer */ulong_t s0; /* saved register/frame pointer */ ulong_t s1; /* saved register */ ulong_t s2; /* saved register */ ulong_t s3; /* saved register */ ulong_t s4; /* saved register */ ulong_t s5; /* saved register */ ulong_t s6; /* saved register */ ulong_t s7; /* saved register */ ulong_t s8; /* saved register */ ulong_t s9; /* saved register */ ulong_t s10; /* saved register */ ulong_t s11; /* saved register */#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) uint32_t fcsr; /* Control and status register */ RV_FP_TYPE fs0; /* saved floating-point register */ RV_FP_TYPE fs1; /* saved floating-point register */ RV_FP_TYPE fs2; /* saved floating-point register */ RV_FP_TYPE fs3; /* saved floating-point register */ RV_FP_TYPE fs4; /* saved floating-point register */ RV_FP_TYPE fs5; /* saved floating-point register */ RV_FP_TYPE fs6; /* saved floating-point register */ RV_FP_TYPE fs7; /* saved floating-point register */ RV_FP_TYPE fs8; /* saved floating-point register */ RV_FP_TYPE fs9; /* saved floating-point register */ RV_FP_TYPE fs10; /* saved floating-point register */ RV_FP_TYPE fs11; /* saved floating-point register */ #endif };

3)z_arch_esf_t 的恢复
FPU的打开的情况下,恢复 caller FPU context,
#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) /* * Determine if we need to restore FP regs based on the previous * (before the csr above) mstatus value available in t5. */ li t1, MSTATUS_FS_INIT and t0, t5, t1 beqz t0, no_fp/* make sure FP is enabled in the restored mstatus */ csrs mstatus, t1 DO_FP_CALLER_SAVED(flr, sp) j 1fno_fp:/* make sure this is reflected in the restored mstatus */ csrc mstatus, t1 1: #endif /* CONFIG_FPU && CONFIG_FPU_SHARING */#ifdef CONFIG_USERSPACE /* * Check if we are returning to user mode. If so then we must * set is_user_mode to true and load the scratch register with * the stack pointer to be used with the next exception to come. */ li t1, MSTATUS_MPP and t0, t4, t1 bnez t0, 1f#if !defined(CONFIG_SMP) /* Set user mode variable */ li t0, 1 la t1, is_user_mode sw t0, 0(t1) #endif/* load scratch reg with stack pointer for next exception entry */ add t0, sp, __z_arch_esf_t_SIZEOF csrw mscratch, t0 1: #endif/* Restore s0 (it is no longer ours) */ lr s0, __z_arch_esf_t_s0_OFFSET(sp)/* Restore caller-saved registers from thread stack */ DO_CALLER_SAVED_T0T1(lr) DO_CALLER_SAVED_REST(lr)#ifdef CONFIG_USERSPACE /* retrieve saved stack pointer */ lr sp, __z_arch_esf_t_sp_OFFSET(sp) #else /* remove esf from the stack */ addi sp, sp, __z_arch_esf_t_SIZEOF #endif/* Call SOC_ERET to exit ISR */ SOC_ERET

总结一下 Zephyr OS上下文的保存是基于结构体实现的(caller, callee均包含了FPU部分),这样可以不关心寄存器的先后问题。Callee部分甚至直接定义在了_thread_t中,静态分配,这就给 clean 这个状态,提供了不保存的方法。简单概括就是,提前分配好FPU registers的空间,可以根据mstatus.fs决定是否使用。
当然 Zephyr OS还是根据 FPU的开和关来决定是否处理 FPU上下文,并没有涉及具体状态的细分。但是参考其架构,是很容易优化的,比如:
一个合理的浮点线程调度流程:
  1. 刚进入的浮点线程,fs = init, 浮点寄存器发生变动后,fs =dirty
  2. 处于dirty状态线程发生了调度,保存后,手动切换到clean状态
  3. 下次切换到该线程时,发现时clean状态,从thread_t->callee_saved中直接加载,然后手动切换到 clean 状态
  4. 线程执行中不存在浮点寄存器的写操作,即保持在clean 状态,下次切换时就不需要保存更新 thread_t->callee_saved的FPU部分
  5. 线程执行中存在浮点寄存器的写操作,状态变成dirty, 再次回到2
定义clean状态的好处主要体现在4上,未发生浮点寄存器的改变,就不需要再一次保存 FPU register,下次加载时继续使用内存thread_t->callee_saved已保存的。这样浮点上下文save ,restore 就完全符合了riscv-privileged Table 3.4的要求
current mstaus.fs off init clean dirty
save context NO NO NO Yes
after save context off init clean clean
(switch to clean from dirty manually)
restore context NO yes Yes /
after save context off init
(switch to init from dirty manually)
clean
(switch to clean from dirty manually)
/on
Any writing FPU register instruction will cause mstatus.fs = dirty, reading not
Restore init with fmv.w.x ftxx, zero
抱着不死心的态度, 仔细再看了一便 riscv-privileged-20211203.pdf,发现另外两处描述
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

此处再次强调了,
  1. FS就是为了减少 FPU save ,restore涉及的
  2. mstatus.fs 是可以setting,那么我们手动改变fs状态是合法的
RISC-V架构下 FPU Context 的动态保存和恢复
文章图片

这段多次提及了 last context save, 有没有似曾相识。现在基本确定我们的推测是正确的,搞了这么久,竟然是自己挖的坑,文档没看仔细,没理解透彻。
【RISC-V架构下 FPU Context 的动态保存和恢复】当然,自己推导测试了一遍,确实加深了理解,可以自信地进行最终方案的确定。

    推荐阅读