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 ROM | 0x0000_0000 | 64 KB | /soc/rom@0 |
| CLINT | 0x0200_0000 | 64 KB | /soc/clint@2000000 |
| PLIC | 0x0C00_0000 | 64 MB | /soc/plic@c000000 |
| UART0 | 0x1000_0000 | 4 KB | /soc/serial@10000000 |
| GPIO | 0x1001_0000 | 4 KB | /soc/gpio@10010000 |
| DDR | 0x8000_0000 | 2 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。
.dts逐项核对。不要凭记忆写地址,一个字节的偏移就可能导致内核在启动时卡死。
riscv,ndev写成实际使用的中断数而非SoC支持的最大中断数;或者忘记在CPU节点下声明cpu0_intc,导致interrupts-extended引用失败。另一个高频错误是timebase-frequency与CLINT实际频率不符,表现为系统时间忽快忽慢。
五、从硬件到设备树的映射关系
把上述要素串起来,可以得到一张“硬件模块—设备树节点—关键属性”的对应图。理解这张图,后续写任何外设节点都只是填空。
这张图的核心价值在于:它把“硬件手册里的模块”和“设备树里的节点”一一对应起来。当你在调试串口无输出时,可以沿着“UART MMIO → serial节点 → interrupts/clocks属性 → PLIC/PRCI节点”这条链路逐级排查,而不是盲目改配置。
六、复盘:三个最容易踩的坑
- 中断号错位:PLIC的中断号从1开始,0保留。外设
interrupts = <10>对应PLIC第10号中断源,而不是第9个。写错会导致中断永远不触发。 - 时钟ID不匹配:时钟控制器的
#clock-cells决定引用格式。若为1,clocks = <&prci 3>;若为0,clocks = <&prci>。混用会编译报错或运行时取到错误频率。 - 地址空间重叠:CLINT、PLIC、UART的
reg区间不能重叠,且必须与SoC手册一致。内核在of_address_to_resource()阶段就会检测冲突,重叠会导致设备无法probe。