设备树的概念(四):Platform设备驱动和设备树

平台驱动程序也与DTs一起工作。这是目前处理平台设备的推荐方式,不再需要使用板级文件,当设备的属性发生变化时也不需要重新编译内核。可以使用OF匹配表进行匹配,这是一种基于DT的匹配机制。下面让我们看看它是如何工作的。

OF匹配表方式

OF匹配表匹配是Platform核心执行的第一个匹配机制,目的是将设备与其驱动程序进行匹配。它使用一个设备树的compatible属性来匹配of_match_table中的设备条目,这是struct driver子结构的一个字段。每个设备节点都有一个compatible属性,它是一个字符串或字符串列表。任何声明compatible属性中列出的字符串之一的平台驱动程序都将触发匹配,并将看到其probe函数被执行。

DT匹配项在内核中被描述为struct of_device_id结构的实例,该结构在linux/mod_devictable .h中定义,如下所示:

// we are only interested in the two last elements of the structure
struct of_device_id {
    [...]
    char compatible[128];
    const void *data;
};

下面是结构中每个元素的含义:

  • char compatible[128]: 用于匹配DT中设备节点的compatible属性的字符串。在发生匹配之前,它们必须是相同的。
  • const void *data: 可以指向任何结构,可以用作每个设备类型的配置数据。

由于of_match_table是一个指针,你可以传递一个struct of_device_id的数组来让你的驱动程序兼容多个设备:

static const struct of_device_id imx_uart_dt_ids[] = {
    { .compatible = "fsl,imx6q-uart", },
    { .compatible = "fsl,imx1-uart", },
    { .compatible = "fsl,imx21-uart", },
    { /* sentinel */ }
};

一旦你填充了你的id数组,它必须被传递到你的平台驱动的of_match_table字段,在driver子结构中:

static struct platform_driver serial_imx_driver = {
    [...]
    .driver = {
        .name = "imx-uart",
        .of_match_table = imx_uart_dt_ids,
        [...]
    },
};

在这一步,只有你的驱动程序知道你的of_device_id数组。为了让内核也得到通知(这样它就可以在platform核心维护的设备列表中存储你的id),你的数组必须在MODULE_DEVICE_TABLE中注册,如下:

MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);

就这些!我们的驱动程序是DT兼容的。回到DT中,让我们声明一个与驱动程序兼容的设备:

uart1: serial@02020000 {
    compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x02020000 0x4000>;
    interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
    [...]
};

这里提供了两个compatible字符串。如果第一个字符串不匹配任何驱动程序,核心将使用第二个字符串继续进行匹配。

当匹配发生时,你的驱动程序的probe函数被调用,probe用一个struct platform_device结构体作为参数,它包含一个struct device dev字段,其中有一个struct device_node *of_node字段,对应于与我们的设备关联的节点,所以你可以使用它来提取设备设置:

static int serial_imx_probe(struct platform_device *pdev)
{
    [...]
    struct device_node *np;
    np = pdev->dev.of_node;
if (of_get_property(np, "fsl,dte-mode", NULL)) sport->dte_mode = 1; [...] }

你可以检查DT节点是否被设置为知道驱动程序作为一个of_match的响应被加载 ,或者从板子的init文件中实例化。然后你应该使用of_match_device函数,以选择产生匹配的struct *of_device_id条目,它可能包含你传递的特定数据:

static int my_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    const struct of_device_id *match;

    match = of_match_device(imx_uart_dt_ids, &pdev->dev);
    if (match) {
        /* Devicetree, extract the data */
        my_data = match->data;
    } else {
        /* Board init file */
        my_data = dev_get_platdata(&pdev->dev);
    }
    [...]
}

处理非设备树平台

在内核中通过CONFIG_OF选项启用了DT支持。当内核中没有启用DT API支持时,你可能希望避免使用它。实现这一点的方法是检查是否设置了CONFIG_OF。我们通常这样做:

#ifdef CONFIG_OF
static const struct of_device_id imx_uart_dt_ids[] = {
    { .compatible = "fsl,imx6q-uart", },
    { .compatible = "fsl,imx1-uart", },
    { .compatible = "fsl,imx21-uart", },
    { /* sentinel */ }
};

/* other device tree dependent code */ [...] #endif

即使在缺少设备树支持时总是定义of_device_id数据类型,但包含在#ifdef CONFIG_OF…#endif中的代码将在构建过程中被忽略。这用于条件编译。这不是你唯一的选择;还有of_match_ptr宏,当OF被禁用时,它简单地返回NULL。在任何地方,你都需要传递你的of_match_table作为参数;它应该被包装在of_match_ptr宏中,这样当OF被禁用时它就会返回NULL。宏定义在include/linux/of.h中:

#define of_match_ptr(_ptr) (_ptr) /* When CONFIG_OF is enabled */
#define of_match_ptr(_ptr) NULL /* When it is not */

我们可以这样使用它:

static int my_probe(struct platform_device *pdev)
{
    const struct of_device_id *match;
    match = of_match_device(of_match_ptr(imx_uart_dt_ids), &pdev->dev);
    [...]
}

static struct platform_driver serial_imx_driver = {
    [...]
    .driver = {
        .name = "imx-uart",
        .of_match_table = of_match_ptr(imx_uart_dt_ids),
    },
}

of_match_ptr这个宏避免了使用#ifdef,当OF被禁用时返回NULL。

支持多个硬件设备,每个设备都有特定的数据

有时,一个驱动程序可以支持不同的硬件,每种硬件都有其特定的配置数据。这些数据可以是专用的函数表、特定的寄存器值或每个设备特有的任何东西。下面的例子描述了一个通用方法:

首先让我们记住include/linux/mod_devictable.h中的struct of_device_id是什么样的:

/*
* Struct used for matching a device
*/
struct of_device_id {
    [...]
    char compatible[128];
    const void *data;
};

我们感兴趣的字段是const void *data,因此我们可以使用它来传递每个特定设备的任何数据。

假设我们拥有三个不同的设备,每个设备都有特定的私人数据。of_device_id.data将包含一个指向特定参数的指针。这个例子参考了drivers/tty/serial/imx.c。

首先,我们声明私有结构:

/* i.MX21 type uart runs on all i.mx except i.MX1 and i.MX6q */
enum imx_uart_type {
    IMX1_UART,
    IMX21_UART,
    IMX6Q_UART,
};

/* device type dependent stuff */
struct imx_uart_data {
    unsigned uts_reg;
    enum imx_uart_type devtype;
};

然后,我们用特定于设备的数据填充数组:

static struct imx_uart_data imx_uart_devdata[] = {
    [IMX1_UART] = {
        .uts_reg = IMX1_UTS,
        .devtype = IMX1_UART,
    },
    [IMX21_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX21_UART,
    },
    [IMX6Q_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX6Q_UART,
    },
};    

每个兼容的条目都与一个特定的数组索引绑定:

static const struct of_device_id imx_uart_dt_ids[] = {
    { .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
    { .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
    { .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);

static struct platform_driver serial_imx_driver = {
    [...]
    .driver = {
        .name = "imx-uart",
        .of_match_table = of_match_ptr(imx_uart_dt_ids),
    },
};

现在在probe函数中,无论匹配项是什么,它都会保存一个指向设备特定结构的指针:

static int imx_probe_dt(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    const struct of_device_id *of_id =
        of_match_device(of_match_ptr(imx_uart_dt_ids), &pdev->dev);
  if (!of_id)
    /* no device tree device */
    return 1;

  [...]   sport
->devdata = of_id->data; /* Get private data back */ }

在上面的代码中,devdata是原始源代码中结构的一个元素,并声明为const struct imx_uart_data *devdata;我们可以在数组中存储任何特定的参数。

混合匹配方法

OF匹配表可以与任何其他匹配机制组合。在下面的例子中,我们混合了DT和设备ID匹配方法:

我们为设备ID匹配方法填充一个数组,每个设备都有它的数据:

static const struct platform_device_id sdma_devtypes[] = {
    {
        .name = "imx51-sdma",
        .driver_data = (unsigned long)&sdma_imx51,
    }, {
        .name = "imx53-sdma",
        .driver_data = (unsigned long)&sdma_imx53,
    }, {
        .name = "imx6q-sdma",
        .driver_data = (unsigned long)&sdma_imx6q,
    }, {
        .name = "imx7d-sdma",
        .driver_data = (unsigned long)&sdma_imx7d,
    }, {
        /* sentinel */
    }
};
MODULE_DEVICE_TABLE(platform, sdma_devtypes);

我们对OF匹配表方法做同样的操作:

static const struct of_device_id sdma_dt_ids[] = {
    { .compatible = "fsl,imx6q-sdma", .data = &sdma_imx6q, },
    { .compatible = "fsl,imx53-sdma", .data = &sdma_imx53, },
    { .compatible = "fsl,imx51-sdma", .data = &sdma_imx51, },
    { .compatible = "fsl,imx7d-sdma", .data = &sdma_imx7d, },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sdma_dt_ids);

probe函数如下所示:

static int sdma_probe(struct platform_device *pdev)
{
    const struct of_device_id *of_id =
        of_match_device(of_match_ptr(sdma_dt_ids), &pdev->dev);
    struct device_node *np = pdev->dev.of_node;

    /* If devicetree, */
    if (of_id)
        drvdata = of_id->data;
    /* else, hard-coded */
    else if (pdev->id_entry)
        drvdata = (void *)pdev->id_entry->driver_data;

    if (!drvdata) {
        dev_err(&pdev->dev, "unable to find driver data\n");
        return -EINVAL;
    }
    [...]
}

然后,我们声明平台驱动程序:

static struct platform_drivers dma_driver = {
    .driver = {
        .name = "imx-sdma",
        .of_match_table = of_match_ptr(sdma_dt_ids),
    },
    .id_table = sdma_devtypes,
    .remove = sdma_remove,
    .probe = sdma_probe,
};
module_platform_driver(sdma_driver);

Platform资源和DTs

平台设备可以与支持DT的系统一起工作,而无需任何额外的修改。通过使用platform_xxx族函数,核心会遍历DT(使用of_xxx族函数)以查找所请求的资源。

反之则不成立,因为of_xxx族函数仅为DT预留。所有资源数据将以通常的方式提供给驱动程序。驱动程序现在知道这个设备是否在板级文件中使用hardcode参数进行了初始化。我们以uart设备节点为例:

uart1: serial@02020000 {
    compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x02020000 0x4000>;
    interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
    dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
    dma-names = "rx", "tx";
};

以下摘录描述了其驱动程序的探测函数。在probe函数中,函数platform_get_resource()可以用来提取任何资源属性(内存区域,dma, irq),或者一个特定的函数,比如platform_get_irq(),它提取DT中interrupts属性提供的irq:

static int my_probe(struct platform_device *pdev)
{
  struct iio_dev *indio_dev;
  struct resource *mem, *dma_res;
  struct xadc *xadc;
  int irq, ret, dmareq;

  /* irq */   irq = platform_get_irq(pdev, 0);   if (irq<= 0)     return -ENXIO;
  [...]
  
/* memory region */   mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);   xadc->base = devm_ioremap_resource(&pdev->dev, mem);   
  /*    * We could have used    * devm_ioremap(&pdev->dev, mem->start, resource_size(mem));    * too.    */   if (IS_ERR(xadc->base))     return PTR_ERR(xadc->base);
  [...]  
  /* second dma channel */   dma_res = platform_get_resource(pdev, IORESOURCE_DMA, 1);   dmareq = dma_res->start;   [...] }

总而言之,对于dma、irq和mem这样的属性,在平台驱动程序中不需要做任何事情来匹配dtb。如果你还记得,该数据与你可以作为平台资源传递的数据是同一类型的。为了理解其中的原因,我们只需要看看这些函数的内部;我们会看到它们内部是如何处理DT函数的。下面是platform_get_irq函数的示例:

int platform_get_irq(struct platform_device *dev, unsigned int num)
{
  [...]
  struct resource *r;
  
  if (IS_ENABLED(CONFIG_OF_IRQ) &&dev->dev.of_node) {     int ret;     ret = of_irq_get(dev->dev.of_node, num);     if (ret > 0 || ret == -EPROBE_DEFER)       return ret;   }
  r
= platform_get_resource(dev, IORESOURCE_IRQ, num);   if (r && r->flags & IORESOURCE_BITS) {     struct irq_data *irqd;     irqd = irq_get_irq_data(r->start);     if (!irqd)       return -ENXIO;
    irqd_set_trigger_type(irqd, r
->flags & IORESOURCE_BITS);   }   return r ? r->start : -ENXIO; }

您可能想知道platform_xxx函数如何从DT中提取资源。这应该是使用of_xxx函数族。对的,但是在系统boot期间,内核在每个设备节点上调用of_platform_device_create_pdata(),这将导致创建一个具有关联资源的平台设备,你可以在该设备上调用platform_xxx族函数。其原型如下:

static struct platform_device *of_platform_device_create_pdata(
  struct device_node *np, const char *bus_id,
  void *platform_data, struct device *parent)

Platform数据与DTs相比

如果你的驱动程序需要平台数据,你应该检查dev.platform_data指针。非空值意味着您的驱动程序已经在板级配置文件中以旧的方式实例化,并且DT不会进入其中。对于从DT实例化的驱动程序,dev.platform_data将为NULL,并且您的平台设备将在DT条目(节点)上获得一个指针,该指针在dev.of_node指针中对应于您的设备,你可以从中提取资源并使用OF API解析和提取应用程序数据。 

还有一种混合方法,我们可以使用它将C文件中声明的平台数据与DT节点关联起来,但这仅适用于特殊情况:DMA、IRQ和内存。此方法仅在驱动程序只需要资源而不需要特定于应用程序的数据时使用。

我们可以将I2C控制器过时的声明转换为DT兼容节点,方法如下:

过时的板级声明:

#define SIRFSOC_I2C0MOD_PA_BASE 0xcc0e0000
#define SIRFSOC_I2C0MOD_SIZE 0x10000
#define IRQ_I2C0
static struct resource sirfsoc_i2c0_resource[] = {
  {
    .start = SIRFSOC_I2C0MOD_PA_BASE,
    .end = SIRFSOC_I2C0MOD_PA_BASE + SIRFSOC_I2C0MOD_SIZE - 1,
    .flags = IORESOURCE_MEM,
  },{
    .start = IRQ_I2C0,
    .end = IRQ_I2C0,
    .flags = IORESOURCE_IRQ,
  },
};

DT节点:

i2c0: i2c@cc0e0000 {
  compatible = "sirf,marco-i2c";
  reg = <0xcc0e0000 0x10000>;
  interrupt-parent = <&phandle_to_interrupt_controller_node>
  interrupts = <0 24 0>;
  #address-cells = <1>;
  #size-cells = <0>;
  status = "disabled";
};

从hardcode设备配置切换到DTs的时刻已经到来。上述提供了处理DT所需的全部内容。现在我们已经具备了定制或添加任何DT节点和属性的必要技能,并从驱动程序中提取它们。

posted @ 2023-03-28 19:50  闹闹爸爸  阅读(729)  评论(0编辑  收藏  举报