Skip to content

Commit

Permalink
ESP32: add support for mounting/umounting stroage
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
bettio committed Sep 29, 2024
1 parent 905adb7 commit 81410bb
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")`
- Support for `code:ensure_loaded/1`
- Support for `io_lib:latin1_char_list/1`
- Add support to Elixir for `Keyword.split/2`
- Support for mounting/unmounting storage on ESP32 (such as SD or internal flash) using
`esp:mount/4` and `esp:umount/1`

### Changed

Expand Down
30 changes: 30 additions & 0 deletions libs/eavmlib/src/esp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/platforms/esp32/components/avm_builtins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}
)

Expand Down
4 changes: 4 additions & 0 deletions src/platforms/esp32/components/avm_builtins/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
299 changes: 299 additions & 0 deletions src/platforms/esp32/components/avm_builtins/storage_nif.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/*
* This file is part of AtomVM.
*
* Copyright 2024 Davide Bettio <[email protected]>
*
* 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 <sdkconfig.h>
#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS

#include <atom.h>
#include <defaultatoms.h>
#include <interop.h>
#include <memory.h>
#include <nifs.h>
#include <term.h>

#include "esp32_sys.h"

#include <stdlib.h>

// file write test
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <driver/sdmmc_host.h>
#include <driver/sdspi_host.h>
#include <esp_vfs_fat.h>

#include <trace.h>

#include "spi_driver.h"

// TODO: allow ro option
enum mount_type
{
FATSPIFlash,
FATSDSPI,
FATSDMMC
};

struct MountedFS
{
struct MiscEntry misc_entry;
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->misc_entry.entry_type = (uintptr_t) nif_esp_mount;
mount->base_path = strdup(target);
mount->mount_type = FATSPIFlash;
synclist_append(&platform->misc_entries, &mount->misc_entry.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->misc_entry.entry_type = (uintptr_t) nif_esp_mount;
mount->base_path = strdup(target);
mount->mount_type = FATSDMMC;
synclist_append(&platform->misc_entries, &mount->misc_entry.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->misc_entry.entry_type = (uintptr_t) nif_esp_mount;
mount->base_path = strdup(target);
mount->mount_type = FATSPIFlash;
synclist_append(&platform->misc_entries, &mount->misc_entry.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 *misc_entries = synclist_wrlock(&platform->misc_entries);

struct ListHead *item;
struct ListHead *tmp;
MUTABLE_LIST_FOR_EACH (item, tmp, misc_entries) {
struct MiscEntry *entry = GET_LIST_ENTRY(item, struct MiscEntry, list_head);
if (entry->entry_type == (uintptr_t) nif_esp_mount) {
struct MountedFS *mount = CONTAINER_OF(entry, struct MountedFS, misc_entry);
if (!strcmp(source, mount->base_path)) {
term ret = do_umount(mount, ctx);
if (ret == OK_ATOM) {
list_remove(&mount->misc_entry.list_head);
free(mount);
}
synclist_unlock(&platform->misc_entries);
return ret;
}
}
}

synclist_unlock(&platform->misc_entries);

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
Loading

0 comments on commit 81410bb

Please sign in to comment.