Skip to content

SD Card

The board includes a microSD (TF) card slot connected to the ESP32-S3 via SPI. There is one important wrinkle: the chip select signal is not wired to a direct GPIO pin. Instead, it routes through the CH422G IO expander (EXIO4), which means SD card access requires initializing the IO expander first. This is a consequence of the board’s dense peripheral layout and limited available GPIO, but it works reliably once you account for the initialization order.

SignalGPIO / PinDescription
MOSIIO11SPI data out to SD card
SCKIO12SPI clock
MISOIO13SPI data in from SD card
CSCH422G EXIO4Chip select (active low, via IO expander)

The correct startup order matters here. The IO expander must be alive and its EXIO4 pin configured as an output before the SPI SD card driver can assert chip select.

  1. Initialize I2C bus (IO8 SDA, IO9 SCL) for CH422G communication

  2. Initialize the CH422G IO expander and configure EXIO4 as output

  3. Assert CS low (EXIO4 LOW) to select the SD card

  4. Initialize the SPI bus on IO11/IO12/IO13

  5. Mount the SD card filesystem

#include <SD.h>
#include <SPI.h>
#include <ESP_IOExpander_Library.h>
#define SD_MOSI 11
#define SD_SCK 12
#define SD_MISO 13
#define SD_CS -1 // Managed by IO expander, not a direct GPIO
ESP_IOExpander_CH422G *expander;
void setup() {
Serial.begin(115200);
// Initialize IO expander first
expander = new ESP_IOExpander_CH422G(
(i2c_port_t)0, ESP_IO_EXPANDER_I2C_CH422G_ADDRESS, 8, 9);
expander->init();
expander->begin();
// Set SD CS pin via expander
expander->pinMode(4, OUTPUT); // EXIO4 = SD_CS
expander->digitalWrite(4, LOW); // Select SD card
// Initialize SPI and SD
SPI.begin(SD_SCK, SD_MISO, SD_MOSI);
if (SD.begin(SD_CS, SPI)) {
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card mounted: %lluMB\n", cardSize);
// List root directory
File root = SD.open("/");
while (File entry = root.openNextFile()) {
Serial.printf(" %s (%d bytes)\n", entry.name(), entry.size());
entry.close();
}
root.close();
} else {
Serial.println("SD Card mount failed");
}
}
void loop() {}

Once the card is mounted, standard Arduino SD library calls work as expected. Here is a minimal data logging example.

void logData(const char *filename, const char *data) {
File file = SD.open(filename, FILE_APPEND);
if (file) {
file.println(data);
file.close();
Serial.printf("Logged to %s\n", filename);
} else {
Serial.printf("Failed to open %s\n", filename);
}
}
String readFile(const char *filename) {
File file = SD.open(filename, FILE_READ);
if (!file) return "";
String content;
while (file.available()) {
content += (char)file.read();
}
file.close();
return content;
}

In ESP-IDF, use spi_bus_initialize() with the SPI pins and mount the SD card filesystem using esp_vfs_fat_sdspi_mount(). The CS pin management through the CH422G requires initializing the IO expander driver separately before the SPI SD host configuration. The VFS mount integrates the SD card into the POSIX file system, so standard fopen(), fwrite(), and fread() calls work transparently.

Card Types

Standard microSD and microSDHC cards are supported in SPI mode. SDXC cards may work if formatted as FAT32, but the SPI mode interface limits maximum throughput compared to native SDIO.

Filesystems

FAT16 and FAT32 are natively supported by both the Arduino SD library and ESP-IDF’s FATFS component. exFAT support requires additional library configuration and is not enabled by default.

Mount Failure

The most common cause is forgetting to initialize the CH422G IO expander before calling SD.begin(). Verify the IO expander is responding on I2C (address 0x20) and that EXIO4 is configured as an output pulled LOW.

Slow Performance

SPI mode is inherently slower than native SDIO. For best throughput, use large block reads/writes rather than byte-at-a-time operations. Also ensure you are using a Class 10 or UHS-I card — older, slower cards will bottleneck the interface.