4. SDIO驱动初始化流程:模块入口与出口函数、probe函数实现、资源申请与映射、中断注册与使能

好,咱们今天来聊聊SDIO驱动的初始化流程。这部分是驱动开发的骨架,说白了就是告诉内核:我这个驱动什么时候加载、什么时候卸载、设备来了怎么处理。我刚开始写SDIO驱动时,总觉得初始化不就是注册几个回调函数嘛,后来踩了不少坑才明白——这里面的门道,远比想象中多。

4.1 模块入口与出口函数:驱动的“开关”

每个内核驱动都有两个基本函数:模块入口和出口。SDIO驱动也不例外。入口函数负责初始化,出口函数负责清理。嗯,这里要注意,顺序很重要——你申请的资源,必须在出口函数里按相反顺序释放。

static int __init sdio_drv_init(void)
{
    int ret;

    pr_info("SDIO driver initializing...\n");

    /* 注册SDIO驱动 */
    ret = sdio_register_driver(&my_sdio_driver);
    if (ret) {
        pr_err("Failed to register SDIO driver: %d\n", ret);
        return ret;
    }

    pr_info("SDIO driver registered successfully\n");
    return 0;
}

static void __exit sdio_drv_exit(void)
{
    pr_info("SDIO driver exiting...\n");
    sdio_unregister_driver(&my_sdio_driver);
}

module_init(sdio_drv_init);
module_exit(sdio_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("SDIO Driver Example");

我个人习惯在入口函数里加一些调试打印,方便确认加载状态。但生产环境中记得去掉,或者用动态调试控制。

小技巧: 使用 module_initmodule_exit 宏时,函数名可以任意,但建议语义清晰。我曾经见过有人把入口函数命名为 init_xxx,出口函数命名为 cleanup_xxx,结果半年后自己都忘了哪个是哪个。

4.2 probe函数实现:设备来了,该干活了

当SDIO控制器检测到设备插入时,内核会调用驱动的probe函数。这是整个初始化的核心——你要在这里完成设备识别、资源分配、功能初始化等一系列操作。

static int my_sdio_probe(struct sdio_func *func,
                         const struct sdio_device_id *id)
{
    struct my_sdio_dev *dev;
    int ret;

    dev_info(&func->dev, "SDIO device found: vendor=0x%04x, device=0x%04x\n",
             id->vendor, id->device);

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

    /* 2. 关联设备 */
    dev->func = func;
    dev_set_drvdata(&func->dev, dev);

    /* 3. 使能SDIO功能 */
    ret = sdio_enable_func(func);
    if (ret) {
        dev_err(&func->dev, "Failed to enable SDIO function: %d\n", ret);
        goto err_free_dev;
    }

    /* 4. 设置块大小(通常为512字节) */
    ret = sdio_set_block_size(func, 512);
    if (ret) {
        dev_err(&func->dev, "Failed to set block size: %d\n", ret);
        goto err_disable_func;
    }

    /* 5. 读取设备信息 */
    ret = my_sdio_read_device_info(dev);
    if (ret) {
        dev_err(&func->dev, "Failed to read device info: %d\n", ret);
        goto err_disable_func;
    }

    dev_info(&func->dev, "SDIO device initialized successfully\n");
    return 0;

err_disable_func:
    sdio_disable_func(func);
err_free_dev:
    kfree(dev);
    return ret;
}

你看,probe函数的流程其实很清晰:先分配内存,再使能功能,然后设置参数,最后读取设备信息。每一步都可能失败,所以错误处理必须到位。我在项目中遇到过一个问题:probe函数里忘记检查 sdio_enable_func 的返回值,结果设备没使能成功,后续所有操作都失败了,排查了半天才发现是这里的问题。

注意: probe函数中不要做耗时操作,比如长时间的轮询或睡眠。如果必须等待设备就绪,考虑使用工作队列或定时器。否则会阻塞内核的设备枚举过程,导致系统启动变慢。

4.3 资源申请与映射:让CPU能访问设备

SDIO设备通常有多个I/O端口和内存区域。要让CPU能访问这些资源,你需要做两件事:申请资源(告诉内核这块区域我用了)和映射(把物理地址转成虚拟地址)。

/* 在probe函数中继续 */
/* 6. 申请I/O端口资源 */
if (!request_region(func->resource[0]->start,
                    resource_size(func->resource[0]),
                    DRIVER_NAME)) {
    dev_err(&func->dev, "Failed to request I/O port region\n");
    ret = -EBUSY;
    goto err_disable_func;
}

/* 7. 申请内存资源并映射 */
dev->regs = ioremap(func->resource[1]->start,
                     resource_size(func->resource[1]));
if (!dev->regs) {
    dev_err(&func->dev, "Failed to ioremap memory region\n");
    ret = -ENOMEM;
    goto err_release_region;
}

dev_info(&func->dev, "Resources mapped: IO=0x%llx, MEM=0x%llx\n",
         func->resource[0]->start,
         func->resource[1]->start);

这里有个细节:func->resource 数组里存放的是设备的资源信息。通常 resource[0] 是I/O端口,resource[1] 是内存区域。但不同设备可能不同,最好查阅设备手册确认。

核心原则: 申请的资源必须在使用完后释放。ioremap 的虚拟地址要用 iounmap 释放,request_region 要用 release_region 释放。我曾经见过一个驱动,probe 时申请了资源,但 remove 函数里忘了释放,结果设备热插拔几次后,系统就报资源冲突了。

4.4 中断注册与使能:让设备能“说话”

SDIO设备通常通过中断通知主机事件。中断注册是初始化流程的最后一步,也是最容易出问题的一步。为什么?因为中断处理函数运行在中断上下文中,不能睡眠,不能调用可能阻塞的函数。

/* 在probe函数中继续 */
/* 8. 注册中断 */
ret = request_threaded_irq(func->irq, NULL,
                           my_sdio_irq_thread,
                           IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
                           DRIVER_NAME, dev);
if (ret) {
    dev_err(&func->dev, "Failed to request IRQ %d: %d\n",
            func->irq, ret);
    goto err_iounmap;
}

/* 9. 使能SDIO中断 */
sdio_claim_host(func);
sdio_writeb(func, 1, SDIO_CCCR_IENx, &ret);  /* 使能功能中断 */
sdio_release_host(func);

dev_info(&func->dev, "Interrupt registered and enabled\n");

这里我用了 request_threaded_irq 而不是 request_irq。为什么?因为SDIO中断处理可能需要执行一些耗时操作,比如读写寄存器、处理数据包。如果用 request_irq,这些操作都在硬中断上下文中执行,会阻塞其他中断。而线程化中断允许你把耗时操作放到内核线程中执行,不影响系统响应。

/* 中断处理函数(硬中断部分) */
static irqreturn_t my_sdio_irq_handler(int irq, void *data)
{
    struct my_sdio_dev *dev = data;

    /* 快速检查中断源 */
    if (!(sdio_readb(dev->func, SDIO_CCCR_INTx, NULL) & BIT(0)))
        return IRQ_NONE;

    /* 返回IRQ_WAKE_THREAD,唤醒线程处理 */
    return IRQ_WAKE_THREAD;
}

/* 中断线程化处理函数 */
static irqreturn_t my_sdio_irq_thread(int irq, void *data)
{
    struct my_sdio_dev *dev = data;
    u8 status;

    sdio_claim_host(dev->func);

    /* 读取中断状态寄存器 */
    status = sdio_readb(dev->func, REG_INT_STATUS, NULL);

    /* 根据状态处理不同中断 */
    if (status & INT_DATA_READY) {
        my_sdio_handle_data(dev);
    }
    if (status & INT_ERROR) {
        my_sdio_handle_error(dev);
    }

    sdio_release_host(dev);

    return IRQ_HANDLED;
}
避坑指南: 我曾经在中断处理函数里直接调用了 sdio_readbsdio_writeb,结果系统频繁死机。后来才发现,SDIO的读写操作需要持有主机锁(sdio_claim_host),而中断上下文中不能睡眠获取锁。解决方案就是用线程化中断,在 my_sdio_irq_thread 中安全地调用SDIO API。

4.5 完整的probe函数流程

把上面所有步骤整合起来,一个完整的probe函数应该是这样的:

static int my_sdio_probe(struct sdio_func *func,
                         const struct sdio_device_id *id)
{
    struct my_sdio_dev *dev;
    int ret;

    /* 1. 分配设备结构 */
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
    dev->func = func;
    dev_set_drvdata(&func->dev, dev);

    /* 2. 使能功能 */
    ret = sdio_enable_func(func);
    if (ret) goto err_free;

    /* 3. 设置块大小 */
    ret = sdio_set_block_size(func, 512);
    if (ret) goto err_disable;

    /* 4. 读取设备信息 */
    ret = my_sdio_read_device_info(dev);
    if (ret) goto err_disable;

    /* 5. 申请资源 */
    if (!request_region(func->resource[0]->start,
                        resource_size(func->resource[0]),
                        DRIVER_NAME)) {
        ret = -EBUSY;
        goto err_disable;
    }

    /* 6. 映射寄存器 */
    dev->regs = ioremap(func->resource[1]->start,
                         resource_size(func->resource[1]));
    if (!dev->regs) {
        ret = -ENOMEM;
        goto err_release;
    }

    /* 7. 注册中断 */
    ret = request_threaded_irq(func->irq, NULL,
                               my_sdio_irq_thread,
                               IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
                               DRIVER_NAME, dev);
    if (ret) goto err_iounmap;

    /* 8. 使能中断 */
    sdio_claim_host(func);
    sdio_writeb(func, 1, SDIO_CCCR_IENx, &ret);
    sdio_release_host(func);

    dev_info(&func->dev, "Probe completed successfully\n");
    return 0;

err_iounmap:
    iounmap(dev->regs);
err_release:
    release_region(func->resource[0]->start,
                   resource_size(func->resource[0]));
err_disable:
    sdio_disable_func(func);
err_free:
    kfree(dev);
    return ret;
}

你看,每个错误标签都对应一个清理操作,而且顺序是反的。这是内核编程的基本功——你申请了什么,就要在错误路径上释放什么。我刚开始写驱动时,经常漏掉某个清理步骤,结果导致内存泄漏或资源泄漏。后来养成了习惯:每写一行申请资源的代码,就立刻在错误处理部分加上对应的释放代码。

总结一下: SDIO驱动初始化流程可以概括为“三步走”——入口/出口函数定义驱动生命周期,probe函数完成设备初始化,资源申请与映射让CPU能访问硬件,中断注册与使能让设备能主动通知主机。每一步都有坑,但只要你按照规范来,就能写出稳定可靠的驱动。

嗯,到这里,SDIO驱动的初始化流程就讲完了。下一章我们会深入探讨数据传输的实现,包括PIO和DMA两种模式的选择与优化。到时候我会分享一些实际项目中的性能调优经验,敬请期待。