3. RISC-V指令集与实时编程模型

PolarFire SoC 中的实时核心基于 RV64GC 指令集架构,包含 64 位整数指令、M/A/F/D/C 扩展以及通用标量指令。对实时控制而言,指令集本身不是抽象概念,它直接决定了中断响应延迟、上下文切换开销和紧耦合内存(TCM)的访问效率。理解 RV64GC 的编程模型,是后续编写确定性实时任务的基础。

RV64GC 指令集基础

RV64GC 中的 G 是 IMAFD 的缩写,C 表示压缩指令扩展。I 提供基础整数运算与加载/存储;M 提供乘除法;A 提供原子操作,用于无锁队列和信号量;F/D 提供单双精度浮点,适合电机控制中的坐标变换;C 将常用指令压缩为 16 位,减少代码体积,对 TCM 容量有限的场景尤其重要。

实时编程中需要特别关注的是加载/存储指令的时序。RISC-V 采用弱内存模型,普通 load/store 不保证跨 hart 的顺序。当实时任务与中断服务程序共享数据时,必须使用 fence 指令建立顺序,或使用 A 扩展的 lr.w/sc.w、amoadd.w 等原子指令。例如在中断中更新一个供任务读取的计数器,应使用 amoadd.w 而非普通读改写,避免丢失更新。

# 原子递增共享计数器(a0 指向计数器地址)
li   t0, 1
amoadd.w zero, t0, (a0)

# 建立跨 hart 的存储顺序
fence rw, rw

另一个关键点是压缩指令对中断入口的影响。C 扩展允许 16 位指令,取指边界不再固定为 4 字节对齐,异常返回地址 mepc 的最低位可能非零。编写汇编中断入口时,必须按 mepc 实际值返回,不能假设 4 字节对齐。

中断与异常处理

PolarFire SoC 的实时核心通过 CLINT 和 PLIC 管理中断。CLINT 负责软件中断与定时器中断,PLIC 负责外部设备中断。机器模式下,mtvec 寄存器设置异常入口基址,mie 使能各类中断,mstatus.MIE 控制全局中断开关。

实时系统要求中断延迟可预测。建议将 mtvec 配置为向量模式,使不同异常跳转到独立入口,减少入口处的判断分支。中断服务程序应尽量短,只做数据搬运和标志置位,把耗时计算留给任务上下文。若必须嵌套,需在入口保存 mstatus 并在退出时恢复,避免中断使能状态错乱。

中断入口的第一条指令应尽快保存 ra、t0-t6、a0-a7 等调用者保存寄存器。若使用 C 扩展,注意 16 位压栈指令的栈对齐要求。

机器模式与用户模式

PolarFire SoC 支持 M/U 两级特权模式。实时任务通常运行在用户模式,通过 ecall 陷入机器模式请求服务;中断和异常处理运行在机器模式。这种隔离提高了系统可靠性,但每次模式切换都有开销。

对硬实时任务,频繁的 ecall 会引入不可忽略的延迟。一种折中方案是把关键实时任务放在机器模式,非关键任务放在用户模式。机器模式任务可直接访问 CLINT、PLIC 和 TCM,响应更快;用户模式任务通过系统调用访问外设,隔离性更好。选择哪种模式,取决于任务的最坏执行时间预算。

实时任务上下文切换

上下文切换是实时调度器的核心操作。在 RISC-V 上,切换需要保存和恢复通用寄存器、浮点寄存器以及 mstatus、mepc 等控制状态。若任务使用 F/D 扩展,还需保存 f0-f31 和 fcsr,这会显著增加切换时间。

为降低切换开销,可采取以下措施:一是让实时任务尽量使用整数运算,避免浮点上下文;二是使用压缩指令减少保存代码体积;三是把任务控制块放在 TCM 中,使保存/恢复的访存延迟确定。下面是一个简化的上下文切换示意,展示从任务 A 到任务 B 的寄存器保存与恢复路径。

实时任务上下文切换(任务A → 任务B) 任务A 运行态 PC / x1-x31 f0-f31 / fcsr mstatus / mepc 保存到 TCB_A TCM 中的任务控制块 访存延迟确定 不经过缓存 从 TCB_B 恢复 PC / x1-x31 f0-f31 / fcsr mstatus / mepc 切换时间构成(示例估算) 整数寄存器保存 约 32 次访存 浮点寄存器保存 约 33 次访存 控制状态保存 mstatus/mepc TCM 访存延迟 确定且低抖动

图中把切换过程拆成保存、恢复和延迟构成三部分。实际实现时,TCB 应放在 TCM 中,使保存和恢复的访存延迟不随缓存状态变化。若 TCB 放在外部 DDR,切换时间会因缓存命中与否产生抖动,破坏实时性。

紧耦合内存使用

TCM 是 PolarFire SoC 实时性的关键资源。它直接挂在核心流水线上,访问延迟固定,不受缓存和总线竞争影响。实时任务的关键代码、中断向量表、TCB 和共享数据缓冲区都应放入 TCM。

使用 TCM 时需注意容量分配。以某实时控制示例为例,假设核心提供 128 KB ITCM 和 128 KB DTCM,可按下表规划。这些数值属于教学示例,实际容量以具体器件手册为准。

区域用途示例大小放置理由
ITCM中断向量表、ISR、调度器32 KB取指延迟确定
ITCM实时控制算法64 KB避免指令缓存抖动
DTCM任务控制块 TCB16 KB切换访存确定
DTCMADC/DAC 共享缓冲区32 KB中断与任务共享
DTCM栈空间64 KB避免栈溢出抖动

把代码和数据放入 TCM 通常通过链接脚本完成。例如将 .tcm_text 段定位到 ITCM 起始地址,将 .tcm_data 段定位到 DTCM。启动时需把 TCM 段从非易失存储搬运到 TCM,搬运代码本身不能依赖 TCM。

MEMORY {
  itcm (rx) : ORIGIN = 0x08000000, LENGTH = 128K
  dtcm (rw) : ORIGIN = 0x08020000, LENGTH = 128K
}

SECTIONS {
  .tcm_text : { *(.tcm_text) } > itcm
  .tcm_data : { *(.tcm_data) } > dtcm
}

判断 TCM 使用是否合理,可以观察两个指标:一是中断响应时间的最坏值是否稳定,二是上下文切换时间的抖动是否在预算内。若把关键路径放入 TCM 后,最坏执行时间明显下降且方差收窄,说明配置有效。反之,若 TCM 容量不足导致关键代码被挤出,实时性反而恶化,此时应优先保证中断向量、ISR 和 TCB 的 TCM 占用。

TCM 容量有限,不要把所有代码都放入 TCM。应只放实时关键路径,非关键任务留在缓存区域,避免 TCM 争用和链接失败。