在嵌入式Linux开发中,SD卡作为最常用的存储介质之一,其驱动实现机制一直是开发者需要深入理解的核心内容。本文将带你深入Linux内核5.4版本中SD卡驱动的实现细节,从mmc_blk_probe函数开始,逐步剖析块设备创建的完整流程,包括关键数据结构如request_queue、gendisk的作用机制,以及mmc_blk_alloc等核心函数的实现原理。
Linux块设备驱动架构是一个多层次的系统,从文件系统到物理设备之间经过了多个抽象层的处理。理解这个架构对于开发SD卡驱动至关重要。
核心层次结构:
在SD卡驱动中,这个流程具体表现为:
code复制文件系统 → 通用块层 → I/O调度层 → mmc_blk层 → mmc core层 → mmc host层 → SD卡硬件
每个层次都有其关键数据结构:
| 层次 | 关键数据结构 | 作用描述 |
|---|---|---|
| 通用块层 | struct bio | 表示一个块I/O操作 |
| I/O调度层 | struct request | 封装bio的请求 |
| I/O调度层 | struct request_queue | 管理请求队列 |
| 块设备驱动层 | struct gendisk | 表示一个磁盘设备 |
MMC子系统是Linux内核中管理MMC/SD卡的核心框架,它采用了标准的Linux设备驱动模型。在SD卡驱动中,mmc_driver是最上层的驱动结构。
c复制static struct mmc_driver mmc_driver = {
.drv = {
.name = "mmcblk",
.pm = &mmc_blk_pm_ops,
},
.probe = mmc_blk_probe,
.remove = mmc_blk_remove,
.shutdown = mmc_blk_shutdown,
};
驱动注册过程发生在mmc_blk_init函数中:
当SD卡插入时,内核会检测到设备并调用mmc_blk_probe函数,这是整个驱动初始化的起点。
mmc_blk_probe是SD卡驱动初始化的核心函数,它主要完成以下工作:
让我们重点看一下关键代码流程:
c复制static int mmc_blk_probe(struct mmc_card *card)
{
// 1. 检查卡是否支持块设备操作
if (!(card->csd.cmdclass & CCC_BLOCK_READ))
return -ENODEV;
// 2. 创建工作队列
card->complete_wq = alloc_workqueue("mmc_complete", WQ_MEM_RECLAIM | WQ_HIGHPRI, 0);
// 3. 分配和初始化块设备结构
md = mmc_blk_alloc(card);
if (IS_ERR(md))
return PTR_ERR(md);
// 4. 处理分区
if (mmc_blk_alloc_parts(card, md))
goto out;
// 5. 关联设备和数据
dev_set_drvdata(&card->dev, md);
// 6. 注册块设备
if (mmc_add_disk(md))
goto out;
// ... 其他处理 ...
}
这个函数中有两个关键子函数需要特别关注:mmc_blk_alloc和mmc_add_disk,它们分别负责块设备结构的分配和系统注册。
mmc_blk_alloc函数是块设备创建的核心,它主要完成以下工作:
容量计算对于SD卡和eMMC有所不同:
c复制if (!mmc_card_sd(card) && mmc_card_blockaddr(card)) {
// eMMC设备从ext_csd寄存器获取容量
size = card->ext_csd.sectors;
} else {
// SD卡从CSD的capacity域获取容量
size = (typeof(sector_t))card->csd.capacity << (card->csd.read_blkbits - 9);
}
关键数据结构初始化流程:
其中,mmc_init_queue函数负责队列的初始化和request_queue的创建:
c复制int mmc_init_queue(struct mmc_queue *mq, struct mmc_card *card)
{
// 初始化标签集
mq->tag_set.ops = &mmc_mq_ops;
mq->tag_set.queue_depth = MMC_QUEUE_DEPTH;
// ... 其他初始化 ...
// 创建request_queue
mq->queue = blk_mq_init_queue(&mq->tag_set);
// 设置队列属性
blk_queue_rq_timeout(mq->queue, 60 * HZ);
mmc_setup_queue(mq, card);
return 0;
}
mmc_add_disk函数负责将准备好的gendisk结构注册到系统中,使得用户空间可以通过设备节点访问SD卡。其主要工作包括:
c复制static int mmc_add_disk(struct mmc_blk_data *md)
{
// 注册gendisk
device_add_disk(md->parent, md->disk, NULL);
// 创建force_ro属性文件
ret = device_create_file(disk_to_dev(md->disk), &md->force_ro);
// 处理启动分区的只读锁定
if ((md->area_type & MMC_BLK_DATA_AREA_BOOT) &&
card->ext_csd.boot_ro_lockable) {
// ... 创建ro_lock属性文件 ...
}
return ret;
}
注册成功后,系统中会出现对应的块设备节点(如/dev/mmcblk0),用户空间程序就可以通过这个设备节点访问SD卡了。
SD卡的I/O请求处理是整个驱动中最关键的部分之一。当用户空间发起读写操作时,请求会经过以下流程:
在MMC子系统中,请求处理由mmc_mq_ops结构体定义的操作集完成:
c复制static const struct blk_mq_ops mmc_mq_ops = {
.queue_rq = mmc_mq_queue_rq,
.init_request = mmc_mq_init_request,
.exit_request = mmc_mq_exit_request,
.complete = mmc_blk_mq_complete,
.timeout = mmc_mq_timed_out,
};
其中,mmc_mq_queue_rq是实际处理请求的核心函数,它会:
在开发SD卡驱动时,经常会遇到各种问题。以下是一些常见问题及解决方法:
常见问题排查表:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无法检测到SD卡 | 电源或时钟未正确配置 | 检查host控制器的初始化 |
| 读写速度慢 | 时钟频率设置过低 | 调整host控制器的时钟配置 |
| 数据损坏 | DMA缓冲区未对齐 | 确保DMA缓冲区按cache line对齐 |
| 随机挂起 | 中断处理不当 | 检查中断处理函数的超时机制 |
调试技巧:
bash复制echo -n 'file drivers/mmc/* +p' > /sys/kernel/debug/dynamic_debug/control
c复制// 在驱动代码中添加队列状态检查
blk_dump_rq_flags(mq->queue, "Queue status");
bash复制echo 1 > /sys/kernel/debug/tracing/events/mmc/enable
cat /sys/kernel/debug/tracing/trace_pipe
在实际项目中,理解这些底层机制可以帮助开发者快速定位和解决复杂的驱动问题,特别是在需要对标准驱动进行定制修改时。