ESP32/components/sd_card/sd_card.c

518 lines
18 KiB
C

/*
* sd_card.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "sd_card.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
#include "esp_vfs_fat.h"
#include "ff.h"
#include "sdmmc_cmd.h"
#include <string.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <fcntl.h>
#include <dirent.h>
/* Format bytes as human-readable (e.g. 1.2K, 4.5M). Writes into buf, max len chars. */
static void fmt_size_human(size_t bytes, char *buf, size_t len) {
if (bytes < 1024) {
snprintf(buf, len, "%zu B", bytes);
} else if (bytes < 1024 * 1024) {
snprintf(buf, len, "%.1f K", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024 * 1024) {
snprintf(buf, len, "%.1f M", bytes / (1024.0 * 1024.0));
} else {
snprintf(buf, len, "%.1f G", bytes / (1024.0 * 1024.0 * 1024.0));
}
}
// Pin definitions for SparkFun microSD Transflash Breakout
// ESP32-C5: no SDMMC host, use SD SPI mode
// SparkFun in SPI: CLK, MOSI(DI), MISO(DO), CS, CD(optional)
// CONFIG_SD_CD_GPIO / CONFIG_SD_CD_ACTIVE_LOW from Kconfig (main/Kconfig.projbuild)
// CONFIG_SD_CD_ACTIVE_LOW is only defined when y; when n it is omitted from sdkconfig.h
#ifndef CONFIG_SD_CD_GPIO
#define CONFIG_SD_CD_GPIO (-1)
#endif
#if defined(CONFIG_IDF_TARGET_ESP32C5)
#define SDSPI_CLK_PIN GPIO_NUM_9
#define SDSPI_MOSI_PIN GPIO_NUM_10
#define SDSPI_MISO_PIN GPIO_NUM_8
#define SDSPI_CS_PIN GPIO_NUM_7
#define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1))
#define SDSPI_HOST_ID SPI2_HOST
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
#define SDSPI_CLK_PIN GPIO_NUM_14
#define SDSPI_MOSI_PIN GPIO_NUM_15
#define SDSPI_MISO_PIN GPIO_NUM_2
#define SDSPI_CS_PIN GPIO_NUM_13
#define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1))
#define SDSPI_HOST_ID SPI2_HOST
#else
#define SDSPI_CLK_PIN GPIO_NUM_14
#define SDSPI_MOSI_PIN GPIO_NUM_15
#define SDSPI_MISO_PIN GPIO_NUM_2
#define SDSPI_CS_PIN GPIO_NUM_13
#define SD_CD_PIN ((gpio_num_t)(CONFIG_SD_CD_GPIO >= 0 ? CONFIG_SD_CD_GPIO : -1))
#define SDSPI_HOST_ID SPI2_HOST
#endif
#include "driver/sdspi_host.h"
#include "driver/spi_common.h"
static const char *TAG = "sd_card";
static bool s_sd_card_mounted = false;
static bool s_cd_configured = false;
static bool s_spi_bus_inited = false;
static sdmmc_card_t *s_card = NULL;
static const char *s_mount_point = "/sdcard";
static uint64_t s_last_status_check_ms = 0;
static bool s_last_status_result = false;
#define SD_STATUS_CHECK_INTERVAL_MS 1000 /* Check card status at most once per second */
static void sd_card_cd_ensure_configured(void) {
if (!s_cd_configured && SD_CD_PIN >= 0) {
/* Limit shift to 0..63 so compiler does not warn; valid GPIOs are 0..48 */
const unsigned int cd_pin = (unsigned int)SD_CD_PIN & 0x3Fu;
gpio_config_t io = {
.pin_bit_mask = (1ULL << cd_pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&io) == ESP_OK) {
s_cd_configured = true;
}
}
}
esp_err_t sd_card_init(void) {
esp_err_t result = ESP_OK;
if (s_sd_card_mounted) {
ESP_LOGW(TAG, "SD card already initialized");
result = ESP_OK;
} else {
ESP_LOGI(TAG, "Initializing SD card via SPI...");
// Initialize SPI bus (required before sdspi mount)
spi_bus_config_t bus_cfg = {
.mosi_io_num = SDSPI_MOSI_PIN,
.miso_io_num = SDSPI_MISO_PIN,
.sclk_io_num = SDSPI_CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
result = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO);
if (result == ESP_OK) {
s_spi_bus_inited = true;
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SDSPI_HOST_ID;
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SDSPI_CS_PIN;
slot_config.host_id = SDSPI_HOST_ID;
// Do not pass gpio_cd to driver: ESP-IDF expects LOW=inserted and blocks init if CD says no card.
// We use CD only for status (sd_card_cd_is_inserted) with configurable polarity.
slot_config.gpio_cd = SDSPI_SLOT_NO_CD;
slot_config.gpio_wp = SDSPI_SLOT_NO_WP;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
result = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card);
if (result != ESP_OK) {
if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false;
}
/* Reset status cache on init failure */
s_last_status_result = false;
s_last_status_check_ms = 0;
if (result == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem. "
"If you want the card to be formatted, set format_if_mount_failed = true.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card is inserted and wiring is correct.", esp_err_to_name(result));
}
} else {
sdmmc_card_print_info(stdout, s_card);
s_sd_card_mounted = true;
/* Reset status cache on successful mount */
s_last_status_result = true;
s_last_status_check_ms = esp_timer_get_time() / 1000;
ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point);
}
} else {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(result));
}
}
return result;
}
esp_err_t sd_card_deinit(void) {
esp_err_t result = ESP_OK;
if (s_sd_card_mounted) {
ESP_LOGI(TAG, "Unmounting SD card...");
result = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card);
if (result == ESP_OK) {
s_sd_card_mounted = false;
s_card = NULL;
/* Reset status cache on unmount */
s_last_status_result = false;
s_last_status_check_ms = 0;
if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false;
}
ESP_LOGI(TAG, "SD card unmounted successfully");
} else {
ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(result));
}
}
return result;
}
bool sd_card_is_ready(void) {
bool result = false;
/* If mounted, assume card is ready (since CD pin doesn't work) */
/* Status check is used to detect removal, but we're lenient about failures */
if (s_sd_card_mounted && s_card != NULL) {
/* Since CD pin doesn't work, probe the card periodically to detect removal */
/* Cache the result to avoid checking too frequently (SDMMC commands have overhead) */
uint64_t now_ms = esp_timer_get_time() / 1000;
uint64_t time_since_check = now_ms - s_last_status_check_ms;
if (time_since_check >= SD_STATUS_CHECK_INTERVAL_MS) {
/* Use CMD13 (SEND_STATUS) to check if card is actually responsive */
/* This is a lightweight operation that will fail if card is removed */
esp_err_t err = sdmmc_get_status(s_card);
if (err == ESP_OK) {
/* Status check passed - card is definitely ready */
s_last_status_result = true;
} else {
/* Status check failed - but be lenient: only mark as not ready if we've */
/* had multiple consecutive failures (to avoid false negatives) */
/* For now, if mounted, assume ready - status check failures might be transient */
/* Only log a warning, don't block writes */
ESP_LOGW(TAG, "SD card status check failed (but assuming ready): %s", esp_err_to_name(err));
/* Keep s_last_status_result as true - trust mount status over status check */
/* This handles cases where sdmmc_get_status() fails but card is still functional */
}
s_last_status_check_ms = now_ms;
}
/* If mounted, always return true (optimistic) - let actual write operations fail if card is gone */
result = true;
} else {
s_last_status_result = false;
result = false;
}
return result;
}
bool sd_card_cd_available(void) {
return (SD_CD_PIN >= 0);
}
bool sd_card_cd_is_inserted(void) {
bool result = false;
if (SD_CD_PIN >= 0) {
sd_card_cd_ensure_configured();
int level = gpio_get_level(SD_CD_PIN);
#if defined(CONFIG_SD_CD_ACTIVE_LOW) && !(CONFIG_SD_CD_ACTIVE_LOW)
result = (level == 1); /* HIGH = inserted (inverted breakout) */
#else
result = (level == 0); /* LOW = inserted (SparkFun default) */
#endif
}
return result;
}
int sd_card_cd_get_level(void) {
int result = -1;
if (SD_CD_PIN >= 0) {
sd_card_cd_ensure_configured();
result = gpio_get_level(SD_CD_PIN);
}
return result;
}
esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
FATFS *fs;
DWORD fre_clust, fre_sect, tot_sect;
char path[32];
snprintf(path, sizeof(path), "%s", s_mount_point);
FRESULT res = f_getfree(path, &fre_clust, &fs);
if (res == FR_OK) {
tot_sect = (fs->n_fatent - 2) * fs->csize;
fre_sect = fre_clust * fs->csize;
if (total_bytes) {
*total_bytes = (uint64_t)tot_sect * 512;
}
if (free_bytes) {
*free_bytes = (uint64_t)fre_sect * 512;
}
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to get free space: %d", res);
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
int flags = O_WRONLY | O_CREAT;
if (append) {
flags |= O_APPEND;
} else {
flags |= O_TRUNC;
}
int fd = open(full_path, flags, 0644);
if (fd >= 0) {
ssize_t written = write(fd, data, len);
close(fd);
if (written >= 0 && (size_t)written == len) {
ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path);
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len);
result = ESP_FAIL;
}
} else {
ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path);
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "r");
if (f != NULL) {
size_t read = fread(data, 1, len, f);
fclose(f);
if (bytes_read) {
*bytes_read = read;
}
ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path);
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path);
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted && size_bytes != NULL) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
struct stat st;
if (stat(full_path, &st) == 0) {
if (S_ISREG(st.st_mode)) {
*size_bytes = (size_t)st.st_size;
result = ESP_OK;
} else {
result = ESP_ERR_INVALID_ARG;
}
} else {
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, size_t len, size_t *bytes_read) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "rb");
if (f != NULL) {
if (fseek(f, (long)offset, SEEK_SET) == 0) {
size_t n = fread(data, 1, len, f);
fclose(f);
if (bytes_read) {
*bytes_read = n;
}
result = ESP_OK;
} else {
fclose(f);
result = ESP_FAIL;
}
} else {
result = ESP_FAIL;
}
}
return result;
}
bool sd_card_file_exists(const char *filename) {
bool result = false;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
struct stat st;
result = (stat(full_path, &st) == 0);
}
return result;
}
esp_err_t sd_card_list_dir(const char *path) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
if (!path || path[0] == '\0') {
snprintf(full_path, sizeof(full_path), "%s", s_mount_point);
} else {
snprintf(full_path, sizeof(full_path), "%s/%s", s_mount_point,
(path[0] == '/') ? path + 1 : path);
}
DIR *d = opendir(full_path);
if (d != NULL) {
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') {
continue;
}
char entry_path[384]; /* full_path(128) + "/" + d_name(255) */
int n = snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, e->d_name);
if (n < 0 || n >= (int)sizeof(entry_path)) {
continue; /* path too long, skip */
}
struct stat st;
if (stat(entry_path, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
printf(" %-32s <DIR>\n", e->d_name);
} else {
char hr_size[16];
fmt_size_human((size_t)st.st_size, hr_size, sizeof(hr_size));
printf(" %-32s %10zu bytes (%s)\n", e->d_name, (size_t)st.st_size, hr_size);
}
} else {
printf(" %-32s ?\n", e->d_name);
}
}
closedir(d);
result = ESP_OK;
} else {
result = ESP_FAIL;
}
}
return result;
}
esp_err_t sd_card_delete_file(const char *filename) {
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_sd_card_mounted) {
char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename);
if (unlink(full_path) == 0) {
ESP_LOGI(TAG, "Deleted file: %s", full_path);
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to delete file: %s", full_path);
result = ESP_FAIL;
}
}
return result;
}