ESP32/components/sd_card/sd_card.c

333 lines
10 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 "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 <dirent.h>
// 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);
const char *mode = append ? "a" : "w";
FILE *f = fopen(full_path, mode);
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path);
return ESP_FAIL;
}
size_t written = fwrite(data, 1, len, f);
fclose(f);
if (written != len) {
ESP_LOGE(TAG, "Failed to write all data: wrote %zu of %zu bytes", written, len);
return ESP_FAIL;
}
ESP_LOGD(TAG, "Wrote %zu bytes to %s", 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;
}
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_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;
}