3、UART设备树绑定:设备树中UART节点的标准属性、解析与匹配流程

在嵌入式Linux系统中,设备树(Device Tree)是描述硬件拓扑结构的核心机制。UART控制器作为典型的串行通信外设,其设备树节点的正确绑定是驱动能够正常工作的前提。本章将深入解析UART节点中必须包含的标准属性,并详细阐述内核从设备树解析到驱动匹配的完整流程。

核心概念:设备树为UART驱动提供了“即插即用”的硬件描述能力。驱动开发者无需修改内核代码,只需调整设备树源文件(.dts),即可适配不同SoC上的UART外设。

3.1 UART设备树节点标准属性详解

一个标准的UART设备树节点通常位于SoC的APB(高级外设总线)或AHB(高级高性能总线)节点下。以下是一个典型的UART节点示例:

uart0: serial@10000000 {
    compatible = "vendor,soc-uart", "ns16550a";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;
    clocks = <&clk_uart0>;
    dmas = <&dma0 0>, <&dma0 1>;
    dma-names = "tx", "rx";
    clock-frequency = <1843200>;
    current-speed = <115200>;
    fifo-size = <16>;
    status = "okay";
};

下表详细列出了UART节点中每个标准属性的含义与作用:

属性名 类型 必需性 描述与典型值
compatible 字符串列表 必需 驱动匹配的关键标识。格式为"厂商,型号"。如"ns16550a"是通用16550A兼容UART,"vendor,soc-uart"是SoC定制UART。内核通过该属性查找对应的驱动。
reg 地址/长度对 必需 描述UART控制器的物理基地址和地址范围。如<0x10000000 0x1000>表示基地址0x10000000,映射4KB空间。
interrupts 中断描述符 必需 指定UART使用的中断号、触发类型和优先级。格式依赖中断控制器(如GIC)。<0 33 4>表示SPI类型、中断号33、高电平触发。
clocks phandle列表 强烈建议 指向UART模块的时钟源。用于驱动使能/关闭UART时钟,计算波特率。如<&clk_uart0>。
dmas / dma-names phandle列表 / 字符串列表 可选 描述DMA传输通道。dmas列出DMA控制器和通道号,dma-names为每个通道命名(如"tx"、"rx")。用于实现DMA模式的UART传输。
clock-frequency 整数 可选 直接指定UART输入时钟频率(Hz)。当clocks属性不可用时,驱动可读取此值计算波特率。
current-speed 整数 可选 默认的波特率设置,如115200。bootloader或内核早期初始化阶段使用。
fifo-size 整数 可选 硬件FIFO深度,如16、32、64。驱动据此配置FIFO触发阈值。
status 字符串 可选 设备状态。"okay"表示启用,"disabled"表示禁用。用于在板级文件中覆盖SoC默认配置。
实战要点:在编写UART设备树节点时,compatible属性应优先使用SoC厂商提供的专用字符串,其次才是通用兼容字符串(如"ns16550a")。这能确保驱动获得最佳的硬件特性支持。

3.2 设备树解析与驱动匹配流程

从设备树二进制文件(DTB)加载到UART驱动成功绑定的过程,涉及内核多个子系统的协作。下图展示了完整的解析与匹配流程:

UART设备树解析与驱动匹配流程图 1. Bootloader加载DTB 2. 内核OF API解析DTB 3. 创建device_node链表 4. platform总线匹配:compatible vs of_match_table 5. 调用UART驱动probe() 6. 读取reg/irq/clocks等资源 关键数据结构 • struct device_node • struct platform_device • struct of_device_id 匹配核心函数 • of_match_device() • platform_match()

上述流程中,第4步“平台总线匹配”是UART驱动绑定的核心环节。内核通过of_match_table中的compatible字符串列表与设备树节点的compatible属性进行逐项比对。匹配成功后,内核调用驱动的probe()函数,驱动在probe()中通过OF API获取reg、interrupts、clocks等资源,完成UART控制器的初始化。

3.3 驱动中的OF API解析示例

在UART驱动的probe()函数中,开发者使用以下OF API从设备树节点中提取配置信息:

static int my_uart_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct resource *res;
    int irq;
    u32 clock_freq;

    // 1. 获取reg属性中的内存资源(基地址+大小)
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENXIO;

    // 2. 获取中断号
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    // 3. 读取clock-frequency属性(可选)
    if (of_property_read_u32(np, "clock-frequency", &clock_freq) == 0) {
        dev_info(dev, "Clock frequency from DT: %u Hz\n", clock_freq);
    }

    // 4. 获取时钟(clocks属性)
    struct clk *clk = devm_clk_get(dev, NULL);
    if (IS_ERR(clk))
        return PTR_ERR(clk);

    // 5. 读取fifo-size属性(可选)
    u32 fifo_size;
    if (of_property_read_u32(np, "fifo-size", &fifo_size) == 0) {
        dev_info(dev, "FIFO size from DT: %u bytes\n", fifo_size);
    }

    // ... 后续初始化代码
    return 0;
}

// 驱动匹配表
static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,soc-uart", },
    { .compatible = "ns16550a", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

static struct platform_driver my_uart_driver = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = "my_uart",
        .of_match_table = my_uart_of_match,
    },
};
module_platform_driver(my_uart_driver);
常见陷阱:当设备树中同时存在clocks和clock-frequency属性时,驱动应优先使用clocks属性获取时钟框架提供的频率,因为时钟框架支持动态调频和电源管理。clock-frequency仅作为后备方案。

3.4 多实例UART的设备树组织

在实际SoC中,通常存在多个UART实例。设备树通过不同的reg地址和interrupts号来区分它们:

uart1: serial@10001000 {
    compatible = "vendor,soc-uart";
    reg = <0x10001000 0x1000>;
    interrupts = <0 34 4>;
    clocks = <&clk_uart1>;
    dmas = <&dma0 2>, <&dma0 3>;
    dma-names = "tx", "rx";
    fifo-size = <64>;
    status = "okay";
};

uart2: serial@10002000 {
    compatible = "vendor,soc-uart";
    reg = <0x10002000 0x1000>;
    interrupts = <0 35 4>;
    clocks = <&clk_uart2>;
    fifo-size = <16>;
    status = "disabled";  // 板级文件中可覆盖为"okay"
};

每个UART实例拥有独立的地址空间、中断线和时钟源。驱动通过platform_get_resource()和platform_get_irq()函数,根据pdev参数自动获取对应实例的资源,实现多实例复用同一驱动代码。

3.5 设备树属性与驱动数据结构的映射关系

下表总结了设备树属性与内核驱动中常用数据结构的对应关系:

设备树属性 内核数据结构/API 获取方式
compatible struct of_device_id 驱动匹配时自动比对
reg struct resource platform_get_resource(pdev, IORESOURCE_MEM, 0)
interrupts unsigned int irq platform_get_irq(pdev, 0)
clocks struct clk * devm_clk_get(dev, NULL) 或 of_clk_get(np, 0)
dmas struct dma_chan * dma_request_slave_channel(dev, "tx")
clock-frequency u32 of_property_read_u32(np, "clock-frequency", &val)
fifo-size u32 of_property_read_u32(np, "fifo-size", &val)
status 设备状态标志 内核在创建platform_device时自动过滤"disabled"节点
最佳实践:在编写UART驱动时,建议将所有可配置的硬件参数(如FIFO大小、波特率、流控模式)都通过设备树属性暴露出来。这样,同一个驱动二进制文件可以适配不同硬件版本的UART控制器,无需重新编译内核。