1. 程式人生 > >嵌入式Linux——nand flash 驅動(三):原始碼分析

嵌入式Linux——nand flash 驅動(三):原始碼分析

再次宣告:本文是看過一些文章後寫的,如果與你的文章有相同的地方,敬請告知,如果對你有幫助,是我的榮幸。

接下來的這篇文章我們將要分析一下nand flash在S3C2440中的驅動函式。下面我們以一張圖來引入:

        從上圖可以看出,MTD裝置層與原始裝置層打交道。通過分析原始碼我們可以知道當上層要求對FLASH進行讀寫時,它會向裝置層發出請求,裝置層的讀寫函式會呼叫原始裝置層中的讀寫函式,即mtd_info結構體(mtd原始裝置層中描述裝置的專用結構體)中的讀寫函式,而mtd_info中的函式會呼叫nand_chip(nand硬體驅動層中描述裝置的結構體,其中包含了針對特定裝置的基本引數和裝置操作函式)中的讀寫函式。所以當我們寫一個flash硬體驅動程式時,有以下步驟:
1. 如果FLASH要分割槽,則定義mtd_partition陣列,將FLASH分割槽資訊記錄其中。
2. 在模組載入時為每一個chip(主分割槽)分配mtd_info和nand_chip的記憶體,根據目標板nand 控制器的特殊情況初始化nand_chip中的實現對FLASH操作的成員函式,如hwcontrol()、dev_ready()、read_byte()、write_byte()等。填充mtd_info,並將其priv成員指向nand_chip。
3. 以mtd_info為引數呼叫nand_scan()函式探測NAND FLASH的存在。nand_scan()函式會從FLASH晶片中讀取其引數,填充相應nand_chip成員。

4. 如果要分割槽,則以mtd_info和mtd_partition為引數呼叫add_mtd_partions(),新增分割槽資訊。在這個函式裡面會為每一個分割槽(不包含主分割槽)分配一個mtd_info結構體,填充,並註冊。

而從上邊的描述中我們知道,如果自己編寫一個nandflash驅動,只需要填充這三個結構體:

mtd_info     nand_chip     mtd_partition

並實現對物理裝置的控制,上層的驅動控制已由mtd做好了,不需要關心。

先說mtd_info:

mtd層用一個數組struct mtd_info *mtd_table[MAX_MTD_DEVICES]儲存系統中所有的裝置,mtd裝置利用struct mtd_info 這個結構來描述,該結構中描述了儲存裝置的基本資訊和具體操作所需要的核心函式,mtd系統的那個機制主要就是圍繞這個結構來實現的。結構體在include/linux/mtd/mtd.h

中定義:

struct mtd_info {
u_char type;            //MTD 裝置型別
u_int32_t flags;        //MTD裝置屬性標誌
u_int32_t size;         //標示了這個mtd裝置的大小
u_int32_t erasesize;    //MTD裝置的擦除單元大小,對於NandFlash來說就是Block的大小
u_int32_t oobblock;      //oob區在頁內的位置,對於2K位元組一頁的nand來說是2K
u_int32_t oobsize;      //oob區的大小,對於2K位元組一頁的nand來說是64
u_int32_t ecctype;      //ecc校驗型別
u_int32_t eccsize;      //ecc的大小
 
char *name;             //裝置的名字
int index;              //裝置在MTD列表中的位置
 
struct nand_oobinfo oobinfo; //oob區的資訊,包括是否使用ecc,ecc的大小
 
//以下是關於mtd的一些讀寫函式,將在nand_base中的nand_scan中過載
int (*erase)
int (*read)
int (*write)
int (*read_ecc)
int (*write_ecc)
int (*read_oob)
int (*read_oob)
 
void *priv;//裝置私有資料指標,對於NandFlash來說指nand晶片的結構:nand_chip
 
}

接下來我們看nand_chip結構,在include/linux/mtd/nand.h中定義:

struct nand_chip {
    void  __iomem    *IO_ADDR_R;    //讀8位I/O地址
    void  __iomem    *IO_ADDR_W;   //寫8位I/O地址
    uint8_t        (*read_byte)(struct mtd_info *mtd);//從晶片讀一個位元組
    u16        (*read_word)(struct mtd_info *mtd);//從晶片讀一個字
    void        (*write_buf)(struct mtd_info *mtd, const uint8_t *buf, int len);//將緩衝區的資料寫入晶片
    void        (*read_buf)(struct mtd_info *mtd, uint8_t *buf, int len);//將晶片中的資料獨到緩衝區中

    int        (*verify_buf)(struct mtd_info *mtd, const uint8_t *buf, int len); //驗證晶片和寫入緩衝區中的資料

   int         (*block_bad)(struct mtd_info *mtd, loff_t ofs, int getchip);//檢查是否壞塊
    int        (*block_markbad)(struct mtd_info *mtd, loff_t ofs);//標記壞塊

    void        (*select_chip)(struct mtd_info *mtd, int chip);          //實現選中晶片
    void        (*cmd_ctrl)(struct mtd_info *mtd, int dat,               
                    unsigned int ctrl);//控制ALE/CLE/nCE,也用於寫命令和地址
    int        (*dev_ready)(struct mtd_info *mtd);//裝置就緒
    void        (*cmdfunc)(struct mtd_info *mtd, unsigned command, int column, int page_addr); //實現命令傳送

    int        (*waitfunc)(struct mtd_info *mtd, struct nand_chip *this);
    void        (*erase_cmd)(struct mtd_info *mtd, int page);//擦除命令的處理
    int        (*scan_bbt)(struct mtd_info *mtd);//掃描壞塊
    int        (*errstat)(struct mtd_info *mtd, struct nand_chip *this, int state, int status, int page);

    int        (*write_page)(struct mtd_info *mtd, struct nand_chip *chip,
                      const uint8_t *buf, int page, int cached, int raw);//寫一頁

    int        chip_delay;//有板決定的延遲時間
   unsigned int    options;//與具體的NAND晶片相關的一些選項,如NAND_NO_AUTOINCR,NAND_BUSWIDTH_16等,至於這些選項具體表示什麼含義,可以參考<linux/mtd/nand.h>,那裡有較為詳細的說明;

    int        page_shift;//用位表示的NAND晶片的page大小,如某片NAND晶片的一個page有512個位元組,那麼page_shift就是9;
    int        phys_erase_shift;//用位表示的NAND晶片的每次可擦除的大小,如某片NAND晶片每次可擦除128K位元組(通常就是一個block的大小)
    int        bbt_erase_shift;//用位表示的bad block table的大小,通常一個bbt佔用一個block,所以bbt_erase_shift通常與phys_erase_shift相等;
    int        chip_shift;用位表示的NAND晶片的容量;
    int        numchips;表示系統中有多少片NAND晶片;
    uint64_t    chipsize;//NAND晶片的大小;
    int        pagemask;//計算page number時的掩碼,總是等於chipsize/page大小- 1;
    int        pagebuf;用來儲存當前讀取的NAND晶片的page number,這樣一來,下次讀取的資料若還是屬於同一個page,就不必再從NAND晶片讀取了,而是從data_buf中直接得到;
    ……………………
    void        *priv;
};

有了前面的介紹,下面我們就開始進行對S3C2440.c 的分析。同樣分析一個驅動程式首先要從他的入口函式開始分析:

static int __init s3c2410_nand_init(void)
{
	printk("S3C24XX NAND Driver, (c) 2004 Simtec Electronics\n");

	platform_driver_register(&s3c2412_nand_driver);
	platform_driver_register(&s3c2440_nand_driver);       /* 平臺裝置驅動模型中對於nand平臺驅動的註冊 */
	return platform_driver_register(&s3c2410_nand_driver);
}

上面就是平臺裝置驅動模型中對於nand平臺驅動的註冊。通過註冊,平臺 會將這個驅動同他所擁有的裝置進行意義比較,如果

.driver= { .name= "s3c2412-nand"}中有名字為s3c2412-nand的裝置,將會呼叫本驅動的probe函式。而通過檢視我們知道有這樣的裝置,他定義在晶片框架下的裝置檔案arch/arm/plat-s3c24xx/devs.c

/* NAND Controller */
static struct resource s3c_nand_resource[] = {
	[0] = {
		.start = S3C2410_PA_NAND,
		.end   = S3C2410_PA_NAND + S3C24XX_SZ_NAND - 1,
		.flags = IORESOURCE_MEM,
	}
};

struct platform_device s3c_device_nand = {
	.name		  = "s3c2410-nand",
	.id		  = -1,
	.num_resources	  = ARRAY_SIZE(s3c_nand_resource),
	.resource	  = s3c_nand_resource,
};

從上面我們可以看出,他通過resource結構體給了我們用於設定nand 暫存器的首尾地址,同時也通過platform_device設定了名字。而當我們的名字匹配上後就進入了probe函式:

static int s3c24xx_nand_probe(struct platform_device *pdev,
			      enum s3c_cpu_type cpu_type)
{
	struct s3c2410_platform_nand *plat = to_nand_plat(pdev);
	struct s3c2410_nand_info *info;
	struct s3c2410_nand_mtd *nmtd;   /* 這個結構體中就包含了mtd_info和nand_chip結構體 */
	struct s3c2410_nand_set *sets;   /* 在sets中有與分割槽相關的設定 */
	struct resource *res;
	int size;
	int nr_sets;
	int setno;
	
	info = kmalloc(sizeof(*info), GFP_KERNEL);  

	memzero(info, sizeof(*info));
	platform_set_drvdata(pdev, info);

	spin_lock_init(&info->controller.lock);
	init_waitqueue_head(&info->controller.wq);

	/* get the clock source and enable it */
	info->clk = clk_get(&pdev->dev, "nand");      /* 在CLKCON暫存器上獲得nand的總開關 */
	clk_enable(info->clk);                        /* 使能總開關,開啟nand 服務 */

	/* allocate and map the resource */

	/* currently we assume we have the one resource */
	res  = pdev->resource;
	size = res->end - res->start + 1;

	info->area = request_mem_region(res->start, size, pdev->name);

	info->device     = &pdev->dev;
	info->platform   = plat;
	info->regs       = ioremap(res->start, size);   /* 為nand 的暫存器重對映,而regs就是他們虛擬地址的首地址 */
	info->cpu_type   = cpu_type;

	/* initialise the hardware */
	err = s3c2410_nand_inithw(info, pdev);      /* 初始化硬體,其實就是設定tacls,twrph0,twrph1的值 */

	sets = (plat != NULL) ? plat->sets : NULL;
	nr_sets = (plat != NULL) ? plat->nr_sets : 1; /*sets為一個數據結構,裡面包含有mtd_partition,即為分割槽資訊。nr_sets為分割槽的個數*/
	info->mtd_count = nr_sets;

	/* allocate our information */
	size = nr_sets * sizeof(*info->mtds);
	info->mtds = kmalloc(size, GFP_KERNEL);
	memzero(info->mtds, size);

	/* initialise all possible chips */
	for (setno = 0; setno < nr_sets; setno++, nmtd++) {
		s3c2410_nand_init_chip(info, nmtd, sets);      /* 初始化nand_chip中的各個引數 */

		nmtd->scan_res = nand_scan(&nmtd->mtd, (sets) ? sets->nr_chips : 1);  /* nand掃描函式 */

		if (nmtd->scan_res == 0) {
			s3c2410_nand_add_partition(info, nmtd, sets);                /* 如果有分割槽資訊,新增分割槽 */
		}
	}
}sets為一個數據結構,裡面包含有mtd_partition,即為分割槽資訊。nr_sets為分割槽的個數*/
	info->mtd_count = nr_sets;

	/* allocate our information */
	size = nr_sets * sizeof(*info->mtds);
	info->mtds = kmalloc(size, GFP_KERNEL);
	memzero(info->mtds, size);

	/* initialise all possible chips */
	for (setno = 0; setno < nr_sets; setno++, nmtd++) {
		s3c2410_nand_init_chip(info, nmtd, sets);      /* 初始化nand_chip中的各個引數 */

		nmtd->scan_res = nand_scan(&nmtd->mtd, (sets) ? sets->nr_chips : 1);  /* nand掃描函式 */

		if (nmtd->scan_res == 0) {
			s3c2410_nand_add_partition(info, nmtd, sets);                /* 如果有分割槽資訊,新增分割槽 */
		}
	}
}

通過對上面程式碼的檢視我們知道有四個函式在probe中是非常重要,他們是:

s3c2410_nand_inithw(info,pdev);              //設定TACLST WRPH0 TWRPH1

s3c2410_nand_init_chip(info, nmtd, sets); //初始化nand_chip

nand_scan                                                  // 完成對flash的探測及mtd_info讀寫函式的賦值

s3c2410_nand_add_partition(info,nmtd, sets); //新增分割槽

而後兩個函式我們在前一篇文章中已經說過,現在我們主要說前兩個函式,首先分析s3c2410_nand_inithw(info,pdev):設定TACLST WRPH0 TWRPH1函式,通過此函式我們可以設定TACLST WRPH0 TWRPH1三個值,也就是設定了:

TACLS:發出ALE/CLE之後多長時間才發出nWE訊號

 TWRPH0:nWE的脈衝寬度,HCLK*(TWRPH0+1)

 TWRPH1:nWE變為高電平後多長時間ALE/CLE能變為低電平,HCLK*(TWRPH1+1),

下面我們進入函式分析:

/* controller setup */
static int s3c2410_nand_inithw(struct s3c2410_nand_info *info,
			       struct platform_device *pdev)
{
	struct s3c2410_platform_nand *plat = to_nand_plat(pdev);
	unsigned long clkrate = clk_get_rate(info->clk);
	int tacls_max = (info->cpu_type == TYPE_S3C2412) ? 8 : 4;
	int tacls, twrph0, twrph1;
	unsigned long cfg = 0;

	/* calculate the timing information for the controller */
	clkrate /= 1000;	/* turn clock into kHz for ease of use */

	if (plat != NULL) {
		tacls = s3c_nand_calc_rate(plat->tacls, clkrate, tacls_max);
		twrph0 = s3c_nand_calc_rate(plat->twrph0, clkrate, 8);
		twrph1 = s3c_nand_calc_rate(plat->twrph1, clkrate, 8);
	} else {
		/* default timings */
		tacls = tacls_max;
		twrph0 = 8;
		twrph1 = 8;
	}
 	switch (info->cpu_type) {
 	case TYPE_S3C2440:
		cfg = S3C2440_NFCONF_TACLS(tacls - 1);
		cfg |= S3C2440_NFCONF_TWRPH0(twrph0 - 1);
		cfg |= S3C2440_NFCONF_TWRPH1(twrph1 - 1);

		/* enable the controller and de-assert nFCE */

		writel(S3C2440_NFCONT_ENABLE, info->regs + S3C2440_NFCONT);
	}

	dev_dbg(info->device, "NF_CONF is 0x%lx\n", cfg);

	writel(cfg, info->regs + S3C2410_NFCONF);
	return 0;
}

而接下來我們就要分析:s3c2410_nand_init_chip函式來確定他是怎樣設定nand_chip結構體重的各個指標函式。

static void s3c2410_nand_init_chip(struct s3c2410_nand_info *info,
				   struct s3c2410_nand_mtd *nmtd,
				   struct s3c2410_nand_set *set)
{
	struct nand_chip *chip = &nmtd->chip;
	void __iomem *regs = info->regs;
	chip->write_buf    = s3c2410_nand_write_buf;     /* 設定寫緩衝函式 */
	chip->read_buf     = s3c2410_nand_read_buf;      /* 設定讀快取函式 */
	chip->select_chip  = s3c2410_nand_select_chip;   /* 設定片選函式 */
	chip->chip_delay   = 50;                         /* 設定延時時間 */
	chip->priv	   = nmtd;                       /* 設定私有資料 */
	chip->options	   = 0;      
	chip->controller   = &info->controller;

	switch (info->cpu_type) {                       /* 這裡是選擇CPU的型號,為了簡潔我只留下了本驅動所用的S3C2440 */

	case TYPE_S3C2440:
		chip->IO_ADDR_W = regs + S3C2440_NFDATA;       /* 設定寫緩衝地址 */
		info->sel_reg   = regs + S3C2440_NFCONT;      
		info->sel_bit	= S3C2440_NFCONT_nFCE;         /* 設定片選地址 */
		chip->cmd_ctrl  = s3c2440_nand_hwcontrol;      /* 設定寫命令函式 */  
		chip->dev_ready = s3c2440_nand_devready;       /* 設定等待就緒函式 */
		break;
  	}
	chip->IO_ADDR_R = chip->IO_ADDR_W;                     /* 設定讀快取地址 */

	nmtd->info	   = info;
	nmtd->mtd.priv	   = chip;                             /* 將nand_chip放入到mtd_info的私有資料中 */
	nmtd->mtd.owner    = THIS_MODULE;                      /* 這個是很重要的 */
	nmtd->set	   = set;

	chip->ecc.mode	    = NAND_ECC_SOFT;                  /* 設定ecc模式為軟體檢測模式 */
}

上面就是對nand_chip結構體的設定了,而以前我們分析知道,當我們沒有去寫nand_chip結構體下的相應的函式時,nand_scan會為我們設定預設的函式,但是這些預設的函式不一定是適合本晶片的。所以我們要自己寫這些函式。

首先我們要寫的是s3c2410_nand_write_buf函式

static void s3c2410_nand_write_buf(struct mtd_info *mtd, const u_char *buf, int len)
{
	struct nand_chip *this = mtd->priv;
	writesb(this->IO_ADDR_W, buf, len); /* 將長度為len的buf值寫到地址IO_ADDR_W中 */
}

上面程式碼就是要將buf的值放入到IO_ADDR_W中,而IO_ADDR_W是寫地址的虛擬地址,他會上一個程式碼中已經設定了。他為

nand資料暫存器NFDATA 的虛擬地址

s3c2410_nand_read_buf函式:

static void s3c2410_nand_read_buf(struct mtd_info *mtd, u_char *buf, int len)
{
	struct nand_chip *this = mtd->priv;
	readsb(this->IO_ADDR_R, buf, len); /* 將長度為len的buf值讀到地址IO_ADDR_W中 */
}

與s3c2410_nand_write_buf函式相似,只是將讀改為了寫。他同樣為nand資料暫存器NFDATA 的虛擬地址

s3c2410_nand_select_chip函式:

static void s3c2410_nand_select_chip(struct mtd_info *mtd, int chip)
{
	struct s3c2410_nand_info *info;
	struct s3c2410_nand_mtd *nmtd;
	struct nand_chip *this = mtd->priv;
	unsigned long cur;

	nmtd = this->priv;
	info = nmtd->info;

	if (chip != -1 && allow_clk_stop(info))
		clk_enable(info->clk);

	cur = readl(info->sel_reg);

	if (chip == -1) {
		cur |= info->sel_bit;
	} else {
		if (nmtd->set != NULL && chip > nmtd->set->nr_chips) {
			dev_err(info->device, "invalid chip %d\n", chip);
			return;
		}

		if (info->platform != NULL) {
			if (info->platform->select_chip != NULL)
				(info->platform->select_chip) (nmtd->set, chip);
		}

		cur &= ~info->sel_bit;
	}

	writel(cur, info->sel_reg);

	if (chip == -1 && allow_clk_stop(info))
		clk_disable(info->clk);
}

該函式最主要的作用就是設定片選訊號。當需要選中時,設定nand控制暫存器NFCONT[1]=0  ,而當需要取消時,設定nand控制暫存器NFCONT[1]=1

而接下來就是寫命令/地址函式s3c2410_nand_hwcontrol

static void s3c2410_nand_hwcontrol(struct mtd_info *mtd, int cmd,
				   unsigned int ctrl)
{
	struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);

	if (cmd == NAND_CMD_NONE)
		return;

	if (ctrl & NAND_CLE)      /* 判斷當為命令時, */
		writeb(cmd, info->regs + S3C2410_NFCMD);      /* 將命令值寫入nand的命令暫存器MFCMD中 */
	else                      /* 否則為寫地址 */
		writeb(cmd, info->regs + S3C2410_NFADDR);     /* 將地址值寫入nand的地址暫存器NFADDR中 */    
}

上函式就是通過判斷引數ctrl的值來判斷是寫命令還是寫地址,並將命令或地址寫入相應的暫存器的虛擬地址中。

而我們最後要自己寫的函式就是s3c2440_nand_devready等待就緒函式:

static int s3c2440_nand_devready(struct mtd_info *mtd)
{
	struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd);
	return readb(info->regs + S3C2440_NFSTAT) & S3C2440_NFSTAT_READY;
}

其實他就是通過返回nand狀態暫存器中bit[0]來確定nand所處的狀態。

而由於我們的驅動程式選擇的是軟體檢測ECC所以不用去完成硬體ECC所要寫的眾多函式而只用下面一句就可以完成了:

chip->ecc.mode	    = NAND_ECC_SOFT;

而如果你選擇了硬體檢測ECC將要完成以下函式的編寫:

            if (hardware_ecc) {
		chip->ecc.calculate = s3c2410_nand_calculate_ecc;
		chip->ecc.correct   = s3c2410_nand_correct_data;
		chip->ecc.mode	    = NAND_ECC_HW;
		chip->ecc.size	    = 512;
		chip->ecc.bytes	    = 3;
		chip->ecc.layout    = &nand_hw_eccoob;

		switch (info->cpu_type) {
		case TYPE_S3C2410:
			chip->ecc.hwctl	    = s3c2410_nand_enable_hwecc;
			chip->ecc.calculate = s3c2410_nand_calculate_ecc;
			break;

		case TYPE_S3C2412:
  			chip->ecc.hwctl     = s3c2412_nand_enable_hwecc;
  			chip->ecc.calculate = s3c2412_nand_calculate_ecc;
			break;

		case TYPE_S3C2440:
  			chip->ecc.hwctl     = s3c2440_nand_enable_hwecc;
  			chip->ecc.calculate = s3c2440_nand_calculate_ecc;
			break;

	    }

由於本驅動並未用硬體ECC所以上面程式要根據自己的nand flash去填寫,如擦除大小是以block為單位,你的block為多大你就寫多大。

完成上面所說的這些,如果你不用分割槽一個nand 的驅動程式就基本完成了,這時候你只用寫

add_mtd_device(&mtd->mtd)

而如果你還要分割槽,那麼你就要填寫分割槽表,然後呼叫 :

 add_mtd_partitions(&mtd->mtd, set->partitions, set->nr_partitions);

而分割槽資訊在(drivers\mtd\nand\nandsim.c)中:

        for (i = 0; i < parts_num; ++i) {
		unsigned long part = parts[i];
		if (!part || part > remains / ns->geom.secsz) {
			NS_ERR("bad partition size.\n");
			ret = -EINVAL;
			goto error;
		}
		ns->partitions[i].name   = get_partition_name(i);
		ns->partitions[i].offset = next_offset;
		ns->partitions[i].size   = part * ns->geom.secsz;
		next_offset += ns->partitions[i].size;
		remains -= ns->partitions[i].size;
	}
	ns->nbparts = parts_num;

而詳細的資訊在plat-s3c24xx/common-smdk.c:

static struct mtd_partition smdk_default_nand_part[] = {
    [0] = {
        name: "bootloader",
        size: 0x00100000,
        offset: 0x0,
    },
    [1] = {
        name: "kernel",
        size: 0x00300000,
        offset: 0x00100000,
    },
    [2] = {
        name: "root",
        size: 0x02800000,
        offset: 0x00400000,
    },
};

現在我們的驅動程式就寫完了。其實這樣分析下來我們會發現其實我們要去寫的東西並不多就三個結構體:

mtd_info    nand_chip     mtd_partition
而完成這三個結構體我們的程式就大體寫完了。所以,我會在下一個篇文章中再引入一個簡單的nand驅動程式的例子——AT91驅動,然後在寫一個自己nand的驅動程式。