To extract a file extension in Arduino C, use the standard C library function strrchr(filename, '.') to locate the last dot in a null-terminated char array, or use filename.substring(filename.lastIndexOf('.') + 1) if you are using the Arduino String class. For SD card directory parsing where memory efficiency matters, the C-string approach via strrchr is the industry standard, provided you implement strict null-pointer and hidden-file checks.
Unlike Python or PHP, Arduino C++ does not have a built-in pathinfo() or os.path.splitext() function. When iterating through directories on a FAT32 SD card or ESP32 LittleFS, the native file objects return standard C-strings. Mishandling these pointers is the number one cause of hard crashes in embedded file-logging projects. This guide walks through the exact hardware setup, memory-safe C-string parsing, and the specific debugging steps required when your ESP32 throws a memory protection fault.
Hardware BOM and SPI Pin Mapping (ESP32 DevKit V1)
The code and logic in this guide specifically target the ESP32 DevKit V1 (38-pin variant, NodeMCU-32S architecture) running the Arduino Core for ESP32 (v3.x). The ESP32 is chosen here over the ATmega328P (Arduino Uno) because its 520KB of SRAM and native SPI DMA make it the standard for high-throughput SD card data logging in 2026.
Bill of Materials
| Component | Exact Model / Variant | Qty | Specifications & Notes |
|---|---|---|---|
| Microcontroller | ESP32 DevKit V1 (38-pin) | 1 | NodeMCU-32S, Dual-core 240MHz, 520KB SRAM. (~$6.00) |
| Storage Module | Adafruit MicroSD Breakout (254) | 1 | Includes 3.3V LDO and level shifters. HW-125 clones work but often lack decoupling caps. (~$7.50) |
| MicroSD Card | SanDisk Ultra 16GB | 1 | Must be formatted as FAT32. SDHC (Class 10). Avoid SDXC (>32GB) for standard SD.h. (~$8.00) |
| Wiring | 24 AWG Silicone Wire | 6 | Keep SPI traces under 4 inches to prevent signal degradation at 20MHz+ clock speeds. |
| Bypass Capacitor | 100nF Ceramic (0.1µF) | 1 | Solder directly across VCC/GND on the SD module if using a cheap clone board. |
SPI Pin Mapping Table
| ESP32 GPIO Pin | SD Module Pin | Wire Color (Std) | Function / Notes |
|---|---|---|---|
| GPIO 23 (VSPI MOSI) | DI / MOSI | Blue | Master Out Slave In (Data to SD) |
| GPIO 19 (VSPI MISO) | DO / MISO | Green | Master In Slave Out (Data from SD) |
| GPIO 18 (VSPI SCK) | CLK / SCK | Yellow | SPI Clock Signal |
| GPIO 5 (VSPI CS) | CS / SS | Orange | Chip Select (Active LOW) |
| 3V3 | VCC / 3V3 | Red | Power (Ensure module has LDO if feeding 5V) |
| GND | GND | Black | Common Ground |
C-Strings vs. Arduino Strings for File Parsing
When you call file.name() using the standard Arduino SD library, it returns a const char* (a standard C-string). You have two choices for extracting the extension: stick to native C-strings or cast it to an Arduino String object. Here is how they compare in an embedded environment.
| Criteria | Native C-String (strrchr) |
Arduino String Object (lastIndexOf) |
|---|---|---|
| Memory Allocation | Zero dynamic allocation. Operates on existing stack/heap pointers. | Requires heap allocation. Creates a new String copy in SRAM. |
| Execution Speed | Extremely fast. Single pass from the end of the string backward. | Slower. Involves object instantiation, copying, and method overhead. |
| Fragmentation Risk | None. Safe for infinite loops and long-running data loggers. | High. Repeated allocation/deallocation causes heap fragmentation, leading to reboots. |
| Syntax Complexity | High. Requires manual pointer arithmetic and null-termination checks. | Low. Familiar dot-notation syntax for web developers. |
| Verdict | Use for production SD/LittleFS logging. | Use only for quick prototyping. |
Complete Compilable Code: SD Card File Extension Filter
The following code targets the ESP32 DevKit V1. It mounts a FAT32 SD card, iterates through the root directory, and uses a memory-safe custom function extractExtension() to isolate the file suffix. It includes explicit error handling for hidden files (like .Trashes) and files without extensions.
#include <SPI.h>
#include <SD.h>
// --- PIN DEFINITIONS (ESP32 DevKit V1 VSPI) ---
#define PIN_SD_MOSI 23
#define PIN_SD_MISO 19
#define PIN_SD_SCK 18
#define PIN_SD_CS 5
// Buffer size for extension (e.g., "jpeg" + null terminator = 5)
#define EXT_BUFFER_SIZE 8
/**
* Safely extracts the file extension from a C-string filename.
* Returns true if a valid extension was found, false otherwise.
*/
bool extractExtension(const char* filename, char* extBuffer, size_t bufferSize) {
if (filename == NULL || extBuffer == NULL || bufferSize == 0) {
return false; // Prevent null pointer dereference
}
// Find the last occurrence of the dot character
const char* dot = strrchr(filename, '.');
// Check 1: No dot found in the entire string
if (dot == NULL) {
extBuffer[0] = '\0';
return false;
}
// Check 2: Dot is the very first character (e.g., hidden file ".gitignore")
if (dot == filename) {
extBuffer[0] = '\0';
return false;
}
// Move pointer past the dot to the actual extension characters
const char* extStart = dot + 1;
// Check 3: Ensure the extension isn't longer than our buffer to prevent overflow
size_t extLen = strlen(extStart);
if (extLen >= bufferSize) {
// Truncate safely if extension is abnormally long
strncpy(extBuffer, extStart, bufferSize - 1);
extBuffer[bufferSize - 1] = '\0';
return true;
}
// Safe to copy
strcpy(extBuffer, extStart);
return true;
}
void setup() {
Serial.begin(115200);
while(!Serial) { delay(10); }
Serial.println("\n--- ESP32 SD Card Extension Parser ---");
// Initialize SPI and SD Card
SPI.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS);
if (!SD.begin(PIN_SD_CS)) {
Serial.println("FATAL: SD Card Mount Failed. Check wiring and FAT32 format.");
while(1) { delay(1000); } // Halt execution
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE) {
Serial.println("No SD card attached.");
return;
}
File root = SD.open("/");
if(!root) {
Serial.println("Failed to open root directory.");
return;
}
if(!root.isDirectory()) {
Serial.println("Root is not a directory.");
return;
}
// Iterate through files
char extBuffer[EXT_BUFFER_SIZE];
File file = root.openNextFile();
while(file) {
const char* fName = file.name();
if (extractExtension(fName, extBuffer, EXT_BUFFER_SIZE)) {
Serial.printf("Found valid file: %s | Extension: %s\n", fName, extBuffer);
// Example: Filter only for CSV data logs
if (strcasecmp(extBuffer, "csv") == 0) {
Serial.println(" -> Processing CSV data log...");
}
} else {
Serial.printf("Skipped file (no ext/hidden): %s\n", fName);
}
file.close();
file = root.openNextFile();
}
root.close();
Serial.println("Directory scan complete.");
}
void loop() {
// Nothing to do in loop for this demonstration
delay(10000);
}
Debugging: Pointer Crashes and Parse Errors
When manipulating C-strings on the ESP32, a single missed boundary check will bypass the Arduino wrapper and trigger a hardware-level memory protection fault. If your serial monitor spits out the following exact error string, your extension parser has failed:
Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.Core 1 register dump:PC: 0x400d1234 PS: 0x00060030 A0: 0x800d1234 A1: 0x3ffb1234
Alternatively, if you are using a custom logging wrapper, you might see a logical error string like: ERR_EXT_PARSE: Null pointer dereference at 0x00000001.
The First Three Things to Check When It Fails
- Check for NULL returns from
strrchr: TheLoadProhibitedcrash almost always happens when a file has no dot (e.g., a directory namedSYSTEM~1or a file namedREADME).strrchrreturnsNULL. If your next line of code isconst char* ext = dot + 1;, you are attempting to read memory address0x00000001, triggering the ESP32 hardware watchdog to panic and reboot. Always wrap the pointer increment in anif (dot != NULL)block. - Check for Hidden Files (Index 0 Dot): macOS and Linux create hidden files like
.DS_Storeor.Trashes. In these cases,strrchrfinds the dot, but it is the very first character of the string. If you parse this as an extension, your code will treat the entire filename as the extension. You must checkif (dot == filename)and reject it. - Check Destination Buffer Overflows: If you use
strcpy(extBuffer, extStart)without checking the length ofextStart, a corrupted FAT32 directory entry returning a 20-character garbage string will overwrite adjacent stack variables. This causes erratic behavior, Wi-Fi drops, or delayed reboots. Always usestrncpyor enforce a length check against your buffer size, as demonstrated in the code above.
For deeper debugging on ESP32 memory faults, utilize the Espressif Arduino Core documentation to decode the stack trace using the esp32_exception_decoder tool in PlatformIO.
Extending and Simplifying the Build
How to Simplify (The RAM Trade-off)
If you are building a quick prototype and do not care about heap fragmentation, you can entirely bypass C-string pointer math by casting the filename to an Arduino String object. This reduces the extraction logic to a single, highly readable line:
String fName = String(file.name());
int dotIndex = fName.lastIndexOf('.');
if (dotIndex > 0) {
String ext = fName.substring(dotIndex + 1);
Serial.println(ext);
}
Warning: Do not use this simplification in a while() loop that runs thousands of times (like a continuous data logger). The repeated creation and destruction of String objects will fragment the ESP32's heap, eventually causing an alloc failed panic after a few hours of uptime.
How to Extend (MIME-Type Mapping for Web Servers)
If you are building an ESP32 web server that serves files from the SD card, the extracted extension is the key to setting the correct HTTP Content-Type header. You can extend the build by passing the extracted extBuffer into a lookup function:
const char* getMimeType(const char* ext) {
if (strcasecmp(ext, "htm") == 0 || strcasecmp(ext, "html") == 0) return "text/html";
if (strcasecmp(ext, "css") == 0) return "text/css";
if (strcasecmp(ext, "js") == 0) return "application/javascript";
if (strcasecmp(ext, "json") == 0) return "application/json";
if (strcasecmp(ext, "png") == 0) return "image/png";
if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0) return "image/jpeg";
return "application/octet-stream"; // Default fallback for binary/unknown
}
By mastering strrchr and implementing strict boundary checks, you ensure your embedded file systems remain stable, memory-efficient, and immune to the pointer crashes that plague beginner SD card projects. Always verify your FAT32 formatting and keep your SPI wiring short to maintain signal integrity at the hardware level.






