From a11ecaeb87c6d66c0d3e931cde9b3f1c25d9d469 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 22 Jun 2026 15:25:05 +0000 Subject: [PATCH 1/2] storage_nif: Extract shared SD card config parser Move the "sdmmc" / "sdspi" host and slot configuration out of nif_esp_mount into sdcard_config_from_source so the upcoming raw block device NIFs can reuse it instead of duplicating the option parsing. Pure refactor: the FAT mount path is unchanged. Signed-off-by: Davide Bettio --- .../components/avm_builtins/storage_nif.c | 143 ++++++++++-------- 1 file changed, 84 insertions(+), 59 deletions(-) diff --git a/src/platforms/esp32/components/avm_builtins/storage_nif.c b/src/platforms/esp32/components/avm_builtins/storage_nif.c index 2e57ec5a7a..55913fdb48 100644 --- a/src/platforms/esp32/components/avm_builtins/storage_nif.c +++ b/src/platforms/esp32/components/avm_builtins/storage_nif.c @@ -216,6 +216,78 @@ static bool storage_nif_configure_sdmmc_slot( } #endif +enum sdcard_interface +{ + SDCardSDMMC, + SDCardSDSPI +}; + +struct SDCardConfig +{ + enum sdcard_interface interface; + sdmmc_host_t host; + union + { +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + sdmmc_slot_config_t mmc_slot; +#endif + sdspi_device_config_t spi_dev; + } slot; +}; + +static bool sdcard_config_from_source( + const char *source, term opts_term, struct SDCardConfig *cfg, GlobalContext *glb) +{ +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + if (!strcmp(source, "sdmmc")) { + sdmmc_host_t host_config = SDMMC_HOST_DEFAULT(); + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + if (UNLIKELY(!storage_nif_configure_sdmmc_slot(opts_term, &slot_config, glb))) { + return false; + } + cfg->interface = SDCardSDMMC; + cfg->host = host_config; + cfg->slot.mmc_slot = slot_config; + return true; + } +#endif + + if (!strcmp(source, "sdspi")) { + sdmmc_host_t host_config = SDSPI_HOST_DEFAULT(); + sdspi_device_config_t spi_slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + + term spi_port = interop_kv_get_value(opts_term, ATOM_STR("\x8", "spi_host"), glb); + spi_host_device_t host_dev; + // spi_driver_get_peripheral already checks if spi_port is valid + if (!spi_driver_get_peripheral(spi_port, &host_dev, glb)) { + return false; + } + spi_slot_config.host_id = host_dev; + + term cs_term = interop_kv_get_value(opts_term, ATOM_STR("\x2", "cs"), glb); + if (UNLIKELY(!term_is_integer(cs_term))) { + return false; + } + spi_slot_config.gpio_cs = term_to_int(cs_term); + + term cd_term + = interop_kv_get_value_default(opts_term, ATOM_STR("\x2", "cd"), UNDEFINED_ATOM, glb); + if (cd_term != UNDEFINED_ATOM) { + if (UNLIKELY(!term_is_integer(cd_term))) { + return false; + } + spi_slot_config.gpio_cd = term_to_int(cd_term); + } + + cfg->interface = SDCardSDSPI; + cfg->host = host_config; + cfg->slot.spi_dev = spi_slot_config; + return true; + } + + return false; +} + static term nif_esp_mount(Context *ctx, int argc, term argv[]) { GlobalContext *glb = ctx->global; @@ -287,15 +359,11 @@ static term nif_esp_mount(Context *ctx, int argc, term argv[]) mount->base_path, source + part_by_name_len, &mount_config, &mount->handle.wl); #endif -// C3 doesn't support this -#ifdef SDMMC_SLOT_CONFIG_DEFAULT - } else if (!strcmp(source, "sdmmc")) { + } else if (!strcmp(source, "sdmmc") || !strcmp(source, "sdspi")) { mount_config.allocation_unit_size = 512; - sdmmc_host_t host_config = SDMMC_HOST_DEFAULT(); - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - - if (UNLIKELY(!storage_nif_configure_sdmmc_slot(opts_term, &slot_config, ctx->global))) { + struct SDCardConfig cfg; + if (!sdcard_config_from_source(source, opts_term, &cfg, ctx->global)) { free(source); free(target); RAISE_ERROR(BADARG_ATOM); @@ -310,61 +378,18 @@ static term nif_esp_mount(Context *ctx, int argc, term argv[]) SMP_LOCK_INIT(mount); mount->base_path = target; target = NULL; - mount->mount_type = FATSDMMC; - ret = esp_vfs_fat_sdmmc_mount( - mount->base_path, &host_config, &slot_config, &mount_config, &mount->handle.card); + if (cfg.interface == SDCardSDSPI) { + mount->mount_type = FATSDSPI; + ret = esp_vfs_fat_sdspi_mount( + mount->base_path, &cfg.host, &cfg.slot.spi_dev, &mount_config, &mount->handle.card); +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + } else { + mount->mount_type = FATSDMMC; + ret = esp_vfs_fat_sdmmc_mount(mount->base_path, &cfg.host, &cfg.slot.mmc_slot, + &mount_config, &mount->handle.card); #endif - - } else if (!strcmp(source, "sdspi")) { - mount_config.allocation_unit_size = 512; - - sdmmc_host_t host_config = SDSPI_HOST_DEFAULT(); - sdspi_device_config_t spi_slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - - term spi_port = interop_kv_get_value(opts_term, ATOM_STR("\x8", "spi_host"), ctx->global); - spi_host_device_t host_dev; - // spi_driver_get_peripheral already checks if spi_port is valid - bool ok = spi_driver_get_peripheral(spi_port, &host_dev, ctx->global); - if (!ok) { - free(source); - free(target); - RAISE_ERROR(BADARG_ATOM); } - spi_slot_config.host_id = host_dev; - - term cs_term = interop_kv_get_value(opts_term, ATOM_STR("\x2", "cs"), ctx->global); - if (UNLIKELY(!term_is_integer(cs_term))) { - free(source); - free(target); - RAISE_ERROR(BADARG_ATOM); - } - spi_slot_config.gpio_cs = term_to_int(cs_term); - - term cd_term = interop_kv_get_value_default( - opts_term, ATOM_STR("\x2", "cd"), UNDEFINED_ATOM, ctx->global); - if (cd_term != UNDEFINED_ATOM) { - if (UNLIKELY(!term_is_integer(cd_term))) { - free(source); - free(target); - RAISE_ERROR(BADARG_ATOM); - } - spi_slot_config.gpio_cd = term_to_int(cd_term); - } - - mount = enif_alloc_resource(platform->mounted_fs_resource_type, sizeof(struct MountedFS)); - if (IS_NULL_PTR(mount)) { - free(source); - free(target); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - SMP_LOCK_INIT(mount); - mount->base_path = target; - target = NULL; - mount->mount_type = FATSDSPI; - - ret = esp_vfs_fat_sdspi_mount( - mount->base_path, &host_config, &spi_slot_config, &mount_config, &mount->handle.card); } else { free(source); free(target); From 9a308ac8397dabbcf4ac8ec9854c83610ac04a17 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 22 Jun 2026 15:48:43 +0000 Subject: [PATCH 2/2] esp32: Add low level SD card block device NIFs Add esp:sdcard_open/2, esp:sdcard_read/2, esp:sdcard_write/3, esp:sdcard_info/1 and esp:sdcard_close/1 to read and write raw SD card sectors without mounting a filesystem, for custom filesystems, partition inspection and raw imaging. The card is opened with the same source and options as esp:mount/4 but initialized via sdmmc_card_init with no VFS/FAT layer. A new AVM_ENABLE_RAW_SDCARD_NIFS option enables them independently of the mount NIFs, so a raw-only build links no filesystem code. Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 + doc/src/programmers-guide.md | 46 ++ libs/avm_esp32/src/esp.erl | 85 +++ .../esp32/components/avm_builtins/Kconfig | 4 + .../components/avm_builtins/storage_nif.c | 483 +++++++++++++++++- .../components/avm_sys/include/esp32_sys.h | 3 + .../test/main/test_erl_sources/CMakeLists.txt | 2 + .../main/test_erl_sources/test_sdcard.erl | 87 ++++ src/platforms/esp32/test/main/test_main.c | 6 + 9 files changed, 716 insertions(+), 2 deletions(-) create mode 100644 src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c82fb122c..58223e0d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for the `safe` option in `erlang:binary_to_term/2` - Added xtensa JIT backend for esp32 platform - Added support for configuring pins and width for sdmmc on ESP32 +- Added `esp:sdcard_open/2`, `esp:sdcard_read/2`, `esp:sdcard_write/3`, `esp:sdcard_info/1` and + `esp:sdcard_close/1` to the ESP32 `esp` module for low level SD card block (sector) access - Added support for map comprehensions - Added USB CDC port drivers for ESP32, RP2, and STM32 platforms diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index 7d282e1127..df6f823622 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -1382,6 +1382,52 @@ These functions allow you to work with external storage devices or partitions on ```{important} Remember to properly unmount any mounted filesystems before powering off or resetting the device to prevent data corruption. + +#### Raw SD card block access + +In addition to mounting a FAT filesystem, AtomVM can access an SD card at the block (sector) +level, without mounting any filesystem. This is useful for implementing custom filesystems, +inspecting partition tables, or reading and writing raw images. + +Open the card with [`esp:sdcard_open/2`](./apidocs/erlang/eavmlib/esp.md#sdcard_open-2). The +source string (`"sdmmc"` or `"sdspi"`) and the options are the same as for `esp:mount/4`: + +```erlang +{ok, SDCard} = esp:sdcard_open("sdmmc", []), +``` + +For an SPI attached card, first create a SPI instance, then pass the `spi_host` and `cs` +options, exactly as with `esp:mount/4`: + +```erlang +SPI = spi:open(SPIConfig), +{ok, SDCard} = esp:sdcard_open("sdspi", [{spi_host, SPI}, {cs, 5}]), +``` + +Use [`esp:sdcard_info/1`](./apidocs/erlang/eavmlib/esp.md#sdcard_info-1) to obtain the card +geometry, then read and write individual sectors with +[`esp:sdcard_read/2`](./apidocs/erlang/eavmlib/esp.md#sdcard_read-2) and +[`esp:sdcard_write/3`](./apidocs/erlang/eavmlib/esp.md#sdcard_write-3). The data passed to +`esp:sdcard_write/3` must be exactly one sector in size: + +```erlang +{ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), +{ok, Sector0} = esp:sdcard_read(SDCard, 0), +ok = esp:sdcard_write(SDCard, SectorCount - 1, <<0:(SectorSize * 8)>>), +``` + +When you are done, close the card with +[`esp:sdcard_close/1`](./apidocs/erlang/eavmlib/esp.md#sdcard_close-1): + +```erlang +ok = esp:sdcard_close(SDCard), +``` + +```{warning} +A given physical card should not be accessed through `esp:sdcard_open/2` and `esp:mount/4` at +the same time. Writing raw sectors bypasses any filesystem, so be careful not to corrupt a +filesystem you intend to keep using. +``` ``` ### Restart and Sleep diff --git a/libs/avm_esp32/src/esp.erl b/libs/avm_esp32/src/esp.erl index fbc2468ec5..a57a63e42c 100644 --- a/libs/avm_esp32/src/esp.erl +++ b/libs/avm_esp32/src/esp.erl @@ -43,6 +43,11 @@ sleep_enable_timer_wakeup/1, mount/4, umount/1, + sdcard_open/2, + sdcard_read/2, + sdcard_write/3, + sdcard_info/1, + sdcard_close/1, nvs_fetch_binary/2, nvs_get_binary/1, nvs_get_binary/2, nvs_get_binary/3, nvs_set_binary/2, nvs_set_binary/3, @@ -148,6 +153,8 @@ | {cd, non_neg_integer()}. -type mount_options() :: [sdmmc_mount_option() | sdspi_mount_option()] | #{atom() => term()}. +-opaque sdcard() :: binary(). + -export_type( [ esp_reset_reason/0, @@ -160,6 +167,7 @@ esp_partition_props/0, mounted_fs/0, mount_options/0, + sdcard/0, task_wdt_config/0, task_wdt_user_handle/0 ] @@ -376,6 +384,83 @@ mount(_Source, _Target, _FS, _Opts) -> umount(_Target) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Source the device to open, either `"sdmmc"' or `"sdspi"' +%% @param Opts options for the device +%% @returns either a tuple having `ok' and the SD card resource, or an error tuple +%% @doc Open an SD card for raw block (sector) access, without mounting any +%% filesystem on it. The returned resource is used with +%% {@link sdcard_read/2}, {@link sdcard_write/3}, {@link sdcard_info/1} +%% and {@link sdcard_close/1}. The card is automatically closed when the +%% resource is garbage collected, but {@link sdcard_close/1} should be +%% preferred. +%% +%% The same options as {@link mount/4} are accepted. +%% +%% A given physical card should not be used through `sdcard_open/2' and +%% {@link mount/4} at the same time. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_open( + Source :: unicode:chardata(), + Opts :: mount_options() +) -> {ok, sdcard()} | {error, term()}. +sdcard_open(_Source, _Opts) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @param Sector the sector (block) number to read +%% @returns a tuple with `ok' and the sector data as a binary, or an error tuple +%% @doc Read a single sector from the SD card. The size of the returned +%% binary is the sector size reported by {@link sdcard_info/1} (usually +%% 512 bytes). +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_read(SDCard :: sdcard(), Sector :: non_neg_integer()) -> + {ok, binary()} | {error, term()}. +sdcard_read(_SDCard, _Sector) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @param Sector the sector (block) number to write +%% @param Data the sector data, its size must be exactly the sector size +%% @returns `ok' or an error tuple +%% @doc Write a single sector to the SD card. `Data' must be a binary whose +%% size is exactly the sector size reported by {@link sdcard_info/1}. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_write(SDCard :: sdcard(), Sector :: non_neg_integer(), Data :: binary()) -> + ok | {error, term()}. +sdcard_write(_SDCard, _Sector, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @returns a tuple with `ok' and a map describing the card, or an error tuple +%% @doc Return information about the SD card. The map contains the +%% `sector_size' (in bytes) and `sector_count' (the number of sectors) +%% keys. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_info(SDCard :: sdcard()) -> + {ok, #{sector_size := pos_integer(), sector_count := non_neg_integer()}} + | {error, term()}. +sdcard_info(_SDCard) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @returns `ok' or an error tuple +%% @doc Close a previously opened SD card and release the underlying +%% peripheral. The resource must not be used after it has been closed. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_close(SDCard :: sdcard()) -> ok | {error, term()}. +sdcard_close(_SDCard) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Namespace NVS namespace %% @param Key NVS key diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 41ad63aaba..0dffd36b6e 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -82,6 +82,10 @@ config AVM_ENABLE_STORAGE_NIFS bool "Enable Storage NIFs" default y +config AVM_ENABLE_RAW_SDCARD_NIFS + bool "Enable raw SD card block device NIFs" + default y + config AVM_ENABLE_GPIO_PORT_DRIVER bool "Enable GPIO port driver" default y diff --git a/src/platforms/esp32/components/avm_builtins/storage_nif.c b/src/platforms/esp32/components/avm_builtins/storage_nif.c index 55913fdb48..379461bb2e 100644 --- a/src/platforms/esp32/components/avm_builtins/storage_nif.c +++ b/src/platforms/esp32/components/avm_builtins/storage_nif.c @@ -19,7 +19,7 @@ */ #include -#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS +#if defined(CONFIG_AVM_ENABLE_STORAGE_NIFS) || defined(CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS) #include #include @@ -38,8 +38,15 @@ #include #include #include +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS #include +#endif +#include +#include #include +#if ESP_IDF_VERSION_MAJOR >= 6 && defined(CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS) +#include +#endif #include @@ -57,6 +64,7 @@ #define SMP_UNLOCK(mounted_fs) #endif +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS // TODO: allow ro option enum mount_type { @@ -86,6 +94,7 @@ const ErlNifResourceTypeInit mounted_fs_resource_type_init = { .members = 1, .dtor = mounted_fs_dtor }; +#endif static term make_esp_error_tuple(esp_err_t err, Context *ctx) { @@ -98,6 +107,7 @@ static term make_esp_error_tuple(esp_err_t err, Context *ctx) return result; } +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS static void opts_to_fatfs_mount_config(term opts_term, esp_vfs_fat_mount_config_t *mount_config) { mount_config->format_if_mount_failed = true; @@ -105,6 +115,7 @@ static void opts_to_fatfs_mount_config(term opts_term, esp_vfs_fat_mount_config_ mount_config->allocation_unit_size = 512; // TODO: make it configurable: disk_status_check_enable = false } +#endif #ifdef SDMMC_SLOT_CONFIG_DEFAULT #if defined(SOC_SDMMC_USE_GPIO_MATRIX) && SOC_SDMMC_USE_GPIO_MATRIX @@ -288,6 +299,32 @@ static bool sdcard_config_from_source( return false; } +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +struct SDCardBlockDevice +{ +#ifndef AVM_NO_SMP + SpinLock lock; +#endif + enum sdcard_interface interface; + bool open; + uint32_t sector_size; + uint32_t sector_count; + sdmmc_card_t *card; + sdspi_dev_handle_t spi_handle; +#if ESP_IDF_VERSION_MAJOR >= 6 + esp_blockdev_handle_t blockdev; +#endif +}; + +static void sdcard_dtor(ErlNifEnv *caller_env, void *obj); + +const ErlNifResourceTypeInit sdcard_resource_type_init = { + .members = 1, + .dtor = sdcard_dtor +}; +#endif + +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS static term nif_esp_mount(Context *ctx, int argc, term argv[]) { GlobalContext *glb = ctx->global; @@ -489,6 +526,388 @@ static void mounted_fs_dtor(ErlNifEnv *caller_env, void *obj) free(mounted_fs->base_path); } +#endif + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +static esp_err_t sdcard_blockdev_read(struct SDCardBlockDevice *dev, void *dst, size_t sector) +{ +#if ESP_IDF_VERSION_MAJOR >= 6 + return dev->blockdev->ops->read(dev->blockdev, dst, dev->sector_size, + (uint64_t) sector * dev->sector_size, dev->sector_size); +#else + return sdmmc_read_sectors(dev->card, dst, sector, 1); +#endif +} + +static esp_err_t sdcard_blockdev_write( + struct SDCardBlockDevice *dev, const void *src, size_t sector) +{ +#if ESP_IDF_VERSION_MAJOR >= 6 + return dev->blockdev->ops->write( + dev->blockdev, src, (uint64_t) sector * dev->sector_size, dev->sector_size); +#else + return sdmmc_write_sectors(dev->card, src, sector, 1); +#endif +} + +static esp_err_t sdcard_blockdev_close(struct SDCardBlockDevice *dev) +{ + esp_err_t ret = ESP_OK; + +#if ESP_IDF_VERSION_MAJOR >= 6 + if (dev->blockdev) { + dev->blockdev->ops->release(dev->blockdev); + dev->blockdev = NULL; + } +#endif + + if (dev->interface == SDCardSDSPI) { + sdspi_host_remove_device(dev->spi_handle); + ret = sdspi_host_deinit(); +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + } else { + ret = sdmmc_host_deinit(); +#endif + } + + free(dev->card); + dev->card = NULL; + + return ret; +} + +static esp_err_t sdcard_blockdev_init(struct SDCardConfig *cfg, struct SDCardBlockDevice *dev) +{ + esp_err_t err; + + dev->interface = cfg->interface; + dev->card = malloc(sizeof(sdmmc_card_t)); + if (IS_NULL_PTR(dev->card)) { + return ESP_ERR_NO_MEM; + } + + if (cfg->interface == SDCardSDSPI) { + err = sdspi_host_init(); + if (UNLIKELY(err != ESP_OK)) { + free(dev->card); + dev->card = NULL; + return err; + } + err = sdspi_host_init_device(&cfg->slot.spi_dev, &dev->spi_handle); + if (UNLIKELY(err != ESP_OK)) { + sdspi_host_deinit(); + free(dev->card); + dev->card = NULL; + return err; + } + cfg->host.slot = dev->spi_handle; + err = sdmmc_card_init(&cfg->host, dev->card); + if (UNLIKELY(err != ESP_OK)) { + sdspi_host_remove_device(dev->spi_handle); + sdspi_host_deinit(); + free(dev->card); + dev->card = NULL; + return err; + } +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + } else { + err = (*cfg->host.init)(); + if (UNLIKELY(err != ESP_OK)) { + free(dev->card); + dev->card = NULL; + return err; + } + err = sdmmc_host_init_slot(cfg->host.slot, &cfg->slot.mmc_slot); + if (UNLIKELY(err != ESP_OK)) { + sdmmc_host_deinit(); + free(dev->card); + dev->card = NULL; + return err; + } + err = sdmmc_card_init(&cfg->host, dev->card); + if (UNLIKELY(err != ESP_OK)) { + sdmmc_host_deinit(); + free(dev->card); + dev->card = NULL; + return err; + } +#else + } else { + free(dev->card); + dev->card = NULL; + return ESP_ERR_NOT_SUPPORTED; +#endif + } + + dev->sector_size = dev->card->csd.sector_size; + dev->sector_count = dev->card->csd.capacity; + +#if ESP_IDF_VERSION_MAJOR >= 6 + err = sdmmc_get_blockdev(dev->card, &dev->blockdev); + if (UNLIKELY(err != ESP_OK)) { + dev->blockdev = NULL; + sdcard_blockdev_close(dev); + return err; + } +#endif + + return ESP_OK; +} + +static term nif_esp_sdcard_open(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + int str_ok; + char *source = interop_term_to_string(argv[0], &str_ok); + if (!str_ok) { + RAISE_ERROR(BADARG_ATOM); + } + + term opts_term = argv[1]; + if (!term_is_list(opts_term) && !term_is_map(opts_term)) { + free(source); + RAISE_ERROR(BADARG_ATOM); + } + + struct SDCardConfig cfg; + bool ok = sdcard_config_from_source(source, opts_term, &cfg, glb); + free(source); + if (!ok) { + RAISE_ERROR(BADARG_ATOM); + } + + struct SDCardBlockDevice *dev + = enif_alloc_resource(platform->sdcard_resource_type, sizeof(struct SDCardBlockDevice)); + if (IS_NULL_PTR(dev)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + SMP_LOCK_INIT(dev); + dev->open = false; + dev->card = NULL; + dev->sector_size = 0; + dev->sector_count = 0; +#if ESP_IDF_VERSION_MAJOR >= 6 + dev->blockdev = NULL; +#endif + + esp_err_t err = sdcard_blockdev_init(&cfg, dev); + + term return_term; + if (UNLIKELY(err != ESP_OK)) { + return_term = make_esp_error_tuple(err, ctx); + } else { + dev->open = true; + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) + TERM_BOXED_RESOURCE_SIZE) + != MEMORY_GC_OK)) { + enif_release_resource(dev); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term dev_term = term_from_resource(dev, &ctx->heap); + return_term = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(return_term, 0, OK_ATOM); + term_put_tuple_element(return_term, 1, dev_term); + } + enif_release_resource(dev); // decrement refcount after enif_alloc_resource + + return return_term; +} + +static term nif_esp_sdcard_read(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + if (UNLIKELY(!term_is_any_integer(argv[1]))) { + RAISE_ERROR(BADARG_ATOM); + } + avm_int64_t sector = term_maybe_unbox_int64(argv[1]); + + // The spinlock is held across the (blocking) card transaction; a mutex + // would be preferable for larger transfers. + SMP_LOCK(dev); + if (UNLIKELY(!dev->open)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(sector < 0 || (uint64_t) sector >= dev->sector_count)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + size_t sector_size = dev->sector_size; + + if (UNLIKELY(memory_ensure_free(ctx, term_binary_heap_size(sector_size) + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + SMP_UNLOCK(dev); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term binary = term_create_uninitialized_binary(sector_size, &ctx->heap, ctx->global); + // sdmmc_read_sectors bounces non-DMA-capable / unaligned destinations (e.g. a + // binary in PSRAM) internally, so the binary is passed directly; it may fail + // with ESP_ERR_NO_MEM under DMA heap pressure. + esp_err_t err = sdcard_blockdev_read(dev, (void *) term_binary_data(binary), (size_t) sector); + SMP_UNLOCK(dev); + + if (UNLIKELY(err != ESP_OK)) { + return make_esp_error_tuple(err, ctx); + } + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, binary); + + return result; +} + +static term nif_esp_sdcard_write(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + if (UNLIKELY(!term_is_any_integer(argv[1]))) { + RAISE_ERROR(BADARG_ATOM); + } + avm_int64_t sector = term_maybe_unbox_int64(argv[1]); + + VALIDATE_VALUE(argv[2], term_is_binary); + term data = argv[2]; + + SMP_LOCK(dev); + if (UNLIKELY(!dev->open)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(term_binary_size(data) != dev->sector_size)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(sector < 0 || (uint64_t) sector >= dev->sector_count)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + + esp_err_t err = sdcard_blockdev_write(dev, term_binary_data(data), (size_t) sector); + SMP_UNLOCK(dev); + + if (UNLIKELY(err != ESP_OK)) { + return make_esp_error_tuple(err, ctx); + } + + return OK_ATOM; +} + +static term nif_esp_sdcard_info(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + SMP_LOCK(dev); + if (UNLIKELY(!dev->open)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector_size = dev->sector_size; + uint32_t sector_count = dev->sector_count; + SMP_UNLOCK(dev); + + size_t needed = TUPLE_SIZE(2) + term_map_size_in_terms(2) + + term_boxed_integer_size(sector_size) + term_boxed_integer_size(sector_count); + if (UNLIKELY(memory_ensure_free(ctx, needed) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term sector_count_value = term_make_maybe_boxed_int64(sector_count, &ctx->heap); + term sector_size_value = term_make_maybe_boxed_int64(sector_size, &ctx->heap); + + term info = term_alloc_map(2, &ctx->heap); + term_set_map_assoc( + info, 0, globalcontext_make_atom(glb, ATOM_STR("\xC", "sector_count")), sector_count_value); + term_set_map_assoc( + info, 1, globalcontext_make_atom(glb, ATOM_STR("\xB", "sector_size")), sector_size_value); + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, info); + + return result; +} + +static term nif_esp_sdcard_close(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + SMP_LOCK(dev); + if (UNLIKELY(!dev->open)) { + SMP_UNLOCK(dev); + RAISE_ERROR(BADARG_ATOM); + } + esp_err_t err = sdcard_blockdev_close(dev); + dev->open = false; + SMP_UNLOCK(dev); + + if (UNLIKELY(err != ESP_OK)) { + return make_esp_error_tuple(err, ctx); + } + + return OK_ATOM; +} + +static void sdcard_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) obj; + if (dev->open) { + esp_err_t err = sdcard_blockdev_close(dev); + dev->open = false; + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGW(TAG, + "Failed to close SD card block device in resource dtor. " + "Please use esp:sdcard_close/1."); + } + } +} +#endif void storage_nif_init(GlobalContext *global) { @@ -496,10 +915,17 @@ void storage_nif_init(GlobalContext *global) ErlNifEnv env; erl_nif_env_partial_init_from_globalcontext(&env, global); +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS platform->mounted_fs_resource_type = enif_init_resource_type( &env, "mounted_fs", &mounted_fs_resource_type_init, ERL_NIF_RT_CREATE, NULL); +#endif +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS + platform->sdcard_resource_type = enif_init_resource_type( + &env, "sdcard", &sdcard_resource_type_init, ERL_NIF_RT_CREATE, NULL); +#endif } +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS static const struct Nif esp_mount_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_esp_mount @@ -509,16 +935,69 @@ static const struct Nif esp_umount_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_esp_umount }; +#endif + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +static const struct Nif esp_sdcard_open_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_open +}; + +static const struct Nif esp_sdcard_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_read +}; + +static const struct Nif esp_sdcard_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_write +}; + +static const struct Nif esp_sdcard_info_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_info +}; + +static const struct Nif esp_sdcard_close_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_close +}; +#endif const struct Nif *storage_nif_get_nif(const char *nifname) { +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS if (strcmp("esp:mount/4", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &esp_mount_nif; - } else if (strcmp("esp:umount/1", nifname) == 0) { + } + if (strcmp("esp:umount/1", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &esp_umount_nif; } +#endif +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS + if (strcmp("esp:sdcard_open/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_open_nif; + } + if (strcmp("esp:sdcard_read/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_read_nif; + } + if (strcmp("esp:sdcard_write/3", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_write_nif; + } + if (strcmp("esp:sdcard_info/1", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_info_nif; + } + if (strcmp("esp:sdcard_close/1", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_close_nif; + } +#endif return NULL; } diff --git a/src/platforms/esp32/components/avm_sys/include/esp32_sys.h b/src/platforms/esp32/components/avm_sys/include/esp32_sys.h index e38368d3db..ccb3d99f7b 100644 --- a/src/platforms/esp32/components/avm_sys/include/esp32_sys.h +++ b/src/platforms/esp32/components/avm_sys/include/esp32_sys.h @@ -83,6 +83,9 @@ struct ESP32PlatformData #ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS ErlNifResourceType *mounted_fs_resource_type; +#endif +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS + ErlNifResourceType *sdcard_resource_type; #endif ErlNifResourceType *partition_mmap_resource_type; }; diff --git a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt index 2d10887f59..b286cef640 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -109,6 +109,7 @@ compile_erlang(test_md5) compile_erlang(test_crypto) compile_erlang(test_monotonic_time) compile_erlang(test_mount) +compile_erlang(test_sdcard) compile_erlang(test_net) compile_erlang(test_rtc_slow) compile_erlang(test_select) @@ -136,6 +137,7 @@ set(erlang_test_beams test_crypto.beam test_monotonic_time.beam test_mount.beam + test_sdcard.beam test_net.beam test_rtc_slow.beam test_select.beam diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl b/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl new file mode 100644 index 0000000000..255c12144d --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl @@ -0,0 +1,87 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_sdcard). +-export([start/0]). + +start() -> + {ok, SDCard} = esp:sdcard_open("sdmmc", []), + ok = test_info(SDCard), + ok = test_read(SDCard), + ok = test_write_roundtrip(SDCard), + ok = test_bad_args(SDCard), + ok = esp:sdcard_close(SDCard), + ok = test_use_after_close(SDCard), + ok = test_open_bad_opts(), + ok. + +test_info(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + true = is_integer(SectorSize) andalso SectorSize > 0, + true = is_integer(SectorCount) andalso SectorCount > 0, + ok. + +test_read(SDCard) -> + {ok, #{sector_size := SectorSize}} = esp:sdcard_info(SDCard), + {ok, Sector0} = esp:sdcard_read(SDCard, 0), + SectorSize = byte_size(Sector0), + ok. + +%% Read the last sector, overwrite it with a known pattern, read it back and +%% finally restore the original contents so the card is left unchanged. +test_write_roundtrip(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + LastSector = SectorCount - 1, + {ok, Original} = esp:sdcard_read(SDCard, LastSector), + Pattern = make_pattern(SectorSize), + ok = esp:sdcard_write(SDCard, LastSector, Pattern), + {ok, Pattern} = esp:sdcard_read(SDCard, LastSector), + ok = esp:sdcard_write(SDCard, LastSector, Original), + {ok, Original} = esp:sdcard_read(SDCard, LastSector), + ok. + +test_bad_args(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + %% Data that is not exactly one sector long is rejected. + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, 0, <<0>>) end), + %% Reading or writing past the last sector is rejected. + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, SectorCount) end), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, SectorCount, make_pattern(SectorSize)) end), + ok. + +test_use_after_close(SDCard) -> + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, 0) end), + ok. + +test_open_bad_opts() -> + %% sdspi without the required spi_host/cs options. + ok = expect_badarg(fun() -> esp:sdcard_open("sdspi", []) end), + ok. + +make_pattern(SectorSize) -> + list_to_binary([N rem 256 || N <- lists:seq(0, SectorSize - 1)]). + +expect_badarg(Fun) -> + try Fun() of + _ -> error + catch + error:badarg -> ok; + _:_ -> not_badarg + end. diff --git a/src/platforms/esp32/test/main/test_main.c b/src/platforms/esp32/test/main/test_main.c index 480b164695..37e54764ee 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -391,6 +391,12 @@ TEST_CASE("test_mount", "[test_run]") term ret_value = avm_test_case("test_mount.beam"); TEST_ASSERT(ret_value == OK_ATOM); } + +TEST_CASE("test_sdcard", "[test_run]") +{ + term ret_value = avm_test_case("test_sdcard.beam"); + TEST_ASSERT(ret_value == OK_ATOM); +} #endif struct pipefs_global_ctx