Arduino Core 3.3.6

This commit is contained in:
Jason2866
2026-01-22 00:13:43 +01:00
parent 879780c5a9
commit aa6e97c918
41 changed files with 4345 additions and 153 deletions
+1
View File
@@ -15,6 +15,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-15]
example:
- "examples/arduino-blink"
- "examples/arduino-fatfs"
- "examples/arduino-rmt-blink"
- "examples/arduino-usb-keyboard"
- "examples/arduino-wifiscan"
+28 -1
View File
@@ -29,8 +29,35 @@ Prerequisites:
[pioarduino Wiki](https://deepwiki.com/pioarduino/platform-espressif32)
The Wiki is AI generated and insane detailed and accurate.
# Features
## Filesystem Support
pioarduino provides native support for multiple filesystem options, allowing you to choose the best solution for your project's needs:
- **LittleFS** (default) - Modern wear-leveling filesystem designed specifically for flash memory. Offers excellent reliability and performance for ESP32 projects.
- **SPIFFS** - Simple legacy filesystem. While still functional, LittleFS is recommended for new projects due to better wear-leveling and reliability.
- **FatFS** - Industry-standard FAT filesystem with broad compatibility across platforms and operating systems.
**Quick Start:**
```ini
[env:myenv]
board_build.filesystem = fatfs
```
**Available Commands:**
```bash
pio run -t buildfs # Build FatFS image
pio run -t uploadfs # Upload FatFS image
pio run -t download_fatfs # Download and extract FatFS from device
```
See the [arduino-fatfs example](examples/arduino-fatfs/) for a complete working example.
### Stable Arduino
currently espressif Arduino 3.3.5 and IDF 5.5.1.251215
currently espressif Arduino 3.3.6 and IDF 5.5.2+
```ini
[env:stable]
+1 -1
View File
@@ -44,7 +44,7 @@
],
"name": "Espressif ESP32-S3-DevKitC-1-N16R2 (16 MB Flash Quad, 2 MB PSRAM Quad)",
"upload": {
"flash_size": "8MB",
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
+1 -1
View File
@@ -44,7 +44,7 @@
],
"name": "Espressif ESP32-S3-DevKitC-1-N4R2 (4 MB Flash Quad, 2 MB PSRAM Quad)",
"upload": {
"flash_size": "8MB",
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
+1 -1
View File
@@ -44,7 +44,7 @@
],
"name": "Espressif ESP32-S3-DevKitC-1-N4R8 (4 MB Flash Quad, 8 MB PSRAM Octal)",
"upload": {
"flash_size": "8MB",
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
+1 -1
View File
@@ -23,7 +23,7 @@
],
"name": "Labplus mPython",
"upload": {
"flash_size": "8MB",
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
+1 -1
View File
@@ -40,7 +40,7 @@
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 460800
},
+864 -34
View File
@@ -12,15 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib.util
import locale
import os
import re
import shlex
import shutil
import struct
import subprocess
import sys
from os.path import isfile, join
from pathlib import Path
from littlefs import LittleFS
from fatfs import Partition, RamDisk, create_extended_partition
from fatfs import create_esp32_wl_image
from fatfs import calculate_esp32_wl_overhead
from fatfs import is_esp32_wl_image, extract_fat_from_esp32_wl
from fatfs.partition_extended import PartitionExtended
from fatfs.wrapper import pyf_mkfs, PY_FR_OK as FR_OK
import importlib.util
from SCons.Script import (
ARGUMENTS,
@@ -48,6 +57,15 @@ build_dir = Path(projectconfig.get("platformio", "build_dir"))
# Configure Python environment through centralized platform management
PYTHON_EXE, esptool_binary_path = platform.setup_python_env(env)
# Load SPIFFS generator from local module
spiffsgen_path = platform_dir / "builder" / "spiffsgen.py"
spec = importlib.util.spec_from_file_location("spiffsgen", str(spiffsgen_path))
spiffsgen = importlib.util.module_from_spec(spec)
sys.modules["spiffsgen"] = spiffsgen
spec.loader.exec_module(spiffsgen)
SpiffsFS = spiffsgen.SpiffsFS
SpiffsBuildConfig = spiffsgen.SpiffsBuildConfig
# Load board configuration and determine MCU architecture
board = env.BoardConfig()
board_id = env.subst("$BOARD")
@@ -391,12 +409,6 @@ def fetch_fs_size(env):
env["FS_PAGE"] = int("0x100", 16)
env["FS_BLOCK"] = int("0x1000", 16)
# FFat specific offsets, see:
# https://github.com/lorol/arduino-esp32fatfs-plugin#notes-for-fatfs
if filesystem == "fatfs":
env["FS_START"] += 4096
env["FS_SIZE"] -= 4096
def __fetch_fs_size(target, source, env):
"""
@@ -414,6 +426,342 @@ def __fetch_fs_size(target, source, env):
return (target, source)
def build_fs_image(target, source, env):
"""
Build filesystem image using littlefs-python.
Args:
target: SCons target (output .bin file)
source: SCons source (directory with files)
env: SCons environment object
Returns:
int: 0 on success, 1 on failure
"""
# Get parameters
source_dir = str(source[0])
target_file = str(target[0])
fs_size = env["FS_SIZE"]
block_size = env.get("FS_BLOCK", 4096)
# Calculate block count
block_count = fs_size // block_size
# Get disk version from board config or project options
# Default to LittleFS version 2.1 (0x00020001)
disk_version_str = "2.1"
# Try to read from project config (env-specific or common section)
for section in ["env:" + env["PIOENV"], "common"]:
if projectconfig.has_option(section, "board_build.littlefs_version"):
disk_version_str = projectconfig.get(section, "board_build.littlefs_version")
break
# Parse version string and create proper version integer
# LittleFS version format: (major << 16) | (minor << 0)
try:
version_parts = str(disk_version_str).split(".")
major = int(version_parts[0])
minor = int(version_parts[1]) if len(version_parts) > 1 else 0
# Format: major in upper 16 bits, minor in lower 16 bits
disk_version = (major << 16) | minor
except (ValueError, IndexError):
print(f"Warning: Invalid littlefs version '{disk_version_str}', using default 2.1")
disk_version = (2 << 16) | 1
try:
# Create LittleFS instance with Arduino / IDF compatible parameters
fs = LittleFS(
block_size=block_size,
block_count=block_count,
read_size=1, # Minimum read size
prog_size=1, # Minimum program size
cache_size=block_size, # Cache size = block size
lookahead_size=32, # Default lookahead buffer
block_cycles=500, # Wear leveling cycles
name_max=64, # ESP-IDF default filename length
disk_version=disk_version,
mount=True
)
# Add all files from source directory
source_path = Path(source_dir)
if source_path.exists():
for item in source_path.rglob("*"):
rel_path = item.relative_to(source_path)
fs_path = rel_path.as_posix()
if item.is_dir():
fs.makedirs(fs_path, exist_ok=True)
# Set directory mtime attribute
try:
mtime = int(item.stat().st_mtime)
fs.setattr(fs_path, 't', mtime.to_bytes(4, 'little'))
except Exception:
pass # Ignore timestamp errors
else:
# Ensure parent directories exist
if rel_path.parent != Path("."):
fs.makedirs(rel_path.parent.as_posix(), exist_ok=True)
# Copy file
with fs.open(fs_path, "wb") as dest:
dest.write(item.read_bytes())
# Set file mtime attribute (ESP-IDF compatible)
try:
mtime = int(item.stat().st_mtime)
fs.setattr(fs_path, 't', mtime.to_bytes(4, 'little'))
except Exception:
pass # Ignore timestamp errors
# Write filesystem image
with open(target_file, "wb") as f:
f.write(fs.context.buffer)
return 0
except Exception as e:
print(f"Error building filesystem image: {e}")
return 1
def build_spiffs_image(target, source, env):
"""
Build SPIFFS filesystem image using spiffsgen.py.
Args:
target: SCons target (output .bin file)
source: SCons source (directory with files)
env: SCons environment object
Returns:
int: 0 on success, 1 on failure
"""
# Get parameters
source_dir = str(source[0])
target_file = str(target[0])
fs_size = env["FS_SIZE"]
page_size = env.get("FS_PAGE", 256)
block_size = env.get("FS_BLOCK", 4096)
# Get SPIFFS configuration from project config or use defaults
obj_name_len = 32
meta_len = 4
use_magic = True
use_magic_len = True
aligned_obj_ix_tables = False
# Check common section first, then env-specific (so env-specific takes precedence)
for section in ["common", "env:" + env["PIOENV"]]:
if projectconfig.has_option(section, "board_build.spiffs.obj_name_len"):
obj_name_len = int(projectconfig.get(section, "board_build.spiffs.obj_name_len"))
if projectconfig.has_option(section, "board_build.spiffs.meta_len"):
meta_len = int(projectconfig.get(section, "board_build.spiffs.meta_len"))
if projectconfig.has_option(section, "board_build.spiffs.use_magic"):
use_magic = projectconfig.getboolean(section, "board_build.spiffs.use_magic")
if projectconfig.has_option(section, "board_build.spiffs.use_magic_len"):
use_magic_len = projectconfig.getboolean(section, "board_build.spiffs.use_magic_len")
if projectconfig.has_option(section, "board_build.spiffs.aligned_obj_ix_tables"):
aligned_obj_ix_tables = projectconfig.getboolean(section, "board_build.spiffs.aligned_obj_ix_tables")
try:
# Create SPIFFS build configuration
spiffs_build_config = SpiffsBuildConfig(
page_size=page_size,
page_ix_len=2, # SPIFFS_PAGE_IX_LEN
block_size=block_size,
block_ix_len=2, # SPIFFS_BLOCK_IX_LEN
meta_len=meta_len,
obj_name_len=obj_name_len,
obj_id_len=2, # SPIFFS_OBJ_ID_LEN
span_ix_len=2, # SPIFFS_SPAN_IX_LEN
packed=True,
aligned=True,
endianness='little',
use_magic=use_magic,
use_magic_len=use_magic_len,
aligned_obj_ix_tables=aligned_obj_ix_tables
)
# Create SPIFFS filesystem
spiffs = SpiffsFS(fs_size, spiffs_build_config)
# Add all files from source directory
source_path = Path(source_dir)
if source_path.exists():
for item in source_path.rglob("*"):
if item.is_file():
rel_path = item.relative_to(source_path)
img_path = "/" + rel_path.as_posix()
spiffs.create_file(img_path, str(item))
# Generate binary image
image = spiffs.to_binary()
# Write to file
with open(target_file, "wb") as f:
f.write(image)
print(f"\nSuccessfully created SPIFFS image: {target_file}")
return 0
except Exception as e:
print(f"Error building SPIFFS image: {e}")
return 1
def build_fatfs_image(target, source, env):
"""
Build FatFS filesystem image with ESP32 Wear Leveling support.
Uses fatfs-ng module to create ESP-IDF compatible WL-wrapped FAT images.
Args:
target: SCons target (output .bin file)
source: SCons source (directory with files)
env: SCons environment object
Returns:
int: 0 on success, 1 on failure
"""
# Get parameters
source_dir = str(source[0])
target_file = str(target[0])
fs_size = env["FS_SIZE"]
sector_size = env.get("FS_SECTOR", 4096)
# ESP-IDF WL layout (following wl_fatfsgen.py):
# [dummy sector] [FAT data] [state1] [state2] [config]
# Total WL sectors: 1 dummy + 2 states + 1 config = 4 sectors
wl_info = calculate_esp32_wl_overhead(fs_size, sector_size)
wl_reserved_sectors = wl_info['wl_overhead_sectors']
fat_fs_size = wl_info['fat_size']
sector_count = wl_info['fat_sectors']
try:
# Create RAM disk with the FAT filesystem size (without WL overhead)
storage = bytearray(fat_fs_size)
disk = RamDisk(storage, sector_size=sector_size, sector_count=sector_count)
# Create partition, format, and mount
base_partition = Partition(disk)
# Format the filesystem with proper workarea size for LFN support
# Workarea needs to be at least sector_size, use 2x for safety with LFN
workarea_size = sector_size * 2
# Create filesystem with parameters matching ESP-IDF expectations:
# - n_fat=2: Two FAT copies for redundancy
# - align=0: Auto-align (let FATFS decide)
# - n_root=512: Number of root directory entries (FAT12/16 only, 0 for FAT32)
# - au_size=0: Auto allocation unit size
ret = pyf_mkfs(
base_partition.pname,
n_fat=2,
align=0,
n_root=512, # Standard root entries for FAT16
au_size=0, # Auto
workarea_size=workarea_size
)
if ret != FR_OK:
raise Exception(f"Failed to format filesystem: error code {ret}")
# Mount the filesystem
base_partition.mount()
# Wrap with extended partition for directory support
partition = PartitionExtended(base_partition)
# Track skipped files
skipped_files = []
# Add all files from source directory
source_path = Path(source_dir)
if source_path.exists():
for item in source_path.rglob("*"):
rel_path = item.relative_to(source_path)
fs_path = "/" + rel_path.as_posix()
if item.is_dir():
try:
partition.mkdir(fs_path)
except Exception:
# Directory might already exist or be root
pass
else:
# Ensure parent directories exist
if rel_path.parent != Path("."):
parent_path = "/" + rel_path.parent.as_posix()
try:
partition.mkdir(parent_path)
except Exception:
pass # Directory might already exist
# Copy file
try:
with partition.open(fs_path, "w") as dest:
dest.write(item.read_bytes())
except Exception as e:
print(f"Warning: Failed to write file {rel_path}: {e}")
skipped_files.append(str(rel_path))
# Unmount filesystem
base_partition.unmount()
# Read boot sector parameters for validation
bytes_per_sector = struct.unpack('<H', storage[11:13])[0]
reserved_sectors = struct.unpack('<H', storage[14:16])[0]
num_fats = storage[16]
sectors_per_fat = struct.unpack('<H', storage[22:24])[0]
total_sectors = struct.unpack('<H', storage[19:21])[0]
# Validate boot sector matches our expectations
if bytes_per_sector != sector_size:
raise Exception(f"Boot sector bytes_per_sector ({bytes_per_sector}) != sector_size ({sector_size})")
print("\nBoot sector validation:")
print(f" Bytes per sector: {bytes_per_sector}")
print(f" Reserved sectors: {reserved_sectors}")
print(f" Number of FATs: {num_fats}")
print(f" Sectors per FAT: {sectors_per_fat}")
print(f" Total sectors: {total_sectors}")
# Wrap FAT image with ESP-IDF wear leveling layer
# This uses the fatfs-ng module's ESP32WearLeveling implementation
print("\nWrapping FAT image with ESP-IDF wear leveling...")
print(f" Layout: {wl_info['layout']}")
print(f" Partition size: {fs_size} bytes")
print(f" FAT filesystem size: {fat_fs_size} bytes ({sector_count} sectors)")
print(f" WL overhead: {wl_reserved_sectors} sectors ({wl_info['wl_overhead_size']} bytes)")
wl_image = create_esp32_wl_image(bytes(storage), fs_size, sector_size)
print(f" WL-wrapped image created ({len(wl_image)} bytes)")
# Write WL-wrapped image to file
with open(target_file, "wb") as f:
f.write(wl_image)
# Print summary
if skipped_files:
print(f"\nWarning: {len(skipped_files)} file(s) skipped:")
for skipped in skipped_files[:10]: # Show first 10
print(f" - {skipped}")
if len(skipped_files) > 10:
print(f" ... and {len(skipped_files) - 10} more")
print(f"\nSuccessfully created ESP-IDF WL-wrapped FAT image: {target_file}")
return 0
except Exception as e:
print(f"Error building FatFS image: {e}")
return 1
def check_lib_archive_exists():
"""
Check if lib_archive is set in platformio.ini configuration.
@@ -427,14 +775,29 @@ def check_lib_archive_exists():
return False
def build_fs_router(target, source, env):
"""Route to appropriate filesystem builder based on filesystem type."""
fs_type = board.get("build.filesystem", "littlefs")
if fs_type == "littlefs":
return build_fs_image(target, source, env)
elif fs_type == "fatfs":
return build_fatfs_image(target, source, env)
elif fs_type == "spiffs":
return build_spiffs_image(target, source, env)
else:
print(f"Error: Unknown filesystem type '{fs_type}'. Supported types: littlefs, fatfs, spiffs")
return 1
def switch_off_ldf():
"""
Disables LDF (Library Dependency Finder) for uploadfs, uploadfsota, and buildfs targets.
Disables LDF (Library Dependency Finder) for uploadfs, uploadfsota, buildfs,
download_fs, and erase targets.
This optimization prevents unnecessary library dependency scanning and compilation
when only filesystem operations are performed.
"""
fs_targets = {"uploadfs", "uploadfsota", "buildfs", "erase"}
fs_targets = {"uploadfs", "uploadfsota", "buildfs", "erase", "download_fs"}
if fs_targets & set(COMMAND_LINE_TARGETS):
# Disable LDF by modifying project configuration directly
env_section = "env:" + env["PIOENV"]
@@ -494,22 +857,6 @@ env.Replace(
ERASEFLAGS=["--chip", mcu, "--port", '"$UPLOAD_PORT"'],
ERASETOOL=uploader_path,
ERASECMD='$ERASETOOL $ERASEFLAGS erase-flash',
MKFSTOOL="mk%s" % filesystem
+ (
(
"_${PIOPLATFORM}_"
+ (
"espidf"
if "espidf" in env.subst("$PIOFRAMEWORK")
else "${PIOFRAMEWORK}"
)
)
if filesystem == "spiffs"
else ""
),
# Legacy `ESP32_SPIFFS_IMAGE_NAME` is used as the second fallback value
# for backward compatibility
ESP32_FS_IMAGE_NAME=env.get(
"ESP32_FS_IMAGE_NAME",
env.get("ESP32_SPIFFS_IMAGE_NAME", filesystem),
@@ -559,15 +906,7 @@ env.Append(
),
DataToBin=Builder(
action=env.VerboseAction(
" ".join(
['"$MKFSTOOL"', "-c", "$SOURCES", "-s", "$FS_SIZE"]
+ (
["-p", "$FS_PAGE", "-b", "$FS_BLOCK"]
if filesystem in ("littlefs", "spiffs")
else []
)
+ ["$TARGET"]
),
build_fs_router,
"Building FS image from '$SOURCES' directory to $TARGET",
),
emitter=__fetch_fs_size,
@@ -776,6 +1115,486 @@ def coredump_analysis(target, source, env):
print(f"Error: Failed to run coredump analysis: {e}")
print(f'Make sure esp-coredump is installed: uv pip install --python "{PYTHON_EXE}" esp-coredump')
def _get_unpack_dir(env):
"""
Get the unpack directory from project configuration.
Args:
env: SCons environment object
Returns:
str: Unpack directory path
"""
unpack_dir = "unpacked_fs"
# Read from project config (env-specific or common section)
for section in ["env:" + env["PIOENV"], "common"]:
if projectconfig.has_option(section, "board_build.unpack_dir"):
unpack_dir = projectconfig.get(section, "board_build.unpack_dir")
break
return unpack_dir
def _prepare_unpack_dir(unpack_dir):
"""
Prepare the unpack directory by removing old content and creating fresh directory.
Args:
unpack_dir: Directory path to prepare
Returns:
Path: Path object for the unpack directory
"""
unpack_path = Path(get_project_dir()) / unpack_dir
if unpack_path.exists():
shutil.rmtree(unpack_path)
unpack_path.mkdir(parents=True, exist_ok=True)
return unpack_path
def _download_partition_image(env, fs_type_filter=None):
"""
Common function to download partition table and filesystem image from device.
Args:
env: SCons environment object
fs_type_filter: List of partition subtypes to look for (e.g., [0x82, 0x83] for LittleFS/SPIFFS)
or [0x81] for FAT. If None, accepts any data partition.
Returns:
tuple: (fs_file_path, fs_start, fs_size, fs_subtype) or (None, None, None, None) on error
"""
# Ensure upload port is set
if not env.subst("$UPLOAD_PORT"):
env.AutodetectUploadPort()
upload_port = env.subst("$UPLOAD_PORT")
download_speed = board.get("download.speed", "115200")
# Download partition table from device
print(f"\nDownloading partition table from {upload_port}...\n")
build_dir = Path(env.subst("$BUILD_DIR"))
build_dir.mkdir(parents=True, exist_ok=True)
partition_file = build_dir / "partition_table_from_flash.bin"
esptool_cmd = [
uploader_path.strip('"'),
"--port", upload_port,
"--baud", str(download_speed),
"--before", "default-reset",
"--after", "hard-reset",
"read-flash",
"0x8000", # Partition table offset
"0x1000", # Partition table size (4KB)
str(partition_file)
]
try:
result = subprocess.run(esptool_cmd, check=False)
if result.returncode != 0:
print("Error: Failed to download partition table")
return None, None, None, None
except Exception as e:
print(f"Error: {e}")
return None, None, None, None
with open(partition_file, 'rb') as f:
partition_data = f.read()
# Parse partition entries (format: 0xAA 0x50 followed by entry data)
entries = [e for e in partition_data.split(b'\xaaP') if len(e) > 0]
fs_start = None
fs_size = None
fs_subtype = None
for entry in entries:
if len(entry) < 32:
continue
# Byte 0: Type (0x01 for data partitions)
# Byte 1: SubType (0x81=FAT, 0x82=SPIFFS, 0x83=LittleFS)
# Bytes 2-5: Offset (4 bytes, little-endian)
# Bytes 6-9: Size (4 bytes, little-endian)
part_subtype = entry[1]
# Check if this partition matches our filter
if fs_type_filter is None or part_subtype in fs_type_filter:
fs_start = int.from_bytes(entry[2:6], byteorder='little', signed=False)
fs_size = int.from_bytes(entry[6:10], byteorder='little', signed=False)
fs_subtype = part_subtype
break
if fs_start is None or fs_size is None:
print("Error: No matching filesystem partition found in partition table")
return None, None, None, None
print(f"\nFound filesystem partition (subtype {hex(fs_subtype)}):")
print(f" Start: {hex(fs_start)}")
print(f" Size: {hex(fs_size)} ({fs_size} bytes)")
# Download filesystem image
fs_file = build_dir / f"downloaded_fs_{hex(fs_start)}_{hex(fs_size)}.bin"
print("\nDownloading filesystem from device...\n")
esptool_cmd = [
uploader_path.strip('"'),
"--port", upload_port,
"--baud", str(download_speed),
"--before", "default-reset",
"--after", "hard-reset",
"read-flash",
hex(fs_start),
hex(fs_size),
str(fs_file)
]
try:
result = subprocess.run(esptool_cmd, check=False)
if result.returncode != 0:
print(f"Error: Download failed with code {result.returncode}")
return None, None, None, None
except Exception as e:
print(f"Error: {e}")
return None, None, None, None
print(f"\nDownloaded to {fs_file}")
return fs_file, fs_start, fs_size, fs_subtype
def _extract_littlefs(fs_file, fs_size, unpack_path, unpack_dir):
"""Extract LittleFS filesystem."""
# Read the downloaded filesystem image
with open(fs_file, 'rb') as f:
fs_data = f.read()
# Use ESP-IDF defaults
block_size = 0x1000 # 4KB
block_count = fs_size // block_size
# Create LittleFS instance and mount the image
fs = LittleFS(
block_size=block_size,
block_count=block_count,
mount=False
)
fs.context.buffer = bytearray(fs_data)
fs.mount()
# Extract all files
file_count = 0
print("\nExtracted files:")
for root, dirs, files in fs.walk("/"):
if not root.endswith("/"):
root += "/"
# Create directories
for dir_name in dirs:
src_path = root + dir_name
dst_path = unpack_path / src_path[1:] # Remove leading '/'
dst_path.mkdir(parents=True, exist_ok=True)
print(f" [DIR] {src_path}")
# Extract files
for file_name in files:
src_path = root + file_name
dst_path = unpack_path / src_path[1:] # Remove leading '/'
dst_path.parent.mkdir(parents=True, exist_ok=True)
with fs.open(src_path, "rb") as src:
file_data = src.read()
dst_path.write_bytes(file_data)
print(f" [FILE] {src_path} ({len(file_data)} bytes)")
file_count += 1
fs.unmount()
print(f"\nSuccessfully extracted {file_count} file(s) to {unpack_dir}")
return 0
def _parse_spiffs_config(fs_data, fs_size):
"""
Auto-detect SPIFFS configuration from the image.
Tries common configurations and validates against the image.
Returns:
dict: SPIFFS configuration parameters or None
"""
# Common ESP32/ESP8266 SPIFFS configurations
common_configs = [
# ESP32/ESP8266 defaults
{'page_size': 256, 'block_size': 4096, 'obj_name_len': 32},
# Alternative configurations
{'page_size': 256, 'block_size': 8192, 'obj_name_len': 32},
{'page_size': 512, 'block_size': 4096, 'obj_name_len': 32},
{'page_size': 256, 'block_size': 4096, 'obj_name_len': 64},
]
print("\nAuto-detecting SPIFFS configuration...")
for config in common_configs:
try:
# Try to parse with this configuration
spiffs_build_config = SpiffsBuildConfig(
page_size=config['page_size'],
page_ix_len=2,
block_size=config['block_size'],
block_ix_len=2,
meta_len=4,
obj_name_len=config['obj_name_len'],
obj_id_len=2,
span_ix_len=2,
packed=True,
aligned=True,
endianness='little',
use_magic=True,
use_magic_len=True,
aligned_obj_ix_tables=False
)
# Try to create and parse the filesystem
spiffs = SpiffsFS(fs_size, spiffs_build_config)
spiffs.from_binary(fs_data)
# If we got here without exception, this config works
print(" Detected SPIFFS configuration:")
print(f" Page size: {config['page_size']} bytes")
print(f" Block size: {config['block_size']} bytes")
print(f" Max filename length: {config['obj_name_len']}")
return {
'page_size': config['page_size'],
'block_size': config['block_size'],
'obj_name_len': config['obj_name_len'],
'meta_len': 4,
'use_magic': True,
'use_magic_len': True,
'aligned_obj_ix_tables': False
}
except Exception:
continue
# If no config worked, return defaults
print(" Could not auto-detect configuration, using ESP32/ESP8266 defaults")
return {
'page_size': 256,
'block_size': 4096,
'obj_name_len': 32,
'meta_len': 4,
'use_magic': True,
'use_magic_len': True,
'aligned_obj_ix_tables': False
}
def _extract_spiffs(fs_file, fs_size, unpack_path, unpack_dir):
"""Extract SPIFFS filesystem with auto-detected configuration."""
# Read the downloaded filesystem image
with open(fs_file, 'rb') as f:
fs_data = f.read()
# Auto-detect SPIFFS configuration
config = _parse_spiffs_config(fs_data, fs_size)
# Create SPIFFS build configuration
spiffs_build_config = SpiffsBuildConfig(
page_size=config['page_size'],
page_ix_len=2,
block_size=config['block_size'],
block_ix_len=2,
meta_len=config['meta_len'],
obj_name_len=config['obj_name_len'],
obj_id_len=2,
span_ix_len=2,
packed=True,
aligned=True,
endianness='little',
use_magic=config['use_magic'],
use_magic_len=config['use_magic_len'],
aligned_obj_ix_tables=config['aligned_obj_ix_tables']
)
# Create SPIFFS filesystem and parse the image
spiffs = SpiffsFS(fs_size, spiffs_build_config)
spiffs.from_binary(fs_data)
# Extract files
file_count = spiffs.extract_files(str(unpack_path))
if file_count == 0:
print("\nNo files were extracted.")
print("The filesystem may be empty, freshly formatted, or contain only deleted entries.")
else:
print(f"\nSuccessfully extracted {file_count} file(s) to {unpack_dir}")
return 0
def _extract_fatfs(fs_file, unpack_path, unpack_dir):
"""Extract FatFS filesystem."""
# Read the downloaded filesystem image
with open(fs_file, 'rb') as f:
fs_data = bytearray(f.read())
# Check if the image looks like a valid FAT filesystem
if len(fs_data) < 512:
print("Error: Downloaded image is too small to be a valid FAT filesystem")
return 1
# Try to detect and extract wear leveling layer
sector_size = 4096 # Default ESP32 sector size
# Check if this is a wear-leveling wrapped image
if is_esp32_wl_image(fs_data, sector_size):
print("Detected Wear Leveling layer, extracting FAT data...")
fat_data = extract_fat_from_esp32_wl(fs_data, sector_size)
if fat_data is None:
print("Error: Failed to extract FAT data from wear-leveling image")
return 1
fs_data = bytearray(fat_data)
print(f" Extracted FAT data: {len(fs_data)} bytes")
else:
print("No Wear Leveling layer detected, treating as raw FAT image...")
# Read sector size from FAT boot sector (offset 0x0B, 2 bytes, little-endian)
sector_size = int.from_bytes(fs_data[0x0B:0x0D], byteorder='little')
# Validate sector size
if sector_size not in [512, 1024, 2048, 4096]:
print(f"Error: Invalid sector size {sector_size}. Must be 512, 1024, 2048, or 4096")
return 1
# Mount with fatfs-python
fs_size_adjusted = len(fs_data)
sector_count = fs_size_adjusted // sector_size
disk = RamDisk(fs_data, sector_size=sector_size, sector_count=sector_count)
partition = create_extended_partition(disk)
partition.mount()
# Extract all files using PartitionExtended.walk() and read_file()
print("Extracting files:\n")
extracted_count = 0
for root, dirs, files in partition.walk("/"):
# Determine target directory
if root == "/":
abs_root = unpack_path
else:
rel_root = root[1:] if root.startswith("/") else root
abs_root = unpack_path / rel_root
abs_root.mkdir(parents=True, exist_ok=True)
# Extract files in current directory
for filename in files:
# Construct source path
if root == "/":
src_file = "/" + filename
else:
src_file = root.rstrip("/") + "/" + filename
dst_file = abs_root / filename
try:
data = partition.read_file(src_file)
dst_file.write_bytes(data)
print(f" FILE: {src_file} ({len(data)} bytes)")
extracted_count += 1
except Exception as e:
print(f" Warning: Failed to extract {src_file}: {e}")
partition.unmount()
# Summary
if extracted_count == 0:
print("\nNo files were extracted.")
print("The filesystem may be empty, freshly formatted, or contain only deleted entries.")
else:
print(f"\nSuccessfully extracted {extracted_count} file(s) to {unpack_dir}")
return 0
def download_fs_action(target, source, env):
"""Download and extract filesystem from device."""
# Get unpack directory (use global env, not the parameter)
unpack_dir = _get_unpack_dir(env)
# Download partition image
fs_file, _fs_start, fs_size, fs_subtype = _download_partition_image(env, None)
if fs_file is None:
return 1
# Read header for detailed filesystem detection
with open(fs_file, 'rb') as f:
header = f.read(16384) # Read more to check for offset FAT
# Detect filesystem type with improved logic
fs_type = None
# 1. Check for LittleFS magic at offset 8 of the superblock
if len(header) >= 16 and header[8:16] == b'littlefs':
fs_type = "littlefs"
# 2. Check for FAT filesystem (with or without Wear Leveling)
if fs_type is None:
# Check multiple possible offsets for FAT boot sector
# ESP32 with WL often has FAT at offset 0x1000 (4096)
fat_offsets = [0, 4096, 8192]
for offset in fat_offsets:
if len(header) >= offset + 512:
boot_sector = header[offset:offset+512]
# Check for FAT boot signature at offset 510-511
if boot_sector[510:512] == b'\x55\xAA':
# Additional validation: check for FAT filesystem markers
# Check for "FAT" string or "MSDOS" in boot sector
if (b'FAT' in boot_sector[0:90] or
b'MSDOS' in boot_sector[0:90] or
b'MSWIN' in boot_sector[0:90]):
# Verify bytes per sector
bytes_per_sector = int.from_bytes(boot_sector[11:13], byteorder='little')
if bytes_per_sector in [512, 1024, 2048, 4096]:
fs_type = "fatfs"
print(f" FAT boot sector found at offset 0x{offset:x}")
break
# 3. Fall back to partition table subtype if no clear signature found
if fs_type is None:
if fs_subtype == 0x81:
fs_type = "fatfs"
elif fs_subtype == 0x82:
# Subtype 0x82 can be either SPIFFS or LittleFS, default to SPIFFS
fs_type = "spiffs"
elif fs_subtype == 0x83:
fs_type = "littlefs"
else:
print(f"Warning: Unknown partition subtype 0x{fs_subtype:02X}, defaulting to SPIFFS")
fs_type = "spiffs"
print(f"\nDetected filesystem: {fs_type.upper()} (partition subtype: 0x{fs_subtype:02X})")
# Prepare unpack directory
unpack_path = _prepare_unpack_dir(unpack_dir)
# Extract filesystem
try:
if fs_type == "littlefs":
return _extract_littlefs(fs_file, fs_size, unpack_path, unpack_dir)
elif fs_type == "spiffs":
return _extract_spiffs(fs_file, fs_size, unpack_path, unpack_dir)
elif fs_type == "fatfs":
return _extract_fatfs(fs_file, unpack_path, unpack_dir)
except Exception as e:
print(f"Error: {e}")
return 1
#
# Target: Build executable and linkable firmware or FS image
#
@@ -1013,6 +1832,17 @@ env.AddPlatformTarget(
"Upload Filesystem Image OTA",
)
# Target: Download Filesystem (auto-detect type)
env.AddPlatformTarget(
"download_fs",
None,
[
env.VerboseAction(BeforeUpload, "Looking for upload port..."),
env.VerboseAction(download_fs_action, "Downloading and extracting filesystem")
],
"Download and extract filesystem from device",
)
# Target: Erase Flash and Upload
env.AddPlatformTarget(
"erase_upload",
+2
View File
@@ -45,6 +45,8 @@ PLATFORMIO_URL_VERSION_RE = re.compile(
# Python dependencies required for ESP32 platform builds
python_deps = {
"platformio": "https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip",
"littlefs-python": ">=0.16.0",
"fatfs-ng": ">=0.1.14",
"pyyaml": ">=6.0.2",
"rich-click": ">=1.8.6",
"zopfli": ">=0.2.2",
+762
View File
@@ -0,0 +1,762 @@
#!/usr/bin/env python
#
# spiffsgen is a tool used to generate a spiffs image from a directory
#
# SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import io
import math
import os
import struct
try:
import typing
TSP = typing.TypeVar('TSP', bound='SpiffsObjPageWithIdx')
ObjIdsItem = typing.Tuple[int, typing.Type[TSP]]
except ImportError:
pass
SPIFFS_PH_FLAG_USED_FINAL_INDEX = 0xF8
SPIFFS_PH_FLAG_USED_FINAL = 0xFC
SPIFFS_PH_FLAG_LEN = 1
SPIFFS_PH_IX_SIZE_LEN = 4
SPIFFS_PH_IX_OBJ_TYPE_LEN = 1
SPIFFS_TYPE_FILE = 1
# Based on typedefs under spiffs_config.h
SPIFFS_OBJ_ID_LEN = 2 # spiffs_obj_id
SPIFFS_SPAN_IX_LEN = 2 # spiffs_span_ix
SPIFFS_PAGE_IX_LEN = 2 # spiffs_page_ix
SPIFFS_BLOCK_IX_LEN = 2 # spiffs_block_ix
class SpiffsBuildConfig(object):
def __init__(self,
page_size, # type: int
page_ix_len, # type: int
block_size, # type: int
block_ix_len, # type: int
meta_len, # type: int
obj_name_len, # type: int
obj_id_len, # type: int
span_ix_len, # type: int
packed, # type: bool
aligned, # type: bool
endianness, # type: str
use_magic, # type: bool
use_magic_len, # type: bool
aligned_obj_ix_tables # type: bool
):
if block_size % page_size != 0:
raise RuntimeError('block size should be a multiple of page size')
self.page_size = page_size
self.block_size = block_size
self.obj_id_len = obj_id_len
self.span_ix_len = span_ix_len
self.packed = packed
self.aligned = aligned
self.obj_name_len = obj_name_len
self.meta_len = meta_len
self.page_ix_len = page_ix_len
self.block_ix_len = block_ix_len
self.endianness = endianness
self.use_magic = use_magic
self.use_magic_len = use_magic_len
self.aligned_obj_ix_tables = aligned_obj_ix_tables
self.PAGES_PER_BLOCK = self.block_size // self.page_size
self.OBJ_LU_PAGES_PER_BLOCK = int(math.ceil(self.block_size / self.page_size * self.obj_id_len / self.page_size))
self.OBJ_USABLE_PAGES_PER_BLOCK = self.PAGES_PER_BLOCK - self.OBJ_LU_PAGES_PER_BLOCK
self.OBJ_LU_PAGES_OBJ_IDS_LIM = self.page_size // self.obj_id_len
self.OBJ_DATA_PAGE_HEADER_LEN = self.obj_id_len + self.span_ix_len + SPIFFS_PH_FLAG_LEN
pad = 4 - (4 if self.OBJ_DATA_PAGE_HEADER_LEN % 4 == 0 else self.OBJ_DATA_PAGE_HEADER_LEN % 4)
self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED = self.OBJ_DATA_PAGE_HEADER_LEN + pad
self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD = pad
self.OBJ_DATA_PAGE_CONTENT_LEN = self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN
self.OBJ_INDEX_PAGES_HEADER_LEN = (self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED + SPIFFS_PH_IX_SIZE_LEN +
SPIFFS_PH_IX_OBJ_TYPE_LEN + self.obj_name_len + self.meta_len)
if aligned_obj_ix_tables:
self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED = (self.OBJ_INDEX_PAGES_HEADER_LEN + SPIFFS_PAGE_IX_LEN - 1) & ~(SPIFFS_PAGE_IX_LEN - 1)
self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD = self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED - self.OBJ_INDEX_PAGES_HEADER_LEN
else:
self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED = self.OBJ_INDEX_PAGES_HEADER_LEN
self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD = 0
self.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM = (self.page_size - self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED) // self.block_ix_len
self.OBJ_INDEX_PAGES_OBJ_IDS_LIM = (self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED) // self.block_ix_len
class SpiffsFullError(RuntimeError):
pass
class SpiffsPage(object):
_endianness_dict = {
'little': '<',
'big': '>'
}
_len_dict = {
1: 'B',
2: 'H',
4: 'I',
8: 'Q'
}
def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None
self.build_config = build_config
self.bix = bix
def to_binary(self): # type: () -> bytes
raise NotImplementedError()
class SpiffsObjPageWithIdx(SpiffsPage):
def __init__(self, obj_id, build_config): # type: (int, SpiffsBuildConfig) -> None
super(SpiffsObjPageWithIdx, self).__init__(0, build_config)
self.obj_id = obj_id
def to_binary(self): # type: () -> bytes
raise NotImplementedError()
class SpiffsObjLuPage(SpiffsPage):
def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None
SpiffsPage.__init__(self, bix, build_config)
self.obj_ids_limit = self.build_config.OBJ_LU_PAGES_OBJ_IDS_LIM
self.obj_ids = list() # type: typing.List[ObjIdsItem]
def _calc_magic(self, blocks_lim): # type: (int) -> int
# Calculate the magic value mirroring computation done by the macro SPIFFS_MAGIC defined in
# spiffs_nucleus.h
magic = 0x20140529 ^ self.build_config.page_size
if self.build_config.use_magic_len:
magic = magic ^ (blocks_lim - self.bix)
# narrow the result to build_config.obj_id_len bytes
mask = (2 << (8 * self.build_config.obj_id_len)) - 1
return magic & mask
def register_page(self, page): # type: (TSP) -> None
if not self.obj_ids_limit > 0:
raise SpiffsFullError()
obj_id = (page.obj_id, page.__class__)
self.obj_ids.append(obj_id)
self.obj_ids_limit -= 1
def to_binary(self): # type: () -> bytes
img = b''
for (obj_id, page_type) in self.obj_ids:
if page_type == SpiffsObjIndexPage:
obj_id ^= (1 << ((self.build_config.obj_id_len * 8) - 1))
img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len], obj_id)
assert len(img) <= self.build_config.page_size
img += b'\xFF' * (self.build_config.page_size - len(img))
return img
def magicfy(self, blocks_lim): # type: (int) -> None
# Only use magic value if no valid obj id has been written to the spot, which is the
# spot taken up by the last obj id on last lookup page. The parent is responsible
# for determining which is the last lookup page and calling this function.
remaining = self.obj_ids_limit
empty_obj_id_dict = {
1: 0xFF,
2: 0xFFFF,
4: 0xFFFFFFFF,
8: 0xFFFFFFFFFFFFFFFF
}
if remaining >= 2:
for i in range(remaining):
if i == remaining - 2:
self.obj_ids.append((self._calc_magic(blocks_lim), SpiffsObjDataPage))
break
else:
self.obj_ids.append((empty_obj_id_dict[self.build_config.obj_id_len], SpiffsObjDataPage))
self.obj_ids_limit -= 1
class SpiffsObjIndexPage(SpiffsObjPageWithIdx):
def __init__(self, obj_id, span_ix, size, name, build_config
): # type: (int, int, int, str, SpiffsBuildConfig) -> None
super(SpiffsObjIndexPage, self).__init__(obj_id, build_config)
self.span_ix = span_ix
self.name = name
self.size = size
if self.span_ix == 0:
self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM
else:
self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_LIM
self.pages = list() # type: typing.List[int]
def register_page(self, page): # type: (SpiffsObjDataPage) -> None
if not self.pages_lim > 0:
raise SpiffsFullError()
self.pages.append(page.offset)
self.pages_lim -= 1
def to_binary(self): # type: () -> bytes
obj_id = self.obj_id ^ (1 << ((self.build_config.obj_id_len * 8) - 1))
img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len] +
SpiffsPage._len_dict[self.build_config.span_ix_len] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN],
obj_id,
self.span_ix,
SPIFFS_PH_FLAG_USED_FINAL_INDEX)
# Add padding before the object index page specific information
img += b'\xFF' * self.build_config.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD
# If this is the first object index page for the object, add filename, type
# and size information
if self.span_ix == 0:
img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[SPIFFS_PH_IX_SIZE_LEN] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN],
self.size,
SPIFFS_TYPE_FILE)
img += self.name.encode() + (b'\x00' * (
(self.build_config.obj_name_len - len(self.name))
+ self.build_config.meta_len
+ self.build_config.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD))
# Finally, add the page index of data pages
for page in self.pages:
page = page >> int(math.log(self.build_config.page_size, 2))
img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.page_ix_len], page)
assert len(img) <= self.build_config.page_size
img += b'\xFF' * (self.build_config.page_size - len(img))
return img
class SpiffsObjDataPage(SpiffsObjPageWithIdx):
def __init__(self, offset, obj_id, span_ix, contents, build_config
): # type: (int, int, int, bytes, SpiffsBuildConfig) -> None
super(SpiffsObjDataPage, self).__init__(obj_id, build_config)
self.span_ix = span_ix
self.contents = contents
self.offset = offset
def to_binary(self): # type: () -> bytes
img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len] +
SpiffsPage._len_dict[self.build_config.span_ix_len] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN],
self.obj_id,
self.span_ix,
SPIFFS_PH_FLAG_USED_FINAL)
img += self.contents
assert len(img) <= self.build_config.page_size
img += b'\xFF' * (self.build_config.page_size - len(img))
return img
class SpiffsBlock(object):
def _reset(self): # type: () -> None
self.cur_obj_index_span_ix = 0
self.cur_obj_data_span_ix = 0
self.cur_obj_id = 0
self.cur_obj_idx_page = None # type: typing.Optional[SpiffsObjIndexPage]
def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None
self.build_config = build_config
self.offset = bix * self.build_config.block_size
self.remaining_pages = self.build_config.OBJ_USABLE_PAGES_PER_BLOCK
self.pages = list() # type: typing.List[SpiffsPage]
self.bix = bix
lu_pages = list()
for i in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK):
page = SpiffsObjLuPage(self.bix, self.build_config)
lu_pages.append(page)
self.pages.extend(lu_pages)
self.lu_page_iter = iter(lu_pages)
self.lu_page = next(self.lu_page_iter)
self._reset()
def _register_page(self, page): # type: (TSP) -> None
if isinstance(page, SpiffsObjDataPage):
assert self.cur_obj_idx_page is not None
self.cur_obj_idx_page.register_page(page) # can raise SpiffsFullError
try:
self.lu_page.register_page(page)
except SpiffsFullError:
self.lu_page = next(self.lu_page_iter)
try:
self.lu_page.register_page(page)
except AttributeError: # no next lookup page
# Since the amount of lookup pages is pre-computed at every block instance,
# this should never occur
raise RuntimeError('invalid attempt to add page to a block when there is no more space in lookup')
self.pages.append(page)
def begin_obj(self, obj_id, size, name, obj_index_span_ix=0, obj_data_span_ix=0
): # type: (int, int, str, int, int) -> None
if not self.remaining_pages > 0:
raise SpiffsFullError()
self._reset()
self.cur_obj_id = obj_id
self.cur_obj_index_span_ix = obj_index_span_ix
self.cur_obj_data_span_ix = obj_data_span_ix
page = SpiffsObjIndexPage(obj_id, self.cur_obj_index_span_ix, size, name, self.build_config)
self._register_page(page)
self.cur_obj_idx_page = page
self.remaining_pages -= 1
self.cur_obj_index_span_ix += 1
def update_obj(self, contents): # type: (bytes) -> None
if not self.remaining_pages > 0:
raise SpiffsFullError()
page = SpiffsObjDataPage(self.offset + (len(self.pages) * self.build_config.page_size),
self.cur_obj_id, self.cur_obj_data_span_ix, contents, self.build_config)
self._register_page(page)
self.cur_obj_data_span_ix += 1
self.remaining_pages -= 1
def end_obj(self): # type: () -> None
self._reset()
def is_full(self): # type: () -> bool
return self.remaining_pages <= 0
def to_binary(self, blocks_lim): # type: (int) -> bytes
img = b''
if self.build_config.use_magic:
for (idx, page) in enumerate(self.pages):
if idx == self.build_config.OBJ_LU_PAGES_PER_BLOCK - 1:
assert isinstance(page, SpiffsObjLuPage)
page.magicfy(blocks_lim)
img += page.to_binary()
else:
for page in self.pages:
img += page.to_binary()
assert len(img) <= self.build_config.block_size
img += b'\xFF' * (self.build_config.block_size - len(img))
return img
def _parse_from_binary(self, block_data): # type: (bytes) -> None
"""Parse block data from binary image.
Args:
block_data: Raw block bytes
"""
self._raw_data = block_data
class SpiffsFS(object):
def __init__(self, img_size, build_config): # type: (int, SpiffsBuildConfig) -> None
if img_size % build_config.block_size != 0:
raise RuntimeError('image size should be a multiple of block size')
self.img_size = img_size
self.build_config = build_config
self.blocks = list() # type: typing.List[SpiffsBlock]
self.blocks_lim = self.img_size // self.build_config.block_size
self.remaining_blocks = self.blocks_lim
self.cur_obj_id = 1 # starting object id
def _create_block(self): # type: () -> SpiffsBlock
if self.is_full():
raise SpiffsFullError('the image size has been exceeded')
block = SpiffsBlock(len(self.blocks), self.build_config)
self.blocks.append(block)
self.remaining_blocks -= 1
return block
def is_full(self): # type: () -> bool
return self.remaining_blocks <= 0
def create_file(self, img_path, file_path): # type: (str, str) -> None
if len(img_path) > self.build_config.obj_name_len:
raise RuntimeError("object name '%s' too long" % img_path)
name = img_path
with open(file_path, 'rb') as obj:
contents = obj.read()
stream = io.BytesIO(contents)
try:
block = self.blocks[-1]
block.begin_obj(self.cur_obj_id, len(contents), name)
except (IndexError, SpiffsFullError):
block = self._create_block()
block.begin_obj(self.cur_obj_id, len(contents), name)
contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN)
while contents_chunk:
try:
block = self.blocks[-1]
try:
# This can fail because either (1) all the pages in block have been
# used or (2) object index has been exhausted.
block.update_obj(contents_chunk)
except SpiffsFullError:
# If its (1), use the outer exception handler
if block.is_full():
raise SpiffsFullError
# If its (2), write another object index page
block.begin_obj(self.cur_obj_id, len(contents), name,
obj_index_span_ix=block.cur_obj_index_span_ix,
obj_data_span_ix=block.cur_obj_data_span_ix)
continue
except (IndexError, SpiffsFullError):
# All pages in the block have been exhausted. Create a new block, copying
# the previous state of the block to a new one for the continuation of the
# current object
prev_block = block
block = self._create_block()
block.cur_obj_id = prev_block.cur_obj_id
block.cur_obj_idx_page = prev_block.cur_obj_idx_page
block.cur_obj_data_span_ix = prev_block.cur_obj_data_span_ix
block.cur_obj_index_span_ix = prev_block.cur_obj_index_span_ix
continue
contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN)
block.end_obj()
self.cur_obj_id += 1
def to_binary(self): # type: () -> bytes
img = b''
all_blocks = []
for block in self.blocks:
all_blocks.append(block.to_binary(self.blocks_lim))
bix = len(self.blocks)
if self.build_config.use_magic:
# Create empty blocks with magic numbers
while self.remaining_blocks > 0:
block = SpiffsBlock(bix, self.build_config)
all_blocks.append(block.to_binary(self.blocks_lim))
self.remaining_blocks -= 1
bix += 1
else:
# Just fill remaining spaces FF's
all_blocks.append(b'\xFF' * (self.img_size - len(all_blocks) * self.build_config.block_size))
img += b''.join([blk for blk in all_blocks])
return img
def from_binary(self, image_data): # type: (bytes) -> None
"""Parse a SPIFFS binary image and populate the filesystem structure.
Args:
image_data: Raw SPIFFS image bytes
"""
if len(image_data) != self.img_size:
raise RuntimeError(f'image size mismatch: expected {self.img_size}, got {len(image_data)}')
# Parse blocks from the image
blocks_count = self.img_size // self.build_config.block_size
for bix in range(blocks_count):
block_offset = bix * self.build_config.block_size
block_data = image_data[block_offset:block_offset + self.build_config.block_size]
block = SpiffsBlock(bix, self.build_config)
block._parse_from_binary(block_data)
self.blocks.append(block)
def extract_files(self, output_dir): # type: (str) -> int
"""Extract all files from the SPIFFS filesystem to a directory.
Args:
output_dir: Directory path where files will be extracted
Returns:
int: Number of files extracted
"""
# Build a map of object_id -> file info
files_map = {} # obj_id -> {'name': str, 'size': int, 'data_pages': [(span_ix, page_data)]}
for block in self.blocks:
# Parse lookup pages to find valid objects
for page_idx in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK):
lu_page_offset = page_idx * self.build_config.page_size
lu_page_data = block._raw_data[lu_page_offset:lu_page_offset + self.build_config.page_size]
# Parse object IDs from lookup page
for i in range(0, len(lu_page_data), self.build_config.obj_id_len):
if i + self.build_config.obj_id_len > len(lu_page_data):
break
obj_id_bytes = lu_page_data[i:i + self.build_config.obj_id_len]
obj_id = struct.unpack(
SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len],
obj_id_bytes
)[0]
# Check if it's a valid object (not erased/empty)
empty_values = {1: 0xFF, 2: 0xFFFF, 4: 0xFFFFFFFF, 8: 0xFFFFFFFFFFFFFFFF}
if obj_id == empty_values[self.build_config.obj_id_len]:
continue
# Check if it's an index page (MSB set)
is_index = obj_id & (1 << ((self.build_config.obj_id_len * 8) - 1))
real_obj_id = obj_id & ~(1 << ((self.build_config.obj_id_len * 8) - 1))
if is_index and real_obj_id not in files_map:
files_map[real_obj_id] = {'name': None, 'size': 0, 'data_pages': []}
# Parse actual pages to get file metadata and content
for page_idx in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK, self.build_config.PAGES_PER_BLOCK):
page_offset = page_idx * self.build_config.page_size
page_data = block._raw_data[page_offset:page_offset + self.build_config.page_size]
# Parse page header
header_fmt = (
SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[self.build_config.obj_id_len] +
SpiffsPage._len_dict[self.build_config.span_ix_len] +
SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN]
)
header_size = struct.calcsize(header_fmt)
if len(page_data) < header_size:
continue
obj_id, span_ix, flags = struct.unpack(header_fmt, page_data[:header_size])
# Check for valid page
empty_id = {1: 0xFF, 2: 0xFFFF, 4: 0xFFFFFFFF, 8: 0xFFFFFFFFFFFFFFFF}[self.build_config.obj_id_len]
if obj_id == empty_id:
continue
is_index = obj_id & (1 << ((self.build_config.obj_id_len * 8) - 1))
real_obj_id = obj_id & ~(1 << ((self.build_config.obj_id_len * 8) - 1))
if is_index and flags == SPIFFS_PH_FLAG_USED_FINAL_INDEX:
# Index page - contains file metadata
if real_obj_id not in files_map:
files_map[real_obj_id] = {'name': None, 'size': 0, 'data_pages': []}
# Only first index page (span_ix == 0) has filename and size
if span_ix == 0:
# Skip to size and type fields
offset = header_size + self.build_config.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD
size_type_fmt = (
SpiffsPage._endianness_dict[self.build_config.endianness] +
SpiffsPage._len_dict[SPIFFS_PH_IX_SIZE_LEN] +
SpiffsPage._len_dict[SPIFFS_PH_IX_OBJ_TYPE_LEN]
)
size_type_size = struct.calcsize(size_type_fmt)
if offset + size_type_size <= len(page_data):
file_size, obj_type = struct.unpack(size_type_fmt, page_data[offset:offset + size_type_size])
offset += size_type_size
# Read filename
name_end = offset + self.build_config.obj_name_len
if name_end <= len(page_data):
name_bytes = page_data[offset:name_end]
# Find null terminator
null_pos = name_bytes.find(b'\x00')
if null_pos != -1:
name_bytes = name_bytes[:null_pos]
filename = name_bytes.decode('utf-8', errors='ignore')
files_map[real_obj_id]['name'] = filename
files_map[real_obj_id]['size'] = file_size
elif not is_index and flags == SPIFFS_PH_FLAG_USED_FINAL:
# Data page - contains file content
if real_obj_id in files_map:
# Extract content (skip header, no padding on data pages)
content_start = header_size
content = page_data[content_start:content_start + self.build_config.OBJ_DATA_PAGE_CONTENT_LEN]
files_map[real_obj_id]['data_pages'].append((span_ix, content))
# Extract files to output directory
file_count = 0
for obj_id, file_info in files_map.items():
if file_info['name'] is None:
continue
# Remove leading slash if present
rel_path = file_info['name'].lstrip('/')
file_path = os.path.join(output_dir, rel_path)
if not rel_path:
print(f" Warning: Skipping file with empty path (obj_id={obj_id})")
continue
# Create parent directories
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# Sort data pages by span index
file_info['data_pages'].sort(key=lambda x: x[0])
# Write file content
with open(file_path, 'wb') as f:
total_written = 0
for span_ix, content in file_info['data_pages']:
# Write only up to the file size
remaining = file_info['size'] - total_written
if remaining <= 0:
break
to_write = min(len(content), remaining)
f.write(content[:to_write])
total_written += to_write
file_count += 1
print(f" Extracted: {file_info['name']} ({file_info['size']} bytes)")
return file_count
class CustomHelpFormatter(argparse.HelpFormatter):
"""
Similar to argparse.ArgumentDefaultsHelpFormatter, except it
doesn't add the default value if "(default:" is already present.
This helps in the case of options with action="store_false", like
--no-magic or --no-magic-len.
"""
def _get_help_string(self, action): # type: (argparse.Action) -> str
if action.help is None:
return ''
if '%(default)' not in action.help and '(default:' not in action.help:
if action.default is not argparse.SUPPRESS:
defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
return action.help + ' (default: %(default)s)'
return action.help
def main(): # type: () -> None
parser = argparse.ArgumentParser(description='SPIFFS Image Generator',
formatter_class=CustomHelpFormatter)
parser.add_argument('image_size',
help='Size of the created image')
parser.add_argument('base_dir',
help='Path to directory from which the image will be created')
parser.add_argument('output_file',
help='Created image output file path')
parser.add_argument('--page-size',
help='Logical page size. Set to value same as CONFIG_SPIFFS_PAGE_SIZE.',
type=int,
default=256)
parser.add_argument('--block-size',
help="Logical block size. Set to the same value as the flash chip's sector size (g_rom_flashchip.sector_size).",
type=int,
default=4096)
parser.add_argument('--obj-name-len',
help='File full path maximum length. Set to value same as CONFIG_SPIFFS_OBJ_NAME_LEN.',
type=int,
default=32)
parser.add_argument('--meta-len',
help='File metadata length. Set to value same as CONFIG_SPIFFS_META_LENGTH.',
type=int,
default=4)
parser.add_argument('--use-magic',
dest='use_magic',
help='Use magic number to create an identifiable SPIFFS image. Specify if CONFIG_SPIFFS_USE_MAGIC.',
action='store_true')
parser.add_argument('--no-magic',
dest='use_magic',
help='Inverse of --use-magic (default: --use-magic is enabled)',
action='store_false')
parser.add_argument('--use-magic-len',
dest='use_magic_len',
help='Use position in memory to create different magic numbers for each block. Specify if CONFIG_SPIFFS_USE_MAGIC_LENGTH.',
action='store_true')
parser.add_argument('--no-magic-len',
dest='use_magic_len',
help='Inverse of --use-magic-len (default: --use-magic-len is enabled)',
action='store_false')
parser.add_argument('--follow-symlinks',
help='Take into account symbolic links during partition image creation.',
action='store_true')
parser.add_argument('--big-endian',
help='Specify if the target architecture is big-endian. If not specified, little-endian is assumed.',
action='store_true')
parser.add_argument('--aligned-obj-ix-tables',
action='store_true',
help='Use aligned object index tables. Specify if SPIFFS_ALIGNED_OBJECT_INDEX_TABLES is set.')
parser.set_defaults(use_magic=True, use_magic_len=True)
args = parser.parse_args()
if not os.path.exists(args.base_dir):
raise RuntimeError('given base directory %s does not exist' % args.base_dir)
with open(args.output_file, 'wb') as image_file:
image_size = int(args.image_size, 0)
spiffs_build_default = SpiffsBuildConfig(args.page_size, SPIFFS_PAGE_IX_LEN,
args.block_size, SPIFFS_BLOCK_IX_LEN, args.meta_len,
args.obj_name_len, SPIFFS_OBJ_ID_LEN, SPIFFS_SPAN_IX_LEN,
True, True, 'big' if args.big_endian else 'little',
args.use_magic, args.use_magic_len, args.aligned_obj_ix_tables)
spiffs = SpiffsFS(image_size, spiffs_build_default)
for root, dirs, files in os.walk(args.base_dir, followlinks=args.follow_symlinks):
for f in files:
full_path = os.path.join(root, f)
spiffs.create_file('/' + os.path.relpath(full_path, args.base_dir).replace('\\', '/'), full_path)
image = spiffs.to_binary()
image_file.write(image)
if __name__ == '__main__':
main()
+5
View File
@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+175
View File
@@ -0,0 +1,175 @@
# FatFS Integration for Platform-Espressif32
This platform now supports FatFS as a filesystem option, analogous to the existing LittleFS integration.
## Features
- **Build FatFS Image**: Creates a FatFS filesystem image from a directory
- **Upload FatFS Image**: Uploads the FatFS image to the ESP32 device
- **Download FatFS Image**: Downloads the FatFS image from the device and extracts it
## Configuration
### platformio.ini
```ini
[env:myenv]
platform = espressif32
board = esp32dev
framework = arduino
; Select FatFS as filesystem
board_build.filesystem = fatfs
; Optional: Directory for extracted files (default: unpacked_fs)
board_build.unpack_dir = unpacked_fs
```
### Partition Table
The partition table must contain a FAT partition (Subtype 0x81):
```csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
ffat, data, fat, 0x290000,0x170000,
```
## Usage
### Build FatFS Image
```bash
# Place files in data/ directory
mkdir -p data
echo "Hello FatFS" > data/test.txt
# Build image
pio run -t buildfs
```
### Upload FatFS Image
```bash
pio run -t uploadfs
```
### Download FatFS Image from Device
```bash
pio run -t download_fatfs
```
Files will be extracted to the configured directory (default: `unpacked_fs`).
## Technical Details
### Python Dependencies
The integration uses the `fatfs-ng` package, which is automatically installed.
### Build Process
1. A RAM disk is created with the configured FAT data size (partition size minus WL overhead)
2. The FatFS is formatted with proper parameters (2 FATs, LFN support)
3. All files from the `data/` directory are copied
4. The FAT image is wrapped with ESP32 Wear Leveling layer
5. The final image is saved as a `.bin` file
**Important**: The build process automatically adds the ESP32 Wear Leveling layer, which is required by the Arduino FFat library. See [WEAR_LEVELING.md](WEAR_LEVELING.md) for details.
### Wear Leveling Layer
ESP32's FFat library requires a wear leveling layer around the FAT filesystem. The build process automatically:
- Reserves sectors for wear leveling metadata
- Wraps the FAT filesystem with WL_State structures
- Calculates proper CRC32 checksums
### Download Process
1. The partition table is downloaded from the device
2. The FAT partition is identified (Subtype 0x81)
3. The filesystem image is downloaded
4. The wear leveling layer is automatically detected and removed
5. The FAT data is mounted and extracted
## Extended Features
The `pyfatfs` package includes extended features for complete directory traversal:
- **Complete Directory Traversal**: `walk()`, `listdir()`, `stat()`
- **Path Operations**: `exists()`, `isfile()`, `isdir()`
- **File Operations**: `remove()`, `rmdir()`, `rename()`, `makedirs()`
- **Convenience Methods**: `read_file()`, `write_file()`
- **Bulk Operations**: `copy_tree_from()`, `copy_tree_to()`
These features enable full filesystem extraction and manipulation.
## Comparison: LittleFS vs FatFS
| Feature | LittleFS | FatFS |
|---------|----------|-------|
| Wear Leveling | Yes | Yes |
| Power-Loss Protection | Yes | Limited |
| Compatibility | ESP-IDF specific | Standard FAT |
| Sector Size | 4096 | 4096 |
| Filesystem Size | Flexible | Larger |
## Example Code (Arduino)
```cpp
#include <FFat.h>
void setup() {
Serial.begin(115200);
// Mount FatFS
if (!FFat.begin(true)) {
Serial.println("FFat Mount Failed");
return;
}
// Read file
File file = FFat.open("/test.txt", "r");
if (file) {
Serial.println(file.readString());
file.close();
}
// Write file
file = FFat.open("/output.txt", "w");
if (file) {
file.println("Hello from ESP32!");
file.close();
}
}
void loop() {
// ...
}
```
## Troubleshooting
### "No FAT filesystem partition found"
- Check the partition table
- Ensure a partition with subtype `fat` (0x81) exists
### Build Errors
```bash
# Recreate Python environment
rm -rf ~/.platformio/penv
pio run
```
## Further Information
- [FatFS Documentation](http://elm-chan.org/fsw/ff/00index_e.html)
- [ESP-IDF FFat Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/fatfs.html)
- [fatfs-ng Repository](https://github.com/Jason2866/pyfatfs)
- [Original fatfs-python](https://github.com/krakonos/fatfs-python)
+224
View File
@@ -0,0 +1,224 @@
# ESP32 FAT Filesystem Test Project
This project tests the FAT filesystem implementation with ESP32 Wear Leveling support.
## Overview
This project demonstrates:
- Building FAT filesystem images with wear leveling layer
- Uploading FAT images to ESP32
- Mounting and reading FAT filesystem on ESP32
- Downloading and extracting FAT images from ESP32
## Requirements
- PlatformIO
- ESP32 development board
- USB cable
## Project Structure
```
arduino-fatfs/
├── data/ # Files to be included in FAT image
│ ├── test.txt
│ ├── README.md
│ └── ...
├── src/
│ └── ffat.ino # Main Arduino sketch
├── partitions.csv # Partition table with FAT partition
├── platformio.ini # PlatformIO configuration
└── unpacked_fs/ # Downloaded files (created by download_fatfs)
```
## Usage
### 1. Build Firmware
```bash
pio run
```
### 2. Build FAT Filesystem Image
Place your files in the `data/` directory, then:
```bash
pio run -t buildfs
```
This creates a FAT filesystem image with ESP32 wear leveling layer at:
`.pio/build/esp32dev/fatfs.bin`
### 3. Upload Firmware and Filesystem
```bash
# Upload firmware
pio run -t upload
# Upload filesystem
pio run -t uploadfs
```
### 4. Monitor Serial Output
```bash
pio run -t monitor
```
Expected output:
```
FFat mounted successfully
Test begin
Total space: 1486848
Free space: 1482752
Listing directory: /
FILE: test.txt SIZE: 12
FILE: README.md SIZE: 1234
Test complete
```
### 5. Download Filesystem from Device
To download and extract the filesystem from the device:
```bash
pio run -t download_fatfs
```
Files will be extracted to `unpacked_fs/` directory.
## Partition Table
The `partitions.csv` defines the flash layout:
```csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
ffat, data, fat, 0x290000,0x170000,
```
The `ffat` partition:
- **Type**: data
- **SubType**: fat (0x81)
- **Offset**: 0x290000 (2,686,976 bytes)
- **Size**: 0x170000 (1,507,328 bytes = ~1.44 MB)
## Wear Leveling
The FAT filesystem is automatically wrapped with ESP32's wear leveling layer:
- **Total partition**: 1,507,328 bytes (368 sectors × 4096 bytes)
- **WL overhead**: 20,480 bytes (5 sectors)
- **FAT data**: 1,486,848 bytes (363 sectors)
Structure:
```
[WL State 1][WL State 2][FAT Data][Temp][WL State 3][WL State 4]
```
See [WEAR_LEVELING.md](../../WEAR_LEVELING.md) for details.
## Troubleshooting
### "FFat Mount Failed"
**Possible causes:**
1. Filesystem not uploaded
2. Wrong partition table
3. Corrupted filesystem
**Solutions:**
```bash
# Rebuild and upload filesystem
pio run -t buildfs
pio run -t uploadfs
# Or erase flash and start fresh
pio run -t erase
pio run -t upload
pio run -t uploadfs
```
### "No FAT filesystem partition found"
**Cause:** Partition table doesn't have a FAT partition
**Solution:** Check `partitions.csv` has a partition with `SubType: fat`
### Files not appearing
**Cause:** Files not in `data/` directory when building
**Solution:**
1. Add files to `data/` directory
2. Rebuild filesystem: `pio run -t buildfs`
3. Upload: `pio run -t uploadfs`
## Code Example
```cpp
#include "FFat.h"
void setup() {
Serial.begin(115200);
// Mount FAT filesystem
if (!FFat.begin(false)) {
Serial.println("FFat Mount Failed");
return;
}
// List files
File root = FFat.open("/");
File file = root.openNextFile();
while (file) {
Serial.printf("File: %s, Size: %d\n",
file.name(), file.size());
file = root.openNextFile();
}
// Read file
File f = FFat.open("/test.txt", "r");
if (f) {
String content = f.readString();
Serial.println(content);
f.close();
}
// Write file
f = FFat.open("/output.txt", "w");
if (f) {
f.println("Hello from ESP32!");
f.close();
}
}
void loop() {
// Your code here
}
```
## Platform Configuration
This project uses a custom platform with FAT filesystem support:
```ini
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = fatfs
board_build.partitions = partitions.csv
```
## References
- [ESP32 FFat Library](https://github.com/espressif/arduino-esp32/tree/master/libraries/FFat)
- [ESP-IDF FAT Filesystem](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/fatfs.html)
- [ESP-IDF Wear Levelling](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/wear-levelling.html)
- [Platform-Espressif32 FAT Integration](../../FATFS_INTEGRATION.md)
- [Wear Leveling Implementation](../../platform-espressif32/WEAR_LEVELING.md)
+380
View File
@@ -0,0 +1,380 @@
# FFat Filesystem Test Guide
This guide explains how to test the ESP32 FFat filesystem with pre-flashed images using Wear Leveling.
## Quick Start
### 1. Prepare Test Files
Add files to the `data/` directory that you want to include in the filesystem:
```bash
# Example files already included:
data/
├── test.txt
├── README.md
├── platformio.ini
└── partitions.csv
```
### 2. Build Filesystem Image
Build the FAT filesystem image with Wear Leveling layer:
```bash
pio run -t buildfs
```
This creates `.pio/build/esp32dev/fatfs.bin` with:
- Your files from `data/` directory
- ESP32 Wear Leveling layer
- Proper FAT filesystem structure
### 3. Upload Firmware and Filesystem
```bash
# Upload firmware
pio run -t upload
# Upload filesystem
pio run -t uploadfs
```
### 4. Monitor Serial Output
```bash
pio run -t monitor
```
## Test Configuration
Edit `src/ffat.ino` to configure tests:
```cpp
// Set to true to format the partition (erases all data!)
#define FORMAT_FFAT false
// Test settings
#define TEST_READ_EXISTING true // Test reading pre-flashed files
#define TEST_WRITE_NEW true // Test writing new files
#define TEST_FILE_IO true // Test I/O performance
```
## Expected Output
### Successful Mount
```
============================================================
ESP32 FFat Filesystem Test
Testing pre-flashed image with Wear Leveling
============================================================
Mounting FFat filesystem...
✓ FFat mounted successfully!
=== Filesystem Information ===
Total space: 1486848 bytes (1.42 MB)
Used space: 12288 bytes (0.01 MB)
Free space: 1474560 bytes (1.41 MB)
Usage: 0.8%
```
### Reading Pre-Flashed Files
```
============================================================
=== Testing Pre-Flashed Files ===
Files in root directory:
Listing directory: /
FILE: test.txt SIZE: 12
FILE: README.md SIZE: 1234
FILE: platformio.ini SIZE: 456
FILE: partitions.csv SIZE: 234
Reading test files:
--- File: /test.txt ---
Reading file: /test.txt
- read from file:
Hello World!
--- File: /README.md ---
Reading file: /README.md
- read from file:
[README content...]
```
### Write Operations
```
============================================================
=== Testing Write Operations ===
1. Creating new file...
Writing file: /test_write.txt
- file written
2. Appending to file...
Appending to file: /test_write.txt
- message appended
Appending to file: /test_write.txt
- message appended
3. Reading back written file:
Reading file: /test_write.txt
- read from file:
Hello from ESP32!
This line was appended.
And another line.
4. Testing rename...
Renaming file /test_write.txt to /renamed.txt
- file renamed
Reading file: /renamed.txt
[content...]
5. Testing delete...
Deleting file: /renamed.txt
- file deleted
File successfully deleted
```
### Performance Test
```
============================================================
Testing file I/O with /benchmark.bin
- writing................................
- 1048576 bytes written in 2345 ms
- reading................................
- 1048576 bytes read in 1234 ms
```
## Troubleshooting
### "FFat Mount Failed"
**Possible causes:**
1. **No FFat partition in partition table**
- Check `partitions.csv` has a `fat` partition
- Verify partition is flashed
2. **Filesystem not flashed**
```bash
pio run -t buildfs
pio run -t uploadfs
```
3. **Missing Wear Leveling layer**
- Ensure you're using the updated platform with WL support
- Rebuild filesystem image
4. **Corrupted filesystem**
- Set `FORMAT_FFAT true` to reformat
- Or erase flash: `pio run -t erase`
### "File not found"
If pre-flashed files are not found:
1. Check files exist in `data/` directory
2. Rebuild filesystem: `pio run -t buildfs`
3. Upload filesystem: `pio run -t uploadfs`
4. Reset ESP32
### "Write failed"
If write operations fail:
1. Check filesystem is not full
2. Verify partition has write permissions
3. Check for filesystem corruption
## Advanced Testing
### Download and Verify Filesystem
After running tests, download the filesystem to verify changes:
```bash
pio run -t download_fatfs
```
Files will be extracted to `unpacked_fs/` directory.
### Compare Original and Downloaded
```bash
# Compare original files
diff data/test.txt unpacked_fs/test.txt
# Check for new files created by tests
ls -la unpacked_fs/
```
### Test Wear Leveling
To verify wear leveling is working:
1. Write many files
2. Download filesystem
3. Check WL state is valid:
```python
from fatfs import is_esp32_wl_image
with open('.pio/build/esp32dev/downloaded_fs_*.bin', 'rb') as f:
data = f.read()
if is_esp32_wl_image(data):
print("✓ Wear Leveling layer is intact")
else:
print("✗ Wear Leveling layer is missing or corrupted")
```
## Test Scenarios
### Scenario 1: Fresh Filesystem
```cpp
#define FORMAT_FFAT true
#define TEST_READ_EXISTING false
#define TEST_WRITE_NEW true
#define TEST_FILE_IO true
```
Tests creating a new filesystem from scratch.
### Scenario 2: Pre-Flashed Image (Default)
```cpp
#define FORMAT_FFAT false
#define TEST_READ_EXISTING true
#define TEST_WRITE_NEW true
#define TEST_FILE_IO true
```
Tests reading pre-flashed files and writing new ones.
### Scenario 3: Read-Only Test
```cpp
#define FORMAT_FFAT false
#define TEST_READ_EXISTING true
#define TEST_WRITE_NEW false
#define TEST_FILE_IO false
```
Only tests reading pre-flashed files without modifications.
### Scenario 4: Performance Only
```cpp
#define FORMAT_FFAT false
#define TEST_READ_EXISTING false
#define TEST_WRITE_NEW false
#define TEST_FILE_IO true
```
Only tests I/O performance.
## Continuous Integration
For automated testing:
```bash
#!/bin/bash
# test_fatfs.sh
# Build and upload
pio run -t buildfs
pio run -t upload
pio run -t uploadfs
# Wait for ESP32 to boot
sleep 2
# Monitor output and check for success
pio run -t monitor | tee test_output.log
# Verify output
if grep -q "✓ All tests completed!" test_output.log; then
echo "Tests PASSED"
exit 0
else
echo "Tests FAILED"
exit 1
fi
```
## Debugging
### Enable Debug Output
```cpp
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true); // Enable ESP32 debug output
// ...
}
```
### Check Partition Table
```bash
# Read partition table from device
pio run -t monitor
# In another terminal
esptool.py --port /dev/ttyUSB0 read_flash 0x8000 0x1000 partition_table.bin
# Parse partition table
python -c "
import struct
with open('partition_table.bin', 'rb') as f:
data = f.read()
for i in range(0, len(data), 32):
entry = data[i:i+32]
if entry[:2] == b'\xAA\x50':
print(f'Partition at {i}: {entry.hex()}')
"
```
### Verify Wear Leveling
```bash
# Download filesystem
pio run -t download_fatfs
# Check WL structure
python -c "
import glob
from fatfs import is_esp32_wl_image, ESP32WearLeveling
import struct
files = glob.glob('.pio/build/esp32dev/downloaded_fs_*.bin')
with open(files[0], 'rb') as f:
data = f.read()
if is_esp32_wl_image(data):
wl = ESP32WearLeveling()
state = data[:48]
fields = struct.unpack('<IIIIIIII', state[:32])
print(f'WL State:')
print(f' pos: {fields[0]}')
print(f' max_pos: {fields[1]}')
print(f' block_size: {fields[5]}')
print(f' version: {fields[6]}')
"
```
## References
- [FFat Library Documentation](https://github.com/espressif/arduino-esp32/tree/master/libraries/FFat)
- [ESP-IDF FAT Filesystem](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/fatfs.html)
+181
View File
@@ -0,0 +1,181 @@
# ESP32 Wear Leveling Implementation for FAT Filesystem
## Overview
This implementation adds ESP32 Wear Leveling layer support to FAT filesystem images created with `fatfs-python`. The wear leveling layer is required by the ESP32 Arduino Core's `FFat` library, which uses ESP-IDF's `esp_vfs_fat_spiflash_mount_rw_wl()` function.
## Problem
The ESP32 Arduino Core expects FAT partitions to be wrapped with a wear leveling layer:
- **Without WL**: Raw FAT filesystem → **Mount fails**
- **With WL**: WL State + FAT filesystem + WL metadata → **Mount succeeds**
## Wear Leveling Structure
```
┌─────────────────────────────────────────────────────────────┐
│ Sector 0: WL State Copy 1 │
├─────────────────────────────────────────────────────────────┤
│ Sector 1: WL State Copy 2 │
├─────────────────────────────────────────────────────────────┤
│ Sector 2-N: FAT Filesystem Data │
│ (Boot sector, FATs, Root dir, Data area) │
├─────────────────────────────────────────────────────────────┤
│ Sector N+1: Temp Sector (for WL operations) │
├─────────────────────────────────────────────────────────────┤
│ Sector N+2: WL State Copy 3 │
├─────────────────────────────────────────────────────────────┤
│ Sector N+3: WL State Copy 4 │
└─────────────────────────────────────────────────────────────┘
```
## WL_State Structure (48 bytes)
```c
typedef struct {
uint32_t pos; // Current position (0)
uint32_t max_pos; // Maximum position (number of FAT sectors)
uint32_t move_count; // Move counter (0)
uint32_t access_count; // Access counter (0)
uint32_t max_count; // Maximum count (update_rate * fat_sectors)
uint32_t block_size; // Block/sector size (4096)
uint32_t version; // WL version (2)
uint32_t device_id; // Device ID (0)
uint8_t reserved[12]; // Reserved (0xFF)
uint32_t crc32; // CRC32 of structure
} WL_State;
```
## Configuration
### Default Values
- **Sector Size**: 4096 bytes (ESP32 standard)
- **Update Rate**: 16 (triggers WL after 16 * sectors writes)
- **WL State Sectors**: 2 copies at start, 2 at end (4 total)
- **Temp Sectors**: 1 sector for WL operations
### Overhead Calculation
```
Total Sectors = Partition Size / Sector Size
WL Overhead = (2 + 2 + 1) = 5 sectors
FAT Sectors = Total Sectors - 5
```
Example for 1.5 MB partition:
- Total: 1,507,328 bytes / 4096 = 368 sectors
- WL Overhead: 5 sectors = 20,480 bytes
- FAT Data: 363 sectors = 1,486,848 bytes
## Usage
### Building FAT Image with WL
The `build_fatfs_image()` function in `main.py` automatically wraps FAT images:
```bash
pio run -t buildfs
```
Output:
```
Building FS image from 'data' directory to .pio/build/esp32dev/fatfs.bin
Wrapping FAT image with ESP32 Wear Leveling layer...
Partition size: 1507328 bytes (368 sectors)
FAT data size: 1486848 bytes (363 sectors)
WL overhead: 5 sectors
Successfully created wear-leveling FAT image
```
### Downloading and Extracting
The `download_fatfs` target automatically detects and extracts WL-wrapped images:
```bash
pio run -t download_fatfs
```
Output:
```
Detected Wear Leveling layer, extracting FAT data...
Extracted FAT data: 1486848 bytes
Extracting files:
FILE: /test.txt (12 bytes)
Successfully extracted 1 file(s) to unpacked_fs
```
## Technical Details
### CRC32 Calculation
The WL_State CRC32 is calculated over the first 44 bytes (excluding the CRC field itself):
```python
state_data = struct.pack('<IIIIIIII12s',
pos, max_pos, move_count, access_count, max_count,
block_size, version, device_id, reserved)
crc = zlib.crc32(state_data) & 0xFFFFFFFF
```
### Sector Alignment
All data must be aligned to sector boundaries (4096 bytes):
- WL State is padded with 0xFF to fill the sector
- FAT data is padded with 0xFF to sector boundary
- Total image size must equal partition size exactly
### Erased Flash Value
Unused areas are filled with `0xFF` (erased flash state):
- Reserved bytes in WL_State: `0xFF`
- Padding after FAT data: `0xFF`
- Temp sector: `0xFF`
## Compatibility
### ESP-IDF Versions
- Tested with ESP-IDF v4.x and v5.x
- Compatible with Arduino-ESP32 core 2.x and 3.x
### Sector Sizes
- **Supported**: 4096 bytes (recommended)
- **Theoretical**: 512, 1024, 2048 bytes (not tested)
### FAT Types
- FAT12 (small partitions)
- FAT16 (medium partitions)
- FAT32 (large partitions, >32MB)
## Troubleshooting
### "FFat Mount Failed"
**Cause**: Image doesn't have wear leveling layer
**Solution**: Rebuild with updated `build_fatfs_image()`:
```bash
pio run -t buildfs
pio run -t uploadfs
```
### "Invalid sector size"
**Cause**: Sector size mismatch between build and ESP32 config
**Solution**: Ensure `CONFIG_WL_SECTOR_SIZE=4096` in sdkconfig
### "Partition too small"
**Cause**: FAT data + WL overhead exceeds partition size
**Solution**: Increase partition size in `partitions.csv` or reduce data
## References
- [ESP-IDF Wear Levelling Component](https://github.com/espressif/esp-idf/tree/master/components/wear_levelling)
- [ESP-IDF FAT Filesystem](https://github.com/espressif/esp-idf/tree/master/components/fatfs)
- [Arduino-ESP32 FFat Library](https://github.com/espressif/arduino-esp32/tree/master/libraries/FFat)
- [mk_esp32fat Tool](https://github.com/TobleMiner/mk_esp32fat) (alternative C implementation)
## License
Same as platform-espressif32 (Apache 2.0)
+224
View File
@@ -0,0 +1,224 @@
# ESP32 FAT Filesystem Test Project
This project tests the FAT filesystem implementation with ESP32 Wear Leveling support.
## Overview
This project demonstrates:
- Building FAT filesystem images with wear leveling layer
- Uploading FAT images to ESP32
- Mounting and reading FAT filesystem on ESP32
- Downloading and extracting FAT images from ESP32
## Requirements
- PlatformIO
- ESP32 development board
- USB cable
## Project Structure
```
arduino-fatfs/
├── data/ # Files to be included in FAT image
│ ├── test.txt
│ ├── README.md
│ └── ...
├── src/
│ └── ffat.ino # Main Arduino sketch
├── partitions.csv # Partition table with FAT partition
├── platformio.ini # PlatformIO configuration
└── unpacked_fs/ # Downloaded files (created by download_fatfs)
```
## Usage
### 1. Build Firmware
```bash
pio run
```
### 2. Build FAT Filesystem Image
Place your files in the `data/` directory, then:
```bash
pio run -t buildfs
```
This creates a FAT filesystem image with ESP32 wear leveling layer at:
`.pio/build/esp32dev/fatfs.bin`
### 3. Upload Firmware and Filesystem
```bash
# Upload firmware
pio run -t upload
# Upload filesystem
pio run -t uploadfs
```
### 4. Monitor Serial Output
```bash
pio run -t monitor
```
Expected output:
```
FFat mounted successfully
Test begin
Total space: 1486848
Free space: 1482752
Listing directory: /
FILE: test.txt SIZE: 12
FILE: README.md SIZE: 1234
Test complete
```
### 5. Download Filesystem from Device
To download and extract the filesystem from the device:
```bash
pio run -t download_fatfs
```
Files will be extracted to `unpacked_fs/` directory.
## Partition Table
The `partitions.csv` defines the flash layout:
```csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
ffat, data, fat, 0x290000,0x170000,
```
The `ffat` partition:
- **Type**: data
- **SubType**: fat (0x81)
- **Offset**: 0x290000 (2,686,976 bytes)
- **Size**: 0x170000 (1,507,328 bytes = ~1.44 MB)
## Wear Leveling
The FAT filesystem is automatically wrapped with ESP32's wear leveling layer:
- **Total partition**: 1,507,328 bytes (368 sectors × 4096 bytes)
- **WL overhead**: 20,480 bytes (5 sectors)
- **FAT data**: 1,486,848 bytes (363 sectors)
Structure:
```
[WL State 1][WL State 2][FAT Data][Temp][WL State 3][WL State 4]
```
See [WEAR_LEVELING.md](../../WEAR_LEVELING.md) for details.
## Troubleshooting
### "FFat Mount Failed"
**Possible causes:**
1. Filesystem not uploaded
2. Wrong partition table
3. Corrupted filesystem
**Solutions:**
```bash
# Rebuild and upload filesystem
pio run -t buildfs
pio run -t uploadfs
# Or erase flash and start fresh
pio run -t erase
pio run -t upload
pio run -t uploadfs
```
### "No FAT filesystem partition found"
**Cause:** Partition table doesn't have a FAT partition
**Solution:** Check `partitions.csv` has a partition with `SubType: fat`
### Files not appearing
**Cause:** Files not in `data/` directory when building
**Solution:**
1. Add files to `data/` directory
2. Rebuild filesystem: `pio run -t buildfs`
3. Upload: `pio run -t uploadfs`
## Code Example
```cpp
#include "FFat.h"
void setup() {
Serial.begin(115200);
// Mount FAT filesystem
if (!FFat.begin(false)) {
Serial.println("FFat Mount Failed");
return;
}
// List files
File root = FFat.open("/");
File file = root.openNextFile();
while (file) {
Serial.printf("File: %s, Size: %d\n",
file.name(), file.size());
file = root.openNextFile();
}
// Read file
File f = FFat.open("/test.txt", "r");
if (f) {
String content = f.readString();
Serial.println(content);
f.close();
}
// Write file
f = FFat.open("/output.txt", "w");
if (f) {
f.println("Hello from ESP32!");
f.close();
}
}
void loop() {
// Your code here
}
```
## Platform Configuration
This project uses a custom platform with FAT filesystem support:
```ini
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = fatfs
board_build.partitions = partitions.csv
```
## References
- [ESP32 FFat Library](https://github.com/espressif/arduino-esp32/tree/master/libraries/FFat)
- [ESP-IDF FAT Filesystem](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/fatfs.html)
- [ESP-IDF Wear Levelling](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/wear-levelling.html)
- [Platform-Espressif32 FAT Integration](../platform-espressif32/FATFS_INTEGRATION.md)
- [Wear Leveling Implementation](../platform-espressif32/WEAR_LEVELING.md)
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
ffat, data, fat, 0x290000,0x170000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x140000
5 app1 app ota_1 0x150000 0x140000
6 ffat data fat 0x290000 0x170000
@@ -0,0 +1,7 @@
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = fatfs
board_build.partitions = partitions.csv
monitor_speed = 115200
+1
View File
@@ -0,0 +1 @@
Test file
+6
View File
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
ffat, data, fat, 0x290000,0x170000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x140000
5 app1 app ota_1 0x150000 0x140000
6 ffat data fat 0x290000 0x170000
+6
View File
@@ -0,0 +1,6 @@
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = fatfs
board_build.partitions = partitions.csv
+478
View File
@@ -0,0 +1,478 @@
#include "FS.h"
#include "FFat.h"
#include "esp_partition.h"
// Test configuration
// Set to true to format the partition (will erase all data!)
// Set to false to test the pre-flashed image from data/ directory
#define FORMAT_FFAT false // Test pre-flashed image
// Test settings
#define TEST_READ_EXISTING true // Test reading files from flashed image
#define TEST_WRITE_NEW true // Test writing new files
#define TEST_FILE_IO true // Test file I/O performance
void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
Serial.printf("Listing directory: %s\r\n", dirname);
File root = fs.open(dirname);
if (!root) {
Serial.println("- failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) {
listDir(fs, file.path(), levels - 1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\tSIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void readFile(fs::FS &fs, const char *path) {
Serial.printf("Reading file: %s\r\n", path);
File file = fs.open(path);
if (!file || file.isDirectory()) {
Serial.println("- failed to open file for reading");
return;
}
Serial.println("- read from file:");
while (file.available()) {
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char *path, const char *message) {
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("- failed to open file for writing");
return;
}
if (file.print(message)) {
Serial.println("- file written");
} else {
Serial.println("- write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char *path, const char *message) {
Serial.printf("Appending to file: %s\r\n", path);
File file = fs.open(path, FILE_APPEND);
if (!file) {
Serial.println("- failed to open file for appending");
return;
}
if (file.print(message)) {
Serial.println("- message appended");
} else {
Serial.println("- append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char *path1, const char *path2) {
Serial.printf("Renaming file %s to %s\r\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("- file renamed");
} else {
Serial.println("- rename failed");
}
}
void deleteFile(fs::FS &fs, const char *path) {
Serial.printf("Deleting file: %s\r\n", path);
if (fs.remove(path)) {
Serial.println("- file deleted");
} else {
Serial.println("- delete failed");
}
}
void testFileIO(fs::FS &fs, const char *path) {
Serial.printf("Testing file I/O with %s\r\n", path);
static uint8_t buf[512];
size_t len = 0;
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("- failed to open file for writing");
return;
}
size_t i;
Serial.print("- writing");
uint32_t start = millis();
for (i = 0; i < 2048; i++) {
if ((i & 0x001F) == 0x001F) {
Serial.print(".");
}
file.write(buf, 512);
}
Serial.println("");
uint32_t end = millis() - start;
Serial.printf(" - %u bytes written in %lu ms\r\n", 2048 * 512, end);
file.close();
file = fs.open(path);
start = millis();
end = start;
i = 0;
if (file && !file.isDirectory()) {
len = file.size();
size_t flen = len;
start = millis();
Serial.print("- reading");
while (len) {
size_t toRead = len;
if (toRead > 512) {
toRead = 512;
}
file.read(buf, toRead);
if ((i++ & 0x001F) == 0x001F) {
Serial.print(".");
}
len -= toRead;
}
Serial.println("");
end = millis() - start;
Serial.printf("- %u bytes read in %lu ms\r\n", flen, end);
file.close();
} else {
Serial.println("- failed to open file for reading");
}
}
void testExistingFiles(fs::FS &fs) {
Serial.println("\n=== Testing Pre-Flashed Files ===");
// List all files in root
Serial.println("\nFiles in root directory:");
listDir(fs, "/", 2);
// Test reading specific files that should exist
const char* testFiles[] = {
"/test.txt",
"/README.md",
"/platformio.ini",
"/partitions.csv"
};
Serial.println("\nReading test files:");
for (int i = 0; i < 4; i++) {
if (fs.exists(testFiles[i])) {
Serial.printf("\n--- File: %s ---\n", testFiles[i]);
readFile(fs, testFiles[i]);
} else {
Serial.printf("File not found: %s\n", testFiles[i]);
}
}
}
void testWriteOperations(fs::FS &fs) {
Serial.println("\n=== Testing Write Operations ===");
// Test creating new file
Serial.println("\n1. Creating new file...");
writeFile(fs, "/test_write.txt", "Hello from ESP32!\n");
// Test appending
Serial.println("\n2. Appending to file...");
appendFile(fs, "/test_write.txt", "This line was appended.\n");
appendFile(fs, "/test_write.txt", "And another line.\n");
// Read back
Serial.println("\n3. Reading back written file:");
readFile(fs, "/test_write.txt");
// Test rename
Serial.println("\n4. Testing rename...");
renameFile(fs, "/test_write.txt", "/renamed.txt");
readFile(fs, "/renamed.txt");
// Test delete
Serial.println("\n5. Testing delete...");
deleteFile(fs, "/renamed.txt");
// Verify deletion
if (!fs.exists("/renamed.txt")) {
Serial.println("File successfully deleted");
} else {
Serial.println("ERROR: File still exists!");
}
}
void testFileSystem(fs::FS &fs) {
Serial.println("\n=== Filesystem Information ===");
Serial.printf("Total space: %10u bytes (%.2f MB)\n",
FFat.totalBytes(), FFat.totalBytes() / 1024.0 / 1024.0);
Serial.printf("Used space: %10u bytes (%.2f MB)\n",
FFat.usedBytes(), FFat.usedBytes() / 1024.0 / 1024.0);
Serial.printf("Free space: %10u bytes (%.2f MB)\n",
FFat.freeBytes(), FFat.freeBytes() / 1024.0 / 1024.0);
float usage = (FFat.usedBytes() * 100.0) / FFat.totalBytes();
Serial.printf("Usage: %.1f%%\n", usage);
}
void printSeparator() {
Serial.println("\n============================================================");
}
void setup() {
Serial.begin(115200);
delay(1000); // Wait for serial monitor
// Enable ESP-IDF debug logging
esp_log_level_set("*", ESP_LOG_DEBUG);
esp_log_level_set("vfs_fat", ESP_LOG_VERBOSE);
esp_log_level_set("wear_levelling", ESP_LOG_VERBOSE);
Serial.println("\n\n");
printSeparator();
Serial.println("ESP32 FFat Filesystem Test");
Serial.println("Testing pre-flashed image with Wear Leveling");
printSeparator();
// Format if requested
if (FORMAT_FFAT) {
Serial.println("\n============================================================");
Serial.println("WARNING: Formatting FFat partition...");
Serial.println("This will erase all data!");
Serial.println("============================================================");
delay(2000);
// First try to mount the WL layer without formatting
Serial.println("\nStep 1: Checking if WL layer can be mounted...");
if (FFat.begin(false, "/ffat", 10, "ffat")) {
Serial.println("✓ WL layer mounted successfully!");
Serial.println(" Now formatting filesystem...");
FFat.end();
} else {
Serial.println("✗ WL layer mount failed - will try to format anyway");
}
Serial.println("\nStep 2: Formatting partition...");
if (!FFat.format(false, (char*)"ffat")) {
Serial.println("ERROR: FFat Format Failed");
Serial.println("\nThis means the Wear Leveling layer itself is broken.");
Serial.println("The WL structure on flash is not compatible with ESP-IDF.");
return;
}
Serial.println("✓ FFat formatted successfully");
Serial.println("\nStep 3: Mounting formatted partition...");
if (!FFat.begin(false, "/ffat", 10, "ffat")) {
Serial.println("ERROR: Cannot mount even after format!");
return;
}
Serial.println("✓ Mounted successfully after format");
// Write some test files so we have data to compare
Serial.println("\nWriting test files...");
writeFile(FFat, "/test.txt", "This is a test file created by ESP32\n");
writeFile(FFat, "/README.md", "# ESP32 Formatted Filesystem\n\nThis was formatted on device.\n");
Serial.println("\n*** IMPORTANT: Now download this filesystem ***");
Serial.println("Run: pio run -t download_fatfs");
Serial.println("This will save the ESP32-formatted image for comparison");
delay(5000); // Give time to read the message
}
// Mount the filesystem
Serial.println("\nMounting FFat filesystem...");
Serial.println("Partition label: 'ffat'");
Serial.println("Format on fail: false");
Serial.println("Max open files: 10");
if (!FFat.begin(false, "/ffat", 10, "ffat")) { // formatOnFail = false
Serial.println("\nERROR: FFat Mount Failed");
Serial.println("\nDiagnostics:");
Serial.println("1. Checking partition...");
// Try to get partition info
const esp_partition_t* partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_DATA_FAT,
"ffat"
);
if (partition) {
Serial.printf(" ✓ Partition found: %s\n", partition->label);
Serial.printf(" Address: 0x%06X\n", partition->address);
Serial.printf(" Size: %u bytes (%.2f MB)\n",
partition->size, partition->size / 1024.0 / 1024.0);
// Check FAT boot sector at offset 0 (RAW FAT, no WL layer in flash)
Serial.println("\n2. Checking FAT boot sector...");
uint8_t buffer[512];
if (esp_partition_read(partition, 0, buffer, 512) == ESP_OK) {
Serial.println(" First 64 bytes at offset 0 (RAW FAT):");
for (int i = 0; i < 64; i++) {
Serial.printf("%02X ", buffer[i]);
if ((i + 1) % 16 == 0) Serial.println();
}
Serial.println();
// Check boot signature
if (buffer[510] == 0x55 && buffer[511] == 0xAA) {
Serial.println(" ✓ Boot signature found (0x55AA)");
// Parse boot sector
char oem[9];
memcpy(oem, buffer + 3, 8);
oem[8] = 0;
uint16_t bytes_per_sector = buffer[11] | (buffer[12] << 8);
uint16_t total_sectors = buffer[19] | (buffer[20] << 8);
Serial.printf(" OEM Name: '%s'\n", oem);
Serial.printf(" Bytes per sector: %u\n", bytes_per_sector);
Serial.printf(" Total sectors: %u\n", total_sectors);
if (bytes_per_sector == 4096 && total_sectors == 362) {
Serial.println(" ✓ Correct format for ESP32 FFat!");
}
} else {
Serial.printf(" ✗ Invalid boot signature: 0x%02X%02X (expected 0x55AA)\n",
buffer[511], buffer[510]);
}
}
// Check FAT table
Serial.println("\n3. Checking FAT table...");
if (esp_partition_read(partition, 4096, buffer, 32) == ESP_OK) {
Serial.println(" FAT1 (first 32 bytes at offset 4096):");
for (int i = 0; i < 32; i++) {
Serial.printf("%02X ", buffer[i]);
if ((i + 1) % 16 == 0) Serial.println();
}
Serial.println();
if (buffer[0] == 0xF8 && buffer[1] == 0xFF && buffer[2] == 0xFF) {
Serial.println(" ✓ Media descriptor correct (F8 FF FF)");
// Check if rest is 00
bool clean = true;
for (int i = 3; i < 32; i++) {
if (buffer[i] != 0x00) {
clean = false;
break;
}
}
if (clean) {
Serial.println(" ✓ FAT table clean (all zeros after media descriptor)");
} else {
Serial.println(" ✗ FAT table has non-zero bytes after media descriptor");
}
}
}
// Check WL metadata at end of partition
Serial.println("\n4. Checking WL metadata at end...");
// WL state1 should be at partition_size - 3 * sector_size
uint32_t wl_state1_offset = partition->size - (3 * 4096);
if (esp_partition_read(partition, wl_state1_offset, buffer, 48) == ESP_OK) {
Serial.printf(" WL State at offset 0x%06X (first 48 bytes):\n", wl_state1_offset);
for (int i = 0; i < 48; i++) {
Serial.printf("%02X ", buffer[i]);
if ((i + 1) % 16 == 0) Serial.println();
}
Serial.println();
// Parse WL_State
uint32_t* words = (uint32_t*)buffer;
Serial.printf(" pos: %u\n", words[0]);
Serial.printf(" max_pos: %u\n", words[1]);
Serial.printf(" block_size: %u\n", words[5]);
Serial.printf(" version: %u\n", words[6]);
if (words[5] == 4096 && words[6] == 2) {
Serial.println(" ✓ WL metadata looks valid");
} else {
Serial.println(" ✗ WL metadata invalid");
}
}
} else {
Serial.println(" ✗ Partition 'ffat' not found!");
Serial.println(" Check partition table in platformio.ini");
}
Serial.println("\nPossible causes:");
Serial.println("- Corrupted FAT filesystem");
Serial.println("- Wrong sector size (should be 4096)");
Serial.println("- Wrong sector count (should be 362)");
Serial.println("\nTry: pio run -t erase && pio run -t upload && pio run -t uploadfs");
return;
}
Serial.println("✓ FFat mounted successfully!");
// Show filesystem info
testFileSystem(FFat);
// Test reading existing files
if (TEST_READ_EXISTING) {
printSeparator();
testExistingFiles(FFat);
}
// Test write operations
if (TEST_WRITE_NEW) {
printSeparator();
testWriteOperations(FFat);
// Show updated filesystem info
printSeparator();
Serial.println("\nFilesystem after write tests:");
testFileSystem(FFat);
}
// Test file I/O performance
if (TEST_FILE_IO) {
printSeparator();
testFileIO(FFat, "/benchmark.bin");
// Clean up benchmark file
deleteFile(FFat, "/benchmark.bin");
}
// Final directory listing
printSeparator();
Serial.println("\nFinal directory listing:");
listDir(FFat, "/", 2);
printSeparator();
Serial.println("\n✓ All tests completed!");
Serial.println("\nFilesystem remains mounted for further testing.");
Serial.println("You can now:");
Serial.println("- Download filesystem: pio run -t download_fatfs");
Serial.println("- Reset to re-run tests");
printSeparator();
}
void loop() {
// Keep the filesystem mounted
// You can add interactive commands here if needed
}
+3
View File
@@ -0,0 +1,3 @@
.pio
.vscode
unpacked_fs/
+12
View File
@@ -0,0 +1,12 @@
# LittleFS Test Data
This directory contains test files that will be included in the LittleFS filesystem image.
## Files
- `test.txt` - Simple text file
- `README.md` - This file
- `platformio.ini` - Copy of project configuration
- `partitions.csv` - Copy of partition table
These files will be flashed to the ESP32's LittleFS partition and can be read by the firmware.
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x170000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x140000
5 app1 app ota_1 0x150000 0x140000
6 spiffs data spiffs 0x290000 0x170000
@@ -0,0 +1,6 @@
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = littlefs
board_build.partitions = partitions.csv
+2
View File
@@ -0,0 +1,2 @@
Hello from LittleFS!
This is a test file.
+6
View File
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x170000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x140000
5 app1 app ota_1 0x150000 0x140000
6 spiffs data spiffs 0x290000 0x170000
+6
View File
@@ -0,0 +1,6 @@
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = littlefs
board_build.partitions = partitions.csv
@@ -0,0 +1,324 @@
#include "FS.h"
#include "LittleFS.h"
// Test configuration
#define FORMAT_LITTLEFS false // Set to true to format (will erase all data!)
// Test settings
#define TEST_READ_EXISTING true // Test reading files from flashed image
#define TEST_WRITE_NEW true // Test writing new files
#define TEST_FILE_IO true // Test file I/O performance
void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
Serial.printf("Listing directory: %s\r\n", dirname);
File root = fs.open(dirname);
if (!root) {
Serial.println("- failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) {
listDir(fs, file.path(), levels - 1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\tSIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void readFile(fs::FS &fs, const char *path) {
Serial.printf("Reading file: %s\r\n", path);
File file = fs.open(path);
if (!file || file.isDirectory()) {
Serial.println("- failed to open file for reading");
return;
}
Serial.println("- read from file:");
while (file.available()) {
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char *path, const char *message) {
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("- failed to open file for writing");
return;
}
if (file.print(message)) {
Serial.println("- file written");
} else {
Serial.println("- write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char *path, const char *message) {
Serial.printf("Appending to file: %s\r\n", path);
File file = fs.open(path, FILE_APPEND);
if (!file) {
Serial.println("- failed to open file for appending");
return;
}
if (file.print(message)) {
Serial.println("- message appended");
} else {
Serial.println("- append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char *path1, const char *path2) {
Serial.printf("Renaming file %s to %s\r\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("- file renamed");
} else {
Serial.println("- rename failed");
}
}
void deleteFile(fs::FS &fs, const char *path) {
Serial.printf("Deleting file: %s\r\n", path);
if (fs.remove(path)) {
Serial.println("- file deleted");
} else {
Serial.println("- delete failed");
}
}
void testFileIO(fs::FS &fs, const char *path) {
Serial.printf("Testing file I/O with %s\r\n", path);
static uint8_t buf[512];
size_t len = 0;
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("- failed to open file for writing");
return;
}
size_t i;
Serial.print("- writing");
uint32_t start = millis();
for (i = 0; i < 2048; i++) {
if ((i & 0x001F) == 0x001F) {
Serial.print(".");
}
file.write(buf, 512);
}
Serial.println("");
uint32_t end = millis() - start;
Serial.printf(" - %u bytes written in %lu ms\r\n", 2048 * 512, end);
file.close();
file = fs.open(path);
start = millis();
end = start;
i = 0;
if (file && !file.isDirectory()) {
len = file.size();
size_t flen = len;
start = millis();
Serial.print("- reading");
while (len) {
size_t toRead = len;
if (toRead > 512) {
toRead = 512;
}
file.read(buf, toRead);
if ((i++ & 0x001F) == 0x001F) {
Serial.print(".");
}
len -= toRead;
}
Serial.println("");
end = millis() - start;
Serial.printf("- %u bytes read in %lu ms\r\n", flen, end);
file.close();
} else {
Serial.println("- failed to open file for reading");
}
}
void testExistingFiles(fs::FS &fs) {
Serial.println("\n=== Testing Pre-Flashed Files ===");
// List all files in root
Serial.println("\nFiles in root directory:");
listDir(fs, "/", 2);
// Test reading specific files that should exist
const char* testFiles[] = {
"/test.txt",
"/README.md",
"/platformio.ini",
"/partitions.csv"
};
Serial.println("\nReading test files:");
for (int i = 0; i < 4; i++) {
if (fs.exists(testFiles[i])) {
Serial.printf("\n--- File: %s ---\n", testFiles[i]);
readFile(fs, testFiles[i]);
} else {
Serial.printf("File not found: %s\n", testFiles[i]);
}
}
}
void testWriteOperations(fs::FS &fs) {
Serial.println("\n=== Testing Write Operations ===");
// Test creating new file
Serial.println("\n1. Creating new file...");
writeFile(fs, "/test_write.txt", "Hello from ESP32!\n");
// Test appending
Serial.println("\n2. Appending to file...");
appendFile(fs, "/test_write.txt", "This line was appended.\n");
appendFile(fs, "/test_write.txt", "And another line.\n");
// Read back
Serial.println("\n3. Reading back written file:");
readFile(fs, "/test_write.txt");
// Test rename
Serial.println("\n4. Testing rename...");
renameFile(fs, "/test_write.txt", "/renamed.txt");
readFile(fs, "/renamed.txt");
// Test delete
Serial.println("\n5. Testing delete...");
deleteFile(fs, "/renamed.txt");
// Verify deletion
if (!fs.exists("/renamed.txt")) {
Serial.println("File successfully deleted");
} else {
Serial.println("ERROR: File still exists!");
}
}
void testFileSystem(fs::FS &fs) {
Serial.println("\n=== Filesystem Information ===");
Serial.printf("Total space: %10u bytes (%.2f MB)\n",
LittleFS.totalBytes(), LittleFS.totalBytes() / 1024.0 / 1024.0);
Serial.printf("Used space: %10u bytes (%.2f MB)\n",
LittleFS.usedBytes(), LittleFS.usedBytes() / 1024.0 / 1024.0);
float usage = (LittleFS.usedBytes() * 100.0) / LittleFS.totalBytes();
Serial.printf("Usage: %.1f%%\n", usage);
}
void printSeparator() {
Serial.println("\n============================================================");
}
void setup() {
Serial.begin(115200);
delay(1000); // Wait for serial monitor
Serial.println("\n\n");
printSeparator();
Serial.println("ESP32 LittleFS Filesystem Test");
Serial.println("Testing pre-flashed LittleFS image");
printSeparator();
// Format if requested
if (FORMAT_LITTLEFS) {
Serial.println("\n============================================================");
Serial.println("WARNING: Formatting LittleFS partition...");
Serial.println("This will erase all data!");
Serial.println("============================================================");
delay(2000);
if (!LittleFS.format()) {
Serial.println("ERROR: LittleFS Format Failed");
return;
}
Serial.println(" LittleFS formatted successfully");
}
// Mount the filesystem
Serial.println("\nMounting LittleFS filesystem...");
Serial.println("Format on fail: false");
if (!LittleFS.begin(false)) { // formatOnFail = false
Serial.println("\nERROR: LittleFS Mount Failed");
Serial.println("\nPossible causes:");
Serial.println("- Filesystem not uploaded");
Serial.println("- Corrupted LittleFS filesystem");
Serial.println("- Wrong partition table");
Serial.println("\nTry: pio run -t uploadfs");
return;
}
Serial.println(" LittleFS mounted successfully!");
// Show filesystem info
testFileSystem(LittleFS);
// Test reading existing files
if (TEST_READ_EXISTING) {
printSeparator();
testExistingFiles(LittleFS);
}
// Test write operations
if (TEST_WRITE_NEW) {
printSeparator();
testWriteOperations(LittleFS);
// Show updated filesystem info
printSeparator();
Serial.println("\nFilesystem after write tests:");
testFileSystem(LittleFS);
}
// Test file I/O performance
if (TEST_FILE_IO) {
printSeparator();
testFileIO(LittleFS, "/benchmark.bin");
// Clean up benchmark file
deleteFile(LittleFS, "/benchmark.bin");
}
// Final directory listing
printSeparator();
Serial.println("\nFinal directory listing:");
listDir(LittleFS, "/", 2);
printSeparator();
Serial.println("\n All tests completed!");
Serial.println("\nFilesystem remains mounted for further testing.");
Serial.println("You can now:");
Serial.println("- Download filesystem: pio run -t download_fs");
Serial.println("- Reset to re-run tests");
printSeparator();
}
void loop() {
// Keep the filesystem mounted
// You can add interactive commands here if needed
}
+3
View File
@@ -0,0 +1,3 @@
.pio
.vscode
unpacked_fs/
+247
View File
@@ -0,0 +1,247 @@
# ESP32 SPIFFS Filesystem Test Project
This project tests the SPIFFS filesystem implementation on ESP32.
## Overview
This project demonstrates:
- Building SPIFFS filesystem images
- Uploading SPIFFS images to ESP32
- Mounting and reading SPIFFS filesystem on ESP32
- Downloading and extracting SPIFFS images from ESP32
## Requirements
- PlatformIO
- ESP32 development board
- USB cable
## Project Structure
```
arduino-spiffs/
├── data/ # Files to be included in SPIFFS image
│ ├── test.txt
│ ├── README.md
│ └── ...
├── src/
│ └── spiffs_test.ino # Main Arduino sketch
├── partitions.csv # Partition table with SPIFFS partition
├── platformio.ini # PlatformIO configuration
└── unpacked_fs/ # Downloaded files (created by download_spiffs)
```
## Usage
### 1. Build Firmware
```bash
pio run
```
### 2. Build SPIFFS Filesystem Image
Place your files in the `data/` directory, then:
```bash
pio run -t buildfs
```
This creates a SPIFFS filesystem image at:
`.pio/build/esp32dev/spiffs.bin`
### 3. Upload Firmware and Filesystem
```bash
# Upload firmware
pio run -t upload
# Upload filesystem
pio run -t uploadfs
```
### 4. Monitor Serial Output
```bash
pio run -t monitor
```
Expected output:
```
SPIFFS mounted successfully
Total space: 1507328
Used space: 12345
Listing directory: /
FILE: test.txt SIZE: 12
FILE: README.md SIZE: 456
Test complete
```
### 5. Download Filesystem from Device
To download and extract the filesystem from the device:
```bash
pio run -t download_spiffs
```
Files will be extracted to `unpacked_fs/` directory.
## Partition Table
The `partitions.csv` defines the flash layout:
```csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x170000,
```
The `spiffs` partition:
- **Type**: data
- **SubType**: spiffs (0x82)
- **Offset**: 0x290000 (2,686,976 bytes)
- **Size**: 0x170000 (1,507,328 bytes = ~1.44 MB)
## SPIFFS Configuration
Default SPIFFS configuration (can be customized in `platformio.ini`):
```ini
board_build.spiffs.page_size = 256
board_build.spiffs.block_size = 4096
board_build.spiffs.obj_name_len = 32
board_build.spiffs.meta_len = 4
board_build.spiffs.use_magic = true
board_build.spiffs.use_magic_len = true
board_build.spiffs.aligned_obj_ix_tables = false
```
These match ESP-IDF defaults:
- `CONFIG_SPIFFS_PAGE_SIZE = 256`
- `CONFIG_SPIFFS_OBJ_NAME_LEN = 32`
- `CONFIG_SPIFFS_META_LENGTH = 4`
- `CONFIG_SPIFFS_USE_MAGIC = true`
- `CONFIG_SPIFFS_USE_MAGIC_LENGTH = true`
## Troubleshooting
### "SPIFFS Mount Failed"
**Possible causes:**
1. Filesystem not uploaded
2. Wrong partition table
3. Corrupted filesystem
**Solutions:**
```bash
# Rebuild and upload filesystem
pio run -t buildfs
pio run -t uploadfs
# Or erase flash and start fresh
pio run -t erase
pio run -t upload
pio run -t uploadfs
```
### "No SPIFFS filesystem partition found"
**Cause:** Partition table doesn't have a SPIFFS partition
**Solution:** Check `partitions.csv` has a partition with `SubType: spiffs`
### Files not appearing
**Cause:** Files not in `data/` directory when building
**Solution:**
1. Add files to `data/` directory
2. Rebuild filesystem: `pio run -t buildfs`
3. Upload: `pio run -t uploadfs`
## Code Example
```cpp
#include "FS.h"
#include "SPIFFS.h"
void setup() {
Serial.begin(115200);
// Mount SPIFFS filesystem
if (!SPIFFS.begin(false)) {
Serial.println("SPIFFS Mount Failed");
return;
}
// List files
File root = SPIFFS.open("/");
File file = root.openNextFile();
while (file) {
Serial.printf("File: %s, Size: %d\n",
file.name(), file.size());
file = root.openNextFile();
}
// Read file
File f = SPIFFS.open("/test.txt", "r");
if (f) {
String content = f.readString();
Serial.println(content);
f.close();
}
// Write file
f = SPIFFS.open("/output.txt", "w");
if (f) {
f.println("Hello from ESP32!");
f.close();
}
}
void loop() {
// Your code here
}
```
## Platform Configuration
This project uses a custom platform with SPIFFS filesystem support:
```ini
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = spiffs
board_build.partitions = partitions.csv
```
## Comparison with Other Filesystems
| Feature | SPIFFS | LittleFS | FatFS |
|---------|--------|----------|-------|
| Wear Leveling | Built-in | Built-in | Requires WL layer |
| Max File Size | ~1MB | Limited by partition | 4GB |
| Directories | No | Yes | Yes |
| Performance | Medium | Fast | Fast |
| RAM Usage | Low | Low | Medium |
| Reliability | Good | Excellent | Good |
## References
- [ESP32 SPIFFS Library](https://github.com/espressif/arduino-esp32/tree/master/libraries/SPIFFS)
- [ESP-IDF SPIFFS](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/spiffs.html)
- [SPIFFS Specification](https://github.com/pellepl/spiffs)
## Notes
- SPIFFS does not support directories (flat filesystem)
- File names are limited to 32 characters by default
- SPIFFS has built-in wear leveling
- Maximum file size depends on available RAM
- SPIFFS is being deprecated in favor of LittleFS in newer ESP-IDF versions
+12
View File
@@ -0,0 +1,12 @@
# SPIFFS Test Data
This directory contains test files that will be included in the SPIFFS filesystem image.
## Files
- `test.txt` - Simple text file
- `README.md` - This file
- `platformio.ini` - Copy of project configuration
- `partitions.csv` - Copy of partition table
These files will be flashed to the ESP32's SPIFFS partition and can be read by the firmware.
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x170000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x140000
5 app1 app ota_1 0x150000 0x140000
6 spiffs data spiffs 0x290000 0x170000
@@ -0,0 +1,6 @@
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = spiffs
board_build.partitions = partitions.csv
+2
View File
@@ -0,0 +1,2 @@
Hello from SPIFFS!
This is a test file.
+6
View File
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x170000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x140000
5 app1 app ota_1 0x150000 0x140000
6 spiffs data spiffs 0x290000 0x170000
+6
View File
@@ -0,0 +1,6 @@
[env:esp32dev]
platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = spiffs
board_build.partitions = partitions.csv
+324
View File
@@ -0,0 +1,324 @@
#include "FS.h"
#include "SPIFFS.h"
// Test configuration
#define FORMAT_SPIFFS false // Set to true to format (will erase all data!)
// Test settings
#define TEST_READ_EXISTING true // Test reading files from flashed image
#define TEST_WRITE_NEW true // Test writing new files
#define TEST_FILE_IO true // Test file I/O performance
void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
Serial.printf("Listing directory: %s\r\n", dirname);
File root = fs.open(dirname);
if (!root) {
Serial.println("- failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) {
listDir(fs, file.path(), levels - 1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\tSIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void readFile(fs::FS &fs, const char *path) {
Serial.printf("Reading file: %s\r\n", path);
File file = fs.open(path);
if (!file || file.isDirectory()) {
Serial.println("- failed to open file for reading");
return;
}
Serial.println("- read from file:");
while (file.available()) {
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char *path, const char *message) {
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("- failed to open file for writing");
return;
}
if (file.print(message)) {
Serial.println("- file written");
} else {
Serial.println("- write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char *path, const char *message) {
Serial.printf("Appending to file: %s\r\n", path);
File file = fs.open(path, FILE_APPEND);
if (!file) {
Serial.println("- failed to open file for appending");
return;
}
if (file.print(message)) {
Serial.println("- message appended");
} else {
Serial.println("- append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char *path1, const char *path2) {
Serial.printf("Renaming file %s to %s\r\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("- file renamed");
} else {
Serial.println("- rename failed");
}
}
void deleteFile(fs::FS &fs, const char *path) {
Serial.printf("Deleting file: %s\r\n", path);
if (fs.remove(path)) {
Serial.println("- file deleted");
} else {
Serial.println("- delete failed");
}
}
void testFileIO(fs::FS &fs, const char *path) {
Serial.printf("Testing file I/O with %s\r\n", path);
static uint8_t buf[512];
size_t len = 0;
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("- failed to open file for writing");
return;
}
size_t i;
Serial.print("- writing");
uint32_t start = millis();
for (i = 0; i < 2048; i++) {
if ((i & 0x001F) == 0x001F) {
Serial.print(".");
}
file.write(buf, 512);
}
Serial.println("");
uint32_t end = millis() - start;
Serial.printf(" - %u bytes written in %lu ms\r\n", 2048 * 512, end);
file.close();
file = fs.open(path);
start = millis();
end = start;
i = 0;
if (file && !file.isDirectory()) {
len = file.size();
size_t flen = len;
start = millis();
Serial.print("- reading");
while (len) {
size_t toRead = len;
if (toRead > 512) {
toRead = 512;
}
file.read(buf, toRead);
if ((i++ & 0x001F) == 0x001F) {
Serial.print(".");
}
len -= toRead;
}
Serial.println("");
end = millis() - start;
Serial.printf("- %u bytes read in %lu ms\r\n", flen, end);
file.close();
} else {
Serial.println("- failed to open file for reading");
}
}
void testExistingFiles(fs::FS &fs) {
Serial.println("\n=== Testing Pre-Flashed Files ===");
// List all files in root
Serial.println("\nFiles in root directory:");
listDir(fs, "/", 2);
// Test reading specific files that should exist
const char* testFiles[] = {
"/test.txt",
"/README.md",
"/platformio.ini",
"/partitions.csv"
};
Serial.println("\nReading test files:");
for (int i = 0; i < 4; i++) {
if (fs.exists(testFiles[i])) {
Serial.printf("\n--- File: %s ---\n", testFiles[i]);
readFile(fs, testFiles[i]);
} else {
Serial.printf("File not found: %s\n", testFiles[i]);
}
}
}
void testWriteOperations(fs::FS &fs) {
Serial.println("\n=== Testing Write Operations ===");
// Test creating new file
Serial.println("\n1. Creating new file...");
writeFile(fs, "/test_write.txt", "Hello from ESP32!\n");
// Test appending
Serial.println("\n2. Appending to file...");
appendFile(fs, "/test_write.txt", "This line was appended.\n");
appendFile(fs, "/test_write.txt", "And another line.\n");
// Read back
Serial.println("\n3. Reading back written file:");
readFile(fs, "/test_write.txt");
// Test rename
Serial.println("\n4. Testing rename...");
renameFile(fs, "/test_write.txt", "/renamed.txt");
readFile(fs, "/renamed.txt");
// Test delete
Serial.println("\n5. Testing delete...");
deleteFile(fs, "/renamed.txt");
// Verify deletion
if (!fs.exists("/renamed.txt")) {
Serial.println("File successfully deleted");
} else {
Serial.println("ERROR: File still exists!");
}
}
void testFileSystem(fs::FS &fs) {
Serial.println("\n=== Filesystem Information ===");
Serial.printf("Total space: %10u bytes (%.2f MB)\n",
SPIFFS.totalBytes(), SPIFFS.totalBytes() / 1024.0 / 1024.0);
Serial.printf("Used space: %10u bytes (%.2f MB)\n",
SPIFFS.usedBytes(), SPIFFS.usedBytes() / 1024.0 / 1024.0);
float usage = (SPIFFS.usedBytes() * 100.0) / SPIFFS.totalBytes();
Serial.printf("Usage: %.1f%%\n", usage);
}
void printSeparator() {
Serial.println("\n============================================================");
}
void setup() {
Serial.begin(115200);
delay(1000); // Wait for serial monitor
Serial.println("\n\n");
printSeparator();
Serial.println("ESP32 SPIFFS Filesystem Test");
Serial.println("Testing pre-flashed SPIFFS image");
printSeparator();
// Format if requested
if (FORMAT_SPIFFS) {
Serial.println("\n============================================================");
Serial.println("WARNING: Formatting SPIFFS partition...");
Serial.println("This will erase all data!");
Serial.println("============================================================");
delay(2000);
if (!SPIFFS.format()) {
Serial.println("ERROR: SPIFFS Format Failed");
return;
}
Serial.println("✓ SPIFFS formatted successfully");
}
// Mount the filesystem
Serial.println("\nMounting SPIFFS filesystem...");
Serial.println("Format on fail: false");
if (!SPIFFS.begin(false)) { // formatOnFail = false
Serial.println("\nERROR: SPIFFS Mount Failed");
Serial.println("\nPossible causes:");
Serial.println("- Filesystem not uploaded");
Serial.println("- Corrupted SPIFFS filesystem");
Serial.println("- Wrong partition table");
Serial.println("\nTry: pio run -t uploadfs");
return;
}
Serial.println("✓ SPIFFS mounted successfully!");
// Show filesystem info
testFileSystem(SPIFFS);
// Test reading existing files
if (TEST_READ_EXISTING) {
printSeparator();
testExistingFiles(SPIFFS);
}
// Test write operations
if (TEST_WRITE_NEW) {
printSeparator();
testWriteOperations(SPIFFS);
// Show updated filesystem info
printSeparator();
Serial.println("\nFilesystem after write tests:");
testFileSystem(SPIFFS);
}
// Test file I/O performance
if (TEST_FILE_IO) {
printSeparator();
testFileIO(SPIFFS, "/benchmark.bin");
// Clean up benchmark file
deleteFile(SPIFFS, "/benchmark.bin");
}
// Final directory listing
printSeparator();
Serial.println("\nFinal directory listing:");
listDir(SPIFFS, "/", 2);
printSeparator();
Serial.println("\n✓ All tests completed!");
Serial.println("\nFilesystem remains mounted for further testing.");
Serial.println("You can now:");
Serial.println("- Download filesystem: pio run -t download_spiffs");
Serial.println("- Reset to re-run tests");
printSeparator();
}
void loop() {
// Keep the filesystem mounted
// You can add interactive commands here if needed
}
+9 -37
View File
@@ -18,7 +18,7 @@
"type": "git",
"url": "https://github.com/pioarduino/platform-espressif32.git"
},
"version": "55.03.35",
"version": "55.03.36",
"frameworks": {
"arduino": {
"script": "builder/frameworks/arduino.py"
@@ -33,31 +33,31 @@
"type": "framework",
"optional": true,
"owner": "espressif",
"version": "https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5.tar.xz"
"version": "https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz"
},
"framework-arduinoespressif32-libs": {
"type": "framework",
"optional": true,
"owner": "espressif",
"version": "https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5-libs.tar.xz"
"version": "https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6-libs.tar.xz"
},
"framework-arduino-c2-skeleton-lib": {
"type": "framework",
"optional": true,
"owner": "espressif",
"version": "https://github.com/pioarduino/platform-espressif32/releases/download/c2-skeleton/c2_8410210c9a_compile_skeleton.zip"
"version": "https://github.com/pioarduino/platform-espressif32/releases/download/c2-skeleton/c2_366dadf354_compile_skeleton.zip"
},
"framework-arduino-c61-skeleton-lib": {
"type": "framework",
"optional": true,
"owner": "espressif",
"version": "https://github.com/pioarduino/platform-espressif32/releases/download/c61-skeleton/c61_d1e854920a_compile_skeleton.zip"
"version": "https://github.com/pioarduino/platform-espressif32/releases/download/c61-skeleton/c61_366dadf354_compile_skeleton.zip"
},
"framework-espidf": {
"type": "framework",
"optional": true,
"owner": "pioarduino",
"version": "https://github.com/pioarduino/esp-idf/releases/download/v5.5.1.251215/esp-idf-v5.5.1.tar.xz"
"version": "https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260116/esp-idf-v5.5.2.tar.xz"
},
"toolchain-xtensa-esp-elf": {
"type": "toolchain",
@@ -115,7 +115,7 @@
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/contrib-piohome-3.4.4.tar.gz"
},
"tool-dfuutil-arduino": {
"type": "uploader",
"type": "tool",
"optional": true,
"owner": "pioarduino",
"package-version": "1.11.0",
@@ -125,36 +125,8 @@
"type": "debugger",
"optional": true,
"owner": "pioarduino",
"package-version": "2.1200.20250707",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/openocd-v0.12.0-esp32-20250707.zip"
},
"tool-mklittlefs": {
"type": "uploader",
"optional": true,
"owner": "pioarduino",
"package-version": "3.2.0",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/mklittlefs-3.2.0-new.zip"
},
"tool-mklittlefs4": {
"type": "uploader",
"optional": true,
"owner": "pioarduino",
"package-version": "4.0.2",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/mklittlefs-4.0.2.zip"
},
"tool-mkspiffs": {
"type": "uploader",
"optional": true,
"owner": "pioarduino",
"package-version": "2.230.0",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/mkspiffs-v2.230.0.zip"
},
"tool-mkfatfs": {
"type": "uploader",
"optional": true,
"owner": "pioarduino",
"package-version": "2.0.1",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/mkfatfs-v2.0.1.zip"
"package-version": "2.1200.20251215",
"version": "https://github.com/pioarduino/registry/releases/download/0.0.1/openocd-v0.12.0-esp32-20251215.zip"
},
"tool-cppcheck": {
"type": "tool",
+4 -76
View File
@@ -707,83 +707,13 @@ class Espressif32Platform(PlatformBase):
if any(tool in package for tool in check_tools):
self.install_tool(package)
def _ensure_mklittlefs_version(self) -> None:
"""Ensure correct mklittlefs version is installed."""
piopm_path = Path(self.packages_dir) / "tool-mklittlefs" / ".piopm"
if piopm_path.exists():
try:
with open(piopm_path, 'r', encoding='utf-8') as f:
package_data = json.load(f)
version = package_data.get('version', '')
if not version.startswith("3."):
safe_remove_file(piopm_path)
logger.info(f"Incompatible mklittlefs version {version} removed (required: 3.x)")
except (json.JSONDecodeError, KeyError):
logger.exception("Error reading mklittlefs package metadata")
def _setup_mklittlefs_for_download(self) -> None:
"""Setup mklittlefs for download functionality with version 4.x."""
mklittlefs_dir = Path(self.packages_dir) / "tool-mklittlefs"
mklittlefs4_dir = Path(self.packages_dir) / "tool-mklittlefs4"
# Ensure mklittlefs 3.x is installed
if not mklittlefs_dir.exists():
self.install_tool("tool-mklittlefs")
if (mklittlefs_dir / "tools.json").exists():
self.install_tool("tool-mklittlefs")
# Install mklittlefs 4.x
if not mklittlefs4_dir.exists():
self.install_tool("tool-mklittlefs4")
if (mklittlefs4_dir / "tools.json").exists():
self.install_tool("tool-mklittlefs4")
# Copy mklittlefs 4.x over 3.x
if mklittlefs4_dir.exists():
# Copy 3.x package.json into 4.x before mirroring 4.x -> 3.x,
# so 3.x dir ends up with 4.x binaries and 3.x metadata.
package_src = mklittlefs_dir / "package.json"
package_dst = mklittlefs4_dir / "package.json"
safe_copy_file(package_src, package_dst)
shutil.copytree(mklittlefs4_dir, mklittlefs_dir, dirs_exist_ok=True)
self.packages.pop("tool-mkfatfs", None)
def _handle_littlefs_tool(self, for_download: bool) -> None:
"""Handle LittleFS tool installation with special download configuration."""
if for_download:
self._setup_mklittlefs_for_download()
else:
self._ensure_mklittlefs_version()
self.install_tool("tool-mklittlefs")
def _install_filesystem_tool(self, filesystem: str, for_download: bool = False) -> None:
"""Install filesystem-specific tools based on the filesystem type."""
tool_mapping = {
"default": lambda: self._handle_littlefs_tool(for_download),
"fatfs": lambda: self.install_tool("tool-mkfatfs"),
"spiffs": lambda: self.install_tool("tool-mkspiffs")
}
handler = tool_mapping.get(filesystem, tool_mapping["default"])
handler()
def _handle_dfuutil_tool(self, variables: Dict, for_download: bool = False) -> None:
def _handle_dfuutil_tool(self, variables: Dict) -> None:
"""Install dfuutil tool for Arduino Nano ESP32 board."""
# Currently only Arduino Nano ESP32 uses the dfuutil tool as uploader
if variables.get("board") == "arduino_nano_esp32":
board_config = self.board_config(variables.get("board"))
uploader = variables.get("board_upload.protocol", board_config.get("upload.protocol", "esptool"))
if uploader == "dfu":
self.install_tool("tool-dfuutil-arduino")
def _configure_filesystem_tools(self, variables: Dict, targets: List[str]) -> None:
"""Configure filesystem tools based on build targets and filesystem type."""
filesystem = variables.get("board_build.filesystem", "littlefs")
if any(target in targets for target in ["buildfs", "uploadfs"]):
self._install_filesystem_tool(filesystem, for_download=False)
if "downloadfs" in targets:
self._install_filesystem_tool(filesystem, for_download=True)
def setup_python_env(self, env):
"""Configure SCons environment with centrally managed Python executable paths."""
# Python environment is centrally managed in configure_default_packages
@@ -822,14 +752,12 @@ class Espressif32Platform(PlatformBase):
self._configure_arduino_framework(frameworks, mcu)
self._configure_espidf_framework(frameworks, variables, board_config, mcu)
self._configure_mcu_toolchains(mcu, variables, targets)
self._handle_littlefs_tool(for_download=False) # Ensure mklittlefs is installed
if "espidf" in frameworks:
self._install_common_idf_packages()
self._configure_rom_elfs_for_exception_decoder(variables)
self._configure_check_tools(variables)
self._configure_filesystem_tools(variables, targets)
self._handle_dfuutil_tool(variables)
logger.info("Package configuration completed successfully")