2. RISC-V架构基础

在把 Linux 驱动移植到 RISC-V 平台之前,必须先建立对 RISC-V 架构的准确认知。驱动工程师不需要像 CPU 设计者那样深究微架构,但必须理解指令集风格、特权级切换、寄存器约定、中断异常模型以及 MMU 的基本行为,否则在调试 probe 失败、DMA 一致性、中断路由和地址映射问题时将无从下手。本节围绕这五个基础点展开,并给出与驱动适配直接相关的观察方法。

一、RISC-V 指令集特点:模块化与规整性

RISC-V 最显著的特征是模块化。基础整数指令集只有 RV32I / RV64I,其余能力通过扩展字母组合,例如 M(乘除)、A(原子)、F/D(单双精度浮点)、C(压缩指令)、V(向量)。Linux 发行版常见的组合是 RV64GC,即 RV64IMAFDC。对驱动而言,这意味着:内核配置必须与目标 SoC 实际支持的扩展一致,否则会出现非法指令异常。

RISC-V 采用 load-store 架构,运算指令只操作寄存器,访存必须通过 load/store 完成;指令长度固定为 32 位(压缩指令为 16 位),编码规整,便于译码。另一个关键点是内存访问的对齐要求:普通 load/store 要求自然对齐,但内核可通过配置支持非对齐访问。驱动中若使用 readl()/writel() 等 MMIO 访问函数,通常已由内核处理字节序与屏障问题,不应直接用指针解引用访问设备寄存器。

驱动适配提示:在设备树中声明的 compatible 字符串必须与驱动 of_match_table 完全匹配,RISC-V 平台尤其依赖设备树描述外设与中断,而不是像 x86 那样依赖 ACPI 枚举。

二、特权级架构:M/S/U 三级与驱动所处位置

RISC-V 定义三个特权级:Machine(M)、Supervisor(S)、User(U)。M 级拥有最高权限,负责时钟、复位、部分中断控制;S 级运行 Linux 内核;U 级运行用户进程。Linux 驱动运行在 S 级,但很多底层操作需要 SBI(Supervisor Binary Interface)向 M 级固件请求服务,例如设置定时器、发送 IPI、关机重启。

因此,驱动中调用 sbi_ecall() 或使用 riscv_sbi 相关接口时,本质是发起一次 ecall 陷入 M 级。理解这一点有助于排查“驱动正常但系统无法关机”“定时器中断不来”等问题:问题往往不在驱动本身,而在 SBI 固件或设备树中的 riscv,ndev、interrupts-extended 配置。

三、寄存器约定:ABI 与调用规范

RISC-V 的寄存器命名与调用约定直接影响内核汇编、异常入口和驱动内联汇编。整数寄存器 x0~x31 有 ABI 别名:zero, ra, sp, gp, tp, t0-t6, s0-s11, a0-a7。其中 a0-a7 用于传参和返回值,s0-s11 为被调用者保存,t0-t6 为调用者保存。驱动中若编写内联汇编,必须遵守该约定,否则会破坏内核栈或返回地址。

寄存器ABI 名用途驱动关注点
x0zero恒为 0不可写
x1ra返回地址异常入口需保存
x2sp栈指针中断上下文切换
x10-x11a0-a1参数/返回值系统调用与 SBI 调用
x8s0/fp帧指针栈回溯
x5t0临时寄存器内联汇编需自行保存

四、中断与异常模型:PLIC、CLINT 与本地中断

RISC-V 的中断分为本地中断和外部中断。本地中断由 CLINT 管理,包括软件中断(IPI)和定时器中断;外部中断由 PLIC 汇聚,再通过 mip/sip 的 SEIP 位上报给 S 级。Linux 中,驱动通过 request_irq() 注册中断处理函数,但底层需要设备树正确描述中断控制器层级。

异常则包括指令非法、load/store 访问错误、ecall 等。驱动中最常见的异常是访问未映射的寄存器地址导致 load access fault,通常表现为内核 oops 并打印 sepc 与 stval。此时应检查设备树 reg 地址、时钟与电源域是否使能,而不是先怀疑驱动逻辑。

# 查看 RISC-V 中断控制器与中断号
cat /proc/interrupts
# 典型输出片段
           CPU0       CPU1
  1:       1234          0  SiFive PLIC  10  uart0
  3:        567          0  SiFive PLIC  14  mmc0
  5:         89          0  RISC-V INTC   5  riscv-timer

五、内存管理单元基础:Sv39 与地址映射

RV64 Linux 通常使用 Sv39 分页模式,虚拟地址 39 位,页表三级。内核空间与用户空间通过页表隔离,驱动中使用的 ioremap() 把物理 MMIO 地址映射到内核虚拟地址,之后才能用 readl()/writel() 访问。DMA 场景下,设备看到的地址是物理地址或 IOMMU 转换后的地址,驱动必须使用 dma_alloc_coherent() 或 dma_map_single() 获取正确的 DMA 地址,不能直接把虚拟地址交给设备。

此外,RISC-V 的页表项包含 A/D 位,若硬件不自动更新,内核需软件模拟。驱动若发现 DMA 数据不一致,应优先检查 cache 一致性与 dma_sync_single_for_cpu/device 的调用时机,而不是盲目修改页表。

RISC-V 驱动适配核心视图:特权级 · 中断 · 地址映射 M 级 Machine SBI 固件 / CLINT / 复位 ecall 陷入 · 定时器 · IPI S 级 Supervisor Linux 内核 / 驱动 request_irq · ioremap · DMA U 级 User 应用程序 系统调用进入 S 级 中断与异常路径 PLIC 外部中断 CLINT 定时/IPI 异常 fault/ecall Sv39 地址映射 虚拟地址 39 位 页表 三级 物理地址 MMIO/DDR 驱动适配关键动作 设备树匹配 compatible → request_irq 注册中断 → ioremap 映射寄存器 → dma_alloc_coherent 分配 DMA 缓冲 异常时优先检查:设备树 reg/interrupts、时钟电源、SBI 固件、cache 一致性

六、案例与误区复盘

某 RISC-V 开发板移植 UART 驱动时,probe 返回 -ENODEV。排查发现设备树中 compatible 写为 "vendor,uart-1.0",而驱动匹配表为 "vendor,uart",二者不一致。修正后 probe 成功,但收发仍异常,进一步发现 interrupts 属性引用了错误的 PLIC 中断号,导致 request_irq() 失败。这说明 RISC-V 驱动适配中,设备树与中断控制器的正确描述与驱动代码同等重要。

另一个常见误区是直接使用物理地址做 DMA。在 Sv39 下,CPU 访问的是虚拟地址,设备访问的是物理地址或 IOMMU 地址,二者不能混用。正确做法是使用 DMA API,并在需要时调用 dma_sync_single_for_cpu() 与 dma_sync_single_for_device() 维护一致性。

复盘要点:RISC-V 驱动问题往往不是“代码写错”,而是设备树、SBI 固件、中断路由、cache 一致性四者之一不匹配。先看 dmesg 中的 oops 地址与 stval,再核对设备树,通常比盲目改驱动更快定位。