4. 设备树实战:编写一个简单的I2C设备节点、SPI设备节点、MMIO设备节点
设备树(Device Tree)是嵌入式Linux系统中描述硬件拓扑结构的核心机制。本章通过三个实战案例,深入讲解I2C、SPI和MMIO设备节点的编写规范、属性含义及与驱动程序的绑定关系。掌握这些基础节点类型,是构建复杂设备树和实现驱动兼容设计的起点。
实战目标:独立编写一个包含I2C温度传感器、SPI Flash存储器、MMIO GPIO控制器的设备树源文件(.dts),并理解每个节点如何与对应的内核驱动匹配。
4.1 I2C设备节点实战
I2C(Inter-Integrated Circuit)是一种同步、多主从、串行通信总线,广泛用于连接低速外设。在设备树中,I2C设备节点作为其控制器节点的子节点存在。
4.1.1 节点结构与关键属性
一个标准的I2C设备节点必须包含以下属性:
| 属性 | 含义 | 示例值 | 是否必须 |
|---|---|---|---|
compatible |
设备兼容字符串,用于驱动匹配 | "ti,tmp102" |
是 |
reg |
I2C从设备地址(7位或10位) | <0x48> |
是 |
interrupt-parent |
中断控制器父节点phandle | &gpio1 |
否 |
interrupts |
中断号与触发类型 | <5 IRQ_TYPE_EDGE_FALLING> |
否 |
status |
设备状态(okay/disabled) | "okay" |
<>否
4.1.2 实战案例:TMP102温度传感器
以下是一个完整的I2C温度传感器节点定义,挂载在I2C控制器i2c0下:
/* I2C控制器节点 */
&i2c0 {
status = "okay";
clock-frequency = <100000>; /* 100kHz标准模式 */
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
/* TMP102温度传感器子节点 */
tmp102: temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
#thermal-sensor-cells = <0>;
status = "okay";
};
};
关键点:I2C地址
0x48是7位地址,在设备树中直接写入。驱动通过of_i2c_register_devices()解析子节点,并调用i2c_new_client_device()创建设备。compatible字符串必须与驱动中of_match_table完全匹配。
4.1.3 I2C节点与驱动匹配流程
下图展示了设备树I2C节点与内核驱动之间的匹配过程:
4.2 SPI设备节点实战
SPI(Serial Peripheral Interface)是一种高速、全双工、同步串行总线,通常用于Flash、ADC、显示控制器等设备。SPI设备节点同样作为控制器节点的子节点存在,但属性与I2C有所不同。
4.2.1 SPI节点特有属性
| 属性 | 含义 | 示例值 | 说明 |
|---|---|---|---|
reg |
片选(CS)线编号 | <0> |
对应硬件CS0 |
spi-max-frequency |
设备支持的最大SPI时钟频率 | <50000000> |
单位Hz,50MHz |
spi-cpol |
时钟极性(无值属性) | 存在即表示CPOL=1 | 可选 |
spi-cpha |
时钟相位(无值属性) | 存在即表示CPHA=1 | 可选 |
spi-cs-high |
片选高电平有效 | 存在即表示高有效 | 默认低有效 |
4.2.2 实战案例:W25Q128 SPI Flash
以下是一个SPI NOR Flash节点定义,挂载在SPI控制器spi0下:
/* SPI控制器节点 */
&spi0 {
status = "okay";
num-cs = <2>; /* 支持2个片选 */
cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>,
<&gpio0 11 GPIO_ACTIVE_LOW>;
/* W25Q128 SPI Flash子节点 */
flash: flash@0 {
compatible = "winbond,w25q128", "jedec,spi-nor";
reg = <0>; /* 使用CS0 */
spi-max-frequency = <50000000>;
spi-cpol; /* 时钟极性=1 */
spi-cpha; /* 时钟相位=1 */
#address-cells = <1>;
#size-cells = <1>;
/* Flash分区定义 */
partition@0 {
label = "bootloader";
reg = <0x00000000 0x00100000>; /* 1MB */
};
partition@1 {
label = "kernel";
reg = <0x00100000 0x00500000>; /* 5MB */
};
partition@2 {
label = "rootfs";
reg = <0x00600000 0x00A00000>; /* 10MB */
};
};
};
注意事项:SPI设备的
compatible通常包含两个字符串:第一个是具体型号(如"winbond,w25q128"),第二个是通用兼容类型(如"jedec,spi-nor")。内核驱动匹配时优先使用第一个,失败后尝试第二个。这种设计提高了设备树的兼容性和复用性。
4.2.3 SPI节点与驱动匹配流程
4.3 MMIO设备节点实战
MMIO(Memory-Mapped I/O)设备通过物理地址空间直接映射到CPU的内存访问域,是最常见的设备类型。设备树中MMIO节点通常包含reg属性描述地址范围,以及interrupts属性描述中断资源。
4.3.1 MMIO节点核心属性
| 属性 | 含义 | 格式 | 示例 |
|---|---|---|---|
reg |
寄存器地址与大小 | <地址 大小> |
<0x02000000 0x1000> |
interrupts |
中断号与触发类型 | <中断号 触发类型> |
<31 IRQ_TYPE_LEVEL_HIGH> |
clocks |
时钟源phandle | <&clk_gate> |
<&clk_uart> |
resets |
复位控制器phandle | <&rst_ctrl 0> |
可选 |
4.3.2 实战案例:GPIO控制器
以下是一个简单的GPIO控制器节点,基于MMIO方式访问寄存器:
/* GPIO控制器节点 */
gpio0: gpio@02000000 {
compatible = "vendor,gpio-controller";
reg = <0x02000000 0x1000>; /* 4KB寄存器空间 */
interrupts = <31 IRQ_TYPE_LEVEL_HIGH>;
interrupt-controller;
#interrupt-cells = <2>; /* 中断类型+触发方式 */
gpio-controller;
#gpio-cells = <2>; /* GPIO号+标志 */
ngpios = <32>; /* 32个GPIO引脚 */
gpio-ranges = <&pinctrl 0 0 32>; /* 引脚复用映射 */
clocks = <&clk_gpio>;
status = "okay";
};
4.3.3 MMIO节点地址映射与驱动访问
驱动通过devm_platform_ioremap_resource()将设备树中的reg地址映射到内核虚拟地址空间:
static int gpio_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
/* 获取并映射reg属性中的第一个地址范围 */
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
/* 获取中断号 */
int irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* 读取GPIO方向寄存器(假设偏移0x00) */
u32 dir = readl(base + 0x00);
/* 设置GPIO输出值(假设偏移0x04) */
writel(0x1, base + 0x04);
return 0;
}
static const struct of_device_id gpio_of_match[] = {
{ .compatible = "vendor,gpio-controller" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_of_match);
static struct platform_driver gpio_driver = {
.probe = gpio_probe,
.driver = {
.name = "vendor-gpio",
.of_match_table = gpio_of_match,
},
};
module_platform_driver(gpio_driver);
MMIO驱动要点:使用
devm_platform_ioremap_resource()自动处理资源管理和内存释放。中断号通过platform_get_irq()获取,无需手动解析设备树。驱动通过of_match_table与设备树节点的compatible属性匹配。
4.3.4 MMIO设备节点与驱动匹配流程
4.4 三种设备节点对比总结
| 对比维度 | I2C设备节点 | SPI设备节点 | MMIO设备节点 |
|---|---|---|---|
| 总线类型 | I2C总线(i2c_bus_type) | SPI总线(spi_bus_type) | 平台总线(platform_bus_type) |
| reg属性含义 | I2C从设备地址 | 片选线编号 | 物理地址与大小 |
| 设备创建函数 | of_i2c_register_devices() |
of_register_spi_devices() |
of_platform_populate() |
| 驱动资源获取 | i2c_client结构体 |
spi_device结构体 |
platform_get_resource() |
| 典型设备 | 传感器、RTC、EEPROM | Flash、ADC、显示屏 | GPIO、UART、DMA控制器 |
| 地址空间 | 逻辑地址(7/10位) | 逻辑编号(CS0/CS1) | 物理地址(MMIO) |
4.5 实战避坑指南
常见错误与解决方案:
- compatible字符串不匹配:驱动中的
of_match_table必须与设备树中的compatible完全一致,包括大小写和厂商前缀。 - I2C地址错误:7位地址左对齐写入,不要包含读写位(最低位)。例如设备地址0x90(8位)对应设备树中的0x48(7位)。
- SPI模式配置遗漏:未设置
spi-cpol/spi-cpha时,内核默认使用SPI模式0(CPOL=0, CPHA=0),可能导致通信失败。 - MMIO地址范围不足:
reg中的size必须覆盖设备所有寄存器,通常为4KB对齐。过小会导致ioremap失败或访问越界。 - 中断号配置错误:中断号是硬件中断号,不是IRQ号。需要参考SoC数据手册,并确认
interrupt-parent指向正确的中断控制器。
4.6 本章小结
本章通过三个实战案例,详细讲解了I2C、SPI和MMIO设备节点的编写方法。核心要点包括:
- I2C节点以
reg表示从设备地址,驱动通过i2c_client访问设备。 - SPI节点以
reg表示片选编号,通过spi-max-frequency和模式属性配置通信参数。 - MMIO节点以
reg表示物理地址范围,驱动通过devm_platform_ioremap_resource()映射寄存器。 - 所有节点都依赖
compatible属性与驱动of_match_table匹配。
掌握这三种基础节点类型,为后续学习复杂设备树(如时钟树、中断控制器、DMA控制器)和ACPI驱动兼容设计打下坚实基础。