來自http://jnds.yo2.cn/articles/%E5%9C%A8nand-flash%E4%B8%8A%E5%BB%BA%E7%AB%8Byaffs2%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89.html
經過了半個多月的努力,終於搞定nandflash的mtd驅動和上層的yaffs2文件系統。這半個多月來幾乎每天都要和挫敗感鬥爭,每天都要忍
受這個方面,那個方面的bug。想想自己這半個多月來,也算看不少資料,得到不少人的幫助,總算是有點心得。鑑於國內搞yaffs2文件系統方面的資料還
是很少,就把自己的心得拿出來與大家共享。
不說閒話了,先介紹一些背景資料 一. 閃存
我們常說的閃存其實只是一個籠統的稱呼,準確地說它是非易失隨機訪問存儲器(NVRAM)的俗稱,特點是斷電後數據不消失,因此可以作為外部存儲器使用。
而所謂的內存是揮發性存儲器,分為DRAM和SRAM兩大類,其中常說的內存主要指DRAM,也就是我們熟悉的DDR、DDR2、SDR、EDO等等。閃
存也有不同類型,其中主要分為NOR型和NAND型兩大類。
閃存的分類
NOR型與NAND型閃存的區別很大,打個比方說,NOR型閃存更像內存,有獨立的地址線和數據線,但價格比較貴,容量比較小;而NAND型更像硬
盤,地址線和數據線是共用的I/O線,類似硬盤的所有信息都通過一條硬盤線傳送一般,而且NAND型與NOR型閃存相比,成本要低一些,而容量大得多。因
此,NOR型閃存比較適合頻繁隨機讀寫的場合,通常用於存儲程序代碼並直接在閃存內運行,手機就是使用NOR型閃存的大戶,所以手機的「內存」容量通常不
大;NAND型閃存主要用來存儲資料,我們常用的閃存產品,如閃存盤、數碼存儲卡都是用NAND型閃存。
這裡我們還需要端正一個概念,那就是閃存的速度其實很有限,它本身操作速度、頻率就比內存低得多,而且NAND型閃存類似硬盤的操作方式效率也比內存
的直接訪問方式慢得多。因此,不要以為閃存盤的性能瓶頸是在接口,甚至想當然地認為閃存盤採用USB2.0接口之後會獲得巨大的性能提升。
前面提到NAND型閃存的操作方式效率低,這和它的架構設計和接口設計有關,它操作起來確實挺像硬盤(其實NAND型閃存在設計之初確實考慮了與硬盤
的兼容性),它的性能特點也很像硬盤:小數據塊操作速度很慢,而大數據塊速度就很快,這種差異遠比其他存儲介質大的多。這種性能特點非常值得我們留意。
NAND型閃存的技術特點
內存和NOR型閃存的基本存儲單元是bit,用戶可以隨機訪問任何一個bit的信息。而NAND型閃存的基本存儲單元是頁(Page)(可以看
到,NAND型閃存的頁就類似硬盤的扇區,硬盤的一個扇區也為512字節)。每一頁的有效容量是512字節的倍數。所謂的有效容量是指用於數據存儲的部
分,實際上還要加上16字節的校驗信息,因此我們可以在閃存廠商的技術資料當中看到「(512+16)Byte」的表示方式。目前2Gb以下容量的
NAND型閃存絕大多數是(512+16)字節的頁面容量,2Gb以上容量的NAND型閃存則將頁容量擴大到(2048+64)字節。
NAND型閃存以塊為單位進行擦除操作。閃存的寫入操作必須在空白區域進行,如果目標區域已經有數據,必須先擦除後寫入,因此擦除操作是閃存的基本操
作。一般每個塊包含32個512字節的頁,容量16KB;而大容量閃存採用2KB頁時,則每個塊包含64個頁,容量128KB。
每顆NAND型閃存的I/O接口一般是8條,每條數據線每次傳輸(512+16)bit信息,8條就是(512+16)×8bit,也就是前面說的
512字節。但較大容量的NAND型閃存也越來越多地採用16條I/O線的設計,如三星編號K9K1G16U0A的芯片就是64M×16bit的NAND
型閃存,容量1Gb,基本數據單位是(256+
×16bit,還是512字節。
尋址時,NAND型閃存通過8條I/O接口數據線傳輸地址信息包,每包傳送8位地址信息。由於閃存芯片容量比較大,一組8位地址只夠尋址256個頁,
顯然是不夠的,因此通常一次地址傳送需要分若干組,佔用若干個時鐘週期。NAND的地址信息包括列地址(頁面中的起始操作地址)、塊地址和相應的頁面地
址,傳送時分別分組,至少需要三次,佔用三個週期。隨著容量的增大,地址信息會更多,需要佔用更多的時鐘週期傳輸,因此NAND型閃存的一個重要特點就是
容量越大,尋址時間越長。而且,由於傳送地址週期比其他存儲介質長,因此NAND型閃存比其他存儲介質更不適合大量的小容量讀寫請求。
二.MTD
MTD是memory technology
Device的縮寫。MTD支持類似於內存的存儲器,它是底層硬件和上層軟件之間的橋樑。對底層來說,它無論對nor型或是nandflash都有很好的
驅動支持,對上層來說,它抽象出文件系統所需要的接口函數。同時由於flash自身的特別之處(既有類似塊設備的特點,又有類似字符設備的特點),MTD
可以把flash同時為塊設備和字符設備。有了MTD,編寫flash的驅動變得十分輕鬆,因為上層的架構都已經做好,我們只用看看flash的
datasheet,寫最底層的控制時序即可。
以下內容為翻譯自mtd官方網站
http://www.linux-mtd.infradead.org/archive/index.html
mtd致力於為存儲器,尤其是flash,設計一個通用的linux下的子系統。
設計這個系統的目標在於,通過這個系統所提供的硬件驅動和上層系統之間的接口,我們可以方便的為新的硬件編寫驅動。
對於底層的硬件驅動來說,它們所以提供是讀,寫,擦除的流程。而文件的存儲形式是和他們無關的(如FTL,FFS2等等),用恰當的形式存儲用戶的數據那時上層系統關注的事情。
MTD的用戶模塊
MTD為用戶提供五種可以直接在用戶空間使用的模塊
字符設備
塊設備
flash轉換層
nandflash轉換層
JFFS2文件系統
三.yaffs2文件系統
針對於flash的文件系統有很多,據我瞭解有jffs(1,2,3),yaffs(1,2)。還有商業的三星開發的RFS(健壯文件系統),專門針對三
星自己的nand和onenand,從底層驅動到上層文件系統一條龍服務,而且號稱和fat格式100%兼容。當時看得我直流口水,心裡把三星恨的咬牙切
齒。 下面主要介紹一下開源的yaffs文件系統。
Yaffs(Yet Another Flash File
System)文件系統是專門針對NAND閃存設計的嵌入式文件系統,目前有YAFFS和YAFFS2兩個版本,一般說來,YAFFS對
512byte/page以下都有很好的支持,而更大的頁就需要YAFFS2了,如2K/page。
Yaffs文件系統有些類似於JFFS/JFFS2文件系統,與之不同的是JFFS1/2文件系統最初是針對NOR
FLASH的應用場合設計的,而NOR FLASH和NAND FLASH本質上有較大的區別,所以儘管JFFS1/2 文件系統也能應用於NAND
FLASH,但由於它在內存佔用和啟動時間方面針對NOR的特性做了一些取捨,所以對NAND來說通常並不是最優的方案。
Yaffs對文件系統上的所有內容(比如正常文件,目錄,鏈接,設備文件等等)都統一當作文件來處理,每個文件都有一個頁面專門存放文件頭,文件頭保存了
文件的模式、所有者id、組id、長度、文件名、Parent Object
ID等信息。因為需要在一頁內放下這些內容,所以對文件名的長度,符號鏈接對象的路徑名等長度都有限制。
前面說到對於NAND
FLASH上的每一頁數據,都有額外的空間用來存儲附加信息,通常NAND驅動只使用了這些空間的一部分,YAFFS正是利用了這部分空間中剩餘的部分來
存儲文件系統相關的內容。同時由於支持的page變大,YAFFS2使用更多的spare
space來存儲這些信息。在結構上YAFFS和YAFFS2有一定的不同,具體結構可以去看一看這篇文檔
http://www.aleph1.co.uk/node/38
那麼這個文件系統是如何運作起來呢。
操作文件系統的第一步自然是取得SuperBlock了,Yaffs文件系統本身在NAND
Flash上並不存在所謂的SuperBlock塊,完全是在文件系統mount的過程中由read_super函數填充的,不過有意思的一點是,由於物
理上沒有存儲superblock塊,所以NAND
Flash上的yaffs文件系統本身沒有存儲filesystem的魔數(MagicNum),在內存中superblock裡的s_magic參數也
是直接賦值的,所以存儲在NAND
FLASH上的任何文件系統都能被當作yaffs文件系統mount上來,只是數據都會被當作錯誤數據放在lost+found目錄中,不知道這算不算
yaffs文件系統的一個bug。
通常一個具體的文件系統在VFS的Super_block結構中除了通用的數據外,還有自己專用的數據,Yaffs文件系統的專用數據是一個yaffs_DeviceStruct結構,主要用來存儲一些相關軟硬件配置信息,相關函數指針和統計信息等。
在mount過程執行read_super的過程中,Yaffs文件系統還需要將文件系統的目錄結構在內存中建立起來。由於沒有super塊,所以需要掃
瞄Yaffs分區,根據從OOB中讀取出的yaffs_tags信息判斷出是文件頭page還是數據page。再根據文件頭page中的內容以及數據
page中的ObjectID/ChunkID/serial
Number等信息在內存中為每個文件(Object)建立一個對應的yaffs_object對象。
在yaffs_object結構中,主要包含了:
如修改時間,用戶ID,組ID等文件屬性;
用作yaffs文件系統維護用的各種標記位如髒(dirty)標記,刪除標記等等;
用作組織結構的,如指向父目錄的Parent指針,指向同級目錄中其他對象鏈表的 siblings雙向鏈表頭結構
此外根據Object類型的不同(目錄,文件,鏈接),對應於某一具體類型的Object,在Yaffs_object中還有其各自專有的數據內容
普通文件:文件尺寸,用於快速查找文件數據塊的yaffs_Tnode 樹的指針等
目錄:目錄項內容雙向鏈表頭(children)
鏈接:softlink的alias,hardlink對應的ObjectID
除了對應於存儲在NAND
FLASH上的object而建立起來的yaffs_object以外,在read_super執行過程中還會建立一些虛擬對象(Fake
Object),這些Fake Object在NAND
FLASH上沒有對應的物理實體,比如在建立文件目錄結構的最初,yaffs會建立四個虛擬目錄(Fake Directory):rootDir,
unlinkedDir, deleteDir,
lostNfoundDir分別用作根目錄,unlinked對象掛接的目錄,delete對象掛接的目錄,無效或零時數據塊掛接的目錄。
通過創建這些yaffs_object,yaffs文件系統就能夠將存儲在NAND FLASH上數據系統的組織起來,在內存中維護一個完整的文件系統結構
另外,我在看YAFFS2的源代碼的時候發現,YAFFS2再mount和umount和YAFFS有所區別,增加一個checkpoint機制,每次在
umount的時候,YAFFS2會開闢專門幾個block用來存取一些信息,等待下次mount的時候就不需要掃瞄整個flash,而只有找出這幾個塊
就可以了,這樣可以大大加速mount的時間。不過仔細的原理我也沒有研究過。有興趣的話可以看一看這封郵件
http://www.aleph1.co.uk/pipermail/yaffs/2006q2/002019.html
我們用的硬件是omap平台,跑得是linux-2.4.21。其實在這個版本的源代碼裡已經有了mtd的支持。不過看一下代碼可以發現,這個版本
mtd驅動對nandflash的支持並不好,而且支持的最大的page不過512byte,可能是代碼比較早的原因吧。因此移植的第一步是更新mtd驅
動。
首先從mtd官方網站下載最新的cvs(好像只有支持ipV6的,才可以直接要連到cvs庫。或者去ftp下載)。不過好像最新的cvs代碼有一點問題,
我用得是mtd-snapshot20050519。解壓這個snapshot,然後是打補丁。相信這一步應該都什麼問題。打完補丁後,可以去
drivers/mtd/nand/看一看,會發現多了好多文件,其中最最重要的是nand_base.c,關於讀寫擦的流程都是這裡實現的。
接下來的事情比較關鍵,就是定義底層的讀寫端口,讀寫控制。我們可以仿照驅動裡提供spia.c,針對自己的硬件寫一個XXX.C。上層的代碼都已經做
好,都是一些函數指針,我們只要把我們自己特定一些函數掛上去就可以了。這一步說難也難,說簡單也簡單。一開始可能覺得無從下手,其實只要多讀幾遍
nand目錄下提供的很多的例子程序,就會有些思路了。寫完這個,就修改一下nand目錄下的MakeFile.common,如果你懶一點,可以把
spia替換你的XXX.o,到時候記得配置內核在mtd的nand下選擇spia支持就行。你要想做得規範一點,其實也簡單,首先在MakeFile裡
加上obj-$(CONFIG_MTD_NAND_XXX) += XXX.o。然後修改Config.in,加上對XXX的配置支持就可以了。
接下來就是按部就班的配置內核,編譯。如果沒錯,那麼恭喜你。不過一般會有錯,我碰到的是提示少了幾個頭文件。我在snapshot裡可以找到,然後一一拷貝的include/linux下就可以了,^_^
開始測試:
首先在內核啟動的時候,mtd會檢測硬件系統中是否有flash存在,這一步是通過讀取flash的ID來判斷。如果這一步是正常的,那麼說明底層驅動的
流程和硬件上是沒有錯誤了。很不幸,我在這裡就掛了。系統提示can『t kernel paging request at virtual
address
0x0c000000,顯示了一大堆寄存器和堆棧就死在那裡了。好不鬱悶。google了好久,發現這一個是一個地址映射的錯誤。因為linux是分為內
核空間和用戶空間的。對於arm來說,內核空間的地址應該是3G-4G處,3G以下是用戶空間。而我在寫我自己的xxx.c文件的時候,IO端口是這樣設
置的
this->IO_ADDR_R = 0x0c000000;
this->IO_ADDR_W = 0x0c000000;
顯然0x0c000000是不屬於內核空間的,因此造成了上述問題。
因此我們要把讀寫命令端口映射到內核空間去,這便是虛擬地址。
this->IO_ADDR_R = (unsigned long)ioremap(0x0c000000, SZ_1K);
this->IO_ADDR_W = (unsigned long)ioremap(0x0c000000, SZ_1K);
因此要用ioremap()函數。這些函數都定義在include/arch/asm-arm/io.h
至於物理地址和虛擬地址應是如何映射的,mm.c有定義
好了,現在內核可以順利啟動了,flash可以順利的認出來了。嘿嘿,掃瞄了一下,還找出了幾個壞塊。
現在flash現在已經成為linux一種設備了。下面這兩條命令
mknod /dev/mtd0 c 90 0
mknod /dev/mtdblock0 b 31 0
把flash的第一個分區映射為字符設備和塊設備。mtd/util/提供了許多有用的工具,比如nandwrite,flash_erase等等。交叉編譯後就可以使用了。
flash_eraseall /dev/mtd0 把分區零的內容全部刪除。
隨便新建一個文件xxx,然後nandwrite -p /dev/mtd0 xxx。其中參數p表示允許填充。因為文件的大小不一定是flash的page的整數倍。
如果提示寫進去了,那麼基本沒問題了。我不放心,又用自己的測試程序在ccs裡測了一下,果然寫進去了。那真叫一個開心啊。迫不及待的mount -t yaffs2 /dev/mtdblock0 /mnt
提示:NAND:geometry problem chunk size is 2048 type is yaffs2
然後退出了mount進程。一盤冷水澆了下來,告訴我苦難的里程剛剛開始咯……
過來好久,才想起要寫,後來發現關於YAFFS2的移植,一些網頁上已經寫的很詳細了,於是就懶得寫了,不如寫幾個自己碰到的問題吧,自己感覺還是挺關鍵的問題。
問題一:地址映射問題(MTD測試階段)
描述:在系統啟動的時候顯示can『t kernel paging request at virtual address 0x0c000000
同時系統 kill init進程。無法啟動
解決:發現是沒有進行地址映射的問題
linux把地址空間分為用戶空間和內核空間,內核空間從3G開始,而物理地址0x0c000000並不屬於內核空間,直接使用就造成了上述問題。
因此我們要把讀寫命令端口映射到內核空間去,這便是虛擬地址。
修改xxx.c:
this->IO_ADDR_R = (unsigned long)ioremap(0x0c000000, SZ_1K);
this->IO_ADDR_W = (unsigned long)ioremap(0x0c000000, SZ_1K);
因此要用ioremap()函數。這些函數都定義在include/arch/asm-arm/io.h
至於物理地址和虛擬地址應是如何映射的,mm.c有定義
問題二:mount問題(YAFFS2測試階段)
描述:mount錯誤,失敗,提示:
NAND:geometry problem chunk size is 2048 type is yaffs2
解決:追蹤代碼發現問題在yaffs_guts.c裡,從代碼可以發現,mount時要求的分區的block數目要大於
dev->internalStartBlock + dev->nReservedBlocks + 2。而我mount的分區大小為一個block,因此不滿足條件。後來mount一個為4095個block的分區,成功。
問題三:文件消失(YAFFS2測試階段)
描述:mount上分區後,可以向/mnt/flash1上寫文件,也可以讀取。
但是umount 後,再mount發現文件消失。
解決:這個問題幾乎鬱悶了我半個月,幾乎每天都在網上看YAFFS的mail
list。一堆一堆的英文,看得都想吐了。到最後快絕望時候,才查了出來,原來最新的mtd中的nand_base.c代碼有問題。在
nand_write_ecc()中,沒有寫oob的內容(如果你選擇NAND_ECC_NONE)。而在yaffs的接口函數中在既有數據又有oob信
息的時候,是直接調用mtd->write_ecc()的。由於nand_write_ecc()沒有寫oob的原因,導致yaffs從oob讀取
信息時,讀到的全是0xff,從而無法構建文件系統。這是最最主要的一個umount後文件消失的原因,其實文件信息全寫進去了,只是tag信息沒有寫進
去。
後來我把mtd換成了mtd-sanpshot20050519,這裡的nand_write_ecc()是寫oob信息的。就解決的umount的問題。
好了,終於寫完了。關於YAFFS2的記憶要告一段落了。很感謝YAFFS2給我帶來這個終身難忘的暑假,88吧。
只能感嘆說,遷入系統愈來愈完整,看到以前人的辛苦