《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 19 章 Linux MTD 设备驱动

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 19 章 Linux MTD 设备驱动

参考:宋宝华 著,机械工业出版社,2015年版


19.1 MTD 设备驱动架构

19.1.1 MTD 的概念

MTD(Memory Technology Device,内存技术设备)是 Linux 内核中专门为Flash 存储器设计的子系统。Flash 存储器(NOR Flash、NAND Flash)与普通磁盘有本质区别,需要专门的驱动框架来处理其特殊性:

复制代码
Flash 存储器的特殊性:

1. 写前必须擦除
   Flash 只能将 1 写为 0,不能将 0 写为 1
   写入前必须先擦除(将所有位置为 1)
   擦除单位:扇区(NOR Flash)或块(NAND Flash)

2. 擦写次数有限
   NOR Flash:约 10 万次擦写
   NAND Flash:约 10 万次擦写(MLC 约 1 万次)
   需要磨损均衡(Wear Leveling)算法

3. NAND Flash 有坏块
   出厂时可能存在坏块
   使用过程中也会产生新坏块
   需要坏块管理(Bad Block Management)

4. 访问方式特殊
   NOR Flash:支持字节寻址,可直接执行代码(XIP)
   NAND Flash:只能按页读写,按块擦除

MTD 子系统的作用:
  ✓ 屏蔽不同 Flash 硬件的差异
  ✓ 提供统一的 Flash 操作接口
  ✓ 支持坏块管理
  ✓ 为上层文件系统(JFFS2/UBIFS/YAFFS2)提供接口
  ✓ 提供字符设备(/dev/mtdN)和块设备(/dev/mtdblockN)接口

19.1.2 MTD 体系结构

复制代码
Linux MTD 体系结构:

┌─────────────────────────────────────────────────────────────┐
│                      用户空间                                │
│  应用程序 / flash_erase / nandwrite / mtd-utils             │
└──────────────────────────┬──────────────────────────────────┘
                           │ /dev/mtdN(字符设备)
                           │ /dev/mtdblockN(块设备)
┌──────────────────────────▼──────────────────────────────────┐
│                   MTD 设备层                                  │
│  ┌──────────────────────┐  ┌──────────────────────────────┐ │
│  │  MTD 字符设备         │  │  MTD 块设备                  │ │
│  │  /dev/mtd0, mtd1...  │  │  /dev/mtdblock0, mtdblock1.. │ │
│  └──────────────────────┘  └──────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                   MTD 核心层(drivers/mtd/)                  │
│  mtd_info 结构体 / mtd_read / mtd_write / mtd_erase         │
│  坏块管理 / 分区管理(MTD Partition)                        │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                   MTD 硬件驱动层                              │
│  ┌──────────────────────┐  ┌──────────────────────────────┐ │
│  │  NOR Flash 驱动       │  │  NAND Flash 驱动             │ │
│  │  CFI/JEDEC 接口       │  │  NAND 控制器驱动             │ │
│  └──────────────────────┘  └──────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
                           │ 操作硬件寄存器
┌──────────────────────────▼──────────────────────────────────┐
│                   Flash 硬件                                  │
│  NOR Flash(M25P80/W25Q64 等)                               │
│  NAND Flash(MT29F2G08 等)                                  │
└─────────────────────────────────────────────────────────────┘

上层文件系统:
  JFFS2(Journalling Flash File System v2)← NOR Flash 常用
  UBIFS(Unsorted Block Image File System)← NAND Flash 常用
  YAFFS2(Yet Another Flash File System 2)← NAND Flash
  SquashFS(只读压缩文件系统)← 根文件系统

19.1.3 MTD 核心数据结构

c 复制代码
#include <linux/mtd/mtd.h>

/*
 * mtd_info:MTD 设备描述符
 * 每个 Flash 分区对应一个 mtd_info
 */
struct mtd_info {
    u_char      type;           /* MTD 类型(MTD_NORFLASH/MTD_NANDFLASH)*/
    uint32_t    flags;          /* 标志(MTD_WRITEABLE 等)*/
    uint64_t    size;           /* 设备总大小(字节)*/

    /* 擦除相关 */
    uint32_t    erasesize;      /* 擦除块大小(字节)*/
    uint32_t    erasesize_shift;/* erasesize 的移位数 */

    /* 读写相关 */
    uint32_t    writesize;      /* 写入单位大小(字节)*/
    uint32_t    writebufsize;   /* 写缓冲区大小 */

    /* OOB(Out-Of-Band)相关(NAND Flash 专用)*/
    uint32_t    oobsize;        /* OOB 区域大小 */
    uint32_t    oobavail;       /* 可用 OOB 字节数 */

    /* 设备名称 */
    const char  *name;

    /* 设备编号 */
    int         index;

    /* 操作函数 */
    int (*erase)(struct mtd_info *mtd, struct erase_info *instr);
    int (*read)(struct mtd_info *mtd, loff_t from, size_t len,
                size_t *retlen, u_char *buf);
    int (*write)(struct mtd_info *mtd, loff_t to, size_t len,
                 size_t *retlen, const u_char *buf);
    int (*read_oob)(struct mtd_info *mtd, loff_t from,
                    struct mtd_oob_ops *ops);
    int (*write_oob)(struct mtd_info *mtd, loff_t to,
                     struct mtd_oob_ops *ops);

    /* 坏块管理(NAND Flash)*/
    int (*block_isbad)(struct mtd_info *mtd, loff_t ofs);
    int (*block_markbad)(struct mtd_info *mtd, loff_t ofs);

    /* 同步 */
    void (*sync)(struct mtd_info *mtd);

    /* 私有数据 */
    void *priv;
    struct device dev;
    int usecount;
};

/*
 * erase_info:擦除操作描述符
 */
struct erase_info {
    struct mtd_info *mtd;
    uint64_t    addr;           /* 擦除起始地址 */
    uint64_t    len;            /* 擦除长度 */
    uint64_t    fail_addr;      /* 失败地址(擦除失败时)*/
    u_long      time;           /* 超时时间 */
    u_long      retries;        /* 重试次数 */
    unsigned    dev;
    unsigned    cell;
    void (*callback)(struct erase_info *self); /* 完成回调 */
    u_long      priv;
    u_char      state;          /* 擦除状态 */
    struct erase_info *next;
};

19.1.4 MTD 核心 API

c 复制代码
/* ── 注册/注销 MTD 设备 ──────────────────────────────────────── */

/* 注册 MTD 设备 */
int add_mtd_device(struct mtd_info *mtd);

/* 注销 MTD 设备 */
int del_mtd_device(struct mtd_info *mtd);

/* ── MTD 操作 API(供上层使用)──────────────────────────────── */

/* 擦除 */
static inline int mtd_erase(struct mtd_info *mtd, struct erase_info *instr)
{
    return mtd->erase(mtd, instr);
}

/* 读取 */
static inline int mtd_read(struct mtd_info *mtd, loff_t from, size_t len,
                            size_t *retlen, u_char *buf)
{
    return mtd->read(mtd, from, len, retlen, buf);
}

/* 写入 */
static inline int mtd_write(struct mtd_info *mtd, loff_t to, size_t len,
                             size_t *retlen, const u_char *buf)
{
    return mtd->write(mtd, to, len, retlen, buf);
}

/* 检查坏块 */
static inline int mtd_block_isbad(struct mtd_info *mtd, loff_t ofs)
{
    return mtd->block_isbad ? mtd->block_isbad(mtd, ofs) : 0;
}

/* 标记坏块 */
static inline int mtd_block_markbad(struct mtd_info *mtd, loff_t ofs)
{
    return mtd->block_markbad(mtd, ofs);
}

19.1.5 MTD 分区

MTD 支持将一个 Flash 芯片划分为多个分区,每个分区对应一个独立的 MTD 设备:

c 复制代码
#include <linux/mtd/partitions.h>

/*
 * mtd_partition:MTD 分区描述
 */
struct mtd_partition {
    const char  *name;          /* 分区名称 */
    uint64_t    size;           /* 分区大小(字节)*/
    uint64_t    offset;         /* 分区起始偏移 */
    uint32_t    mask_flags;     /* 标志掩码 */
    struct nand_ecclayout *ecclayout; /* ECC 布局(NAND)*/
};

/* 典型的 Flash 分区方案(以 128MB NAND Flash 为例)*/
static struct mtd_partition nand_partitions[] = {
    {
        .name   = "bootloader",    /* U-Boot */
        .offset = 0,
        .size   = 1 * 1024 * 1024,  /* 1MB */
    },
    {
        .name   = "bootloader-env", /* U-Boot 环境变量 */
        .offset = MTDPART_OFS_APPEND,  /* 紧接上一分区 */
        .size   = 1 * 1024 * 1024,  /* 1MB */
    },
    {
        .name   = "kernel",         /* Linux 内核 */
        .offset = MTDPART_OFS_APPEND,
        .size   = 8 * 1024 * 1024,  /* 8MB */
    },
    {
        .name   = "rootfs",         /* 根文件系统 */
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL, /* 剩余全部空间 */
    },
};

/* 注册分区 */
int mtd_device_register(struct mtd_info *master,
                         const struct mtd_partition *parts,
                         int nr_parts);

设备树中的 MTD 分区配置

dts 复制代码
/* 设备树中的 NAND Flash 分区 */
&gpmi {
    status = "okay";

    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        partition@0 {
            label = "bootloader";
            reg = <0x000000 0x100000>;  /* 0 ~ 1MB */
        };

        partition@100000 {
            label = "kernel";
            reg = <0x100000 0x800000>;  /* 1MB ~ 9MB */
        };

        partition@900000 {
            label = "rootfs";
            reg = <0x900000 0x7700000>; /* 9MB ~ 128MB */
        };
    };
};

19.2 NOR Flash 驱动

19.2.1 NOR Flash 特性

复制代码
NOR Flash 特性:

接口:并行地址/数据总线(类似 SRAM)
访问:支持字节寻址,可直接执行代码(XIP,eXecute In Place)
擦除:按扇区擦除(64KB~128KB)
读取:快速,随机访问
写入:较慢,按字节或字写入
容量:通常 1MB~128MB
典型芯片:
  Spansion S29GL256N(256Mbit,并行接口)
  Micron M25P80(8Mbit,SPI 接口)
  Winbond W25Q64(64Mbit,SPI 接口)

NOR Flash 命令集:
  CFI(Common Flash Interface):Intel/AMD 标准
  JEDEC:早期标准
  SPI NOR:通过 SPI 接口访问的 NOR Flash

19.2.2 CFI NOR Flash 驱动

Linux 内核提供了通用的 CFI NOR Flash 驱动(drivers/mtd/chips/cfi_cmdset_0001.c 等),大多数并行 NOR Flash 无需编写专用驱动:

c 复制代码
/*
 * 使用 CFI 驱动的 NOR Flash 配置(通过 platform_data)
 */
#include <linux/mtd/physmap.h>

/* 平台数据:描述 NOR Flash 的物理连接 */
static struct physmap_flash_data nor_flash_data = {
    .width      = 2,            /* 数据总线宽度(字节):1/2/4 */
    .parts      = nor_partitions,
    .nr_parts   = ARRAY_SIZE(nor_partitions),
};

/* NOR Flash 的内存资源(物理地址)*/
static struct resource nor_flash_resource = {
    .start  = 0x08000000,       /* NOR Flash 物理基地址 */
    .end    = 0x09FFFFFF,       /* 32MB */
    .flags  = IORESOURCE_MEM,
};

/* 平台设备 */
static struct platform_device nor_flash_device = {
    .name           = "physmap-flash",
    .id             = 0,
    .dev            = {
        .platform_data = &nor_flash_data,
    },
    .num_resources  = 1,
    .resource       = &nor_flash_resource,
};

19.2.3 SPI NOR Flash 驱动

SPI NOR Flash 通过 SPI 接口访问,Linux 提供了 m25p80 驱动(现已整合到 spi-nor 框架):

c 复制代码
/*
 * spi_nor_simple.c ------ SPI NOR Flash 驱动框架
 * 以 W25Q64(64Mbit = 8MB)为例
 */

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/spi-nor.h>

/* W25Q64 命令集 */
#define W25Q_WRITE_ENABLE   0x06   /* 写使能 */
#define W25Q_WRITE_DISABLE  0x04   /* 写禁止 */
#define W25Q_READ_STATUS1   0x05   /* 读状态寄存器1 */
#define W25Q_READ_STATUS2   0x35   /* 读状态寄存器2 */
#define W25Q_WRITE_STATUS   0x01   /* 写状态寄存器 */
#define W25Q_READ_DATA      0x03   /* 读数据 */
#define W25Q_FAST_READ      0x0B   /* 快速读 */
#define W25Q_PAGE_PROGRAM   0x02   /* 页编程(写入)*/
#define W25Q_SECTOR_ERASE   0x20   /* 扇区擦除(4KB)*/
#define W25Q_BLOCK_ERASE32  0x52   /* 块擦除(32KB)*/
#define W25Q_BLOCK_ERASE64  0xD8   /* 块擦除(64KB)*/
#define W25Q_CHIP_ERASE     0xC7   /* 全片擦除 */
#define W25Q_READ_JEDEC_ID  0x9F   /* 读 JEDEC ID */
#define W25Q_POWER_DOWN     0xB9   /* 掉电 */
#define W25Q_RELEASE_PD     0xAB   /* 释放掉电 */

/* 状态寄存器位 */
#define W25Q_STATUS_BUSY    BIT(0)  /* 忙标志 */
#define W25Q_STATUS_WEL     BIT(1)  /* 写使能锁存 */

/* 驱动私有数据 */
struct w25q_flash {
    struct spi_device   *spi;
    struct mtd_info      mtd;
    struct mutex         lock;
    u32                  jedec_id;
    u32                  page_size;     /* 页大小(256字节)*/
    u32                  sector_size;   /* 扇区大小(4KB)*/
    u32                  n_sectors;     /* 扇区总数 */
};

/* ── SPI 传输辅助函数 ──────────────────────────────────────── */

static int w25q_spi_write_then_read(struct w25q_flash *flash,
                                     const u8 *txbuf, unsigned txlen,
                                     u8 *rxbuf, unsigned rxlen)
{
    return spi_write_then_read(flash->spi, txbuf, txlen, rxbuf, rxlen);
}

/* 读取状态寄存器 */
static u8 w25q_read_status(struct w25q_flash *flash)
{
    u8 cmd = W25Q_READ_STATUS1;
    u8 status;
    w25q_spi_write_then_read(flash, &cmd, 1, &status, 1);
    return status;
}

/* 等待操作完成(轮询 BUSY 位)*/
static int w25q_wait_ready(struct w25q_flash *flash, unsigned long timeout_ms)
{
    unsigned long deadline = jiffies + msecs_to_jiffies(timeout_ms);

    do {
        if (!(w25q_read_status(flash) & W25Q_STATUS_BUSY))
            return 0;
        cond_resched();
    } while (time_before(jiffies, deadline));

    return -ETIMEDOUT;
}

/* 发送写使能命令 */
static int w25q_write_enable(struct w25q_flash *flash)
{
    u8 cmd = W25Q_WRITE_ENABLE;
    return spi_write(flash->spi, &cmd, 1);
}

/* ── 读取 JEDEC ID ──────────────────────────────────────────── */

static int w25q_read_jedec_id(struct w25q_flash *flash)
{
    u8 cmd = W25Q_READ_JEDEC_ID;
    u8 id[3];
    int ret;

    ret = w25q_spi_write_then_read(flash, &cmd, 1, id, 3);
    if (ret)
        return ret;

    flash->jedec_id = (id[0] << 16) | (id[1] << 8) | id[2];
    pr_info("w25q: JEDEC ID = 0x%06X\n", flash->jedec_id);

    /* W25Q64 的 JEDEC ID = 0xEF4017 */
    /* EF = Winbond, 40 = SPI Flash, 17 = 64Mbit */
    return 0;
}

/* ── MTD 操作函数实现 ──────────────────────────────────────── */

/* 读取数据 */
static int w25q_mtd_read(struct mtd_info *mtd, loff_t from, size_t len,
                          size_t *retlen, u_char *buf)
{
    struct w25q_flash *flash = mtd->priv;
    u8 cmd[5];
    int ret;

    if (from + len > mtd->size)
        return -EINVAL;

    mutex_lock(&flash->lock);

    /* 构造读命令:0x03 + 3字节地址 */
    cmd[0] = W25Q_READ_DATA;
    cmd[1] = (from >> 16) & 0xFF;
    cmd[2] = (from >> 8)  & 0xFF;
    cmd[3] = (from >> 0)  & 0xFF;

    ret = w25q_spi_write_then_read(flash, cmd, 4, buf, len);
    if (ret == 0)
        *retlen = len;

    mutex_unlock(&flash->lock);
    return ret;
}

/* 擦除扇区 */
static int w25q_mtd_erase(struct mtd_info *mtd, struct erase_info *instr)
{
    struct w25q_flash *flash = mtd->priv;
    u32 addr = instr->addr;
    u32 len  = instr->len;
    u8 cmd[4];
    int ret;

    /* 检查地址和长度是否对齐 */
    if (addr % mtd->erasesize || len % mtd->erasesize)
        return -EINVAL;

    mutex_lock(&flash->lock);

    while (len > 0) {
        /* 写使能 */
        ret = w25q_write_enable(flash);
        if (ret)
            goto err;

        /* 发送扇区擦除命令(4KB)*/
        cmd[0] = W25Q_SECTOR_ERASE;
        cmd[1] = (addr >> 16) & 0xFF;
        cmd[2] = (addr >> 8)  & 0xFF;
        cmd[3] = (addr >> 0)  & 0xFF;

        ret = spi_write(flash->spi, cmd, 4);
        if (ret)
            goto err;

        /* 等待擦除完成(最长 400ms)*/
        ret = w25q_wait_ready(flash, 400);
        if (ret) {
            pr_err("w25q: 擦除超时,地址 0x%08x\n", addr);
            instr->fail_addr = addr;
            goto err;
        }

        addr += mtd->erasesize;
        len  -= mtd->erasesize;
    }

    instr->state = MTD_ERASE_DONE;
    mutex_unlock(&flash->lock);
    mtd_erase_callback(instr);
    return 0;

err:
    instr->state = MTD_ERASE_FAILED;
    mutex_unlock(&flash->lock);
    return ret;
}

/* 写入数据(按页写入,每页 256 字节)*/
static int w25q_mtd_write(struct mtd_info *mtd, loff_t to, size_t len,
                           size_t *retlen, const u_char *buf)
{
    struct w25q_flash *flash = mtd->priv;
    u32 page_offset, page_size;
    u8 cmd[4];
    int ret = 0;

    if (to + len > mtd->size)
        return -EINVAL;

    mutex_lock(&flash->lock);

    *retlen = 0;

    while (len > 0) {
        /* 计算当前页内的偏移和可写字节数 */
        page_offset = (u32)to % flash->page_size;
        page_size   = min_t(u32, len, flash->page_size - page_offset);

        /* 写使能 */
        ret = w25q_write_enable(flash);
        if (ret)
            break;

        /* 发送页编程命令 */
        cmd[0] = W25Q_PAGE_PROGRAM;
        cmd[1] = (to >> 16) & 0xFF;
        cmd[2] = (to >> 8)  & 0xFF;
        cmd[3] = (to >> 0)  & 0xFF;

        /* 发送命令 + 数据 */
        {
            struct spi_transfer xfers[2] = {
                { .tx_buf = cmd,  .len = 4 },
                { .tx_buf = buf,  .len = page_size },
            };
            struct spi_message msg;
            spi_message_init(&msg);
            spi_message_add_tail(&xfers[0], &msg);
            spi_message_add_tail(&xfers[1], &msg);
            ret = spi_sync(flash->spi, &msg);
        }

        if (ret)
            break;

        /* 等待写入完成(最长 3ms)*/
        ret = w25q_wait_ready(flash, 3);
        if (ret)
            break;

        to      += page_size;
        buf     += page_size;
        len     -= page_size;
        *retlen += page_size;
    }

    mutex_unlock(&flash->lock);
    return ret;
}

/* ── probe 函数 ──────────────────────────────────────────────── */

static int w25q_probe(struct spi_device *spi)
{
    struct w25q_flash *flash;
    int ret;

    flash = devm_kzalloc(&spi->dev, sizeof(*flash), GFP_KERNEL);
    if (!flash)
        return -ENOMEM;

    flash->spi = spi;
    mutex_init(&flash->lock);

    /* 配置 SPI 参数 */
    spi->max_speed_hz = 50000000;  /* 50MHz */
    spi->mode         = SPI_MODE_0;
    spi->bits_per_word = 8;
    spi_setup(spi);

    /* 读取 JEDEC ID,验证芯片 */
    ret = w25q_read_jedec_id(flash);
    if (ret)
        return ret;

    /* 根据 JEDEC ID 确定芯片参数 */
    switch (flash->jedec_id) {
    case 0xEF4017:  /* W25Q64 */
        flash->page_size   = 256;
        flash->sector_size = 4096;
        flash->n_sectors   = 2048;  /* 8MB / 4KB = 2048 扇区 */
        break;
    case 0xEF4018:  /* W25Q128 */
        flash->page_size   = 256;
        flash->sector_size = 4096;
        flash->n_sectors   = 4096;  /* 16MB / 4KB = 4096 扇区 */
        break;
    default:
        dev_err(&spi->dev, "不支持的 Flash 芯片:0x%06X\n",
                flash->jedec_id);
        return -ENODEV;
    }

    /* 初始化 mtd_info */
    flash->mtd.name      = "w25q-nor";
    flash->mtd.type      = MTD_NORFLASH;
    flash->mtd.flags     = MTD_CAP_NORFLASH;
    flash->mtd.size      = (u64)flash->sector_size * flash->n_sectors;
    flash->mtd.erasesize = flash->sector_size;
    flash->mtd.writesize = 1;           /* NOR Flash 支持字节写入 */
    flash->mtd.writebufsize = flash->page_size;
    flash->mtd.erase     = w25q_mtd_erase;
    flash->mtd.read      = w25q_mtd_read;
    flash->mtd.write     = w25q_mtd_write;
    flash->mtd.priv      = flash;
    flash->mtd.dev.parent = &spi->dev;

    spi_set_drvdata(spi, flash);

    /* 注册 MTD 设备 */
    ret = mtd_device_register(&flash->mtd, NULL, 0);
    if (ret) {
        dev_err(&spi->dev, "注册 MTD 设备失败:%d\n", ret);
        return ret;
    }

    dev_info(&spi->dev, "W25Q NOR Flash 初始化成功,大小 %llu MB\n",
             flash->mtd.size / 1024 / 1024);
    return 0;
}

static int w25q_remove(struct spi_device *spi)
{
    struct w25q_flash *flash = spi_get_drvdata(spi);
    mtd_device_unregister(&flash->mtd);
    return 0;
}

static const struct spi_device_id w25q_id[] = {
    { "w25q64", 0 },
    { "w25q128", 0 },
    {}
};
MODULE_DEVICE_TABLE(spi, w25q_id);

static const struct of_device_id w25q_of_match[] = {
    { .compatible = "winbond,w25q64" },
    { .compatible = "winbond,w25q128" },
    {}
};
MODULE_DEVICE_TABLE(of, w25q_of_match);

static struct spi_driver w25q_driver = {
    .driver = {
        .name           = "w25q",
        .of_match_table = w25q_of_match,
    },
    .probe    = w25q_probe,
    .remove   = w25q_remove,
    .id_table = w25q_id,
};
module_spi_driver(w25q_driver);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("W25Q SPI NOR Flash 驱动");

19.3 NAND Flash 驱动

19.3.1 NAND Flash 特性

复制代码
NAND Flash 特性:

接口:串行 I/O 总线(8位或16位数据总线)
访问:只能按页读写(512B/2KB/4KB/8KB),按块擦除(64页/128页)
容量:通常 128MB~TB 级
速度:读写速度比 NOR Flash 快
价格:比 NOR Flash 便宜
坏块:出厂有坏块,使用中产生新坏块

NAND Flash 组织结构(以 2KB 页为例):
┌─────────────────────────────────────────────────────────────┐
│                        芯片                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                      块 0                            │   │
│  │  ┌──────────────────────────────────────────────┐   │   │
│  │  │  页 0(2048B 数据 + 64B OOB)                │   │   │
│  │  │  页 1(2048B 数据 + 64B OOB)                │   │   │
│  │  │  ...(共 64 页)                              │   │   │
│  │  └──────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────┘   │
│  块 1、块 2 ... 块 N(共 N 个块)                           │
└─────────────────────────────────────────────────────────────┘

OOB(Out-Of-Band)区域用途:
  - ECC 校验数据(纠正位翻转错误)
  - 坏块标记(Bad Block Marker)
  - 文件系统元数据(JFFS2/UBIFS)

19.3.2 NAND Flash 驱动框架

Linux 提供了 NAND 子系统(drivers/mtd/nand/),驱动只需实现底层硬件操作:

c 复制代码
#include <linux/mtd/nand.h>

/*
 * nand_chip:NAND Flash 芯片描述符
 * 包含 NAND 操作函数和芯片参数
 */
struct nand_chip {
    void __iomem    *IO_ADDR_R;     /* 读数据寄存器地址 */
    void __iomem    *IO_ADDR_W;     /* 写数据寄存器地址 */

    /* 底层硬件操作函数(驱动必须实现)*/
    void (*cmd_ctrl)(struct mtd_info *mtd, int dat, unsigned int ctrl);
    int  (*dev_ready)(struct mtd_info *mtd);
    void (*select_chip)(struct mtd_info *mtd, int chip);
    void (*write_buf)(struct mtd_info *mtd, const uint8_t *buf, int len);
    void (*read_buf)(struct mtd_info *mtd, uint8_t *buf, int len);

    /* ECC 配置 */
    struct nand_ecc_ctrl ecc;

    /* 芯片参数(由 NAND 子系统自动检测)*/
    int     page_shift;     /* 页大小的移位数 */
    int     phys_erase_shift; /* 物理擦除块大小的移位数 */
    int     chip_shift;     /* 芯片大小的移位数 */
    int     numchips;       /* 芯片数量 */
    uint64_t chipsize;      /* 单片大小 */
    int     pagemask;       /* 页掩码 */
    int     pagebuf;        /* 当前缓存的页号 */

    /* 坏块管理 */
    int (*block_bad)(struct mtd_info *mtd, loff_t ofs, int getchip);
    int (*block_markbad)(struct mtd_info *mtd, loff_t ofs);

    /* 私有数据 */
    void    *priv;
};

/*
 * NAND 控制信号(cmd_ctrl 的 ctrl 参数)
 */
#define NAND_NCE    0x01    /* 片选(低有效)*/
#define NAND_CLE    0x02    /* 命令锁存使能 */
#define NAND_ALE    0x04    /* 地址锁存使能 */
#define NAND_CTRL_CLE   (NAND_NCE | NAND_CLE)
#define NAND_CTRL_ALE   (NAND_NCE | NAND_ALE)
#define NAND_CTRL_CHANGE 0x80

19.3.3 完整的 NAND Flash 控制器驱动

c 复制代码
/*
 * my_nand.c ------ NAND Flash 控制器驱动
 * 以简化的 NAND 控制器为例
 */

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/nand.h>
#include <linux/mtd/partitions.h>
#include <linux/clk.h>
#include <linux/io.h>
#include <linux/dma-mapping.h>

/* NAND 控制器寄存器 */
#define NAND_CMD_REG        0x00    /* 命令寄存器 */
#define NAND_ADDR_REG       0x04    /* 地址寄存器 */
#define NAND_DATA_REG       0x08    /* 数据寄存器 */
#define NAND_STATUS_REG     0x0C    /* 状态寄存器 */
#define NAND_CTRL_REG       0x10    /* 控制寄存器 */
#define NAND_ECC_REG        0x14    /* ECC 寄存器 */

/* 状态寄存器位 */
#define NAND_STATUS_BUSY    BIT(0)  /* 控制器忙 */
#define NAND_STATUS_RB      BIT(1)  /* NAND R/B# 引脚状态 */

/* 控制寄存器位 */
#define NAND_CTRL_CE        BIT(0)  /* 片选 */
#define NAND_CTRL_CLE       BIT(1)  /* 命令锁存使能 */
#define NAND_CTRL_ALE       BIT(2)  /* 地址锁存使能 */
#define NAND_CTRL_WP        BIT(3)  /* 写保护 */

/* NAND Flash 分区 */
static struct mtd_partition my_nand_partitions[] = {
    {
        .name   = "bootloader",
        .offset = 0,
        .size   = 2 * 1024 * 1024,   /* 2MB */
    },
    {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 8 * 1024 * 1024,   /* 8MB */
    },
    {
        .name   = "rootfs",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,  /* 剩余全部 */
    },
};

/* 驱动私有数据 */
struct my_nand_host {
    struct nand_chip    chip;       /* 内嵌 nand_chip(必须是第一个成员)*/
    struct mtd_info     mtd;        /* MTD 设备 */
    void __iomem       *base;       /* 寄存器基地址 */
    struct clk         *clk;        /* 时钟 */
    int                 irq;        /* 中断号 */
    struct completion   op_done;    /* 操作完成信号 */
};

/* ── 底层硬件操作函数 ──────────────────────────────────────── */

/*
 * cmd_ctrl:发送命令或地址
 * dat:命令/地址字节(-1 表示无数据)
 * ctrl:控制信号(NAND_CLE/NAND_ALE/NAND_NCE)
 */
static void my_nand_cmd_ctrl(struct mtd_info *mtd, int dat,
                              unsigned int ctrl)
{
    struct nand_chip *chip = mtd->priv;
    struct my_nand_host *host = container_of(chip, struct my_nand_host, chip);
    u32 reg_ctrl = 0;

    /* 设置控制信号 */
    if (ctrl & NAND_CLE)
        reg_ctrl |= NAND_CTRL_CLE;
    if (ctrl & NAND_ALE)
        reg_ctrl |= NAND_CTRL_ALE;
    if (ctrl & NAND_NCE)
        reg_ctrl |= NAND_CTRL_CE;

    writel(reg_ctrl, host->base + NAND_CTRL_REG);

    /* 写入命令或地址 */
    if (dat != NAND_CMD_NONE)
        writeb(dat, host->base + NAND_DATA_REG);
}

/*
 * dev_ready:检查 NAND Flash 是否就绪
 * 返回:1(就绪),0(忙)
 */
static int my_nand_dev_ready(struct mtd_info *mtd)
{
    struct nand_chip *chip = mtd->priv;
    struct my_nand_host *host = container_of(chip, struct my_nand_host, chip);

    return (readl(host->base + NAND_STATUS_REG) & NAND_STATUS_RB) ? 1 : 0;
}

/*
 * read_buf:从 NAND Flash 读取数据
 */
static void my_nand_read_buf(struct mtd_info *mtd, uint8_t *buf, int len)
{
    struct nand_chip *chip = mtd->priv;
    struct my_nand_host *host = container_of(chip, struct my_nand_host, chip);
    int i;

    for (i = 0; i < len; i++)
        buf[i] = readb(host->base + NAND_DATA_REG);
}

/*
 * write_buf:向 NAND Flash 写入数据
 */
static void my_nand_write_buf(struct mtd_info *mtd, const uint8_t *buf, int len)
{
    struct nand_chip *chip = mtd->priv;
    struct my_nand_host *host = container_of(chip, struct my_nand_host, chip);
    int i;

    for (i = 0; i < len; i++)
        writeb(buf[i], host->base + NAND_DATA_REG);
}

/* ── 中断处理(操作完成)──────────────────────────────────── */

static irqreturn_t my_nand_irq_handler(int irq, void *dev_id)
{
    struct my_nand_host *host = dev_id;

    /* 清除中断 */
    writel(0x01, host->base + NAND_STATUS_REG);

    /* 通知等待的操作 */
    complete(&host->op_done);

    return IRQ_HANDLED;
}

/* ── probe 函数 ──────────────────────────────────────────────── */

static int my_nand_probe(struct platform_device *pdev)
{
    struct my_nand_host *host;
    struct nand_chip *chip;
    struct mtd_info *mtd;
    struct resource *res;
    int ret;

    /* 分配私有数据 */
    host = devm_kzalloc(&pdev->dev, sizeof(*host), GFP_KERNEL);
    if (!host)
        return -ENOMEM;

    init_completion(&host->op_done);

    /* 获取寄存器资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    host->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(host->base))
        return PTR_ERR(host->base);

    /* 获取中断号 */
    host->irq = platform_get_irq(pdev, 0);
    if (host->irq < 0)
        return host->irq;

    /* 申请中断 */
    ret = devm_request_irq(&pdev->dev, host->irq,
                            my_nand_irq_handler, 0, "my-nand", host);
    if (ret)
        return ret;

    /* 获取并使能时钟 */
    host->clk = devm_clk_get(&pdev->dev, NULL);
    if (!IS_ERR(host->clk))
        clk_prepare_enable(host->clk);

    /* 初始化 nand_chip */
    chip = &host->chip;
    chip->cmd_ctrl   = my_nand_cmd_ctrl;
    chip->dev_ready  = my_nand_dev_ready;
    chip->read_buf   = my_nand_read_buf;
    chip->write_buf  = my_nand_write_buf;

    /* 设置 ECC 模式 */
    chip->ecc.mode   = NAND_ECC_SOFT;      /* 软件 ECC */
    chip->ecc.algo   = NAND_ECC_HAMMING;   /* Hamming 码 */
    /* 或使用硬件 ECC */
    /* chip->ecc.mode = NAND_ECC_HW; */

    /* 设置 IO 地址(用于默认的读写函数)*/
    chip->IO_ADDR_R = host->base + NAND_DATA_REG;
    chip->IO_ADDR_W = host->base + NAND_DATA_REG;

    /* 初始化 mtd_info */
    mtd = &host->mtd;
    mtd->priv  = chip;
    mtd->name  = "my-nand";
    mtd->owner = THIS_MODULE;
    mtd->dev.parent = &pdev->dev;

    /* 扫描 NAND Flash(自动检测芯片参数)*/
    ret = nand_scan(mtd, 1);  /* 1 = 芯片数量 */
    if (ret) {
        dev_err(&pdev->dev, "NAND 扫描失败:%d\n", ret);
        goto err_clk;
    }

    /* 注册 MTD 设备(含分区)*/
    ret = mtd_device_register(mtd, my_nand_partitions,
                               ARRAY_SIZE(my_nand_partitions));
    if (ret) {
        dev_err(&pdev->dev, "注册 MTD 设备失败:%d\n", ret);
        goto err_nand;
    }

    platform_set_drvdata(pdev, host);

    dev_info(&pdev->dev, "NAND Flash 初始化成功\n");
    dev_info(&pdev->dev, "  页大小:%u 字节\n", mtd->writesize);
    dev_info(&pdev->dev, "  块大小:%u 字节\n", mtd->erasesize);
    dev_info(&pdev->dev, "  总大小:%llu MB\n", mtd->size / 1024 / 1024);
    return 0;

err_nand:
    nand_release(mtd);
err_clk:
    if (!IS_ERR(host->clk))
        clk_disable_unprepare(host->clk);
    return ret;
}

static int my_nand_remove(struct platform_device *pdev)
{
    struct my_nand_host *host = platform_get_drvdata(pdev);

    mtd_device_unregister(&host->mtd);
    nand_release(&host->mtd);

    if (!IS_ERR(host->clk))
        clk_disable_unprepare(host->clk);

    return 0;
}

static const struct of_device_id my_nand_of_match[] = {
    { .compatible = "myvendor,my-nand" },
    {}
};
MODULE_DEVICE_TABLE(of, my_nand_of_match);

static struct platform_driver my_nand_driver = {
    .probe  = my_nand_probe,
    .remove = my_nand_remove,
    .driver = {
        .name           = "my-nand",
        .of_match_table = my_nand_of_match,
    },
};
module_platform_driver(my_nand_driver);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("My NAND Flash 控制器驱动");

19.3.4 NAND Flash 的 ECC

ECC(Error Correction Code,错误纠正码)是 NAND Flash 驱动的关键功能:

复制代码
ECC 的必要性:

NAND Flash 的位翻转问题:
  - 由于物理特性,NAND Flash 读取时可能出现位翻转
  - 1 变成 0,或 0 变成 1
  - 需要 ECC 来检测和纠正这些错误

ECC 类型:
  1. Hamming 码(1位纠错,2位检错)
     每 256 字节数据需要 3 字节 ECC
     适合 SLC NAND Flash

  2. BCH 码(多位纠错)
     每 512 字节数据可纠正 4/8/16/24 位错误
     适合 MLC/TLC NAND Flash

  3. RS 码(Reed-Solomon)
     用于 NAND Flash 的另一种 ECC 算法

ECC 存储位置:
  ECC 数据存储在 OOB(Out-Of-Band)区域
  每页 2KB 数据 + 64B OOB
  OOB 中存储:ECC 数据 + 坏块标记 + 文件系统元数据

Linux NAND ECC 模式:
  NAND_ECC_NONE:不使用 ECC(不推荐)
  NAND_ECC_SOFT:软件 ECC(CPU 计算)
  NAND_ECC_HW:硬件 ECC(NAND 控制器计算,性能好)
  NAND_ECC_HW_SYNDROME:硬件 ECC(syndrome 模式)
  NAND_ECC_ON_DIE:片上 ECC(NAND 芯片内部计算)

19.4 MTD 字符设备与块设备

19.4.1 MTD 字符设备(/dev/mtdN)

MTD 字符设备提供对 Flash 的直接访问接口,支持擦除、读写等操作:

bash 复制代码
# 查看 MTD 设备
cat /proc/mtd
# dev:    size   erasesize  name
# mtd0: 00100000 00010000 "bootloader"    ← 1MB,64KB 擦除块
# mtd1: 00800000 00020000 "kernel"        ← 8MB,128KB 擦除块
# mtd2: 07700000 00020000 "rootfs"        ← 119MB

# 查看 MTD 设备文件
ls /dev/mtd*
# /dev/mtd0    ← 字符设备(可读写擦除)
# /dev/mtd0ro  ← 只读字符设备
# /dev/mtdblock0 ← 块设备(可挂载文件系统)

# 使用 mtd-utils 工具操作 Flash
# 安装:apt-get install mtd-utils

# 擦除 Flash 分区
flash_erase /dev/mtd1 0 0   # 擦除整个 mtd1 分区(0=从头,0=全部)

# 写入内核镜像
nandwrite -p /dev/mtd1 zImage   # -p:跳过坏块

# 读取 Flash 内容
nanddump /dev/mtd1 -f kernel.bin  # 读取 mtd1 到文件

# 擦除并写入(合并操作)
flash_erase /dev/mtd2 0 0
nandwrite -p /dev/mtd2 rootfs.ubifs

# 查看坏块
nandtest -m /dev/mtd2   # 测试并标记坏块

19.4.2 MTD 字符设备的 ioctl

c 复制代码
#include <mtd/mtd-user.h>

/* MTD 字符设备的 ioctl 命令 */

/* 获取 MTD 设备信息 */
struct mtd_info_user info;
ioctl(fd, MEMGETINFO, &info);
printf("大小:%u,擦除块:%u,写入单位:%u\n",
       info.size, info.erasesize, info.writesize);

/* 擦除 Flash */
struct erase_info_user erase;
erase.start  = 0;           /* 擦除起始地址 */
erase.length = info.erasesize; /* 擦除一个块 */
ioctl(fd, MEMERASE, &erase);

/* 获取坏块信息 */
loff_t offset = 0;
int ret = ioctl(fd, MEMGETBADBLOCK, &offset);
if (ret == 1)
    printf("偏移 0x%llx 是坏块\n", offset);

/* 标记坏块 */
loff_t bad_offset = 0x100000;
ioctl(fd, MEMSETBADBLOCK, &bad_offset);

/* 锁定/解锁 Flash */
struct erase_info_user lock;
lock.start  = 0;
lock.length = info.size;
ioctl(fd, MEMLOCK, &lock);    /* 锁定 */
ioctl(fd, MEMUNLOCK, &lock);  /* 解锁 */

19.4.3 MTD 块设备(/dev/mtdblockN)

MTD 块设备将 Flash 模拟为普通块设备,可以挂载文件系统:

bash 复制代码
# MTD 块设备的使用

# 格式化为 FAT 文件系统(NOR Flash,不推荐用于 NAND)
mkdosfs /dev/mtdblock0

# 挂载 JFFS2 文件系统(适合 NOR Flash)
mount -t jffs2 /dev/mtdblock2 /mnt/flash

# 挂载 UBIFS 文件系统(适合 NAND Flash)
# 需要先通过 UBI 层
ubiformat /dev/mtd2
ubiattach /dev/ubi_ctrl -m 2
ubimkvol /dev/ubi0 -N rootfs -m
mount -t ubifs ubi0:rootfs /mnt/rootfs

# 挂载 SquashFS(只读,适合根文件系统)
mount -t squashfs /dev/mtdblock2 /mnt/rootfs -o ro

19.4.4 MTD 与文件系统的关系

复制代码
MTD 文件系统选择指南:

NOR Flash:
  JFFS2(Journalling Flash File System v2)
    - 日志文件系统,掉电安全
    - 支持压缩
    - 适合小容量(< 32MB)
    - 挂载时间较长(需要扫描整个 Flash)

NAND Flash:
  UBIFS(Unsorted Block Image File System)
    - 基于 UBI(Unsorted Block Images)层
    - UBI 提供磨损均衡和坏块管理
    - 性能好,适合大容量
    - 推荐用于 NAND Flash

  YAFFS2(Yet Another Flash File System 2)
    - 专为 NAND Flash 设计
    - 不需要 UBI 层
    - Android 早期使用

  SquashFS(只读)
    - 高压缩比
    - 适合只读根文件系统
    - 通常与 UBIFS 配合使用

文件系统层次(NAND Flash):
  应用程序
      ↓
  VFS(虚拟文件系统)
      ↓
  UBIFS(文件系统)
      ↓
  UBI(磨损均衡层)
      ↓
  MTD(Flash 抽象层)
      ↓
  NAND 控制器驱动
      ↓
  NAND Flash 硬件

19.4.5 MTD 驱动的完整测试

bash 复制代码
# 加载 NAND Flash 驱动
sudo insmod my_nand.ko

# 查看 MTD 设备
cat /proc/mtd
# dev:    size   erasesize  name
# mtd0: 00200000 00020000 "bootloader"
# mtd1: 00800000 00020000 "kernel"
# mtd2: 07600000 00020000 "rootfs"

# 查看设备文件
ls /dev/mtd*
# /dev/mtd0  /dev/mtd0ro  /dev/mtdblock0
# /dev/mtd1  /dev/mtd1ro  /dev/mtdblock1
# /dev/mtd2  /dev/mtd2ro  /dev/mtdblock2

# 测试 NOR Flash(W25Q64)
# 擦除第一个扇区(4KB)
flash_erase /dev/mtd0 0 1

# 写入测试数据
echo "Hello MTD!" | dd of=/dev/mtd0 bs=1 count=10

# 读取验证
dd if=/dev/mtd0 bs=1 count=10 | hexdump -C
# 00000000  48 65 6c 6c 6f 20 4d 54  44 21                    |Hello MTD!|

# 测试 NAND Flash
# 擦除整个 rootfs 分区
flash_erase /dev/mtd2 0 0

# 写入 UBIFS 镜像
nandwrite -p /dev/mtd2 rootfs.ubifs

# 挂载 UBIFS
ubiattach /dev/ubi_ctrl -m 2
mount -t ubifs ubi0:rootfs /mnt/rootfs

# 查看挂载结果
ls /mnt/rootfs
df -h /mnt/rootfs

# 坏块测试
nandtest /dev/mtd2

# 查看 MTD 统计信息
cat /sys/class/mtd/mtd0/erasesize
cat /sys/class/mtd/mtd0/size
cat /sys/class/mtd/mtd0/writesize
cat /sys/class/mtd/mtd0/name

本章小结

章节 核心知识点 关键 API
19.1 MTD设备驱动架构 Flash特殊性(写前擦除/擦写次数/坏块);MTD体系结构图(用户空间→MTD设备层→MTD核心→硬件驱动);mtd_info/erase_info核心结构体;MTD核心API;MTD分区(mtd_partition/设备树配置) add_mtd_device()mtd_device_register()mtd_erase()mtd_read()mtd_write()
19.2 NOR Flash驱动 NOR Flash特性(XIP/字节寻址/扇区擦除);CFI驱动(physmap-flash);完整W25Q64 SPI NOR驱动(JEDEC ID读取/擦除/读取/页编程/等待就绪);SPI传输实现 mtd_device_register()spi_write_then_read()spi_sync()
19.3 NAND Flash驱动 NAND Flash特性(页读写/块擦除/坏块/OOB);nand_chip结构体;cmd_ctrl/dev_ready/read_buf/write_buf四个必须实现的函数;完整NAND控制器驱动(含ECC配置/nand_scan/分区注册);ECC类型详解(Hamming/BCH/RS) nand_scan()nand_release()mtd_device_register()
19.4 MTD字符设备与块设备 /dev/mtdN字符设备;/dev/mtdblockN块设备;mtd-utils工具集(flash_erase/nandwrite/nanddump);MTD ioctl(MEMGETINFO/MEMERASE/MEMGETBADBLOCK);文件系统选择(JFFS2/UBIFS/YAFFS2/SquashFS);完整测试流程 flash_erasenandwriteMEMERASEMEMGETBADBLOCK

MTD 驱动开发要点

复制代码
1. NOR Flash 驱动
   通常使用内核提供的 CFI/JEDEC 通用驱动
   SPI NOR Flash 使用 spi-nor 框架
   只需实现 SPI 传输层

2. NAND Flash 驱动
   必须实现:cmd_ctrl、dev_ready、read_buf、write_buf
   必须配置 ECC(推荐硬件 ECC)
   使用 nand_scan() 自动检测芯片参数

3. ECC 配置
   SLC NAND:Hamming 码(1位纠错)
   MLC/TLC NAND:BCH 码(多位纠错)
   优先使用硬件 ECC(NAND_ECC_HW)

4. 坏块管理
   NAND 子系统自动处理坏块
   驱动只需实现 block_isbad 和 block_markbad

5. 文件系统选择
   NOR Flash → JFFS2
   NAND Flash → UBIFS(推荐)或 YAFFS2
   只读根文件系统 → SquashFS

6. 调试工具
   cat /proc/mtd          ← 查看 MTD 设备
   flash_erase            ← 擦除 Flash
   nandwrite/nanddump     ← 读写 NAND Flash
   nandtest               ← 测试坏块

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年