Linux 之 MTD 子系统框架
一、MTD 简介
MTD,Memory Technology Device即内存技术设备
- 字符设备和块设备的区别在于前者只能被顺序读写,后者可以随机访问;同时,两者读写数据的基本单元不同。
- 字符设备,以字节为基本单位,在Linux中,字符设备实现的比较简单,不需要缓冲区即可直接读写,内核例程和用户态API一一对应,用户层的Read函数直接对应了内核中的Read例程,这种映射关系由字符设备的file_operations维护。
- 块设备,则以块为单位接受输入和返回输出。对这种设备的读写是按块进行的,其接口相对于字符设备复杂,read、write API没有直接到块设备层,而是直接到文件系统层,然后再由文件系统层发起读写请求。
- 同时,由于块设备的IO性能与CPU相比很差,因此,块设备的数据流往往会引入文件系统的Cache机制。
- MTD设备既非块设备也不是字符设备,但可以同时提供字符设备和块设备接口来操作它。
二、MTD 在内核中的框架
MTD的所有源码位于/drivers/mtd子目录下:
2.1 flash 硬件层
硬件驱动层负责在init时驱动Flash硬件并建立从具体设备到MTD原始设备映射关系
tip: 映射关系通常包括 分区信息、I/O映射及特定函数的映射
drivers/mtd/chips : CFI/jedec接口通用驱动
drivers/mtd/nand : nand通用驱动和部分底层驱动程序
drivers/mtd/maps : nor flash映射关系相关函数
drivers/mtd/spi-nor: nor flash底层驱动
2.2 MTD原始设备层
用于描述MTD原始设备的数据结构是mtd_info,它定义了大量的关于MTD的数据和操作函数。
MTD原始设备层由两部分构成,一部分是MTD原始设备的通用代码,另一部分是各个特定Flash的数据,比如分区。
主要构成的文件有:
mtdcore.c : MTD原始设备接口相关实现
mtdpart.c : MTD分区接口相关实现
用于描述 MTD原始设备的数据结构是mtd_info,这其中定义了大量的关于MTD的数据和操作函数。
- mtd_idr(mtdcore.c=>static DEFINE_IDR(mtd_idr))是所有MTD原始设备的列表,所有mtd设备通过i = idr_alloc(&mtd_idr, mtd, 0, 0, GFP_KERNEL)等类似操作与mtd_idr建立连接。
- mtd_part(mtdpart.c)是用于表示MTD原始设备分区的结构,其中包含了mtd_info,因为每一个分区都是被看成一个MTD原始设备加在mtd_idr中的,mtd_part.mtd中的大部分数据都从该分区的主分区mtd_part->master中获得(add_mtd_partitions)。
- 在drivers/mtd/maps/子目录下存放的是特定的flash的数据,每一个文件都描述了一块板子上的flash。其中调用add_mtd_device()、del_mtd_device()建立/删除mtd_info结构,并将其加入/删除mtd_idr(或者调用add_mtd_partition()、del_mtd_partition()(mtdpart.c)建立/删除mtd_part结构并将mtd_part.mtd_info加入/删除mtd_idr 中)。
2.3 MTD 设备层
基于MTD原始设备,linux系统可以定义出MTD的块设备(主设备号31)和字符设备(设备号90)。
mtdchar.c : MTD字符设备接口相关实现
mtdblock.c : MTD块设备接口相关实现
- MTD字符设备的定义在mtdchar.c中实现,注册一系列file operation函数(lseek、open、close、read、write)。
- MTD块设备的定义在mtdblock.c(其调用mtd_blkdevs.c的函数),在mtd_blkdevs.c=> add_mtd_blktrans_dev函数中,以mtd_blkdevs.c=> block_device_operations mtd_block_ops为ops,以mtdblock.c=>mtd_blktrans_ops mtdblock_tr的.name和.major注册块设备、生成块设备节点。
2.4 MTD设备节点
通过mknod在/dev子目录下建立MTD块设备节点(主设备号为31)和MTD字符设备节点(主设备号为90),/dev/mtdN是字符设备节点,/dev/mtdblockN是块设备节点。
通过访问此设备节点即可访问MTD字符设备和块设备
/dev/mtdN和/dev/mtdblockN的关系和区别:
1.它们是对同一分区的不同的描述方法
2.生成节点的方法
字符节点是mtdchar.c注册生成的,支持open,read,write,ioctl等。flash_erase, nanddump等都是对字符节点,通过read,write,ioctl等执行。见分层调用流程
块设备节点对应mtdblock.c和mtd_blkdevs.c,是Flash驱动中用add_mtd_partitions()添加MTD设备分区,而生成的对应的块设备。
MTD块设备驱动程序可以让flash器件伪装成块设备,实际上它通过把整块的erase block放到ram里面进行访问,然后再更新到flash,用户可以在这个块设备上创建通常的文件系统。
对于MTD块设备,MTD设备层是不提供ioctl的实现方法的,不能使用nandwrite,flash_eraseall,flash_erase等工具去对/dev/mtdblockN去进行操作。3.mtd-utils工具只能用与/dev/mtdN的MTD字符设备。mount、umount命令只对/dev/mtdblockN的MTD块设备有效。
2.5 文件系统
- 在Bootloader中将JFFS(或JFFS2)的文件系统映像jffs.image(或jffs2.img)烧到flash的某一个分区中,在/arch/arm/mach-your/arch.c文件的your_fixup函数中将该分区作为根文件系统挂载。
- 内核启动后,通过mount 命令可以将flash中的其余分区作为文件系统挂载到mountpoint上。
三、MTD 数据结构
重要的数据结构:
- mtd_info 表示mtd原始设备, 所有mtd_info结构体被存放在mtd_info数组mtd_table中
- mtd_part 表示MTD分区,其中包含了 mtd_info,每一个分区都是被看成一个MTD 原始设备
四、MTD相关层实现
4.1 MTD 设备层
mtd字符设备接口:
mtdchar.c 实现了字符设备接口,通过它,用户可以直接操作Flash 设备。
Ø 通过read()、write()系统调用可以读写Flash。
Ø 通过一系列IOCTL 命令可以获取Flash 设备信息、擦除Flash、读写NAND 的OOB、获取OOB layout 及检查NAND 坏块等(MEMGETINFO、MEMERASE、MEMREADOOB、MEMWRITEOOB、MEMGETBADBLOCK IOCRL)
tip: mtd_read和mtd_write直接直接调用mtd_info的read 函数,因此,字符设备接口跳过patition这一层
mtd块设备接口:
主要原理是将Flash的erase block 中的数据在内存中建立映射,然后对其进行修改,最后擦除Flash 上的block,将内存中的映射块写入Flash 块。整个过程被称为read/modify/erase/rewrite 周期。
但是,这样做是不安全的,当下列操作序列发生时,read/modify/erase/poweroff,就会丢失这个block 块的数据。
块设备模拟驱动按照block 号和偏移量来定位文件,因此在Flash 上除了文件数据,基本没有额外的控制数据。
4.2 MTD硬件驱动层
NOR Flash驱动结构
Linux系统实现了针对cfi,jedec等接口的通用NOR Flash驱动
在上述接口驱动基础上,芯片级驱动较简单
定义具体内存映射结构体map_info,然后通过接口类型后调用do_map_probe()
以h720x-flash.c为例(位于drivers/mtd/maps)
- 定义map_info结构体, 初始化成员name, size, phys, bankwidth
- 通过ioremap映射成员virt(虚拟内存地址)
- 通过函数simple_map_init初始化map_info成员函数read,write,copy_from,copy_to
- 调用do_map_probe进行cfi接口探测, 返回mtd_info结构体
- 通过parse_mtd_partitions, add_mtd_partitions注册mtd原始设备
NAND Flash驱动结构
Linux实现了通用NAND驱动(drivers/mtd/nand/nand_base.c)
tip: For more, check 内核中的NAND代码布局
芯片级驱动需要实现nand_chip结构体
MTD使用nand_chip来表示一个NAND FLASH芯片, 该结构体包含了关于Nand Flash的地址信息,读写方法,ECC模式,硬件控制等一系列底层机制。
Ø NAND芯片级初始化
主要有以下几个步骤:
- 分配nand_chip内存,根据目标板及NAND控制器初始化nand_chip中成员函数(若未初始化则使用nand_base.c中的默认函数),将mtd_info中的priv指向nand_chip(或板相关私有结构),设置ecc模式及处理函数
- 以mtd_info为参数调用nand_scan()探测NAND FLash。
nand_scan_ident()会读取nand芯片ID,并根据mtd->priv即nand_chip中成员初始化mtd_info
- 若有分区,则以mtd_info和mtd_partition为参数调用add_mtd_partitions()添加分区信息
Ø MTD对NAND芯片的读写
主要分三部分:
A、struct mtd_info中的读写函数,如read,write_oob等,这是MTD原始设备层与FLASH硬件层之间的接口;
B、struct nand_ecc_ctrl中的读写函数,如read_page_raw,write_page等,主要用来做一些与ecc有关的操作;
C、struct nand_chip中的读写函数,如read_buf,cmdfunc等,与具体的NAND controller相关,就是这部分函数与硬件交互,通常需要我们自己来实现。
tip: nand_chip中的读写函数虽然与具体的NAND controller相关,但是MTD也为我们提供了默认的读写函数,如果NAND controller比较通用(使用PIO模式),那么对NAND芯片的读写与MTD提供的这些函数一致,就不必自己实现这些函数。
eg: 以读为例
MTD上层会调用struct mtd_info中的读page函数,即nand_read函数。
接着nand_read函数会调用struct nand_chip中cmdfunc函数,这个cmdfunc函数与具体的NAND controller相关,它的作用是使NAND controller向NAND 芯片发出读命令,NAND芯片收到命令后,就会做好准备等待NAND controller下一步的读取。
接着nand_read函数又会调用struct nand_ecc_ctrl中的read_page函数,而read_page函数又会调用struct nand_chip中read_buf函数,从而真正把NAND芯片中的数据读取到buffer中(所以这个read_buf的意思其实应该是read into buffer,另外,这个buffer是struct mtd_info中的nand_read函数传下来的)。
read_buf函数返回后,read_page函数就会对buffer中的数据做一些处理,比如校验ecc,以及若数据有错,就根据ecc对数据修正之类的,最后read_page函数返回到nand_read函数中。
对NAND芯片的其它操作,如写,擦除等,都与读操作类似 。
五、spi-nor flash 的注册与发现
5.1 nor flash 先在内核驱动声明
源码路径:drivers/mtd/spi-nor/micron-st.c
会有很多厂家的nor-flash 产品。
5.2 spi-nor 的核心层,会自动probe 上面的表格
源码路径:drivers/mtd/spi-nor/core.c
执行 spi_nor_probe 函数:
接着执行 spi_nor_scan 函数:
执行 spi_nor_get_flash_info 函数:
上面的函数, 先是spi_nor_mach_id()匹配 name,即在设备树中声明的flash,如果在flash_info 表中,查找不到;
再接着调用 spi_nor_read_id()函数 读取物理上连接 的flash id.
接着调用 spi_nor_read_id():
读取到ID后, 和manufactures[] 表进行匹配。下面看一下这个表:
这个厂家表,就包含 了spi_nor_st 刚开始声明的 flash_info 表。
最后在 probe 函数中, 调用 mtd_device_register()注册进mtd 层。
5.3 spi-nor flash 的默认写读擦除的函数接口是什么时候注册的?
在 spi_nor_probe 调到 spi_nor_scan 函数中, 初始化了这些函数接口。
如下:spi_nor_scan()
那么举一个具体的函数:spi_nor_write()->spi_nor_write_data() ->nor->controller_ops->write(), 就调用到 spi 控制器的write 函数。
六、SPI 控制器的注册和驱动
spi 控制器的源码路径:
drivers/spi/*
spi probe 会注册spi 控制器的发送接受数据的函数。内核代码中有2种方式:
1、中断
2、轮询
6.1 中断方式
6.2 轮询方式
代码中有一个隐晦的地方,其实是有2个轮询的代码。
第一个:dw_spi_poll_transfer()函数
这个比较明显。
第二个:dw_spi_exec_mem_op()函数
实际按照大小轮询的函数: dw_spi_write_then_read()。还需注意一点, 在执行dw_spi_write_then_read()前是禁中段和禁抢占的。
看dw_spi_write_then_read():
上面就是根据len , 读取数据。
七、deepseek 写的 从mtd 层spi 控制器的调用
1、write(fd, data, size); // 用户空间调用
2. VFS (虚拟文件系统) 层
-
系统调用入口:
-
SYSCALL_DEFINE3(write, ...)
(在fs/read_write.c
) -
调用
ksys_write()
-
调用
vfs_write()
-
-
文件操作路由:
-
通过
file->f_op->write_iter
或file->f_op->write
-
MTD 字符设备注册的是
mtdchar_write()
-
3. MTD 字符设备层 (drivers/mtd/mtdchar.c
)
static ssize_t mtdchar_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct mtd_info *mtd = file->private_data;
// ...
ret = mtd_write(mtd, *ppos, count, &retlen, kbuf);
// ...
}
4. MTD 核心层 (drivers/mtd/mtdcore.c
)
int mtd_write(struct mtd_info *mtd, loff_t to, size_t len, size_t *retlen,
const u_char *buf)
{
// 参数检查
// 调用具体设备的写入函数
ret = mtd->_write(mtd, to, len, retlen, buf);
}
5. SPI NOR Flash 驱动层 (drivers/mtd/spi-nor/
)
对于 SPI NOR Flash,mtd->_write
指向 spi_nor_write()
:
static int spi_nor_write(struct mtd_info *mtd, loff_t to, size_t len,
size_t *retlen, const u_char *buf)
{
// 转换为 SPI NOR 结构体
struct spi_nor *nor = mtd_to_spi_nor(mtd);
// 执行写操作
ret = nor->write(nor, to, len, buf);
// ...
}
实际的 nor->write
通常是 spi_nor_write_std()
:
static int spi_nor_write_std(struct spi_nor *nor, loff_t to, size_t len,
const u_char *buf)
{
// 发送写使能命令
spi_nor_write_enable(nor);
// 准备页编程命令
nor->program_opcode = SPINOR_OP_PP;
// 通过SPI子系统执行传输
ret = spi_nor_write_data(nor, to, len, buf);
}
6. SPI 子系统 (drivers/spi/spi.c
)
spi_nor_write_data()
最终会调用 SPI 子系统的接口:
static int spi_nor_write_data(struct spi_nor *nor, loff_t to, size_t len,
const u_char *buf)
{
// 准备SPI消息
struct spi_mem_op op = SPI_MEM_OP(...);
// 执行SPI传输
ret = spi_mem_exec_op(nor->spimem, &op);
}
spi_mem_exec_op()
的调用链:
spi_mem_exec_op()
→ spi_mem_access_start()
→ ctlr->mem_ops->exec_op() // 控制器特定的实现
7. SPI 控制器驱动 (如 drivers/spi/spi-bcm2835.c
)
具体控制器的 exec_op
实现示例:
static int bcm2835_spi_exec_op(struct spi_mem *mem,
const struct spi_mem_op *op)
{
// 配置SPI控制器寄存器
bcm2835_wr(spi, BCM2835_SPI_CS, cs);
bcm2835_wr(spi, BCM2835_SPI_DLEN, op->data.nbytes);
// 写入数据到TX FIFO
for (i = 0; i < op->data.nbytes; i++)
bcm2835_wr(spi, BCM2835_SPI_FIFO, op->data.buf.in[i]);
// 启动传输
bcm2835_wr(spi, BCM2835_SPI_CS, cs | BCM2835_SPI_CS_TA);
// 等待传输完成
while (!(bcm2835_rd(spi, BCM2835_SPI_CS) & BCM2835_SPI_CS_DONE));
// 读取状态等后续处理
}
8. 硬件寄存器操作
最终控制器驱动会通过读写寄存器操作硬件:
writel(value, spi->regs + offset); // 写寄存器
readl(spi->regs + offset); // 读寄存器