编写 MUSB 胶合层

作者:

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 胶合层,其模型基于内核源代码树中的许多 MUSB 胶合层。此层位于 drivers/usb/musb/jz4740.c。在本文档中,我将介绍 jz4740.c 胶合层的基本知识,解释不同的部分以及为了编写自己的设备胶合层需要完成的工作。

Linux MUSB 基础知识

要开始学习该主题,请阅读 USB On-the-Go 基础知识(请参阅资源),该文章介绍了硬件级别的 USB OTG 操作。德州仪器和模拟器件的几篇 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    |
---------------------------------

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

就像 Linux USB 驱动程序需要向 Linux USB 子系统注册自身一样,MUSB 胶合层首先需要向 MUSB 控制器驱动程序注册自身。这将使控制器驱动程序了解胶合层支持的设备以及在检测到或释放受支持的设备时要调用的函数;请记住,我们这里讨论的是嵌入式控制器芯片,因此不会在运行时进行插入或删除。

所有这些信息都通过胶合层中定义的 platform_driver 结构传递到 MUSB 控制器驱动程序

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

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

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

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

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

让我们来看一下 probe 函数的步骤,该步骤引导胶合层向控制器驱动程序注册自身。

注意

为了便于阅读,每个函数将被拆分为逻辑部分,每个部分都显示为与其他部分无关。

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 行),胶合层分配时钟 -- 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 行)将胶合层私有持有的设备数据传递给控制器驱动程序。下一步是通过 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。

获取 MUSB PHY 驱动程序数据是通过 usb_get_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 胶合层时,您可能需要在这两个函数中处理更多处理。

从 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;
}

这是设备注册过程的最后一部分,胶合层将控制器硬件设备添加到 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 控制器硬件的基本设置和注册之外,胶合层还负责处理 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;
}

在这里,胶合层主要必须读取相关的硬件寄存器,并将其值传递给控制器驱动程序,控制器驱动程序将处理触发 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 设备控制器的另一个特性,将在后面的设备怪癖中讨论。

胶合层仍然需要注册 IRQ 处理程序。请记住初始化函数中第 14 行的指令

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

    return 0;
}

此指令设置一个指向胶合层 IRQ 处理程序函数的指针,以便当控制器硬件发出 IRQ 时,控制器硬件可以调用该处理程序。现在已实现并注册了中断处理程序。

设备平台数据

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

平台数据特定于您的硬件,尽管它可能涵盖广泛的设备,并且通常位于 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 行)描述了 UDC 收发器,并带有名称和 ID 号。

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

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

第一个数组(第 9 行到第 11 行)定义了 UDC 寄存器基内存地址:start 指向第一个寄存器内存地址,end 指向最后一个寄存器内存地址,而 flags 成员定义了我们正在处理的资源类型。因此,IORESOURCE_MEM 用于定义寄存器内存地址。第二个数组(第 14 行到第 17 行)定义了 UDC IRQ 寄存器地址。由于 JZ4740 UDC 只有一个可用的 IRQ 寄存器,因此 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 位掩码。然后,资源字段(第 29 行)只是指向之前定义的资源结构的指针,而 num_resources 字段(第 28 行)跟踪资源结构中定义的数组数量(在这种情况下,之前定义了两个资源数组)。

现在已经完成了对 arch/ 级别 UDC 平台数据的快速概述,让我们回到 drivers/usb/musb/jz4740.c 中的 MUSB 胶合层特定平台数据。

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,
};

首先,胶合层配置与控制器硬件特性相关的控制器驱动程序操作的一些方面。这是通过 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_PERIPHERALMUSB_OTG(它是双模式)。

请记住,jz4740_musb_platform_data 然后用于传达平台数据信息,正如我们在Linux MUSB 基础中的 probe 函数中所看到的那样。

设备怪癖

完成特定于您设备的平台数据后,您可能还需要在胶合层中编写一些代码来解决某些设备特定的限制。这些怪癖可能是由于某些硬件错误造成的,或者只是 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 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 控制器硬件仅在外围设备模式下使用而处于未定义的硬件状态这一事实的方法。因此,胶合层会屏蔽掉这些缺失的位,以避免通过在从 MUSB_INTRUSB 读取的值与实际在寄存器中实现的位之间执行逻辑 AND 操作来避免寄生中断。

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

结论

编写 Linux MUSB 粘合层应该是一个更容易的任务,本文档尝试展示这项工作的来龙去脉。

JZ4740 USB 设备控制器相当简单,我希望它的粘合层可以作为好奇者的一个很好的例子。配合当前的 MUSB 粘合层,本文档应该提供足够的入门指导;如果出现任何问题,linux-usb 邮件列表存档是另一个有用的浏览资源。

致谢

非常感谢 Lars-Peter Clausen 和 Maarten ter Huurne 在我编写 JZ4740 粘合层时回答我的问题,并帮助我使代码保持良好状态。

我还要感谢整个 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 设备驱动程序

德州仪器 USB 配置 Wiki 页面: https://web.archive.org/web/20201215135015/http://processors.wiki.ti.com/index.php/Usbgeneralpage