4、SDIO驱动初始化流程:模块加载与卸载、probe函数实现、设备识别与配置、资源申请与映射

好,咱们今天来聊聊SDIO驱动的初始化流程。这部分说白了,就是驱动和硬件第一次「握手」的过程。我做了这么多年驱动开发,发现很多新手栽跟头就栽在这里——不是probe没跑通,就是资源映射出了岔子。别急,咱们一步步拆开来看。

4.1 模块加载与卸载:驱动的「开关」

每个Linux驱动都有两个基本动作:加载和卸载。SDIO驱动也不例外。你想想看,驱动加载时内核要做什么?注册驱动、初始化数据结构、准备好和硬件通信。卸载时呢?反过来,释放资源、注销驱动。

我个人习惯这样写模块入口和出口:

static int __init sdio_drv_init(void)
{
    int ret;
    ret = sdio_register_driver(&my_sdio_driver);
    if (ret) {
        pr_err("SDIO driver registration failed: %d\n", ret);
        return ret;
    }
    pr_info("My SDIO driver loaded successfully\n");
    return 0;
}

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

module_init(sdio_drv_init);
module_exit(sdio_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("SDIO Driver Example");

嗯,这里要注意一点:__init 和 __exit 这两个宏别漏了。它们告诉内核,这些函数只在初始化或卸载时使用,之后可以回收内存。我在项目中遇到过有人忘了加,结果内存白白浪费了几KB——虽然不多,但嵌入式设备讲究的就是「锱铢必较」。

4.2 probe函数实现:驱动的「身份证验证」

probe函数是驱动初始化的核心。它什么时候被调用?当内核发现一个SDIO设备,并且这个设备的VID(厂商ID)和PID(产品ID)匹配到你的驱动时,probe就会被触发。

说白了,probe就是驱动和硬件的「相亲现场」——内核把设备信息递给你,你得确认「嗯,这设备我能管」,然后开始干活。

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

    dev_info(&func->dev, "Probing SDIO device: 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;
    sdio_set_drvdata(func, dev);

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

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

    // 5. 读取设备寄存器,验证通信
    ret = my_sdio_read_cfg(func);
    if (ret) {
        dev_err(&func->dev, "Failed to read device config\n");
        goto err_disable;
    }

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

err_disable:
    sdio_disable_func(func);
err_free:
    kfree(dev);
    return ret;
}

看到没?probe里我做了五件事:分配内存、关联设备、使能功能、设置块大小、读取配置。每一步都可能失败,所以错误处理一定要到位。我曾经在一个项目里忘了调用 sdio_disable_func,结果设备一直处于半使能状态,后续读写全乱了——那叫一个头疼。

4.3 设备识别与配置:匹配表与寄存器初始化

设备识别靠什么?靠 sdio_device_id 匹配表。内核拿着这个表,去和实际插入的SDIO设备比对。匹配上了,probe就被调用。

字段 含义 示例值
vendor 厂商ID 0x02D0 (Broadcom)
device 产品ID 0x4324 (WiFi芯片)
class 设备类别 0x00 (标准)
driver_data 驱动私有数据 自定义值
static const struct sdio_device_id my_sdio_ids[] = {
    { SDIO_DEVICE(0x02D0, 0x4324) },
    { /* end: all zero */ },
};
MODULE_DEVICE_TABLE(sdio, my_sdio_ids);

配置阶段呢?我一般会在probe里读取设备的CIS(Card Information Structure),确认设备支持的功能。然后根据需求配置相关寄存器。比如设置中断使能、选择数据传输模式等。

小技巧: 我个人习惯在probe里加一个「设备自检」步骤——写一个已知值到某个寄存器,再读回来验证。这样能快速发现通信问题,而不是等到读写数据时才报错。

4.4 资源申请与映射:内存、中断与IO

驱动要正常工作,离不开三种资源:内存、中断、IO地址。申请和映射的顺序很重要,搞反了容易出问题。

内存申请:

除了probe里分配的私有数据结构,SDIO驱动通常还需要DMA缓冲区。我建议用 dma_alloc_coherent 来申请,保证物理地址连续:

dev->dma_buf = dma_alloc_coherent(&func->dev, BUF_SIZE,
                                   &dev->dma_handle, GFP_KERNEL);
if (!dev->dma_buf) {
    dev_err(&func->dev, "Failed to allocate DMA buffer\n");
    ret = -ENOMEM;
    goto err_disable;
}

中断申请:

SDIO设备通过SDIO中断通知主机。申请中断时,要指定触发方式和处理函数:

ret = request_threaded_irq(func->irq, NULL, my_sdio_irq_handler,
                           IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
                           "my_sdio", dev);
if (ret) {
    dev_err(&func->dev, "Failed to request IRQ %d\n", func->irq);
    goto err_dma;
}

嗯,这里有个坑:SDIO中断是电平触发的,所以一定要加 IRQF_ONESHOT 标志。不加的话,中断处理函数还没执行完,同一个中断又来了——死循环。我曾经在这个问题上折腾了两天,最后发现是标志位没设对。

IO映射:

SDIO设备的IO空间通过 sdio_readb、sdio_writeb 等函数访问,不需要像PCI那样做ioremap。但如果你需要访问设备内部的FIFO或大块内存区域,可以考虑使用 sdio_memcpy_fromio 和 sdio_memcpy_toio:

// 读取设备配置寄存器
u8 reg_val = sdio_readb(func, 0x10, &ret);
if (ret)
    dev_err(&func->dev, "Failed to read register 0x10\n");

// 批量读取数据
sdio_memcpy_fromio(func, dev->dma_buf, 0x1000, BUF_SIZE);
警告: 千万别在probe里做耗时操作!比如等待设备复位、大量数据读写等。probe函数执行时,内核是持有锁的,你卡住了,整个系统都可能受影响。我建议把耗时的初始化放到一个工作队列里去做。

4.5 初始化流程全景图

说了这么多,咱们用一张图把整个流程串起来。这张图是我自己画的一个简化版,但核心逻辑都在里面了:

SDIO驱动初始化流程 模块加载 (module_init) 注册SDIO驱动 (sdio_register_driver) 设备匹配 (sdio_device_id 表) probe 函数被调用 probe 内部执行步骤 ① 分配私有数据结构 ② 使能SDIO功能 ③ 设置块大小 ④ 读取设备配置 ⑤ 申请资源 (内存/中断) 初始化完成,设备就绪 卸载路径 模块卸载 (module_exit) 释放资源,注销驱动

这张图把整个流程分成了两条路径:加载路径(绿色)和卸载路径(红色)。加载时,从模块入口一路走到probe,完成资源申请;卸载时,反过来释放所有资源。你想想看,如果卸载时漏了某个资源的释放,下次加载就会出问题——内存泄漏、中断冲突,各种怪毛病都来了。

核心要点:
  • probe函数是初始化的「心脏」,所有关键步骤都在这里完成
  • 资源申请要「谁申请谁释放」,probe里申请的资源,remove里必须释放
  • 错误处理要完整,任何一个步骤失败都要能回滚到安全状态
  • 匹配表要准确,VID/PID写错了,驱动根本不会加载

好了,SDIO驱动初始化流程就讲到这里。这部分内容虽然基础,但却是整个驱动的基石。我见过太多驱动出问题,追根溯源都是初始化没做好——要么资源没申请够,要么中断配置错了。记住一句话:初始化做扎实了,后面就顺了。

避坑指南: 我曾经在一个项目里,probe函数返回了0,但实际上设备根本没初始化成功。为什么?因为我在某个步骤失败后,忘了设置返回值。结果内核以为驱动加载成功了,后面读写全崩了。所以,probe里每个错误路径都要明确返回负的错误码,千万别漏了。

可可精品资料库