2.14 SD Card File Manager

Browse, navigate, and manage files on the SD card through a graphical file explorer.

Overview

This example creates a complete file manager for the SD card. You can browse folders, view file details (name, size), delete files, and navigate through the directory tree — all from the touchscreen, no computer needed.

What You’ll Learn

  • Building a file system explorer with the LVGL list widget

  • Implementing directory navigation with a breadcrumb path display

  • Displaying file metadata (name, size, path)

  • Deleting files from the SD card through the UI

Before You Begin

Complete the setup steps in Preparation before continuing.

Note

Use the recommended Arduino IDE settings in Recommended Board Settings unless this lesson says otherwise.

Open the example sketch from the code package:

LAFVIN-ESP32-S3-Multimedia-Kit-main/2.LVGL/14_LVGL_File_Manager/14_LVGL_File_Manager.ino

Note

You need a Micro SD card formatted as FAT32 inserted. The SD card from 1.4 SD Card (SDMMC) with test files will work perfectly.

Upload and Test

  1. Insert a FAT32-formatted Micro SD card.

  2. Connect the board with a USB Type-C data cable.

  3. Open 14_LVGL_File_Manager.ino in Arduino IDE.

  4. In the Tools menu, select ESP32S3 Dev Module as the board and choose the correct serial port.

  5. Click Upload.

  6. After uploading, the screen shows:

    • A path breadcrumb at the top (e.g., Path: /music/)

    • A scrollable list of files and folders with icons and file sizes

    • A status bar at the bottom showing item count and storage usage

    Tapping a folder navigates into it. Tapping a file opens a dialog with its name, size, path, and type — plus a Delete button to remove it. Tap the exit button to navigate up to the parent directory.

LVGL File Manager

How the Code Works

The file manager UI logic is mainly implemented in File_Manager_ui.cpp.

SD Card Initialisation

The SD card is configured with the standard ESP32-S3 SDMMC pinout (CLK=39, CMD=38, D0=40). The init_sd_card() function sets these pins, mounts the filesystem, and returns early if the card is already ready.

bool init_sd_card(void) {
  if (s_sd_ready) {
    return true;
  }

  Serial.println("Initializing SD card for file manager...");
  SD_MMC.setPins(kSdClkPin, kSdCmdPin, kSdD0Pin);

  s_sd_ready = SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_DEFAULT, 5);
  if (!s_sd_ready) {
    Serial.println("SD card mount failed.");
  } else {
    Serial.println("SD card ready.");
  }
  return s_sd_ready;
}

Directory Navigation

The navigate_to() function normalises the target path, opens it as a directory, and refreshes the file list. It returns false if the path is null, empty, or not a valid directory.

bool navigate_to(const char *path) {
  if (path == nullptr || path[0] == '\0') {
    return false;
  }

  const String normalized = normalize_path(path, true);
  File dir = SD_MMC.open(normalized.c_str());
  if (!dir || !dir.isDirectory()) {
    if (dir) {
      dir.close();
    }
    return false;
  }

  copy_string(s_current_path, sizeof(s_current_path), normalized);
  dir.close();
  refresh_file_list();
  return true;
}

The navigate_to_parent() function strips the last path segment and calls navigate_to(), or returns to root if already at the top level.

File Listing

The refresh_file_list() function clears the list widget, adds a .. parent entry when not at root, then iterates over the directory entries. Each entry is stored in a FileEntry struct array (up to 48 entries) and rendered as a list button with the appropriate icon and file size.

s_entry_count = 0;

if (strcmp(s_current_path, "/") != 0) {
  lv_obj_t *parent_btn = lv_list_add_btn(g_file_manager_ui.list, LV_SYMBOL_LEFT, "..");
  lv_obj_add_event_cb(
    parent_btn,
    file_item_event_handler,
    LV_EVENT_CLICKED,
    reinterpret_cast<void *>(static_cast<uintptr_t>(kParentEntryIndex)));
}

File file = dir.openNextFile();
while (file && s_entry_count < kMaxEntries) {
  const String raw_name = String(file.name());
  const String entry_name = basename_from_path(normalize_path(raw_name, false));
  String full_path;
  if (strcmp(s_current_path, "/") == 0) {
    full_path = normalize_path("/" + entry_name, file.isDirectory());
  } else {
    full_path = normalize_path(String(s_current_path) + "/" + entry_name, file.isDirectory());
  }
  copy_string(s_entries[s_entry_count].name, sizeof(s_entries[s_entry_count].name), entry_name);
  copy_string(s_entries[s_entry_count].path, sizeof(s_entries[s_entry_count].path), full_path);
  s_entries[s_entry_count].is_dir = file.isDirectory();
  s_entries[s_entry_count].size_bytes = static_cast<size_t>(file.size());

  const char *symbol = s_entries[s_entry_count].is_dir ? LV_SYMBOL_DIRECTORY : LV_SYMBOL_FILE;
  lv_obj_t *btn = lv_list_add_btn(g_file_manager_ui.list, symbol, s_entries[s_entry_count].name);
  lv_obj_add_event_cb(
    btn,
    file_item_event_handler,
    LV_EVENT_CLICKED,
    reinterpret_cast<void *>(static_cast<uintptr_t>(s_entry_count)));

  if (!s_entries[s_entry_count].is_dir) {
    lv_obj_t *btn_label = lv_obj_get_child(btn, 1);
    if (btn_label != nullptr) {
      char size_text[24];
      format_size(s_entries[s_entry_count].size_bytes, size_text, sizeof(size_text));
      lv_label_set_text_fmt(btn_label, "%s  (%s)", s_entries[s_entry_count].name, size_text);
    }
  }

  ++s_entry_count;
  file.close();
  file = dir.openNextFile();
}

The bottom status bar is then updated with the item count and storage usage (Items: N  |  Storage: used / total MB).

File Deletion

The delete_event_handler() is invoked when the user taps the Delete button in the file dialog. It removes the selected file from the SD card and refreshes the list.

void delete_event_handler(lv_event_t *e) {
  if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
    return;
  }

  if (s_selected_file_path[0] == '\0') {
    hide_dialog();
    return;
  }

  if (SD_MMC.remove(s_selected_file_path)) {
    char status_buffer[96];
    snprintf(status_buffer, sizeof(status_buffer), "Deleted: %s", s_selected_file_name);
    set_status_text(status_buffer);
  } else {
    char status_buffer[96];
    snprintf(status_buffer, sizeof(status_buffer), "Delete failed: %s", s_selected_file_name);
    set_status_text(status_buffer);
  }

  hide_dialog();
  refresh_file_list();
}

In the provided sketch:

  • file_item_event_handler() dispatches list-item taps: folders call navigate_to(), files call show_file_dialog(), and .. calls navigate_to_parent()

  • show_file_dialog() displays file metadata (type, size, path) in a popup and disables the Delete button for directories

  • The exit button in the header calls navigate_to_parent(), moving up one directory level

  • File sizes are formatted by format_size(), which chooses B, KB, or MB units automatically

Try This Next

  • Add a “New Folder” button to create directories from the touchscreen

  • Display file modification dates alongside sizes

  • Add drag-to-scroll for long file lists