# MS-RTOS NOR Flash 驱动开发

本章将介绍 MS-RTOS NOR Flash 驱动开发及测试。

NOR Flash 根据数据传输的位数可以分为并行(Parallel)NOR Flash和串行(SPI)NOR Flash(即SPI Flash)。SPI NOR Flash每次传输一个bit位的数据,parallel NOR Flash每次传输多个bit位的数据(有x8和x16bit两种);SPI Flash便宜,接口简单点,但速度慢。

NAND Flash 具有较快的抹写时间,而且每个存储单元的面积也较小,这让NAND Flash相较于NOR Flash具有较高的存储密度与较低的每比特成本。同时它的可抹除次数也高出NOR Flash十倍。然而NAND Flash 的I/O接口并没有随机存取外部地址总线,它必须以区块性的方式进行读取,NAND Flash典型的区块大小是数百至数千比特。NAND Flash非常适合用于储存卡之类的大量存储设备。

eMMC (Embedded Multi Media Card) 为MMC协会所订立的,eMMC 相当于 NandFlash+主控IC ,对外的接口协议与SD、TF卡一样,主要是针对手机或平板电脑等产品的内嵌式存储器标准规格。eMMC的一个明显优势是在封装中集成了一个控制器,它提供标准接口并管理闪存,使得手机厂商就能专注于产品开发的其它部分,并缩短向市场推出产品的时间。

# 1. NOR Flash 基础知识

# 1.1 NOR Flash 接口

NOR Flash 根据数据传输的位数可以分为并行(Parallel)NOR Flash和串行(SPI)NOR Flash(即SPI Flash),接下来我们主要讲解 SPI Flash 的相关内容。

NOR Flash_interface

FLASH芯片(型号:W25Q128)是一种使用SPI通讯协议的NOR FLASH存储器,它的CS/CLK/DIO/DO引脚分别连接到了STM32对应的SDI引脚NSS/SCK/MOSI/MISO上,其中STM32的NSS引脚是一个普通的GPIO,不是SPI的专用NSS引脚,所以程序中我们要使用软件控制的方式。

FLASH芯片中还有WP和HOLD引脚。WP引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。

# 1.2 NOR Flash 读写

主机首先通过MOSI线向FLASH芯片发送第一个字节数据为"9F h",当FLASH芯片收到该数据后,它会解读成主机向它发送了"JEDEC指令",然后它就作出该命令的响应:通过MISO线把它的厂商ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备ID来测试硬件是否连接正常,或用于识别设备。

NOR Flash_read_id

在向FLASH芯片存储矩阵写入数据前,首先要使能写操作,通过"Write Enable"命令即可写使能。我们只关注这个状态寄存器的第0位"BUSY",当这个位为"1"时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行"擦除"或"数据写入"的操作。利用指令表中的"Read Status Register"指令可以获取FLASH芯片状态寄存器的内容。

NOR Flash_status

通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的FLASH芯片支持"扇区擦除"、"块擦除"以及"整片擦除"。扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送"写使能"指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。

NOR Flash_erase

目标扇区被擦除完毕后,就可以向它写入数据了。与EEPROM类似,FLASH芯片也有页写入命令,使用页写入命令最多可以一次向FLASH传输256个字节的数据,我们把这个单位为页大小。

NOR Flash_write

相对于写入,FLASH芯片的数据读取要简单得多,使用读取指令"Read Data"即可。发送了指令编码及要读的起始地址后,FLASH芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH芯片就会一直返回数据。

NOR Flash_read

# 2. NOR Flash 驱动框架

# 2.1 驱动相关数据结构

(1)ms_io_device_t

在 MS-RTOS 中,在注册快设备驱动时,我们只需要注册一个驱动为“null”的设备,然后实现一个文件系统底层读写操作函数集的结构体(由具体使用的文件系统决定),并在注册设备时将该结构体作为设备的私有数据传入。

/*
 * Private information of chip
 */
typedef struct {
    char              *chip_name;
    ms_uint32_t        id;
    ms_uint8_t         en_4byte_addr;

    ms_size_t          size;
    ms_size_t          sector_size;
    ms_size_t          page_size;

    const ms_chip_cmd *cmds;
} ms_chip_info;

/*
 * Provide to litterfs for data transmission
 */
typedef struct {
    const ms_chip_info *cur_chip_info;
    ms_spi_device_t     spi_dev;
} privinfo_t;

/*
 * nor flash infomation
 */
typedef struct {
    ms_io_device_t     dev;
    privinfo_t         priv;
} ms_nor_dev_t;

以 SPI Flash 为例,我们将 SPI Flash 挂到 LittleFS ,则在注册 Flash 设备时,使用如下代码:

ms_io_device_register(&dev->dev, NOR Flash_port->dev_path, "ms_null", lfs_cfg);

(2)struct lfs_config

在 MS-RTOS 中,将一个块设备以指定的文件系统挂载到指定挂载点时,调用 ms_io_mount 来进行挂载操作。以 SPI Flash 为例,我们将 SPI Flash 挂到 LittleFS ,则使用如下代码:

struct lfs_config {
    // Read a region in a block. Negative error codes are propogated
    // to the user.
    int (*read)(const struct lfs_config *c, lfs_block_t block,
            lfs_off_t off, void *buffer, lfs_size_t size);

    // Program a region in a block. The block must have previously
    // been erased. Negative error codes are propogated to the user.
    // May return LFS_ERR_CORRUPT if the block should be considered bad.
    int (*prog)(const struct lfs_config *c, lfs_block_t block,
            lfs_off_t off, const void *buffer, lfs_size_t size);

    // Erase a block. A block must be erased before being programmed.
    // The state of an erased block is undefined. Negative error codes
    // are propogated to the user.
    // May return LFS_ERR_CORRUPT if the block should be considered bad.
    int (*erase)(const struct lfs_config *c, lfs_block_t block);

    // Sync the state of the underlying block device. Negative error codes
    // are propogated to the user.
    int (*sync)(const struct lfs_config *c);
    
    .......
};

lfs_cfg->context        = &dev->priv;
lfs_cfg->read_size      =  1U;
lfs_cfg->prog_size      =  priv->cur_chip_info->page_size;
lfs_cfg->block_size     =  priv->cur_chip_info->sector_size;
lfs_cfg->block_count    =  priv->cur_chip_info->size / priv->cur_chip_info->sector_size;
lfs_cfg->cache_size     =  priv->cur_chip_info->page_size;
lfs_cfg->block_cycles   =  500U;
lfs_cfg->lookahead_size =  8U * ((priv->cur_chip_info->size 
                                  / priv->cur_chip_info->sector_size + 63U) / 64U);
lfs_cfg->read           =  __NOR Flash_block_read;
lfs_cfg->prog           =  __NOR Flash_block_prog;
lfs_cfg->erase          =  __NOR Flash_block_erase;
lfs_cfg->sync           =  __NOR Flash_block_sync;

ms_io_mount(NOR Flash_port->mount_path,  NOR Flash_port->dev_path, 
            MS_LITTLEFS_NAME, MS_NULL);

# 2.2 驱动的注册和卸载

(1)Flash 驱动开发流程:

搞定 SPI 的基本收发单元后,还需要了解如何对 Flash 芯片进行读写。Flash 芯片自定义了很多指令,我们通过控制 SPI 总线向 Flash 芯片发送指令,Flash 芯片收到后就会执行相应的操作。

  • 获取必要的软硬件开发资源,了解设备的基本特性;
  • 确定 Flash 的型号,存储结构体和容量等信息;
  • 熟悉 Flash 芯片指令表,找到主要的读写指令并封装成函数接口;
  • 读取 Flash 芯片的 ID,确定 SPI 总线和 Flash 能正常通信;
  • 实现文件系统需要的底层操作接口;
  • 编写测试程序,对读写数据进行校验;

(2)驱动的注册和卸载接口:

ms_err_t ms_io_driver_register(ms_io_driver_t *drv);
ms_err_t ms_io_driver_unregister(ms_io_driver_t *drv); //此接口暂不开放

(3)设备节点的注册和卸载接口:

ms_err_t ms_io_device_register(ms_io_device_t *dev, const char *dev_path, 
                               const char *drv_name, ms_ptr_t ctx);
ms_err_t ms_io_device_unregister(ms_io_device_t *dev);

# 2.3 NOR Flash 驱动示例

NOR Flash 驱动示例,仅作为参考。

#define __MS_IO
#include "ms_config.h"
#include "ms_rtos.h"
#include "ms_io_core.h"
#include "includes.h"
#include "ms_littlefs.h"

/*
 * Read a region in a block. Negative error codes are propogated to the user.
 */
static int __spi_nor_block_read(const struct lfs_config *c, lfs_block_t block,
                                   lfs_off_t off, void *buffer, lfs_size_t size)
{
    int ret;

    if (BSP_QSPI_Read((uint8_t *)buffer, 
                      (block * c->block_size + off), size) == QSPI_OK) {
        ret = LFS_ERR_OK;
    } else {
        ret = LFS_ERR_CORRUPT;
    }

    return ret;
}

/*
 * Program a region in a block. The block must have previously
 * been erased. Negative error codes are propogated to the user.
 * May return LFS_ERR_CORRUPT if the block should be considered bad.
 */
static int __spi_nor_block_prog(const struct lfs_config *c, lfs_block_t block,
                                   lfs_off_t off, const void *buffer, lfs_size_t size)
{
    int ret;

    if (BSP_QSPI_Write((uint8_t *)buffer, 
                       (block * c->block_size + off), size) == QSPI_OK) {
        ret = LFS_ERR_OK;
    } else {
        ret = LFS_ERR_CORRUPT;
    }

    return ret;
}

/*
 * Erase a block. A block must be erased before being programmed.
 * The state of an erased block is undefined. Negative error codes
 * are propogated to the user.
 * May return LFS_ERR_CORRUPT if the block should be considered bad.
 *
 */
static int __spi_nor_block_erase(const struct lfs_config *c, lfs_block_t block)
{
    int ret;

    if (BSP_QSPI_Erase_Block(block * c->block_size) == QSPI_OK) {
        ret = LFS_ERR_OK;
    } else {
        ret = LFS_ERR_CORRUPT;
    }

    return ret;
}

/*
 * Sync the state of the underlying block device. Negative error codes
 * are propogated to the user.
 */
static int __spi_nor_block_sync(const struct lfs_config *c)
{
    return 0;
}

/*
 * configuration of the filesystem is provided by this struct
 */
static struct lfs_config spi_nor_cfg = {
    /*
     * block device operations
     */
    .read  = __spi_nor_block_read,
    .prog  = __spi_nor_block_prog,
    .erase = __spi_nor_block_erase,
    .sync  = __spi_nor_block_sync,
};

/*
 * Create spi nor flash device file and mount
 */
ms_err_t spi_nor_dev_init(const char *path, const char *mnt_path)
{
    static ms_io_device_t spi_nor_dev;
    QSPI_Info info;
    ms_err_t err;

    if (BSP_QSPI_Init() == QSPI_OK) {
        if (BSP_QSPI_GetInfo(&info) == QSPI_OK) {
            spi_nor_cfg.read_size      = 1U;
            spi_nor_cfg.prog_size      = info.ProgPageSize;
            spi_nor_cfg.block_size     = info.EraseSectorSize;
            spi_nor_cfg.block_count    = info.EraseSectorsNumber;
            spi_nor_cfg.cache_size     = info.ProgPageSize;
            spi_nor_cfg.block_cycles   = 500U;
            spi_nor_cfg.lookahead_size = 8U * ((spi_nor_cfg.block_count + 63U) / 64U);

            err = ms_io_device_register(&spi_nor_dev, path, "ms_null", &spi_nor_cfg);
            if (err == MS_ERR_NONE) {
                err = ms_io_mount(mnt_path, path, MS_LITTLEFS_NAME, MS_NULL);
            }
            
        } else {
            err = MS_ERR;
        }
    } else {
        err = MS_ERR;
    }

    return err;
}

# 3. 文件读写应用

# 3.1 文件系统读写

#include <ms_rtos.h>
#include <string.h>
#include "test/include/greatest.h"

/*
 * Marco definition area
 */
#define TEST_FILE_PATH1         "/nor/1.txt"
#define TEST_FILE_PATH2         "/nor/2.txt"
#define TEST_FILE_PATH3         "/nor/3.txt"
#define TEST_DIR_PATH           "/nor/workspace"
#define TEST_LINK_FILE          "/nor/link_file"
#define TEST_BUFFER_SIZE        (64)
#define TEST_FILE_SIZE          (4*1024)

/*
 * test_file_read
 */
int main(int argc, char *argv[])
{
    int         fd;
    ms_ssize_t  ret;
    ms_uint8_t  read_buf[10];
    ms_uint8_t  write_buf[10] = {11, 12, 13, 14, 15, 16, 17, 18, 19, 20};

    fd = ms_io_open(TEST_FILE_PATH2, O_CREAT | O_RDWR, 0666);
    if (fd < 0) {
        ms_printf("[error]: open file %s failed!\n", TEST_FILE_PATH2);
        return  (-1);
    }

    ret = ms_io_write(fd, write_buf, sizeof(write_buf));
    if (ret != 10) {
        ms_printf("[error]: write failed! errno = %d\n", errno);
        ms_io_close(fd);
        return  (-1);
    }

    ret = ms_io_lseek(fd, 0, SEEK_SET);
    if (ret != 0) {
        ms_printf("[error]: lseek failed! errno = %d\n", errno);
        ms_io_close(fd);
        return  (-1);
    }

    ret = ms_io_read(fd, read_buf, sizeof(read_buf));
    if (ret != 10) {
        ms_printf("[error]: read failed! errno = %d\n", errno);
        ms_io_close(fd);
        return  (-1);
    }

    if (memcmp(write_buf, read_buf, sizeof(read_buf)) != 0) {
        ms_io_close(fd);
        return  (-1);
    }

    ms_io_close(fd);

    return  (0);
}

# 3.2 已支持的文件系统

MS-RTOS 针对各种储存介质和需求提供了丰富的文件系统支持:

文件系统 面向的储存介质 特性
devfs 设备文件系统
MS-FLASHFS MCU 内部 FLASH 掉电安全,用于存放 APP 镜像和启动参数文件,支持 APP XIP
fatfs SD卡、U盘 FAT 文件系统,PC 交换数据便利,开源免费
littlefs NOR FLASH 掉电安全、磨损平衡,开源免费
yaffs NAND FLASH 掉电安全、磨损平衡、坏块管理,十分成熟,商用收费
uffs NAND FLASH 掉电安全、磨损平衡、坏块管理,内存占用较 yaffs 少,开源免费
edgefs SD卡、U盘 掉电安全,商用收费

# 附录(Appendix)

1. Reference

SPI 读写 Flash:https://blog.csdn.net/flyleaf91/article/details/52325513

2. FAQ

(1)简述 FLASH 存储器与 EEPROM 存储器的区别?

  • 首先从IO引脚占用方面比较,EEPROM只需占用两个IO引脚,时钟(clk)和数据(data)引脚,外加 电源三个引脚即可,符合I2C通讯协议。而FLASH需要占用更多IO引脚,有并行和串行的,串行的需要一个 片选(cs)引脚(可用作节电功耗控制),一个时钟(clk)引脚,FLASH读出和写入引脚各一个,也就是 四个。并行的需要8个数据引脚,当然比串行的读写速度要快。

  • 从功能方面比较,EEPROM可以单字节读写,FLASH部分芯片只能以块方式擦除(整片擦除),部 分芯片可以单字节写入(编程),一般需要采用块写入方式;FLASH比EEPROM读写速度更快,可靠性更高。但 比单片机片内RAM的读写还要慢。

  • 价格方面比较,FLASH应该要比EEPROM贵。