3. RISC-V硬件架构回顾

设备树不是凭空写出来的,它是对一颗SoC真实硬件拓扑的“声明式描述”。在动手写.dts之前,必须先把RISC-V SoC的硬件骨架理清楚:CPU如何切换特权级、中断从哪里来又送到哪里、外设寄存器挂在哪个物理地址、时钟和复位由谁提供。本节把这些要素串成一条主线,为后续的节点编写建立“硬件—设备树”的对应直觉。

一、RISC-V特权级:设备树里看不见,却决定一切

RISC-V定义了三个标准特权级:M(Machine,机器态)、S(Supervisor,监管态)、U(User,用户态)。M态是最高权限,负责最底层的机器模式寄存器和物理内存保护;S态是操作系统内核运行的地方;U态运行普通应用程序。Linux内核运行在S态,通过SBI(Supervisor Binary Interface)向M态固件(如OpenSBI)请求服务。

设备树中与特权级最直接相关的字段是riscv,isa和CPU节点的mmu-type,以及中断相关的interrupt-controller层级。例如:

cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    timebase-frequency = <10000000>;
    cpu@0 {
        device_type = "cpu";
        reg = <0>;
        status = "okay";
        compatible = "riscv";
        riscv,isa = "rv64imafdc";
        mmu-type = "riscv,sv39";
        clock-frequency = <1000000000>;
        cpu0_intc: interrupt-controller {
            #interrupt-cells = <1>;
            interrupt-controller;
            compatible = "riscv,cpu-intc";
        };
    };
};

注意timebase-frequency:它决定mtime计数器的频率,是CLINT定时器的基础,写错会导致系统时钟跑飞。而mmu-type决定页表级数,直接影响内核启动时的内存映射方式。

二、中断控制器:PLIC与CLINT的分工

RISC-V的中断体系由两个独立模块构成,这是与ARM GIC最大的不同:

  • CLINT(Core Local Interruptor):处理核间中断(IPI)和定时器中断(MTIP/STIP)。它连接每个hart的mtime、mtimecmp,是调度器心跳的来源。
  • PLIC(Platform-Level Interrupt Controller):处理外部设备中断,如UART、GPIO、以太网、DMA。它把几十上百个中断源汇聚,按优先级仲裁后分发给各个hart的M态或S态外部中断线。

设备树中,PLIC节点通常长这样:

plic: interrupt-controller@c000000 {
    compatible = "sifive,plic-1.0.0", "riscv,plic0";
    reg = <0x0 0xc000000 0x0 0x4000000>;
    #interrupt-cells = <1>;
    interrupt-controller;
    riscv,ndev = <53>;
    interrupts-extended = <
        &cpu0_intc 11 &cpu0_intc 9
        &cpu1_intc 11 &cpu1_intc 9
    >;
};

其中interrupts-extended把PLIC的S态外部中断(编号9)和M态外部中断(编号11)分别接到各CPU的本地中断控制器上。riscv,ndev声明中断源数量,写少了会导致高编号外设中断无法注册。外设节点则通过interrupt-parent = <&plic>和interrupts = <10>声明自己占用哪一路中断。

三、内存映射与时钟复位框架

RISC-V SoC的物理地址空间通常划分为:DDR主存区、外设寄存器区(MMIO)、Boot ROM区、CLINT/PLIC区。设备树用reg属性描述每个节点的基地址和长度,用ranges描述总线地址转换。一个典型的地址空间布局如下表:

区域起始地址大小设备树对应节点
Boot ROM0x0000_000064 KB/soc/rom@0
CLINT0x0200_000064 KB/soc/clint@2000000
PLIC0x0C00_000064 MB/soc/plic@c000000
UART00x1000_00004 KB/soc/serial@10000000
GPIO0x1001_00004 KB/soc/gpio@10010000
DDR0x8000_00002 GB/memory@80000000

时钟与复位框架在设备树中通过clocks、clock-names、resets、reset-names四个属性表达。SoC通常有一个时钟控制器(如compatible = "sifive,fu540-c000-prci")和一个复位控制器,外设节点引用它们:

uart0: serial@10000000 {
    compatible = "sifive,fu540-c000-uart", "sifive,uart0";
    reg = <0x0 0x10000000 0x0 0x1000>;
    interrupt-parent = <&plic>;
    interrupts = <10>;
    clocks = <&prci SIFIVE_FU540_C000_CLK_UART0>;
    clock-names = "baudclk";
    status = "disabled";
};

这里clocks的第二个参数是时钟ID,必须与时钟控制器的#clock-cells定义一致。如果时钟控制器声明#clock-cells = <1>,那么每个引用都要带一个ID;若为0,则只写phandle。这是初学者最容易写错的地方之一。

四、SoC典型外设与设备树节点速查

一颗典型RISC-V SoC(如SiFive FU540、StarFive JH7110、Allwinner D1)常见外设包括:UART、GPIO、I2C、SPI、SDIO、以太网、USB、PWM、Watchdog、RTC、DMA。它们在设备树中的共性写法是:compatible匹配驱动、reg声明寄存器、interrupts声明中断、clocks声明时钟、status控制使能。差异在于各外设特有的属性,例如以太网的phy-mode、SDIO的bus-width、I2C的clock-frequency。

实操建议:拿到一块新板子时,先读SoC手册的“Memory Map”章节,把CLINT、PLIC、UART、DDR的基地址抄下来,再对照厂商提供的参考.dts逐项核对。不要凭记忆写地址,一个字节的偏移就可能导致内核在启动时卡死。
常见误区:把PLIC的riscv,ndev写成实际使用的中断数而非SoC支持的最大中断数;或者忘记在CPU节点下声明cpu0_intc,导致interrupts-extended引用失败。另一个高频错误是timebase-frequency与CLINT实际频率不符,表现为系统时间忽快忽慢。

五、从硬件到设备树的映射关系

把上述要素串起来,可以得到一张“硬件模块—设备树节点—关键属性”的对应图。理解这张图,后续写任何外设节点都只是填空。

RISC-V SoC 硬件模块 → 设备树节点映射 硬件模块 CPU / Hart M / S / U 特权级 CLINT mtime / mtimecmp / IPI PLIC 外部中断汇聚与分发 时钟 / 复位控制器 PRCI / Reset 外设 MMIO UART / GPIO / I2C / SPI DDR 主存 0x8000_0000 起 设备树节点 /cpus/cpu@0 riscv,isa / mmu-type /soc/clint@2000000 compatible / reg /soc/plic@c000000 riscv,ndev / interrupts-extended /soc/prci@10000000 #clock-cells / #reset-cells /soc/serial@10000000 interrupts / clocks / status /memory@80000000 device_type / reg 关键属性与作用 timebase-frequency 决定 mtime 计数频率 interrupts-extended PLIC 到 CPU 本地中断线 riscv,ndev PLIC 支持的最大中断号 clocks / clock-names 外设时钟源与 ID interrupt-parent 指向 PLIC 或本地控制器 status = "okay" 使能或禁用节点

这张图的核心价值在于:它把“硬件手册里的模块”和“设备树里的节点”一一对应起来。当你在调试串口无输出时,可以沿着“UART MMIO → serial节点 → interrupts/clocks属性 → PLIC/PRCI节点”这条链路逐级排查,而不是盲目改配置。

六、复盘:三个最容易踩的坑

  1. 中断号错位:PLIC的中断号从1开始,0保留。外设interrupts = <10>对应PLIC第10号中断源,而不是第9个。写错会导致中断永远不触发。
  2. 时钟ID不匹配:时钟控制器的#clock-cells决定引用格式。若为1,clocks = <&prci 3>;若为0,clocks = <&prci>。混用会编译报错或运行时取到错误频率。
  3. 地址空间重叠:CLINT、PLIC、UART的reg区间不能重叠,且必须与SoC手册一致。内核在of_address_to_resource()阶段就会检测冲突,重叠会导致设备无法probe。