/* * 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 #include #include #include #include #include /* 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 \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; }