在讀者學習本章之前,最好了解Nand Flash讀寫過程和操作,可以參考:Nand Flash裸機操作。
一開始想在本章寫eMMC框架和設備驅動,但是沒有找到關於eMMC設備驅動具體寫法,所以本章仍繼續完成Nand Flash設備驅動,eMMC這個坑留在以后填。如果讀者開發板為eMMC,本節驅動可能無法正常執行。
在裸機操作中,讀者應了解Nand Flash時序圖、Nand Flash片選、讀寫和擦除等操作,在此不再贅述。
一、Nand Flash驅動分析
Nand Flash設備驅動放在drivers/mtd/nand目錄下,mtd(memory technology device,存儲技術設備)是用於訪問存儲設備(ROM、flash)的子系統。mtd的主要目的是為了使新的存儲設備的驅動更加簡單,因此它在硬件和頂層之間提供了一個抽象的接口。
讀者可在此目錄下任意選擇一個單板驅動文件進行分析,我選擇的是davinci_nand.c文件。
Nand Flash和Nor Flash文件鏈接:
https://files.cnblogs.com/files/Lioker/18_nand_nor.zip
首先來看它的入口函數:
1 static int __init nand_davinci_init(void) 2 { 3 return platform_driver_probe(&nand_davinci_driver, nand_davinci_probe); 4 }
我們進入platform_driver的probe函數中,看看它是如何初始化

1 static int __init nand_davinci_probe(struct platform_device *pdev) 2 { 3 struct davinci_nand_pdata *pdata = pdev->dev.platform_data; 4 struct davinci_nand_info *info; 5 struct resource *res1; 6 struct resource *res2; 7 void __iomem *vaddr; 8 void __iomem *base; 9 int ret; 10 uint32_t val; 11 nand_ecc_modes_t ecc_mode; 12 struct mtd_partition *mtd_parts = NULL; 13 int mtd_parts_nb = 0; 14 15 ... 16 /* 初始化硬件,如設置TACLS、TWRPH0、TWRPH1等 */ 17 platform_set_drvdata(pdev, info); 18 ... 19 /* 配置mtd_info結構體,它是nand_chip的抽象 */ 20 info->mtd.priv = &info->chip; 21 info->mtd.name = dev_name(&pdev->dev); 22 info->mtd.owner = THIS_MODULE; 23 24 info->mtd.dev.parent = &pdev->dev; 25 26 /* 配置nand_chip結構體 */ 27 info->chip.IO_ADDR_R = vaddr; 28 info->chip.IO_ADDR_W = vaddr; 29 info->chip.chip_delay = 0; 30 info->chip.select_chip = nand_davinci_select_chip; 31 ... 32 /* Set address of hardware control function */ 33 info->chip.cmd_ctrl = nand_davinci_hwcontrol; 34 info->chip.dev_ready = nand_davinci_dev_ready; 35 36 /* Speed up buffer I/O */ 37 info->chip.read_buf = nand_davinci_read_buf; 38 info->chip.write_buf = nand_davinci_write_buf; 39 40 /* Use board-specific ECC config */ 41 ecc_mode = pdata->ecc_mode; 42 43 ret = -EINVAL; 44 switch (ecc_mode) { 45 case NAND_ECC_NONE: 46 case NAND_ECC_SOFT: /* 啟動軟件ECC */ 47 pdata->ecc_bits = 0; 48 break; 49 case NAND_ECC_HW: /* 啟動硬件ECC */ 50 ... 51 break; 52 default: 53 ret = -EINVAL; 54 goto err_ecc; 55 } 56 info->chip.ecc.mode = ecc_mode; 57 58 /* 使能nand clk */ 59 info->clk = clk_get(&pdev->dev, "aemif"); 60 ... 61 ret = clk_enable(info->clk); 62 ... 63 val = davinci_nand_readl(info, A1CR_OFFSET + info->core_chipsel * 4); 64 ... 65 /* 掃描Nand Flash */ 66 ret = nand_scan_ident(&info->mtd, pdata->mask_chipsel ? 2 : 1, NULL); 67 ... 68 /* second phase scan */ 69 ret = nand_scan_tail(&info->mtd); 70 /* 以上nand_scan_ident()和nand_scan_tail()兩步可以使用nand_scan()代替 */ 71 72 if (mtd_has_cmdlinepart()) { 73 static const char *probes[] __initconst = { 74 "cmdlinepart", NULL 75 }; 76 /* 設置分區 */ 77 mtd_parts_nb = parse_mtd_partitions(&info->mtd, probes, 78 &mtd_parts, 0); 79 } 80 81 if (mtd_parts_nb <= 0) { 82 mtd_parts = pdata->parts; 83 mtd_parts_nb = pdata->nr_parts; 84 } 85 86 /* Register any partitions */ 87 if (mtd_parts_nb > 0) { 88 ret = mtd_device_register(&info->mtd, mtd_parts, 89 mtd_parts_nb); 90 if (ret == 0) 91 info->partitioned = true; 92 } 93 94 /* If there's no partition info, just package the whole chip 95 * as a single MTD device. 96 */ 97 if (!info->partitioned) 98 ret = mtd_device_register(&info->mtd, NULL, 0) ? -ENODEV : 0; 99 ... 100 return ret; 101 }
probe()函數所做的有以下幾點:
1. 初始化硬件,如設置TACLS、TWRPH0、TWRPH1等
2. 配置mtd_info結構體,它是nand_chip等底層Flash結構體的抽象,用於描述MTD設備,定義了MTD數據和操作函數
3. 配置nand_chip結構體
4. 啟動軟件ECC
5. 使用clk_get()和clk_enable()獲取並使能Nand Flash時鍾
6. 使用nand_scan()掃描Nand Flash
7. 使用parse_mtd_partitions()解析命令行中設置的分區。若命令行中沒有設置mtdparts返回0;若設置了並且解析沒問題,那么返回分區的個數,否則返回小於0的數
8. 使用mtd_device_register()注冊Nand Flash分區
其中,
1. nand_scan()函數調用關系如下:
nand_scan(&info->mtd, pdata->mask_chipsel ? 2 : 1); -> nand_scan_ident(mtd, maxchips, NULL); /* 獲取Nand Flash存儲器類型 */ -> nand_get_flash_type(mtd, chip, busw, &nand_maf_id, &nand_dev_id, table); -> nand_scan_tail(mtd); /* 設置Nand Flash底層讀寫擦除等函數 */
nand_get_flash_type()函數定義如下:
1 static struct nand_flash_dev *nand_get_flash_type(struct mtd_info *mtd, struct nand_chip *chip, int busw, int *maf_id, int *dev_id, struct nand_flash_dev *type) 2 { 3 int i, maf_idx; 4 u8 id_data[8]; 5 int ret; 6 7 /* Select the device */ 8 chip->select_chip(mtd, 0); /* nand_chip的片選函數 */ 9 10 /* 11 * Reset the chip, required by some chips (e.g. Micron MT29FxGxxxxx) 12 * after power-up 13 */ 14 chip->cmdfunc(mtd, NAND_CMD_RESET, -1, -1); 15 16 /* Send the command for reading device ID */ 17 chip->cmdfunc(mtd, NAND_CMD_READID, 0x00, -1); 18 19 /* Read manufacturer and device IDs */ 20 *maf_id = chip->read_byte(mtd); /* 調用read_byte函數讀取廠家ID */ 21 *dev_id = chip->read_byte(mtd); /* 設備ID */ 22 23 /* Try again to make sure, as some systems the bus-hold or other 24 * interface concerns can cause random data which looks like a 25 * possibly credible NAND flash to appear. If the two results do 26 * not match, ignore the device completely. 27 */ 28 29 chip->cmdfunc(mtd, NAND_CMD_READID, 0x00, -1); 30 31 for (i = 0; i < 2; i++) 32 id_data[i] = chip->read_byte(mtd); 33 34 /* 打印參數信息 */ 35 if (id_data[0] != *maf_id || id_data[1] != *dev_id) { 36 printk(KERN_INFO "%s: second ID read did not match " 37 "%02x,%02x against %02x,%02x\n", __func__, 38 *maf_id, *dev_id, id_data[0], id_data[1]); 39 return ERR_PTR(-ENODEV); 40 } 41 ... 42 /* 校驗產品ID */ 43 if (!type) 44 type = nand_flash_ids; 45 46 for (; type->name != NULL; type++) 47 if (*dev_id == type->id) 48 break; 49 ... 50 }
代碼中第46行可看出,nand_flash_ids[]數組是個全局變量,通過循環匹配設備ID,確定Nand Flash的大小、位數等規格。其定義如下:
struct nand_flash_dev nand_flash_ids[] = { #ifdef CONFIG_MTD_NAND_MUSEUM_IDS {"NAND 1MiB 5V 8-bit", 0x6e, 256, 1, 0x1000, 0}, {"NAND 2MiB 5V 8-bit", 0x64, 256, 2, 0x1000, 0}, {"NAND 4MiB 5V 8-bit", 0x6b, 512, 4, 0x2000, 0}, {"NAND 1MiB 3,3V 8-bit", 0xe8, 256, 1, 0x1000, 0}, {"NAND 1MiB 3,3V 8-bit", 0xec, 256, 1, 0x1000, 0}, {"NAND 2MiB 3,3V 8-bit", 0xea, 256, 2, 0x1000, 0}, {"NAND 4MiB 3,3V 8-bit", 0xd5, 512, 4, 0x2000, 0}, {"NAND 4MiB 3,3V 8-bit", 0xe3, 512, 4, 0x2000, 0}, {"NAND 4MiB 3,3V 8-bit", 0xe5, 512, 4, 0x2000, 0}, {"NAND 8MiB 3,3V 8-bit", 0xd6, 512, 8, 0x2000, 0}, ... };
2. 我們如果不傳入命令行參數,parse_mtd_partitions()函數沒有作用,就需要自己構建分區表。mtd_device_register()函數傳入參數中應有分區表
int mtd_device_register(struct mtd_info *master, const struct mtd_partition *parts, int nr_parts) { return parts ? add_mtd_partitions(master, parts, nr_parts) : add_mtd_device(master); }
參數struct mtd_partition *parts即為分區表,其定義和示例如下:
/* 定義 */ struct mtd_partition { char *name; /* 分區名,如bootloader、params、kernel和root */ uint64_t size; /* 分區大小*/ uint64_t offset; /* 分區所在的偏移值 */ uint32_t mask_flags; /* 掩碼標識 */ struct nand_ecclayout *ecclayout; /* oob布局 */ }; /* 示例 */ static const struct mtd_partition partition_info[] = { { .name = "NAND FS 0", .offset = 0, .size = 8 * 1024 * 1024 }, { .name = "NAND FS 1", .offset = MTDPART_OFS_APPEND, /* 接着上一個 */ .size = MTDPART_SIZ_FULL /* 余下的所有空間 */ } };
簡單分析完了Nand Flash設備驅動,接下來我們來分析MTD子系統框架。
二、MTD子系統框架分析
在開發板中ls /dev/mtd*,我們可以看到MTD設備既有塊設備也有字符設備,塊設備(mtdblockx)針對文件系統,字符設備(mtdx)針對格式化等操作。
在上一節中,我們知道了mtd_info是nand_chip等底層Flash結構體的抽象,因此我們可以得到如下框架。
在上一節中,我們知道了mtd_device_register()函數最終調用add_mtd_device(master)函數添加MTD設備。根據上圖可以確定添加的是MTD原始設備。
add_mtd_device()函數定義如下:
1 int add_mtd_device(struct mtd_info *mtd) 2 { 3 struct mtd_notifier *not; /* MTD通知結構體,用於添加刪除mtd_info */ 4 int i, error; 5 ... 6 /* 配置mtd_info */ 7 mtd->index = i; 8 mtd->usecount = 0; 9 ... 10 mtd->erasesize_mask = (1 << mtd->erasesize_shift) - 1; 11 mtd->writesize_mask = (1 << mtd->writesize_shift) - 1; 12 ... 13 mtd->dev.type = &mtd_devtype; 14 mtd->dev.class = &mtd_class; 15 mtd->dev.devt = MTD_DEVT(i); 16 dev_set_name(&mtd->dev, "mtd%d", i); 17 dev_set_drvdata(&mtd->dev, mtd); 18 device_register(&mtd->dev); /* 創建device */ 19 ... 20 list_for_each_entry(not, &mtd_notifiers, list) 21 not->add(mtd); /* 調用mtd_notifier的add函數 */ 22 23 return 0; 24 }
其中,struct mtd_notifier定義如下:
struct mtd_notifier { void (*add)(struct mtd_info *mtd); void (*remove)(struct mtd_info *mtd); struct list_head list; };
struct mtd_notifier的注冊注銷函數定義如下:
/* 注冊函數 */ void register_mtd_user (struct mtd_notifier *new) { struct mtd_info *mtd; mutex_lock(&mtd_table_mutex); list_add(&new->list, &mtd_notifiers); __module_get(THIS_MODULE); mtd_for_each_device(mtd) new->add(mtd); mutex_unlock(&mtd_table_mutex); } /* 注銷函數 */ int unregister_mtd_user (struct mtd_notifier *old) { struct mtd_info *mtd; mutex_lock(&mtd_table_mutex); module_put(THIS_MODULE); mtd_for_each_device(mtd) old->remove(mtd); list_del(&old->list); mutex_unlock(&mtd_table_mutex); return 0; }
至此,各個結構體層次已經出來了,如下圖所示:
既然mtd_info是nand_chip等底層Flash結構體的抽象,那么用於表示Nor Flash的結構體是什么呢,第三節我們就來分析這個問題。
三、Nor Flash驅動分析
進入drivers/mtd/目錄中,Nor Flash和Nand Flash一樣,必然會有自己的目錄。根據排除法確定Nor Flash設備驅動文件所在的目錄為maps。
讀者可在此目錄下任意選擇一個單板驅動文件進行分析,我選擇的是dc21285.c文件。
首先來看它的入口函數:
1 static int __init init_dc21285(void) 2 { 3 int nrparts; 4 5 /* Determine bankwidth */ 6 switch (*CSR_SA110_CNTL & (3<<14)) { 7 ... 8 case SA110_CNTL_ROMWIDTH_32: 9 dc21285_map.bankwidth = 4; 10 dc21285_map.read = dc21285_read32; 11 dc21285_map.write = dc21285_write32; 12 dc21285_map.copy_to = dc21285_copy_to_32; 13 break; 14 ... 15 } 16 ... 17 /* 根據Nor Flash物理地址映射Nor Flash空間 */ 18 dc21285_map.virt = ioremap(DC21285_FLASH, 16*1024*1024); /* DC21285_FLASH為物理起始地址,16*1024*1024為FLASH大小 */ 19 ... 20 /* NOR有兩種規范 21 * 1. jedec:內核中定義有jedec_table結構體,里面存放有NOR Flash的大小、名字等信息。如果內核中沒有定義我們使用的NOR Flash,就必須手動添加 22 * 2. cfi:common flash interface,是新的NOR Flash規范,Flash本身包含有屬性,和Nand Flash相同 23 */ 24 if (machine_is_ebsa285()) { 25 dc21285_mtd = do_map_probe("cfi_probe", &dc21285_map); 26 } else { 27 dc21285_mtd = do_map_probe("jedec_probe", &dc21285_map); 28 } 29 30 if (!dc21285_mtd) { 31 iounmap(dc21285_map.virt); 32 return -ENXIO; 33 } 34 35 dc21285_mtd->owner = THIS_MODULE; 36 37 nrparts = parse_mtd_partitions(dc21285_mtd, probes, &dc21285_parts, 0); 38 mtd_device_register(dc21285_mtd, dc21285_parts, nrparts); 39 ... 40 return 0; 41 }
通過此函數,我們可以知道表示Nor Flash的結構體為struct map_info dc21285_map。
init()函數所做的有以下幾點:
1. 申請mtd_info結構體內存空間
2. 申請並配置map_info結構體
3. 映射與map_info->phys物理地址對應的map_info->virt虛擬內存,其大小為Flash真實大小,它放在map_info->size
4. 使用do_map_probe()設置map_info結構體
5. 使用parse_mtd_partitions()解析命令行中設置的分區。若命令行中沒有設置mtdparts返回0;若設置了並且解析沒問題,那么返回分區的個數,否則返回小於0的數
6. 使用mtd_device_register()注冊Nor Flash分區
do_map_probe()函數調用關系如下:
1 dc21285_mtd = do_map_probe("cfi_probe", &dc21285_map); 2 -> struct mtd_chip_driver *drv = get_mtd_chip_driver(name); 3 -> list_for_each(pos, &chip_drvs_list) 4 if (!strcmp(this->name, name)) /* 匹配驅動 */ 5 return this; /* 返回的是mtd_chip_driver *drv */ 6 -> ret = drv->probe(map); /* 調用驅動的probe()函數返回mtd_info */
四、Nand Flash驅動和Nor Flash驅動編寫
Nand Flash驅動源代碼:

1 #include <linux/module.h> 2 #include <linux/types.h> 3 #include <linux/init.h> 4 #include <linux/kernel.h> 5 #include <linux/string.h> 6 #include <linux/ioport.h> 7 #include <linux/platform_device.h> 8 #include <linux/delay.h> 9 #include <linux/err.h> 10 #include <linux/slab.h> 11 #include <linux/clk.h> 12 #include <linux/mtd/mtd.h> 13 #include <linux/mtd/nand.h> 14 #include <linux/mtd/nand_ecc.h> 15 #include <linux/mtd/partitions.h> 16 17 #include <asm/io.h> 18 //#include <asm/arch/regs-nand.h> 19 //#include <asm/arch/nand.h> 20 21 struct itop_nand_regs 22 { 23 unsigned long nfconf ; 24 unsigned long nfcont ; 25 unsigned long nfcmd ; 26 unsigned long nfaddr ; 27 unsigned long nfdata ; 28 unsigned long nfeccd0 ; 29 unsigned long nfeccd1 ; 30 unsigned long nfeccd ; 31 unsigned long nfstat ; 32 unsigned long nfestat0; 33 unsigned long nfestat1; 34 unsigned long nfmecc0 ; 35 unsigned long nfmecc1 ; 36 unsigned long nfsecc ; 37 unsigned long nfsblk ; 38 unsigned long nfeblk ; 39 }; 40 41 static struct nand_chip *itop_nand; 42 static struct mtd_info *itop_mtd; 43 static struct itop_nand_regs *nand_regs; 44 static struct clk *clk; 45 46 static struct mtd_partition itop_nand_part[] = { 47 [0] = { 48 .name = "bootloader", 49 .size = 0x00080000, 50 .offset = 0, 51 }, 52 [1] = { 53 .name = "params", 54 .offset = MTDPART_OFS_APPEND, 55 .size = 0x00020000, 56 }, 57 [2] = { 58 .name = "kernel", 59 .offset = MTDPART_OFS_APPEND, 60 .size = 0x00400000, 61 }, 62 [3] = { 63 .name = "root", 64 .offset = MTDPART_OFS_APPEND, 65 .size = MTDPART_SIZ_FULL, 66 } 67 }; 68 69 static void itop_nand_select_chip(struct mtd_info *mtd, int chipnr) 70 { 71 if (chipnr == -1) 72 { 73 /* 取消選中: NFCONT[1]設為1 */ 74 nand_regs->nfcont |= (1 << 1); 75 } 76 else 77 { 78 /* 選中: NFCONT[1]設為0 */ 79 nand_regs->nfcont &= ~(1 << 1); 80 } 81 } 82 83 static void itop_nand_cmd_ctrl(struct mtd_info *mtd, int cmd, unsigned int ctrl) 84 { 85 if (ctrl & NAND_CLE) 86 { 87 /* MFDCMMD = cmd */ 88 nand_regs->nfcmd = cmd; 89 } 90 else 91 { 92 /* NFDADDR = cmd */ 93 nand_regs->nfaddr = cmd; 94 } 95 } 96 97 static int itop_nand_device_ready(struct mtd_info *mtd) 98 { 99 /* 判斷NFSTAT[0]: 1 表示ready */ 100 return (nand_regs->nfstat & (1 << 0)); 101 } 102 103 static int itop_nand_init(void) 104 { 105 /* 1. 分配nand_chip */ 106 itop_nand = kzalloc(sizeof(struct nand_chip), GFP_KERNEL); 107 108 /* 2. 設置nand_chip */ 109 itop_mtd = kzalloc(sizeof(struct mtd_info), GFP_KERNEL); 110 itop_mtd->owner = THIS_MODULE; 111 itop_mtd->priv = itop_nand; 112 113 nand_regs = ioremap(0x4E000000, sizeof(struct itop_nand_regs)); 114 115 itop_nand->select_chip = itop_nand_select_chip; 116 itop_nand->cmd_ctrl = itop_nand_cmd_ctrl; /* 命令最后調用它 */ 117 itop_nand->IO_ADDR_R = &nand_regs->nfdata; /* 讀數據最后調用它 */ 118 itop_nand->IO_ADDR_W = &nand_regs->nfdata; /* 寫數據 */ 119 itop_nand->dev_ready = itop_nand_device_ready; /* 狀態位 */ 120 itop_nand->ecc.mode = NAND_ECC_SOFT; /* 開啟ECC */ 121 122 /* 3. 硬件相關的操作 */ 123 /* 注意使能時鍾 */ 124 clk = clk_get(NULL, "nand"); 125 clk_enable(clk); 126 nand_regs->nfconf = ((0 << 12) | (1 << 8) | (0 << 4)); 127 nand_regs->nfcont = ((1 << 1) | (1 << 0)); 128 129 /* 4. nand_scan() */ 130 nand_scan(itop_mtd, 1); 131 132 /* 5. mtd_device_register() */ 133 mtd_device_register(itop_mtd, itop_nand_part, 4); 134 135 return 0; 136 } 137 138 static void itop_nand_exit(void) 139 { 140 kfree(itop_nand); 141 kfree(itop_mtd); 142 iounmap(nand_regs); 143 } 144 145 module_init(itop_nand_init); 146 module_exit(itop_nand_exit); 147 148 MODULE_LICENSE("GPL");
Nor Flash驅動源代碼(有可能部分開發板中沒有Nor Flash):

1 #include <linux/module.h> 2 #include <linux/types.h> 3 #include <linux/init.h> 4 #include <linux/kernel.h> 5 #include <linux/string.h> 6 #include <linux/ioport.h> 7 #include <linux/platform_device.h> 8 #include <linux/delay.h> 9 #include <linux/err.h> 10 #include <linux/slab.h> 11 #include <linux/clk.h> 12 #include <linux/mtd/mtd.h> 13 #include <linux/mtd/map.h> 14 #include <linux/mtd/partitions.h> 15 16 #include <asm/io.h> 17 18 static struct map_info *nor_map; 19 static struct mtd_info *nor_mtd; 20 21 static struct mtd_partition nor_part[] = { 22 [0] = { 23 .name = "bootloader", 24 .size = 0x00080000, 25 .offset = 0, 26 }, 27 28 [1] = { 29 /* 沒有那么大內存 */ 30 #if 0 31 .name = "params", 32 .offset = MTDPART_OFS_APPEND, 33 .size = 0x00020000, 34 }, 35 [2] = { 36 .name = "kernel", 37 .offset = MTDPART_OFS_APPEND, 38 .size = 0x00400000, 39 }, 40 [3] = { 41 #endif 42 .name = "root", 43 .offset = MTDPART_OFS_APPEND, 44 .size = MTDPART_SIZ_FULL, 45 } 46 }; 47 48 static int itop_nor_init(void) 49 { 50 /* 分配空間 */ 51 nor_map = kzalloc(sizeof(struct map_info), GFP_KERNEL); 52 nor_mtd = kzalloc(sizeof(struct mtd_info), GFP_KERNEL); 53 54 nor_map->bankwidth = 2; 55 nor_map->name = "itop_nor"; 56 nor_map->phys = 0; 57 nor_map->size = 0x100000; 58 nor_map->virt = ioremap(nor_map->phys, nor_map->size); 59 60 nor_mtd = do_map_probe("cfi_probe", nor_map); 61 62 if (!nor_mtd) 63 nor_mtd = do_map_probe("jedec_probe", nor_map); 64 65 if (!nor_mtd) { 66 iounmap(nor_map->virt); 67 kfree(nor_mtd); 68 kfree(nor_map); 69 return -ENXIO; 70 } 71 72 nor_mtd->owner = THIS_MODULE; 73 74 /* 2表示分區個數 */ 75 mtd_device_register(nor_mtd, nor_part, 2); 76 77 return 0; 78 } 79 80 static void itop_nor_exit(void) 81 { 82 if (nor_mtd) { 83 kfree(nor_map); 84 kfree(nor_mtd); 85 iounmap(nor_map->virt); 86 } 87 } 88 89 module_init(itop_nor_init); 90 module_exit(itop_nor_exit); 91 92 MODULE_LICENSE("GPL");
Makefile:

1 KERN_DIR = /work/itop4412/tools/linux-3.5 2 3 all: 4 make -C $(KERN_DIR) M=`pwd` modules 5 6 clean: 7 make -C $(KERN_DIR) M=`pwd` modules clean 8 rm -rf modules.order 9 10 obj-m += nand.o nor.o
下一章 19、eMMC驅動框架分析