5. [mmc subsystem] mmc core(第五章)——card相关模块(mmc type card)
零、說明(重要,需要先搞清楚概念有助于后面的理解)
1、mmc core
card相關模塊為對應card實現(xiàn)相應的操作,包括初始化操作、以及對應的總線操作集合。負責和對應card協(xié)議層相關的東西。
這里先學習mmc type card。后續(xù)再學習sd type card。
對應代碼:
drivers/mmc/core/mmc.c(提供接口), drivers/mmc/core/mmc-ops.c(提供和mmc type card協(xié)議相關的操作), drivers/mmc/core/mmc-ops.h2、另外,這里繼續(xù)強調一下mmc的概念
mmc core是指mmc subsystem的核心實現(xiàn),這里的mmc是表示mmc總線、接口、設備相關的一種統(tǒng)稱,可以理解為一種軟件架構。
而mmc type card則是指mmc卡或者emmc。
總之,這里的mmc是兩種概念概念,需要自己先消化一下。
3、mmc總線和mmc_bus
在本文里面這兩個是不同的概念。
mmc_bus是指mmc core抽象出來的虛擬總線,和mmc設備對應的硬件總線無關,是一種軟件概念。
而本文的mmc總線是一種物理概念,是實際的總線,是和host controller直接相關聯(lián)的。
一、API總覽
1、mmc type card匹配相關
- mmc_attach_mmc
提供給mmc core主模塊使用,用于綁定card到host bus上(也就是card和host的綁定)。
通過mmc_host獲取mmc type card信息,初始化mmc_card,并進行部分驅動,最后將其注冊到mmc_bus上。
原型:int mmc_attach_mmc(struct mmc_host *host)2、mmc type card協(xié)議相關操作
- mmc_ops提供了部分和mmc type
card協(xié)議相關操作,這些操作會在mmc.c中mmc的初始化過程中被使用到。
建議先簡單了解一下mmc協(xié)議的內容。后續(xù)會進行總結。
- mmc_go_idle
發(fā)送CMD0指令,GO_IDLE_STATE
使mmc card進入idle state。
雖然進入到了Idle State,但是上電復位過程并不一定完成了,這主要靠讀取OCR的busy位來判斷,而流程歸結為下一步。
- mmc_send_op_cond
發(fā)送CMD1指令,SEND_OP_COND
這里會設置card的工作電壓寄存器OCR,并且通過busy位(bit31)來判斷card的上電復位過程是否完成,如果沒有完成的話需要重復發(fā)送。
完成之后,mmc card進入ready state。
- mmc_all_send_cid
這里會發(fā)送CMD2指令,ALL_SEND_CID
廣播指令,使card回復對應的CID寄存器的值。在這里就相應獲得了CID寄存器的值了,存儲在cid中。
完成之后,MMC card會進入Identification State。
- mmc_set_relative_addr
發(fā)送CMD3指令,SET_RELATIVE_ADDR
設置該mmc card的關聯(lián)地址為card->rca,也就是0x0001
完成之后,該MMC card進入standby模式。
- mmc_send_csd
發(fā)送CMD9指令,MMC_SEND_CSD
要求mmc card發(fā)送csd寄存器,存儲到card->raw_csd中,也就是原始的csd寄存器的值。
此時mmc card還是處于standby state
- mmc_select_card & mmc_deselect_cards
發(fā)送CMD7指令,SELECT/DESELECT CARD
選擇或者斷開指定的card
這時卡進入transfer state。后續(xù)可以通過各種指令進入到receive-data state或者sending-data state依次來進行數(shù)據(jù)的傳輸
- mmc_get_ext_csd
發(fā)送CMD8指令,SEND_EXT_CSD
這里要求處于transfer state的card發(fā)送ext_csd寄存器,這里獲取之后存放在ext_csd寄存器中
這里會使card進入sending-data state,完成之后又退出到transfer state。
- mmc_switch
發(fā)送CMD6命令,MMC_SWITCH
用于設置ext_csd寄存器的某些bit
- mmc_send_status
發(fā)送CMD13命令,MMC_SEND_STATUS
要求card發(fā)送自己當前的狀態(tài)寄存器
- mmc_send_cid
發(fā)送CMD10命令,MMC_SEND_CID
要求mmc card回復cid寄存器
- mmc_card_sleepawake
發(fā)送CMD5命令,MMC_SLEEP_AWAKE
使card進入或者退出sleep state,由參數(shù)決定。關于sleep state是指card的一種狀態(tài),具體參考emmc 5.1協(xié)議。
先結合協(xié)議理解上述幾個mmc type card的操作函數(shù)有助于理解后續(xù)mmc card的初始化代碼。具體參考第五節(jié)。
二、數(shù)據(jù)結構
1、mmc_ops & mmc_ops_unsafe
struct mmc_bus_ops表示mmc host在總線上的操作集合,由host的card 設備來決定,mmc type card、sd type card相應的操作集合是不一樣的。
mmc_ops和mmc_ops_unsafe則表示mmc type card所屬的host對于總線的操作集合。
static const struct mmc_bus_ops mmc_ops = {.awake = mmc_awake, .sleep = mmc_sleep, .remove = mmc_remove, .detect = mmc_detect,.suspend = NULL,.resume = NULL,.power_restore = mmc_power_restore,.alive = mmc_alive,.change_bus_speed = mmc_change_bus_speed, };static const struct mmc_bus_ops mmc_ops_unsafe = {.awake = mmc_awake, // 使mmc總線上的mmc type card退出sleep state.sleep = mmc_sleep, // 使mmc總線的mmc type card進入sleep state.remove = mmc_remove, // 釋放mmc type card.detect = mmc_detect, // 檢測mmc總線的mmc type card是否拔出.suspend = mmc_suspend, // suspend掉mmc總線上的mmc type card,注意不僅僅會使card進入sleep state,還會對clock以及mmc cache進行操作.resume = mmc_resume, // resume上mmc總線上的mmc type card.power_restore = mmc_power_restore, // 恢復mmc總線上的mmc type card的電源狀態(tài).alive = mmc_alive, // 檢測mmc總線上的mmc type card狀態(tài)是否正常.change_bus_speed = mmc_change_bus_speed, // 修改mmc總線時鐘頻率 };mmc_ops_unsafe和mmc_ops的區(qū)別在于是否實現(xiàn)suspend和resume方法。
對于card不可移除的host來說,需要使用mmc_ops_unsafe這個mmc_bus_ops來支持suspend和resume。
之所以在上述注釋中不斷說明mmc總線,是為了強調應該和mmc_bus虛擬總線區(qū)分開來,這里的mmc總線是物理概念、是和host controller直接相關聯(lián)的。
2、mmc_type
struct device_type mmc_type中為mmc_card定義了很多屬性,可以在sysfs中進行查看。
/sys/class/mmc_host/mmc0/mmc0:0001 或者/sys/bus/mmc/devices/mmc0:0001下可以查看到如下屬性 block cid csd date driver enhanced_area_offset enhanced_area_size erase_size fwrev hwrev manfid name oemid power preferred_erase_size prv raw_rpmb_size_mult rel_sectors runtime_pm_timeout serial subsystem type ueventmmc_type對應實現(xiàn)如下:
static struct device_type mmc_type = {.groups = mmc_attr_groups, };static const struct attribute_group *mmc_attr_groups[] = {&mmc_std_attr_group,NULL, };static struct attribute_group mmc_std_attr_group = {.attrs = mmc_std_attrs, };MMC_DEV_ATTR(cid, "%08x%08x%08x%08x\n", card->raw_cid[0], card->raw_cid[1],card->raw_cid[2], card->raw_cid[3]); MMC_DEV_ATTR(csd, "%08x%08x%08x%08x\n", card->raw_csd[0], card->raw_csd[1],card->raw_csd[2], card->raw_csd[3]); //...................略過一些 MMC_DEV_ATTR(raw_rpmb_size_mult, "%#x\n", card->ext_csd.raw_rpmb_size_mult); MMC_DEV_ATTR(rel_sectors, "%#x\n", card->ext_csd.rel_sectors);static struct attribute *mmc_std_attrs[] = {&dev_attr_cid.attr,&dev_attr_csd.attr,&dev_attr_date.attr,&dev_attr_erase_size.attr,&dev_attr_preferred_erase_size.attr,&dev_attr_fwrev.attr,&dev_attr_hwrev.attr, //.....................略過一些&dev_attr_rel_sectors.attr,NULL, };補充說明,可以發(fā)現(xiàn)這些信息都是從mmc_card的cid寄存器和ext_csd寄存器中讀取的。
三、接口代碼說明
1、mmc_attach_mmc實現(xiàn)
用于通過mmc_host獲取mmc type card信息,初始化mmc_card,并進行部分驅動,最后將其注冊到mmc_bus上。
主要工作:
設置總線模式
選擇一個card和host都支持的最低工作電壓
對于不同type的card,相應mmc總線上的操作協(xié)議也可能有所不同。所以需要設置相應的總線操作集合(mmc_host->bus_ops)
初始化card使其進入工作狀態(tài)(mmc_init_card)
為card構造對應的mmc_card并且注冊到mmc_bus中(mmc_add_card,具體參考《mmc core——bus模塊說明》)
代碼如下:
int mmc_attach_mmc(struct mmc_host *host) {int err;u32 ocr;BUG_ON(!host);WARN_ON(!host->claimed);/* Set correct bus mode for MMC before attempting attach */ /* 在嘗試匹配之前,先設置正確的總線模式 */if (!mmc_host_is_spi(host))mmc_set_bus_mode(host, MMC_BUSMODE_OPENDRAIN);/* 獲取card的ocr寄存器 */err = mmc_send_op_cond(host, 0, &ocr);// 發(fā)送CMD1命令(MMC_SEND_OP_COND),并且參數(shù)為0// 這里獲取OCR(Operation condition register)32位的OCR包含卡設備支持的工作電壓表,存儲到ocr變量中// 如果Host的IO電壓可調整,那調整前需要讀取OCR。為了不使卡誤進入Inactive State,可以給MMC卡發(fā)送不帶參數(shù)的CMD1,這樣可以僅獲取OCR寄存器,而不會改變卡的狀態(tài)。/* 對于不同type的card,相應mmc總線上的操作協(xié)議也可能有所不同 */ /* 所以這里設置mmc_host的總線操作集合,為mmc_ops_unsafe或者mmc_ops,上述已經(jīng)說明 */mmc_attach_bus_ops(host);// 設置host->bus_ops,也就是會為host的bus選擇一個操作集,對于non-removable的host來說,這里對應應該為mmc_ops_unsafe/* 為card選擇一個HOST和card都支持的最低電壓 */if (host->ocr_avail_mmc)host->ocr_avail = host->ocr_avail_mmc; // 選擇mmc的可用ocr值作為host的ocr_avail值if (ocr & 0x7F) {ocr &= ~0x7F; // 在標準MMC協(xié)議中,OCR寄存器的bit6-0位是屬于保留位,并不會使用,所以這里對應將其清零}host->ocr = mmc_select_voltage(host, ocr); // 通過OCR寄存器選擇一個HOST和card都支持的最低電壓/* 調用mmc_init_card初始化該mmc type card,這里是核心函數(shù),后續(xù)會繼續(xù)說明 */err = mmc_init_card(host, host->ocr, NULL); // 初始化該mmc type card,并為其分配和初始化一個對應的mmc_cardif (err)goto err;/* 將分配到的mmc_card注冊到mmc_bus中 */mmc_release_host(host); // 先釋放掉host,可能是在mmc_add_card中會獲取這個hosterr = mmc_add_card(host->card); // 調用到mmc_add_card,將card注冊到設備驅動模型中。// 這時候該mmc_card就掛在了mmc_bus上,會和mmc_bus上的block這類mmc driver匹配起來。具體再學習mmc card driver的時候再說明。mmc_claim_host(host); // 再次申請hostif (err)goto remove_card;/* clock scaling相關的東西,這里暫時先不關心 */mmc_init_clk_scaling(host);register_reboot_notifier(&host->card->reboot_notify);return 0;remove_card:mmc_release_host(host);mmc_remove_card(host->card);mmc_claim_host(host);host->card = NULL; err:mmc_detach_bus(host);pr_err("%s: error %d whilst initialising MMC card\n",mmc_hostname(host), err);return err; }重點說明
(1)在attach過程中,有一個很重要的函數(shù)mmc_init_card,第四節(jié)就要圍繞這個函數(shù)進行展開。
(2)調用了mmc_add_card之后mmc_card就掛在了mmc_bus上,會和mmc_bus上的block(mmc_driver)匹配起來。相應block(mmc_driver)就會進行probe,驅動card,實現(xiàn)card的實際功能(也就是存儲設備的功能)。會對接到塊設備子系統(tǒng)中。具體在學習mmc card driver的時候再說明。
四、mmc type card內部核心代碼說明
1、mmc_init_card
在第三節(jié)中,可以看出mmc_attach_mmc中的一個核心函數(shù)就是mmc_init_card,用于對mmc type card進行實質性的初始化,并為其分配和初始化一個對應的mmc_card。這部分和協(xié)議相關,需要先學習一下mmc協(xié)議。
- 主要工作
- 根據(jù)協(xié)議初始化mmc type card,使其進入相應狀態(tài)(standby state)
- 為mmc type card構造對應mmc_card并進行設置
- 從card的csd寄存器以及ext_csd寄存器獲取card信息并設置到mmc_card的相應成員中
- 根據(jù)host屬性以及一些需求修改ext_csd寄存器的值
- 設置mmc總線時鐘頻率以及位寬
- 代碼如下
后續(xù)會有一篇文章《結合log分析emmc初始化過程中的命令流程》來說明上述mmc_init_card的實際流程。
2、mmc_ops_unsafe相關函數(shù)實現(xiàn)
選擇幾個重點的進行說明:
static const struct mmc_bus_ops mmc_ops_unsafe = {.awake = mmc_awake, // 使mmc總線上的mmc type card退出sleep state.sleep = mmc_sleep, // 使mmc總線的mmc type card進入sleep state.remove = mmc_remove, // 釋放mmc type card.detect = mmc_detect, // 檢測mmc總線的mmc type card是否拔出.suspend = mmc_suspend, // suspend掉mmc總線上的mmc type card,注意不僅僅會使card進入sleep state,還會對clock以及mmc cache進行操作.resume = mmc_resume, // resume上mmc總線上的mmc type card.power_restore = mmc_power_restore, // 恢復mmc總線上的mmc type card的電源狀態(tài).alive = mmc_alive, // 檢測mmc總線上的mmc type card狀態(tài)是否正常.change_bus_speed = mmc_change_bus_speed, // 修改mmc總線時鐘頻率 };/**********************使mmc總線上的mmc type card退出sleep state************************/ static int mmc_awake(struct mmc_host *host) {//...if (card && card->ext_csd.rev >= 3) { // 判斷版本是否大于3err = mmc_card_sleepawake(host, 0); // 發(fā)送CMD5指令,MMC_SLEEP_AWAKE,參數(shù)為0,表示退出sleep state.(如果參數(shù)為1就是進入sleep state)// 完成之后,該MMC card從sleep state進入standby模式。}//... }/**********************檢測mmc總線的mmc type card是否拔出************************/ static void mmc_detect(struct mmc_host *host) {int err;mmc_rpm_hold(host, &host->card->dev);mmc_claim_host(host);/* 檢測card是否被拔出 */err = _mmc_detect_card_removed(host);mmc_release_host(host);/** if detect fails, the device would be removed anyway;* the rpm framework would mark the device state suspended.*/ /* card并沒有被拔出,說明出現(xiàn)異常了,標記card的rpm狀態(tài)為suspend */if (!err)mmc_rpm_release(host, &host->card->dev);/* card確實被拔出,正常釋放card */if (err) {mmc_remove(host);mmc_claim_host(host);mmc_detach_bus(host);mmc_power_off(host);mmc_release_host(host);} }/********************** 修改mmc總線時鐘頻率************************/ /*** mmc_change_bus_speed() - Change MMC card bus frequency at runtime* @host: pointer to mmc host structure* @freq: pointer to desired frequency to be set** Change the MMC card bus frequency at runtime after the card is* initialized. Callers are expected to make sure of the card's* state (DATA/RCV/TRANSFER) beforing changing the frequency at runtime.*/ static int mmc_change_bus_speed(struct mmc_host *host, unsigned long *freq) {int err = 0;struct mmc_card *card;mmc_claim_host(host);/** Assign card pointer after claiming host to avoid race* conditions that may arise during removal of the card.*/card = host->card;if (!card || !freq) {err = -EINVAL;goto out;}/* 確定出一個可用頻率 */if (mmc_card_highspeed(card) || mmc_card_hs200(card)|| mmc_card_ddr_mode(card)|| mmc_card_hs400(card)) {if (*freq > card->ext_csd.hs_max_dtr)*freq = card->ext_csd.hs_max_dtr;} else if (*freq > card->csd.max_dtr) {*freq = card->csd.max_dtr;}if (*freq < host->f_min)*freq = host->f_min;/* 根據(jù)實際要設置的頻率值來設置時鐘 */if (mmc_card_hs400(card)) {err = mmc_set_clock_bus_speed(card, *freq);if (err)goto out;} else {mmc_set_clock(host, (unsigned int) (*freq));}/* 對于hs200來說,修改完頻率之后需要執(zhí)行execute_tuning來選擇一個合適的采樣點 */if (mmc_card_hs200(card) && card->host->ops->execute_tuning) {/** We try to probe host driver for tuning for any* frequency, it is host driver responsibility to* perform actual tuning only when required.*/mmc_host_clk_hold(card->host);err = card->host->ops->execute_tuning(card->host,MMC_SEND_TUNING_BLOCK_HS200);mmc_host_clk_release(card->host);if (err) {pr_warn("%s: %s: tuning execution failed %d. Restoring to previous clock %lu\n",mmc_hostname(card->host), __func__, err,host->clk_scaling.curr_freq);mmc_set_clock(host, host->clk_scaling.curr_freq); // 采樣失敗,設置回原來的時鐘頻率}} out:mmc_release_host(host);return err; }五、mmc ops接口說明
1、說明
- mmc_ops提供了部分和mmc type card協(xié)議相關操作,這些操作會在mmc.c中mmc的初始化過程中被使用到。
- 這些操作都會發(fā)起mmc請求,因此會調用mmc core主模塊的mmc請求API,會在mmc core中進行說明。
- 建議先簡單了解一下mmc協(xié)議的內容。后續(xù)會進行總結。
2、代碼說明
以下說明比較典型和比較特殊的接口
- mmc_send_status(典型)
發(fā)送CMD13命令,MMC_SEND_STATUS
要求card發(fā)送自己當前的狀態(tài)寄存器
int mmc_send_status(struct mmc_card *card, u32 *status) {int err;struct mmc_command cmd = {0};BUG_ON(!card);BUG_ON(!card->host);/* 主要是根據(jù)對應命令構造struct mmc_command */cmd.opcode = MMC_SEND_STATUS; // 設置命令操作碼opcode,這里設置為MMC_SEND_STATUS,也就是CMD13if (!mmc_host_is_spi(card->host))cmd.arg = card->rca << 16; // 設置命令的對應參數(shù),這里設置為card的RCA地址cmd.flags = MMC_RSP_SPI_R2 | MMC_RSP_R1 | MMC_CMD_AC; // 設置請求的一些標識,包括命令類型,response類型等等/* 調用mmc_wait_for_cmd發(fā)送命令請求并且等待命令處理完成。 */err = mmc_wait_for_cmd(card->host, &cmd, MMC_CMD_RETRIES);if (err)return err;/* NOTE: callers are required to understand the difference* between "native" and SPI format status words!*/ /* 對response的處理 */if (status)*status = cmd.resp[0]; // 依照協(xié)議,response[0]存儲了status,因此這里提取cmd.resp[0]return 0; }- mmc_send_op_cond(特殊)
發(fā)送CMD1指令,SEND_OP_COND
在idle狀態(tài)時,向卡傳送Host支持的電壓范圍,卡回復OCR的值以及上電復位的狀態(tài)。如果發(fā)送的電壓參數(shù)為0,則卡僅傳回OCR的值,并不進行判斷。如果發(fā)送的電壓參數(shù)存在,則和卡本身的OCR對比,若不符合,則卡進入Inactive State,符合,則返回OCR寄存器的值。
其實和典型的接口類似,但是其特殊之處在于需要通過busy位(bit31)來判斷card的上電復位過程是否完成,如果沒有完成的話需要重復發(fā)送。
int mmc_send_op_cond(struct mmc_host *host, u32 ocr, u32 *rocr) {struct mmc_command cmd = {0};int i, err = 0;BUG_ON(!host);/* 主要是根據(jù)對應命令構造struct mmc_command */cmd.opcode = MMC_SEND_OP_COND;cmd.arg = mmc_host_is_spi(host) ? 0 : ocr;cmd.flags = MMC_RSP_SPI_R1 | MMC_RSP_R3 | MMC_CMD_BCR;/* 需要判斷status的busy(bit31)來判斷上電復位是否完成,如果沒有完成的話需要重復發(fā)送。 */for (i = 100; i; i--) {err = mmc_wait_for_cmd(host, &cmd, 0);if (err)break;/* if we're just probing, do a single pass */if (ocr == 0) // ocr為0,說明只是讀取ocr寄存器的值,不進行判斷break;/* otherwise wait until reset completes */if (mmc_host_is_spi(host)) {if (!(cmd.resp[0] & R1_SPI_IDLE))break;} else {if (cmd.resp[0] & MMC_CARD_BUSY)break;// 如果發(fā)送的電壓參數(shù)存在,則和卡本身的OCR對比,若不符合,則卡進入Inactive State,符合,則返回OCR寄存器的值。// 同時,需要判斷OCR寄存器的busy位來判斷上電復位是否完成。}err = -ETIMEDOUT;mmc_delay(10);}if (rocr && !mmc_host_is_spi(host))*rocr = cmd.resp[0];return err; }- mmc_send_ext_csd
發(fā)送CMD8指令,SEND_EXT_CSD
這里要求處于transfer state的card發(fā)送ext_csd寄存器,這里獲取之后存放在ext_csd寄存器中
這里會使card進入sending-data state,完成之后又退出到transfer state。
特殊之處在于涉及到了DATA線上的數(shù)據(jù)傳輸,會調用到內部接口mmc_send_cxd_data。
如下:
int mmc_switch(struct mmc_card *card, u8 set, u8 index, u8 value,unsigned int timeout_ms) {return __mmc_switch(card, set, index, value, timeout_ms, true, false); }int __mmc_switch(struct mmc_card *card, u8 set, u8 index, u8 value,unsigned int timeout_ms, bool use_busy_signal,bool ignore_timeout) {int err;struct mmc_command cmd = {0};unsigned long timeout;u32 status;BUG_ON(!card);BUG_ON(!card->host);/* 主要是根據(jù)對應命令構造struct mmc_command */cmd.opcode = MMC_SWITCH;cmd.arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) |(index << 16) |(value << 8) |set;cmd.flags = MMC_CMD_AC;if (use_busy_signal)cmd.flags |= MMC_RSP_SPI_R1B | MMC_RSP_R1B;elsecmd.flags |= MMC_RSP_SPI_R1 | MMC_RSP_R1;cmd.cmd_timeout_ms = timeout_ms;cmd.ignore_timeout = ignore_timeout;/* 調用mmc_wait_for_cmd發(fā)送命令請求并且等待命令處理完成。 */err = mmc_wait_for_cmd(card->host, &cmd, MMC_CMD_RETRIES);if (err)return err;/* No need to check card status in case of unblocking command */if (!use_busy_signal)return 0;/* 調用mmc_send_status發(fā)送CMD13獲取card status,等待card退出programming state。 */timeout = jiffies + msecs_to_jiffies(MMC_OPS_TIMEOUT_MS);do {err = mmc_send_status(card, &status);if (err)return err;if (card->host->caps & MMC_CAP_WAIT_WHILE_BUSY)break;if (mmc_host_is_spi(card->host))break;/* Timeout if the device never leaves the program state. */if (time_after(jiffies, timeout)) {pr_err("%s: Card stuck in programming state! %s\n",mmc_hostname(card->host), __func__);return -ETIMEDOUT;}} while (R1_CURRENT_STATE(status) == R1_STATE_PRG);if (mmc_host_is_spi(card->host)) {if (status & R1_SPI_ILLEGAL_COMMAND)return -EBADMSG;} else {if (status & 0xFDFFA000)pr_warning("%s: unexpected status %#x after ""switch", mmc_hostname(card->host), status);if (status & R1_SWITCH_ERROR)return -EBADMSG;}return 0; }后續(xù)會有一篇文章《結合log分析emmc初始化過程中的命令流程》來說明上述mmc_init_card的實際流程。
轉載于:https://www.cnblogs.com/linhaostudy/p/10811290.html
總結
以上是生活随笔為你收集整理的5. [mmc subsystem] mmc core(第五章)——card相关模块(mmc type card)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python文件IO操作
- 下一篇: Hexo搭建个人网站