From 333b7693785e01235aff27d57b933077bba9b8d5 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 stroage Allow to mount and umount external storage such as SDs/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 | 300 ++++++++++++++++++ .../test/main/test_erl_sources/CMakeLists.txt | 3 + .../test/main/test_erl_sources/test_mount.erl | 55 ++++ src/platforms/esp32/test/main/test_main.c | 11 + 8 files changed, 407 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 b85320761..801b591b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Add support to Elixir for `Keyword.split/2` - Support for `binary:split/3` and `string:find/2,3` - Support for large tuples (more than 255 elements) in external terms. +- 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..f6c4574d0 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 :: list(proplists:property()) | #{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..75ca07a38 --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/storage_nif.c @@ -0,0 +1,300 @@ +/* + * 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 "esp32_sys.h" + +#include + +// file write test +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "spi_driver.h" + +// TODO: allow ro option +enum mount_type +{ + FATSPIFlash, + FATSDSPI, + FATSDMMC +}; + +struct MountedFS +{ + struct PlatformAttachment attachment; + char *base_path; + enum mount_type mount_type; + union + { + sdmmc_card_t *card; + wl_handle_t wl; + } handle; +}; + +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[]) +{ + 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 || strlen(target) > 8) { + 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) { + RAISE_ERROR(BADARG_ATOM); + } + + if (!term_is_list(opts_term) && !term_is_map(opts_term)) { + RAISE_ERROR(BADARG_ATOM); + } + + struct ESP32PlatformData *platform = ctx->global->platform_data; + + esp_vfs_fat_mount_config_t mount_config = {}; + opts_to_fatfs_mount_config(opts_term, &mount_config); + + esp_err_t ret = -1; + + 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; + + struct MountedFS *mount = malloc(sizeof(struct MountedFS)); + mount->attachment.attachment_type = (uintptr_t) nif_esp_mount; + mount->base_path = strdup(target); + mount->mount_type = FATSPIFlash; + synclist_append(&platform->attachment_list, &mount->attachment.list_head); + +#if ESP_IDF_VERSION_MAJOR >= 5 + ret = esp_vfs_fat_spiflash_mount_rw_wl( + target, source + part_by_name_len, &mount_config, &mount->handle.wl); +#else + ret = esp_vfs_fat_spiflash_mount( + target, 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(); + + struct MountedFS *mount = malloc(sizeof(struct MountedFS)); + mount->attachment.attachment_type = (uintptr_t) nif_esp_mount; + mount->base_path = strdup(target); + mount->mount_type = FATSDMMC; + synclist_append(&platform->attachment_list, &mount->attachment.list_head); + + ret = esp_vfs_fat_sdmmc_mount( + target, &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) { + 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) { + VALIDATE_VALUE(cd_term, term_is_integer); + spi_slot_config.gpio_cd = term_to_int(cd_term); + } + + struct MountedFS *mount = malloc(sizeof(struct MountedFS)); + mount->attachment.attachment_type = (uintptr_t) nif_esp_mount; + mount->base_path = strdup(target); + mount->mount_type = FATSPIFlash; + synclist_append(&platform->attachment_list, &mount->attachment.list_head); + + ret = esp_vfs_fat_sdspi_mount( + target, &host_config, &spi_slot_config, &mount_config, &mount->handle.card); + } else { + RAISE_ERROR(BADARG_ATOM); + } + + term return_term = OK_ATOM; + if (UNLIKELY(ret != ESP_OK)) { + return_term = make_esp_error_tuple(ret, ctx); + } + + free(source); + free(target); + + return return_term; +} + +static term do_umount(struct MountedFS *mount, Context *ctx) +{ + esp_err_t ret = ESP_OK; + + switch (mount->mount_type) { + 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 (UNLIKELY(ret != ESP_OK)) { + return make_esp_error_tuple(ret, ctx); + } + + return OK_ATOM; +} + +static term nif_esp_umount(Context *ctx, int argc, term argv[]) +{ + term source_term = argv[0]; + + int str_ok; + char *source = interop_term_to_string(source_term, &str_ok); + if (!str_ok || strlen(source) >= 8) { + RAISE_ERROR(BADARG_ATOM); + } + + struct ESP32PlatformData *platform = ctx->global->platform_data; + struct ListHead *attachment_list = synclist_wrlock(&platform->attachment_list); + + struct ListHead *item; + struct ListHead *tmp; + MUTABLE_LIST_FOR_EACH (item, tmp, attachment_list) { + struct PlatformAttachment *entry = GET_LIST_ENTRY(item, struct PlatformAttachment, list_head); + if (entry->attachment_type == (uintptr_t) nif_esp_mount) { + struct MountedFS *mount = CONTAINER_OF(entry, struct MountedFS, attachment); + if (!strcmp(source, mount->base_path)) { + term ret = do_umount(mount, ctx); + if (ret == OK_ATOM) { + list_remove(&mount->attachment.list_head); + free(mount->base_path); + free(mount); + } + synclist_unlock(&platform->attachment_list); + return ret; + } + } + } + + synclist_unlock(&platform->attachment_list); + + RAISE_ERROR(BADARG_ATOM); +} + +void storage_nif_init(GlobalContext *global) {} + +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/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..d06df89bb --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_mount.erl @@ -0,0 +1,55 @@ +% +% 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 = mount_working_sdmmc(), + ok = umount_prev(), + ok = mount_missing_fat_partition(), + ok = umount_prev(). + +mount_working_sdmmc() -> + ok = 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 = umount_missing(), + ok = esp:umount("/test"). + +mount_missing_fat_partition() -> + {error, esp_err_not_found} = esp:mount("/dev/partition/by-name/missingpart", "/test", fat, []), + ok. + +umount_missing() -> + try esp:umount("/testm") of + ok -> error + catch + error:badarg -> ok; + _:_ -> not_badarg + end. + +umount_prev() -> + try esp:umount("/test") 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;