对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

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: NAND flash是一种非易失性存储器。在升级STM32时,需要利用NAND flash来存储新的固件,以便将其读入设备中执行更新。 通常,首先需要将新的固件烧录到NAND flash中。这可以通过连接NAND控制器来完成。然后,采用硬件或软件方法将NAND flash中的固件写入到STM32的闪存中。 在stm32设备中,可以使用bootloader来完成NAND flash更新。bootloader是一种特殊程序,可以在正常启动过程之前运行,以便在不破坏旧的系统软件的情况下更新新内容。 使用bootloader下载NAND flash中的固件的过程通常需要使用特殊程序或器件(例如JTAG调试器或USB转串口)来连接STM32设备和计算机。 连接后,可以使用相应的软件工具来加载固件,它会将固件写入到NAND flash中。然后,bootloader读取并将其复制到设备的闪存中。 总的来说,更新STM32设备的固件需要使用NAND flash来存储新的固件,并使用bootloader来执行升级过程。在升级时需要注意保持设备的电源稳定并避免中途操作中断,以免影响设备的正常运行。 ### 回答2: NAND Flash是一种常见的存储器件,而STM32是一种常用的微控制器。要使用NAND Flash升级STM32,首先需要了解它们各自的工作原理和操作方法。 NAND Flash是通过串行方式进行数据读写的存储器件,它适合用于大容量数据的存储。而STM32则是一种嵌入式微控制器,可以控制各种外设和执行指令等操作。在使用NAND Flash升级STM32时,需要注意以下几点: 1. 选择正确的NAND Flash芯片和驱动程序: 首先需要了解STM32所使用的芯片的规格和参数,然后选择一款兼容的NAND Flash芯片和相应的驱动程序。 2. 连接NAND Flash和STM32: 接下来,需要将NAND Flash芯片和STM32连接起来,通常可以通过SPI或者SDIO等接口进行连接。 3. 编写相应的程序: 根据芯片和驱动程序的要求编写相应的程序,并进行测试,以确保升级操作的正常进行。 4. 对STM32进行升级: 最后,可以使用NAND Flash升级STM32,将新的固件程序写入到STM32内部存储器中,从而实现升级操作。 总之,使用NAND Flash升级STM32需要具备一定的专业技能和知识,并且需要注意相关细节和操作步骤,以确保操作的成功和安全性。 ### 回答3: NANDFlash是一种高速存储设备,采用闪存芯片,因此我们可以用它来升级STM32。 升级STM32需要使用Bootloader,可以通过STM32CUBE Programmer软件来实现。首先,我们需要将升级文件存储在NANDFlash中。接下来,将STM32与计算机连接,确保Bootloader已启动。然后,打开STM32CUBE Programmer软件,选择正确的微控制器芯片类别和通信端口,将NANDFlash连接到计算机上,并选择升级文件所在的路径。开始升级后进度条上会有进度显示,当升级完成时,会显示升级成功的提示。总之,NANDFlash升级STM32的一种方便有效的方式,通过Bootloader和STM32CUBE Programmer软件,我们可以快速、简便地完成升级工作,提高STM32的性能和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值