はじめに
自分の備忘録も兼ねて、M5Stack AtomS3R の中に CSV ファイルを保存しておき、プログラムから読み出して使う方法をまとめます。今回はロボットの動き(モーション)を CSV に書いておき、番号を指定して再生する用途で使っています。
この記事は、次のような人を想定しています。
- Arduino や PlatformIO でプログラムを書き込んだことはある
- マイコンに「ファイル」を保存したことはない
そのため、まずマイコンのメモリやファイルシステムの基本から説明し、そのあとで実際の手順と仕組みを説明します。
この記事で扱うこと
- 基礎知識(メモリ・ファイルシステム・SPIFFS・パーティション)
- PlatformIO の設定
- CSV を AtomS3R に書き込む手順
- 書き込みの仕組み
- 読み込みの仕組み
環境
| 項目 | 内容 |
|---|---|
| マイコン | M5Stack AtomS3R(ESP32-S3、フラッシュ 8MB) |
| 開発環境 | VS Code + PlatformIO |
| プラットフォーム | espressif32 |
| フレームワーク | Arduino |
| ファイルシステム | SPIFFS |
0. 全体像
最初に、全体の流れを図にしておきます。細かい言葉は次の章で説明します。
- パソコンの
dataフォルダにある CSV を、1 個のファイル(spiffs.bin)にまとめる - それを USB 経由で AtomS3R のフラッシュメモリに書き込む
- AtomS3R のプログラムが、そこからファイル名を指定して CSV を読む
①と②は PlatformIO のボタンを押すだけで実行できます。③はプログラムの中に自分で書きます。
1. 基礎知識
1-1. マイコンの 2 種類のメモリ:RAM とフラッシュ
AtomS3R に載っている ESP32-S3 には、大きく分けて 2 種類のメモリがあります。
| RAM | フラッシュメモリ | |
|---|---|---|
| 電源を切ると | 消える | 残る |
| 主な用途 | 変数・計算の途中結果 | プログラム本体・保存しておきたいデータ |
| パソコンでいうと | メモリ | SSD |
| AtomS3R の容量 | 内蔵 512KB + PSRAM 8MB | 8MB |
プログラムの中で int x = 10; のように作った変数は RAM に入ります。電源を切ると消えます。
一方、PlatformIO の Upload ボタンで書き込んだプログラムはフラッシュメモリに入っています。電源を切っても、次に電源を入れたときにまた動くのはそのためです。
CSV ファイルも電源を切ったあとに残しておきたいので、フラッシュメモリに保存します。
1-2. ファイルシステムとは
フラッシュメモリは、そのままでは「0x000000 番地から 0x7FFFFF 番地まで、数字が 800 万個ほど並んだ箱」でしかありません。「mtn1.csv というファイル」という区切りはありません。
そこで、
- どの番地からどの番地までが、どのファイルの中身か
- そのファイルの名前は何か
- ファイルの大きさは何バイトか
といった情報をフラッシュの中に一緒に記録しておき、ファイル名を指定するだけで中身を読み書きできるようにする仕組みを使います。これを ファイルシステム と呼びます。
パソコンでも同じで、NTFS、 FAT32 や exFAT というファイルシステムで管理されています。
1-3. SPIFFS とは
SPIFFS(SPI Flash File System)は、ESP32 のようなマイコンのフラッシュメモリ向けに作られた、小さくて軽いファイルシステムです。Arduino からは SPIFFS ライブラリで使えます。
#include "SPIFFS.h"
SPIFFS.begin(); // 使い始める
File f = SPIFFS.open("/mtn1.csv", "r"); // ファイルを開く
パソコンのファイルシステムと比べると、次のような違いがあります。
- フォルダが作れない:全部のファイルが 1 か所に並ぶ
-
ファイル名は
/で始める:/mtn1.csvのように書く -
ファイル名は 31 文字まで(先頭の
/を含む)
同じ用途の LittleFS というファイルシステムもあり、こちらはフォルダが使えて、書き込み中の電源断にも強いようです。新しく作るなら LittleFS を選ぶのもよいと思います。この記事では SPIFFS を使います。
1-4. パーティションとは
8MB のフラッシュメモリは、全部をファイル置き場に使っているわけではありません。プログラム本体を置く場所、設定を置く場所、ファイルを置く場所……というように、用途ごとに区切って使っています。この区切りを パーティション と呼び、区切り方を書いた表を パーティションテーブル と呼びます。
パソコンで 1 台の SSD を「C ドライブ」と「D ドライブ」に分けるのと同じ考え方です。
AtomS3R(今回の設定)では次のように区切られています。
CSV が入るのは spiffs パーティション(1.5MB) です。プログラム本体の置き場(app0)とは別の場所なので、プログラムを書き換えても CSV は消えませんし、CSV を書き換えてもプログラムは消えません。
1-5. なぜ CSV をプログラムに埋め込まず、ファイルにするのか
モーションデータは、プログラムの中に配列として直接書くこともできます。
// プログラムに直接書く場合
const float motion1[][26] = {
{0, 0, 0.0285, -4.28, ...},
{0, 0, 0, 0, ...},
};
ただしこの方法だと、モーションを 1 つ直すたびに、プログラム全体をビルドして書き込み直すことになります。
CSV ファイルにしておけば、
- Excel や表計算ソフトで編集できる
- 他のツール(モーション作成ソフトなど)が出力した CSV をそのまま使える
- CSV だけを入れ替えられる(プログラムのビルドが要らない)
という利点があります。
1-6. CSV ファイルとは
CSV(Comma-Separated Values)は、表の 1 行をテキストの 1 行に、セルの区切りをカンマ , にした ファイル形式です。中身はただのテキストなので、メモ帳でも開けます。
今回使う CSV は次のような形です。
frm num, delay, body_pos_x, body_pos_y, body_pos_z, body_rot_x, ... , leg_rot_z_r
1,50,0,0,0.02857149,-4.285714,0,0,0.24964,-0.350017,0.100034,0,...
2,50,0,0,0,0,0,0,0.25,-0.35,0.1,0,-0.25,-0.35,0.1,0,...
1 行目は列の名前(ヘッダ)、2 行目以降は 1 行が 1 コマ分の姿勢です。全部で 28 列あります。
| 列 | 内容 |
|---|---|
| 1 列目 | フレーム番号(読み込み時は使わない) |
| 2 列目 | delay:次の姿勢へ何秒かけて移るか(20ms 単位。50 なら 50 × 20ms = 1 秒) |
| 3〜28 列目 | 姿勢を表す 26 個の値(胴体・腕・脚の位置と回転) |
ファイル名は mtn<番号>.csv(mtn1.csv、mtn2.csv、……)にしています。プログラムは番号からファイル名を作って読みます。
2. PlatformIO の設定
2-1. フォルダ構成
csv_write/
├── platformio.ini ← プロジェクトの設定
├── data/ ← ★ここに置いたファイルが AtomS3R に書き込まれる
│ ├── mtn1.csv
│ ├── mtn2.csv
│ └── ...
└── src/
└── main.cpp ← プログラム
プロジェクト直下に data という名前のフォルダを作り、そこに CSV を入れます。 PlatformIO は「data フォルダの中身をファイル置き場に書き込む」と決まっています。フォルダ名を間違えると書き込まれません。
2-2. パーティションテーブルの中身
今回の設定(ボード esp32-s3-devkitc-1)で使われるパーティションテーブル default_8MB.csv は、次の内容です(PlatformIO のインストール先 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/ にあります)。
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x330000,
app1, app, ota_1, 0x340000,0x330000,
spiffs, data, spiffs, 0x670000,0x180000,
coredump, data, coredump,0x7F0000,0x10000,
各列の意味は次のとおりです。
| 列 | 意味 |
|---|---|
| Name | パーティションの名前 |
| Type / SubType | 用途(app はプログラム、data + spiffs はファイル置き場) |
| Offset | フラッシュの何番地から始まるか(16 進数) |
| Size | 大きさ(16 進数) |
spiffs の行を見ると、0x670000 番地から 0x180000 バイト(= 1,572,864 バイト = 1.5MB) がファイル置き場だとわかります。
今回の CSV は 25 ファイルで合計約 97KB なので、十分に収まります。もっと大きなファイルを置きたいときは、パーティションテーブルを自作して board_build.partitions = my_partitions.csv で指定します。
3. 書き込み手順
- 書き込みたい CSV を
dataフォルダに入れる - シリアルモニタを開いていたら閉じる(開いたままだと USB ポートが使えず失敗します)
- VS Code の左端にある PlatformIO のアイコン(アリの顔)をクリックする
- PROJECT TASKS → ATOMS3R → Platform を開く
- Build Filesystem Image をクリックする
- Upload Filesystem Image をクリックする
ここで「ATOMS3R」と表示されるのは、platformio.ini の [env:ATOMS3R] の名前です。
ターミナルからコマンドで実行することもできます。
pio run -e ATOMS3R -t buildfs
pio run -e ATOMS3R -t uploadfs
覚えておくこと
-
「Upload」と「Upload Filesystem Image」は別の操作です。Upload はプログラム本体を、Upload Filesystem Image は
dataフォルダの中身を書き込みます。最初は両方を実行してください。 - CSV だけを直したときは、Upload Filesystem Image だけを実行すれば済みます。
- Upload Filesystem Image を押すと Build Filesystem Image も自動で実行されるので、⑤ は省略してもかまいません。
4. 書き込みの仕組み
ボタン 2 つで書き込めますが、PlatformIO の内部では何が起きているのでしょうか。espressif32 プラットフォームのビルドスクリプト(~/.platformio/platforms/espressif32/builder/main.py)を読むと、次の 2 つのツールを順に呼んでいることがわかります。
4-1. Build Filesystem Image:フォルダを 1 個のファイルにまとめる
このボタンは、data フォルダの中身を「SPIFFS の形式で並べたら、フラッシュの中身はこうなる」というデータに変換し、1 個のファイルに保存します。このファイルを イメージファイル と呼びます。
使っているツールは mkspiffs(make SPIFFS)で、実際には次のようなコマンドが実行されています。
mkspiffs -c data -s 0x180000 -p 256 -b 4096 .pio/build/ATOMS3R/spiffs.bin
| オプション | 意味 |
|---|---|
-c data |
data フォルダの中身からイメージを作る(c は create) |
-s 0x180000 |
イメージの大きさ。パーティションテーブルの spiffs の Size と同じ 1.5MB |
-p 256 |
ページサイズ(SPIFFS が一度に読み書きする単位)256 バイト |
-b 4096 |
ブロックサイズ(フラッシュを消去する単位)4096 バイト |
.pio/build/ATOMS3R/spiffs.bin |
出力するイメージファイル |
ポイントは次の 3 つです。
-
大きさ
0x180000はパーティションテーブルから自動で読み取られます。 自分で指定する必要はありません。 - できあがった
spiffs.binには、CSV の中身だけでなく、ファイル名や大きさなどの管理情報も入っています。 - CSV の合計は 97KB ほどですが、
spiffs.binはパーティションと同じ 1.5MB になります(空き領域もまるごと含むため)。
data/mtn1.csv は、SPIFFS の中では /mtn1.csv という名前で登録されます。
4-2. Upload Filesystem Image:イメージをフラッシュに書き込む
このボタンは、spiffs.bin を AtomS3R のフラッシュの spiffs パーティションの位置にそのまま書き込みます。使っているツールは、ESP32 にデータを書き込むための esptool.py です。
esptool.py --chip esp32s3 --port COMx --baud 1500000 \
write_flash -z --flash_mode qio --flash_freq 80m --flash_size 8MB \
0x670000 .pio/build/ATOMS3R/spiffs.bin
| 部分 | 意味 |
|---|---|
--chip esp32s3 |
書き込み先のチップ |
--port COMx |
AtomS3R がつながっている USB ポート |
--baud 1500000 |
通信速度(platformio.ini の upload_speed) |
write_flash -z |
フラッシュに書き込む(-z は圧縮して送る) |
0x670000 .pio/.../spiffs.bin |
0x670000 番地から spiffs.bin を書き込む |
0x670000 はパーティションテーブルの spiffs の Offset です。
いつもの Upload ボタン(プログラムの書き込み)も同じ esptool.py を使っていて、違うのは書き込む番地とファイルだけです。
| ボタン | 書き込むファイル | 書き込む番地 |
|---|---|---|
| Upload |
firmware.bin(プログラム) |
0x10000(app0) |
| Upload Filesystem Image |
spiffs.bin(CSV) |
0x670000(spiffs) |
書き込む場所が重ならないので、片方だけを書き換えられます。
Upload Filesystem Image は spiffs パーティション全体を上書きします。プログラムの実行中に SPIFFS に保存したファイルがあれば、それも消えます。
5. 読み込みの仕組み
ここからは AtomS3R 側のプログラムです。
5-1. まずは最小のプログラム
実際のプログラムの前に、CSV を 1 つ開いて中身をシリアルモニタに表示するだけ の最小のプログラムを示します。
#include <Arduino.h>
#include "SPIFFS.h"
void setup() {
Serial.begin(115200);
delay(1000);
// ① ファイルシステムを使い始める
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS のマウントに失敗");
return;
}
// ② 保存されているファイルの一覧を表示する
File root = SPIFFS.open("/");
File entry = root.openNextFile();
while (entry) {
Serial.printf("%s %d bytes\n", entry.name(), (int)entry.size());
entry = root.openNextFile();
}
// ③ ファイルを開く
File file = SPIFFS.open("/mtn3.csv", "r");
if (!file) {
Serial.println("ファイルが見つからない");
return;
}
// ④ 1 行ずつ読んで表示する
while (file.available()) {
String line = file.readStringUntil('\n');
Serial.println(line);
}
// ⑤ ファイルを閉じる
file.close();
}
void loop() {
}
順に説明します。
① SPIFFS.begin(true):ファイルシステムを使い始める
パーティションテーブルから spiffs パーティションを探し、ファイルを読み書きできる状態にします。この操作を マウント と呼びます。ファイルを扱う前に、必ず 1 回呼びます。
引数の true は「マウントに失敗したら、パーティションをフォーマット(初期化)してからマウントする」という意味です。Upload Filesystem Image をまだ一度もしていない AtomS3R でも、空のファイル置き場としてマウントできます。
② ファイルの一覧を表示する
SPIFFS.open("/") で一番上の場所を開き、openNextFile() で中のファイルを 1 つずつ取り出します。ファイルがなくなると entry が空になり、ループが終わります。書き込みがうまくいったかを確かめるのに便利です。
③ SPIFFS.open("/mtn3.csv", "r"):ファイルを開く
1 つ目の引数はファイル名、2 つ目はモードです。
| モード | 意味 |
|---|---|
"r" |
読み込み(read) |
"w" |
書き込み(write)。中身は消えて新しく書き直す |
"a" |
追記(append)。今の中身の後ろに書き足す |
ファイルがないと file は空になり、if (!file) が成り立ちます。
④ 1 行ずつ読む
-
file.available():まだ読んでいないデータがあればtrue -
file.readStringUntil('\n'):改行文字\nが出てくるまでを読み、Stringとして返す(改行文字そのものは含まない)
これを繰り返すと、ファイルの先頭から 1 行ずつ取り出せます。
⑤ file.close():ファイルを閉じる
使い終わったファイルは閉じます。開いたままにしておくと、メモリを使ったままになります。
ここからは、実際のプログラム(main.cpp)で CSV をどう読んでいるかを見ていきます。流れは次のとおりです。
5-2. マウント
最小の例と同じく、setup() の最後でマウントしています。
#include "SPIFFS.h" // ESP32向けのSPIFFSライブラリをインクルード
void setup() {
// ...(他の初期化)
if (!SPIFFS.begin(true)) {
USBSerial.println("SPIFFSのマウントに失敗");
return;
}
}
USBSerial は AtomS3R の USB 経由のシリアル出力です(最小の例の Serial と同じものと考えてかまいません)。
5-3. 読み込むファイル名を作る
コントローラから BLE で「何番のモーションを再生するか」を受け取り、ファイル名を作ります。
// 6番目(bleVal6)が 0→1 でCSV読み込みをトリガ
if (lastBleVal6 == 0 && bleVal6 == 1) {
csvFileToLoad = "/mtn" + String(fileNumber) + ".csv";
if (!isCsvProcessing) {
playbackRequest = true;
isCsvProcessing = true; // タスク起動前にセット
}
}
lastBleVal6 = bleVal6;
-
fileNumberが3なら、csvFileToLoadは"/mtn3.csv"になります。先頭の/を忘れないようにします。 -
bleVal6が 0 から 1 に変わった瞬間だけ「再生の合図」として扱います。1 のままのときは何もしないので、同じモーションが何度も始まることはありません。(この辺のトリガの挙動はコントローラからの制御なので今回は説明を割愛します。) -
isCsvProcessingは「いま読み込み・再生中かどうか」の目印です。再生中に次の合図が来ても無視します。
5-4. 読み込み用のタスクを起動する
loop() の中で合図を見つけたら、タスク を作って読み込みと再生を任せます。
if (playbackRequest && isCsvProcessing) {
char filePath[64];
csvFileToLoad.toCharArray(filePath, 64);
xTaskCreate(csvProcessingTask, "CSVTask", 8192, filePath, 1, NULL);
playbackRequest = false;
}
void csvProcessingTask(void* param) {
char filePath[64];
strncpy(filePath, (char*)param, 63);
filePath[63] = '\0';
ProcessCsvAndInterpolate(filePath); // CSV を読んで再生する
playbackRequest = false;
isCsvProcessing = false;
vTaskDelete(NULL); // 終わったらタスクを消す
}
タスクとは、loop() とは別に、同時に並行して動く処理のことです。ESP32 には FreeRTOS という仕組みが入っていて、xTaskCreate で新しいタスクを作れます。
| 引数 | 値 | 意味 |
|---|---|---|
| 1 | csvProcessingTask |
タスクとして動かす関数 |
| 2 | "CSVTask" |
タスクの名前(デバッグ用) |
| 3 | 8192 |
タスクが使うメモリ(スタック)の大きさ |
| 4 | filePath |
関数に渡す値(ここではファイル名) |
| 5 | 1 |
優先度 |
| 6 | NULL |
作ったタスクを後で操作するための値(今回は使わない) |
モーションの再生には数秒かかります。もし loop() の中で直接再生すると、その数秒間は loop() の他の処理(センサーの読み取りや通信など)が止まってしまいます。タスクに分けることで、再生しながら他の処理も続けられます。
5-5. CSV を読んで、列に分けて、数値にする
いよいよ CSV を読む部分です。まず、1 行分のデータを入れておく入れ物(構造体)を用意しています。
#define NUM_VARIABLES 26
// CSVの1行分のデータを保持する構造体
struct FrameData {
int delayMultiplier; // 2列目のdelay値(20msec単位)
float values[NUM_VARIABLES]; // 3列目~28列目の26個の数値
};
// CSVの各フレームのデータを格納するベクター
std::vector<FrameData> frames;
-
FrameDataは CSV の 1 行分です。delay(整数)1 個と、姿勢の値(小数)26 個を持ちます。 -
std::vector<FrameData>は、FrameDataをいくつでも追加できる配列です。CSV の行数は決まっていないので、普通の配列ではなくvectorを使っています。
読み込みの関数は次のとおりです。
void ProcessCsvAndInterpolate(const char* filePath) {
// (A) ファイルを開く
File file = SPIFFS.open(filePath, "r");
if (!file) return;
frames.clear();
// (B) 全部の行を読んでおく
std::vector<String> lines;
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() > 0) lines.push_back(line);
}
file.close();
if (lines.size() < 2) return;
// (C) 1 行ずつ、カンマで列に分ける
for (size_t i = 1; i < lines.size(); i++) {
String line = lines[i];
const int expectedColumns = 28;
String columns[expectedColumns];
int col = 0, startIndex = 0;
while (true) {
int commaIndex = line.indexOf(',', startIndex);
if (commaIndex == -1) { columns[col++] = line.substring(startIndex); break; }
columns[col++] = line.substring(startIndex, commaIndex);
startIndex = commaIndex + 1;
if (col >= expectedColumns) break;
}
if (col < expectedColumns) continue;
// (D) 文字を数値に変換して保存する
FrameData frame;
frame.delayMultiplier = columns[1].toInt();
for (int j = 0; j < NUM_VARIABLES; j++) frame.values[j] = columns[j + 2].toFloat();
frames.push_back(frame);
}
if (frames.empty()) return;
// ... 以降は frames を使ってモーションを再生する
}
順に見ていきます。
(A) ファイルを開く
最小の例と同じです。ファイルがなければ、何もせずに関数を抜けます。frames.clear() で、前回読んだデータを消しておきます。
(B) 全部の行を読んでおく
1 行ずつ読んで、lines という vector に入れていきます。
-
line.trim():行の前後にある空白や改行文字を取り除きます。Windows で作った CSV は行末が\r\nになっていて、readStringUntil('\n')だけだと\rが残ります。trim()で\rも消えるので、どちらの改行コードの CSV でも同じように読めます。 -
if (line.length() > 0):空行(最後の行の後ろなど)は捨てます。
全部読み終わったら、すぐに file.close() で閉じています。
lines.size() < 2 は「ヘッダしかない(データが 1 行もない)」という意味なので、その場合は終わります。
(C) 1 行ずつ、カンマで列に分ける
i = 1 から始めているのは、lines[0] はヘッダ(列の名前)なので読み飛ばす ためです。
1 行を列に分ける部分は、次の String の機能を使っています。
| 書き方 | 意味 | 例(line = "1,50,0.25") |
|---|---|---|
line.indexOf(',', 開始位置) |
開始位置から探して、最初の , が何文字目か。なければ -1
|
line.indexOf(',', 0) → 1
|
line.substring(a, b) |
a 文字目から b 文字目の手前までを切り出す |
line.substring(0, 1) → "1"
|
line.substring(a) |
a 文字目から最後までを切り出す |
line.substring(5) → "0.25"
|
"1,50,0.25" を例に、ループの動きを追うと次のようになります。
| 回 | startIndex | 見つかった , の位置 |
切り出した文字列 | 入れる場所 |
|---|---|---|---|---|
| 1 | 0 | 1 | "1" |
columns[0] |
| 2 | 2 | 4 | "50" |
columns[1] |
| 3 | 5 | なし(-1) |
"0.25"(最後まで) |
columns[2] |
, が見つからなくなったら、残りを最後の列として入れてループを抜けます。
28 列そろわなかった行(途中で切れている行など)は、continue で読み飛ばします。
(D) 文字を数値に変換して保存する
ここまでの columns は、まだ "50" や "0.25" という 文字列 です。計算に使うには数値に変換します。
| 書き方 | 意味 | 例 |
|---|---|---|
toInt() |
文字列を整数に変換 |
"50" → 50
|
toFloat() |
文字列を小数に変換 |
"0.25" → 0.25
|
- 2 列目(
columns[1])を整数にしてdelayMultiplierに入れる - 3〜28 列目(
columns[2]〜columns[27])を小数にしてvalues[0]〜values[25]に入れる - 1 列目(フレーム番号)は使わない
できた frame を frames.push_back(frame) で frames の最後に追加します。これを全部の行で繰り返すと、frames に CSV の全データがそろいます。
あとは frames を先頭から順に使って、ロボットの姿勢を少しずつ変えていきます(この部分は CSV の読み込みとは関係ないので省略します)。
6. よくあるつまずき
| 症状 | 原因と対策 |
|---|---|
SPIFFS.open が失敗する |
ファイル名の先頭に / がない。"mtn1.csv" ではなく "/mtn1.csv"
|
| ファイルが見つからない | Upload Filesystem Image をしていない。またはフォルダ名が data ではない |
| Upload Filesystem Image が失敗する | シリアルモニタが開いたまま。閉じてからやり直す |
| CSV を直したのに反映されない | Upload(プログラム)だけをして、Upload Filesystem Image をしていない |
| サブフォルダのファイルが読めない | SPIFFS にはフォルダがない。data/sub/a.csv は /sub/a.csv という名前の 1 個のファイルになる |
| 長い名前のファイルが読めない | SPIFFS のファイル名は / を含めて 31 文字まで |
| 実行中に保存したファイルが消えた | Upload Filesystem Image はファイル置き場全体を上書きする |
まとめ
- マイコンのフラッシュメモリは電源を切っても消えない。そこに SPIFFS というファイルシステムを置くと、ファイル名でデータを読み書きできる。
- フラッシュは パーティション で区切られていて、AtomS3R(今回の設定)では
0x670000番地からの 1.5MB がファイル置き場。 - PlatformIO では、
dataフォルダに CSV を置いて Build Filesystem Image → Upload Filesystem Image を押すだけで書き込める。- 内部では
mkspiffsがdataフォルダをspiffs.binにまとめ、esptool.pyがそれを0x670000番地に書き込んでいる。
- 内部では
- プログラムからは
SPIFFS.begin()でマウントし、SPIFFS.open("/mtn1.csv", "r")で開いて、readStringUntil('\n')で 1 行ずつ、indexOf(',')とsubstring()で 1 列ずつ取り出し、toInt()/toFloat()で数値にして使う。
