编写 MUSB Glue Layer

作者:

Apelete Seketeli

简介

Linux MUSB 子系统是更大的 Linux USB 子系统的一部分。 它为不使用通用主机控制器接口(UHCI)或开放主机控制器接口(OHCI)的嵌入式 USB 设备控制器(UDC)提供支持。

相反,这些嵌入式 UDC 依赖于 USB On-the-Go(OTG)规范,它们至少部分实现该规范。 在大多数情况下使用的硅参考设计是在 Mentor Graphics Inventra™ 设计中找到的多点 USB 高速双角色控制器(MUSB HDRC)。

作为自学练习,我已经为 Ingenic JZ4740 SoC 编写了一个 MUSB glue layer,该层模仿内核源代码树中的许多 MUSB glue layer。 该层可以在 drivers/usb/musb/jz4740.c 中找到。 在本文档中,我将介绍 jz4740.c glue layer 的基础知识,解释不同的部分以及编写自己的设备 glue layer 需要做什么。

Linux MUSB 基础

要开始讨论这个主题,请阅读 USB On-the-Go 基础(请参阅资源),其中提供了硬件级别 USB OTG 操作的介绍。 Texas Instruments 和 Analog Devices 的一些 Wiki 页面也提供了 Linux 内核 MUSB 配置的概述,尽管重点是这些公司提供的一些特定设备。 最后,熟悉 USB 主页上的 USB 规范可能会派上用场,并通过编写 USB 设备驱动文档提供实际示例(同样,请参阅资源)。

Linux USB 堆栈是一个分层架构,其中 MUSB 控制器硬件位于最低层。 MUSB 控制器驱动程序将 MUSB 控制器硬件抽象到 Linux USB 堆栈

    ------------------------
    |                      | <------- drivers/usb/gadget
    | Linux USB Core Stack | <------- drivers/usb/host
    |                      | <------- drivers/usb/core
    ------------------------
               ⬍
   --------------------------
   |                        | <------ drivers/usb/musb/musb_gadget.c
   | MUSB Controller driver | <------ drivers/usb/musb/musb_host.c
   |                        | <------ drivers/usb/musb/musb_core.c
   --------------------------
               ⬍
---------------------------------
| MUSB Platform Specific Driver |
|                               | <-- drivers/usb/musb/jz4740.c
|       aka "Glue Layer"        |
---------------------------------
               ⬍
---------------------------------
|   MUSB Controller Hardware    |
---------------------------------

如上所述,glue layer 实际上是位于控制器驱动程序和控制器硬件之间的平台特定代码。

正如 Linux USB 驱动程序需要向 Linux USB 子系统注册自身一样,MUSB glue layer 也需要首先向 MUSB 控制器驱动程序注册自身。 这将使控制器驱动程序知道 glue layer 支持哪些设备以及在检测到或释放支持的设备时要调用的函数; 请记住,我们在这里讨论的是嵌入式控制器芯片,因此在运行时不能插入或移除。

所有这些信息都通过一个 platform_driver 结构传递给 MUSB 控制器驱动程序,该结构在 glue layer 中定义为

static struct platform_driver jz4740_driver = {
    .probe      = jz4740_probe,
    .remove     = jz4740_remove,
    .driver     = {
        .name   = "musb-jz4740",
    },
};

当检测到匹配的设备时,分别调用 probe 和 remove 函数指针,并释放匹配的设备。 name 字符串描述了此 glue layer 支持的设备。 在当前情况下,它与 arch/mips/jz4740/platform.c 中声明的 platform_device 结构匹配。 请注意,我们在此处不使用设备树绑定。

为了向控制器驱动程序注册自身,glue layer 需要经过几个步骤,基本上是分配控制器硬件资源并初始化几个电路。 为此,它需要跟踪在这些步骤中使用的信息。 这是通过定义私有 jz4740_glue 结构来完成的

struct jz4740_glue {
    struct device           *dev;
    struct platform_device  *musb;
    struct clk      *clk;
};

dev 和 musb 成员都是设备结构变量。 第一个保存有关设备的通用信息,因为它是基本的设备结构,而后者保存与设备注册到的子系统更密切相关的信息。 clk 变量保存与设备时钟操作相关的信息。

让我们来看看 probe 函数的步骤,该函数使 glue layer 向控制器驱动程序注册自身。

注意

为了便于阅读,每个函数将被分成逻辑部分,每个部分都显示为独立于其他部分。

static int jz4740_probe(struct platform_device *pdev)
{
    struct platform_device      *musb;
    struct jz4740_glue      *glue;
    struct clk                      *clk;
    int             ret;

    glue = devm_kzalloc(&pdev->dev, sizeof(*glue), GFP_KERNEL);
    if (!glue)
        return -ENOMEM;

    musb = platform_device_alloc("musb-hdrc", PLATFORM_DEVID_AUTO);
    if (!musb) {
        dev_err(&pdev->dev, "failed to allocate musb device\n");
        return -ENOMEM;
    }

    clk = devm_clk_get(&pdev->dev, "udc");
    if (IS_ERR(clk)) {
        dev_err(&pdev->dev, "failed to get clock\n");
        ret = PTR_ERR(clk);
        goto err_platform_device_put;
    }

    ret = clk_prepare_enable(clk);
    if (ret) {
        dev_err(&pdev->dev, "failed to enable clock\n");
        goto err_platform_device_put;
    }

    musb->dev.parent        = &pdev->dev;

    glue->dev           = &pdev->dev;
    glue->musb          = musb;
    glue->clk           = clk;

    return 0;

err_platform_device_put:
    platform_device_put(musb);
    return ret;
}

probe 函数的前几行分配并分配 glue、musb 和 clk 变量。GFP_KERNEL 标志(第 8 行)允许分配过程休眠并等待内存,因此可以在锁定情况下使用。 PLATFORM_DEVID_AUTO 标志(第 12 行)允许自动分配和管理设备 ID,以避免设备命名空间与显式 ID 发生冲突。 使用 devm_clk_get()(第 18 行),glue layer 分配时钟 -- devm_ 前缀指示 clk_get() 是受管理的:当设备释放时,它会自动释放分配的时钟资源数据 -- 并启用它。

然后是注册步骤

static int jz4740_probe(struct platform_device *pdev)
{
    struct musb_hdrc_platform_data  *pdata = &jz4740_musb_platform_data;

    pdata->platform_ops     = &jz4740_musb_ops;

    platform_set_drvdata(pdev, glue);

    ret = platform_device_add_resources(musb, pdev->resource,
                        pdev->num_resources);
    if (ret) {
        dev_err(&pdev->dev, "failed to add resources\n");
        goto err_clk_disable;
    }

    ret = platform_device_add_data(musb, pdata, sizeof(*pdata));
    if (ret) {
        dev_err(&pdev->dev, "failed to add platform_data\n");
        goto err_clk_disable;
    }

    return 0;

err_clk_disable:
    clk_disable_unprepare(clk);
err_platform_device_put:
    platform_device_put(musb);
    return ret;
}

第一步是通过 platform_set_drvdata()(第 7 行)将 glue layer 私有持有的设备数据传递给控制器驱动程序。 下一步是通过 platform_device_add_resources()(第 9 行)传递设备资源信息,这些信息此时也私有持有。

最后一步是将平台特定数据传递给控制器驱动程序(第 16 行)。 平台数据将在 设备平台数据 中讨论,但这里我们关注 musb_hdrc_platform_data 结构(第 3 行)中的 platform_ops 函数指针(第 5 行)。 此函数指针使 MUSB 控制器驱动程序知道要调用哪个函数进行设备操作

static const struct musb_platform_ops jz4740_musb_ops = {
    .init       = jz4740_musb_init,
    .exit       = jz4740_musb_exit,
};

在这里,我们有最小的情况,其中控制器驱动程序在需要时仅调用 init 和 exit 函数。 事实是,JZ4740 MUSB 控制器是一个基本控制器,缺少其他控制器中的一些功能,否则我们可能还有指向一些其他函数的指针,例如电源管理函数或在 OTG 和非 OTG 模式之间切换的函数。

在该注册过程的这一点上,控制器驱动程序实际上调用 init 函数

static int jz4740_musb_init(struct musb *musb)
{
    musb->xceiv = usb_get_phy(USB_PHY_TYPE_USB2);
    if (!musb->xceiv) {
        pr_err("HS UDC: no transceiver configured\n");
        return -ENODEV;
    }

    /* Silicon does not implement ConfigData register.
     * Set dyn_fifo to avoid reading EP config from hardware.
     */
    musb->dyn_fifo = true;

    musb->isr = jz4740_musb_interrupt;

    return 0;
}

jz4740_musb_init() 的目标是获取 MUSB 控制器硬件的收发器驱动程序数据,并像往常一样将其传递给 MUSB 控制器驱动程序。 收发器是控制器硬件内部的电路,负责发送/接收 USB 数据。 由于它是 OSI 模型物理层的实现,因此收发器也称为 PHY。

使用 usb_get_phy() 获取 MUSB PHY 驱动程序数据,该函数返回指向包含驱动程序实例数据的结构的指针。 接下来的几个指令(第 12 行和第 14 行)分别用作怪癖和设置 IRQ 处理。 怪癖和 IRQ 处理将在后面的 设备怪癖处理 IRQ 中讨论

static int jz4740_musb_exit(struct musb *musb)
{
    usb_put_phy(musb->xceiv);

    return 0;
}

作为 init 的对应方,exit 函数在即将释放控制器硬件本身时释放 MUSB PHY 驱动程序。

同样,请注意,在这种情况下,由于 JZ4740 控制器硬件的基本功能集,init 和 exit 非常简单。 为更复杂的控制器硬件编写 musb glue layer 时,您可能需要在这两个函数中处理更多处理。

从 init 函数返回后,MUSB 控制器驱动程序跳回到 probe 函数

static int jz4740_probe(struct platform_device *pdev)
{
    ret = platform_device_add(musb);
    if (ret) {
        dev_err(&pdev->dev, "failed to register musb device\n");
        goto err_clk_disable;
    }

    return 0;

err_clk_disable:
    clk_disable_unprepare(clk);
err_platform_device_put:
    platform_device_put(musb);
    return ret;
}

这是设备注册过程的最后一部分,其中 glue layer 将控制器硬件设备添加到 Linux 内核设备层次结构中:在此阶段,有关该设备的所有已知信息都将传递给 Linux USB 核心堆栈

static int jz4740_remove(struct platform_device *pdev)
{
    struct jz4740_glue  *glue = platform_get_drvdata(pdev);

    platform_device_unregister(glue->musb);
    clk_disable_unprepare(glue->clk);

    return 0;
}

作为 probe 的对应方,remove 函数注销 MUSB 控制器硬件(第 5 行)并禁用时钟(第 6 行),从而允许对其进行门控。

处理 IRQ

除了 MUSB 控制器硬件的基本设置和注册之外,glue layer 还负责处理 IRQ

static irqreturn_t jz4740_musb_interrupt(int irq, void *__hci)
{
    unsigned long   flags;
    irqreturn_t     retval = IRQ_NONE;
    struct musb     *musb = __hci;

    spin_lock_irqsave(&musb->lock, flags);

    musb->int_usb = musb_readb(musb->mregs, MUSB_INTRUSB);
    musb->int_tx = musb_readw(musb->mregs, MUSB_INTRTX);
    musb->int_rx = musb_readw(musb->mregs, MUSB_INTRRX);

    /*
     * The controller is gadget only, the state of the host mode IRQ bits is
     * undefined. Mask them to make sure that the musb driver core will
     * never see them set
     */
    musb->int_usb &= MUSB_INTR_SUSPEND | MUSB_INTR_RESUME |
        MUSB_INTR_RESET | MUSB_INTR_SOF;

    if (musb->int_usb || musb->int_tx || musb->int_rx)
        retval = musb_interrupt(musb);

    spin_unlock_irqrestore(&musb->lock, flags);

    return retval;
}

在这里,glue layer 主要必须读取相关的硬件寄存器并将其值传递给控制器驱动程序,控制器驱动程序将处理触发 IRQ 的实际事件。

中断处理程序关键部分受到 spin_lock_irqsave() 和对应 spin_unlock_irqrestore() 函数(分别为第 7 行和第 24 行)的保护,这些函数可防止中断处理程序代码同时被两个不同的线程运行。

然后读取相关的中断寄存器(第 9 到 11 行)

  • MUSB_INTRUSB:指示当前哪些 USB 中断处于活动状态,

  • MUSB_INTRTX:指示当前哪些 TX 端点的中断处于活动状态,

  • MUSB_INTRRX:指示当前哪些 TX 端点的中断处于活动状态。

请注意,musb_readb() 用于最多读取 8 位寄存器,而 musb_readw() 允许我们最多读取 16 位寄存器。 可以根据您的设备寄存器的大小使用其他函数。 有关更多信息,请参见 musb_io.h

第 18 行的指令是 JZ4740 USB 设备控制器的另一个怪癖,将在后面的 设备怪癖 中讨论。

但是,glue layer 仍然需要注册 IRQ 处理程序。 请记住 init 函数的第 14 行上的指令

static int jz4740_musb_init(struct musb *musb)
{
    musb->isr = jz4740_musb_interrupt;

    return 0;
}

此指令设置指向 glue layer IRQ 处理程序函数的指针,以便控制器硬件在 IRQ 来自控制器硬件时回调该处理程序。 现在已实现并注册中断处理程序。

设备平台数据

为了编写 MUSB glue layer,您需要一些描述控制器硬件硬件功能的数据,称为平台数据。

平台数据特定于您的硬件,尽管它可能涵盖广泛的设备,并且通常位于 arch/ 目录中的某个位置,具体取决于您的设备架构。

例如,JZ4740 SoC 的平台数据位于 arch/mips/jz4740/platform.c 中。 在 platform.c 文件中,JZ4740 SoC 的每个设备都通过一组结构进行描述。

这是 arch/mips/jz4740/platform.c 中涵盖 USB 设备控制器(UDC)的部分

/* USB Device Controller */
struct platform_device jz4740_udc_xceiv_device = {
    .name = "usb_phy_gen_xceiv",
    .id   = 0,
};

static struct resource jz4740_udc_resources[] = {
    [0] = {
        .start = JZ4740_UDC_BASE_ADDR,
        .end   = JZ4740_UDC_BASE_ADDR + 0x10000 - 1,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = JZ4740_IRQ_UDC,
        .end   = JZ4740_IRQ_UDC,
        .flags = IORESOURCE_IRQ,
        .name  = "mc",
    },
};

struct platform_device jz4740_udc_device = {
    .name = "musb-jz4740",
    .id   = -1,
    .dev  = {
        .dma_mask          = &jz4740_udc_device.dev.coherent_dma_mask,
        .coherent_dma_mask = DMA_BIT_MASK(32),
    },
    .num_resources = ARRAY_SIZE(jz4740_udc_resources),
    .resource      = jz4740_udc_resources,
};

jz4740_udc_xceiv_device 平台设备结构(第 2 行)使用名称和 ID 编号描述了 UDC 收发器。

在撰写本文时,请注意 usb_phy_gen_xceiv 是用于所有内置引用 USB IP 或自主且不需要任何 PHY 编程的收发器的特定名称。 您需要在内核配置中设置 CONFIG_NOP_USB_XCEIV=y 才能使用相应的收发器驱动程序。 如果我们想要一个特定的 ID 编号,则 id 字段可以设置为 -1(相当于 PLATFORM_DEVID_NONE)、-2(相当于 PLATFORM_DEVID_AUTO)或以 0 开头表示第一个此类设备。

jz4740_udc_resources 资源结构(第 7 行)定义了 UDC 寄存器基地址。

第一个数组(第 9 到 11 行)定义了 UDC 寄存器基内存地址:start 指向第一个寄存器内存地址,end 指向最后一个寄存器内存地址,flags 成员定义了我们正在处理的资源类型。 因此,IORESOURCE_MEM 用于定义寄存器内存地址。 第二个数组(第 14 到 17 行)定义了 UDC IRQ 寄存器地址。 由于只有一个 IRQ 寄存器可用于 JZ4740 UDC,因此 start 和 end 指向相同的地址。 IORESOURCE_IRQ 标志指示我们正在处理 IRQ 资源,并且名称 mc 实际上在 MUSB 核心中进行了硬编码,以便控制器驱动程序可以通过按名称查询来检索此 IRQ 资源。

最后,jz4740_udc_device 平台设备结构(第 21 行)描述了 UDC 本身。

musb-jz4740 名称(第 22 行)定义了用于此设备的 MUSB 驱动程序; 请记住,这实际上是我们在 Linux MUSB 基础 中的 jz4740_driver 平台驱动程序结构中使用的名称。 id 字段(第 23 行)设置为 -1(相当于 PLATFORM_DEVID_NONE),因为我们不需要设备的 ID:MUSB 控制器驱动程序已经设置为在 Linux MUSB 基础 中分配自动 ID。 在 dev 字段中,我们关心此处与 DMA 相关的信息。 dma_mask 字段(第 25 行)定义了将要使用的 DMA 掩码的宽度,而 coherent_dma_mask(第 26 行)具有相同的目的,但对于 alloc_coherent DMA 映射:在这两种情况下,我们都使用 32 位掩码。 然后,resource 字段(第 29 行)只是指向之前定义的资源结构的指针,而 num_resources 字段(第 28 行)跟踪资源结构中定义的数组数量(在本例中,之前定义了两个资源数组)。

现在快速概述了 arch/ 级别的 UDC 平台数据,让我们回到 drivers/usb/musb/jz4740.c 中的 MUSB glue layer 特定平台数据

static struct musb_hdrc_config jz4740_musb_config = {
    /* Silicon does not implement USB OTG. */
    .multipoint = 0,
    /* Max EPs scanned, driver will decide which EP can be used. */
    .num_eps    = 4,
    /* RAMbits needed to configure EPs from table */
    .ram_bits   = 9,
    .fifo_cfg = jz4740_musb_fifo_cfg,
    .fifo_cfg_size = ARRAY_SIZE(jz4740_musb_fifo_cfg),
};

static struct musb_hdrc_platform_data jz4740_musb_platform_data = {
    .mode   = MUSB_PERIPHERAL,
    .config = &jz4740_musb_config,
};

首先,glue layer 配置与控制器硬件规范相关的控制器驱动程序操作的某些方面。 这是通过 jz4740_musb_config musb_hdrc_config 结构完成的。

定义控制器硬件的 OTG 功能,multipoint 成员(第 3 行)设置为 0(相当于 false),因为 JZ4740 UDC 与 OTG 不兼容。 然后 num_eps(第 5 行)定义控制器硬件的 USB 端点数量,包括端点 0:这里我们有 3 个端点 + 端点 0。 接下来是 ram_bits(第 7 行),它是 MUSB 控制器硬件的 RAM 地址总线的宽度。 当控制器驱动程序无法通过读取相关的控制器硬件寄存器来自动配置端点时,需要此信息。 当我们在 设备怪癖 中处理设备怪癖时,将讨论此问题。 最后两个字段(第 8 行和第 9 行)也是关于设备怪癖的:fifo_cfg 指向 USB 端点配置表,fifo_cfg_size 跟踪该配置表中条目数量的大小。 有关更多信息,请参见后面的 设备怪癖

然后,此配置嵌入在 jz4740_musb_platform_data musb_hdrc_platform_data 结构(第 11 行)中:config 是指向配置结构本身的指针,mode 告诉控制器驱动程序控制器硬件是否只能用作 MUSB_HOST、仅用作 MUSB_PERIPHERAL 还是用作双模 MUSB_OTG

请记住,正如我们在 Linux MUSB 基础 中的 probe 函数中所见,然后使用 jz4740_musb_platform_data 来传递平台数据信息。

设备怪癖

完成特定于您的设备的平台数据后,您可能还需要在 glue layer 中编写一些代码来解决某些设备特定的限制。 这些怪癖可能是由于某些硬件错误引起的,或者仅仅是不完全实现 USB On-the-Go 规范的结果。

JZ4740 UDC 表现出此类怪癖,即使在您正在使用的控制器硬件中可能找不到这些怪癖,我们也会在此处讨论其中一些怪癖以供参考。

让我们先回到 init 函数

static int jz4740_musb_init(struct musb *musb)
{
    musb->xceiv = usb_get_phy(USB_PHY_TYPE_USB2);
    if (!musb->xceiv) {
        pr_err("HS UDC: no transceiver configured\n");
        return -ENODEV;
    }

    /* Silicon does not implement ConfigData register.
     * Set dyn_fifo to avoid reading EP config from hardware.
     */
    musb->dyn_fifo = true;

    musb->isr = jz4740_musb_interrupt;

    return 0;
}

第 12 行上的指令可帮助 MUSB 控制器驱动程序解决控制器硬件缺少用于 USB 端点配置的寄存器的事实。

如果没有这些寄存器,控制器驱动程序将无法从硬件读取端点配置,因此我们使用第 12 行的指令绕过从硅读取配置,而是依赖于描述端点配置的硬编码表

static const struct musb_fifo_cfg jz4740_musb_fifo_cfg[] = {
    { .hw_ep_num = 1, .style = FIFO_TX, .maxpacket = 512, },
    { .hw_ep_num = 1, .style = FIFO_RX, .maxpacket = 512, },
    { .hw_ep_num = 2, .style = FIFO_TX, .maxpacket = 64, },
};

查看上面的配置表,我们看到每个端点都由三个字段描述:hw_ep_num 是端点编号,style 是其方向(对于控制器驱动程序在控制器硬件中发送数据包,为 FIFO_TX,或者从硬件接收数据包,为 FIFO_RX),maxpacket 定义了可以通过该端点传输的每个数据包的最大大小。 从表中读取,控制器驱动程序知道端点 1 可用于一次发送和接收 512 字节的 USB 数据包(这实际上是一个批量输入/输出端点),端点 2 可用于一次发送 64 字节的数据包(这实际上是一个中断端点)。

请注意,此处没有关于端点 0 的信息:该端点在每个硅设计中默认实现,并根据 USB 规范具有预定义的配置。 有关端点配置表的更多示例,请参见 musb_core.c

现在让我们回到中断处理程序函数

static irqreturn_t jz4740_musb_interrupt(int irq, void *__hci)
{
    unsigned long   flags;
    irqreturn_t     retval = IRQ_NONE;
    struct musb     *musb = __hci;

    spin_lock_irqsave(&musb->lock, flags);

    musb->int_usb = musb_readb(musb->mregs, MUSB_INTRUSB);
    musb->int_tx = musb_readw(musb->mregs, MUSB_INTRTX);
    musb->int_rx = musb_readw(musb->mregs, MUSB_INTRRX);

    /*
     * The controller is gadget only, the state of the host mode IRQ bits is
     * undefined. Mask them to make sure that the musb driver core will
     * never see them set
     */
    musb->int_usb &= MUSB_INTR_SUSPEND | MUSB_INTR_RESUME |
        MUSB_INTR_RESET | MUSB_INTR_SOF;

    if (musb->int_usb || musb->int_tx || musb->int_rx)
        retval = musb_interrupt(musb);

    spin_unlock_irqrestore(&musb->lock, flags);

    return retval;
}

以上第 18 行上的指令是控制器驱动程序解决 MUSB_INTRUSB 寄存器中缺少用于 USB 主机模式操作的某些中断位(因此处于未定义的硬件状态)的事实的一种方法,因为此 MUSB 控制器硬件仅在外围设备模式下使用。 因此,glue layer 通过在从 MUSB_INTRUSB 读取的值与实际在寄存器中实现的位之间执行逻辑 AND 运算来屏蔽这些丢失的位,以避免出现寄生中断。

这些只是 JZ4740 USB 设备控制器中发现的一些怪癖。 一些其他的怪癖已在 MUSB 核心中直接解决,因为这些修复程序足够通用,可以为其他控制器硬件最终提供更好的问题处理。

结论

编写 Linux MUSB glue layer 应该是一项更容易的任务,因为本文档试图展示此练习的来龙去脉。

JZ4740 USB 设备控制器非常简单,我希望它的 glue layer 可以作为好奇者的一个很好的例子。 结合当前的 MUSB glue layer 使用,本文档应该提供足够的指导来帮助您入门; 如果任何事情失控,linux-usb 邮件列表存档是另一个有用的浏览资源。

鸣谢

非常感谢 Lars-Peter Clausen 和 Maarten ter Huurne 在我编写 JZ4740 glue layer 时回答我的问题,并帮助我使代码处于良好状态。

我也要感谢 Qi-Hardware 社区的广泛热情指导和支持。

资源

USB 主页:https://www.usb.org

linux-usb 邮件列表存档:https://marc.info/?l=linux-usb

USB On-the-Go 基础:https://www.maximintegrated.com/app-notes/index.mvp/id/1822

编写 USB 设备驱动

Texas Instruments USB 配置 Wiki 页面:https://web.archive.org/web/20201215135015/http://processors.wiki.ti.com/index.php/Usbgeneralpage