對nand flash設備進行升級

前言

這段時間在給板子開發一個升級的功能,板子的Flash使用的是nand flash,使用mtd去管理分區。在正式開始講升級部分之前,我們先了解一下nand flash和mtd的基本知識,最後我還會說一下怎麼升級ubi格式的rootfs分區。

正文

1、nand flash

參考鏈接:https://blog.csdn.net/lee_jimmy/article/details/82084241

想要給nand flash進行升級,我們當然需要了解nand flash的基本特性了。雖然每個廠家的nand flash性能會有差異,但是基本結構是差不多的。比如我手上的這款nand flash(TC58BVG1S3HTAI0),基本情況如下:
                                                      x8
Memory cell array          2112 × 128K ×8
Register                          2112 ×8
Page size                      2112 bytes
Block size                     (128K + 4K) bytes

意思就是一個page爲2112 bytes = 2k + 64 bytes,其中64 bytes爲OOB的大小,因爲nand flash會發生位反轉,所以OOB一般用來做壞塊校驗;每個block爲(128K + 4K) bytes,即由64個 page組成。其實block是個虛擬的概念,目的是爲了更好的管理存儲空間而已,只有page是真實存在的;另外,nand flash還有一個重要的特性,每一位只能從1寫爲0,不能從0寫爲1。正因爲這個特點,所以在刷寫分區之前,我們都需要將分區擦除一遍,將每一位都刷成1,才能繼續寫入我們的升級包。

2、MTD基礎知識

參考鏈接:https://www.cnblogs.com/pengdonglin137/p/3467960.html

MTD,Memory Technology Device即內存技術設備,對於用到nand Flash的Linux開發板來說,一般都通過mtd驅動來管理分區。

相信很多同學用過dd命令來給自己的開發板升級過bootloader或者kernel,比如EMMC的kernel分區就可以通過dd命令將數據寫入到塊設備節點來升級:dd if=boot.img of=/dev/block/boot。mtd驅動同時提供了字符設備節點和塊設備節點給上層去讀寫分區數據:

字符設備節點:/dev/mtd0
塊設備節點:/dev/mtdblock0

不過我這次要介紹的升級方法,不是直接通過dd命令或者其它命令去讀寫節點,而是利用mtd提供的ioctl接口去升級。這麼做的原因有兩個:
(1)一套正式的升級程序不應該是上層應用直接用命令的方式,而應該將底層的升級程序封裝成接口給上層應用去調用
(2)在底層的升級接口中,我們可以根據需求靈活地定製不同的升級流程
詳細的升級接口我們會在下一節講解,這裏先介紹一下mtd驅動中幾個重要的數據結構:

(1)mtd_info

結構體的定義在include\uapi\mtd\mtd-abi.h:

struct mtd_info_user {
    __u8 type;
    __u32 flags;
    __u32 size;    /* Total size of the MTD */
    __u32 erasesize;
    __u32 writesize;
    __u32 oobsize;    /* Amount of OOB data per block (e.g. 16) */
    __u64 padding;    /* Old obsolete field; do not use */
};

(2)mtd字符設備接口

通過一系列ioctl命令可以獲取Flash設備信息、擦除Flash、讀寫NAND 的OOB、獲取OOB layout 及檢查NAND 壞塊等(MEMGETINFO、MEMERASE、MEMREADOOB、MEMWRITEOOB、MEMGETBADBLOCK) 。我們的升級接口程序就是通過一系列的ioctl接口來實現。
定義同樣在:include\uapi\mtd\mtd-abi.h

3、升級接口講解

有了前兩小節的知識,我們正式開始講解升級接口程序。升級程序主要分爲兩大類:
(1)擦除對應的分區並標記壞塊(擦除最小單位爲block)
(2)寫數據到分區(寫入單位爲page)

3.1、擦除接口

static int mtd_erase(const struct mtd_info_user *info, int fd, int eb)
{
    int ret = 0;
    struct erase_info_user ei;

    /*
     * ei.start:擦除的起始偏移
     * ei.length:擦除的長度
     */
    ei.start = eb * info->erasesize;
    ei.length = info->erasesize;
    /*
     * MEMERASE:擦除mtd分區的ioctl number
     * ei:賦值後的struct erase_info_user
     */
    ret = ioctl(fd, MEMERASE, &ei);
    if(ret != 0)
        printf(1,"MEMERASE error:error reason = %s",strerror(errno));

    return ret;
}

int ufs_mtd_erase(int fd)
{
    int ret = 0;

    /* 定義mtd_info結構體 */
    struct mtd_info_user info;
    /* fd:對應分區的字符設備節點文件描述符
     * MEMGETINFO:獲取mtd設備分區信息的ioctl number
     * info:保存返回結果賦值
     */
    ret = ioctl(fd, MEMGETINFO, &info);
    if (ret != 0)
        return ret;

    unsigned int eb, eb_cnt;
    uint64_t offset = 0;
    /*
     * size:mtd分區的大小,一般比實際需要寫入的數據大一定值
     * erasesize:一次擦除的大小,一般爲nand Flash的block大小
     * eb_cnt:要擦除的次數
     */
    eb_cnt = info.size / info.erasesize;

    for (eb = 0; eb < eb_cnt; eb++) {
        offset = (uint64_t)eb * info.erasesize; /* 擦除的起始偏移 */
        int ret = mtd_is_bad(&info, fd, eb); /* 3.2小節 */
        if (ret > 0) /* 是壞塊則不擦除,跳過 */
            continue;
        else if (ret < 0) /* 無法判斷,直接出錯退出 */
            return -1;

        /* 實際擦除動作在mtd_erase函數 */
        if (mtd_erase(&info, fd, eb) != 0) {
            /* 3.3小節 */
            ret = mtd_set_bad(&info, fd, eb);
            if(ret) /* 無法標記爲壞塊則直接退出 */
                return -1;
        }
    }

    return ret;
    
}

3.2、判斷壞塊接口

static int mtd_is_bad(const struct mtd_info_user *info, int fd, int eb)
{
    int ret = 0;
    loff_t seek;

    /*
     * seek:block的起始偏移
     * MEMGETBADBLOCK:判斷是否爲壞塊的ioctl number
     */
    seek = (loff_t)eb * info->erasesize;
    ret = ioctl(fd, MEMGETBADBLOCK, &seek);

    return ret;
}

3.3、標記壞塊接口

static int mtd_set_bad(const struct mtd_info_user *info, int fd, int eb)
{
    int ret = 0;
    loff_t seek;

    /*
     * seek:壞塊的起始偏移
     * MEMSETBADBLOCK:標記壞塊的ioctl number
     */
    seek = (loff_t)eb * info->erasesize;
    ret = ioctl(fd, MEMSETBADBLOCK, &seek);

    return ret;
}

 3.4、寫分區接口

ssize_t ufs_mtd_writenooob(int fd, char *buf, size_t count)
{
    int ret = 0;
    
    /* points to the current page */
    char *writebuf = NULL;
    char *pblock=NULL;

    struct mtd_info_user info;
    ret = ioctl(fd, MEMGETINFO, &info);
    if (ret != 0)
        return ret;

    int writesize = info.writesize; /* 每次寫的數據大小,一般爲一頁 */
    int oob_size = info.oobsize; /* OOB的大小 */
    int eb_size = info.erasesize;
    int size = info.size;
    int  Leftpagesize= 0;
    uint64_t offset = 0;
    unsigned int eb, eb_cnt,i;
    eb_cnt = info.size / info.erasesize; /* 一共多少個block */
    long long checkstart = 0;
    long long blockstart = lseek(fd, 0, SEEK_CUR); /* 將文件讀寫指針移到文件開始 */
    pblock = buf;
    /* count值一般等於一個block的大小 */
    if(count >eb_size)
        DEBUG(1,"param error ,write size is bigger than one block");

    /*
     * 因爲每次都是一頁一頁的寫數據,所以如果寫入的分區鏡像大小不滿足頁對齊的話,需要將其補齊,並將數據內容填充爲0
     */
    if((count%writesize) !=0)
    {
        Leftpagesize= writesize-(count %writesize);
        for(i=0;i<Leftpagesize;i++) /* 將內存最後一個塊也填充爲00 */
        {
            buf[count+i] = 0x00;
        }
    }
    count=count+Leftpagesize; /* 頁對齊後的大小 */

    /*
     * 跳過壞塊,不能寫入
     */
    for (offset = blockstart; offset < size; offset += eb_size)
    {
        ret = mtd_is_bad(&info, fd, offset / eb_size);
        if (ret > 0) {  /* 如果是壞塊,則不寫入 */
            continue;
        } else if (ret < 0) {
            return -1;
        } else {
            if (lseek(fd, offset, SEEK_SET) != offset)
                return -1;
            blockstart = (long long)offset; /* 找到了不是壞塊的地方,設置爲開始寫入的偏移 */
            break;
        }
    }
    checkstart = blockstart;
    int pagelen = writesize + oob_size;

    char *data = malloc(pagelen);
    if (!data)
    {
        errno = ENOMEM;
        return -1;
    }
    memset(data, 0xff, pagelen);

    /* 每次寫一個page,大小爲writesize */
    for (offset = 0; offset < count; offset += writesize)
    {
        writebuf = buf + offset; /* 指向寫入數據的開始地址 */

        /* 將數據寫入到對應的字符設備節點文件描述符 */
        ret = write(fd, writebuf, writesize);
        if (ret != writesize)
        {
            DEBUG(1,"cannot write %d bytes to mtd, offset %lld)",
                    count, blockstart);
            goto free_data;
        }
        
        blockstart += writesize;
    }
    
    /*
     * 爲了避免寫入數據有誤,我們這裏可以增加一個校驗接口,也就是調用read函數讀取回來數據,再和源數據比較是否一致。
     * 爲了代碼流程簡潔,我就不給出了檢驗接口了
     */

    free(data);
    ret = count;

    return ret;

free_data:    
    if(data)    
        free(data);
    return -1;

}

/**************************************************************************
* 函數名稱: ufs_writenooob
* 功能描述:不帶oob的鏡像寫入flash(數據非頁對齊時進行填充)
* 參數說明:int fd                設備句柄
*                   char *buf            鏡像指針
*                   int img_size         升級的目標鏡像大小
* 返 回 值:正確:返回img_size,寫入flash的數據大小
                  否則返回錯誤值,錯誤代碼定義見頭文件
*函數說明:
***************************************************************************/
ssize_t ufs_writenooob(int fd, char *buf, size_t img_size)
{
    int off;
    int ret;
    int op_size = 0;
    int cnt;
    int mtdsize = 0;
    ret = ioctl(fd, MEMGETINFO, &info);
    if (ret != 0)
        return ret;
    op_size = info.erasesize; /* 擦除大小 */
    mtdsize = info.size; /* mtd分區大小 */

    if(mtdsize<img_size) /* 分區比升級包還小肯定要出錯 */
        return -1;    
    
    /*
     * 每次一個block一個block的操作
     */
    for(off = 0; off < img_size; off += op_size)
    {
        if((img_size-off)<op_size)
            op_size = img_size-off;

        /* 實際寫函數 */
        cnt = ufs_mtd_writenooob(fd, buf, op_size);
        if(cnt <0)
            return -1;
        buf =buf+op_size;
    }
    return img_size;    
}

4、ubi格式rootfs鏡像製作、燒錄及分區掛載

參考鏈接:https://blog.csdn.net/hktkfly6/article/details/46961407

4.1、鏡像燒錄

前幾節我們提供的升級接口,其實都是針對裸設備的,何爲裸設備?就是沒有文件系統的,比如uboot和kernel分區都是不帶文件系統的。但是一般的開發板在kernel起來後都會掛載一個根文件系統,也就是我們熟悉的rootfs,裏面會跑各種的應用程序。對於帶文件系統的分區,我們就不能用上面的升級接口了。
我手上開發板的根文件系統是ubi格式的,針對ubi格式的分區,Linux有特定的命令去燒錄:

# ubiformat -h
ubiformat version 1.5.1 - a tool to format MTD devices and flash UBI images

Usage: ubiformat <MTD device node file name> [-s <bytes>] [-O <offs>] [-n]
                        [-Q <num>] [-f <file>] [-S <bytes>] [-e <value>] [-x <num>] [-y] [-q] [-v] [-h]
                        [--sub-page-size=<bytes>] [--vid-hdr-offset=<offs>] [--no-volume-table]
                        [--flash-image=<file>] [--image-size=<bytes>] [--erase-counter=<value>]
                        [--image-seq=<num>] [--ubi-ver=<num>] [--yes] [--quiet] [--verbose]
                        [--help] [--version]

Example 1: ubiformat /dev/mtd0 -y - format MTD device number 0 and do
           not ask questions.
Example 2: ubiformat /dev/mtd0 -q -e 0 - format MTD device number 0,
           be quiet and force erase counter value 0.

而我自己用到的命令就是:

ubiformat -y /dev/mtd4 -f rootfs.ubi

其中,/dev/mtd4代表根文件系統分區的字符設備節點,rootfs.ubi就是我自己製作出來的ubi格式的燒錄鏡像。當然,實際升級程序我們肯定不是用這個命令,但是我們也只需要將ubiformat命令的源碼中的相應接口拿出來用就好了,這裏我就不細說了。

4.2、鏡像製作

4.2.1、製作rootfs.ubifs鏡像

製作鏡像的命令:

mkfs.ubifs -d /jimmy/buildroot/output/target -e 0x1f000 -c 975 -m 2048 -v -o rootfs.ubifs

以上命令的含義爲:
-d:將/jimmy/buildroot/output/target文件夾製作爲UBIFS文件系統鏡像
-o:輸出的鏡像名爲rootfs.ubifs
-m:參數指定了最小的I/O操作的大小,也就是NAND FLASH一個page的大小
-e:參數指定了邏輯擦除快的大小
-c:指定了最大的邏輯塊號 

但是上面的命令製作出來的鏡像是不能通過ubiformat進行燒錄的,還需要下面一步:

ubinize -o rootfs.ubi -m 2048 -p 0x20000 -s 2048 ubinize.cfg

以上命令的含義爲:
-o:輸出的鏡像文件,也算最後用ubiformat命令能燒錄的文件
-m:參數指定了最小的I/O操作的大小,也就是NAND FLASH一個page的大小
-p:nand Flash的物理擦除塊大小,可以通過命令查看,第3列的值就是
# cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00200000 00020000 "bootloader"
-s:指定子頁大小,當爲nor flash時,此值應指定爲1,當爲nand flash時需指定此值爲nand flash的子頁大小
ubinize.cfg:配置文件,制定了一些參數

[ubifs]
mode=ubi
vol_id=0
vol_type=dynamic
vol_name=rootfs
vol_alignment=1
vol_flags=autoresize
image=rootfs.ubifs #輸入的鏡像文件

通過上面製作出來的rootfs.ubi鏡像就可以直接用ubiformat來升級分區了。

4.3、分區掛載

我在rootfs.cpio的/init腳本中進行掛載root分區:

if [ -c "/dev/ubi_ctrl" ]; then
    ubiattach /dev/ubi_ctrl -m 4
    if [ $? -ne 0]; then
        ubiattach /dev/ubi_ctrl -m 5
    fi
    mount -t ubifs /dev/ubi0_0 /mnt
fi

我有兩個rootfs分區,分別對應/dev/mtd4和/dev/mtd5,先嚐試掛載/dev/mtd4,如果失敗了再掛載備份的/dev/mtd5

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章