NandFlash ECC 校驗演算法原理與實現
ECC的全稱是Error Checking and Correction,是一種用於Nand的差錯檢測和修正演算法。如果操作時序和電路穩定性不存在問題的話,NAND Flash出錯的時候一般不會造成整個Block或是Page不能讀取或是全部出錯,而是整個Page(例如512Bytes)中只有一個或幾個bit出錯。ECC能糾正1個位元錯誤和檢測2個位元錯誤,而且計算速度很快,但對1位元以上的錯誤無法糾正,對2位元以上的錯誤不保證能檢測。
校驗碼生成演算法:ECC校驗每次對256位元組的資料進行操作,包含列校驗和行校驗。對每個待校驗的Bit位求異或,若結果為0,則表明含有偶數個1;若結果為1,則表明含有奇數個1。列校驗規則如表1所示。256位元組資料形成256行、8列的矩陣,矩陣每個元素表示一個Bit位。
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
其中CP0 ~ CP5 為六個Bit位,表示Column Parity(列極性),
CP0為第0、2、4、6列的極性,CP1為第1、3、5、7列的極性,
CP2為第0、1、4、5列的極性,CP3為第2、3、6、7列的極性,
CP4為第0、1、2、3列的極性,CP5為第4、5、6、7列的極性。
用公式表示就是:CP0=Bit0^Bit2^Bit4^Bit6, 表示第0列內部256個Bit位異或之後再跟第2列256個Bit位異或,再跟第4列、第6列的每個Bit位異或,這樣,CP0其實是256*4=1024個Bit位異或的結果。CP1 ~ CP5 依此類推。
行校驗如下圖所示
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
其中RP0 ~ RP15 為十六個Bit位,表示Row Parity(行極性),
RP0為第0、2、4、6、….252、254 個位元組的極性
RP1-----1、3、5、7……253、255
RP2----0、1、4、5、8、9…..252、253 (處理2個Byte,跳過2個Byte)
RP3---- 2、3、6、7、10、11…..254、255 (跳過2個Byte,處理2個Byte)
RP4---- 處理4個Byte,跳過4個Byte;
RP5---- 跳過4個Byte,處理4個Byte;
RP6---- 處理8個Byte,跳過8個Byte
RP7---- 跳過8個Byte,處理8個Byte;
RP8---- 處理16個Byte,跳過16個Byte
RP9---- 跳過16個Byte,處理16個Byte;
RP10----處理32個Byte,跳過32個Byte
RP11----跳過32個Byte,處理32個Byte;
RP12----處理64個Byte,跳過64個Byte
RP13----跳過64個Byte,處理64個Byte;
RP14----處理128個Byte,跳過128個Byte
RP15----跳過128個Byte,處理128個Byte;
可見,RP0 ~ RP15 每個Bit位都是128個位元組(也就是128行)即128*8=1024個Bit位求異或的結果。
綜上所述,對256位元組的資料共生成了6個Bit的列校驗結果,16個Bit的行校驗結果,共22個Bit。在Nand中使用3個位元組存放校驗結果,多餘的兩個Bit位置1。存放次序如下表所示:
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
以K9F1208為例,每個Page頁包含512位元組的資料區和16位元組的OOB區。前256位元組資料生成3位元組ECC校驗碼,後256位元組資料生成3位元組ECC校驗碼,共6位元組ECC校驗碼存放在OOB區中,存放的位置為OOB區的第0、1、2和3、6、7位元組。
校驗碼生成演算法的C語言實現
在Linux核心中ECC校驗演算法所在的檔案為drivers/mtd/nand/nand_ecc.c,其實現有新、舊兩種,在2.6.27及更早的核心中使用的程式,從2.6.28開始已經不再使用,而換成了效率更高的程式。可以在Documentation/mtd/nand_ecc.txt 檔案中找到對新程式的詳細介紹。
首先分析一下2.6.27核心中的ECC實現,原始碼見:
http://lxr.linux.no/linux+v2.6.27/drivers/mtd/nand/nand_ecc.c
43/*
44* Pre-calculated 256-way 1 byte column parity
45*/
47 0x00, 0x55, 0x56, 0x03, 0x59, 0x0c, 0x0f, 0x5a, 0x5a, 0x0f, 0x0c, 0x59, 0x03, 0x56, 0x55, 0x00,
48 0x65, 0x30, 0x33, 0x66, 0x3c, 0x69, 0x6a, 0x3f, 0x3f, 0x6a, 0x69, 0x3c, 0x66, 0x33, 0x30, 0x65,
49 0x66, 0x33, 0x30, 0x65, 0x3f, 0x6a, 0x69, 0x3c, 0x3c, 0x69, 0x6a, 0x3f, 0x65, 0x30, 0x33, 0x66,
50 0x03, 0x56, 0x55, 0x00, 0x5a, 0x0f, 0x0c, 0x59, 0x59, 0x0c, 0x0f, 0x5a, 0x00, 0x55, 0x56, 0x03,
51 0x69, 0x3c, 0x3f, 0x6a, 0x30, 0x65, 0x66, 0x33, 0x33, 0x66, 0x65, 0x30, 0x6a, 0x3f, 0x3c, 0x69,
52 0x0c, 0x59, 0x5a, 0x0f, 0x55, 0x00, 0x03, 0x56, 0x56, 0x03, 0x00, 0x55, 0x0f, 0x5a, 0x59, 0x0c,
53 0x0f, 0x5a, 0x59, 0x0c, 0x56, 0x03, 0x00, 0x55, 0x55, 0x00, 0x03, 0x56, 0x0c, 0x59, 0x5a, 0x0f,
54 0x6a, 0x3f, 0x3c, 0x69, 0x33, 0x66, 0x65, 0x30, 0x30, 0x65, 0x66, 0x33, 0x69, 0x3c, 0x3f, 0x6a,
55 0x6a, 0x3f, 0x3c, 0x69, 0x33, 0x66, 0x65, 0x30, 0x30, 0x65, 0x66, 0x33, 0x69, 0x3c, 0x3f, 0x6a,
56 0x0f, 0x5a, 0x59, 0x0c, 0x56, 0x03, 0x00, 0x55, 0x55, 0x00, 0x03, 0x56, 0x0c, 0x59, 0x5a, 0x0f,
57 0x0c, 0x59, 0x5a, 0x0f, 0x55, 0x00, 0x03, 0x56, 0x56, 0x03, 0x00, 0x55, 0x0f, 0x5a, 0x59, 0x0c,
58 0x69, 0x3c, 0x3f, 0x6a, 0x30, 0x65, 0x66, 0x33, 0x33, 0x66, 0x65, 0x30, 0x6a, 0x3f, 0x3c, 0x69,
59 0x03, 0x56, 0x55, 0x00, 0x5a, 0x0f, 0x0c, 0x59, 0x59, 0x0c, 0x0f, 0x5a, 0x00, 0x55, 0x56, 0x03,
60 0x66, 0x33, 0x30, 0x65, 0x3f, 0x6a, 0x69, 0x3c, 0x3c, 0x69, 0x6a, 0x3f, 0x65, 0x30, 0x33, 0x66,
61 0x65, 0x30, 0x33, 0x66, 0x3c, 0x69, 0x6a, 0x3f, 0x3f, 0x6a, 0x69, 0x3c, 0x66, 0x33, 0x30, 0x65,
0x00, 0x55, 0x56, 0x03, 0x59, 0x0c, 0x0f, 0x5a, 0x5a, 0x0f, 0x0c, 0x59, 0x03, 0x56, 0x55, 0x00
63};
為了加快計算速度,程式中使用了一個預先計算好的列極性表。這個表中每一個元素都是unsigned char型別,表示8位二進位制數。
表中8位二進位制數每位的含義:
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
這個表的意思是:對0~255這256個數,計算並存儲每個數的列校驗值和行校驗值,以數作陣列下標。比如 nand_ecc_precalc_table[ 13 ] 儲存13的列校驗值和行校驗值,13的二進位制表示為 00001101, 其CP0 = Bit0^Bit2^Bit4^Bit6 = 0;
CP1 = Bit1^Bit3^Bit5^Bit7 = 1;
CP2 = Bit0^Bit1^Bit4^Bit5 = 1;
CP3 = Bit2^Bit3^Bit6^Bit7 = 0;
CP4 = Bit0^Bit1^Bit2^Bit3 = 1;
CP5 = Bit4^Bit5^Bit6^Bit7 = 0;
其行極性RP = Bit0^Bit1^Bit2^Bit3^Bit4^Bit5^Bit6^Bit7 = 1;
則nand_ecc_precalc_table[ 13 ] 處儲存的值應該是 0101 0110,即0x56.
注意,陣列nand_ecc_precalc_table的下標其實是我們要校驗的一個位元組資料。
(這句話最重要,立刻明白了怎麼回事,網上其它人寫的ECC演算法都是來回抄,好多都抄錯了,弄得我不知所云,暈頭轉向。)
理解了這個表的含義,也就很容易寫個程式生成這個表了。程式見附件中的 MakeEccTable.c檔案。
有了這個表,對單位元組資料dat,可以直接查表 nand_ecc_precalc_table[ dat ] 得到 dat的行校驗值和列校驗值。 但是ECC實際要校驗的是256位元組的資料,需要進行256次查表,對得到的256個查表結果進行按位異或,最終結果的 Bit0 ~ Bit5 即是256位元組資料的 CP0 ~ CP5.
/* Build up column parity */
/* Get CP0 - CP5 from table */
86 //這裡省略了一些,後面會介紹
91 }
Reg1
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
在這裡,計算列極性的過程其實是先在一個位元組資料的內部計算CP0 ~ CP5, 每個位元組都計算完後再與其它位元組的計算結果求異或。而表1中是先對一列Bit0求異或,再去異或一列Bit2。 這兩種只是計算順序不同,結果是一致的。 因為異或運算的順序是可交換的。
行極性的計算要複雜一些。
nand_ecc_precalc_table[] 表中的 Bit6 已經儲存了每個單位元組數的行極性值。對於待校驗的256位元組資料,分別查表,如果其行極性為1,則記錄該資料所在的行索引(也就是for迴圈的i值),這裡的行索引是很重要的,因為RP0 ~ RP15 的計算都是跟行索引緊密相關的,如RP0只計算偶數行,RP1只計算奇數行,等等。
/* Build up column parity */
/* Get CP0 - CP5 from table */
/* All bit XOR = 1 ? */
90 }
91 }
這裡的關鍵是理解第88和89行。Reg3和reg2都是unsigned char 型的變數,並都初始化為零。
行索引(也就是for迴圈裡的i)的取值範圍為0~255,根據表2可以得出以下規律:
RP0只計算行索引的Bit0為0的行,RP1只計算行索引的Bit0為1的行;
RP2只計算行索引的Bit1為0的行,RP3只計算行索引的Bit1為1的行;
RP4只計算行索引的Bit2為0的行,RP5只計算行索引的Bit2為1的行;
RP6只計算行索引的Bit3為0的行,RP7只計算行索引的Bit3為1的行;
RP8只計算行索引的Bit4為0的行,RP9只計算行索引的Bit4為1的行;
RP10只計算行索引的Bit5為0的行,RP11只計算行索引的Bit5為1的行;
RP12只計算行索引的Bit6為0的行,RP13只計算行索引的Bit6為1的行;
RP14只計算行索引的Bit7為0的行,RP15只計算行索引的Bit7為1的行;
已經知道,異或運算的作用是判斷位元位為1的個數,跟位元位為0的個數沒有關係。如果有偶數個1則異或的結果為0,如果有奇數個1則異或的結果為1。
那麼,程式第88行,對所有行校驗為1的行索引按位異或運算,作用便是:
判斷在所有行校驗為1的行中,
屬於RP1計算範圍內的行有多少個------由reg3的Bit 0指示,0表示有偶數個,1表示有奇數個;
屬於RP3計算範圍內的行有多少個------由reg3的Bit 1指示,0表示有偶數個,1表示有奇數個;
屬於RP5計算範圍內的行有多少個------由reg3的Bit 2指示,0表示有偶數個,1表示有奇數個;
屬於RP7計算範圍內的行有多少個------由reg3的Bit 3指示,0表示有偶數個,1表示有奇數個;
屬於RP9計算範圍內的行有多少個------由reg3的Bit 4指示,0表示有偶數個,1表示有奇數個;
屬於RP11計算範圍內的行有多少個------由reg3的Bit 5指示,0表示有偶數個,1表示有奇數個;
屬於RP13計算範圍內的行有多少個------由reg3的Bit 6指示,0表示有偶數個,1表示有奇數個;
屬於RP15計算範圍內的行有多少個------由reg3的Bit 7指示,0表示有偶數個,1表示有奇數個;
所以,reg3每個Bit位的作用如下表所示:
Reg3
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
第89行,對所有行校驗為1的行索引按位取反之後,再按位異或,作用就是判斷位元位為0的個數。比如reg2的Bit0為0表示:所有行校驗為1的行中,行索引的Bit0為0的行有偶數個,也就是落在RP0計算範圍內的行有偶數個。所以得到結論:
在所有行校驗為1的行中,
屬於RP0計算範圍內的行有多少個------由reg2的Bit 0指示,0表示有偶數個,1表示有奇數個;
屬於RP2計算範圍內的行有多少個------由reg2的Bit 1指示,0表示有偶數個,1表示有奇數個;
屬於RP4計算範圍內的行有多少個------由reg2的Bit 2指示,0表示有偶數個,1表示有奇數個;
屬於RP6計算範圍內的行有多少個------由reg2的Bit 3指示,0表示有偶數個,1表示有奇數個;
屬於RP8計算範圍內的行有多少個------由reg2的Bit 4指示,0表示有偶數個,1表示有奇數個;
屬於RP10計算範圍內的行有多少個------由reg2的Bit 5指示,0表示有偶數個,1表示有奇數個;
屬於RP12計算範圍內的行有多少個------由reg2的Bit 6指示,0表示有偶數個,1表示有奇數個;
屬於RP14計算範圍內的行有多少個------由reg2的Bit 7指示,0表示有偶數個,1表示有奇數個;
所以,reg2每個Bit位的作用如下表所示:
Reg2
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
至此,只用了一個查詢表和一個for迴圈,就把所有的校驗位CP0 ~ CP5 和RP0 ~ RP15全都計算出來了。下面的任務只是按照表3的格式,把這些位元位重新排列一下順序而已。
從reg2和reg3中抽取出 RP8~RP15放在tmp1中,抽取出RP0~RP7放在tmp2中,
Reg1左移兩位,低兩位置1,
然後把tmp2, tmp1, reg1 放在 ECC碼的三個位元組中。
ECC糾錯演算法
當往NAND Flash的page中寫入資料的時候,每256位元組我們生成一個ECC校驗和,稱之為原ECC校驗和,儲存到PAGE的OOB(out-of-band)資料區中。當從NAND Flash中讀取資料的時候,每256位元組我們生成一個ECC校驗和,稱之為新ECC校驗和。
將從OOB區中讀出的原ECC校驗和新ECC校驗和按位異或,若結果為0,則表示不存在錯(或是出現了 ECC無法檢測的錯誤);若3個位元組異或結果中存在11個位元位為1,表示存在一個位元錯誤,且可糾正;若3個位元組異或結果中只存在1個位元位為1,表示 OOB區出錯;其他情況均表示出現了無法糾正的錯誤。
假設ecc_code_raw[3] 儲存原始的ECC校驗碼,ecc_code_new[3] 儲存新計算出的ECC校驗碼,其格式如下表所示:
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
對ecc_code_raw[3] 和 ecc_code_new[3] 按位異或,得到的結果三個位元組分別儲存在s0,s1,s2中,如果s0s1s2中共有11個Bit位為1,則表示出現了一個位元位錯誤,可以修正。定位出錯的位元位的方法是,先確定行地址(即哪個位元組出錯),再確定列地址(即該位元組中的哪一個Bit位出錯)。
確定行地址的方法是,設行地址為unsigned char byteoffs,抽取s1中的Bit7,Bit5,Bit3,Bit1,作為 byteoffs的高四位, 抽取s0中的Bit7,Bit5,Bit3,Bit1 作為byteoffs的低四位, 則byteoffs的值就表示出錯位元組的行地址(範圍為0 ~ 255)。
確定列地址的方法是:抽取s2中的Bit7,Bit5,Bit3 作為 bitnum 的低三位,bitnum其餘位置0,則bitnum的表示出錯Bit位的列地址 (範圍為0 ~ 7)。
下面以一個簡單的例子探索一下這其中的奧妙。
假設待校驗的資料為兩個位元組,0x45(二進位制為0100 0101)和0x38(二進位制為0011 1000),其行列校驗碼如下表所示:
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
從表中可以計算出CP5 ~ CP0的值,列在下表的第一行(原始資料)。假設現在有一個數據位發生變化,0x38變為0x3A,也就是Byte
1的Bit 1由0變成了1,計算得到新的CP5 ~ CP0值放在下表第2行(變化後資料)。新舊校驗碼求異或的結果放在下表第三行。
可見,當 Bit
1發生變化時,列校驗值中只有CP1,CP2,CP4發生了變化,而CP0,CP3,CP5沒變化,也就是說6個Bit校驗碼有一半發生變化,則求異或的結果中有一半為1。同理,行校驗求異或的結果也有一半為1。這就是為什麼前面說256位元組資料中的一個Bit位發生變化時,新舊22Bit校驗碼求異或的結果中會有11個Bit 位為1。
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
再來看怎麼定位出錯的Bit位。以列地址為例,若CP5發生變化(異或後的CP5=1),則出錯處肯定在 Bit 4 ~ Bit 7中;若CP5無變化(異或後的CP5=0),則出錯處在 Bit 0 ~ Bit 3 中,這樣就篩選掉了一半的Bit位。剩下的4個Bit位中,再看CP3是否發生變化,又選出2個Bit位。剩下的2Bit位中再看CP1是否發生變化,則最終可定位1個出錯的Bit位。下面的樹形結構更清晰地展示了這個判決過程:
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
圖表 1 出錯Bit列地址定位的判決樹
注意:圖中的CP指的是求異或之後的結果中的CP
為什麼只用CP4,CP2,CP0呢?其實這裡麵包含冗餘資訊,因為CP5=1則必有CP4=0,CP5=0則必有CP4=1,也就是CP5跟CP4一定相反,同理,CP3跟CP2一定相反,CP1跟CP0一定相反。所以只需要用一半就行了。
這樣,我們從異或結果中抽取出CP5,CP3,CP1位,便可定位出錯Bit位的列地址。比如上面的例子中CP5/CP3/CP1 = 001,表示Bit 1出錯。
同理,行校驗RP1發生變化,抽取RP1,可知Byte 1發生變化。這樣定位出Byte 1的Bit 0出錯。
當資料位256位元組時,行校驗使用RP0 ~ RP15,抽取異或結果的RP15,RP13,RP11,RP9,RP7,RP5,RP3,RP1位便可定位出哪個Byte出錯,再用CP5,CP3,CP1定位哪個Bit出錯