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 的寄存器保存与恢复路径。
图中把切换过程拆成保存、恢复和延迟构成三部分。实际实现时,TCB 应放在 TCM 中,使保存和恢复的访存延迟不随缓存状态变化。若 TCB 放在外部 DDR,切换时间会因缓存命中与否产生抖动,破坏实时性。
紧耦合内存使用
TCM 是 PolarFire SoC 实时性的关键资源。它直接挂在核心流水线上,访问延迟固定,不受缓存和总线竞争影响。实时任务的关键代码、中断向量表、TCB 和共享数据缓冲区都应放入 TCM。
使用 TCM 时需注意容量分配。以某实时控制示例为例,假设核心提供 128 KB ITCM 和 128 KB DTCM,可按下表规划。这些数值属于教学示例,实际容量以具体器件手册为准。
| 区域 | 用途 | 示例大小 | 放置理由 |
|---|---|---|---|
| ITCM | 中断向量表、ISR、调度器 | 32 KB | 取指延迟确定 |
| ITCM | 实时控制算法 | 64 KB | 避免指令缓存抖动 |
| DTCM | 任务控制块 TCB | 16 KB | 切换访存确定 |
| DTCM | ADC/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 占用。