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 访问函数,通常已由内核处理字节序与屏障问题,不应直接用指针解引用访问设备寄存器。
二、特权级架构: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 名 | 用途 | 驱动关注点 |
|---|---|---|---|
| x0 | zero | 恒为 0 | 不可写 |
| x1 | ra | 返回地址 | 异常入口需保存 |
| x2 | sp | 栈指针 | 中断上下文切换 |
| x10-x11 | a0-a1 | 参数/返回值 | 系统调用与 SBI 调用 |
| x8 | s0/fp | 帧指针 | 栈回溯 |
| x5 | t0 | 临时寄存器 | 内联汇编需自行保存 |
四、中断与异常模型: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 开发板移植 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() 维护一致性。
dmesg 中的 oops 地址与 stval,再核对设备树,通常比盲目改驱动更快定位。