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的流程異常。

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