1. 设备树与驱动开发的基础概念
在嵌入式Linux开发中,设备树(Device Tree)和驱动程序的配合使用是一个非常重要的环节。设备树就像是一张硬件地图,它详细描述了系统中所有硬件设备的配置信息。而驱动程序则是操作这些硬件的软件接口。
我刚开始接触设备树时,常常困惑于硬件描述和驱动代码之间是如何衔接的。特别是在处理内存映射(reg)和中断(irq)这类关键资源时,经常遇到驱动无法正确获取资源的问题。后来通过研究platform_get_resource这个关键API,才真正理解了内核是如何把设备树中的硬件描述转换成驱动可用的资源。
设备树中关于资源描述最常见的有两种:
- reg属性:描述设备的内存映射区域,包括起始地址和长度
- interrupts属性:描述设备使用的中断号
举个例子,一个简单的设备树节点可能长这样:
my_device { compatible = "vendor,my-device"; reg = <0x10000000 0x1000>; interrupts = <15>; };这个节点描述了一个设备,它的寄存器空间从0x10000000开始,大小是0x1000字节,使用的中断号是15。驱动需要获取这些信息才能正确操作硬件。
2. platform_get_resource的工作原理
2.1 资源获取的基本流程
platform_get_resource是驱动开发者最常用的API之一,它的作用是从platform_device中获取指定的资源。在实际项目中,我发现这个API背后其实隐藏着复杂的处理逻辑。
先来看一个典型的使用场景:
struct resource *res; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "Failed to get MEM resource\n"); return -ENODEV; }这段代码尝试获取platform_device的第一个内存资源。内核处理这个请求的完整流程可以分为几个关键步骤:
- 内核启动时解析设备树,创建platform_device结构体
- 将设备树中的reg和interrupts属性转换为标准的resource结构
- 驱动调用platform_get_resource获取这些资源
- 内核根据资源类型和索引返回对应的resource结构
2.2 内存资源的解析细节
对于内存资源(IORESOURCE_MEM),内核主要通过of_address_to_resource函数来完成转换。这个函数会处理设备树中的reg属性,将其转换为resource结构。
我曾在调试一个PCIe驱动时,发现设备无法正常工作,最终发现是因为reg属性解析出错。通过深入研究,发现of_address_to_resource内部会调用__of_address_to_resource,这个函数做了几件重要的事情:
- 解析reg属性的地址和大小
- 处理地址映射和转换(如果需要)
- 设置resource结构的start和end字段
- 处理可选的reg-names属性
一个典型的reg属性可能包含多个地址范围:
reg = <0x10000000 0x1000>, // 寄存器区域1 <0x20000000 0x2000>; // 寄存器区域2驱动可以通过索引来获取不同的区域:
res1 = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第一个区域 res2 = platform_get_resource(pdev, IORESOURCE_MEM, 1); // 获取第二个区域3. 中断资源的处理机制
3.1 中断号的获取与转换
中断资源的处理比内存资源更为复杂。在设备树中,中断通常通过interrupts属性描述,但驱动最终需要的是Linux内核的虚拟中断号。
platform_get_irq是获取中断号的主要接口,它的典型用法如下:
int irq = platform_get_irq(pdev, 0); if (irq < 0) { dev_err(&pdev->dev, "Failed to get IRQ resource\n"); return irq; }这个函数背后调用了of_irq_get来完成实际的转换工作。我曾在调试一个GPIO中断时发现,设备树中定义的中断号与驱动获取到的并不相同。这是因为内核在启动过程中会对中断号进行重新映射。
3.2 中断资源的早期处理
在内核启动的早期阶段,of_platform_bus_create函数会处理设备树节点,创建对应的platform_device。对于中断资源,它会调用of_irq_to_resource_table函数:
static void of_device_alloc(struct device_node *np, ...) { // 处理内存资源 for (i = 0; i < num_reg; i++, res++) { rc = of_address_to_resource(np, i, res); WARN_ON(rc); } // 处理中断资源 if (of_irq_to_resource_table(np, res, num_irq) != num_irq) pr_debug("not all legacy IRQ resources mapped for %s\n", np->name); }这个函数最终会调用of_irq_get来获取实际的中断号,并填充resource结构。值得注意的是,对于中断资源,resource的start和end字段都会被设置为相同的中断号。
4. PCIe设备树资源的特殊处理
4.1 ranges属性的解析
PCIe设备的资源处理有其特殊性,主要体现在设备树中的ranges属性上。一个典型的PCIe节点可能包含如下定义:
pcie0: pcie@0xd4288000 { #address-cells = <3>; #size-cells = <2>; reg = <0xd4210000 0x800>, <0xd4288000 0x1000>; reg-names = "pciephy", "pciectrl"; ranges = <0x81000000 0 0 0xE0010000 0 0x00010000 0x82000000 0 0xE0020000 0xE0020000 0 0x04000000>; interrupts = <18>; };ranges属性描述了PCI地址空间到CPU地址空间的映射关系。在内核中,这个属性的解析是通过of_pci_range_parser_init和for_each_of_pci_range函数完成的。
4.2 PCIe资源转换实例
在PCIe主机控制器驱动中,通常会看到这样的资源处理代码:
struct of_pci_range range; struct of_pci_range_parser parser; if (of_pci_range_parser_init(&parser, np)) { dev_err(pp->dev, "missing ranges property\n"); return -EINVAL; } for_each_of_pci_range(&parser, &range) { unsigned long restype = range.flags & IORESOURCE_TYPE_BITS; if (restype == IORESOURCE_IO) { // 处理I/O空间 of_pci_range_to_resource(&range, np, &pp->io); pp->io.start = range.pci_addr + global_io_offset; pp->io.end = pp->io.start + range.size - 1; } if (restype == IORESOURCE_MEM) { // 处理内存空间 of_pci_range_to_resource(&range, np, &pp->mem); pp->mem.start = range.cpu_addr; pp->mem.end = pp->mem.start + range.size - 1; } }这段代码展示了如何将设备树中的ranges属性转换为驱动可用的资源信息。for_each_of_pci_range宏会遍历所有的地址范围,根据flags字段区分I/O空间和内存空间,然后分别处理。
5. 实际开发中的经验与技巧
5.1 常见问题排查
在开发过程中,经常会遇到资源获取失败的情况。根据我的经验,可以从以下几个方面排查:
- 检查设备树节点:确认reg和interrupts属性是否正确定义
- 验证compatible字符串:确保驱动和设备树的compatible匹配
- 检查资源索引:platform_get_resource的索引要从0开始顺序使用
- 查看内核启动日志:搜索设备节点名,看是否有资源分配错误
我曾经遇到过一个案例,驱动无法获取中断资源,最终发现是因为设备树中的interrupt-parent设置不正确。这种情况下,内核日志通常会给出相关提示。
5.2 调试技巧
为了更直观地了解资源分配情况,可以使用以下方法:
查看/sys文件系统:
cat /sys/devices/platform/<device>/resource这个文件会显示设备的所有资源信息
使用devicetree编译器:
dtc -I fs /sys/firmware/devicetree/base可以查看内核实际使用的设备树
添加调试打印:在驱动probe函数中添加资源信息的打印
dev_info(&pdev->dev, "Registers: %pr\n", platform_get_resource(pdev, IORESOURCE_MEM, 0)); dev_info(&pdev->dev, "IRQ: %d\n", platform_get_irq(pdev, 0));5.3 性能优化建议
在处理大量资源时,需要注意以下几点:
- 避免重复获取资源:在probe函数中获取资源后保存到设备私有数据结构中
- 合理使用devm_接口:自动管理资源释放,减少错误可能性
- 注意资源冲突:特别是中断共享时,要正确设置IRQF_SHARED标志
在开发一个复杂的多功能设备驱动时,我最初每次操作都重新获取资源,导致性能下降。后来改为在probe时一次性获取并保存所有资源,性能得到了显著提升。