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

国产SPI Flash在Linux系统下的驱动适配与移植实战

1. 项目概述:当国产平台遇上国产Flash

最近在基于复旦微电子的FMQL系列平台(可以理解为国产化的ZYNQ)进行Linux系统开发时,遇到了一个挺典型但又有点棘手的问题:系统引导程序U-Boot和Linux内核无法正确识别板载的国产SPI Flash芯片,更别提后续的挂载和分区了。这个问题在从Xilinx ZYNQ平台迁移到国产平台,或者初次使用某些国产Flash芯片时,出现的概率不低。表面上看,系统启动后找不到存储设备,启动日志里可能只有一堆SPI控制器初始化成功,但后续探测不到任何Flash型号的信息,直接导致根文件系统无法挂载,系统启动失败。其核心原因在于,无论是U-Boot还是Linux内核,其默认的驱动支持列表(俗称“驱动支持包”)主要围绕国际大厂(如Winbond、Macronix、Spansion等)的芯片构建,对于很多新兴的国产SPI Flash,它们的硬件ID(JEDEC ID)或内部指令集可能未被收录,或者需要一些特殊的配置参数。这就好比你的电脑装好了系统,但偏偏不认你新买的国产固态硬盘,系统自然就跑不起来。解决这个问题,需要我们深入到U-Boot和Kernel的驱动层面,进行针对性的适配和修改。这个过程不仅涉及驱动代码的修改,还牵涉到设备树(Device Tree)的配置、编译系统的调整,是一个完整的嵌入式Linux驱动移植案例。

2. 问题根因深度剖析:为什么“不认识”?

要解决问题,首先得搞清楚U-Boot和Kernel是如何“认识”一块SPI Flash的。这个过程并非魔法,而是一套标准的协议和匹配机制。

2.1 SPI Flash识别的基础:JEDEC ID与驱动表

每一颗符合JEDEC标准的SPI Flash芯片,在上电后,都可以通过发送0x9F指令读取一个唯一的制造商和设备ID,通常是3个或更多字节。例如,华邦(Winbond)的W25Q128JV芯片,其ID可能是0xEF 0x40 0x18。U-Boot和Kernel的SPI NOR Flash驱动(通常是drivers/mtd/spi-nor/目录下的核心驱动)内部维护了一张巨大的“闪存信息表”(spi_nor_ids或类似的结构体数组)。这张表就像一本“芯片户口本”,里面记录了已知芯片的ID、名称、容量、页大小、扇区大小、支持的指令集(如4字节地址模式、Quad SPI使能方式)等关键参数。

当驱动初始化时,它会通过SPI总线发送0x9F命令读取芯片的实际ID,然后拿着这个ID去内部的“户口本”里逐条比对。一旦找到完全匹配的条目,驱动就认为“认识”了这个芯片,并按照表中定义的参数对其进行初始化和操作。如果遍历完整个表都找不到匹配项,驱动就会报错,最常见的日志就是“Unknown SPI NOR flash”或直接探测失败,后续的分区解析、挂载自然无从谈起。

2.2 国产Flash的常见“水土不服”点

国产SPI Flash芯片为了追求更好的性能、更低的成本或实现差异化,可能会在以下几个方面与国际通用标准产生细微差异,导致驱动无法直接识别:

  1. JEDEC ID未收录:这是最直接的原因。驱动表里根本没有这个ID,自然不认识。很多国产芯片会使用自己申请的制造商ID,或者复用某个公共ID但设备号不同。
  2. 4字节地址模式(4-Byte Address Mode)使能方式不同:对于容量大于16MB(128Mb)的Flash,需要用到4字节地址来寻址。国际大厂芯片通常通过写入特定的扩展状态寄存器位(如Bank Register)来切换。而有些国产芯片可能需要发送一个特殊的指令(如0xB7)来进入4字节模式,或者在芯片上电后默认就需要配置,这个配置流程如果不符合驱动内置的通用逻辑,就会导致读写地址错乱。
  3. Quad SPI(QSPI)使能序列差异:为了提高读写速度,需要使能QSPI模式。标准流程是通过写状态寄存器来设置。但部分国产芯片可能需要先执行一个“写使能”指令,再发送一个特定的“QSPI使能”指令序列,这个序列如果和驱动预设的spi_nor_quad_enable通用函数不匹配,QSPI模式就无法生效,性能会大打折扣。
  4. 扇区/块擦除大小不标准:驱动里定义的擦除操作(如4KB扇区擦除、32KB块擦除、64KB块擦除)指令可能不适用。国产芯片可能使用不同的指令码,或者支持的擦除粒度不同。
  5. 上电后的默认状态:有些芯片上电后可能处于某种“深度省电”或“特殊模式”,需要先发送一个唤醒指令才能响应0x9F读ID命令。

在FMQL平台上,SPI控制器的驱动本身(通常是Cadence或Synopsys的IP核驱动)一般是正常的,问题就出在连接在这个控制器上的Flash芯片信息没有被NOR Flash驱动层识别。

3. 解决方案总览:从驱动到设备树的完整适配路径

解决这个问题的核心思路就是:将我们使用的国产SPI Flash芯片的“身份信息”和“操作手册”,添加到U-Boot和Linux内核的驱动中去,并确保硬件连接信息正确无误。整个适配工作可以分为三个主要部分,它们环环相扣:

  1. U-Boot侧适配:确保U-Boot能够识别Flash、读写数据,以便完成内核镜像(Image)、设备树(dtb)、根文件系统(rootfs)的加载。这是系统启动的第一步。
  2. Linux内核侧适配:确保内核在启动后期能够识别同一块Flash,并正确初始化MTD(内存技术设备)子系统,为后续挂载文件系统做准备。
  3. 设备树(Device Tree)配置:这是连接硬件描述和软件驱动的桥梁。需要准确描述Flash芯片连接在哪个SPI控制器、哪个片选(CS)上,以及它的一些基本属性。U-Boot和Kernel都会读取设备树中的信息。

整个流程的终点,是让系统能够看到Flash上的分区(例如,/dev/mtdblock0,/dev/mtdblock1),并成功挂载根文件系统。下面,我们按照实际操作顺序,一步步拆解。

4. U-Boot侧驱动适配详解

U-Boot的适配是首要任务,因为它是加载内核的载体。我们需要修改U-Boot的源码。

4.1 定位与修改Flash信息表

首先,找到U-Boot中SPI NOR Flash驱动的源文件。通常路径是:u-boot/drivers/mtd/spi/spi-nor-ids.c。这个文件里包含了那个巨大的spi_nor_idsflash_info表。

我们需要为国产芯片添加一个新的条目。假设我们使用的国产Flash型号为FM25Q128,容量128Mbit(16MB),通过示波器或芯片手册得知其JEDEC ID为0xC8 0x40 0x18

打开spi-nor-ids.c,在合适的位置(通常是按制造商ID排序)添加如下结构体:

/* 在文件末尾的spi_nor_ids数组里添加,或者找到类似的地方 */ { .name = "FM25Q128", .id = {0xc8, 0x40, 0x18}, // JEDEC ID .id_len = 3, .sector_size = SZ_4K, // 扇区大小4KB .n_sectors = 4096, // 总扇区数: 16MB / 4KB = 4096 .page_size = 256, // 页大小256字节 .flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_4B_OPCODES, .nr_sectors = SZ_16M / SZ_4K, // 另一种容量表示方式 .mfr_id = 0xc8, // 制造商ID },

关键参数解析:

  • .flags:这是芯片特性标志位,至关重要。
    • SPI_NOR_HAS_LOCK:表示芯片支持块保护/锁定功能。
    • SPI_NOR_HAS_TB:表示芯片顶部/底部块保护配置位(Top/Bottom)。
    • SPI_NOR_4B_OPCODES这是重点!表示此芯片支持4字节地址模式的指令集。对于16MB及以上的芯片,必须确认并正确设置。如果芯片不支持或使能方式特殊,可能需要后续额外的配置。
  • .sector_size.n_sectors:定义了擦除和操作的基本单位。必须与芯片手册严格一致。

注意:以上是一个示例,实际参数必须严格参照你所使用的国产Flash芯片的数据手册(Datasheet)进行填写。.flags字段的配置尤其需要仔细核对手册中关于写保护、4字节模式、QSPI模式的描述。

4.2 处理特殊的4字节地址与QSPI模式

如果芯片的4字节模式或QSPI模式使能方式比较特殊,可能还需要在drivers/mtd/spi/spi-nor-core.c中为这款芯片编写专用的初始化函数(flash_fixup函数)。

例如,如果FM25Q128需要在初始化时发送一个特定命令0xB7来进入4字节模式,而通用驱动没做这个操作,你就需要添加一个修复函数:

static void fm25q128_fixup(struct spi_nor *nor) { /* 进入4字节地址模式 */ nor->write_reg(nor, SPINOR_OP_EN4B, NULL, 0); nor->addr_width = 4; // 将地址宽度设置为4字节 } /* 然后在spi_nor_ids表对应条目的.flags中加上SPI_NOR_4B_OPCODES, 并在驱动中某个fixup hooks数组里关联这个ID和fm25q128_fixup函数。*/

查找U-Boot中已有的类似芯片(如w25q256)的fixup代码,模仿其写法是最快的方式。

4.3 配置U-Boot的设备树

U-Boot会使用自己的设备树(通常是从dts文件编译出的dtb),或者使用内核的设备树。我们需要确保设备树中SPI Flash节点正确。

找到FMQL平台对应的U-Boot设备树源文件(如arch/arm/dts/fmql-xxx.dts),定位到SPI控制器节点(可能是&qspi)。在其中添加或修改flash子节点:

&qspi { status = "okay"; num-cs = <1>; flash@0 { compatible = "jedec,spi-nor"; reg = <0>; // 片选0 spi-max-frequency = <50000000>; // SPI时钟频率,根据芯片和PCB布线能力设置 spi-rx-bus-width = <4>; // 接收总线宽度,1为标准SPI,4为QSPI spi-tx-bus-width = <4>; // 发送总线宽度 #address-cells = <1>; #size-cells = <1>; /* 可选:定义U-Boot使用的分区 */ partition@0 { label = "boot"; reg = <0x0000000 0x100000>; // 偏移0,大小1MB,放U-Boot }; partition@100000 { label = "kernel"; reg = <0x100000 0x800000>; // 偏移1MB,大小8MB,放内核 }; // ... 其他分区 }; };

compatible = "jedec,spi-nor";这个属性非常重要,它告诉U-Boot使用标准的SPI NOR驱动来匹配这个设备。

4.4 编译与测试

修改完成后,编译U-Boot。使用make menuconfig(或直接修改.config)确保CONFIG_SPI_FLASHCONFIG_DM_SPICONFIG_DM_SPI_FLASH等选项已开启。

将新的U-Boot镜像烧写到板子上(可以通过JTAG或SD卡启动链)。上电后,在U-Boot命令行中,使用sf probe命令探测Flash:

Zynq> sf probe 0:0 50000000 0

这条命令的含义是:探测SPI总线0、片选0,频率50MHz,模式0。如果驱动识别成功,你会看到类似Found: FM25Q128的信息。之后可以使用sf readsf writesf erase等命令进行读写擦除测试,验证驱动是否完全工作正常。

实操心得:在U-Boot下测试Flash驱动是最直接和安全的,因为此时系统状态简单。务必在这里完成基本的读写验证,确认芯片ID识别正确、擦写功能正常,再进入内核侧的适配,可以避免很多复合型问题。

5. Linux内核侧驱动适配详解

内核侧的适配原理与U-Boot类似,但代码位置和细节略有不同。内核的SPI NOR驱动更加复杂和完整。

5.1 在内核中添加Flash信息

内核的SPI NOR Flash信息表通常位于:linux/drivers/mtd/spi-nor/spi-nor.c。找到名为spi_nor_idsflash_info的数组。

同样地,为我们国产芯片添加一个新条目。格式与U-Boot类似,但字段名可能稍有差异:

{ .name = "fm25q128", .id = {0xc8, 0x40, 0x18}, .len = 3, .sector_size = SZ_4K, .n_sectors = 4096, .page_size = 256, .flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_4B_OPCODES, .mfr_id = 0xc8, },

内核特有的重要参数:

  • .flags:内核定义的标志位可能更丰富,例如SPI_NOR_QUAD_READ表示支持Quad SPI读取。如果芯片支持,务必加上,能极大提升读取速度。
  • .fixups:这是一个函数指针。如果芯片有特殊的初始化需求(如非标准的4字节模式使能、QSPI使能、或需要配置某些寄存器),就需要在这里指定一个自定义的fixup函数。这是解决“驱动识别但操作异常”问题的关键。

5.2 实现特殊的Fixup函数

如果芯片需要特殊初始化,需要在spi-nor-core.c的同文件或附近实现一个fixup函数,并在上述ID条目中引用。

例如,实现一个使能Quad SPI模式的fixup:

static int fm25q128_quad_enable(struct spi_nor *nor) { int ret; u8 sr_cr[2]; // 1. 写使能 ret = spi_nor_write_enable(nor); if (ret) return ret; // 2. 读取状态寄存器2(假设Quad使能位在状态寄存器2的第1位) ret = spi_nor_read_reg(nor, SPINOR_OP_RDSR2, &sr_cr[1], 1); if (ret) return ret; // 3. 设置Quad使能位 sr_cr[1] |= BIT(1); // 将第1位置1 // 4. 写回状态寄存器2 ret = spi_nor_write_reg(nor, SPINOR_OP_WRSR2, &sr_cr[1], 1); if (ret) return ret; // 5. 等待写操作完成 return spi_nor_wait_till_ready(nor); } /* 然后定义一个fixup结构体 */ static const struct spi_nor_fixups fm25q128_fixups = { .default_init = fm25q128_quad_enable, // 在默认初始化时调用 }; /* 最后在spi_nor_ids表的条目中,添加.fixups = &fm25q128_fixups */

5.3 配置内核设备树

内核的设备树描述需要与U-Boot保持一致或更详细。通常我们会修改Linux内核源码中的设备树源文件(.dts),路径如arch/arm/boot/dts/fmql-xxx.dts

SPI Flash节点的写法与U-Boot非常相似,但内核的分区解析方式更灵活:

&qspi { status = "okay"; num-cs = <1>; flash@0 { compatible = "jedec,spi-nor"; reg = <0>; spi-max-frequency = <50000000>; spi-rx-bus-width = <4>; spi-tx-bus-width = <4>; #address-cells = <1>; #size-cells = <1>; /* 方法一:内核命令行传递分区信息(老式)*/ /* 需要在bootargs中添加类似 `mtdparts=spi0.0:1M(boot),8M(kernel),-(rootfs)` */ /* 方法二:在设备树中定义分区(推荐)*/ partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; partition@0 { label = "boot"; reg = <0x0000000 0x00100000>; // 1MB read-only; }; partition@100000 { label = "kernel"; reg = <0x00100000 0x00800000>; // 8MB }; partition@900000 { label = "rootfs"; reg = <0x00900000 0x00700000>; // 7MB }; }; }; };

使用设备树定义分区(方法二)是现在更主流和清晰的做法,它不依赖内核命令行,分区信息在驱动加载时就直接确定了。

5.4 内核配置与编译

在内核源码目录下,使用make menuconfig进行配置:

  1. 确保Device Drivers -> Memory Technology Device (MTD) support被选中。
  2. 进入MTD子菜单,确保SPI-NOR device support被选中。
  3. 检查Chip drivers下,是否有你添加的芯片型号(如FM25Q128)对应的选项,通常添加ID后它会自动出现在“Supported SPI Flash chips”列表中,确保它被编译进内核(*)或模块(M)。

编译内核和设备树,生成Imagedtb文件。

6. 系统启动与问题排查实录

将适配好的U-Boot、内核镜像(Image)、设备树(dtb)以及根文件系统,按照你设定的分区布局,烧写到SPI Flash的对应位置。上电启动,观察串口日志。

6.1 成功启动的日志特征

如果一切顺利,你会在内核启动日志中看到类似以下关键信息:

[ 1.205000] spi-nor spi0.0: Found FM25Q128 (16 MiB) [ 1.210000] spi-nor spi0.0: using quad read mode [ 1.215000] 5 fixed-partitions partitions found on MTD device spi0.0 [ 1.221000] Creating 5 MTD partitions on "sp0.0": [ 1.226000] 0x000000000000-0x000000100000 : "boot" [ 1.231000] 0x000000100000-0x000000900000 : "kernel" [ 1.236000] 0x000000900000-0x000001000000 : "rootfs" ... [ 2.500000] VFS: Mounted root (squashfs filesystem) readonly on device 31:2.

看到Found FM25Q128和分区创建成功,并且根文件系统被挂载,就说明驱动和分区识别完全成功了。

6.2 常见问题与排查技巧

在实际操作中,几乎不可能一次成功。以下是几个最常见的坑和排查手段:

  1. 问题:内核日志显示spi-nor spi0.0: unrecognized JEDEC id bytes: c8, 40, 18

    • 原因:内核的spi_nor_ids表中没有添加该芯片ID。
    • 排查:确认添加的ID结构体格式正确,.id数组内容与读取到的完全一致,并确认内核配置已包含该芯片支持,且驱动被编译进内核。
  2. 问题:识别成功,但后续出现读写错误、系统挂起或内核崩溃

    • 原因:Flash参数(如扇区大小、页大小)设置错误,或者4字节地址模式、QSPI模式使能不正确。
    • 排查
      • 核对手册:逐字核对数据手册中关于Page ProgramSector EraseBlock Erase指令码、以及4字节模式(EN4B/EX4B)和QSPI模式(QE位)使能方法的描述。
      • 简化测试:在内核驱动中,暂时将.flags中的SPI_NOR_4B_OPCODESSPI_NOR_QUAD_READ等高级特性标志去掉,退回到最基础的3字节地址、标准SPI模式进行测试。如果基础模式工作正常,问题就出在高级特性的配置上。
      • 使用U-Boot对比:在U-Boot下用sf命令进行同样的读写操作。如果U-Boot正常而内核异常,说明两边的驱动实现有差异,重点检查内核侧的.fixups函数。
  3. 问题:分区无法识别,/dev/下没有mtdblock*设备节点

    • 原因:设备树中分区节点partitionscompatible属性不对,或者地址/大小超出了Flash实际范围。
    • 排查:检查设备树中partitions节点的compatible = "fixed-partitions";是否正确。使用cat /proc/mtd命令查看内核识别的MTD设备信息,如果Flash被识别为一个整体(如mtd0)但没有子分区,就是分区解析失败。
  4. 问题:系统启动后,Flash读写速度极慢

    • 原因:QSPI模式没有成功使能,驱动降级到了标准SPI模式。
    • 排查:查看内核启动日志,确认是否有using quad read mode字样。如果没有,检查芯片手册的QSPI使能流程,并确保在.flags中设置了SPI_NOR_QUAD_READ,且对应的.fixups函数正确执行了使能序列。有时还需要检查设备树中spi-rx-bus-widthspi-tx-bus-width是否都设置为<4>
  5. 问题:U-Boot能识别,内核不能识别(或反之)

    • 原因:两边驱动表中的ID或参数不一致;或者设备树中SPI控制器的配置(如时钟相位、极性spi-cpol,spi-cpha)不一致。
    • 排查:确保U-Boot和内核源码中添加的芯片ID、容量、扇区大小等参数完全一致。对比两边的设备树,确保SPI控制器节点(如&qspi)的基础配置相同。FMQL平台的SPI控制器可能有一些特有的属性需要配置。

一个高级调试技巧:使用逻辑分析仪或示波器抓取SPI波形。这是终极的排查手段。在上电启动时,抓取SPI总线上0x9F读ID命令的响应波形,可以100%确认芯片实际输出的ID字节。还可以抓取4字节模式或QSPI模式的使能命令序列,与数据手册对比,看驱动发出的指令是否正确。这能直接定位是软件驱动问题,还是硬件连接或芯片本身的问题。

7. 进阶:分区方案的灵活配置与优化

在解决基本识别问题后,一个合理的分区方案对产品至关重要。除了在设备树中写死分区,还有更灵活的方式:

  1. 使用Cmdline分区:在内核启动参数bootargs中设置mtdparts。这种方式可以在不重新编译设备树的情况下修改分区,适合开发调试阶段。但需要在U-Boot和内核中均开启CONFIG_MTD_CMDLINE_PARTS支持。
  2. 使用U-Boot环境变量传递分区表:更高级的做法是将分区信息定义在U-Boot的环境变量中,U-Boot在启动内核时,将其动态添加到bootargs里。这提供了最大的灵活性。
  3. 针对Flash特性优化文件系统:SPI NOR Flash有写寿命限制。对于需要频繁写的区域(如日志、配置),可以考虑使用支持损耗均衡的UBIFS文件系统(针对MTD设备),而不是直接使用mtdblock模拟的块设备挂载ext4jffs2。这需要在内核中启用CONFIG_MTD_UBICONFIG_UBIFS_FS,并调整分区和启动脚本。

整个适配过程,从芯片识别到稳定运行,是对开发者阅读手册、理解驱动框架、调试硬件协同能力的综合考验。尤其是在国产化替代的背景下,深入掌握这套流程,意味着你能让系统与更多“非标准”的国产芯片稳定协作,这无疑是极具价值的经验。每次成功解决一个类似问题,你对SPI子系统、MTD框架乃至Linux设备模型的理解,都会加深一层。

http://www.jsqmd.com/news/1297485/

相关文章:

  • Windows 系统下 GitHub SSH 全局配置完全指南
  • 通信基础(二)USART 与 UART:特点、区别与电平标准全解
  • 2026年7月全国名牌商用洗地机排行榜:这三款闭眼入! - 工业清洁测评社
  • 3分钟掌握FanControl:Windows风扇控制的终极中文设置指南
  • 2FAuth安全架构深度解析:从数据加密到RFC合规的实战指南
  • 拒绝做只会用工具的脚本小子,网络安全进阶学习路线与资源整理
  • 终极指南:如何在3分钟内使用AutoUnipus完成U校园自动刷课任务
  • 保山母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 2026年度优选:北京玻璃钢轴流风机优质厂商找哪家?——山东耀凯空调设备有限公司深度解析 - 装修教育财税推荐2026
  • OpenClaw智能助手框架:模块化架构与性能优化实践
  • Sqli、Xss、Upload靶场中使用的PHP函数
  • OpenCV C++形态学操作-开运算与闭运算
  • 2026年优选:长沙口碑光氢离子除臭净化设备服务商——湖南允中环保工程有限公司深度解析 - 装修教育财税推荐2026
  • 性能测试实战:从JMeter压测到瓶颈定位的完整解决方案
  • Java开发实战:从环境配置到内存管理的核心避坑指南
  • ASO内卷无解?本地化才是低成本破局的顶级增量外挂
  • AI搜索代码问题私密诊断手册(仅开放给GitHub Star≥500的开发者):含12个未公开的AST语义校验checklist
  • ESP8266/ESP32 PWM引脚全解析:避坑指南与实战配置
  • 丽水母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • Kimi LeetCode 3791. 给定范围内平衡整数的数目 Python3实现
  • 北海母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • C++26新特性实战——静态反射与std::expected,面试必问的新考点
  • 华为MetaERP Oracle EBS 与 Oracle Fusion 在库存成本核算方面的详细对比,涵盖各业务场景及对应的会计核算分录。成本方法概览Oracle EBSEBS 支持以下成本方
  • 西门子伺服驱动器接口详解:从电源到通讯的完整接线指南
  • STM32嵌入式入门实战:按键与光敏传感器控制LED与蜂鸣器
  • 2026下半年武汉云祺灾备系统选型指南:找到有实力的机构联系方式 - 装修教育财税推荐2026
  • 2026上海工厂选生产管理系统当心!90%企业都踩过的选型大坑
  • 三个系统都能查,为什么就是查不到一条完整的订单——跨系统语义打通的工程逻辑
  • 上架检快速定位APK隐私风险
  • STM32定时器硬件同步:多轴电机控制与数据采集的精准时序解决方案