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函数。
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控制器的寄存器布局差异较大,但初始化逻辑具有共性。
/* 硬件初始化示例:以端点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;
}
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 控制打印输出,避免生产环境日志泛滥。