4. UDC驱动probe函数编写要点

UDC(USB Device Controller)驱动的 probe 函数是驱动与内核设备模型绑定的入口点。它负责将设备树中描述的硬件信息转化为内核可管理的设备实例,完成硬件初始化、中断资源申请,并最终将UDC注册到USB Gadget框架中。本章将拆解这一完整流程,并给出每个环节的编写要点与常见陷阱。

编写原则:probe函数应遵循“先获取资源,再初始化硬件,最后注册框架”的顺序。任何一步失败都应回滚已分配的资源,避免系统处于不一致状态。

4.1 设备树匹配与platform_driver注册

UDC驱动通常以 platform_driver 形式存在,通过 of_match_table 与设备树节点匹配。设备树节点中描述了UDC的寄存器基地址、中断号、时钟、DMA通道等资源。

static const struct of_device_id my_udc_of_match[] = {
    { .compatible = "vendor,my-udc", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_udc_of_match);

static struct platform_driver my_udc_driver = {
    .probe  = my_udc_probe,
    .remove = my_udc_remove,
    .driver = {
        .name = "my-udc",
        .of_match_table = my_udc_of_match,
        .pm = &my_udc_pm_ops,
    },
};
module_platform_driver(my_udc_driver);
匹配流程:内核启动时,platform总线根据设备树节点中的compatible属性遍历已注册的platform_driver列表,找到匹配项后调用probe函数。
设备树匹配流程 设备树节点 compatible = "vendor,my-udc" platform总线匹配 遍历of_match_table probe函数调用 my_udc_probe() 说明:设备树节点通过compatible属性与驱动匹配 匹配成功后,内核自动调用probe函数并传入platform_device指针

4.2 资源获取:从设备树解析寄存器与中断

在probe函数中,首先需要从 platform_device 中获取硬件资源。核心API包括:

资源类型 获取API 返回值 说明
寄存器基地址 platform_get_resource(pdev, IORESOURCE_MEM, 0) struct resource * 获取第0个MEM资源,包含物理地址和长度
寄存器虚拟映射 devm_ioremap_resource(dev, res) void __iomem * 自动管理生命周期,失败返回ERR_PTR
中断号 platform_get_irq(pdev, 0) int 获取第0个中断资源,失败返回负值
时钟 devm_clk_get(dev, "usb") struct clk * 获取名为"usb"的时钟,需先使能
DMA掩码 dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)) int 设置DMA地址宽度,失败则无法使用DMA
static int my_udc_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_udc *udc;
    struct resource *res;
    int irq, ret;

    /* 1. 分配私有数据结构 */
    udc = devm_kzalloc(dev, sizeof(*udc), GFP_KERNEL);
    if (!udc)
        return -ENOMEM;

    /* 2. 获取寄存器资源并映射 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    udc->regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(udc->regs))
        return PTR_ERR(udc->regs);

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

    /* 4. 获取并使能时钟 */
    udc->clk = devm_clk_get(dev, "usb");
    if (IS_ERR(udc->clk))
        return PTR_ERR(udc->clk);
    ret = clk_prepare_enable(udc->clk);
    if (ret)
        return ret;

    /* 5. 设置DMA掩码 */
    ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
    if (ret)
        goto err_disable_clk;

    /* ... 后续初始化 ... */
    return 0;

err_disable_clk:
    clk_disable_unprepare(udc->clk);
    return ret;
}
避坑提示:使用 devm_ 系列API可以自动管理资源释放,但时钟的使能/禁用手动配对。在probe失败路径中,务必调用 clk_disable_unprepare 回滚已使能的时钟。

4.3 UDC硬件初始化:控制器复位与端点配置

硬件初始化是probe函数的核心步骤,通常包括:控制器软复位、设置USB设备地址(默认为0)、配置端点寄存器、初始化FIFO、设置电源管理等。不同UDC控制器的寄存器布局差异较大,但初始化逻辑具有共性。

UDC硬件初始化状态机 控制器复位 时钟使能 端点配置 FIFO初始化 各步骤关键操作 • 复位:设置RUN/STOP位为0,等待复位完成标志 • 端点配置:设置端点类型(控制/批量/中断/同步)、最大包大小、方向 • FIFO初始化:分配FIFO深度,设置FIFO基地址指针
/* 硬件初始化示例:以端点0配置为例 */
static int my_udc_hw_init(struct my_udc *udc)
{
    u32 reg;

    /* 1. 软复位:等待复位完成 */
    writel(MY_UDC_SOFT_RST, udc->regs + MY_UDC_CTRL);
    udelay(10);
    reg = readl(udc->regs + MY_UDC_STS);
    if (!(reg & MY_UDC_STS_RST_DONE)) {
        dev_err(udc->dev, "Controller reset timeout\n");
        return -ETIMEDOUT;
    }

    /* 2. 配置端点0:控制端点,最大包64字节 */
    reg = MY_UDC_EPT_TYPE_CTRL | MY_UDC_EPT_MPS(64);
    writel(reg, udc->regs + MY_UDC_EPT_CFG(0));

    /* 3. 配置端点1:批量IN端点,最大包512字节 */
    reg = MY_UDC_EPT_TYPE_BULK | MY_UDC_EPT_DIR_IN | MY_UDC_EPT_MPS(512);
    writel(reg, udc->regs + MY_UDC_EPT_CFG(1));

    /* 4. 配置端点2:批量OUT端点,最大包512字节 */
    reg = MY_UDC_EPT_TYPE_BULK | MY_UDC_EPT_DIR_OUT | MY_UDC_EPT_MPS(512);
    writel(reg, udc->regs + MY_UDC_EPT_CFG(2));

    /* 5. 初始化FIFO:端点0 FIFO基地址0x100,端点1 FIFO基地址0x200 */
    writel(0x100, udc->regs + MY_UDC_FIFO_BASE(0));
    writel(0x200, udc->regs + MY_UDC_FIFO_BASE(1));

    /* 6. 使能控制器 */
    reg = readl(udc->regs + MY_UDC_CTRL);
    reg |= MY_UDC_CTRL_RUN;
    writel(reg, udc->regs + MY_UDC_CTRL);

    return 0;
}

4.4 中断申请:request_irq与中断处理函数设计

中断是UDC驱动响应USB主机请求的关键机制。probe函数中需要注册中断处理函数,并在其中分发端点事件。中断处理函数的设计直接影响USB传输的实时性。

中断类型 触发条件 处理优先级 典型处理动作
USB复位中断 主机发送复位信号 最高 重置端点状态机,清空FIFO
端点0中断 控制传输完成 高 解析Setup包,调用gadget层回调
批量/中断端点中断 数据传输完成 中 更新DMA描述符,通知gadget层
SOF中断 每帧开始 低 帧计数更新,用于ISO传输
/* 中断申请 */
static int my_udc_request_irq(struct platform_device *pdev, struct my_udc *udc)
{
    int irq, ret;

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* 注册中断处理函数:使用IRQF_SHARED表示共享中断 */
    ret = devm_request_irq(&pdev->dev, irq, my_udc_irq_handler,
                           IRQF_SHARED, "my-udc", udc);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
        return ret;
    }

    udc->irq = irq;
    return 0;
}

/* 中断处理函数框架 */
static irqreturn_t my_udc_irq_handler(int irq, void *data)
{
    struct my_udc *udc = data;
    u32 int_sts;

    /* 1. 读取中断状态寄存器 */
    int_sts = readl(udc->regs + MY_UDC_INT_STS);

    /* 2. 清除已处理的中断标志(写1清除) */
    writel(int_sts, udc->regs + MY_UDC_INT_STS);

    /* 3. 分发中断 */
    if (int_sts & MY_UDC_INT_RESET) {
        /* USB复位处理:重置端点状态 */
        my_udc_reset_handler(udc);
    }

    if (int_sts & MY_UDC_INT_EPT0) {
        /* 端点0控制传输处理 */
        my_udc_ep0_handler(udc);
    }

    if (int_sts & MY_UDC_INT_EPT1) {
        /* 端点1批量传输处理 */
        my_udc_ep_handler(udc, 1);
    }

    /* 4. 返回IRQ_HANDLED表示已处理 */
    return IRQ_HANDLED;
}
中断设计要点:
  • 中断处理函数应尽量短小,耗时操作(如DMA描述符处理)应推迟到tasklet或workqueue中执行。
  • 清除中断标志必须在读取数据之后、重新使能中断之前,避免丢失中断。
  • 对于共享中断,必须检查中断状态寄存器确认是否为本设备的中断,否则返回IRQ_NONE。

4.5 注册UDC到Gadget框架:usb_add_gadget_udc

硬件初始化完成后,最后一步是将UDC注册到USB Gadget框架。核心API是 usb_add_gadget_udc,它接受一个 struct usb_gadget 指针,并将其绑定到UDC驱动上。注册成功后,Gadget框架会开始监听主机发起的枚举请求。

/* 注册UDC到Gadget框架 */
static int my_udc_register_gadget(struct my_udc *udc)
{
    struct usb_gadget *gadget = &udc->gadget;
    int ret;

    /* 1. 填充gadget结构体 */
    gadget->ops = &my_udc_ops;          /* 操作函数集 */
    gadget->ep0 = &udc->ep[0];          /* 端点0 */
    gadget->name = "my-udc";
    gadget->dev.parent = udc->dev;
    gadget->max_speed = USB_SPEED_HIGH;  /* 支持高速模式 */
    gadget->sg_supported = true;         /* 支持Scatter-Gather */

    /* 2. 注册UDC */
    ret = usb_add_gadget_udc(udc->dev, gadget);
    if (ret) {
        dev_err(udc->dev, "Failed to register UDC: %d\n", ret);
        return ret;
    }

    dev_info(udc->dev, "UDC registered successfully\n");
    return 0;
}

/* probe函数完整流程 */
static int my_udc_probe(struct platform_device *pdev)
{
    struct my_udc *udc;
    int ret;

    /* 1. 分配私有数据 */
    udc = devm_kzalloc(&pdev->dev, sizeof(*udc), GFP_KERNEL);
    if (!udc)
        return -ENOMEM;
    udc->dev = &pdev->dev;
    platform_set_drvdata(pdev, udc);

    /* 2. 获取资源 */
    ret = my_udc_get_resources(pdev, udc);
    if (ret)
        return ret;

    /* 3. 硬件初始化 */
    ret = my_udc_hw_init(udc);
    if (ret)
        goto err_disable_clk;

    /* 4. 申请中断 */
    ret = my_udc_request_irq(pdev, udc);
    if (ret)
        goto err_hw_deinit;

    /* 5. 注册UDC */
    ret = my_udc_register_gadget(udc);
    if (ret)
        goto err_hw_deinit;

    return 0;

err_hw_deinit:
    my_udc_hw_deinit(udc);
err_disable_clk:
    clk_disable_unprepare(udc->clk);
    return ret;
}
probe函数完整流程 1. 分配私有数据结构 2. 获取资源(MEM/IRQ/CLK) 3. 硬件初始化(复位/端点/FIFO) 4. 申请中断(request_irq) 5. 注册UDC(usb_add_gadget_udc) 任何一步失败都需要回滚已分配的资源,确保系统状态一致

4.6 常见错误与调试方法

错误现象 可能原因 排查方法
probe返回-ENODEV 设备树compatible不匹配 检查设备树节点compatible属性与驱动of_match_table是否一致
ioremap失败 寄存器地址范围错误或已被占用 使用 cat /proc/iomem 查看地址冲突
中断不触发 中断号错误或中断控制器未配置 检查设备树interrupts属性,使用 cat /proc/interrupts 确认中断注册
usb_add_gadget_udc失败 gadget结构体未正确填充 检查ops、ep0、max_speed等字段是否非空
时钟使能后系统挂起 时钟频率配置错误或时钟源未稳定 使用clk_summary调试时钟树,检查时钟频率
调试建议:在probe函数的关键步骤添加 dev_dbg 打印,通过动态调试(dynamic debug)在运行时开启。使用 echo 'file my_udc.c +p' > /sys/kernel/debug/dynamic_debug/control 控制打印输出,避免生产环境日志泛滥。