LittleFS integration with LVGL v8.3.11

Description

Call to lv_fs_drv_register(&fs_drv) is crashing and causing a reboot loop.

What MCU/Processor/Board and compiler are you using?

ESP32S3 within an Elecrow CrowPanel 7.0-inch. Compiler is Arduino 2 (via the Visual Micro Extension for Visual Studio 2022).

What LVGL version are you using?

8.3.11. Later versions seem to be incompatible with my hardware.

What do you want to achieve?

I am trying to integrate LittleFS v2.8.1 with my version of LVGL. This version of LittleFS was chosen as it was released very close to when LVGL 8.3.11 was released. I am attempting this integration so that I am able to load images into LVGL from the data partition of my flash rather than compiling the images directly into my program. When the images are compiled directly into my program, the program size exceeds the size of my program partition.

What have you tried so far?

  1. I’ve created a minimal Arduino sketch that configures LittleFS, including all block-level-operation callbacks.
  2. I’ve confirmed that LittleFS successfully mounts.
  3. I’ve configured the File System Driver, including all operation callbacks.
  4. I’ve attempted to register the File System Driver with LVGL.

Step 4 is where a fatal crash occurs.

Code to reproduce

#include "EnvironmentManager.h" // Contains #includes for lvgl, lgfx, lfs, esp_partition, etc.

const esp_partition_t* littlefs_partition = NULL;
lv_fs_drv_t fs_drv;
lfs_t lfs;
lfs_file_t file;
bool fileSystemMounted = false;

void dumpPartitions() {
	// Create an iterator that finds any partition.
	esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
	if (it == NULL) {
		Serial.println("No partitions found!");
		return;
	}

	Serial.println("Partitions found in flash:");
	while (it != NULL) {
		const esp_partition_t* part = esp_partition_get(it);
		if (part) {
			Serial.printf("Name: %-10s  Type: 0x%02X  Subtype: 0x%02X  Offset: 0x%08X  Size: 0x%08X\n",
				part->label, part->type, part->subtype, part->address, part->size);
		}
		it = esp_partition_next(it);
	}

	esp_partition_iterator_release(it);
}

// Helper function to initialize our flash partition pointer.  
// Call this before mounting LittleFS.
bool init_littlefs_partition() {
	Serial.println("Attempting to initialize littlefs partition...");

	esp_partition_iterator_t it = esp_partition_find(
		ESP_PARTITION_TYPE_DATA,
		ESP_PARTITION_SUBTYPE_DATA_SPIFFS,
		"spiffs"  // Using the label exactly as dumped.
	);
	if (it == NULL) {
		Serial.println("Partition iterator returned NULL.");
		return false;
	}

	const esp_partition_t* part = esp_partition_get(it);
	esp_partition_iterator_release(it);

	if (part == NULL) {
		Serial.println("Partition get returned NULL.");
		return false;
	}

	littlefs_partition = part;
	Serial.print("littlefs_partition found at address: 0x");
	Serial.println(littlefs_partition->address, HEX);
	return true;
}


// The read callback: read data from flash into 'buffer'
int lfs_read(const struct lfs_config* c, lfs_block_t block, lfs_off_t off, void* buffer, lfs_size_t size) {
	// Calculate flash address:  
	// block * block_size + offset
	uint32_t addr = block * c->block_size + off;
	esp_err_t err = esp_partition_read(littlefs_partition, addr, buffer, size);

	if (err != ESP_OK) {
		return LFS_ERR_IO;
	}
	return 0;
}

// The erase callback: erase an entire block
int lfs_erase(const struct lfs_config* c, lfs_block_t block) {
	// For erase operations, start address is block number * block_size.
	uint32_t addr = block * c->block_size;
	// Erase the block-sized region.  
	// Note: Many flash devices have a specific erase size that might differ from c->block_size.
	esp_err_t err = esp_partition_erase_range(littlefs_partition, addr, c->block_size);

	if (err != ESP_OK) {
		return LFS_ERR_IO;
	}
	return 0;
}

// The program (write) callback: write data from 'buffer' to flash
int lfs_prog(const struct lfs_config* c, lfs_block_t block, lfs_off_t off, const void* buffer, lfs_size_t size) {
	uint32_t addr = block * c->block_size + off;
	esp_err_t err = esp_partition_write(littlefs_partition, addr, buffer, size);

	if (err != ESP_OK) {
		return LFS_ERR_IO;
	}
	return 0;
}

// The sync callback: flush any cached data. Often a no-op.
int lfs_sync(const struct lfs_config* c) {
	// For many flash devices, writes go through immediately so there's nothing extra to do.
	return 0;
}

bool fs_ready_cb(_lv_fs_drv_t* drv) {
	Serial.println("fs_ready_cb() called.");
	return fileSystemMounted;
}

void* fs_open_cb(lv_fs_drv_t* drv, const char* path, lv_fs_mode_t mode) {
	Serial.println("fs_open_cb() called on Path:");
	Serial.println(path);
	// Allocate memory for an lfs_file_t object.
	lfs_file_t* file = new lfs_file_t;
	int flags = 0;

	// Map the LVGL mode to LittleFS flags.
	if (mode == LV_FS_MODE_RD) {
		flags = LFS_O_RDONLY;
	}
	else if (mode == LV_FS_MODE_WR) {
		// Write-only; create the file if it doesn't exist.
		flags = LFS_O_WRONLY | LFS_O_CREAT;
	}
	else if (mode == (LV_FS_MODE_WR | LV_FS_MODE_RD)) {
		// Both read and write; create if needed.
		flags = LFS_O_RDWR | LFS_O_CREAT;
	}
	else {  // Fallback/default: read-only.
		flags = LFS_O_RDONLY;
	}

	// Attempt to open the file through LittleFS.
	int err = lfs_file_open(&lfs, file, path, flags);
	if (err < 0) {
		delete file;
		return NULL;  // Indicate failure.
	}

	return file;  // Return pointer to the lfs_file_t object.
}

lv_fs_res_t fs_close_cb(lv_fs_drv_t* drv, void* file_p) {
	Serial.println("fs_close_cb() called");
	lfs_file_t* file = (lfs_file_t*)file_p;
	int err = lfs_file_close(&lfs, file);
	delete file;
	return (err == 0) ? LV_FS_RES_OK : LV_FS_RES_FS_ERR;
}

lv_fs_res_t fs_read_cb(lv_fs_drv_t* drv, void* file_p, void* buf, uint32_t btr, uint32_t* br) {
	Serial.println("fs_read_cb() called");
	lfs_file_t* file = (lfs_file_t*)file_p;
	lfs_ssize_t res = lfs_file_read(&lfs, file, buf, btr);
	if (res < 0) {
		return LV_FS_RES_FS_ERR;
	}
	*br = (uint32_t)res;
	return LV_FS_RES_OK;
}

lv_fs_res_t fs_write_cb(lv_fs_drv_t* drv, void* file_p, const void* buf, uint32_t btw, uint32_t* bw) {
	Serial.println("fs_write_cb() called");
	lfs_file_t* file = (lfs_file_t*)file_p;
	lfs_ssize_t res = lfs_file_write(&lfs, file, buf, btw);
	if (res < 0) {
		return LV_FS_RES_FS_ERR;
	}
	*bw = (uint32_t)res;
	return LV_FS_RES_OK;
}

lv_fs_res_t fs_seek_cb(lv_fs_drv_t* drv, void* file_p, uint32_t pos, lv_fs_whence_t whence) {
	Serial.println("fs_seek_cb() called.");
	lfs_file_t* file = (lfs_file_t*)file_p;
	int origin = 0;
	// Map LVGL's seek definition to LittleFS values.
	// (Assuming LV_FS_SEEK_SET, LV_FS_SEEK_CUR, and LV_FS_SEEK_END are defined similarly to standard SEEK_SET, etc.)
	switch (whence) {
	case LV_FS_SEEK_SET: origin = LFS_SEEK_SET; break;
	case LV_FS_SEEK_CUR: origin = LFS_SEEK_CUR; break;
	case LV_FS_SEEK_END: origin = LFS_SEEK_END; break;
	default: origin = LFS_SEEK_SET; break;
	}
	int err = lfs_file_seek(&lfs, file, pos, origin);
	return (err >= 0) ? LV_FS_RES_OK : LV_FS_RES_FS_ERR;
}

lv_fs_res_t fs_tell_cb(lv_fs_drv_t* drv, void* file_p, uint32_t* pos_p) {
	Serial.println("fs_tell_cb() called.");
	lfs_file_t* file = (lfs_file_t*)file_p;
	lfs_soff_t pos = lfs_file_tell(&lfs, file);
	if (pos < 0)
		return LV_FS_RES_FS_ERR;
	*pos_p = (uint32_t)pos;
	return LV_FS_RES_OK;
}

void* fs_dir_open_cb(lv_fs_drv_t* drv, const char* path) {
	Serial.println("fs_dir_open_cb() called.");
	// Allocate memory for an lfs_dir_t object.
	lfs_dir_t* dir = new lfs_dir_t;
	int err = lfs_dir_open(&lfs, dir, path);
	if (err < 0) {
		delete dir;
		return NULL;  // Indicate failure.
	}
	return dir;  // Return pointer to the opened directory.
}

lv_fs_res_t fs_dir_read_cb(lv_fs_drv_t* drv, void* rddir_p, char* fn) {
	Serial.println("fs_dir_read_cb() called.");
	// Cast rddir_p to the directory type used by LittleFS.
	lfs_dir_t* dir = (lfs_dir_t*)rddir_p;

	// Declare a structure to hold file information.
	struct lfs_info info;

	// Read the next directory entry.
	// lfs_dir_read returns 0 when there are no more entries, or a negative error code.
	int res = lfs_dir_read(&lfs, dir, &info);

	if (res <= 0) {
		// No more entries (or an error occurred): indicate end-of-directory.
		fn[0] = '\0';
		return LV_FS_RES_OK;
	}

	// Copy the file name to the provided buffer.
	// Ensure we don't overflow the buffer (assume LV_FS_MAX_PATH_LENGTH is defined).
	strncpy(fn, info.name, LV_FS_MAX_PATH_LENGTH - 1);
	fn[LV_FS_MAX_PATH_LENGTH - 1] = '\0';  // Guarantee null termination.

	return LV_FS_RES_OK;
}

lv_fs_res_t fs_dir_close_cb(lv_fs_drv_t* drv, void* rddir_p) {
	Serial.println("fs_dir_close_cb() called.");
	lfs_dir_t* dir = *(lfs_dir_t**)rddir_p;
	lfs_dir_close(&lfs, dir);
	delete dir;
	return LV_FS_RES_OK;
}


// Configuration of the filesystem is provided by this struct
const struct lfs_config cfg = {
	// Block device operations
	.read = lfs_read,
	.prog = lfs_prog,
	.erase = lfs_erase,
	.sync = lfs_sync,

	// Block device configuration
	.read_size = 16,
	.prog_size = 16,
	.block_size = 4096,
	.block_count = 224,
	.block_cycles = 500,
	.cache_size = 16,
	.lookahead_size = 16,
};

void setup()
{  
	Serial.begin(115200);
	delay(2000); // Wait for the serial connection to initialize
	WiFi.mode(WIFI_STA);
	Serial.println("Setup beginning...");
	Serial.println("Attempting to read partition table...");
	delay(2000);
	dumpPartitions();
	delay(2000);
	bool partitionInitialized = init_littlefs_partition();	
	if (partitionInitialized) {
		// Mount the filesystem
		int err = lfs_mount(&lfs, &cfg);

		if (err) {
			Serial.println("Failed to mount LittleFS");
			fileSystemMounted = false;
		}
		else {
			Serial.println("LittleFS mounted successfully!");
			fileSystemMounted = true;
		}
	}
	if (fileSystemMounted) {
		// Initialize the file system driver
		memset(&fs_drv, 0, sizeof(fs_drv));
		lv_fs_drv_init(&fs_drv);
		fs_drv.letter = 'S';
		fs_drv.cache_size = 0; // 0: Don't use cache

		// File system event callbacks
		fs_drv.ready_cb = fs_ready_cb;				/* Callback to tell if the drive is ready to use */
		fs_drv.open_cb = fs_open_cb;				/* Callback to open a file */
		fs_drv.close_cb = fs_close_cb;				/* Callback to close a file */
		fs_drv.read_cb = fs_read_cb;				/* Callback to read a file */
		fs_drv.write_cb = fs_write_cb;				/* Callback to write a file */
		fs_drv.seek_cb = fs_seek_cb;				/* Callback to seek in a file (Move cursor) */
		fs_drv.tell_cb = fs_tell_cb;				/* Callback to tell the cursor position  */
		fs_drv.dir_open_cb = fs_dir_open_cb;		/* Callback to open directory to read its content */
		fs_drv.dir_read_cb = fs_dir_read_cb;        /* Callback to read a directory's content */
		fs_drv.dir_close_cb = fs_dir_close_cb;      /* Callback to close a directory */

		// Register the file system driver in LittlevGL
		Serial.println("Registering file system driver in LittleGL...");
		lv_fs_drv_register(&fs_drv); /******** CRASHES HERE ************/

		//tft.setup();
		lv_timer_handler();
		//pinMode(WIFI_AP_PIN, INPUT_PULLUP);	
	}
	//EnvironmentManager.init();
	//EnvironmentManager.checkWiFiStatus(&DisplayManager, &WebServerManager);
    
    Serial.println("Setup done.");
}

// The loop function runs over and over again until power down or reset
void loop() {
    lv_tick_inc(5);
    lv_timer_handler();
    delay(5);
}

Screenshot and/or video

I don’t have a screenshot or video. But here is the serial monitor output:

Setup beginning…
Attempting to read partition table…
Partitions found in flash:
Name: nvs Type: 0x01 Subtype: 0x02 Offset: 0x00009000 Size: 0x00005000
Name: otadata Type: 0x01 Subtype: 0x00 Offset: 0x0000E000 Size: 0x00002000
Name: app0 Type: 0x00 Subtype: 0x10 Offset: 0x00010000 Size: 0x00300000
Name: spiffs Type: 0x01 Subtype: 0x82 Offset: 0x00310000 Size: 0x000E0000
Name: coredump Type: 0x01 Subtype: 0x03 Offset: 0x003F0000 Size: 0x00010000
Attempting to initialize littlefs partition…
littlefs_partition found at address: 0x310000
D:\source\repos\Arduino\libraries\littlefs-2.8.1\lfs.c:4403:debug: Found older minor version v2.0 < v2.1
D:\source\repos\Arduino\libraries\littlefs-2.8.1\lfs.c:4474:debug: Found pending gstate 0x000002000000000000000000
LittleFS mounted successfully!
Registering file system driver in LittleGL…
Guru Meditation Error: Core 1 panic’ed (LoadProhibited). Exception was unhandled.