1. Linux设备树基础与内核启动流程
设备树(Device Tree)是Linux内核用于描述硬件配置的数据结构,它彻底改变了ARM架构下硬件描述的方式。在设备树出现之前,ARM架构的板级支持包(BSP)需要为每块开发板编写大量硬编码的C文件,导致内核充斥着各种board-*.c文件。设备树的引入实现了硬件描述与内核代码的分离,使得同一内核镜像可以支持多种硬件平台。
内核启动过程中,设备树的解析始于start_kernel()函数,这个函数会调用setup_arch()进行架构相关的初始化。对于ARM架构,关键调用链如下:
c复制start_kernel()
--> setup_arch()
--> setup_machine_fdt(__fdt_pointer)
--> unflatten_device_tree()
注意:__fdt_pointer是U-Boot传递给内核的设备树二进制(DTB)物理地址,这个地址在ARM64架构下通常通过x0寄存器传递,在ARM32架构下则通过r2寄存器传递。
2. 设备树解析的详细过程
2.1 设备树内存映射阶段
内核首先需要将设备树二进制数据从物理地址映射到内核虚拟地址空间。这个过程由setup_machine_fdt()完成,主要步骤包括:
- 检查设备树魔数(0xd00dfeed)
- 验证设备树版本兼容性
- 调用early_init_dt_verify()进行基础校验
- 将设备树映射到虚拟内存
c复制void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{
void *dt_virt;
int size;
dt_virt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);
if (!dt_virt)
return NULL;
memblock_reserve(dt_phys, size);
return dt_virt;
}
2.2 设备树展开过程
unflatten_device_tree()是解析过程的核心,它采用两阶段处理策略:
c复制void __init unflatten_device_tree(void)
{
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);
of_alias_scan(early_init_dt_alloc_memory_arch);
unittest_unflatten_overlay_base();
}
第一阶段遍历设备树计算内存需求,第二阶段实际构建device_node结构体。这种两阶段方法在内核中很常见,可以有效避免内存浪费。
2.3 device_node结构解析
设备树节点最终被转换为device_node结构体,其关键字段包括:
c复制struct device_node {
const char *name; // 节点名(如"i2c1")
const char *type; // 设备类型(已弃用)
phandle phandle; // 节点句柄
const char *full_name; // 完整路径名(如"/soc/i2c@400000")
struct property *properties; // 属性链表
struct property *deadprops; // 已删除属性
struct device_node *parent; // 父节点指针
struct device_node *child; // 子节点指针
struct device_node *sibling; // 兄弟节点指针
// 其他字段省略...
};
属性结构体property的定义如下:
c复制struct property {
char *name; // 属性名(如"compatible")
int length; // 属性值长度
void *value; // 属性值指针
struct property *next; // 下一个属性
unsigned long _flags; // 内部标志位
unsigned int unique_id; // 唯一ID
};
3. 设备驱动匹配机制深度解析
3.1 驱动注册流程
以PCI设备为例,驱动注册的核心调用链如下:
c复制pci_register_driver()
--> __pci_register_driver()
--> driver_register()
--> bus_add_driver()
--> driver_attach()
--> __driver_attach()
--> driver_match_device()
--> drv->bus->match(dev, drv)
对于设备树设备,match函数最终会调用of_driver_match_device()。
3.2 设备树匹配机制
设备树匹配的核心是比较驱动程序的of_match_table和设备的compatible属性:
c复制static int of_driver_match_device(struct device *dev, struct device_driver *drv)
{
return of_match_device(drv->of_match_table, dev) != NULL;
}
of_match_table通常定义如下:
c复制static const struct of_device_id imx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
{ .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
{ /* sentinel */ }
};
匹配过程会遍历设备树节点的compatible属性,与驱动of_match_table中的条目逐一比较:
c复制const struct of_device_id *of_match_node(const struct of_device_id *matches,
const struct device_node *node)
{
if (!matches)
return NULL;
while (matches->name[0] || matches->type[0] || matches->compatible[0]) {
int match = 1;
if (matches->name[0])
match &= node->name && !strcmp(matches->name, node->name);
if (matches->type[0])
match &= node->type && !strcmp(matches->type, node->type);
if (matches->compatible[0])
match &= __of_device_is_compatible(node, matches->compatible,
matches->type, matches->name);
if (match)
return matches;
matches++;
}
return NULL;
}
3.3 匹配优先级规则
内核设备匹配遵循以下优先级:
- 设备树匹配(of_match_table)
- ACPI匹配
- ID表匹配(如pci_device_id)
- 名称匹配(dev->name与drv->name)
4. 设备树与驱动开发实战技巧
4.1 设备树编写规范
良好的设备树编写应遵循以下原则:
- 使用标准的compatible字符串格式:"厂商,设备型号"
- 合理组织节点层次结构
- 正确使用地址和大小表示(#address-cells、#size-cells)
- 为节点添加有意义的标签(label)
示例:
dts复制// 好的写法
i2c1: i2c@400000 {
compatible = "nxp,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x400000 0x4000>;
interrupts = <0 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks 125>;
status = "okay";
eeprom: at24@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
4.2 驱动开发中的设备树处理
在驱动中获取设备树数据的常用API:
c复制// 获取兼容性字符串
const char *compat = of_get_property(node, "compatible", NULL);
// 读取reg属性
u32 reg[2];
of_property_read_u32_array(node, "reg", reg, 2);
// 获取中断号
int irq = irq_of_parse_and_map(node, 0);
// 获取时钟
struct clk *clk = of_clk_get(node, 0);
// 获取GPIO
int gpio = of_get_named_gpio(node, "enable-gpio", 0);
4.3 调试技巧
设备树相关调试方法:
-
查看解析后的设备树:
bash复制ls /proc/device-tree/ cat /proc/device-tree/model -
内核启动参数添加
of=prune可以裁剪未使用的设备树节点 -
使用dtc工具反编译DTB:
bash复制
dtc -I dtb -O dts -o output.dts /boot/board.dtb -
内核配置开启CONFIG_OF_OVERLAY支持动态设备树
5. 常见问题与解决方案
5.1 驱动未能正确匹配
可能原因及排查步骤:
- 检查dmesg是否有"no matching node found"警告
- 确认设备树节点的compatible属性与驱动完全一致
- 检查设备树节点status是否为"okay"
- 确认驱动是否编译进内核或正确加载
5.2 资源获取失败
典型错误处理:
c复制res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "failed to get memory resource\n");
return -ENXIO;
}
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "failed to get IRQ: %d\n", irq);
return irq;
}
5.3 属性读取错误
安全读取属性的最佳实践:
c复制u32 val;
int ret;
ret = of_property_read_u32(node, "clock-frequency", &val);
if (ret) {
dev_warn(dev, "clock-frequency not found, using default\n");
val = DEFAULT_CLOCK;
}
6. 性能优化建议
- 延迟初始化:对于非关键资源,可以使用deferred_probe机制
- 合理使用_OF_DECLARE宏替代module_init来初始化早期设备
- 避免在probe函数中执行耗时操作
- 对频繁访问的设备树属性进行缓存
c复制static int my_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct my_data *data;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 缓存常用属性
of_property_read_u32(np, "sample-rate", &data->sample_rate);
of_property_read_u32(np, "buffer-size", &data->buf_size);
// 其他初始化...
}
在实际项目中,我发现设备树的合理使用可以显著提高驱动的可移植性。曾经在一个项目中,通过将硬件相关的配置全部移到设备树中,我们实现了同一驱动支持三个不同硬件版本的目标,大大减少了维护成本。
