/* * 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 "driver/gpio.h" #include "sdkconfig.h" #include "esp_vfs_fat.h" #include "ff.h" #include "sdmmc_cmd.h" #include #include #include #include #include #include // 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 void sd_card_cd_ensure_configured(void) { if (s_cd_configured || SD_CD_PIN < 0) { return; } /* 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) { if (s_sd_card_mounted) { ESP_LOGW(TAG, "SD card already initialized"); return ESP_OK; } 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, }; esp_err_t err = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err)); return err; } 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 }; err = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card); if (err != ESP_OK) { if (s_spi_bus_inited) { spi_bus_free(SDSPI_HOST_ID); s_spi_bus_inited = false; } if (err == 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(err)); } return err; } sdmmc_card_print_info(stdout, s_card); s_sd_card_mounted = true; ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point); return ESP_OK; } esp_err_t sd_card_deinit(void) { if (!s_sd_card_mounted) { return ESP_OK; } ESP_LOGI(TAG, "Unmounting SD card..."); esp_err_t ret = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card); if (ret == ESP_OK) { s_sd_card_mounted = false; s_card = NULL; 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(ret)); } return ret; } bool sd_card_is_ready(void) { return s_sd_card_mounted; } bool sd_card_cd_available(void) { return (SD_CD_PIN >= 0); } bool sd_card_cd_is_inserted(void) { if (SD_CD_PIN < 0) { return false; } 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) return (level == 1); /* HIGH = inserted (inverted breakout) */ #else return (level == 0); /* LOW = inserted (SparkFun default) */ #endif } int sd_card_cd_get_level(void) { if (SD_CD_PIN < 0) { return -1; } sd_card_cd_ensure_configured(); return gpio_get_level(SD_CD_PIN); } esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) { if (!s_sd_card_mounted) { return ESP_ERR_INVALID_STATE; } 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) { ESP_LOGE(TAG, "Failed to get free space: %d", res); return ESP_FAIL; } 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; } return ESP_OK; } esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) { if (!s_sd_card_mounted) { return ESP_ERR_INVALID_STATE; } 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) { ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path); return ESP_FAIL; } ssize_t written = write(fd, data, len); close(fd); if (written < 0 || (size_t)written != len) { ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len); return ESP_FAIL; } ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path); return ESP_OK; } esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) { if (!s_sd_card_mounted) { return ESP_ERR_INVALID_STATE; } 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) { ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path); return ESP_FAIL; } 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); return ESP_OK; } esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) { if (!s_sd_card_mounted || size_bytes == NULL) { return ESP_ERR_INVALID_STATE; } 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) { return ESP_FAIL; } if (!S_ISREG(st.st_mode)) { return ESP_ERR_INVALID_ARG; } *size_bytes = (size_t)st.st_size; return ESP_OK; } esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, size_t len, size_t *bytes_read) { if (!s_sd_card_mounted) { return ESP_ERR_INVALID_STATE; } 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) { return ESP_FAIL; } if (fseek(f, (long)offset, SEEK_SET) != 0) { fclose(f); return ESP_FAIL; } size_t n = fread(data, 1, len, f); fclose(f); if (bytes_read) { *bytes_read = n; } return ESP_OK; } bool sd_card_file_exists(const char *filename) { if (!s_sd_card_mounted) { return false; } char full_path[128]; snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, (filename[0] == '/') ? "" : "/", filename); struct stat st; return (stat(full_path, &st) == 0); } esp_err_t sd_card_list_dir(const char *path) { if (!s_sd_card_mounted) { return ESP_ERR_INVALID_STATE; } 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) { return ESP_FAIL; } 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 { printf(" %-32s %10zu bytes\n", e->d_name, (size_t)st.st_size); } } else { printf(" %-32s ?\n", e->d_name); } } closedir(d); return ESP_OK; } esp_err_t sd_card_delete_file(const char *filename) { if (!s_sd_card_mounted) { return ESP_ERR_INVALID_STATE; } 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_LOGE(TAG, "Failed to delete file: %s", full_path); return ESP_FAIL; } ESP_LOGI(TAG, "Deleted file: %s", full_path); return ESP_OK; }