Zephyr驱动SPI NOR Flash实战:从零打通片外存储链路
你有没有遇到过这样的窘境?
手里的MCU固件越做越大,Web资源、AI模型、日志全塞进去后,原本1MB的片上Flash瞬间告急。OTA升级只能分块搬运,配置一改就怕写坏,连个像样的文件系统都不敢挂——这几乎是每个嵌入式开发者都会撞上的“容量墙”。
而破局的关键,就藏在那颗不起眼的8引脚芯片里:SPI NOR Flash。
今天,我们就以Zephyr系统为舞台,手把手带你把一颗W25Q128这样的外部Flash,真正变成可读、可写、可执行、可管理的“第二大脑”。不是简单跑个例程,而是从硬件连接到分区映射,从驱动加载到XIP启动,全流程打通。
为什么是SPI NOR?别再只盯着片内Flash了
先说结论:如果你的应用需要执行代码、中等以上容量、高可靠性,那么SPI NOR Flash几乎是唯一合理的选择。
我们常听说SPI NAND便宜、容量大,EEPROM写起来方便,但它们真的适合Zephyr这类RTOS场景吗?
| 特性 | SPI NOR Flash | SPI NAND Flash | EEPROM |
|---|---|---|---|
| 随机访问 | ✅ 支持(任意地址读) | ❌ 不支持(必须页读) | ✅ 支持 |
| XIP支持 | ✅ 可直接取指执行 | ❌ 需搬移至RAM | ⚠️ 容量太小无意义 |
| 擦写寿命 | ~10万次 | ~10万次(需ECC) | ~百万次 |
| 数据保持 | 20年 | 10年(依赖ECC) | 40年 |
| 坏块管理 | 无(出厂即可靠) | 必须处理 | 无 |
| 典型容量 | 16Mb ~ 2Gb | 1Gb ~ 8Gb | ≤1Mb |
看到区别了吗?NOR的优势在于“确定性”:没有坏块、无需ECC、支持随机读——这意味着你可以放心地让它运行代码(XIP),也可以用它存关键配置而不担心数据损坏。
更关键的是,Zephyr原生对jedec,spi-nor的支持已经非常成熟,只要你接对线、配好设备树,几乎不需要写一行驱动代码就能用起来。
硬件怎么接?别让一个走线毁掉整个设计
先来看最基础的四线SPI连接方式(以W25Q系列为例):
MCU (e.g., STM32H7) ↔ W25Q128JV ----------------------------------------------- SPI1_SCK → SCK SPI1_MOSI → SI (DI) SPI1_MISO → SO (DO) SPI1_NSS → CS# VCC → VCC (3.3V 或 1.8V) GND → GND WP# ──┬─→ VCC (上拉,禁用写保护) └─→ GPIO? (可选,软件控制) HOLD# ──→ VCC (上拉,保持使能)几个容易翻车的设计点:
- 去耦电容不能省:在VCC引脚附近并联
0.1μF + 10μF陶瓷电容,否则上电时序可能出问题; - WP# 和 HOLD# 引脚必须上拉:否则芯片可能进入不可预测状态;
- CS# 走线尽量短:建议 < 10cm,避免高频通信时反射干扰;
- MISO/MOSI 等长走线:尤其是频率超过30MHz时,阻抗匹配很重要;
- 独立LDO供电更稳:如果主板噪声大,建议给Flash单独供电。
小技巧:如果你的MCU支持QSPI或Octal-SPI,完全可以把传输速率干到80MB/s以上。但对于大多数Zephyr应用,50MHz标准SPI已绰绰有余。
设备树配置:让Zephyr“看见”你的Flash
Zephyr的一切始于设备树(.dts)。只要定义得当,系统会自动加载JEDEC SPI-NOR通用驱动,无需你实现底层SPI收发逻辑。
&spi1 { status = "okay"; clock-frequency = <10000000>; /* 初始调试用10MHz */ flash0: nor_flash@0 { compatible = "jedec,spi-nor"; reg = <0>; // 片选索引 spi-max-frequency = <50000000>; // 最高支持50MHz label = "NOR_FLASH"; partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; partition-bootloader { label = "bootloader"; reg = <0x00000000 0x00020000>; /* 128KB */ }; partition-app { label = "application"; reg = <0x00020000 0x00100000>; /* 1MB */ }; partition-config { label = "config"; reg = <0x00120000 0x00010000>; /* 64KB */ }; partition-log { label = "log_storage"; reg = <0x00130000 0x000D0000>; /* 832KB */ }; }; }; };重点说明:
compatible = "jedec,spi-nor"是关键,它触发Zephyr内置驱动绑定;label将来可用于FLASH_AREA_ID(label)宏定位分区;- 分区大小和偏移要与实际固件布局一致,尤其OTA时不能重叠;
- 所有操作都基于物理偏移 + 分区基址,不要硬编码绝对地址。
编译后可通过fromelf --regions build/zephyr/zephyr.elf或objdump -h查看各段落分布是否匹配。
驱动怎么工作?揭秘Zephyr的Flash抽象层(FAL)
你以为要自己写SPI命令发送?错了。
Zephyr通过Flash Abstraction Layer(FAL)把所有底层细节封装了起来。你只需要调用标准API,剩下的交给驱动。
核心机制如下:
- 设备树解析→ 构建
struct device对象; - 驱动绑定→
spi_nor_driver_api_init()注册函数表; - 接口统一化→ 上层使用
flash_read/write/erase,不关心是片内还是片外; - 分区映射→
flash_area_open()根据ID返回对应区域信息; - 权限隔离→ 不同模块只能访问授权分区,提升安全性。
这就意味着:同一套代码,在STM32上跑片内Flash没问题,在nRF53上跑外置NOR也一样跑得通。
实战代码:完成一次完整的读写擦测试
下面这个函数,展示了如何在一个外部Flash分区中安全地完成“擦除 → 写入 → 读取验证”的完整流程。
#include <zephyr/device.h> #include <zephyr/drivers/flash.h> #include <zephyr/storage/flash_map.h> #include <string.h> #include <logging/log.h> LOG_MODULE_REGISTER(flash_demo, LOG_LEVEL_INF); #define PARTITION_ID FLASH_AREA_ID(application) #define TEST_OFFSET 0x1000 // 偏移1个扇区(避免破坏头部) #define PAGE_SIZE 256 #define SECTOR_SIZE 4096 void spi_nor_flash_test(void) { const struct flash_area *fa; const struct device *flash_dev; uint8_t tx_buf[PAGE_SIZE]; uint8_t rx_buf[PAGE_SIZE]; int rc; // 1. 打开指定分区 if (flash_area_open(PARTITION_ID, &fa)) { LOG_ERR("Failed to open flash area"); return; } flash_dev = flash_area_get_device(fa); if (!device_is_ready(flash_dev)) { LOG_ERR("Flash device not ready"); goto close_area; } LOG_INF("Erasing sector at 0x%lx", fa->fa_off + TEST_OFFSET); rc = flash_erase(flash_dev, fa->fa_off + TEST_OFFSET, SECTOR_SIZE); if (rc) { LOG_ERR("Erase failed: %d", rc); goto close_area; } // 准备测试数据 memset(tx_buf, 0xAA, sizeof(tx_buf)); LOG_INF("Writing page @ offset 0x%lx", fa->fa_off + TEST_OFFSET); rc = flash_write(flash_dev, fa->fa_off + TEST_OFFSET, tx_buf, sizeof(tx_buf)); if (rc) { LOG_ERR("Write failed: %d", rc); goto close_area; } // 读取验证 memset(rx_buf, 0, sizeof(rx_buf)); rc = flash_read(flash_dev, fa->fa_off + TEST_OFFSET, rx_buf, sizeof(rx_buf)); if (rc) { LOG_ERR("Read failed: %d", rc); goto close_area; } if (memcmp(tx_buf, rx_buf, sizeof(tx_buf)) == 0) { LOG_INF("✅ Flash read/write test PASSED!"); } else { LOG_ERR("❌ Data mismatch!"); } close_area: flash_area_close(fa); }关键注意事项:
- 必须先擦除再写入:NOR Flash特性决定,未擦除区域写入无效;
- 写入必须对齐页边界:通常为256字节,且不能跨页;
- 擦除粒度更大:最小4KB扇区,别指望字节级擦除;
- 错误码必须检查:特别是在低电压或高温环境下,操作可能失败;
- 避免频繁擦写:虽然寿命10万次,但局部磨损仍会导致早期失效。
如何解决常见“坑”?这些经验能救你一命
🛑 问题1:写进去的数据读出来不对
最常见的原因是没擦除就直接写。NOR Flash只能将比特从1变0,不能从0变1。只有擦除操作才能把整块恢复成全1状态。
✅ 解决方案:
- 写前务必调用flash_erase();
- 使用逻辑层(如LittleFS)自动管理擦写顺序。
🛑 问题2:SPI通信超时或CRC错误
多发生在高频(>30MHz)或长PCB走线场景。
✅ 解决方案:
- 降低SPI频率至10~20MHz调试;
- 检查CPOL/CPHA是否匹配(W25Q系列一般为CPOL=0, CPHA=0);
- 添加__ASSERT()或重试机制:
for (int retry = 0; retry < 3; retry++) { rc = flash_write(dev, offset, buf, len); if (rc == 0) break; k_msleep(10); }🛑 问题3:OTA升级中途断电,系统变砖
这是OTA最怕的情况。
✅ 解决方案:
- 使用双Bank机制:A/B分区轮流更新;
- 写入时采用“写+校验+标记”三步法;
- 利用CONFIG_PM_DEVICE在写前唤醒Flash;
- 启动时Bootloader检测有效镜像再跳转。
进阶玩法:不只是存储,还能直接运行代码(XIP)
最酷的功能来了:让你的应用程序直接从SPI NOR中执行!
Zephyr支持XIP(eXecute In Place),前提是:
- MCU具备XIP能力(如STM32QSPI、i.MX RT系列);
- 外部Flash通过QSPI/Octal-SPI高速连接;
- 链接脚本(linker script)将
.text段指向外部Flash地址。
一旦启用XIP,你的1MB应用可以直接在外置Flash中运行,仅占用极少量RAM作为缓存,极大缓解内存压力。
配合MT7921等WiFi模组,甚至可以在外部Flash中存放HTML/CSS/JS资源,实现本地Web UI服务。
文件系统加持:用LittleFS实现智能管理
裸操作Flash太原始?那就上文件系统。
Zephyr集成的LittleFS正是为NOR Flash量身打造的轻量级日志结构文件系统,具备:
- ✅ 自动磨损均衡(wear leveling)
- ✅ 断电安全(power-loss resilience)
- ✅ 动态GC(garbage collection)
- ✅ POSIX-like API 接口
只需在prj.conf中开启:
CONFIG_FS_LITTLEFS=y CONFIG_FLASH_MAP=y CONFIG_PM_DEVICE=n # 若不需电源管理可关闭然后挂载即可:
#include <zephyr/fs/littlefs.h> #include <zephyr/fs/fs.h> static struct fs_mount_t littlefs_mnt = { .type = FS_LITTLEFS, .mnt_point = "/lfs", .fs_data = &lfs_data, }; struct fs_mount_t *mp = &littlefs_mnt; int rc = fs_mount(mp); if (rc == 0) { LOG_INF("LittleFS mounted at %s", mp->mnt_point); }从此你可以像Linux一样操作文件:
int fd = open("/lfs/config.json", O_RDWR | O_CREAT); write(fd, json_str, len); close(fd);再也不用手动管理偏移、页、扇区了。
结语:这不是终点,而是新起点
当你第一次成功从SPI NOR Flash中读出自己写入的数据,那种“我真正掌控了硬件”的感觉,是任何框架都无法替代的。
而这条路才刚刚开始:
- 你可以把Bootloader放进内部Flash,主程序放在外部,实现安全启动;
- 可以结合TF-M构建可信执行环境(TEE);
- 可以用外部Flash缓存OTA差分包,节省带宽;
- 更可以用它跑Zephyr的
settings_subsys,实现配置持久化。
掌握Zephyr下的SPI NOR驱动开发,不只是解决容量问题,更是构建现代嵌入式系统的基石能力。
如果你正在做物联网终端、工业控制器、智能仪表,或者只是想摆脱“Flash不够用”的焦虑——现在就是动手的最佳时机。
如果你在实践中遇到了其他挑战,比如特定型号识别失败、QSPI初始化卡住、或多分区OTA调度问题,欢迎留言交流。我们可以一起深挖每一行寄存器背后的真相。