乐趣区

关于程序员:RISCV架构下-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 元价格也很便宜,果决动手测试,顺便反对一下!

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 开发板新建工程

2)基于 RT-Thread 新建工程

3)编译测试

这次应用的 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)
#endif

    addi sp, sp, RT_CONTEXT_SIZE
    mret
    

浮点线程 1

thread1 里加了一个稍简单的浮点操作

浮点线程 2

thread2 简略些

线程优先级

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");
}

运行后果

线程 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,只是一些异样标记和舍入模式,暂未放进栈里

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;
    int                i;

    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*FREGBYTES

    FSTORE  f0, 0 * FREGBYTES(sp)
    FSTORE  f1, 1 * FREGBYTES(sp)
    FSTORE  f2, 2 * FREGBYTES(sp)
    FSTORE  f3, 3 * FREGBYTES(sp)
    FSTORE  f4, 4 * FREGBYTES(sp)
    FSTORE  f5, 5 * FREGBYTES(sp)
    FSTORE  f6, 6 * FREGBYTES(sp)
    FSTORE  f7, 7 * FREGBYTES(sp)
    FSTORE  f8, 8 * FREGBYTES(sp)
    FSTORE  f9, 9 * FREGBYTES(sp)
    FSTORE  f10, 10 * FREGBYTES(sp)
    FSTORE  f11, 11 * FREGBYTES(sp)
    FSTORE  f12, 12 * FREGBYTES(sp)
    FSTORE  f13, 13 * FREGBYTES(sp)
    FSTORE  f14, 14 * FREGBYTES(sp)
    FSTORE  f15, 15 * FREGBYTES(sp)
    FSTORE  f16, 16 * FREGBYTES(sp)
    FSTORE  f17, 17 * FREGBYTES(sp)
    FSTORE  f18, 18 * FREGBYTES(sp)
    FSTORE  f19, 19 * FREGBYTES(sp)
    FSTORE  f20, 20 * FREGBYTES(sp)
    FSTORE  f21, 21 * FREGBYTES(sp)
    FSTORE  f22, 22 * FREGBYTES(sp)
    FSTORE  f23, 23 * FREGBYTES(sp)
    FSTORE  f24, 24 * FREGBYTES(sp)
    FSTORE  f25, 25 * FREGBYTES(sp)
    FSTORE  f26, 26 * FREGBYTES(sp)
    FSTORE  f27, 27 * FREGBYTES(sp)
    FSTORE  f28, 28 * FREGBYTES(sp)
    FSTORE  f29, 29 * FREGBYTES(sp)
    FSTORE  f30, 30 * FREGBYTES(sp)
    FSTORE  f31, 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)
#endif

    addi sp, sp, RT_CONTEXT_SIZE
 /* load float reg */
#ifdef ARCH_RISCV_FPU

    FLOAD   f0, 0 * FREGBYTES(sp)
    FLOAD   f1, 1 * FREGBYTES(sp)
    FLOAD   f2, 2 * FREGBYTES(sp)
    FLOAD   f3, 3 * FREGBYTES(sp)
    FLOAD   f4, 4 * FREGBYTES(sp)
    FLOAD   f5, 5 * FREGBYTES(sp)
    FLOAD   f6, 6 * FREGBYTES(sp)
    FLOAD   f7, 7 * FREGBYTES(sp)
    FLOAD   f8, 8 * FREGBYTES(sp)
    FLOAD   f9, 9 * FREGBYTES(sp)
    FLOAD   f10, 10 * FREGBYTES(sp)
    FLOAD   f11, 11 * FREGBYTES(sp)
    FLOAD   f12, 12 * FREGBYTES(sp)
    FLOAD   f13, 13 * FREGBYTES(sp)
    FLOAD   f14, 14 * FREGBYTES(sp)
    FLOAD   f15, 15 * FREGBYTES(sp)
    FLOAD   f16, 16 * FREGBYTES(sp)
    FLOAD   f17, 17 * FREGBYTES(sp)
    FLOAD   f18, 18 * FREGBYTES(sp)
    FLOAD   f19, 19 * FREGBYTES(sp)
    FLOAD   f20, 20 * FREGBYTES(sp)
    FLOAD   f21, 21 * FREGBYTES(sp)
    FLOAD   f22, 22 * FREGBYTES(sp)
    FLOAD   f23, 23 * FREGBYTES(sp)
    FLOAD   f24, 24 * FREGBYTES(sp)
    FLOAD   f25, 25 * FREGBYTES(sp)
    FLOAD   f26, 26 * FREGBYTES(sp)
    FLOAD   f27, 27 * FREGBYTES(sp)
    FLOAD   f28, 28 * FREGBYTES(sp)
    FLOAD   f29, 29 * FREGBYTES(sp)
    FLOAD   f30, 30 * FREGBYTES(sp)
    FLOAD   f31, 31 * FREGBYTES(sp)
    addi    sp, 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)
#endif

    addi sp, sp, RT_CONTEXT_SIZE
     /* load float reg */
#ifdef ARCH_RISCV_FPU

    FLOAD   f0, 0 * FREGBYTES(sp)
    FLOAD   f1, 1 * FREGBYTES(sp)
    FLOAD   f2, 2 * FREGBYTES(sp)
    FLOAD   f3, 3 * FREGBYTES(sp)
    FLOAD   f4, 4 * FREGBYTES(sp)
    FLOAD   f5, 5 * FREGBYTES(sp)
    FLOAD   f6, 6 * FREGBYTES(sp)
    FLOAD   f7, 7 * FREGBYTES(sp)
    FLOAD   f8, 8 * FREGBYTES(sp)
    FLOAD   f9, 9 * FREGBYTES(sp)
    FLOAD   f10, 10 * FREGBYTES(sp)
    FLOAD   f11, 11 * FREGBYTES(sp)
    FLOAD   f12, 12 * FREGBYTES(sp)
    FLOAD   f13, 13 * FREGBYTES(sp)
    FLOAD   f14, 14 * FREGBYTES(sp)
    FLOAD   f15, 15 * FREGBYTES(sp)
    FLOAD   f16, 16 * FREGBYTES(sp)
    FLOAD   f17, 17 * FREGBYTES(sp)
    FLOAD   f18, 18 * FREGBYTES(sp)
    FLOAD   f19, 19 * FREGBYTES(sp)
    FLOAD   f20, 20 * FREGBYTES(sp)
    FLOAD   f21, 21 * FREGBYTES(sp)
    FLOAD   f22, 22 * FREGBYTES(sp)
    FLOAD   f23, 23 * FREGBYTES(sp)
    FLOAD   f24, 24 * FREGBYTES(sp)
    FLOAD   f25, 25 * FREGBYTES(sp)
    FLOAD   f26, 26 * FREGBYTES(sp)
    FLOAD   f27, 27 * FREGBYTES(sp)
    FLOAD   f28, 28 * FREGBYTES(sp)
    FLOAD   f29, 29 * FREGBYTES(sp)
    FLOAD   f30, 30 * FREGBYTES(sp)
    FLOAD   f31, 31 * FREGBYTES(sp)
    addi    sp, sp, 32 * FREGBYTES
#endif
    mret

测试

同样的线程,运行后不会呈现问题了

FPU 动静保留

尽管以后的 FPU 动态保留计划能够解决 FPU 上下文问题,然而代价还是太大

一旦使能了 FPU, 无论线程是否应用 FPU,上下文切换时均会 save , restore 32 个 float registers

所以还是心愿能够像 ARM 那样依据以后程序状态来决定是否保留浮点寄存器组,如上面 PendSV:

在《riscv-privileged-20211203.pdf》文档 P26 中发现了 mstatus FS 域的定义。咱们齐全能够依据 FS 是否是 Dirty 状态,来决定是否保留寄存器组,这样只在须要的时候,保留额定的 32 的浮点寄存器,平时能够省下很多工夫,进步零碎切换效率。

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_saved

    frcsr 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 1f

no_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 的阐明

FPU 的测试代码

总结:

  1. Zephyr 配置里有个 CONFIG_FPU_SHARING,全局开关 FPU
  2. 创立工作时,能够抉择是否应用 CPU 的 float registers
  3. 抉择 K_FP_REGS 后,调度时会减少额定的步骤去保留和复原浮点寄存器上下文!

Zephyr 这样解决,就须要工作创立时十分小心应用 thread_option 选项,如果未使能 K_FP_REGS 的工作中又波及到浮点的操作,就很可能导致浮点上下文异样。

因为编译器是全局设置,一旦开启 硬件 FPU, 发现了浮点运算,就可能应用浮点指令的 (有时会应用 lib 库优化,不应用),它不会为你的失误买单。

mstatus.fs

尽管 Zephyr 的动静保留不是很完满,但也给咱们提供了一个很好的参考,上面将依据 mstatus.fs 实现动静 FPU 保留与复原

再次 review《riscv-privileged-20211203.pdf》文档,Table3.4 给出了 FPU 上下文在特权模式下的依据 mstatus.fs 域保留和复原的动作倡议

P27 页有段很重要的解释阐明

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 拜访 和立刻数问题

  1. 首先 RV32F 指令操作的 rd ,rs1, rs2 大部分均是浮点寄存器
  2. 浮点寄存器的间接赋值只有一个 flw 指令:flw rd offset[11:0] (rs1)

    从上面的汇编代码能够看出浮点线程是很费线程栈的,寄存器的加载必须从内存中读取,不能应用立刻数。

  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 前

load 后

能够看到一执行 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 状态就须要手动复原一下

这样就有很多状态和特权文档有出入,所以打算基于实测后果简化代码

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 就是一种软件定义呢

尽管两种计划都失常,暂未发现问题,然而总感觉怪怪的,按理说一个官网文档,不应该有这么显著的谬误,网上看了一下也有网友对此疑难

百思不得其解,明天冲澡的时候恍然大悟:

如果这个 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:
 *
 *    __<structure>_<member>_OFFSET
 *
 * The macro "GEN_NAMED_OFFSET_SYM(structure, member, name)" is also provided
 * to create the symbol with the following form:
 *
 *    __<structure>_<name>_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 <toolchain.h>
#include <stddef.h>

/* 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_saved

    frcsr 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 1f

no_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 1f

no_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,发现另外两处形容

此处再次强调了,

  1. FS 就是为了缩小 FPU save,restore 波及的
  2. mstatus.fs 是能够 setting, 那么咱们手动扭转 fs 状态是非法的

这段屡次提及了 last context save, 有没有似曾相识。当初根本确定咱们的揣测是正确的,搞了这么久,居然是本人挖的坑,文档没看认真,没了解透彻。

当然,本人推导测试了一遍,的确加深了了解,能够自信地进行最终计划的确定。

退出移动版