MTD 驱动开发指南
[ English | 简体中文 ]
一、概述
本文档详细指导开发者如何为 openvela 系统适配基于 MTD (Memory Technology Device) 模型的存储设备,如 NOR Flash 和 NAND Flash。您将学习如何实现 MTD 驱动的下半部接口,并将其注册到系统中。
1、前提条件
在开始之前,请确保您已经熟悉 openvela 的存储驱动框架。建议您先阅读存储驱动框架指南。
2、核心数据结构:mtd_dev_s
在 openvela 中,所有 MTD 设备都通过 struct mtd_dev_s 结构体进行抽象。您的主要任务就是实现这个结构体中定义的回调函数,并填充设备信息。
// 定义于 nuttx/include/nuttx/mtd/mtd.h
struct mtd_dev_s
{
// 擦除指定数量的块
int (*erase)(FAR struct mtd_dev_s *dev, off_t startblock, size_t nblocks);
// 按块读取
ssize_t (*bread)(FAR struct mtd_dev_s *dev, off_t startblock,
size_t nblocks, FAR uint8_t *buffer);
// 按块写入
ssize_t (*bwrite)(FAR struct mtd_dev_s *dev, off_t startblock,
size_t nblocks, FAR const uint8_t *buffer);
// 按字节读取 (可选)
ssize_t (*read)(FAR struct mtd_dev_s *dev, off_t offset, size_t nbytes,
FAR uint8_t *buffer);
#ifdef CONFIG_MTD_BYTE_WRITE
// 按字节写入 (可选, 需开启 CONFIG_MTD_BYTE_WRITE)
ssize_t (*write)(FAR struct mtd_dev_s *dev, off_t offset, size_t nbytes,
FAR const uint8_t *buffer);
#endif
// 控制接口,用于获取几何信息、整片擦除等
int (*ioctl)(FAR struct mtd_dev_s *dev, int cmd, unsigned long arg);
// MTD 设备名称
FAR const char *name;
};
二、MTD 驱动开发流程

开发 MTD 驱动下半部通常遵循以下步骤:
- 实现设备操作接口:根据硬件手册,编写
erase,bread,bwrite,ioctl等函数的具体实现。 - 实例化
mtd_dev_s:定义一个静态的mtd_dev_s结构体变量,并将上一步实现的函数指针赋值给它。 - 提供初始化函数:编写一个全局的初始化函数(如
my_chip_mtd_initialize()),该函数返回已实例化的mtd_dev_s结构体指针。 - 注册 MTD 设备:在板级初始化代码中,调用您的初始化函数获取 MTD 设备实例,然后使用
register_mtddriver()将其注册到 VFS 中。 - (可选)创建分区:如果需要,调用
register_mtdpartition()将单个 MTD 设备划分为多个逻辑分区。
三、实现 MTD 下半部接口
本节将以 NOR Flash 和 NAND Flash 为例,介绍接口实现的要点。
1、实现 NOR Flash 驱动
openvela 在 nuttx/drivers/mtd/skeleton.c 提供了一个优秀的 NOR Flash 驱动模板。您可以以此为起点进行开发。
1.1 实例化 mtd_dev_s
首先,定义一个私有设备结构体,并在其中包含一个 mtd_dev_s 实例。然后,创建一个静态全局变量,并为其操作函数集赋予您的实现。
// 示例:定义私有设备结构体
struct my_nor_dev_s {
struct mtd_dev_s mtd;
// 添加其他硬件相关的私有数据,如锁、状态等
};
// 示例:函数实现
static int my_nor_erase(FAR struct mtd_dev_s *dev, off_t startblock, size_t nblocks);
static ssize_t my_nor_bread(FAR struct mtd_dev_s *dev, off_t startblock, size_t nblocks, FAR uint8_t *buf);
// ... 其他函数实现
// 示例:实例化
static struct my_nor_dev_s g_nor_dev = {
.mtd = {
.erase = my_nor_erase,
.bread = my_nor_bread, // 按照page size读取
.bwrite = my_nor_bwrite, // 按照page size写入
.read = my_nor_read, // 按照字节读取
.write = my_nor_write, // 按照字节写入的接口(依赖CONFIG_MTD_BYTE_WRITE)
.ioctl = my_nor_ioctl,
.name = "my_nor_flash"
}
};
1.2. 实现 ioctl 接口
ioctl 是一个关键接口,其中 MTDIOC_GEOMETRY 命令必须实现。您需要在此返回 Flash 的几何信息。
static int my_nor_ioctl(FAR struct mtd_dev_s *dev, int cmd, unsigned long arg) {
switch (cmd) {
case MTDIOC_GEOMETRY:
{
FAR struct mtd_geometry_s *geo = (FAR struct mtd_geometry_s *)arg;
if (geo) {
// 根据您的硬件手册填充这些值
geo->blocksize = 256; // 读/写块大小 (Page Size)
geo->erasesize = 4096; // 擦除块大小 (Sector Size)
geo->neraseblocks = 1024; // 总擦除块数量
return OK;
}
return -EINVAL;
}
case MTDIOC_BULKERASE:
{
// 实现整片擦除逻辑
return OK;
}
// ... 处理其他命令
default:
return -ENOTTY;
}
}
ioctl接口完整定义在nuttx/include/nuttx/mtd/mtd.h
1.3 参考实现
nuttx/drivers/mtd/w25.c
/****************************************************************************
* Name: w25_initialize
*
* Description:
* Create an initialize MTD device instance. MTD devices are not registered
* in the file system, but are created as instances that can be bound to
* other functions (such as a block or character driver front end).
*
****************************************************************************/
FAR struct mtd_dev_s *w25_initialize(FAR struct spi_dev_s *spi)
{
FAR struct w25_dev_s *priv;
int ret;
w25_finfo("spi: %p\n", spi);
/* Allocate a state structure (we allocate the structure instead of using
* a fixed, static allocation so that we can handle multiple FLASH devices.
* The current implementation would handle only one FLASH part per SPI
* device (only because of the SPIDEV_FLASH(0) definition) and so would
* have to be extended to handle multiple FLASH parts on the same SPI bus.
*/
/* 分配实例 */
priv = kmm_zalloc(sizeof(struct w25_dev_s));
if (priv)
{
/* Initialize the allocated structure (unsupported methods were
* nullified by kmm_zalloc).
*/
priv->mtd.erase = w25_erase;
priv->mtd.bread = w25_bread;
priv->mtd.bwrite = w25_bwrite;
priv->mtd.read = w25_read;
priv->mtd.ioctl = w25_ioctl;
#if defined(CONFIG_MTD_BYTE_WRITE) && !defined(CONFIG_W25_READONLY)
priv->mtd.write = w25_write;
#endif
priv->mtd.name = "w25";
priv->spi = spi;
/* Deselect the FLASH */
SPI_SELECT(spi, SPIDEV_FLASH(0), false);
/* Identify the FLASH chip and get its capacity */
ret = w25_readid(priv);
if (ret != OK)
{
/* Unrecognized! Discard all of that work we just did and
* return NULL
*/
w25_ferr("ERROR: Unrecognized\n");
kmm_free(priv);
return NULL;
}
...
w25_finfo("Return %p\n", priv);
return (FAR struct mtd_dev_s *)priv;
}
2、实现 NAND Flash 驱动
NAND Flash 的管理比 NOR Flash 更复杂,涉及坏块管理 (BBM) 和错误纠正码 (ECC)。openvela 提供了针对 NAND 的抽象层。
2.1 核心数据结构:nand_raw_s
openvela 针对 NAND 闪存适配提供了底层驱动抽象,即 struct nand_raw_s。这一抽象结构体为驱动开发人员提供了一种标准化的方式来处理 NAND 闪存相关操作。驱动开发人员只需对 struct nand_raw_s 进行实例化,也就是创建该结构体的具体对象,填充相应的成员变量以适配实际的 NAND 闪存硬件特性。
您需要实现 struct nand_raw_s 结构体,它定义了更底层的 NAND 操作。
// 定义于 nuttx/drivers/mtd/nand_raw.h
struct nand_raw_s {
/* NAND data description */
struct nand_model_s model; /* The NAND model storage */
uintptr_t cmdaddr; /* NAND command address base */
uintptr_t addraddr; /* NAND address address base */
uintptr_t dataaddr; /* NAND data address */
uint8_t ecctype; /* See NANDECC_* definitions */
// 底层操作函数
CODE int (*eraseblock)(FAR struct nand_raw_s *raw, off_t block);
CODE int (*rawread)(FAR struct nand_raw_s *raw, off_t block,
unsigned int page, FAR void *data, FAR void *spare);
CODE int (*rawwrite)(FAR struct nand_raw_s *raw, off_t block,
unsigned int page, FAR const void *data,
FAR const void *spare);
#ifdef CONFIG_MTD_NAND_HWECC
CODE int (*readpage)(FAR struct nand_raw_s *raw, off_t block,
unsigned int page, FAR void *data, FAR void *spare);
CODE int (*writepage)(FAR struct nand_raw_s *raw, off_t block,
unsigned int page, FAR const void *data,
FAR const void *spare);
#endif
...
}
在位错误纠正(ECC,Error - Correcting Code)处理方面,提供了 3 种方式:
- 不需要 ECC 处理。
- 软件 ECC:当开启配置项
CONFIG_MTD_NAND_SWECC时,驱动支持通过软件方式进行 ECC 计算和错误纠正。软件 ECC 的实现意味着系统会在软件层面完成 ECC 码的生成、存储以及在读取数据时的错误检测和纠正操作。 - 硬件 ECC:若采用硬件 ECC 方式,驱动的下半部逻辑不进行 ECC 计算,而是依赖硬件本身来提供 ECC 功能。硬件会自动完成 ECC 码的生成和错误检测纠正,减轻了软件的负担,提高了处理效率。
对应 ecc_type 字段:
- NANDECC_NONE:不需要 ECC 处理。
- NANDECC_SWECC:软件 ECC,使用通用的 Nuttx ECC处 理逻辑,配合
CONFIG_MTD_NAND_SWECC使用。 - NANDECC_HWECC:硬件 ECC ,配合
CONFIG_MTD_NAND_HWECC使用。
nuttx/include/nuttx/mtd/nand_raw.h
/* Type of ECC to be performed (may also need to be enabled in the
* configuration)
*
* NANDECC_NONE No ECC, only raw NAND FLASH accesses
* NANDECC_SWECC Software ECC. Handled by the common MTD logic.
* NANDECC_HWECC Values >= 2 are various hardware ECC implementations
* all handled by the lower-half, raw NAND FLASH driver.
* These hardware ECC types may be extended beginning
* with the value NANDECC_HWECC.
*
* Software ECC is performed by common, upper-half MTD logic; All
* hardware assisted ECC operations are handled by the platform-specific,
* lower-half driver.
*/
#define NANDECC_NONE 0
#define NANDECC_SWECC 1
#define NANDECC_HWECC 2
2.2. 开发流程
- 实现并实例化
struct nand_raw_s。 - 调用
nand_initialize(FAR struct nand_raw_s *raw)。此函数会处理 ONFI (Open NAND Flash Interface) 协议探测、ECC 初始化等通用逻辑,并返回一个配置好的struct mtd_dev_s *实例。 - 完成实例化之后,能够通过
register_mtddriver函数将其注册到虚拟文件系统层,以便系统进行统一管理和调度。
2.3. ECC 和坏块管理
-
ECC:
openvela支持硬件 ECC 和软件 ECC (CONFIG_MTD_NAND_SWECC)。rawread/rawwrite:实现这两个接口时,您需要负责读/写数据区和备用区 (Spare Area),但不进行 ECC 计算。readpage/writepage:如果您的硬件支持 ECC,请实现这两个接口。writepage应触发硬件生成 ECC 并写入,readpage应触发硬件进行 ECC 校验和纠正。
-
坏块管理:
- 您的驱动需要决定坏块标记在备用区的存储位置。
ioctl接口需要实现MTDIOC_ISBAD和MTDIOC_MARKBAD命令。
2.4 参考实现
nuttx/arch/arm/src/sam34/sam4s_nand.c
struct mtd_dev_s *sam_nand_initialize(int cs)
{
struct sam_nandcs_s *priv;
struct mtd_dev_s *mtd;
uintptr_t cmdaddr;
uintptr_t addraddr;
uintptr_t dataaddr;
uint8_t ecctype;
int ret;
finfo("CS%d\n", cs);
if (SAM_SMCCS_BASE(cs) == SAM_SMC_CS0_BASE)
{
/* Refer to the pre-allocated NAND device structure */
priv = &g_cs0nand;
/* Set up the NAND addresses. These must be provided in the board.h
* header file.
*/
cmdaddr = BOARD_NCS0_NAND_CMDADDR;
addraddr = BOARD_NCS0_NAND_ADDRADDR;
dataaddr = BOARD_NCS0_NAND_DATAADDR;
/* Pass on the configured ECC type */
ecctype = SAM34_NCS0_ECCTYPE;
}
else
{
ferr("ERROR: CS%d unsupported or invalid\n", cs);
return NULL;
}
/* Initialize the device structure */
memset(priv, 0, sizeof(struct sam_nandcs_s));
priv->raw.cmdaddr = cmdaddr;
priv->raw.addraddr = addraddr;
priv->raw.dataaddr = dataaddr;
priv->raw.ecctype = ecctype;
priv->raw.eraseblock = nand_eraseblock;
priv->raw.rawread = nand_rawread;
priv->raw.rawwrite = nand_rawwrite;
priv->cs = cs;
priv->rb = GPIO_SMC_RB;
ret = board_nandflash_config(cs);
if (ret < 0)
{
ferr("ERROR: board_nandflash_config failed for CS%d: %d\n",
cs, ret);
return NULL;
}
/* Reset the NAND FLASH part */
nand_reset(priv);
/* Probe the NAND part. On success, an MTD interface that wraps
* our raw NAND interface is returned.
**/
mtd = nand_initialize(&priv->raw);
if (!mtd)
{
ferr("ERROR: CS%d nand_initialize failed\n", cs);
return NULL;
}
/* Return the MTD wrapper interface as the MTD device */
return mtd;
}
四、注册与分区
1、注册 MTD 设备
在板级初始化代码中,调用 register_mtddriver() 将您的设备注册到 /dev 目录下。
// 在板级初始化函数中
FAR struct mtd_dev_s *mtd_dev;
// 1. 获取 MTD 设备实例
mtd_dev = my_nor_initialize(); // 您编写的初始化函数
if (mtd_dev) {
// 2. 注册设备
int ret = register_mtddriver("/dev/mynor", mtd_dev, 0666, NULL);
if (ret < 0) {
// 处理错误
}
}
2、创建 MTD 分区
openvela 允许您将一个物理 MTD 设备划分为多个逻辑分区,便于管理。
- 核心函数:
mtd_partition()创建一个代表分区的 MTD 子设备。 - 注册函数:
register_mtdpartition()封装了创建和注册分区的逻辑。
// 示例:将 /dev/mynor 划分为两个分区
// "config" 分区: 从第 0 块开始,共 16 个擦除块
register_mtdpartition("/dev/config", 0666, "/dev/mynor", 0, 16);
// "userdata" 分区: 从第 16 块开始,共 1008 个擦除块
register_mtdpartition("/dev/userdata", 0666, "/dev/mynor", 16, 1008);
3、参考实现
一个典型的 MTD 设备分区创建 ptable 分区表例子如下:
struct partition_s
{
char name[NAME_MAX + 1]; // 分区名
size_t index; // 分区index
size_t firstblock; // 分区逻辑起始block地址
size_t nblocks; // 分区占用block数量,这个地方是page单位,不是eraseblock
size_t blocksize; // 分区每个block大小
};
#define FLASH_FIRSTBLOCK 0x10000000 //addr
#define FLASH_BLOCK 0x1000 //block size
#define SST_FLASH_SIZE (512 * 1024)
#define TEE_FLASH_SIZE (512 * 1024)
#define AP_FLASH_SIZE (2 * 1024 * 1024)
#define AUDIO_FLASH_SIZE (2 * 1024 * 1024)
static const struct partition_s ptable[] =
{
{
.name = "sst",
.firstblock = FALSH_FIRSTBLOCK / FLASH_BLOCK,
.nblocks = SST_FLASH_SIZE / FLASH_BLOCK,
},
{
.name = "tee",
.firstblock = (FALSH_FIRSTBLOCK + SST_FLASH_SIZE) / FLASH_BLOCK,
.nblocks = TEE_FLASH_SIZE / FLASH_BLOCK,
},
{
.name = "ap",
.firstblock = (FALSH_FIRSTBLOCK + SST_FLASH_SIZE + TEE_FLASH_SIZE) / FLASH_BLOCK,
.nblocks = AP_FLASH_SIZE / FLASH_BLOCK,
},
{
.name = "audio",
.firstblock = (FALSH_FIRSTBLOCK + SST_FLASH_SIZE + TEE_FLASH_SIZE + AP_FLASH_SIZE) / FLASH_BLOCK,
.nblocks = AUDIO_FLASH_SIZE / FLASH_BLOCK,
},
}
//分区示例代码, path 为 parent mtd path
for (int idx = 0; idx < sizeof(ptable)/sizeof(ptable[0]); idx++)
{
char dev[32];
snprintf(dev, sizeof(dev), "/dev/%s", &ptable[idx]->name);
register_mtdpartition(dev, 0, path,
&ptable[idx]->firstblock,
&ptable[idx]->nblocks);
}
五、相关内核配置 (Kconfig)
确保在 defconfig 文件中启用了必要的 MTD 和相关模块:
# 启用 MTD 核心支持
CONFIG_MTD=y
# 启用 MTD 分区支持
CONFIG_MTD_PARTITION=y
CONFIG_MTD_PROGMEM=y
# 启用 BCH 代理,以支持通过文件接口访问 MTD
CONFIG_BCH=y
CONFIG_BCH_BUFFER_ALIGNMENT=32 //86panel & k03 alignment = 0
# 读写 cache 的配置
CONFIG_DRVR_WRITEBUFFER=y
CONFIG_DRVR_WRDELAY=350
# 多数项目下不开读buffer
# CONFIG_DRVR_READAHEAD is not set
# CONFIG_DRVR_READBYTES is not set
六、测试与验证
openvela 提供了丰富的测试工具来验证您的 MTD 驱动:
- fstest:一个综合性的文件系统压力测试工具,可以挂载文件系统到您的 MTD 分区上进行读写、擦除、耗尽空间等测试。详情请参见 fstest 文件系统压力测试工具指南。
- cmocka_block_test:虽然名为 block test,但它也可以通过 MTD 的块设备代理(如
/dev/mtdblock0)来测试底层的bread和bwrite接口是否正确。详情请参见 blktest 块设备 I/O 测试指南。