1. 项目概述:当MicroPython遇上SPI Flash存储
在嵌入式开发中,外扩存储设备的选择往往让人纠结。最近我在一个物联网传感器项目中尝试了W25QXX系列SPI Flash与MicroPython的搭配方案,实测发现这对组合既能保持MicroPython的开发效率优势,又能突破芯片内部存储的限制。W25QXX作为市面上最常见的SPI接口Flash芯片之一,从社区支持到性价比都堪称完美。本文将分享如何用MicroPython的SPI接口驱动这类芯片,并实现可靠的文件存储方案。
2. 硬件选型与电路设计
2.1 W25QXX系列特性对比
以常见的W25Q64JV为例,这颗64Mbit(8MB)的芯片支持标准SPI和QSPI接口,最高时钟频率可达104MHz。与同系列其他型号相比,它的页编程时间仅0.7ms,整片擦除也只要10秒左右。实际选购时要注意尾缀差异:W25Q64FV的工作电压范围是2.7-3.6V,而W25Q64JV支持更宽的1.8-3.6V电压,更适合电池供电场景。
2.2 典型连接电路
在ESP32开发板上实现时,建议采用如下接法:
code复制W25Q64 ESP32
CS <----> GPIO5 (可配置)
CLK <----> GPIO18 (硬件SPI固定)
MOSI <----> GPIO23 (硬件SPI固定)
MISO <----> GPIO19 (硬件SPI固定)
WP# 接高电平(禁用写保护)
HOLD# 接高电平(禁用保持功能)
VCC 接3.3V
GND 共地
注意:不同型号开发板的SPI引脚可能不同,使用前务必查阅对应硬件手册。比如树莓派Pico的默认SPI0引脚是GPIO16(CS)、GPIO18(SCK)、GPIO19(MISO)、GPIO20(MOSI)。
3. MicroPython驱动实现
3.1 底层SPI通信封装
首先初始化硬件SPI接口,这里以ESP32为例:
python复制from machine import Pin, SPI
import time
class W25QXX:
def __init__(self, spi_bus=1, cs_pin=5, baudrate=20_000_000):
self.cs = Pin(cs_pin, Pin.OUT)
self.spi = SPI(spi_bus,
baudrate=baudrate,
polarity=0,
phase=0,
firstbit=SPI.MSB)
self._wait_ready()
def _wait_ready(self):
while True:
self.cs(0)
self.spi.write(b'\x05') # 发送读状态寄存器1指令
status = self.spi.read(1)[0]
self.cs(1)
if not (status & 0x01): # 检查BUSY位
break
time.sleep_ms(1)
3.2 关键操作指令实现
芯片操作主要依赖几个核心指令,这里给出页编程的实现示例:
python复制def write_page(self, addr, data):
if len(data) > 256:
raise ValueError("Page size exceeded")
self._write_enable()
self.cs(0)
self.spi.write(bytes([0x02]) + addr.to_bytes(3, 'big')) # 页编程指令+地址
self.spi.write(data)
self.cs(1)
self._wait_ready()
# 验证写入内容
read_back = self.read_data(addr, len(data))
if read_back != data:
raise RuntimeError("Verify failed at 0x{:06x}".format(addr))
4. 文件系统集成方案
4.1 制作MicroPython可识别的文件系统
W25QXX本身只是存储介质,要让MicroPython直接使用需要构建文件系统。推荐使用如下步骤创建LittleFS:
- 在PC端准备磁盘镜像:
bash复制dd if=/dev/zero of=flash.img bs=1k count=8192 # 创建8MB镜像
mkfs.lfs -b 4096 -r 256 -p 256 -s 8192 flash.img # 格式化
- 通过编程器将镜像写入Flash:
python复制with open('flash.img', 'rb') as f:
img_data = f.read()
flash = W25QXX()
for i in range(0, len(img_data), 256):
chunk = img_data[i:i+256]
flash.write_page(i, chunk)
4.2 动态挂载文件系统
在MicroPython中运行时挂载:
python复制import os, vfs
bdev = W25QXXBlockDevice(spi_bus=1, cs_pin=5) # 需实现块设备接口
os.VfsLfs2.mkfs(bdev) # 首次使用需要格式化
vfs = os.VfsLfs2(bdev)
os.mount(vfs, '/flash')
5. 性能优化技巧
5.1 QSPI模式加速访问
支持QSPI的型号可以通过四线模式提升速度。以W25Q64JV为例:
python复制def _enable_qspi(self):
self.cs(0)
self.spi.write(b'\x35') # 读状态寄存器3
sr3 = self.spi.read(1)[0]
self.cs(1)
if not (sr3 & 0x01): # 检查QE位
self._write_enable()
self.cs(0)
self.spi.write(b'\x31\x02') # 写状态寄存器3,设置QE位
self.cs(1)
self._wait_ready()
5.2 缓存策略优化
频繁的小文件操作建议实现RAM缓存:
python复制class CachedFlash:
def __init__(self, flash, cache_size=4):
self.flash = flash
self.cache = {}
self.dirty = set()
def write(self, addr, data):
self.cache[addr] = data
self.dirty.add(addr)
if len(self.dirty) >= cache_size:
self.flush()
def flush(self):
for addr in self.dirty:
self.flash.write_page(addr, self.cache[addr])
self.dirty.clear()
6. 常见问题排查
6.1 芯片无响应排查步骤
- 测量VCC电压是否在2.7-3.6V范围内
- 用逻辑分析仪抓取SPI波形,确认CS信号有效
- 尝试降低SPI时钟频率(如降到1MHz)
- 发送0x9F指令读取JEDEC ID,正常应返回0xEF4017(W25Q64JV)
6.2 数据写入异常处理
当出现写入后读取不一致时:
- 检查WP#引脚是否被意外拉低
- 确认每次写操作前调用了_write_enable()
- 测量电源稳定性,电压跌落可能导致写入失败
- 检查是否跨页写入(每页256字节边界)
7. 实际应用案例
7.1 物联网设备日志存储
在野外气象站项目中,我使用W25Q64存储传感器历史数据:
python复制def log_data(sensor_id, values):
timestamp = time.time()
record = f"{timestamp},{sensor_id},{','.join(map(str,values))}\n"
with open('/flash/log.csv', 'a') as f:
f.write(record)
# 每天自动轮转日志
if time.localtime()[2] != getattr(log_data, '_last_day', 0):
os.rename('/flash/log.csv', f'/flash/log_{time.localtime()[:3]}.csv')
log_data._last_day = time.localtime()[2]
7.2 OTA固件更新方案
实现安全的固件更新流程:
- 将新固件下载到Flash的空闲区域
- 计算SHA256校验和
- 校验通过后更新引导标志
- 重启后引导加载器根据标志选择启动分区
关键代码片段:
python复制def update_firmware(url):
import urequests, uhashlib
# 下载到备用区(假设从4MB偏移开始)
r = urequests.get(url)
with open('/flash/firmware.bin', 'wb') as f:
f.write(r.content)
# 计算校验和
sha = uhashlib.sha256()
with open('/flash/firmware.bin', 'rb') as f:
while True:
chunk = f.read(512)
if not chunk: break
sha.update(chunk)
# 写入元信息
with open('/flash/update.flag', 'w') as f:
f.write(json.dumps({
'version': '1.2.0',
'sha256': sha.digest().hex(),
'timestamp': time.time()
}))
machine.reset()
