mmc子系统总结

mmc总结

学习mmc部分的kernel代码,看了相关bug的处理,进行总结。文章分为以下部分。
1.mmc结构总结
2.mmc host controller
3.sd卡插入后的注册过程。
4.card层
5.数据传输流程
6.相关bug处理

1.mmc结构总结

mmc子系统按照连接对象的不同,分为emmc, sd, sdio三种类型。如果嵌入式系统中需要使用sd卡,emmc, sdio wifi,则在该系统中会有3个mmc host controller,分别连接这3个设备。从下往上,分为mmc host controller driver, core, card,三层。
1.mmc host controller层直接和硬件相关,上层传送过来的data和command读写请求( mmc_request),由mmc host controller driver具体操作硬件来实现。
2.core层主要负责mmc协议相关的功能,card detect之后的初始化,由core层向controller发起的mmc_request一般都是command请求。
3.card层应该是把mmc抽象成为一个block device driver, 向上层注册这个block device. 上层会向这个block device发起数据业务,由card层处理后,向controller层传递下去。由card层下发的mmc_request主要是数据收发请求。
4.mmc_init()
register bus type. 分为两种bus type, mmc_bus_type, sdio_bus_type, 其中mmc_bus_type中的match函数是直接return 1,匹配card层的驱动和连接的设备。而sdio_bus_type的match函数需要通过sdio_match_device来匹配device和driver. 比如一个sdio wifi device,在kernel中有好几个wifi driver,需要通过设备的id信息和驱动的sdio_device_id信息来找到正确的driver。

2.mmc controller driver

host controller probe: 读取dts中的host controller相关的配置,如中断号,基地址,存储在pdata中,初始化host controller, alloc struct mmc_host mmc, 填充mmc_host结构体, request irq handler用于数据传输,Dma_alloc_coherent descriptor buffer, request irq handler用于card detect. mmc_add_host(mmc); 向上层注册device.

struct mmc_host_ops需要了解,这个是host driver需要实现的一系列回调函数,主要有如下几个重要函数:
struct mmc_host_ops {
.request // 最重要的数据传输函数
.set_ios // set power, clock, bus width
.get_cd // card absent or present
.get_ro //read/write or read only
.start_signal_voltage_switch
.card_busy
.execute_tuning
.hw_reset //reset card
};

host driver实现的request回调函数流程:
如果是multiblock 读写,首先发送一个cmd(填写进一个descriptor结构体中),
des_cmd_cur->cmd_index = MMC_SET_BLOCK_COUNT;
desc_cur->cmd_arg = mrq->data->blocks;
dma_map_sg()为设备产生恰当的总线地址,它会合并物理上临近的内存区域
for_each_sg, 每个sg中的内存块的地址都被填写进host的descriptor的data_address域中, 有多少个sg 内存块, 就有多少个descriptor
然后准备一个descriptor, 填充stop cmd. 作为结束,设置end of chain(一系列的descriptor的结束)
des_cmd_cur->cmd_index = MMC_STOP_TRANSMISSION;
des_cmd_cur->end_of_chain = 1;
启动dma传输
irq,传输完成后,进入irq handler, 检查host state, 判断是否有error, 退出irq handler之后, 触发irq对应的thread, 读取mrq对应的response, 调用mrq的回调函数mrq->done(mrq);

3.卡插入后的注册过程

mmc sd card detect
通过request_threaded_irq 来注册一个thread, 不注册irq handler, 卡的探测不在中断里做,因为卡的探测程序里面有delay, 耗时较长,不适合在中断服务程序里做,在中断触发的thread里面做就可以了。
卡插入拔出时会触发gpio中断,进入中断触发的thread里面,探测卡的插入状态,使用mutex_lock锁住卡的插入状态的探测部分,卡插入拔出时的抖动可能会导致反复触发中断,反复进入这个thread, 所以使用mutex_lock锁住关键部分代码。因为卡探测部分有delay,耗时长,所以不能使用spin_lock.

卡插入后触发gpio中断,之后触发detect thread,在thread中判断卡的插入状态,之后调用Mmc_detect_change函数,在函数中触发之前注册的delayed work :mmc_rescan. 调用之前注册的卡探测函数:host->ops->get_cd(host),因为之前已经探测过,get_cd只是查询一下之前卡探测结果。探测到卡插入,调用mmc_claim_host,判断host是否被占用了,如果被占用,则加入wait queue, 主动调用schedule(), 挂起task, 直到host被释放之后,再被唤醒。 mmc_hw_reset_for_init(host) reset card(拉gpio脚), sdio_reset(host); //sd卡会忽略这个reset, cmd0 card进入go idle状态。发送CMD8, 首先调用 mmc_attach_sdio(host), 如果插的是sd卡,会返回err, 然后继续调用mmc_attach_sd(host),
驱动循环发送ACMD41、CMD55给卡,读取OCR寄存器,成功后,依次发送CMD2(读CID)、CMD3(得到RCA)、CMD9(读CSD)、CMD7(选择卡)。后面还有几个命令分别是ACMD41&CMD51,使用CMD6切换一些功能,如切换到高速模式。经过上述步骤,已经确定当前插入的卡是一张有效、可识别的存储卡。然后调用mmc_add_card()把存储卡加到系统中。正式与系统驱动连接在一起。

**mmc_attach_sd**
    mmc_send_app_op_cond(host, 0, &ocr); //cmd41, 读取ocr register, ocr和power有关
    mmc_attach_bus(host, &mmc_sd_ops); //host->bus_ops = mmc_sd_ops
    mmc_select_voltage(host, ocr); //根据读取到的ocr
    mmc_sd_init_card(host, rocr, NULL);
        CMD2 get cid,       
        struct mmc_card *card;   card = mmc_alloc_card(host, &sd_type);     //struct mmc_card *card; 作为device存在,
        get csd
        cmd7  mmc_select_card(card);
        mmc_sd_setup_card
    mmc_add_card(host->card);  //device_add(card->dev);注册device

card->part来自ext_csd. ext_csd存放的是什么呢?就是emmc的’系统分区‘,不是user分区,如下:
4 Default Areas ofMemory Device
–2 x Boot Area Partitions for Booting(可以使128k,4mb 8mb不等)
–1 x Replay Protected Memory Block Area Partition—–RPMB
–1 x User Data Area

3.CARD层相关

card 层初始化

static int __init mmc_blk_init(void)
    register_blkdev(MMC_BLOCK_MAJOR, "mmc"); //将块设备名”mmc”和主设备号注册到块层中
    mmc_register_driver(&mmc_driver);   //register mmc driver

sd卡插入后,card device register, 跟mmc_driver 匹配上。调用到 mmc_blk_probe(struct mmc_card *card

**mmc_blk_probe**
    mmc_blk_alloc //分配并填充mmc_blk_data结构体,md,分配disk节点/dev/block/mmcblk0
        md = kzalloc(sizeof(struct **mmc_blk_data**), GFP_KERNEL); //mmc_blk_data,重要结构体
//md->part, md->disk,  md->card, md->queue 
// struct **mmc_blk_data包括 gendisk指针和mmc_queue
    alloc_disk  //分配通用 gendisk 结构体
    mmc_init_queue(&md->queue, card, &md->lock, subname);
        mq->queue = blk_init_queue(mmc_request_fn, lock);
        mmc_alloc_sg 
        mq->thread = kthread_run(mmc_queue_thread, mq, "mmcqd/%d%s",
    md->queue.issue_fn = mmc_blk_issue_rq;
    snprintf(md->disk->disk_name, sizeof(md->disk->disk_name), "mmcblk%d%s", md-   >name_idx, subname ? subname : ""); //节点名字
mmc_blk_alloc_parts(card, md)
    mmc_blk_alloc_part(card, md, card->part[idx].part_cfg, card->part[idx].size >> 9….)
        struct *mmc_blk_data  part_md = mmc_blk_alloc_req(card, disk_to_dev(md->disk), size, default_ro,//分配病填充了一个mmc_blk_data,里面保存的是一个分区的信息。
    list_add(&part_md->part, &md->part);//  位置1 把分区链接到md->part链表上
    mmc_add_disk(md) //增加disk 到节点:/dev/block/mmcblk0
    list_for_each_entry(part_md, &md->part, part) { // 找到位置1的操作的part_md
    mmc_add_disk(part_md)
} //循环增加disk的分区到各自节点:一般是两个part节点,/dev/block/mmcblk0boot0和 boot1
    XXX_emmc_partition_ops(card, md->disk); //自己加入的代码,读取emmc中的分区信息,分区信息储存在约定的某个位置。把分区信息储存   在gendisk->part_tbl->part[partno]中。

card层结构体
mmc_blk_data
gendisk:使用gendisk(通用磁盘)结构体来表示1 个独立的磁盘设备(或分区)。
major、first_minor 和minors 共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备
号,而次设备号则不同。fops 为block_device_operations,即上节描述的块设备操作集合。queue 是内核
用来管理这个设备的I/O 请求队列的指针。capacity 表明设备的容量,以512 个字节为单位。private_data
可用于指向磁盘的任何私有数据,用法与字符设备驱动file 结构体的private_data 类似。

struct disk_part_tbl __rcu *part_tbl;

struct hd_struct __rcu *part[]; 来描述一块设备上的某一分区

这里写图片描述
struct request_queue *queue;

io请求,经过io调度器,转换成request,放到gendisk结构中的request_queue中。

major、first_minor 和minors 共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备
号,而次设备号则不同。fops 为block_device_operations,即上节描述的块设备操作集合。queue 是内核
用来管理这个设备的I/O 请求队列的指针。capacity 表明设备的容量,以512 个字节为单位。private_data
可用于指向磁盘的任何私有数据,用法与字符设备驱动file 结构体的private_data 类似

5.mmc数据传输过程:

io请求从上层传下来之后,先经过IO调度层,转换成了request, 填充到request_queue中,
mmc_request_fn —> mmc_queue_thread

mmc_queue_thread
    req = blk_fetch_request(q);
    mq->issue_fn(mq, req);  // mmc_blk_issue_rq
    mmc_blk_issue_rq
        mmc_blk_issue_rw_rq(mq, NULL);
        mmc_start_req(card->host, areq, (int *) &status);
        __mmc_start_data_req(host, areq->mrq);      mrq->done = mmc_wait_data_done;
        mmc_start_request(host, mrq);
        host->ops->request(host, mrq);
data传输完成,触发irq
    mrq->done()——> mmc_wait_data_done
    wake_up_interruptible(&context_info->wait);
    mmc_start_req 中的mmc_wait_for_data_req_done 

mmc 命令传输过程:
并不是所有的mmc命令都是读写命令,那其他的命令该如何完成呢,他们与mmc的读写命令有什么差别。我们用mmc的CMD8 SEND_IF_COND作为例子,mmc_send_if_cond()是发送CMD8的函数。
函数很简单,进来就初始化一个局部变量struct mmc_command cmd。填好命令CMD8,给定返回的RSP参数值,无需初始化cmd->data,因为CMD8没有数据阶段。直接通过mmc_wait_for_cmd() 发送出去。
mmc_wait_for_cmd()里面创建mrq结构变量,之前说过mrq变量的意义, mrq.cmd = cmd; cmd->data = NULL; mmc_wait_for_req(host, &mrq);
__mmc_start_req() 启动 mmc_start_request() 这基本跟读写命令的流程就一致了。
mmc_wait_for_req_done() 等待wait_for_completion_io(&mrq->completion); 看得出来这里面和读写流程的不同,在本次传输启动后,立刻同步等待中断到来。因为单次的CMD8命令并没有其他的循环处理,因此如果不再本次处理等待,将来也没有机会再进入同步等待阶段。
本次的wait等待是mrq->completion,和读写命令的也有所不同。仔细看__mmc_start_req() mrq->done = mmc_wait_done; 而读写的是mrq->done=mmc_wait_data_done。剩下的事情就是返回处理结果。
对于又有命令又有数据的单次命令,譬如mmc_send_cxd_data(). mrq.data也需要赋值,我们知道读写命令里面,需要初始化data->sg变量。这里也不例外,data->sg的初始化由sg_init_one(&sg, data_buf, len);完成,看函数名就知道,这是一个初始化单一数据处理的dma。只需要传输一次,大部分是做读取用。

6.bug处理
host controller层。如果修改descriptor的timeout域,设置超时出错的时间,超时后后会产生中断。

host tunning: Host sends CMD19/21 to device, the device sends back tuning packet, record CRC status, change RX clock phase and RX delay line, till whole 360 degree scanned. Select the middle as working setting.

卡插入后进行pinmux setting时,用mutex_lock住,不用spin_lock, 因为pinmux setting耗费时间。为什么要lock?因为插插入时,如果有抖动,反复触发irq thread,导致重复进入pinmux setting的关键代码,导致pinmux setting的流程异常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章