NAND FTL
前言
工作測試闆用了H750核心闆子產品,子產品上有一塊512MB NAND Flash,提供的FTL參考比較混亂,使用中也不穩定,網上有一堆闡述FTL的文章,都沒有可以直接使用的源代碼,還是自己寫一個吧,自己的比較香!
成熟的FTL都是商用收費的,免費的uffs又不相容window fat,這樣就不能模拟U盤,是以都促使了寫一個适合自己使用的。
一、FTL是什麼?
FTL是平台檔案系統到NAND之間的中間層,因為NAND是頁讀取,塊寫入(如果寫入區非FF),塊的程式設計和擦除過程中,還會發生壞塊,塊本身的擦除次數還有壽命限制
是以,檔案系統讀寫NAND時候,FTL在中間做位址轉換,壞塊管理等轉換
完整的FTL功能通常包括如下:
1 - 壞塊管理
nand block都有一定擦寫次數,到達一定次數後,讀寫會不穩定,當發現寫入或擦除失敗時,應該标記為壞塊,不再使用這些塊
2 - 擦寫平衡
原因同上,不能一直在固定塊擦除寫入,例如邏輯sector0資料,寫入block0 page0,下一次sector再寫入時,不是擦除block0後重新寫入,而是在block2 page0寫入,block0标記為垃圾塊,block2标記為新的邏輯塊,當找不到空閑塊時,進行垃圾塊擦除回收
3 - 掉電保護
隻要在新塊寫入成功之後,再把原有塊标記為垃圾塊,一定程度上,防止了資料丢失
4 - 冷熱資料
有些資料是頻繁讀取和寫入的,例如系統LOG,稱為熱資料,有些資料是寫入之後,很少變動的,例如UI圖檔等,稱為冷資料,為了避免頻繁擦寫尋找空閑塊,應該盡可能把熱資料和冷資料,分别存在不同區塊
二、功能實作
FTL整個流程還是有點複雜的,寫了滿滿的注釋,和LOG列印,目前測試下來基本可以用了。
沒有做冷熱資料,因為一方面主要做靜态存儲,另一方面能力有限,不知道怎麼下手。
NAND CHIP是h27u4g8f,4096個block,每個block 64page,每個page main 2048byte,spare 64byte
spare byte0~15,存儲ECC
spare byte16~17,正常FFFF,如果是壞塊,則不為FFFF
spare byte18~19,正常FFFF,如果是垃圾塊,則不為FFFF
spare byte20~21,如果FFFF,是空閑塊,如果0 ~ 4095,是使用中邏輯塊
上電初始化NAND,讀取ID,然後讀取block 0 ~ 4095的PAGE0的SPARE,判斷塊狀态,生成LUT
寫扇區:
邏輯轉到實體,查找實體塊,找到了,如果寫入區都是FF,則直接寫入,如果寫入區有資料,重新查找一個空閑塊,把之前塊資料複制到新塊,并寫入新資料,新塊标記為目前邏輯塊,之前塊标記為垃圾塊
讀扇區:
邏輯轉到實體,查找實體塊,找到了,讀取扇區資料,如果沒找到,找一塊空閑塊,标記為目前邏輯塊,然後讀取扇區資料,理論上傳回應該全都是FF
以上兩個動作,都會查找實體塊,如果找不到需要的實體塊,就查找全部的垃圾塊,然後擦除垃圾塊,重新标記為空閑塊,達到了垃圾塊回收的效果。垃圾塊回收完成後,讀寫動作重新查找實體塊,如果還沒找到,傳回NO FREE ERROR
二、相關代碼
#include "stdint.h"
#include "rtthread.h"
#include "sram.h"
#include "vopftl.h"
#define DBG
#define DBG_TAG "ftl"
#include "log.h"
#ifndef NULL
#define NULL (0)
#endif
#define FTL_EOK 0
#define FTL_ERROR 1
#define FTL_NO_LIFE 2
#define FTL_NO_FREE 3
#define FTL_WRITE_ERR 4
#define FTL_COPY_ERR 5
#define PAGE_SIZE (2048)
#define PAGE_NUM (64)
#define BLOCK_NUM (4096)
#define OOB_SIZE 16
#define OOB_BAD_POS 0
#define OOB_GARBAGE_POS 2
#define OOB_LOGICAL_POS 4
#define OOB_GET_BAD (*(uint16_t *)&p_oobbuf[OOB_BAD_POS])
#define OOB_GET_GARBAGE (*(uint16_t *)&p_oobbuf[OOB_GARBAGE_POS])
#define OOB_GET_LOGICAL (*(uint16_t *)&p_oobbuf[OOB_LOGICAL_POS])
#define BLOCK_FREE 0xffff
#define BLOCK_BAD 0xfffe
#define BLOCK_GARBAGE 0xfffd
#define BLOCK_ACTIVE 0xfffc
#define BLOCK_ABNORMAL 0xfffb
static nand_drv_t *pdrv = NULL;
ALIGN(4)
static uint8_t oob_buf[OOB_SIZE];
static uint8_t *p_oobbuf = (uint8_t *)oob_buf;
ALIGN(4)
static uint8_t page_buf[PAGE_SIZE];
static uint8_t *p_pagebuf = (uint8_t *)page_buf;
ALIGN(4)
static uint16_t lut_buf[BLOCK_NUM];
static uint16_t *p_lutbuf = (uint16_t *)lut_buf;
static uint16_t cur_logical;
static uint16_t cur_physical;
static uint16_t cur_page;
static uint32_t sector_max = 0;
//标記壞塊
static uint8_t block_mask_bad(uint16_t phyblock)
{
uint8_t buf[2] = {0x00, 0x00};
//寫入壞塊标記
LOG_D("block %d mask bad\r\n", phyblock);
if (pdrv->write_page(phyblock, 0, NULL, OOB_BAD_POS, buf, 2))
{
return FTL_ERROR;
}
//更新表格
p_lutbuf[phyblock] = BLOCK_BAD;
return FTL_EOK;
}
//标記垃圾塊
static uint8_t block_mask_garbage(uint16_t phyblock)
{
uint8_t buf[2] = {0x00, 0x00};
//寫入垃圾塊标記
LOG_D("block %d mask garbage\r\n", phyblock);
if (pdrv->write_page(phyblock, 0, NULL, OOB_GARBAGE_POS, buf, 2))
{
return FTL_ERROR;
}
//更新表格
p_lutbuf[phyblock] = BLOCK_GARBAGE;
return FTL_EOK;
}
//标記邏輯塊
static uint8_t block_mask_logical(uint16_t phyblock, uint16_t logblock)
{
uint8_t buf[2] = {0x00, 0x00};
buf[0] = logblock & 0xff;
buf[1] = logblock >> 8;
LOG_D("block %d mask logical %d\r\n", phyblock, logblock);
//寫入邏輯塊
if (pdrv->write_page(phyblock, 0, NULL, OOB_LOGICAL_POS, buf, 2))
{
return FTL_ERROR;
}
//更新表格
p_lutbuf[phyblock] = logblock;
return FTL_EOK;
}
//邏輯塊轉實體塊位址
static uint8_t block_log2phy(uint16_t *phyblock, uint16_t logblock)
{ //搜尋表格
for (uint16_t i = 0; i < BLOCK_NUM; i++)
{
if (p_lutbuf[i] == logblock)
{ //傳回實際實體塊
*phyblock = i;
LOG_D("block logical %d to physical %d\r\n", logblock, i);
return FTL_EOK;
}
}
LOG_D("block logical %d to physical fail\r\n", logblock);
return FTL_ERROR;
}
//釋放垃圾塊
static uint8_t collect_garbage_block(uint16_t phyblock, uint8_t odd_even)
{
uint16_t odd,even;
odd = 0; even = 0;
for (uint16_t block = 0; block < BLOCK_NUM; block++)
{
if (p_lutbuf[block] == BLOCK_GARBAGE)
{ //找到垃圾塊
if (pdrv->erase_block(block))
{ //擦除失敗,标記壞塊
block_mask_bad(block);
}
else
{ //擦除成功
if (block & 0x1){odd++;}else{even++;}
//更新表格
p_lutbuf[block] = BLOCK_FREE;
LOG_D("collect garbage block - %d\r\n", block);
}
}
}
LOG_D("collect garbage block number - odd %d, even %d\r\n", odd, even);
//需要判斷奇偶
if (odd_even)
{
if (((phyblock & 0x1) == 1)&&(odd )){return FTL_EOK;}
if (((phyblock & 0x1) == 0)&&(even)){return FTL_EOK;}
}
else
{
if ((odd)||(even)){return FTL_EOK;}
}
return (FTL_NO_FREE);
}
//選址一個空閑塊,選擇是否需要奇偶判斷
static uint8_t select_free_block(uint16_t *phyblock, uint16_t logblock, uint8_t odd_even)
{
LOG_D("select free block fot logic %d\r\n", logblock);
aa: //查找可用塊
for (uint16_t block = 0; block < BLOCK_NUM; block++)
{ //塊空閑,不判斷奇偶或者判斷奇偶
if ((p_lutbuf[block] == BLOCK_FREE)&&
((odd_even == 0)||((odd_even > 0)&&((block & 0x1)==(*phyblock & 0x1)))))
{ //找到一個可用的空閑塊,并且在同一PLANE
bb: //先做一遍擦除測試
LOG_D("select free block - %d\r\n", block);
LOG_D("erase physical block %d\r\n", block);
if (pdrv->erase_block(block))
{ //擦除失敗,标記壞塊,更新表格,重新查找
block_mask_bad(block);
goto aa;
}
//是一個好塊,标記邏輯塊
if (block_mask_logical(block, logblock))
{ //标記失敗,進行擦除測試
goto bb;
}
//傳回新的實體塊
*phyblock = block;
LOG_D("select free block - %d, pass\r\n", block);
return FTL_EOK;
}
}
//釋放垃圾塊
LOG_D("select free block fail, so collect garbage block\r\n");
if (collect_garbage_block(*phyblock, odd_even))
{ //釋放失敗
LOG_D("not any garbage block can be free\r\n");
return FTL_NO_FREE;
}
//釋放成功,重新開始
goto aa;
}
//資料複制到新塊并寫入新的資料
static uint8_t writecopy_block(uint16_t *phyblock, uint16_t logblock, uint16_t page, uint8_t *buf)
{
uint8_t rst = FTL_EOK;
uint16_t sblock = *phyblock;
uint16_t tblock = *phyblock;
aa:
//找一塊可用的複制塊, 判斷奇偶
if (select_free_block(&tblock, logblock, 1))
{
return FTL_NO_FREE;
}
//複制塊資料
for (uint16_t i = 0; i < PAGE_NUM; i++)
{
rst = FTL_EOK;
if (i == page)
{ //寫入資料
if (pdrv->write_page(tblock, i, buf, 0, NULL, 0)){rst = FTL_WRITE_ERR;}
}
else
{ //複制資料
if (pdrv->copyback_page(sblock, i, tblock, i)){rst = FTL_COPY_ERR;}
}
if (rst != FTL_EOK)
{ //寫入失敗,标記目前塊為垃圾塊,重新開始查找空閑塊
block_mask_garbage(tblock);
goto aa;
}
}
//操作成功,标記之前塊為垃圾塊
block_mask_garbage(sblock);
//更新目前實體塊
*phyblock = tblock;
return FTL_EOK;
}
//讀寫打開
uint8_t ftl_open(uint32_t sector)
{
//超出操作範圍
if (sector >= sector_max)
{
LOG_D("ftl open sector fail, %d / %d\r\n", sector, sector_max);
return FTL_ERROR;
}
//邏輯塊位置
cur_logical = sector / PAGE_NUM;
cur_page = sector % PAGE_NUM;
cur_physical = 0xffff;
//轉成實際實體塊
if (block_log2phy(&cur_physical, cur_logical))
{ //沒有找到,申請一個新塊
if (select_free_block(&cur_physical, cur_logical, 0))
{ //申請失敗
return FTL_ERROR;
}
}
LOG_D("ftl open sector pass, logical %d, physical %d, page %d\r\n", cur_logical, cur_physical, cur_page);
return FTL_EOK;
}
//讀寫關閉
uint8_t ftl_close(void)
{
LOG_D("ftl close, logical %d, physical %d, page %d\r\n", cur_logical, cur_physical, cur_page);
cur_logical = 0xffff;
cur_page = 0;
cur_physical = 0xffff;
return FTL_EOK;
}
//寫入一個扇區
uint8_t ftl_write_sector(uint8_t *buf, uint32_t count)
{
uint16_t i;
uint8_t *pdat = buf;
while (count > 0)
{
//參數異常
if (cur_physical >= BLOCK_NUM){return FTL_ERROR;}
//先讀出寫入區域的資料
pdrv->read_page(cur_physical, cur_page, p_pagebuf, NULL);
for (i = 0; i < PAGE_SIZE; i++)
{
if (p_pagebuf[i] != 0xff){break;}
}
//資料寫入
if (i == PAGE_SIZE)
{ //區域全空,可以直接寫入
if (pdrv->write_page(cur_physical, cur_page, pdat, 0, NULL, 0))
{ //寫入失敗, 複制寫入到新的實體塊
if (writecopy_block(&cur_physical, cur_logical, cur_page, pdat))
{ //複制失敗
return FTL_ERROR;
}
}
}
else
{ //區域非空,寫入到新的塊
if (writecopy_block(&cur_physical, cur_logical, cur_page, pdat))
{ //複制失敗
return FTL_ERROR;
}
}
LOG_D("ftl write pass, logical %d, physical %d, page %d\r\n", cur_logical, cur_physical, cur_page);
count--;
//還有後續資料需要寫入
if (count > 0)
{ //已經是實體塊最後一頁
if ((cur_page + 1) >= PAGE_NUM)
{ //邏輯塊+1, 轉成實際實體塊
cur_logical++;
if (block_log2phy(&cur_physical, cur_logical))
{ //沒有找到,申請一個新塊
if (select_free_block(&cur_physical, cur_logical, 0))
{ //申請失敗
return FTL_NO_FREE;
}
}
}
else
{ //到下一頁
cur_page++;
}
//資料位址增加
pdat += PAGE_SIZE;
}
}
return FTL_EOK;
}
//讀出一個扇區
uint8_t ftl_read_sector(uint8_t *buf, uint32_t count)
{
uint8_t *pdat = buf;
while (count > 0)
{
//參數異常
if (cur_physical >= BLOCK_NUM){return FTL_ERROR;}
//讀出區域資料
pdrv->read_page(cur_physical, cur_page, pdat, NULL);
LOG_D("ftl read pass, logical %d, physical %d, page %d\r\n", cur_logical, cur_physical, cur_page);
count--;
//還有後續資料需要讀出
if (count > 0)
{ //已經是實體塊最後一頁
if ((cur_page + 1) >= PAGE_NUM)
{ //邏輯塊+1, 轉成實際實體塊
cur_logical++;
if (block_log2phy(&cur_physical, cur_logical))
{ //沒有找到,申請一個新塊
if (select_free_block(&cur_physical, cur_logical, 0))
{ //申請失敗
return FTL_ERROR;
}
}
}
else
{ //到下一頁
cur_page++;
}
//資料位址增加
pdat += PAGE_SIZE;
}
}
return FTL_EOK;
}
uint32_t ftl_get_sector_count(void)
{
return (sector_max);
}
uint32_t ftl_get_sector_size(void)
{
return (PAGE_SIZE);
}
uint32_t ftl_is_init(void)
{
return 0;
}
#if 0
uint32_t fatfs_ftl_lock(void)
{
}
uint32_t fatfs_ftl_unlock(void)
{
}
uint32_t fatfs_ftl_ready(void)
{
}
uint32_t udisk_ftl_lock(void)
{
}
uint32_t udisk_ftl_unlock(void)
{
}
uint32_t udisk_ftl_ready(void)
{
}
#endif
//掃描全部實體塊
static void scan_block(void)
{
uint16_t v_bad, v_garbage, v_free, v_active, v_abnormal;
uint32_t readerr = 0;
//掃描全部BLOCK,産生轉換表
v_bad = 0;
v_garbage = 0;
v_free = 0;
v_active = 0;
v_abnormal = 0;
for (uint16_t i = 0; i < BLOCK_NUM; i++)
{ //讀取OOB資訊
if (pdrv->read_page(i, 0, NULL, p_oobbuf))
{ //讀取錯誤
readerr++;
}
if (OOB_GET_BAD != 0xffff)
{ //損壞塊
p_lutbuf[i] = BLOCK_BAD;
v_bad++;
}
else if (OOB_GET_GARBAGE != 0xffff)
{ //垃圾塊
p_lutbuf[i] = BLOCK_GARBAGE;
v_garbage++;
}
else if (OOB_GET_LOGICAL == 0xffff)
{ //空閑塊
p_lutbuf[i] = BLOCK_FREE;
v_free++;
}
else if (OOB_GET_LOGICAL < BLOCK_NUM)
{ //活動塊
p_lutbuf[i] = OOB_GET_LOGICAL;
v_active++;
}
else
{ //異常塊
p_lutbuf[i] = BLOCK_ABNORMAL;
v_abnormal++;
}
}
LOG_D("ftl nand scan complete, read error - %d\r\n", readerr);
LOG_D("block bad: %d\r\n", v_bad);
LOG_D("block garbage: %d\r\n", v_garbage);
LOG_D("block free: %d\r\n", v_free);
LOG_D("block active: %d\r\n", v_active);
LOG_D("block abnormal: %d\r\n", v_abnormal);
sector_max = (v_garbage + v_free + v_active) * PAGE_NUM;
uint32_t tmp1 = (v_garbage + v_free) * PAGE_NUM * PAGE_SIZE / (1024 * 1024);
uint32_t tmp2 = (v_garbage + v_free + v_active) * PAGE_NUM * PAGE_SIZE / (1024 * 1024);
LOG_D("ftl total sector %d, free %dMByte, capacity %dMByte\r\n", sector_max, tmp1, tmp2);
}
//擦除全部實體塊
static void format_block(void)
{
uint16_t i;
LOG_D("ftl format:\r\n");
for (i = 0; i < BLOCK_NUM; i++)
{
if (pdrv->erase_block(i))
{
LOG_D("ftl format block %d fail\r\n", i);
block_mask_bad(i);
}
}
}
uint8_t ftl_init(void)
{
pdrv = (nand_drv_t *)&nand_h27u4g8f;
//晶片初始化
if (pdrv->chip_init()){return 1;}
LOG_D("chip init pass - %08x\r\n", pdrv->id);
scan_block();
return FTL_EOK;
}
#ifdef FINSH_USING_MSH
#include "stdlib.h"
#include "ll_random.h"
static void ftl_msh_function(int argc, char **argv)
{
uint16_t i,j;
uint8_t *buf;
if ((argc == 2)&&(rt_strcmp(argv[1], "lut") == 0))
{
rt_kprintf("ftl lut:\r\n");
for (i = 0; i < BLOCK_NUM / 32; i++)
{
for (j = 0; j < 32; j++)
{
rt_kprintf("%04x ", lut_buf[i * 32 + j]);
}
rt_kprintf("\r\n");
}
}
if ((argc == 2)&&(rt_strcmp(argv[1], "format") == 0))
{
format_block();
}
if ((argc == 2)&&(rt_strcmp(argv[1], "scan") == 0))
{
scan_block();
}
if ((argc == 2)&&(rt_strcmp(argv[1], "garbage") == 0))
{
collect_garbage_block(0, 0);
}
if ((argc == 3)&&(rt_strcmp(argv[1], "read") == 0))
{
buf = rt_malloc_align(PAGE_SIZE, 4);
rt_memset(buf, 0xff, PAGE_SIZE);
if (ftl_open(atoi(argv[2])) == FTL_EOK)
{
ftl_read_sector(buf, 1);
ftl_close();
}
for (i = 0; i < PAGE_SIZE / 64; i++)
{
for (j = 0; j < 64; j++)
{
rt_kprintf("%02x ", buf[i * 64 + j]);
}
rt_kprintf("\r\n");
}
rt_free_align(buf);
}
if ((argc == 3)&&(rt_strcmp(argv[1], "write") == 0))
{
buf = rt_malloc_align(PAGE_SIZE, 4);
rt_memset(buf, 0xff, PAGE_SIZE);
ll_random_generate((uint32_t *)buf, PAGE_SIZE / 4);
if (ftl_open(atoi(argv[2])) == FTL_EOK)
{
ftl_write_sector(buf, 1);
ftl_close();
}
rt_free_align(buf);
}
}
MSH_CMD_EXPORT_ALIAS(ftl_msh_function, ftl, ftl msh function);
#endif
測試
總結
很多時候,除非你找到的是,商用的很成熟的代碼和工具。
如果是不知名到第三方,總是比較被動,或者到處都是坑,一旦發現問題的時候,也很難去解決
是以,要學會耐心啃一些協定,多看一些晶片資料,搞懂裡面的原理和機制,有了自己的了解之後,就可以寫出一個适合需求,又放心使用的驅動了。