嵌入式Linux下SPI NOR Flash驱动开发:实现固件的可靠读写与擦除
在嵌入式Linux系统中,SPI NOR Flash常用于存放Bootloader、内核镜像、文件系统及关键配置数据。其容量适中(通常4MB~64MB)、读取速度快、接口简单,但驱动开发中最棘手的并非读写本身,而是如何保证在意外掉电、擦写磨损等场景下的数据可靠性。本文以W25Q64(8MB)为例,展示基于MTD(Memory Technology Device)子系统的SPI NOR Flash驱动开发全流程。
一、硬件连接与设备树配置
SPI NOR Flash通过四线SPI接口(CS、CLK、MOSI、MISO)连接SoC。在设备树中,将其描述为SPI总线下的从设备:
&spi1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi1_pins>;
flash: w25q64@0 {
compatible = "winbond,w25q64", "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <80000000>;
m25p,fast-read;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
bootloader@0 {
label = "u-boot";
reg = <0x000000 0x100000>; // 1MB
};
kernel@100000 {
label = "kernel";
reg = <0x100000 0x500000>; // 5MB
};
rootfs@600000 {
label = "rootfs";
reg = <0x600000 0x200000>; // 2MB
};
};
};
};
compatible属性使用"jedec,spi-nor"可以自动匹配内核自带的SPI NOR驱动框架(drivers/mtd/spi-nor),无需从头编写。但如果需要定制化操作(如特殊写保护策略),则需编写独立的驱动。
二、驱动框架与probe函数
内核MTD子系统为NOR Flash提供了统一的抽象层。驱动只需实现struct mtd_info中的必要回调,并调用mtd_device_register注册分区。
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>
struct nor_flash {
struct spi_device *spi;
struct mtd_info mtd;
u8 *buffer; // 临时缓冲区
};
static int nor_read(struct mtd_info *mtd, loff_t from, size_t len,
size_t *retlen, u_char *buf) {
struct nor_flash *flash = mtd_to_nor(mtd);
struct spi_transfer t = {
.tx_buf = flash->buffer,
.rx_buf = buf,
.len = 5 + len, // 指令+地址+数据
};
// 构造READ命令 (0x03) + 3字节地址
flash->buffer[0] = 0x03;
flash->buffer[1] = (from >> 16) & 0xff;
flash->buffer[2] = (from >> 8) & 0xff;
flash->buffer[3] = from & 0xff;
spi_sync_transfer(flash->spi, &t, 1);
*retlen = len;
return 0;
}
static int nor_write(struct mtd_info *mtd, loff_t to, size_t len,
size_t *retlen, const u_char *buf) {
struct nor_flash *flash = mtd_to_nor(mtd);
// 先写使能 (WREN 0x06)
// 再发页编程命令 (0x02) + 地址 + 数据
// 等待状态寄存器BUSY位清零
// 实际代码需处理跨页写、擦除等
return 0;
}
static int nor_erase(struct mtd_info *mtd, struct erase_info *instr) {
// 发送扇区擦除命令 (0x20) 或 块擦除 (0xD8)
// 等待擦除完成
return 0;
}
static int nor_probe(struct spi_device *spi) {
struct nor_flash *flash;
flash = devm_kzalloc(&spi->dev, sizeof(*flash), GFP_KERNEL);
flash->spi = spi;
flash->mtd.type = MTD_NORFLASH;
flash->mtd.flags = MTD_CAP_NORFLASH;
flash->mtd.size = 8 * 1024 * 1024; // 8MB
flash->mtd.erasesize = 4096; // 4KB扇区
flash->mtd.writesize = 1;
flash->mtd._read = nor_read;
flash->mtd._write = nor_write;
flash->mtd._erase = nor_erase;
mtd_device_register(&flash->mtd, NULL, 0);
return 0;
}
static int nor_remove(struct spi_device *spi) {
struct nor_flash *flash = spi_get_drvdata(spi);
mtd_device_unregister(&flash->mtd);
return 0;
}
static struct spi_driver nor_driver = {
.driver = {
.name = "my_nor_flash",
.of_match_table = of_match_ptr(of_match_table),
},
.probe = nor_probe,
.remove = nor_remove,
};
module_spi_driver(nor_driver);
三、可靠性保障措施
写保护:NOR Flash通常有状态寄存器中的BP位控制区块保护。驱动在初始化时应读取并设置保护范围,防止意外擦写关键区域。
掉电保护:页编程和擦除操作期间掉电可能导致数据损坏。驱动应在操作前关闭中断,操作完成后恢复;对于关键数据,可写入两次并校验。
磨损均衡:对于频繁更新的配置区,驱动层可做简单的动态映射,但通常交由上层UBIFS或JFFS2处理。
错误处理:每次擦写后读取状态寄存器,确认操作成功;若多次失败,标记坏块(NOR坏块极少,但依然存在)。
四、测试与调试
驱动加载后,/dev/mtd0等设备节点出现。使用mtd_debug工具验证读写:
# 擦除整个设备
flash_erase /dev/mtd0 0 0
# 写入数据
dd if=/dev/urandom of=/tmp/test.bin bs=1024 count=1
mtd_debug write /dev/mtd0 0 1024 /tmp/test.bin
# 读出对比
mtd_debug read /dev/mtd0 0 1024 /tmp/read.bin
cmp /tmp/test.bin /tmp/read.bin
若一致,说明驱动基本功能正常。进一步可用mtd-utils中的flashcp测试擦写循环稳定性。
五、写在最后
嵌入式Linux下SPI NOR Flash驱动的核心是围绕MTD子系统实现_read、_write、_erase三个回调,并处理好SPI通信时序与状态寄存器轮询。可靠性则依赖于写保护配置、掉电防护和操作确认。实际项目中,大多数情况下直接使用内核的spi-nor通用驱动即可,只有在需要特殊分区策略或硬件定制时才需自行开发。掌握这套流程后,无论是更换Flash型号还是调整分区布局,都能快速适配。





