Using the RP2040/RP2350's PIO peripheral, SDIO support has been added to the SdFat library. The Adafruit Fork of the SdFat library has merged in these changes as well.
SDIO provides a generally faster transfer rate than using generic SPI, but it has some special requirements:
- Four consecutive GPIO pins of the RP2040/RP2350 must be used.
-
The SD card itself must be wired for SDIO support.
- The PID 254 SD breakout does NOT support SDIO
- The PID 2922 Adalogger FeatherWing does NOT support SDIO
- The PID 4682 SD breakout does support SDIO
- Adafruit RP2040/RP2350 based Metros and Feather Adaloggers with an SD card slot support SDIO
- Adafruit Fruit Jam - Mini RP2350 Computer does support SDIO.
The SDIO support shown here is for RP2040 or RP2350 based boards only.
Library Installation
You can install the Adafruit Fork of the SDFat library for Arduino using the Library Manager in the Arduino IDE.
Click the Manage Libraries ... menu item, search for Adafruit SDFat and select the SDFat - Adafruit Fork library:
// RP2040/RP2350 PIO SDIO setup and test.
/*
This example requires a SDIO Card socket with the following six lines.
CLK - A clock signal sent to the card by the MCU.
CMD - A bidirectional line for for commands and responses.
DAT[0:3] - Four bidirectional lines for data transfer.
CLK and CMD can be connected to any GPIO pins. DAT[0:3] can be connected
to any four consecutive GPIO pins in the order DAT0, DAT1, DAT2, DAT3.
For testing, I use several RP2040/RP2350 boards.
These Adafruit boards have a builtin SDIO socket.
https://learn.adafruit.com/adafruit-metro-rp2040
https://learn.adafruit.com/adafruit-metro-rp2350
https://learn.adafruit.com/adafruit-feather-rp2040-adalogger
The Feather RP2350 Adalogger is coming soon.
I use this SD socket breakout board for other boards.
https://learn.adafruit.com/adafruit-microsd-spi-sdio
Wires should be short since signals can be as faster than 50 MHz.
*/
#ifndef DISABLE_FS_H_WARNING
#define DISABLE_FS_H_WARNING // Disable warning for type File not defined.
#endif
#include "SdFat_Adafruit_Fork.h"
//------------------------------------------------------------------------------
// Example GPIO definitions I use for debug. Edit for your setup.
// Run this example as is to print the symbol for your variant.
//
#if defined(HAS_BUILTIN_PIO_SDIO)
// Note: fourth paramter of SdioConfig is the PIO clkDiv with default 1.00.
#define SD_CONFIG SdioConfig(PIN_SD_CLK, PIN_SD_CMD_MOSI, PIN_SD_DAT0_MISO)
#elif defined(ARDUINO_RASPBERRY_PI_PICO) || defined(ARDUINO_RASPBERRY_PI_PICO_2)
// CLK: GPIO10, CMD: GPIO11, DAT[0,3]: GPIO[12, 15].
#define SD_CONFIG SdioConfig(10u, 11u, 12u)
#elif defined(ARDUINO_ADAFRUIT_FEATHER_RP2350_HSTX)
// CLK: GPIO10, CMD: GPIO11, DAT[0,3]: GPIO[22, 25].
#define SD_CONFIG SdioConfig(10u, 11u, 22u)
#else // defined(ARDUINO_ARCH_RP2040)
#warning "Undefined SD_CONFIG. Run this program for the Variant Symbol."
#endif // defined(ARDUINO_ARCH_RP2040)
//------------------------------------------------------------------------------
// Class File is not defined by SdFat since the RP2040 system defines it.
// 1 for FAT16/FAT32, 2 for exFAT, 3 for FAT16/FAT32 and exFAT.
#define SD_FAT_TYPE 3
#if SD_FAT_TYPE == 1
SdFat32 sd;
File32 file;
#elif SD_FAT_TYPE == 2
SdExFat sd;
ExFile file;
#elif SD_FAT_TYPE == 3
SdFs sd;
FsFile file;
#else // SD_FAT_TYPE
#error Invalid SD_FAT_TYPE
#endif // SD_FAT_TYPE
void setup() {
Serial.begin(9600);
while (!Serial) {
yield();
}
Serial.println("Type any character to start\n");
while (!Serial.available()) {
yield();
}
Serial.printf("Variant Symbol: ARDUINO_%s\n\n", BOARD_NAME);
#if defined(SD_CONFIG)
SdioConfig cfg = SD_CONFIG;
Serial.printf("clkPin: %d, cmdPin: %d, dat0Pin: %d, clkDiv: %4.2f\n",
cfg.clkPin(), cfg.cmdPin(), cfg.dat0Pin(), cfg.clkDiv());
if (!sd.begin(SD_CONFIG)) {
sd.initErrorHalt(&Serial);
}
Serial.println("Card successfully initialized.");
Serial.println("\nls:");
sd.ls(LS_A | LS_DATE | LS_SIZE); // Add LS_R for recursive list.
Serial.println("\nDone! Try the bench example next.");
#else // #if defined(SD_CONFIG)
Serial.println("Error: SD_CONFIG undefined for your board.");
Serial.println("Define clkPin, cmdPin, and dat0Pin above.");
#endif
}
void loop() {}
Upload the sketch to your board and open up the Serial Monitor (Tools -> Serial Monitor) at 9600 baud. Press <ENTER> in the input area of the Serial Monitor:
If successful, the board name will be shown (variant symbol) as well as a listing of the SD card contents.
Speed Test
The bench example in the SDFat library can be used to benchmark the resulting speed of SD card access.
/*
* This program is a simple binary write/read benchmark.
*/
#ifndef DISABLE_FS_H_WARNING
#define DISABLE_FS_H_WARNING // Disable warning for type File not defined.
#endif // DISABLE_FS_H_WARNING
#include "SdFat.h"
#include "FreeStack.h"
#include "sdios.h"
// SD_FAT_TYPE = 0 for SdFat/File as defined in SdFatConfig.h,
// 1 for FAT16/FAT32, 2 for exFAT, 3 for FAT16/FAT32 and exFAT.
#if defined __has_include
#if __has_include(<FS.h>)
#define SD_FAT_TYPE 3 // Can't use SdFat/File
#endif // __has_include(<FS.h>)
#endif // defined __has_include
#ifndef SD_FAT_TYPE
#define SD_FAT_TYPE 0 // Use SdFat/File
#endif // SD_FAT_TYPE
/*
Change the value of SD_CS_PIN if you are using SPI and
your hardware does not use the default value, SS.
Common values are:
Arduino Ethernet shield: pin 4
Sparkfun SD shield: pin 8
Adafruit SD shields and modules: pin 10
*/
// SDCARD_SS_PIN is defined for the built-in SD on some boards.
#ifndef SDCARD_SS_PIN
const uint8_t SD_CS_PIN = SS;
#else // SDCARD_SS_PIN
// Assume built-in SD is used.
const uint8_t SD_CS_PIN = SDCARD_SS_PIN;
#endif // SDCARD_SS_PIN
// Try max SPI clock for an SD. Reduce SPI_CLOCK if errors occur.
#define SPI_CLOCK SD_SCK_MHZ(50)
// Try to select the best SD card configuration.
#if defined(HAS_TEENSY_SDIO)
#define SD_CONFIG SdioConfig(FIFO_SDIO)
#elif defined(HAS_BUILTIN_PIO_SDIO)
// See the Rp2040SdioSetup example for boards without a builtin SDIO socket.
#define SD_CONFIG SdioConfig(PIN_SD_CLK, PIN_SD_CMD_MOSI, PIN_SD_DAT0_MISO)
// Definitions for my Pico debug tests when zero and // are removed.
#elif 0 // defined(ARDUINO_RASPBERRY_PI_PICO) || defined(ARDUINO_RASPBERRY_PI_PICO_2)
// CLK: GPIO10, CMD: GPIO11, DAT[0,3]: GPIO[12, 15].
#define SD_CONFIG SdioConfig(10u, 11u, 12u)
#elif ENABLE_DEDICATED_SPI
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SPI_CLOCK)
#else // HAS_TEENSY_SDIO
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, SHARED_SPI, SPI_CLOCK)
#endif // HAS_TEENSY_SDIO
// Set PRE_ALLOCATE true to pre-allocate file clusters.
const bool PRE_ALLOCATE = true;
// Set SKIP_FIRST_LATENCY true if the first read/write to the SD can
// be avoid by writing a file header or reading the first record.
const bool SKIP_FIRST_LATENCY = true;
// Size of read/write.
const size_t BUF_SIZE = 512;
// File size in MB where MB = 1,000,000 bytes.
const uint32_t FILE_SIZE_MB = 5;
// Write pass count.
const uint8_t WRITE_COUNT = 2;
// Read pass count.
const uint8_t READ_COUNT = 2;
// Full read verify - will require twice as much buffer memory.
#define FULL_READ_VERIFY false
//==============================================================================
// End of configuration constants.
//------------------------------------------------------------------------------
// File size in bytes.
const uint64_t FILE_SIZE = 1000000ULL * FILE_SIZE_MB;
// Insure 4-byte alignment.
uint32_t buf32[(BUF_SIZE + 3) / 4];
uint8_t* buf = (uint8_t*)buf32;
#if SD_FAT_TYPE == 0
SdFat sd;
File file;
#elif SD_FAT_TYPE == 1
SdFat32 sd;
File32 file;
#elif SD_FAT_TYPE == 2
SdExFat sd;
ExFile file;
#elif SD_FAT_TYPE == 3
SdFs sd;
FsFile file;
#else // SD_FAT_TYPE
#error Invalid SD_FAT_TYPE
#endif // SD_FAT_TYPE
// Serial output stream
ArduinoOutStream cout(Serial);
//------------------------------------------------------------------------------
// Store error strings in flash to save RAM.
#define error(s) sd.errorHalt(&Serial, F(s))
//------------------------------------------------------------------------------
void cidDmp() {
cid_t cid;
if (!sd.card()->readCID(&cid)) {
error("readCID failed");
}
cout << F("\nManufacturer ID: ");
cout << uppercase << showbase << hex << int(cid.mid) << dec << endl;
cout << F("OEM ID: ") << cid.oid[0] << cid.oid[1] << endl;
cout << F("Product: ");
for (uint8_t i = 0; i < 5; i++) {
cout << cid.pnm[i];
}
cout << F("\nRevision: ") << cid.prvN() << '.' << cid.prvM() << endl;
cout << F("Serial number: ") << hex << cid.psn() << dec << endl;
cout << F("Manufacturing date: ");
cout << cid.mdtMonth() << '/' << cid.mdtYear() << endl;
cout << endl;
}
//------------------------------------------------------------------------------
void clearSerialInput() {
uint32_t m = micros();
do {
if (Serial.read() >= 0) {
m = micros();
}
} while (micros() - m < 10000);
}
//------------------------------------------------------------------------------
void setup() {
Serial.begin(9600);
// Wait for USB Serial
while (!Serial) {
yield();
}
delay(1000);
cout << F("\nUse a freshly formatted SD for best performance.\n");
if (!ENABLE_DEDICATED_SPI) {
cout << F(
"\nSet ENABLE_DEDICATED_SPI nonzero in\n"
"SdFatConfig.h for best SPI performance.\n");
}
if (!SD_HAS_CUSTOM_SPI && !USE_SPI_ARRAY_TRANSFER && isSpi(SD_CONFIG)) {
cout << F(
"\nSetting USE_SPI_ARRAY_TRANSFER nonzero in\n"
"SdFatConfig.h may improve SPI performance.\n");
}
// use uppercase in hex and use 0X base prefix
cout << uppercase << showbase << endl;
}
//------------------------------------------------------------------------------
void loop() {
float s;
uint32_t t;
uint32_t maxLatency;
uint32_t minLatency;
uint32_t totalLatency;
bool skipLatency;
// Discard any input.
clearSerialInput();
// F() stores strings in flash to save RAM
cout << F("Type any character to start\n");
while (!Serial.available()) {
yield();
}
#if HAS_UNUSED_STACK
cout << F("FreeStack: ") << FreeStack() << endl;
#endif // HAS_UNUSED_STACK
if (!sd.begin(SD_CONFIG)) {
sd.initErrorHalt(&Serial);
}
if (sd.fatType() == FAT_TYPE_EXFAT) {
cout << F("Type is exFAT") << endl;
} else {
cout << F("Type is FAT") << int(sd.fatType()) << endl;
}
cout << F("Card size: ") << sd.card()->sectorCount() * 512E-9;
cout << F(" GB (GB = 1E9 bytes)") << endl;
cidDmp();
// open or create file - truncate existing file.
if (!file.open("bench.dat", O_RDWR | O_CREAT | O_TRUNC)) {
error("open failed");
}
// fill buf with known data
if (BUF_SIZE > 1) {
for (size_t i = 0; i < (BUF_SIZE - 2); i++) {
buf[i] = 'A' + (i % 26);
}
buf[BUF_SIZE - 2] = '\r';
}
buf[BUF_SIZE - 1] = '\n';
cout << F("FILE_SIZE_MB = ") << FILE_SIZE_MB << endl;
cout << F("BUF_SIZE = ") << BUF_SIZE << F(" bytes\n");
cout << F("Starting write test, please wait.") << endl << endl;
// do write test
uint32_t n = FILE_SIZE / BUF_SIZE;
cout << F("write speed and latency") << endl;
cout << F("speed,max,min,avg") << endl;
cout << F("KB/Sec,usec,usec,usec") << endl;
for (uint8_t nTest = 0; nTest < WRITE_COUNT; nTest++) {
file.truncate(0);
if (PRE_ALLOCATE) {
if (!file.preAllocate(FILE_SIZE)) {
error("preAllocate failed");
}
}
maxLatency = 0;
minLatency = 9999999;
totalLatency = 0;
skipLatency = SKIP_FIRST_LATENCY;
t = millis();
for (uint32_t i = 0; i < n; i++) {
uint32_t m = micros();
if (file.write(buf, BUF_SIZE) != BUF_SIZE) {
error("write failed");
}
m = micros() - m;
totalLatency += m;
if (skipLatency) {
// Wait until first write to SD, not just a copy to the cache.
skipLatency = file.curPosition() < 512;
} else {
if (maxLatency < m) {
maxLatency = m;
}
if (minLatency > m) {
minLatency = m;
}
}
}
file.sync();
t = millis() - t;
// Remove any unused space in the file.
file.truncate();
s = file.fileSize();
cout << s / t << ',' << maxLatency << ',' << minLatency;
cout << ',' << totalLatency / n << endl;
}
cout << endl << F("Starting read test, please wait.") << endl;
cout << endl << F("read speed and latency") << endl;
cout << F("speed,max,min,avg") << endl;
cout << F("KB/Sec,usec,usec,usec") << endl;
// do read test
#if FULL_READ_VERIFY
uint8_t cmp[BUF_SIZE];
memcpy(cmp, buf, BUF_SIZE);
#endif // FULL_READ_VERIFY
for (uint8_t nTest = 0; nTest < READ_COUNT; nTest++) {
file.rewind();
maxLatency = 0;
minLatency = 9999999;
totalLatency = 0;
skipLatency = SKIP_FIRST_LATENCY;
t = millis();
for (uint32_t i = 0; i < n; i++) {
buf[BUF_SIZE - 1] = 0;
uint32_t m = micros();
int32_t nr = file.read(buf, BUF_SIZE);
if (nr != BUF_SIZE) {
error("read failed");
}
m = micros() - m;
totalLatency += m;
#if FULL_READ_VERIFY
if (memcmp(buf, cmp, BUF_SIZE)) {
#else // FULL_READ_VERIFY
if (buf[BUF_SIZE - 1] != '\n') {
#endif // FULL_READ_VERIFY
error("data check error");
}
if (skipLatency) {
skipLatency = false;
} else {
if (maxLatency < m) {
maxLatency = m;
}
if (minLatency > m) {
minLatency = m;
}
}
}
s = file.fileSize();
t = millis() - t;
cout << s / t << ',' << maxLatency << ',' << minLatency;
cout << ',' << totalLatency / n << endl;
}
cout << endl << F("Done") << endl;
file.close();
sd.end();
}
Upload the sketch to your board and open up the Serial Monitor (Tools -> Serial Monitor) at 9600 baud. Press in the input area of the Serial Monitor:
The sketch will then perform various read/write access tests and output the results. Different setups will have different values, but the SDIO results should be faster than the SPI results.
Here we show example output comparing SDIO speed vs SPI speed:
The results indicate that SDIO is ~5x faster than SPI.
Page last edited July 30, 2025
Text editor powered by tinymce.