From 29906082976afd2951fb5b6aef334b120760baa8 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 29 Sep 2024 12:20:26 +0200 Subject: [PATCH] ESP32: add support for mounting/umounting storage Allow to mount and umount external storage such as SD/MMC or internal flash using `esp:mount/4` and `esp:umount/1`. Right now only `fat` filesystem is supported. Their semantic and parameters resembles unix mount and umount syscalls. Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 + libs/eavmlib/src/esp.erl | 30 ++ .../components/avm_builtins/CMakeLists.txt | 3 +- .../esp32/components/avm_builtins/Kconfig | 4 + .../components/avm_builtins/storage_nif.c | 376 ++++++++++++++++++ .../components/avm_sys/include/esp32_sys.h | 4 + .../test/main/test_erl_sources/CMakeLists.txt | 3 + .../test/main/test_erl_sources/test_mount.erl | 47 +++ src/platforms/esp32/test/main/test_main.c | 11 + 9 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 src/platforms/esp32/components/avm_builtins/storage_nif.c create mode 100644 src/platforms/esp32/test/main/test_erl_sources/test_mount.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 87edf36ff..1e21aad5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Support for `binary:copy/1,2` - Support for directory listing using POSIX APIs: (`atomvm:posix_opendir/1`, `atomvm:posix_readdir/1`, `atomvm:posix_closedir/1`). +- Support for mounting/unmounting storage on ESP32 (such as SD or internal flash) using +`esp:mount/4` and `esp:umount/1` ### Changed diff --git a/libs/eavmlib/src/esp.erl b/libs/eavmlib/src/esp.erl index 1b87dc122..ea341c8f5 100644 --- a/libs/eavmlib/src/esp.erl +++ b/libs/eavmlib/src/esp.erl @@ -38,6 +38,8 @@ sleep_enable_ulp_wakeup/0, deep_sleep/0, deep_sleep/1, + mount/4, + umount/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, @@ -279,6 +281,34 @@ deep_sleep() -> deep_sleep(_SleepMS) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Source the device that will be mounted +%% @param Target the path where the filesystem will be mounted +%% @param FS the filesystem, only fat is supported now +%% @param Opts +%% @returns ok in case of success, otherwise an error +%% @doc Mount a filesystem +%% @end +%%----------------------------------------------------------------------------- +-spec mount( + Source :: unicode:chardata(), + Target :: unicode:chardata(), + FS :: fat, + Opts :: proplists:proplist() | #{atom() => term()} +) -> ok. +mount(_Source, _Target, _FS, _Opts) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Target the path of the mounted filesystem that should be unmounted +%% @returns ok +%% @doc Unmounts filesystem located at given path +%% @end +%%----------------------------------------------------------------------------- +-spec umount(Target :: unicode:chardata()) -> ok. +umount(_Target) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Namespace NVS namespace %% @param Key NVS key diff --git a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt index ff163591a..861dc9e30 100644 --- a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt @@ -27,6 +27,7 @@ set(AVM_BUILTIN_COMPONENT_SRCS "rtc_slow_nif.c" "socket_driver.c" "spi_driver.c" + "storage_nif.c" "uart_driver.c" "otp_crypto_platform.c" "otp_net_platform.c" @@ -55,7 +56,7 @@ endif() idf_component_register( SRCS ${AVM_BUILTIN_COMPONENT_SRCS} INCLUDE_DIRS "include" - PRIV_REQUIRES "libatomvm" "avm_sys" "nvs_flash" "driver" "esp_event" "esp_wifi" ${ADDITIONAL_PRIV_REQUIRES} + PRIV_REQUIRES "libatomvm" "avm_sys" "nvs_flash" "driver" "esp_event" "esp_wifi" "fatfs" ${ADDITIONAL_PRIV_REQUIRES} ${OPTIONAL_WHOLE_ARCHIVE} ) diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 02fb3ed6a..ad260f853 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -45,6 +45,10 @@ config AVM_RTC_SLOW_MAX_SIZE # 4KB is a reasonable default default 4096 +config AVM_ENABLE_STORAGE_NIFS + bool "Enable Storage 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 new file mode 100644 index 000000000..682e9556b --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/storage_nif.c @@ -0,0 +1,376 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2024 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 + */ + +#include +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp32_sys.h" + +#include + +#include +#include +#include + +#include + +#include "spi_driver.h" + +#define TAG "storage_nif" + +#ifndef AVM_NO_SMP +#define SMP_LOCK(mounted_fs) smp_spinlock_lock(mounted_fs->lock) +#define SMP_UNLOCK(mounted_fs) smp_spinlock_unlock(mounted_fs->lock) +#else +#define SMP_LOCK(mounted_fs) +#define SMP_UNLOCK(mounted_fs) +#endif + +// TODO: allow ro option +enum mount_type +{ + Unmounted, + FATSPIFlash, + FATSDSPI, + FATSDMMC +}; + +struct MountedFS +{ +#ifndef AVM_NO_SMP + SpinLock *lock; +#endif + char *base_path; + enum mount_type mount_type; + union + { + sdmmc_card_t *card; + wl_handle_t wl; + } handle; +}; + +static void mounted_fs_dtor(ErlNifEnv *caller_env, void *obj); + +const ErlNifResourceTypeInit mounted_fs_resource_type_init = { + .members = 1, + .dtor = mounted_fs_dtor +}; + +static term make_esp_error_tuple(esp_err_t err, Context *ctx) +{ + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, ERROR_ATOM); + term_put_tuple_element(result, 1, esp_err_to_term(ctx->global, err)); + return result; +} + +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; + mount_config->max_files = 8; + mount_config->allocation_unit_size = 512; + // TODO: make it configurable: disk_status_check_enable = false +} + +static term nif_esp_mount(Context *ctx, int argc, term argv[]) +{ + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + term source_term = argv[0]; + term target_term = argv[1]; + term filesystem_type_term = argv[2]; + term opts_term = argv[3]; + + int str_ok; + char *source = interop_term_to_string(source_term, &str_ok); + if (!str_ok) { + RAISE_ERROR(BADARG_ATOM); + } + + char *target = interop_term_to_string(target_term, &str_ok); + if (!str_ok) { + free(source); + RAISE_ERROR(BADARG_ATOM); + } + if (strlen(target) > 8) { + free(source); + free(target); + RAISE_ERROR(BADARG_ATOM); + } + + term fat_term + = globalcontext_existing_term_from_atom_string(ctx->global, ATOM_STR("\x3", "fat")); + if (term_is_invalid_term(fat_term) || filesystem_type_term != fat_term) { + free(source); + free(target); + RAISE_ERROR(BADARG_ATOM); + } + + if (!term_is_list(opts_term) && !term_is_map(opts_term)) { + free(source); + free(target); + RAISE_ERROR(BADARG_ATOM); + } + + esp_vfs_fat_mount_config_t mount_config = {}; + opts_to_fatfs_mount_config(opts_term, &mount_config); + + esp_err_t ret = -1; + struct MountedFS *mount = NULL; + + const char *part_by_name_prefix = "/dev/partition/by-name/"; + int part_by_name_len = strlen(part_by_name_prefix); + if (!strncmp(part_by_name_prefix, source, part_by_name_len)) { + mount_config.allocation_unit_size = CONFIG_WL_SECTOR_SIZE; + + 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); + } + mount->base_path = target; + target = NULL; + mount->mount_type = FATSPIFlash; + +#if ESP_IDF_VERSION_MAJOR >= 5 + ret = esp_vfs_fat_spiflash_mount_rw_wl( + mount->base_path, source + part_by_name_len, &mount_config, &mount->handle.wl); +#else + ret = esp_vfs_fat_spiflash_mount( + 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")) { + mount_config.allocation_unit_size = 512; + + sdmmc_host_t host_config = SDMMC_HOST_DEFAULT(); + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + + mount = enif_alloc_resource(platform->mounted_fs_resource_type, sizeof(struct MountedFS)); + if (IS_NULL_PTR(mount)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + 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); +#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_default( + opts_term, ATOM_STR("\x8", "spi_host"), term_invalid_term(), ctx->global); + spi_host_device_t host_dev; + 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_default( + opts_term, ATOM_STR("\x2", "cs"), term_invalid_term(), ctx->global); + VALIDATE_VALUE(cs_term, term_is_integer); + 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); + } + 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); + enif_release_resource(mount); + RAISE_ERROR(BADARG_ATOM); + } + + free(source); + + term return_term = term_invalid_term(); + if (UNLIKELY(ret != ESP_OK)) { + mount->mount_type = Unmounted; + return_term = make_esp_error_tuple(ret, ctx); + } else { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) + TERM_BOXED_RESOURCE_SIZE) + != MEMORY_GC_OK)) { + enif_release_resource(mount); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term mount_term = enif_make_resource(erl_nif_env_from_context(ctx), mount); + return_term = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(return_term, 0, OK_ATOM); + term_put_tuple_element(return_term, 1, mount_term); + } + enif_release_resource(mount); + + return return_term; +} + +static esp_err_t do_umount(struct MountedFS *mount) +{ + SMP_LOCK(mount); + esp_err_t ret = ESP_FAIL; + + switch (mount->mount_type) { + case Unmounted: + ret = ESP_OK; + break; + + case FATSPIFlash: +#if ESP_IDF_VERSION_MAJOR >= 5 + ret = esp_vfs_fat_spiflash_unmount_rw_wl(mount->base_path, mount->handle.wl); +#else + ret = esp_vfs_fat_spiflash_unmount(mount->base_path, mount->handle.wl); +#endif + break; + + case FATSDSPI: + case FATSDMMC: + ret = esp_vfs_fat_sdcard_unmount(mount->base_path, mount->handle.card); + break; + } + + if (ret == ESP_OK) { + mount->mount_type = Unmounted; + } + + SMP_UNLOCK(mount); + return ret; +} + +static term nif_esp_umount(Context *ctx, int argc, term argv[]) +{ + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *mount_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->mounted_fs_resource_type, &mount_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct MountedFS *mounted_fs = (struct MountedFS *) mount_obj_ptr; + + if (UNLIKELY(mounted_fs->mount_type == Unmounted)) { + RAISE_ERROR(BADARG_ATOM); + } + + esp_err_t ret = do_umount(mounted_fs); + + if (UNLIKELY(ret != ESP_OK)) { + return make_esp_error_tuple(ret, ctx); + } + + return OK_ATOM; +} + +static void mounted_fs_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct MountedFS *mounted_fs = (struct MountedFS *) obj; + esp_err_t ret = do_umount(mounted_fs); + + if (UNLIKELY(ret != ESP_OK)) { + ESP_LOGW(TAG, "Failed umount for %s in resource dtor. Please use esp:umount/1.", + mounted_fs->base_path); + } + + free(mounted_fs->base_path); +} + +void storage_nif_init(GlobalContext *global) +{ + struct ESP32PlatformData *platform = global->platform_data; + + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + platform->mounted_fs_resource_type = enif_init_resource_type( + &env, "mounted_fs", &mounted_fs_resource_type_init, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif esp_mount_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_mount +}; + +static const struct Nif esp_umount_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_umount +}; + +const struct Nif *storage_nif_get_nif(const char *nifname) +{ + 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) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_umount_nif; + } + + return NULL; +} + +REGISTER_NIF_COLLECTION(storage, storage_nif_init, NULL, storage_nif_get_nif) + +#endif 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 30a3ab5b5..b09165405 100644 --- a/src/platforms/esp32/components/avm_sys/include/esp32_sys.h +++ b/src/platforms/esp32/components/avm_sys/include/esp32_sys.h @@ -114,6 +114,10 @@ struct ESP32PlatformData #endif mbedtls_ctr_drbg_context random_ctx; bool random_is_initialized; + +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS + ErlNifResourceType *mounted_fs_resource_type; +#endif }; typedef void (*port_driver_init_t)(GlobalContext *global); 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 4cc13d62b..72de3ddac 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -43,6 +43,7 @@ compile_erlang(test_list_to_binary) compile_erlang(test_md5) compile_erlang(test_crypto) compile_erlang(test_monotonic_time) +compile_erlang(test_mount) compile_erlang(test_net) compile_erlang(test_rtc_slow) compile_erlang(test_select) @@ -62,6 +63,7 @@ add_custom_command( test_md5.beam test_crypto.beam test_monotonic_time.beam + test_mount.beam test_net.beam test_rtc_slow.beam test_select.beam @@ -78,6 +80,7 @@ add_custom_command( "${CMAKE_CURRENT_BINARY_DIR}/test_md5.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_crypto.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_monotonic_time.beam" + "${CMAKE_CURRENT_BINARY_DIR}/test_mount.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_net.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_rtc_slow.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_select.beam" diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_mount.erl b/src/platforms/esp32/test/main/test_erl_sources/test_mount.erl new file mode 100644 index 000000000..b7ae0db6e --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_mount.erl @@ -0,0 +1,47 @@ +% +% This file is part of AtomVM. +% +% Copyright 2024 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_mount). +-export([start/0]). + +start() -> + {ok, Ref} = mount_working_sdmmc(), + ok = umount_prev(Ref), + ok = mount_missing_fat_partition(), + ok = umount_prev(Ref). + +mount_working_sdmmc() -> + Ref = esp:mount("sdmmc", "/test", fat, []), + {ok, Fd} = atomvm:posix_open("/test/test.txt", [o_rdwr, o_creat], 8#644), + ok = atomvm:posix_close(Fd), + ok = esp:umount(Ref), + {ok, Ref}. + +mount_missing_fat_partition() -> + {error, esp_err_not_found} = esp:mount("/dev/partition/by-name/missingpart", "/test", fat, []), + ok. + +umount_prev(Ref) -> + try esp:umount(Ref) of + ok -> 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 83076c348..509f948c7 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -242,6 +242,17 @@ TEST_CASE("test_monotonic_time", "[test_run]") TEST_ASSERT(ret_value == OK_ATOM); } +#if !CONFIG_IDF_TARGET_ESP32C3 +// this test is failing on v5.0.7 due to some kind of problem with atomvm:posix_open +#if ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1 +TEST_CASE("test_mount", "[test_run]") +{ + term ret_value = avm_test_case("test_mount.beam"); + TEST_ASSERT(ret_value == OK_ATOM); +} +#endif +#endif + struct pipefs_global_ctx { int max_fd;