5.27 MTD设备
5.27 MTD设备
📚 本节导读
学习时长: 约 30 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言基础、Flash 存储基础知识、5.1 设备驱动框架
本节讲解 OneOS 的 MTD(Memory Technology Device,内存技术设备)。MTD 是 OneOS 对 NAND/NOR 等原始 Flash 存储器的抽象层,提供统一的页读写、块擦除接口。它最特别的设计是双接口模型:每个 MTD 设备同时注册为字符设备(按字节偏移访问,适合上层数据分区)和 MTD 设备(按页访问,适合底层驱动)。
本节会带你:
- 理解"为什么 MTD 要提供两套接口"(字符接口 vs 页接口的区别与适用场景);
- 逐个拆解字符接口的
char_dev_read/char_dev_write/char_dev_erase的完整源码(含跨页/跨块处理); - 拆解 MTD 页接口的
mtd_dev_read/mtd_dev_write/mtd_dev_erase的源码; - 掌握
mtd_device_register如何同时注册字符设备和 MTD 设备。
说明:本节所有源码均取自项目
drivers/mtd/mtd.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 MTD(Memory Technology Device)双接口设备模型
- 掌握
os_mtd_geometry几何信息结构体 - 逐个拆解字符接口读写擦除的完整源码(含跨页/跨块边界处理)
- 逐个拆解 MTD 页接口读写擦除的完整源码
- 掌握
mtd_device_register的双设备注册机制
一、概述
1.1 MTD 解决什么问题
一句话核心:MTD 设备适用于需要以统一接口访问原始 Flash(NAND/NOR),且既支持按字节偏移读写、又支持按页访问的场景。它让你"上层用字符接口按字节读写数据,底层驱动用页接口按页批量操作"。
为什么用 MTD:
- Flash 的访问单位是页/块,但上层应用(文件系统、数据分区)往往想按字节偏移读写。MTD 的字符接口在内部完成"字节偏移 → 页地址 → 跨页拼接"的转换。
- 底层驱动(NAND/SFlash)用页接口按页批量读写,更高效。
- 双接口让"上层友好"和"底层高效"兼顾。
1.2 双接口模型
MTD 设备(struct os_mtd_device)
┌────────────────────────────┐
│ char_dev(字符设备接口) │ ← 按字节偏移读写
│ mtd_dev(MTD 设备接口) │ ← 按页读写
│ geometry(页/块/容量) │
│ mtd_ops(read/write/erase)│
└────────────────────────────┘mtd_device_register 会注册两个设备:
- 以
name注册字符设备(char_dev,OS_DEVICE_TYPE_CHAR)。 - 以
name + "block"注册 MTD 设备(mtd_dev,OS_DEVICE_TYPE_MTD)。
关键理解 —— 为什么叫 name + "block":MTD 设备作为"块级"访问接口,命名上加 block 后缀区分。例如 mtd_device_register(&mtd_dev, "nand0") 会注册 nand0(字符)和 nand0block(MTD 页接口)。
1.3 MTD 的三级操作与几何信息
MTD 底层通过 mtd_ops 的三个函数操作 Flash:
struct os_mtd_ops
{
int (*read_page)(os_mtd_device_t *mtd, uint32_t page_addr, uint8_t *buff, uint32_t page_nr);
int (*write_page)(os_mtd_device_t *mtd, uint32_t page_addr, const uint8_t *buff, uint32_t page_nr);
int (*erase_block)(os_mtd_device_t *mtd, uint32_t page_addr, uint32_t page_nr);
};| 操作 | 说明 |
|---|---|
read_page | 从 page_addr 读 page_nr 页到 buff |
write_page | 把 buff 写 page_nr 页到 page_addr |
erase_block | 从 page_addr 擦除 page_nr 页对应的块 |
几何信息 os_mtd_geometry:
struct os_mtd_geometry
{
uint32_t page_size; /* page size measured in bytes */
uint32_t block_size; /* block size measured in bytes */
uint32_t capacity; /* capacity measured in bytes */
};关键理解 —— 页/块/容量的换算:
- 总页数 =
capacity / page_size。 - 每块页数 =
block_size / page_size。 - MTD 的
erase_block按"页数"来指定块(page_nr),是因为块内页数 =block_size / page_size,用页数表达块数方便统一。
1.4 MTD 使用场景
1.4.1 一句话核心
MTD 设备适用于基于 NAND/NOR Flash 做统一访问、需要同时提供字节级和页级接口的场景。它是 NAND(5.28)和 Block(5.29)之间的桥梁。
1.4.2 典型应用场景
场景一:字符接口按字节读写数据
上层通过 nand0(字符设备)用 os_device_read(dev, offset, buf, size) 按字节偏移读写任意长度数据,MTD 内部自动跨页处理:
应用读 offset=1000, size=3000(跨 2 页)
│
▼
char_dev_read
├─ 计算起始页 page_addr = 1000 / page_size
├─ 页内偏移 pos = 1000 % page_size
└─ 循环:整页读 or 页内部分读 + 拼接为什么用 MTD:字符接口让上层无需关心页边界,直接按字节偏移访问。
场景二:底层按页批量读写
底层驱动通过 nand0block(MTD 设备)按页批量读写,如文件系统扇区:
os_device_read(dev, page_addr, buffer, page_nr) /* pos=页号, size=页数 */为什么用 MTD:页接口直接映射 mtd_ops->read_page/write_page,高效且语义清晰。
1.4.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要文件系统块设备 | Block 设备(5.29) | Block 提供扇区/几何,直接供文件系统 |
| 需要分区管理 | FAL(5.17) | FAL 在 MTD 之上做分区抽象 |
| 需要逻辑坏块/ECC 管理 | NAND(5.28) | NAND 在 MTD 之上加坏块管理、ECC |
二、核心数据结构
2.1 MTD 几何信息 struct os_mtd_geometry
来源:drivers/mtd/mtd.h
struct os_mtd_geometry
{
uint32_t page_size; /* page size measured in bytes */
uint32_t block_size; /* block size measured in bytes */
uint32_t capacity; /* capacity measured in bytes */
};| 字段 | 说明 |
|---|---|
page_size | 页大小(字节,如 2048) |
block_size | 块大小(字节,如 131072,即 128KB) |
capacity | 总容量(字节,如 128MB) |
2.2 MTD 操作函数表 struct os_mtd_ops
struct os_mtd_ops
{
int (*read_page)(os_mtd_device_t *mtd, uint32_t page_addr, uint8_t *buff, uint32_t page_nr);
int (*write_page)(os_mtd_device_t *mtd, uint32_t page_addr, const uint8_t *buff, uint32_t page_nr);
int (*erase_block)(os_mtd_device_t *mtd, uint32_t page_addr, uint32_t page_nr);
};2.3 MTD 设备结构体
struct os_mtd_device
{
struct os_device char_dev;
struct os_device mtd_dev;
struct os_mtd_geometry geometry;
const struct os_mtd_ops *mtd_ops;
void *priv;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
char_dev | 字符设备接口(按字节偏移读写,注册名 = name,操作表 char_ops) |
mtd_dev | MTD 页接口(按页号读写,注册名 = name+"block",操作表 mtd_ops) |
geometry | 几何信息(页/块/容量) |
mtd_ops | 底层读写擦操作表(read_page/write_page/erase_block) |
priv | 私有数据 |
关键理解 ——
char_dev/mtd_dev双设备共用底层:char_dev_read/write/erase内部通过os_container_of(dev, os_mtd_device_t, char_dev)反推出整个os_mtd_device,再调mtd_dev->mtd_ops->*。所以两个接口最终都调用同一份mtd_ops,只是字符接口多了"字节偏移→页号→跨页拼接"的转换。
2.4 控制命令
来源:drivers/mtd/mtd.h,与项目逐字一致:
#define OS_DEVICE_CTRL_MTD_GETGEOME IOC_MTD(0) /* Get geometry information. */
#define OS_DEVICE_CTRL_MTD_SYNC IOC_MTD(1) /* Flush data to block device. */
#define OS_DEVICE_CTRL_MTD_ERASE IOC_MTD(2) /* Erase block on block device. */
#define OS_DEVICE_CTRL_MTD_AUTOREFRESH IOC_MTD(3) /* Block device : enter/exit auto refresh mode. */| 命令 | 含义 |
|---|---|
OS_DEVICE_CTRL_MTD_GETGEOME | 获取几何信息 |
OS_DEVICE_CTRL_MTD_SYNC | 把数据刷新到块设备 |
OS_DEVICE_CTRL_MTD_ERASE | 擦除块 |
OS_DEVICE_CTRL_MTD_AUTOREFRESH | 进入/退出自动刷新模式 |
三、核心源码详解
说明:以下源码均取自
drivers/mtd/mtd.c,与项目源码逐字一致(含原始英文注释)。
3.1 辅助宏(页/块移位)
来源:drivers/mtd/mtd.c
#define mtd_block_shift(mtd_dev) (os_ffs(mtd_dev->geometry.block_size) - 1)
#define mtd_page_shift(mtd_dev) (os_ffs(mtd_dev->geometry.page_size) - 1)关键理解:mtd_page_shift = log2(page_size)(如页 2048 → ffs(2048)-1 = 12-1 = 11)。用移位代替除法,把"字节偏移"转"页号"更快。
数值演算(页大小 2048,字节偏移 4096):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
mtd_page_shift | ffs(2048)-1 = 11 | 11 |
| 页号 | 4096 >> 11 | 2 |
| 页内偏移 | 4096 & (2048-1) | 0 |
3.2 字符设备读 char_dev_read
一句话定位:按字节偏移读任意长度数据,自动处理跨页拼接。
完整源码(drivers/mtd/mtd.c,与项目一致):
static os_ssize_t char_dev_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)
{
os_mtd_device_t *mtd_dev = (os_mtd_device_t *)os_container_of(dev, os_mtd_device_t, char_dev);
int ret = 0;
uint8_t *buff = buffer;
uint32_t page_addr;
uint8_t *page_buff;
uint32_t page_size;
uint32_t count, remain;
OS_ASSERT(dev);
OS_ASSERT(buff);
if (pos + size > mtd_dev->geometry.capacity)
{
os_kprintf("read [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
dev->name,
pos,
size,
mtd_dev->geometry.capacity);
return 0;
}
page_size = mtd_dev->geometry.page_size;
page_buff = os_calloc(1, page_size);
if (page_buff == OS_NULL)
{
os_kprintf("read, memroy lack %s.\r\n", dev->name);
return 0;
}
uint32_t page_shift = mtd_page_shift(mtd_dev);
page_addr = pos >> mtd_page_shift(mtd_dev);
pos &= page_size - 1;
remain = size;
while (remain != 0)
{
if (pos != 0 || remain < page_size)
{
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, page_buff, 1);
}
else
{
count = remain & ~(page_size - 1);
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, buff, remain >> page_shift);
}
if (ret != 0)
{
os_kprintf("flash device(%s) read error, %d .\r\n", dev->name, page_addr);
break;
}
if (pos != 0 || remain < page_size)
{
count = min(page_size - pos, remain);
memcpy(buff, page_buff + pos, count);
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buff += count;
remain -= count;
pos += count;
pos &= page_size - 1;
}
os_free(page_buff);
return (size - remain);
}分步详解:
第 1 步:边界校验(条件 pos + size > capacity)
if (pos + size > mtd_dev->geometry.capacity)
{
os_kprintf("read [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
dev->name,
pos,
size,
mtd_dev->geometry.capacity);
return 0;
}详细解释:若读取范围超出容量,打印越界信息(含偏移/大小/容量)并返回 0(读失败)。
第 2 步:申请页缓冲并计算起始页(条件 page_buff == OS_NULL)
page_size = mtd_dev->geometry.page_size;
page_buff = os_calloc(1, page_size);
if (page_buff == OS_NULL)
{
os_kprintf("read, memroy lack %s.\r\n", dev->name);
return 0;
}
uint32_t page_shift = mtd_page_shift(mtd_dev);
page_addr = pos >> mtd_page_shift(mtd_dev);
pos &= page_size - 1;详细解释:申请一页的临时缓冲(用于页内部分读)。page_addr = pos >> page_shift 得到起始页号,pos &= page_size - 1 把 pos 变成页内偏移。
第 3 步:循环读数据直到读完(条件 remain != 0)
remain = size;
while (remain != 0)
{
if (pos != 0 || remain < page_size)
{
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, page_buff, 1);
}
else
{
count = remain & ~(page_size - 1);
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, buff, remain >> page_shift);
}
if (ret != 0)
{
os_kprintf("flash device(%s) read error, %d .\r\n", dev->name, page_addr);
break;
}详细解释:remain 是剩余待读字节数。分支判断:
pos != 0(页内偏移非零)或remain < page_size(不足一页):只能读1 页到临时缓冲page_buff。- 否则(页对齐且至少一页):按
remain >> page_shift一次读多页直接进用户缓冲。 - 读失败(
ret != 0)则打印错误并break退出循环。
第 4 步:拼接/推进指针(无条件执行)
if (pos != 0 || remain < page_size)
{
count = min(page_size - pos, remain);
memcpy(buff, page_buff + pos, count);
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buff += count;
remain -= count;
pos += count;
pos &= page_size - 1;详细解释:若刚才是"部分页读",从 page_buff + pos 拷贝 count = min(page_size - pos, remain) 字节到用户缓冲,页号 +1;若是整页读,页号直接推进 remain >> page_shift。然后推进 buff、减去 remain、更新页内偏移 pos。
第 5 步:释放缓冲并返回(无条件执行)
os_free(page_buff);
return (size - remain);
}详细解释:释放临时缓冲,返回实际读字节数 size - remain(remain 已是 0)。
char_dev_read() 条件走向总结:
char_dev_read() 条件走向总结:
├── pos + size > capacity(越界)→ 打印 + 返回 0
├── page_buff 分配失败 → 打印 + 返回 0
├── page_addr = pos >> page_shift;pos &= page_size - 1
├── while (remain != 0):
│ ├── (pos != 0 或 remain < page_size) → 读 1 页到 page_buff
│ │ └── 拷贝 min(page_size - pos, remain) 字节;page_addr++
│ ├── 否则 → 一次读 remain>>page_shift 页到 buff;page_addr += remain>>page_shift
│ ├── 读失败 → 打印 + break
│ ├── buff += count;remain -= count;pos = (pos + count) & (page_size-1)
├── os_free(page_buff)
└── 返回 size - remain3.3 字符设备写 char_dev_write
一句话定位:按字节偏移写任意长度数据,跨页时"预读旧页再改"。
完整源码(drivers/mtd/mtd.c,与项目一致):
static os_ssize_t char_dev_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size)
{
os_mtd_device_t *mtd_dev = (os_mtd_device_t *)os_container_of(dev, os_mtd_device_t, char_dev);
int ret = 0;
const uint8_t *buff = buffer;
uint32_t page_addr;
uint8_t *page_buff;
uint32_t page_size;
uint32_t count, remain;
OS_ASSERT(mtd_dev);
OS_ASSERT(buff);
if (pos + size > mtd_dev->geometry.capacity)
{
os_kprintf("write [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
dev->name,
pos,
size,
mtd_dev->geometry.capacity);
return 0;
}
remain = size;
page_size = mtd_dev->geometry.page_size;
page_buff = os_calloc(1, page_size);
if (page_buff == OS_NULL)
{
os_kprintf("write, memroy lack %s.\r\n", dev->name);
return 0;
}
uint32_t page_shift = mtd_page_shift(mtd_dev);
page_addr = pos >> page_shift;
pos &= page_size - 1;
write_loop:
if (pos != 0 || remain < page_size)
{
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, page_buff, 1);
if (ret != 0)
{
os_kprintf("pre read failed %s, %d .\r\n", dev->name, page_addr);
goto end;
}
}
while (remain != 0)
{
if (pos != 0 || remain < page_size)
{
count = min(page_size - pos, remain);
memcpy(page_buff + pos, buff, count);
ret = mtd_dev->mtd_ops->write_page(mtd_dev, page_addr, page_buff, 1);
}
else
{
count = remain & ~(page_size - 1);
ret = mtd_dev->mtd_ops->write_page(mtd_dev, page_addr, buff, remain >> page_shift);
}
if (ret != 0)
{
os_kprintf("write failed %s, %d .\r\n", dev->name, page_addr);
break;
}
if (pos != 0 || remain < page_size)
{
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buff += count;
remain -= count;
pos += count;
pos &= page_size - 1;
/* last not full page */
if ((remain < page_size) && (remain != 0))
goto write_loop;
}
end:
os_free(page_buff);
return (size - remain);
}分步详解:
第 1 步:边界校验与准备(条件 pos + size > capacity;page_buff == OS_NULL)
if (pos + size > mtd_dev->geometry.capacity)
{
os_kprintf("write [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
dev->name, pos, size, mtd_dev->geometry.capacity);
return 0;
}
remain = size;
page_size = mtd_dev->geometry.page_size;
page_buff = os_calloc(1, page_size);
if (page_buff == OS_NULL)
{
os_kprintf("write, memroy lack %s.\r\n", dev->name);
return 0;
}
page_addr = pos >> page_shift;
pos &= page_size - 1;详细解释:与读操作相同的越界检查(打印越界信息返回 0)和页缓冲申请(分配失败返回 0),计算起始页号 page_addr = pos >> page_shift 与页内偏移 pos &= page_size - 1。
第 2 步:跨页写前"预读旧页"(条件 pos != 0 或 remain < page_size)
write_loop:
if (pos != 0 || remain < page_size)
{
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, page_buff, 1);
if (ret != 0)
{
os_kprintf("pre read failed %s, %d .\r\n", dev->name, page_addr);
goto end;
}
}详细解释:为什么写前要预读整页:Flash 只能整页编程,若只改页的一部分,必须先把整页旧数据读出来(page_buff),改其中一部分,再整页写回,否则未改动部分会变成 0xFF(丢数据)。write_loop: 是标签,供循环末尾跳回。
第 3 步:循环写入(条件 remain != 0)
while (remain != 0)
{
if (pos != 0 || remain < page_size)
{
count = min(page_size - pos, remain);
memcpy(page_buff + pos, buff, count);
ret = mtd_dev->mtd_ops->write_page(mtd_dev, page_addr, page_buff, 1);
}
else
{
count = remain & ~(page_size - 1);
ret = mtd_dev->mtd_ops->write_page(mtd_dev, page_addr, buff, remain >> page_shift);
}
if (ret != 0)
{
os_kprintf("write failed %s, %d .\r\n", dev->name, page_addr);
break;
}详细解释:与读对应:部分页写时,把数据拷进 page_buff + pos 再整页写;整页写时直接批量写。写失败(ret != 0)则打印错误并 break。
第 4 步:推进指针与循环收尾(无条件执行)
if (pos != 0 || remain < page_size)
{
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buff += count;
remain -= count;
pos += count;
pos &= page_size - 1;
/* last not full page */
if ((remain < page_size) && (remain != 0))
goto write_loop;详细解释:推进指针。关键:若剩余不足一页且非零("最后不完整的页"),跳回 write_loop 重新"预读旧页再写",确保最后一页也能正确部分写。
第 5 步:释放并返回(无条件执行)
end:
os_free(page_buff);
return (size - remain);
}详细解释:释放缓冲,返回实际写字节数。
char_dev_write() 条件走向总结:
char_dev_write() 条件走向总结:
├── 越界 → 返回 0;页缓冲分配失败 → 返回 0
├── page_addr = pos >> page_shift;pos &= page_size - 1
├── write_loop:(若 pos != 0 或 remain < page_size)
│ ├── 预读整页到 page_buff(失败 → goto end)
├── while (remain != 0):
│ ├── 部分页 → 改 page_buff 再整页写;整页 → 批量写
│ ├── 写失败 → 打印 + break
│ ├── 推进 buff/remain/pos
│ └── (remain < page_size 且 remain != 0) → goto write_loop
├── end: 释放缓冲
└── 返回 size - remain3.4 字符设备擦除 char_dev_erase
完整源码(drivers/mtd/mtd.c,与项目一致):
static int char_dev_erase(os_device_t *dev, uint32_t offset, size_t size)
{
int ret = 0;
os_mtd_device_t *mtd_dev = (os_mtd_device_t *)os_container_of(dev, os_mtd_device_t, char_dev);
OS_ASSERT(mtd_dev);
if (offset + size > mtd_dev->geometry.capacity)
{
os_kprintf("part erase [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
dev->name,
offset,
size,
mtd_dev->geometry.capacity);
return -1;
}
uint32_t end;
uint32_t page_addr;
uint32_t block_size = mtd_dev->geometry.block_size;
uint32_t page_shift = mtd_page_shift(mtd_dev);
end = offset + size;
page_addr = offset >> page_shift;
offset &= ~(block_size - 1);
if ((end - offset) & ~(block_size - 1))
{
ret = mtd_dev->mtd_ops->erase_block(mtd_dev, page_addr, ((end - offset) & ~(block_size - 1)) >> page_shift);
if (ret != 0)
{
os_kprintf("erase failed %s, %d.\r\n", dev->name, page_addr);
return -1;
}
page_addr += ((end - offset) & ~(block_size - 1)) >> page_shift;
}
if ((end - offset) & (block_size - 1))
{
ret = mtd_dev->mtd_ops->erase_block(mtd_dev, page_addr, 1);
if (ret != 0)
{
os_kprintf("erase failed %s, %d.\r\n", dev->name, page_addr);
return -2;
}
return ((end - offset) & ~(block_size - 1)) + block_size;
}
return (end - offset) & ~(block_size - 1);
}分步详解:
第 1 步:边界校验(条件 offset + size > capacity)
if (offset + size > mtd_dev->geometry.capacity)
{
os_kprintf("part erase [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
dev->name,
offset,
size,
mtd_dev->geometry.capacity);
return -1;
}详细解释:擦除范围越界返回 -1。
第 2 步:计算块对齐后的范围(无条件执行)
end = offset + size;
page_addr = offset >> page_shift;
offset &= ~(block_size - 1);详细解释:end 是擦除终点。page_addr 是起始页号。offset &= ~(block_size - 1) 把 offset 向下对齐到块边界(抹掉块内偏移),这样从对齐块开始擦。
第 3 步:擦除完整块部分(条件 有整块可擦)
if ((end - offset) & ~(block_size - 1))
{
ret = mtd_dev->mtd_ops->erase_block(mtd_dev, page_addr, ((end - offset) & ~(block_size - 1)) >> page_shift);
if (ret != 0)
{
os_kprintf("erase failed %s, %d.\r\n", dev->name, page_addr);
return -1;
}
page_addr += ((end - offset) & ~(block_size - 1)) >> page_shift;
}详细解释:(end - offset) & ~(block_size - 1) 是擦除范围中整块对齐的字节数。若有整块(非 0),擦除这些块对应的页(失败则打印并返回 -1),并把 page_addr 推进到剩余部分。
第 4 步:擦除剩余不满一个块的"尾块"(条件 有尾块)
if ((end - offset) & (block_size - 1))
{
ret = mtd_dev->mtd_ops->erase_block(mtd_dev, page_addr, 1);
if (ret != 0)
{
os_kprintf("erase failed %s, %d.\r\n", dev->name, page_addr);
return -2;
}
return ((end - offset) & ~(block_size - 1)) + block_size;
}
return (end - offset) & ~(block_size - 1);
}详细解释:(end - offset) & (block_size - 1) 是擦除范围中不足一个块的尾块。若有尾块,需要多擦一个整块(因为 Flash 块是最小擦除单位,失败则打印并返回 -2),并返回 整块字节数 + block_size(实际擦除的字节数)。否则直接返回整块字节数。
关键理解 —— 为什么尾块要多擦一整块:Flash 块是最小擦除单位,无法只擦块内一部分。即使只差 1 字节也要擦一个整块,所以返回的"实际擦除大小"可能大于请求的大小。
char_dev_erase() 条件走向总结:
char_dev_erase() 条件走向总结:
├── 越界 → 返回 -1
├── page_addr = offset >> page_shift;offset 下对齐到块边界
├── 若有整块((end-offset) & ~(block_size-1) 非 0)
│ ├── 擦整块对应页;失败 → 返回 -1
│ └── page_addr 推进
├── 若有尾块((end-offset) & (block_size-1) 非 0)
│ ├── 擦 1 块;失败 → 返回 -2
│ └── 返回 整块字节数 + block_size
└── 否则返回 整块字节数3.5 MTD 页接口 mtd_dev_read / mtd_dev_write / mtd_dev_erase
一句话定位:MTD 设备接口直接映射到 mtd_ops,语义为"按页号访问"。
完整源码(drivers/mtd/mtd.c,与项目一致):
static os_ssize_t mtd_dev_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)
{
int ret;
os_mtd_device_t *mtd_dev = (os_mtd_device_t *)os_container_of(dev, os_mtd_device_t, mtd_dev);
uint32_t page_addr = pos;
uint32_t page_nr = size;
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, buffer, page_nr);
if (ret == OS_SUCCESS)
return size;
return 0;
}分步详解:
第 1 步:直接把 pos 当页号、size 当页数(无条件执行)
uint32_t page_addr = pos;
uint32_t page_nr = size;
ret = mtd_dev->mtd_ops->read_page(mtd_dev, page_addr, buffer, page_nr);详细解释:关键区别:MTD 页接口的 pos 语义是页号(不是字节偏移),size 语义是页数(不是字节数)。直接透传给 mtd_ops->read_page。这与字符接口(pos 是字节偏移)完全不同。
第 2 步:返回读写结果(无条件执行)
if (ret == OS_SUCCESS)
return size;
return 0;
}详细解释:成功返回 size(页数),失败返回 0。
mtd_dev_write / mtd_dev_erase 结构类似(write 直接透传 write_page,erase 带越界检查后透传 erase_block):
static os_ssize_t mtd_dev_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size)
{
int ret;
os_mtd_device_t *mtd_dev = (os_mtd_device_t *)os_container_of(dev, os_mtd_device_t, mtd_dev);
uint32_t page_addr = pos;
uint32_t page_nr = size;
ret = mtd_dev->mtd_ops->write_page(mtd_dev, page_addr, buffer, page_nr);
if (ret == OS_SUCCESS)
return size;
return 0;
}mtd_dev_erase(带越界检查):
static int mtd_dev_erase(os_device_t *dev, uint32_t page_addr, size_t page_nr)
{
int ret = 0;
os_mtd_device_t *mtd_dev = (os_mtd_device_t *)os_container_of(dev, os_mtd_device_t, mtd_dev);
OS_ASSERT(mtd_dev);
if (page_addr + page_nr > mtd_dev->geometry.capacity / mtd_dev->geometry.page_size)
{
os_kprintf("erase [%s] address out of bound page_addr:0x%x, page_nr:0x%x.\r\n",
dev->name,
page_addr,
page_nr);
return 0;
}
ret = mtd_dev->mtd_ops->erase_block(mtd_dev, page_addr, page_nr);
if (ret != 0)
{
os_kprintf("erase failed %s, %d.\r\n", dev->name, page_addr);
return 0;
}
return page_nr;
}详细解释:mtd_dev_erase 先校验 page_addr + page_nr 不超过总页数(capacity / page_size),再调用 mtd_ops->erase_block。
3.6 mtd_device_register —— 双设备注册
完整源码(drivers/mtd/mtd.c,与项目一致):
os_err_t mtd_device_register(os_mtd_device_t *mtd_dev, const char *name)
{
char mtd_name[32] = {0};
OS_ASSERT(mtd_dev->mtd_ops != OS_NULL);
OS_ASSERT(mtd_dev->mtd_ops->read_page != OS_NULL);
OS_ASSERT(mtd_dev->mtd_ops->write_page != OS_NULL);
OS_ASSERT(mtd_dev->mtd_ops->erase_block != OS_NULL);
/* mtd device */
mtd_dev->mtd_dev.type = OS_DEVICE_TYPE_MTD;
mtd_dev->mtd_dev.ops = &mtd_ops;
strcat(mtd_name, name);
strcat(mtd_name, "block");
os_device_register(&mtd_dev->mtd_dev, mtd_name);
/* char device */
mtd_dev->char_dev.type = OS_DEVICE_TYPE_CHAR;
mtd_dev->char_dev.ops = &char_ops;
os_device_register(&mtd_dev->char_dev, name);
return OS_SUCCESS;
}分步详解:
第 1 步:断言操作函数表完整(无条件执行)
OS_ASSERT(mtd_dev->mtd_ops != OS_NULL);
OS_ASSERT(mtd_dev->mtd_ops->read_page != OS_NULL);
OS_ASSERT(mtd_dev->mtd_ops->write_page != OS_NULL);
OS_ASSERT(mtd_dev->mtd_ops->erase_block != OS_NULL);详细解释:MTD 必须实现读页、写页、擦块三个函数,缺失则断言失败(开发期发现配置错误)。
第 2 步:注册 MTD 设备(name + "block")(无条件执行)
mtd_dev->mtd_dev.type = OS_DEVICE_TYPE_MTD;
mtd_dev->mtd_dev.ops = &mtd_ops;
strcat(mtd_name, name);
strcat(mtd_name, "block");
os_device_register(&mtd_dev->mtd_dev, mtd_name);详细解释:给 mtd_dev 设置类型 OS_DEVICE_TYPE_MTD 和操作表 mtd_ops(页接口),用 name + "block" 注册。为什么加 block 后缀:让页接口设备名与字符设备区分开。
第 3 步:注册字符设备(name)(无条件执行)
mtd_dev->char_dev.type = OS_DEVICE_TYPE_CHAR;
mtd_dev->char_dev.ops = &char_ops;
os_device_register(&mtd_dev->char_dev, name);
return OS_SUCCESS;
}详细解释:给 char_dev 设置类型 OS_DEVICE_TYPE_CHAR 和操作表 char_ops(字节接口),用原名 name 注册。
mtd_device_register() 条件走向总结:
mtd_device_register() 条件走向总结:
├── OS_ASSERT:mtd_ops 及 read/write/erase 非空
├── mtd_dev.type = MTD;mtd_dev.ops = &mtd_ops
├── 以 name + "block" 注册 MTD 设备
├── char_dev.type = CHAR;char_dev.ops = &char_ops
└── 以 name 注册字符设备 → 返回 OS_SUCCESS四、完整使用示例
示例:注册 MTD 设备并通过双接口访问
第 1 部分:实现 MTD 操作函数并注册
#include <mtd.h>
#include <os_stddef.h>
/* 底层读写擦函数(实际驱动中调用具体 Flash 芯片) */
static int my_mtd_read_page(os_mtd_device_t *mtd, uint32_t page_addr,
uint8_t *buff, uint32_t page_nr) { return page_nr; }
static int my_mtd_write_page(os_mtd_device_t *mtd, uint32_t page_addr,
const uint8_t *buff, uint32_t page_nr) { return page_nr; }
static int my_mtd_erase_block(os_mtd_device_t *mtd, uint32_t page_addr,
uint32_t page_nr) { return OS_SUCCESS; }
static const struct os_mtd_ops my_mtd_ops = {
.read_page = my_mtd_read_page, .write_page = my_mtd_write_page,
.erase_block = my_mtd_erase_block,
};
void mtd_device_init(void)
{
static os_mtd_device_t mtd_dev;
os_err_t ret;
/* 配置几何信息 */
mtd_dev.geometry.page_size = 2048; /* 页 2KB */
mtd_dev.geometry.block_size = 131072; /* 块 128KB */
mtd_dev.geometry.capacity = 128 * 1024 * 1024; /* 128MB */
mtd_dev.mtd_ops = &my_mtd_ops;
/* 注册:同时注册 nand0(字符)和 nand0block(MTD 页接口) */
ret = mtd_device_register(&mtd_dev, "nand0");
if (ret != OS_SUCCESS)
{
os_kprintf("mtd register failed\r\n");
return;
}
}第 2 部分:通过字符接口按字节读写
#include <device.h>
#include <os_stddef.h>
void mtd_char_access_demo(void)
{
os_device_t *dev;
uint8_t buf[3000]; /* 跨 2 页(3000 > 2048) */
os_ssize_t n;
/* 打开字符设备(字节接口) */
dev = os_device_open("nand0");
if (dev == OS_NULL) return;
/* 从字节偏移 1000 读 3000 字节,内部自动跨页拼接 */
n = os_device_read(dev, 1000, buf, sizeof(buf));
if (n != sizeof(buf))
{
os_kprintf("read failed, n=%d\r\n", n);
return;
}
os_kprintf("char read ok, %d bytes\r\n", n);
}第 3 部分:通过 MTD 页接口按页访问
void mtd_page_access_demo(void)
{
os_device_t *dev;
uint8_t page_buf[2048];
os_ssize_t n;
/* 打开 MTD 设备(页接口,名带 block 后缀) */
dev = os_device_open("nand0block");
if (dev == OS_NULL) return;
/* pos = 页号 5,size = 1(页),读第 5 页 */
n = os_device_read(dev, 5, page_buf, 1);
if (n != 1)
{
os_kprintf("page read failed\r\n");
return;
}
os_kprintf("page read ok, page 5\r\n");
}说明:示例展示 MTD 双接口的标准用法。实际效果:
mtd_device_register同时注册字符设备和 MTD 页接口设备;字符接口pos/size是字节偏移/字节数(自动跨页),MTD 接口pos/size是页号/页数(直接透传)。
注意:MTD 页接口(
nand0block)的pos是页号、size是页数,与字符接口(nand0)的字节语义不同,使用时不要混淆。跨页写时字符接口会"预读旧页再改",这是 Flash 整页编程的要求。
五、MTD 的适配(接入新 Flash 芯片)
5.1 先认识适配层:MTD 框架怎么"长"到 Flash 上
drivers/mtd/mtd.c 是跨平台统一框架(字符/页双接口、跨页转换、擦除对齐、注册),不碰具体 Flash 寄存器。真正读写擦具体芯片(NOR/NAND)的是各芯片/厂商适配(如 mtd_nand.c、mtd_nor.c、或基于 FAL 的 fal_mtd.c)。换 Flash 芯片,就是实现一套 os_mtd_ops 并 mtd_device_register,框架层不用改。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用层(字符接口 nand0 / 页接口 nand0block) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ MTD 框架层(drivers/mtd/mtd.c,通用不改) │
│ char_dev 跨页读写 + mtd_dev 页接口 + 擦除对齐 │
└───────────────┬──────────────────────────┬────────────────┘
│ ①实现 os_mtd_ops 3 回调 │ ②mtd_device_register
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 芯片适配层(mtd_nand.c / mtd_nor.c / fal_mtd.c)★需实现★│
│ read_page / write_page / erase_block(页号语义) │
└───────────────┬──────────────────────────┬────────────────┘
│ HAL/底层驱动 │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(NAND / NOR Flash 芯片) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入新 Flash 只需做两件事:① 实现 struct os_mtd_ops 三个回调(read_page/write_page/erase_block,参数都是页号/页数);② 填 os_mtd_device_t 的 geometry(页/块/容量)并 mtd_device_register 注册。框架层不用改。
5.2 接口契约:接入 Flash 需要实现什么
| 层 | 接口/回调 | 职责 | 参数语义 |
|---|---|---|---|
| 操作回调 | os_mtd_ops.read_page | 读 page_nr 页到 buff | page_addr=页号、page_nr=页数 |
| 操作回调 | os_mtd_ops.write_page | 写 page_nr 页 | 同上 |
| 操作回调 | os_mtd_ops.erase_block | 擦 page_nr 页(须块对齐) | 同上 |
| 几何信息 | os_mtd_device.geometry | 页大小/块大小/容量 | 填对则框架跨页/对齐正确 |
| 注册 | mtd_device_register | 注册字符 + 页双接口 | 自动分配双设备 |
5.3 适配新 Flash 芯片的标准流程
第 1 步:查手册确认几何——页大小、块大小、容量,以及 NAND 是否需要 OOB/坏块处理(NAND 需在 write_page 后处理 ECC)。
第 2 步:实现 os_mtd_ops 三回调——按页号/页数实现读写擦,页号换算成芯片真实地址(page_addr * page_size 或直接给控制器)。
第 3 步:填几何并注册——填 geometry(page_size/block_size/capacity)和 mtd_ops,mtd_device_register(dev, name)。
第 4 步:测试——字符接口 os_device_read/write/control(ERASE) 验证跨页读写;页接口验证页号语义;擦除验证块对齐。
5.4 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
read_page/write_page/erase_block 都实现? | 漏一个则对应操作失败(空回调) |
| 页号换算正确? | page_addr 是页号,换算成真实地址错误则数据错位 |
geometry 填对? | 页/块/容量错则字符接口跨页、擦除对齐误判 |
| NAND 是否处理 ECC/坏块? | NAND 需 ECC(硬件或软件),漏则数据易错 |
| 擦除是否块对齐? | 底层 erase_block 要求页号按块对齐,框架已保证,底层需支持 |
📝 本节小结
核心要点回顾
本节围绕 MTD 内存技术设备展开,核心要点如下:
1. 双接口模型
- 字符接口(
name):pos/size是字节偏移/字节数,自动跨页拼接。 - MTD 页接口(
name + "block"):pos/size是页号/页数,直接透传mtd_ops。
2. 字符接口的关键处理
- 读:
page_addr = pos >> page_shift、pos &= page_size - 1,循环读页 + 拼接。 - 写:跨页部分写前预读旧页再改(Flash 整页编程要求),最后一页通过
goto write_loop重新预读。 - 擦:整块部分批量擦,尾块多擦一整块(块是最小擦除单位)。
3. 几何信息
page_size/block_size/capacity;总页数 =capacity / page_size。mtd_page_shift = log2(page_size),用移位代替除法。
4. 注册机制
mtd_device_register断言三个操作非空,注册name(字符)和name+"block"(MTD)。
5. 硬件适配
- 接入新 Flash = 实现
os_mtd_ops三回调(read_page/write_page/erase_block,页号语义)+ 填geometry+mtd_device_register,框架层不用改。 - NAND 需处理 ECC/坏块;NOR 无需擦前读但写前需擦。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 混淆两套接口的语义 | 字符接口 pos 是字节偏移、size 是字节数;MTD 接口 pos 是页号、size 是页数,两者完全不同 |
忘记 MTD 设备名带 block | MTD 页接口注册名是 name + "block",打开时要用 "nand0block" |
| 跨页写未预读旧页 | 字符写跨页部分写会预读旧页,这是 Flash 整页编程要求;自己实现底层时不要漏 |
| 擦除时忽略块对齐 | Flash 块是最小擦除单位,尾块也要多擦一整块,实际擦除大小可能大于请求大小 |
| 注册时漏实现某个操作 | mtd_device_register 用 OS_ASSERT 强制要求 read/write/erase 三个函数都实现 |
| 越界检查缺失 | 字符读/写/擦和 MTD 擦都有越界检查(pos + size > capacity),底层不要绕过 |
| 底层页号换算错 | os_mtd_ops 回调的 page_addr 是页号,换算成真实芯片地址错则数据错位 |
| NAND 未处理 ECC/坏块 | NAND 需 ECC(硬件/软件)和坏块标记,漏则数据易错或写到坏块 |
下一步
掌握了 MTD 内存技术设备后,接下来学习 NAND Flash 设备:
- 5.28 NAND设备:了解 NAND 的页/块/区三级寻址、坏块管理与硬件 ECC。