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_init 和 module_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 的返回值,结果设备没使能成功,后续所有操作都失败了,排查了半天才发现是这里的问题。
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] 是内存区域。但不同设备可能不同,最好查阅设备手册确认。
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_readb 和 sdio_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驱动的初始化流程就讲完了。下一章我们会深入探讨数据传输的实现,包括PIO和DMA两种模式的选择与优化。到时候我会分享一些实际项目中的性能调优经验,敬请期待。