FreeRTOS RISC-V 浮点上下文切换移植:在 IAR 工程中完整保存 FPU 寄存器

从问题原理、官方仓库现状到完整移植与验证

适用实例:AS32X601(RV32IMAFDC)

在带有 F/D 扩展的 RISC-V MCU 上,IAR 能生成浮点指令,并不意味着 FreeRTOS 会自动保护浮点寄存器。只要移植层没有把 f0~f31 和 fcsr 纳入任务上下文,两个浮点任务之间的一次正常抢占,就可能把计算现场悄悄破坏。

本文以 AS32X601 为完整实例,为 FreeRTOS Kernel V10.5.1 的 IAR RISC-V 端口补充浮点上下文保存与恢复。实现同时覆盖 F、D 扩展,并采用 mstatus.FS 驱动的条件读写,避免纯整数任务在每次切换时都访问 32 个浮点寄存器。

本文实例基于 FreeRTOS Kernel V10.5.1、IAR RISC-V 和 AS32X601(RV32IMAFDC)。V11.3.0 已在官方 GCC RISC-V 端口加入 FPU 上下文保存,但同版本 IAR RISC-V 端口没有对应实现,因此旧版 IAR 工程仍需自行补齐移植层。本文代码不能不经核对直接套用到其他端口。

目录

1. 为什么必须保存浮点上下文

1.1 一个典型的故障过程

假设系统中有两个可抢占任务,它们都进行浮点运算:

任务 A 的 C 变量看起来都在自己的栈中,但编译器会把中间结果长时间保留在浮点寄存器中。若上下文切换只保存 x1~x31mepc 和 mstatus,任务 B 便能无意中修改任务 A 的浮点现场。

需要特别区分两件事:

  • 工具链支持 FPU:编译器和汇编器能够生成 faddfmulfswfsd 等指令。
  • RTOS 保护 FPU 上下文:任务切出时保存 FP 寄存器,任务切入时恢复该任务自己的值。

前者不会自动带来后者。浮点运算在裸机单线程程序中正常,只能说明硬件和工具链可用,不能证明多任务切换是安全的。

1.2 为什么这种问题很难复现

浮点上下文缺失通常不是每次都出错。它要求任务恰好在一个浮点值仍驻留寄存器时被抢占,并且另一个执行流随后覆盖了相关寄存器。因此,下面这些变化都可能让现象暂时消失:

  • 修改编译优化级别,导致寄存器分配变化;
  • 增加日志,使调度时序和寄存器活跃区间变化;
  • 降低 tick 频率,减少关键窗口内被抢占的概率;
  • 测试任务实际使用软浮点库函数,而没有生成预期的硬件 FP 指令;
  • 浮点常量被编译器提前折叠,运行时根本没有发生浮点计算。

所以正确的验证方法不是“打印几次结果都正常”,而是主动制造高频抢占、确认反汇编存在 FP 指令,并长时间比较数值结果。

2. FreeRTOS 官方仓库现状

截至 2026 年 7 月 22 日,官方仓库存在明确的版本和工具链边界:

内核与端口

RISC-V FPU 上下文状态

V10.5.1 GCC RISC-V

未集成

V10.5.1 IAR RISC-V

未集成

V11.3.0 GCC RISC-V

已集成,通过 configENABLE_FPU 启用

V11.3.0 IAR RISC-V

在该标签的端口源码中未发现等价实现

官方依据如下:

这里对 IAR 的判断是对指定 Git 标签源码的检查结果,不应扩展为“FreeRTOS 永远不支持 IAR FPU”,也不能把 GCC V11.3.0 的结论写成“所有 RISC-V 工具链端口都已支持”。

2.1 官方 V11.3.0 GCC 端口怎么做

官方 GCC 端口的核心思路是:

  1. 通过 configENABLE_FPU == 1 显式启用。
  2. 使用编译器提供的 __riscv_flen 选择 flw/fsw 或 fld/fsd
  3. 根据 mstatus.FS 判断当前 FP 状态是否为 Dirty。
  4. Dirty 时才向下扩展任务 SP,保存 f0~f31 与 fcsr;非 Dirty 时不创建 FP 帧。
  5. 无论 FP 帧是否存在,当前保存 SP 都以 mepcmstatus 两个公共 word 开头,恢复代码据此读取保存的 FS。
  6. Dirty 上下文落栈后把硬件当前 FS 标为 Clean,但栈中保存的原始 FS 仍保持 Dirty。

官方并不会扫描任务代码,也不会自动扩大 xTaskCreate() 请求的任务栈。它只在上下文切换时观察硬件 FS:纯整数任务的已保存上下文没有 FP 帧;可能执行硬件浮点的任务仍需由用户按最坏情况留足栈空间。

本文采用相同的公共帧头和 FS 自描述可变帧算法,同时做两项必要适配:IAR 汇编器用全局唯一的保存/恢复子程序避免宏内重复标签;AS32X601 的 RV32D 帧增加 4 字节对齐修正,保证 fld/fsd 使用自然对齐地址。因此,本文实现应称为“官方风格的 IAR 适配”,而不是 GCC 文件的逐字复制。

3. 理解 mstatus.FS

RISC-V mstatus 的 FS 字段位于 bit [14:13],用于描述浮点单元状态:

FS

名称

含义

切换策略

00

Off

浮点状态关闭

不访问 FP 寄存器

01

Initial

已启用,但尚无需要保存的修改

跳过保存

10

Clean

FP 状态与已保存状态一致

跳过保存

11

Dirty

FP 状态已被修改

保存完整 FP 上下文

当任务执行会改变浮点状态的指令后,符合规范的硬件会把 FS 标记为 Dirty。上下文切换代码因此不需要扫描任务代码,也不需要要求每个浮点任务手动调用登记函数,只需检查 FS。

需要保护的 FP 上下文包括:

  • f0~f31:32 个浮点寄存器;
  • fcsr:包含浮点舍入模式和累计异常标志。

只保存 f0~f31 而遗漏 fcsr,会让不同任务之间的舍入模式或异常状态互相污染。

3.1 __riscv_flen 决定保存宽度

工具链通常根据目标 ISA 定义 __riscv_flen

  • __riscv_flen == 32:F 扩展,使用 fsw/flw,每个 FP 寄存器保存 4 字节;
  • __riscv_flen == 64:D 扩展,使用 fsd/fld,每个 FP 寄存器保存 8 字节;
  • 未定义:当前构建不包含 F/D 浮点扩展,FP 代码应被排除。

__riscv_flen 描述浮点寄存器宽度,不等同于 __riscv_xlen。RV32 核心同样可以实现 64 位宽的 D 扩展浮点寄存器。

4. 本文采用的方案

本文采用“mstatus.FS 自描述的可变 FP 帧”:

  1. 通过 configENABLE_FPU == 1 显式启用,并用 __riscv_flen 选择 F/D 保存宽度。
  2. 整数上下文统一以 mepcmstatus 两个公共 word 开头,整数寄存器从 slot 2 开始。
  3. 新任务只创建整数帧,把 FS 初始化为 Initial,不预留 FP 帧。
  4. 任务切出时检查当前 FS;只有 Dirty 才执行 sp -= portFPU_CONTEXT_SIZE 并保存完整 FP 状态。
  5. 任务切入时读取当前保存帧 slot 1 中的 FS;只有 Dirty 才恢复 FP 状态并抬高 SP。
  6. 非 Dirty 路径既不读写 FP 寄存器,也不移动 SP。

属性

本文 IAR V10.5.1 适配

官方 V11.3.0 GCC 方案

启用条件

configENABLE_FPU == 1 加 IAR __riscv_flen

configENABLE_FPU == 1 加编译器 ISA 宏

帧存在性

保存的 mstatus.FS

保存的 mstatus.FS

非 Dirty 时

不访问 FP 寄存器,不移动 SP

不访问 FP 寄存器,不移动 SP

Dirty 时

动态压入 f0~f31 与 fcsr

动态压入 f0~f31 与 fcsr

汇编组织

IAR 全局子程序,避免宏标签重复

GCC 汇编宏

RV32D 对齐

AS32X601 增加 4 字节尺寸修正

使用官方通用布局

__riscv_flen 仍然只是移植层的编译期 ISA 能力宏,不是“当前任务使用了浮点”的运行时标志。真正按任务区分上下文的是硬件 FS;FreeRTOS 不扫描任务函数,也不会替用户调整栈深度。

5. 在 IAR 端口中加入 FPU 上下文

下面以 FreeRTOS V10.5.1 的 IAR RISC-V 目录为例。开始修改前,请先备份以下文件:

本文假定端口已经定义:

  • portWORD_SIZE:RV32 下为 4;
  • store_x/load_x:整数 word 的保存和加载伪指令;
  • portcontextSAVE_CONTEXT_INTERNAL 和 portcontextRESTORE_CONTEXT:原有整数上下文宏;
  • pxCurrentTCB:当前任务 TCB 指针;
  • CSR_MSTATUSmstatus 的 CSR 定义。

如果你的端口命名不同,应先完成符号映射,不要直接粘贴。

5.1 在 portContext.h 定义 F/D 相关宏

将下面代码放在整数寄存器宽度与 store_x/load_x 定义之后:

configENABLE_FPU 必须同时对 IAR C 编译器和汇编预处理器可见。启用它却没有选择 F/D 目标 ISA时直接报错,避免生成“配置声称有 FPU、汇编器却没有 FP 指令”的半有效固件。

5.2 接入整数上下文保存宏

官方可变帧成立的前提是:TCB 中保存的 SP 无论是否带有 FP 帧,都指向相同格式的公共头。为此,整数寄存器从 slot 2 开始,slot 0/1 留给 mepc/mstatus

非 Dirty 时 vPortFPUSave 原样返回,公共头就是整数帧的 slot 0/1;Dirty 时它先下移 SP,公共头便写在新 FP 帧的最低地址。两种情况下 TCB 都直接保存当前 SP。

5.3 修改 trap wrapper 并按保存的 FS 恢复

旧固定帧实现会在 trap wrapper 中额外执行一次 sp -= portWORD_SIZE。采用公共头后必须删除这次下移,直接写 slot 0:

如果端口在统一 freertos_risc_v_trap_handler 中保存 mepc,同样删除原来的额外 SP 下移,其余中断分发逻辑保持不变。

恢复入口先读公共头。t3 保存 slot 1 的原始值,并且 FP 恢复子程序不得破坏它:

先写入清除了 MIE 的保存 mstatus,是为了让后续 fld/flw 在合法 FS 状态下执行,同时避免整数上下文尚未恢复完成就立即响应新中断。

5.4 新任务只构造整数帧

官方可变帧方案不会在创建任务时猜测它将来是否执行浮点代码,也不会给每个任务预留 FP 帧。新任务只建立 31-word 整数上下文,并把初始 mstatus.FS 设为 Initial。它第一次运行时没有 FP 数据需要恢复;真正执行 FP 指令后,硬件把 FS 推进到 Dirty,下一次切出才动态压入 FP 帧。

因此初始栈与纯整数任务的已保存栈使用同一布局:

任务产生 Dirty 后才出现第二种布局,见下图:

pxPortInitialiseStack 的关键是一次性分配公共整数帧,并按 5.2 节的相同 slot 写入任务入口、初始状态和参数:

其余整数寄存器槽可以保持端口原有的调试填充值,也可以清零;它们不影响 FP 帧判定。特别注意:这里没有 portFPU_CONTEXT_SIZE 的减法,也没有初始化 f0~f31 或 fcsr

pxPortInitialiseStack 在不同 FreeRTOS/IAR 端口中的任务返回地址和调试填充值可能不同。移植时保留原端口语义,但 slot 编号必须与 5.2、5.3 节完全一致。

5.5 调整 xPortStartFirstTask

AS32X601 示例端口使用 ret 启动第一个任务,而不是让它先经过普通的 portcontextRESTORE_CONTEXT。由于新任务一定只有整数帧,这条特殊启动路径不应调用 FP 恢复,也不应跳过 FP 空间;它直接按公共整数布局取出入口和寄存器:

这里只是 AS32X601 现有 ret 首任务策略的适配;任务一旦运行并发生普通切换,后续都走 5.3 节的统一 mret 恢复路径。如果你的端口本来就用统一恢复宏启动首任务,则不要再保留这段特例。

5.6 完整实现 vPortFPUSave

IAR 汇编宏多次展开时不能自动唯一化内部标签,因此仍使用全局唯一的子程序。入口 t3 保存原始 mstatus;非 Dirty 路径直接返回,不移动 SP:

由于 ra 已进入整数上下文,函数内部使用 call/ret 不会丢失任务返回地址。t3 也已经作为 x28 保存,子程序只使用 t0/t1 检查 FS,并保留原始 Dirty 值供调用者写入公共头。

5.7 完整实现 vPortFPURestore

恢复入口的 SP 指向公共头,t3 是 slot 1 中保存的 mstatus。非 Dirty 路径直接返回;Dirty 路径恢复完整 FP 状态并把 SP 抬回整数帧:

RV32F 的 132 字节增量会让 f31/fcsr 复用旧整数帧已经空出的 slot 0/1;RV32D 的 268 字节 AS32 修正则把 fcsr 保持在对齐后的 FP 帧内。两者恢复后 SP 都准确回到整数帧基址。

5.8 芯片扩展宏不要重复保存

AS32X601 示例中的 portasmSAVE_ADDITIONAL_REGISTERS 和 portasmRESTORE_ADDITIONAL_REGISTERS 是空宏。如果其他芯片已经通过这两个宏保存厂商扩展状态,不能假设它们可原样保留:本文调用它们时 SP 已位于 FP 帧边界,必须重新审核宏使用的是相对偏移还是自行移动 SP,并保证保存与恢复次序严格相反。

在本文方案中,FPU 已由 portContext.h/portASM.s 直接处理,不要再在 chip-specific 宏中预留或保存第二份 FP 帧,否则会形成重复上下文并破坏栈布局。

6. 栈空间核算

AS32X601 是 RV32,portWORD_SIZE=4。可变帧方案的额外开销取决于任务保存时的 FS:

RV32F 的 132B 看起来小于“32 × 4B + 4B fcsr + 8B 公共头”的 140B,是因为压入 FP 帧后,最高处的 f31/fcsr 复用了原整数帧的 slot 0/1。RV32D 不能机械套用同一差值:在 AS32X601 的 124B 整数帧之后,选择 268B 增量可使 f0 的 base+8 地址保持 8 字节对齐,并让公共头仍位于新 SP 的 slot 0/1。

FreeRTOS 不会扫描每个任务的机器码来判断它是否使用浮点,也不会自动增大 xTaskCreate() 的 usStackDepth。所谓“按需”只发生在切换现场:保存的 mstatus.FS == Dirty 才压入 FP 帧;新任务和未变 Dirty 的纯整数任务不占这段额外空间。

因此栈预算应按任务分类:

  • 明确不会生成 FP 指令的纯整数任务,可以按整数端口原有方法测量;
  • 任何可能直接或间接执行 FP 指令的任务,都必须在最坏栈深基础上再容纳 132B(F)或 268B(D);
  • 日志格式化、数学库和第三方回调也可能让一个“看起来是整数”的任务变成 FP 用户;
  • Idle/Timer 任务只有在其调用链确实使用 FP 时才需要这段余量。

任务恢复 FP 数据后,硬件会在加载寄存器或后续浮点计算时再次把 FS 置为 Dirty,所以活跃的 FP 任务通常会在后续每次切出/切入都承担完整 FP 保存恢复成本。可变帧主要避免纯整数任务的空间和访存开销,不等于只保存一次。

不要照抄一个“通用安全值”。建议先显著放大相关任务栈,开启溢出检测,在目标优化级别和最长调用链下运行压力测试,再依据 uxTaskGetStackHighWaterMark() 加项目规定的安全裕量回调。

7. IAR 工程配置

在 IAR 中打开:

Project → Options → General Options → Target

选择与芯片真实能力一致的 ISA/FPU 配置。AS32X601 示例工程的项目文件记录为:

RV32IMAFDC

其中 F 表示单精度扩展,D 表示双精度扩展;选择 D 后,工具链走 __riscv_flen == 64 路径。不要因为本文代码支持 D,就在只有 F 扩展的硬件上强行选择 D,否则程序会在执行 fld/fsd 等指令时触发非法指令异常。

同时在 FreeRTOSConfig.h(或工程统一预定义宏)中显式启用:

#define configENABLE_FPU 1

该宏必须对 C 编译和 portASM.s 的汇编预处理同时可见。只选择 ISA 而不启用 configENABLE_FPU,业务代码仍可能生成 FP 指令,但移植层不会保存它们;只启用宏而不选择 F/D ISA,则应由 5.1 节的编译期检查立即报错。

配置完成后需要确认三件事:

  1. 汇编器处理 portASM.s 时确实定义了 configENABLE_FPU == 1
  2. 汇编器确实定义了与目标一致的 __riscv_flen
  3. C 文件和汇编文件使用一致的 ISA 设置,不能一边按 F/D 编译业务代码,另一边把移植层当作无 FPU 构建。

ISA 选择与 C ABI 也不是同一概念。ISA 决定硬件指令是否可用,ABI 决定函数参数、返回值和寄存器保存约定。即使工程采用整数 ABI,只要编译器在函数内部生成 FP 指令,任务切换仍然必须保护真实使用到的 FP 状态。

8. 如何验证保存与恢复

8.1 先开启栈保护

FreeRTOSConfig.h 中开启第二级栈溢出检查:

并在应用代码中实现 hook:

如果使用软件定时器,还要同步检查 configTIMER_TASK_STACK_DEPTH;它经常直接引用 configMINIMAL_STACK_SIZE,不能只增大用户任务栈。

8.2 三任务抢占测试

下面的测试使用两个不同的浮点递推任务和一个纯整数任务。volatile 输入用于阻止编译器在编译期算完全部表达式;每轮运算后主动 yield,扩大任务切换覆盖浮点活跃区间的概率。

TEST_TASK_STACK_WORDS=512 只是给本测试留出较宽裕的起点,不是所有产品的推荐常数。运行后应通过调试器观察:

  • ulFpErrorCount 应始终为 0;
  • uxMinFreeAuxMinFreeB 和 uxMinFreeInteger 应保留可接受余量;
  • 两个浮点任务的 mstatus.FS 会进入 Dirty;
  • 纯整数任务自己的 FS 不应因其业务代码变成 Dirty。

还应在任务刚切出后检查 TCB 保存的 SP。公共头在两种布局中都保持一致:SP+0 是 mepcSP+4 是保存的 mstatus

  • 纯整数任务:保存的 FS 应为 Initial 或 Clean,TCB SP 直接指向整数帧,保存路径没有额外移动 SP;
  • 浮点任务:保存的 FS 应为 Dirty,TCB SP 指向动态 FP 帧的公共头;
  • 对同一保存点比较 Dirty 与非 Dirty 路径,RV32F 的 SP 差应为 132B,AS32X601 RV32D 的 SP 差应为 268B;
  • 恢复后 SP 必须回到进入 trap 前的位置,不能随切换次数持续漂移。

8.3 一定要检查反汇编

C 测试只有在确实生成硬件浮点指令时才有意义。请在 IAR Disassembly 窗口或列表文件中确认浮点任务包含相应指令,例如:

还要观察 taskYIELD() 前后的局部变量是否有机会保持在 FP 寄存器。如果编译器把 x 在 yield 前主动写回栈,测试仍能验证基本调度路径,但降低了暴露“未保存 FP 寄存器”问题的能力。这时可以调整优化级别、增加运算链,或在实验代码中使用工具链支持的寄存器约束。

8.4 判定标准

完成以下检查后,才能认为移植通过:

9. 使用边界和升级建议

9.1 ISR 中使用 FPU

本文解决的是任务上下文。中断发生后,任务上下文通常在 ISR 入口保存;但 ISR 是否允许使用 FPU、嵌套中断是否会再次覆盖 FP 状态、ISR 返回时 FS 如何处理,取决于完整 trap 入口设计。

最稳妥的规则是:在没有专门验证前,普通 ISR 不使用浮点运算,也避免会间接生成浮点代码的 %f 格式化、数学库或回调。若产品必须在 ISR 中使用 FPU,应单独设计和测试 ISR FP 上下文协议,不能把本文任务测试当作证明。

9.2 ISA 与 ABI 要分别核对

  • ISA/FPU 选项决定 faddflwfld 等指令是否存在;
  • ABI 决定跨 C 函数调用时哪些参数和寄存器由谁保存;
  • FreeRTOS 的抢占式任务切换不等同于普通函数调用,最终仍以端口汇编保存了哪些寄存器为准。

9.3 其他 RISC-V 芯片

迁移到其他芯片时至少核对:

  • 核心是 RV32 还是 RV64;
  • 实现 F、D 中的哪一种;
  • mstatus.FS 是否按目标特权架构版本实现;
  • 栈对齐要求是否仍为本文假设;
  • 是否存在需要通过 portasmSAVE_ADDITIONAL_REGISTERS 处理的厂商扩展状态。

本文的 33/67-word Dirty 增量来自 AS32X601 的 RV32 公共头与对齐设计,不可直接推广到 RV64 或不同整数栈布局。

9.4 升级 FreeRTOS 时不要叠加两套实现

若未来切换到 V11.3.0 或更新版本的 GCC RISC-V 官方 FPU 端口,应整体采用并验证官方 portContext.hportASM.S、配置宏和栈布局。本文虽沿用官方可变帧思路,但包含 IAR 语法和 AS32X601 RV32D 对齐适配;不要把两套保存宏叠加,否则极易出现重复保存和 SP 偏移冲突。

若继续使用 IAR,也要在每次升级后重新检查官方 IAR RISC-V 目录,因为本文关于“尚未集成”的结论绑定到明确的 Git 标签和调查日期,不能永久化。

9.5 本文未覆盖的场景

以下场景需要独立设计:

  • RISC-V Vector 扩展上下文;
  • SMP 或多 hart 调度;
  • Supervisor/User mode 任务切换;
  • 嵌套中断中的 FPU 使用;
  • 与芯片安全扩展、DSP 或自定义协处理器状态共同保存。

10. 总结

在 IAR 工程中启用 RISC-V F/D 指令,只完成了“可以计算”这一步。要让 FreeRTOS 多任务安全使用 FPU,还必须让移植层保存 f0~f31fcsr,并让公共头、任务切出和任务切入三条路径使用完全一致的布局。

本文方案适用于以 FreeRTOS V10.5.1 IAR RISC-V 端口为基础的 AS32X601 工程:

  • 通过 __riscv_flen 同时覆盖 F/D;
  • 通过保存的 mstatus.FS == Dirty 决定是否动态压入和恢复 FP 帧;
  • 新任务与非 Dirty 任务不预留 FP 帧,Dirty 时 F 增加 33 words/132B、D 增加 67 words/268B;
  • 通过固定格式的 mepc/mstatus 公共头保持两种栈布局可判定、SP 可逆;
  • 通过双浮点任务、纯整数任务、反汇编和栈高水位共同验证。

最后再强调一次官方边界:FreeRTOS V11.3.0 已在 GCC RISC-V 端口集成 FPU 上下文保存;对于 IAR,应以你实际使用版本的官方标签源码为准。在采用本文代码前,先画清楚自己的栈帧,再逐项核对 SP——底层上下文切换中,一个 word 的误差就足以把问题变成随机崩溃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值