427 lines
13 KiB
C
427 lines
13 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 <fcntl.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);
|
|
|
|
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 <DIR>\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;
|
|
}
|