Arduino SPI Flash数据记录实战:与CircuitPython无缝共享文件
1. 项目概述为什么要在Arduino上折腾SPI Flash如果你玩过一阵子Arduino尤其是像Adafruit Feather M0 Express这类功能更强大的板子大概率会注意到一个“隐藏”功能板子上那颗不起眼的小黑片——SPI Flash芯片。它不像SD卡那样有个卡槽插拔方便也不像EEPROM那样容量小、寿命有限。这颗芯片就静静地焊在板子上通常有2MB到8MB的容量对于很多嵌入式项目来说这空间相当可观了。那么我们为什么要费劲去用它呢简单说就三点永久、快速、无缝。永久存储不像RAM断电就丢数据SPI Flash是非易失性存储器。你记录的温度数据、设备配置参数、甚至是几段小代码掉电后依然存在。高速访问通过SPI总线直接与主控芯片通信速度比通过文件系统访问SD卡要快延迟也更低适合需要频繁读写小块数据的场景。生态融合这是Adafruit M0/M4系列Express板子的一个妙处。这块Flash芯片默认被CircuitPython用作文件系统就是那个CIRCUITPY盘符。这意味着你可以用CircuitPython快速写脚本、存文件然后用Arduino写高性能、低功耗的控制逻辑两者读写同一块存储区域实现真正的软硬件协同。我最初用它是因为一个环境监测项目。需要每10分钟记录一次温湿度和光照数据持续数月。SD卡方案怕震动接触不良EEPROM容量又不够。板载的SPI Flash就成了最可靠的选择。更妙的是我可以用CircuitPython快速验证传感器和文件写入逻辑然后用Arduino C重写以获得更精确的定时和更低功耗数据文件还能共用省去了数据迁移的麻烦。本文将手把手带你利用Adafruit SPIFlash库把这块“隐藏的硬盘”用起来。核心就两件事第一像操作SD卡一样进行数据记录Data Logging第二在Arduino和CircuitPython之间优雅地共享文件。你会发现它没你想的那么复杂。2. 硬件与库准备认识你的“存储伙伴”工欲善其事必先利其器。在写代码之前我们得先搞清楚硬件是谁以及如何用软件驱动它。2.1 硬件平台解析不只是“另一块Arduino”本项目主要针对Adafruit Feather M0 Express、Metro M0 Express、ItsyBitsy M0 Express及其类似的M4 Express板卡如Feather M4 Express。这些板子的共同特点是集成了一个Winbond W25Qxx系列或类似品牌的SPI Flash芯片。以Feather M0 Express为例这颗Flash芯片通过硬件SPI接口与主控芯片ATSAMD21G18连接并且独占一个片选CS引脚。这意味着你不需要额外飞线库已经为你配置好了硬件底层。在代码中它通常被定义为SPI1端口和SS1引脚。注意并非所有Arduino板都内置SPI Flash。常见的Uno、Nano就没有。如果你的项目需要此功能选择Express系列板卡是前提。你也可以通过外接SPI Flash模块实现类似功能但本文讨论的是板载集成方案其稳定性和便利性更高。2.2 核心库安装与依赖Arduino IDE本身并不直接支持这块Flash的文件操作我们需要借助Adafruit封装好的库。打开Arduino IDE依次点击工具 - 管理库...打开库管理器。你需要搜索并安装以下两个库Adafruit SPIFlash这是核心驱动库负责与Flash芯片进行底层通信擦除、读写字节。SdFat - Adafruit Fork这是Adafruit维护的SdFat库分支。Adafruit SPIFlash库的文件系统功能FAT格式依赖于它。它提供了我们熟悉的open(),read(),print()等文件操作接口。安装时请务必选择Adafruit发布的版本。安装完成后你可以在文件 - 示例菜单中找到Adafruit SPIFlash库提供的丰富示例这是我们学习的主要材料。2.3 关于Max SPI/Max QSPI设置的深度解读在Arduino IDE的“工具”菜单中针对M4 Express板卡你可能会看到“Max SPI”和“Max QSPI”这两个令人困惑的选项。原始资料里提到“最好保持默认”但知其所以然很重要。Max SPI (通常默认24 MHz)这个设置决定了主SPI外设SPI的时钟源频率。对于大多数需要读写操作的设备如SD卡、某些传感器必须保持在默认的24MHz。因为SPI的读取时序对时钟质量非常敏感超频会导致读取失败。但如果你只驱动纯写入型设备比如某些OLED或TFT屏幕它们只接受控制器发来的数据不需要回读状态那么可以尝试提高这个频率如48MHz甚至60MHz以获得更快的刷新率。核心原则只要你的项目涉及SPI读取操作就别动这个设置。Max QSPI (针对板载Flash)这个设置专门影响连接板载SPI Flash的QSPI总线一种更快的4线SPI。对于绝大多数Arduino项目你几乎感觉不到它的影响因为文件读写操作很少成为性能瓶颈。只有在特定CPU频率下且进行大量连续文件操作时如高速播放GIF动画提升此设置可能带来细微改善。对于数据记录和文件交互应用强烈建议保持默认值。简单来说除非你明确知道自己在做什么并且遇到了确切的性能瓶颈否则不要修改这两个时钟设置。错误的设置是导致SPI设备包括Flash无法正常工作的常见“玄学”问题之一。3. 核心操作一与CircuitPython文件系统交互这是最酷的部分让Arduino和CircuitPython成为“好朋友”。关键在于使用一个特殊的类Adafruit_M0_Express_CircuitPython。3.1 原理CircuitPython的“磁盘布局”CircuitPython启动时会将SPI Flash芯片格式化成特定的布局一部分存放CircuitPython解释器和内置库另一部分则作为一个FAT文件系统挂载为CIRCUITPY驱动器。Adafruit_M0_Express_CircuitPython类的作用就是让Arduino代码能够理解并访问这个特定的文件系统布局而不是把整块芯片当做一个空白磁盘。3.2 实战读写CircuitPython的文件我们直接剖析fatfs_circuitpython示例代码。首先关键的初始化代码如下#include Adafruit_SPIFlash.h #include Adafruit_M0_Express_CircuitPython.h // 对于Feather M0 Express硬件SPI引脚已固定 #define FLASH_SS SS1 // Flash芯片的片选引脚 #define FLASH_SPI_PORT SPI1 // Flash使用的SPI端口 // 1. 创建Flash驱动对象 Adafruit_SPIFlash flash(FLASH_SS, FLASH_SPI_PORT); // 2. 创建CircuitPython文件系统适配器对象 Adafruit_M0_Express_CircuitPython pythonfs(flash);代码解读Adafruit_SPIFlash flash这个对象直接操作Flash硬件。Adafruit_M0_Express_CircuitPython pythonfs这个对象以flash对象为基础提供了一个符合CircuitPython分区规则的文件系统视图。后续所有文件操作都通过pythonfs进行。重要前提在运行此Arduino程序前你必须先给板子刷入CircuitPython固件。这是因为只有CircuitPython的引导程序才会创建我们所需的文件系统结构。去Adafruit官网下载对应板子的最新CircuitPython.uf2文件双击板子复位按钮进入引导加载模式BOOT盘把.uf2文件拖进去即可。3.3 文件读取以boot.py为例让我们看如何读取CircuitPython创建的文件void setup() { Serial.begin(115200); while (!Serial) delay(10); // 等待串口连接实际产品中可去掉 if (!pythonfs.begin()) { Serial.println(Failed to mount CircuitPython filesystem!); while (1); } // 检查并读取boot.py if (pythonfs.exists(boot.py)) { File bootPy pythonfs.open(boot.py, FILE_READ); Serial.println(--- boot.py内容 ---); while (bootPy.available()) { char c bootPy.read(); Serial.write(c); // 逐字符打印 } Serial.println(\n--- 结束 ---); bootPy.close(); // 良好习惯关闭文件 } else { Serial.println(没有找到boot.py文件。); } }操作要点pythonfs.begin()挂载文件系统必须在所有操作前调用。.exists(“文件名”)安全检查避免打开不存在的文件。.open(“文件名”, FILE_READ)以只读模式打开文件返回一个File对象。file.available()与file.read()这是流式读取的标准模式与从Serial读取数据完全一样。3.4 文件写入追加数据记录向一个既存文件或新建文件追加数据是数据记录的典型操作void loop() { // 以“追加写入”模式打开文件。文件不存在则创建。 File dataFile pythonfs.open(data.txt, FILE_WRITE); if (dataFile) { // 模拟采集传感器数据 float temperature readTemperature(); int humidity readHumidity(); // 像使用Serial一样将数据写入文件 dataFile.print(millis()); // 时间戳 dataFile.print(,); dataFile.print(temperature, 2); // 温度保留2位小数 dataFile.print(,); dataFile.println(humidity); // 湿度并换行 dataFile.close(); // 关闭文件确保数据物理写入Flash Serial.println(数据已记录到data.txt); delay(60000); // 每分钟记录一次 } else { Serial.println(打开data.txt失败); } }关键细节FILE_WRITE模式此模式为“追加写入”。如果文件存在新数据写在末尾如果不存在则创建新文件。务必关闭文件dataFile.close()。对于Flash存储关闭文件操作会触发底层将缓存数据真正写入芯片。如果不关闭最近写入的数据可能在断电后丢失。数据格式使用CSV逗号分隔值格式是通用且简单的选择后续用Excel、Python或CircuitPython都容易处理。实操心得完成Arduino端的数据写入后你想在电脑上查看data.txt怎么办很简单再次按下板子的复位按钮进入CircuitPython模式。此时电脑上会再次出现CIRCUITPY盘符直接打开里面的data.txt文件就能看到Arduino刚刚记录的数据了。这种无缝切换是开发调试的巨大福音。4. 核心操作二独立的SPI Flash数据记录如果你不需要与CircuitPython交互或者想独占整个Flash芯片获得最大空间那么可以将其格式化为一个标准的FAT文件系统。这时需要使用fatfs_format和fatfs_datalogging示例。4.1 格式化Flash芯片警告此操作将清空Flash内所有数据包括可能存在的CircuitPython系统。运行fatfs_format示例串口监视器会提示你输入OK确认。格式化过程需要约一分钟期间不要断电。格式化成功后Flash芯片就变成了一张纯粹的“空白SD卡”。此时你需要使用Adafruit_SPIFlash和FatFileSystem类来操作它而不是之前的Adafruit_M0_Express_CircuitPython类。4.2 独立数据记录示例以下是基于fatfs_datalogging的精简和注释版#include Adafruit_SPIFlash.h #include SdFat.h // 使用SdFat库进行文件操作 Adafruit_SPIFlash flash(FLASH_SS, FLASH_SPI_PORT); FatFileSystem fatfs; // 标准FAT文件系统对象 #define FILE_NAME “datalog.csv” void setup() { Serial.begin(115200); while (!Serial); if (!flash.begin()) { Serial.println(“Flash初始化失败”); while(1); } if (!fatfs.begin(flash)) { Serial.println(“文件系统挂载失败是否需要格式化”); while(1); } // 检查文件是否存在不存在则写入表头 if (!fatfs.exists(FILE_NAME)) { File dataFile fatfs.open(FILE_NAME, FILE_WRITE); if (dataFile) { dataFile.println(“时间戳(ms),温度(℃),湿度(%)”); // CSV表头 dataFile.close(); Serial.println(“创建文件并写入表头。”); } } } void loop() { File dataFile fatfs.open(FILE_NAME, FILE_WRITE); if (dataFile) { long timestamp millis(); float temp readTempSensor(); float humidity readHumiditySensor(); // 高效写入避免多次调用print构建字符串一次写入 String dataLine String(timestamp) “,” String(temp, 2) “,” String(humidity, 1); dataFile.println(dataLine); dataFile.close(); // 可选在串口也输出一份用于实时监控 Serial.println(“记录: ” dataLine); delay(5000); // 每5秒记录一次 } }性能优化技巧减少文件打开/关闭频率频繁开关文件会产生开销。对于高速记录可以考虑在setup中打开文件在loop中持续写入仅在特定条件如定时、满缓存或程序结束时才关闭。但要注意不关闭文件有数据丢失风险需权衡。批量写入如上例所示使用String拼接好一行数据然后用一次println写入比多次调用dataFile.print()更高效。缓冲区管理SdFat库内部有缓冲区。写入数据时数据先到缓冲区缓冲区满或文件关闭时才写入物理介质。了解这一点有助于调试“为什么刚写的数据没立刻看到”。5. 高级技巧与故障排查实录掌握了基本读写我们来看看一些进阶玩法和常见坑位。5.1 文件管理目录、删除与信息查询fatfs_full_usage示例展示了完整的文件操作。这里列举几个实用函数// 1. 创建目录 if (fatfs.mkdir(“/logs”)) { Serial.println(“目录logs创建成功”); } // 2. 重命名文件 if (fatfs.rename(“/old.txt”, “/new.txt”)) { Serial.println(“文件重命名成功”); } // 3. 删除文件 if (fatfs.remove(“/trash.txt”)) { Serial.println(“文件删除成功”); } // 4. 打开文件并获取信息 File myFile fatfs.open(“data.txt”, FILE_READ); if (myFile) { Serial.print(“文件大小: “); Serial.println(myFile.size()); // 字节数 Serial.print(“是否目录: “); Serial.println(myFile.isDirectory() ? “是” : “否”); myFile.close(); } // 5. 遍历目录 File root fatfs.open(“/”); while (File entry root.openNextFile()) { Serial.print(entry.name()); if (entry.isDirectory()) { Serial.println(“/”); } else { Serial.print(“\t\t”); Serial.println(entry.size(), DEC); } entry.close(); }5.2 常见问题与排查指南在实际项目中你几乎一定会遇到下面这些问题。这是我的踩坑记录问题1程序卡在while (!Serial);拔掉USB后板子不工作。原因这是为了方便调试让程序等待串口监视器打开。一旦拔掉USB串口永远等不到程序就卡死了。解决产品化时务必删除或注释掉这行代码。调试时再打开。问题2上传代码后板子在Arduino IDE中彻底“消失”找不到串口。原因上传的代码可能崩溃或进入了深度睡眠导致USB栈无法工作。解决使用“双击复位法”强制进入引导加载模式。在Arduino IDE中打开一个简单示例如Blink。选择正确的板卡型号至关重要。点击“上传”。当IDE状态栏显示“正在上传…”时快速双击板子上的RESET按钮。此时板载红色LED应呈现呼吸脉冲状表示已进入引导模式IDE应能检测到并完成上传。问题3使用某些USB线或USB口时电脑无法识别板子。原因你很可能使用了仅充电Charge-Only的USB线这种线没有数据传输线芯。解决立即换一根已知可传数据的USB线。这是硬件开发中最常见也最令人沮丧的问题之一。建议备一根质量好的短线专供开发。问题4Arduino程序无法读取CircuitPython创建的文件或反之。原因文件系统对象用错了。排查要与CircuitPython共享文件必须使用Adafruit_M0_Express_CircuitPython pythonfs(flash);。如果使用fatfs.begin(flash)格式化了Flash那么CircuitPython将无法识别这个文件系统。核心二者只能选其一。共享数据就用CircuitPython类独占使用就用FatFileSystem类并先格式化。问题5数据记录一段时间后文件损坏或无法打开。原因未正常关闭文件在写入操作后没有调用file.close()就断电了。电源不稳定在写入Flash的瞬间突然断电。Flash寿命SPI Flash有擦写次数限制通常10万次以上但频繁擦写同一扇区会加速其老化。解决与预防务必调用close()尤其是在每次写入循环结束时。添加电源监控如果使用电池检测电压过低时应停止写入并安全关闭文件。磨损均衡策略对于需要频繁记录日志的项目不要总是追加到同一个大文件。可以按时间或大小分割文件如log_001.txt,log_002.txt写满一个再开下一个。甚至可以设计简单的循环缓冲区在固定几个文件中轮换写入。5.3 功耗优化启用Buck Converter仅限部分M4板卡部分Adafruit M4板卡如Feather M4 Express设计了一个可选的1.8V降压转换器Buck Converter相比默认的线性稳压器LDO它能显著降低板子的整体功耗。如何启用在你的setup()函数最开始处添加一行代码SUPC-VREG.bit.SEL 1; // 启用Buck Converter注意事项先查原理图只有板子上物理焊接了电感通常标有L1或类似的型号才支持此功能。Feather M4 Express是支持的。副作用降压转换器可能会在电源上引入轻微的开关噪声。这可能导致ADC模拟读取和DAC模拟输出的精度略有下降。如果你的项目对模拟信号精度要求极高如高精度传感器采样请谨慎启用或进行测试。对于纯数字逻辑和数据记录应用启用它通常能节省约4mA的电流对电池供电项目很有吸引力。6. 项目实战构建一个离线数据记录器让我们综合运用以上知识设计一个简单的、电池供电的温湿度数据记录器。设计目标每10分钟记录一次温湿度和时间戳。数据以CSV格式存储方便导出分析。低功耗运行使用锂电池供电。数据文件可通过CircuitPython模式直接USB拷贝。硬件清单Adafruit Feather M0 Express (内置SPI Flash)DHT22 温湿度传感器 (连接至引脚5)3.7V锂电池核心代码框架#include Adafruit_SPIFlash.h #include Adafruit_M0_Express_CircuitPython.h #include “DHT.h” #define DHTPIN 5 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); Adafruit_SPIFlash flash(FLASH_SS, FLASH_SPI_PORT); Adafruit_M0_Express_CircuitPython pythonfs(flash); #define LOG_INTERVAL_MS 600000 // 10分钟 #define FILE_NAME “env_log.csv” void setup() { // 初始化串口调试用产品中可去掉 Serial.begin(115200); // while (!Serial); // 产品中注释掉 dht.begin(); // 初始化Flash和文件系统 if (!flash.begin()) { Serial.println(F(“Flash初始化失败”)); return; } if (!pythonfs.begin()) { Serial.println(F(“文件系统挂载失败”)); return; } // 如果是第一次运行创建文件并写入CSV表头 if (!pythonfs.exists(FILE_NAME)) { File logFile pythonfs.open(FILE_NAME, FILE_WRITE); if (logFile) { logFile.println(“Timestamp(UTC ms),Temperature(C),Humidity(%)”); logFile.close(); Serial.println(F(“日志文件已创建”)); } } Serial.println(F(“数据记录器启动...”)); } void loop() { static unsigned long lastLogTime 0; unsigned long now millis(); // 检查是否到达记录间隔 if (now - lastLogTime LOG_INTERVAL_MS) { lastLogTime now; // 读取传感器 float h dht.readHumidity(); float t dht.readTemperature(); // 检查读数是否有效 if (isnan(h) || isnan(t)) { Serial.println(F(“读取DHT22失败”)); return; } // 打开文件并追加数据 File logFile pythonfs.open(FILE_NAME, FILE_WRITE); if (logFile) { logFile.print(now); // 使用millis()作为简单时间戳 logFile.print(“,”); logFile.print(t, 1); // 温度1位小数 logFile.print(“,”); logFile.println(h, 1); // 湿度1位小数 logFile.close(); // 关键确保数据写入 // 调试输出 Serial.print(F(“已记录: “)); Serial.print(now); Serial.print(F(“, “)); Serial.print(t); Serial.print(F(“C, “)); Serial.print(h); Serial.println(F(“%”)); } else { Serial.println(F(“打开日志文件失败”)); } } // 在记录间隔内可以让CPU进入低功耗睡眠模式以省电 // 此处为简化示例仅使用delay。实际应用应使用ArduinoLowPower库。 delay(1000); // 每秒检查一次 }部署与数据提取流程将上述代码上传至Feather M0 Express。连接电池和传感器设备开始自动记录。需要导出数据时按下板载复位按钮板子将进入CircuitPython模式。电脑上会出现CIRCUITPY盘符直接打开并复制env_log.csv文件即可。复制完成后再次按复位按钮设备将重新运行Arduino数据记录程序。这个项目展示了SPI Flash作为嵌入式设备“数据黑匣子”的完美应用。它可靠、集成度高并且通过CircuitPython桥接实现了极其便捷的数据导出方式无需额外的读卡器或复杂的通信协议。