当前位置: 首页 > news >正文

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 数据结构

重要的数据结构: 

  1. mtd_info  表示mtd原始设备, 所有mtd_info结构体被存放在mtd_info数组mtd_table中
  2. 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 (虚拟文件系统) 层

  1. 系统调用入口

    • SYSCALL_DEFINE3(write, ...) (在 fs/read_write.c)

    • 调用 ksys_write()

    • 调用 vfs_write()

  2. 文件操作路由

    • 通过 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);          // 读寄存器

相关文章:

  • 手机打电话时由对方DTMF响应切换多级IVR语音菜单(话术脚本与实战)
  • 第十节第六部分:常见API:DateTimeFormatter、Period、Duration
  • 智能办公协同系统开发日志(三):画板模块设计与实现全记录
  • Minion-Agent:软件测试领域的智能自动化实践
  • JUC入门(五)
  • DeepSeek Mermaid:如何将文本直接转化为精美图表? -优雅草卓伊凡
  • LeetCode Hot100(滑动窗口)
  • STL 转 STP 深度技术指南:从 3D 打印模型到工程标准的跨领域转换全解析(附迪威模型在线方案)
  • ai之pdf解析工具 PPStructure 还是PaddleOCR
  • 微信小程序之Promise-Promise初始用
  • 华为模拟器练习简单的拓扑图(3台路由器和2台pc)
  • 线性Wi-Fi FEM被卷死,非线性FEM是未来?
  • 【学习笔记】机器学习(Machine Learning) | 第七章|神经网络(3)
  • Linux 搭建FTP服务器(vsftpd)
  • Spring Boot与Eventuate Tram整合:构建可靠的事件驱动型分布式事务
  • spring中的BeanFactoryAware接口详解
  • SpringBoot Day_03
  • 【Spark集成HBase】Spark读写HBase表
  • 一次Java Full GC 的排查
  • JAVA EE(进阶)_CSS
  • 临邑建设局网站/淄博seo网络公司
  • wordpress做文字站/王通seo赚钱培训
  • 网站代运营性价比高/网站建设的重要性
  • 做外汇都看那些网站/如何免费找精准客户
  • 设计类网站/舆情服务公司
  • windows不能用wordpress/长清区seo网络优化软件