Linux内核5.4下SD卡驱动开发实战:从mmc_blk_probe到块设备注册全流程解析
在嵌入式Linux开发中,SD卡作为最常用的存储介质之一,其驱动实现机制一直是开发者需要深入理解的核心内容。本文将带你深入Linux内核5.4版本中SD卡驱动的实现细节,从mmc_blk_probe函数开始,逐步剖析块设备创建的完整流程,包括关键数据结构如request_queue、gendisk的作用机制,以及mmc_blk_alloc等核心函数的实现原理。
1. Linux块设备驱动框架概述
Linux块设备驱动架构是一个多层次的系统,从文件系统到物理设备之间经过了多个抽象层的处理。理解这个架构对于开发SD卡驱动至关重要。
核心层次结构:
- 文件系统层:如ext4、FAT32等,负责文件的组织和管理
- 通用块层(Generic Block Layer):处理块I/O请求的抽象
- I/O调度层:优化请求顺序,提高磁盘访问效率
- 块设备驱动层:直接与硬件交互的具体实现
在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 | 表示一个磁盘设备 |
2. MMC子系统驱动模型分析
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函数中:
- 注册RPMB总线类型
- 分配字符设备区域
- 注册块设备主设备号(MMC_BLOCK_MAJOR)
- 将mmc_driver注册到mmc_bus上
当SD卡插入时,内核会检测到设备并调用mmc_blk_probe函数,这是整个驱动初始化的起点。
3. mmc_blk_probe函数深度解析
mmc_blk_probe是SD卡驱动初始化的核心函数,它主要完成以下工作:
- 设备验证:检查卡是否支持块读写操作
- 工作队列创建:分配mmc_complete工作队列
- 块设备分配:调用mmc_blk_alloc分配和初始化块设备结构
- 分区处理:为可能的多个物理分区分配资源
- 设备注册:通过mmc_add_disk将gendisk注册到系统
让我们重点看一下关键代码流程:
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,它们分别负责块设备结构的分配和系统注册。
4. mmc_blk_alloc实现机制
mmc_blk_alloc函数是块设备创建的核心,它主要完成以下工作:
- 计算SD卡容量(以扇区为单位)
- 分配mmc_blk_data结构体
- 初始化mmc_queue和request_queue
- 创建并设置gendisk结构
- 关联各种数据结构
容量计算对于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_blk_data结构体
- 设置只读属性
- 分配gendisk结构体
- 初始化mmc_queue
- 创建request_queue
- 设置gendisk属性并关联request_queue
其中,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;
}
5. mmc_add_disk与块设备注册
mmc_add_disk函数负责将准备好的gendisk结构注册到系统中,使得用户空间可以通过设备节点访问SD卡。其主要工作包括:
- 调用device_add_disk注册gendisk
- 创建设备属性文件(如force_ro)
- 处理特殊分区(如RPMB)的只读锁定
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卡了。
6. 请求处理机制分析
SD卡的I/O请求处理是整个驱动中最关键的部分之一。当用户空间发起读写操作时,请求会经过以下流程:
- 文件系统将请求转换为bio结构
- 通用块层将bio封装为request
- I/O调度层将request加入队列
- 驱动从队列获取并处理request
在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是实际处理请求的核心函数,它会:
- 从request中提取命令和数据
- 通过mmc_host接口发送到底层硬件
- 处理完成中断和回调
7. 实际开发中的关键问题与调试技巧
在开发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");
- 使用ftrace跟踪请求流程:
bash复制echo 1 > /sys/kernel/debug/tracing/events/mmc/enable
cat /sys/kernel/debug/tracing/trace_pipe
在实际项目中,理解这些底层机制可以帮助开发者快速定位和解决复杂的驱动问题,特别是在需要对标准驱动进行定制修改时。
