Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SD/MMC/FAT partition mouting with esp:mount/4 and umount #1289

Merged
merged 3 commits into from
Oct 8, 2024

Conversation

bettio
Copy link
Collaborator

@bettio bettio commented Sep 27, 2024

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@bettio bettio changed the title Esp mount WIP: Esp mount Sep 28, 2024
main.avm, data, phy, 0x210000, 0x100000
boot.avm, data, phy, 0x1D0000, 0x80000,
main.avm, data, phy, 0x250000, 0x80000
fatpart, data, phy, 0x2D0000, 0x100000
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petermm: honestly it is still totally WIP, I just pushed stuff for having a few CI runs ;)

@bettio bettio force-pushed the esp-mount branch 2 times, most recently from d52cf89 to 6784189 Compare September 28, 2024 17:01
@petermm
Copy link
Contributor

petermm commented Sep 28, 2024

confirmed working taking image and saving to sd card on esp32cam:

  def start() do
    :ok = :esp32cam.init()
    IO.inspect("Camera initialized.~n")
    :ok = :esp.mount("sdmmc", "/test", :fat, [])
    loop(1)
  end

  def loop(count) do
    {:ok, image} = :esp32cam.capture()
    path = "/test/img_" <> Integer.to_string(count) <> ".jpg"

    case :atomvm.posix_open(path, [:o_wronly, :o_creat, :o_excl], 0o644) do
      {:ok, fd} ->
        {:ok, l} = :atomvm.posix_write(fd, image)
        :atomvm.posix_close(fd)

      _ ->
        IO.inspect("some error")
    end

    IO.inspect("Captured image.  size=#{:erlang.byte_size(image)}")
    loop(count + 1)
  end

@bettio bettio changed the title WIP: Esp mount SD/FAT partition mouting with esp:mount/4 and umount Sep 29, 2024
@bettio bettio changed the title SD/FAT partition mouting with esp:mount/4 and umount SD/MMC/FAT partition mouting with esp:mount/4 and umount Sep 29, 2024
@bettio bettio requested a review from petermm September 29, 2024 10:24
@@ -114,6 +124,8 @@ struct ESP32PlatformData
#endif
mbedtls_ctr_drbg_context random_ctx;
bool random_is_initialized;

struct SyncList misc_entries;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This not the most elegant solution maybe, but for sure avoids changing multiple times ESP32PlatformData.
I suggest keeping this for attaching structs that are not so frequently used, such as mount points, for which a linear search is good enough.

@pguyot on a second thought misc_entries might be not the best name, what about attachments_list?

target, &host_config, &slot_config, &mount_config, &mount->handle.card);
#endif

} else if (!strcmp(source, "sdspi")) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help for manual testing this might be appreciated.

Copy link
Collaborator

@UncleGrumpy UncleGrumpy Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried with several devices but it seems none of the ones I own use SDMMC_HOST_DEFAULT() or SDSPI_HOST_DEFAULT() pins, so for wide spread compatibility these will need to be configurable.

@bettio
Copy link
Collaborator Author

bettio commented Sep 29, 2024

confirmed working taking image and saving to sd card on esp32cam:

  def start() do
    :ok = :esp32cam.init()
    IO.inspect("Camera initialized.~n")
    :ok = :esp.mount("sdmmc", "/test", :fat, [])
    loop(1)
  end

  def loop(count) do
    {:ok, image} = :esp32cam.capture()
    path = "/test/img_" <> Integer.to_string(count) <> ".jpg"

    case :atomvm.posix_open(path, [:o_wronly, :o_creat, :o_excl], 0o644) do
      {:ok, fd} ->
        {:ok, l} = :atomvm.posix_write(fd, image)
        :atomvm.posix_close(fd)

      _ ->
        IO.inspect("some error")
    end

    IO.inspect("Captured image.  size=#{:erlang.byte_size(image)}")
    loop(count + 1)
  end

Can you re-run this test after my recent changes?

libs/eavmlib/src/esp.erl Outdated Show resolved Hide resolved
libs/eavmlib/src/esp.erl Outdated Show resolved Hide resolved
src/platforms/esp32/components/avm_builtins/CMakeLists.txt Outdated Show resolved Hide resolved
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of altering platform data, why don't you use a resource? It would unmount if garbage collected, and would make iterating on the list of mount points unnecessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was one of my thoughts, but I wanted to mimic the same semantic that exists on regular unix where disks are unmounted using the path as handle, so in case we generalize our API we are already support the same semantics.

But, after your feedback, I was thinking that one day we can store paths inside of refs on systems that uses paths as handles for unmounting.

Does it makes sense to you?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. If the system requires paths to unmount, it is unlikely very short on memory so we can maintain a copy around of the path in the resource. While on ESP32, a resource will consume a little less than the linked list with the copy of the path.

Copy link
Collaborator Author

@bettio bettio Sep 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pguyot on a 3rd thought there are other problems with this approach:

  • Basically open files are resources, so we need to make sure that they are closed and garbage collected before running umount.
  • I dind't check if umounting can cause errors when files are open, but I don't think there is an option to postpone destructor and running it again when all files have been closed, in order to try umount a second time
  • I'm not sure if performing I/O (that might also fail) from a resource destructor is a good idea, but here I can change my mind

In short explicit umount I think gives a better control over operation order and error handling

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never thought umount should only be implicit. It should be explicit and resource garbage collection should serve as a fallback. The same thing is true with files. Close should be explicitely called.

If necessary, we can increase the ref count of resources that need to be destroyed later. But in this case, it shouldn't be the case. It's a programming error to not explicitely call umount or close, but an error that doesn't leak with a resource (and does without a resource).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I fixed this comment.


struct MountedFS *mount = malloc(sizeof(struct MountedFS));
mount->misc_entry.entry_type = (uintptr_t) nif_esp_mount;
mount->base_path = strdup(target);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is freed, but if this is moved to a resource we probably no longer need it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a memory leak for sure, indeed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we could avoid the allocation of strdup here by simply moving ownership of the pointer (e.g. base_path = target; target = NULL;).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed.

@bettio bettio force-pushed the esp-mount branch 2 times, most recently from 81410bb to 333b769 Compare September 29, 2024 18:37
return make_esp_error_tuple(ret, ctx);
}

mounted_fs->mount_type = Unmounted;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pguyot I'm looking for a suggestion here, should I protect the MountedFS resource with some kind of lock?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After analysis, I believe we need some kind of protection because esp idf function esp_vfs_fat_spiflash_unmount_rw_wl doesn't look thread safe to me.

https://github.com/espressif/esp-idf/blob/46acfdce969f03c02b001fe4d24fa9e98f6adc5e/components/fatfs/vfs/vfs_fat_spiflash.c#L214

If we call it twice with the same value close enough, first core would free the pointer and second would try to free it before it's nullified.

The lock could be implemented around mount_type with a special umounting type, or just setting umounted before it's actually umounted, since we don't have the umounted => mounted transition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the end of the day I realized that using a SpinLock was guaranteed to be safe and headache free.

Source :: unicode:chardata(),
Target :: unicode:chardata(),
FS :: fat,
Opts :: list(proplists:property()) | #{atom() => term()}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proplists:proplist() is directly a list.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


char *target = interop_term_to_string(target_term, &str_ok);
if (!str_ok || strlen(target) > 8) {
RAISE_ERROR(BADARG_ATOM);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we free source here before returning an invalid term?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and sorry for the pedantic repeated mistake, I think I fixed all of them.


struct MountedFS *mount = malloc(sizeof(struct MountedFS));
mount->misc_entry.entry_type = (uintptr_t) nif_esp_mount;
mount->base_path = strdup(target);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we could avoid the allocation of strdup here by simply moving ownership of the pointer (e.g. base_path = target; target = NULL;).

src/platforms/esp32/components/avm_builtins/storage_nif.c Outdated Show resolved Hide resolved
return make_esp_error_tuple(ret, ctx);
}

mounted_fs->mount_type = Unmounted;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After analysis, I believe we need some kind of protection because esp idf function esp_vfs_fat_spiflash_unmount_rw_wl doesn't look thread safe to me.

https://github.com/espressif/esp-idf/blob/46acfdce969f03c02b001fe4d24fa9e98f6adc5e/components/fatfs/vfs/vfs_fat_spiflash.c#L214

If we call it twice with the same value close enough, first core would free the pointer and second would try to free it before it's nullified.

The lock could be implemented around mount_type with a special umounting type, or just setting umounted before it's actually umounted, since we don't have the umounted => mounted transition.

@bettio bettio force-pushed the esp-mount branch 3 times, most recently from 378bfd9 to 2990608 Compare October 2, 2024 22:45
@bettio bettio requested a review from pguyot October 2, 2024 22:45
@petermm
Copy link
Contributor

petermm commented Oct 3, 2024

hmm :esp.mount("sdspi", "/test", :fat, spi_host: spi, cs: 5) returns {:ok, ""} now and then just stalls.

@bettio bettio force-pushed the esp-mount branch 3 times, most recently from 100b5d4 to 20ee7d6 Compare October 3, 2024 22:01
union
{
sdmmc_card_t *card;
wl_handle_t wl;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why not use wear leveling for all mounts? It is supported on SD and flash, for both SPIFFS and FatFS.

It's not in the scope of this PR obviously, but I think it would be very beneficial to explore using a filesystem and wear leveling for the application partition, so we do not prematurely wear out the first few sectors of the app partition from heavy rewrites of the same few blocks at the beginning of the partition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it depends on the kind of usage you are planning.
However I would rather iterate.

@bettio
Copy link
Collaborator Author

bettio commented Oct 5, 2024

Tested with spiflash on both esp-idf v5 and v4.
It mounts correctly, opendir and open/write/close work correctly too.

@petermm
Copy link
Contributor

petermm commented Oct 5, 2024

Not working here sdspi:

  def start do
    spi_settings = [
      bus_config: [
        miso: 19,
        mosi: 23,
        sclk: 18,
        peripheral: "spi3"
      ]
    ]
    spi = :spi.open(spi_settings)
    IO.inspect("spi")
    ab = :esp.mount("sdspi", "/test", :fat, spi_host: spi, cs: 5)
    IO.inspect(ab)
    path = "/test/test.txt"
    IO.inspect("start")

    case :atomvm.posix_open(path, [:o_wronly, :o_creat, :o_excl], 0o644) do
      {:ok, fd} ->
        {:ok, l} = :atomvm.posix_write(fd, "My String")
        IO.inspect(:atomvm.posix_close(fd))
        IO.inspect("done")

      err ->
        IO.inspect(err)
        IO.inspect("some error")
    end

    :ok = :esp.umount("/test")

gives:


AtomVM init.
I (2616) sys: Loaded BEAM partition main.avm at address 0x250000 (size=1048576 bytes)
Starting application...
I (2746) spi_driver: SPI Bus initialized.
"spi"
I (2766) gpio: GPIO[5]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
{:ok, ""}
I (2846) gpio: GPIO[5]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
"start"
{:error, :enoent}
"some error"
CRASH

Don't understand the return value of :esp.mount being {:ok, ""}, the two gpio: GPIO[5] logs suggests to me the spi is being torn down somehow.. - worked before, but not currently (in my testing)

%% @doc Unmounts filesystem located at given path
%% @end
%%-----------------------------------------------------------------------------
-spec umount(Target :: unicode:chardata()) -> ok.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec needs to be updated, as it's a resource now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Target the path of the mounted filesystem that should be unmounted
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation needs to be updated as it's a resource now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Target :: unicode:chardata(),
FS :: fat,
Opts :: proplists:proplist() | #{atom() => term()}
) -> ok.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec of return type needs to be updated

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

%% @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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation of return type needs to be updated

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Add missing `export_type([...])` and missing `proplists` type

Signed-off-by: Davide Bettio <[email protected]>
Increase factory app partition size.

Signed-off-by: Davide Bettio <[email protected]>
Target :: unicode:chardata(),
FS :: fat,
Opts :: proplists:proplist() | #{atom() => term()}
) -> {ok, mounted_fs()} | {error, Reason}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is related to the CI build error, but you might want to specify Reason :: term() here, and again below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's the quick typical lunch break change mistake !

%% @doc Unmounts filesystem located at given path
%% @end
%%-----------------------------------------------------------------------------
-spec umount(Target :: mounted_fs()) -> ok | {error, Reason}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

term_put_tuple_element(return_term, 0, OK_ATOM);
term_put_tuple_element(return_term, 1, mount_term);
}
enif_release_resource(mount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not working sdmmc nor sdspi.

moving enif_release_resource(mount); down into the umount function makes it all work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petermm this surprises me, and probably @pguyot can give an hint here.

  • enif_alloc_resource gives a resources that has already the ref counter set to 1.
  • enif_make_resource increments it to 2
  • we need enif_make_resource at line 273 to get it back to 1, so it can GC later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh my, it appears to have been build gremlins somehow, despite everything being cleaned out:/

(changed down to otp26 and it started working, going back to otp27 kept it working :/)

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 <[email protected]>
Copy link
Contributor

@petermm petermm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested successfully sdmmc and sdspi.

@bettio bettio merged commit 9be487f into atomvm:release-0.6 Oct 8, 2024
92 of 93 checks passed
petermm added a commit to petermm/AtomVM that referenced this pull request Oct 11, 2024
Adds a Storage section to the Programmers guide, with the recently added esp:mount/umount atomvm#1289

Signed-off-by: Peter M <[email protected]>
petermm added a commit to petermm/AtomVM that referenced this pull request Oct 11, 2024
Adds a Storage section to the Programmers guide, with the recently added esp:mount/umount atomvm#1289

Signed-off-by: Peter M <[email protected]>
petermm added a commit to petermm/AtomVM that referenced this pull request Oct 11, 2024
Adds a Storage section to the Programmers guide, with the recently added esp:mount/umount atomvm#1289

Signed-off-by: Peter M <[email protected]>
petermm added a commit to petermm/AtomVM that referenced this pull request Oct 12, 2024
Adds a Storage section to the Programmers guide, with the recently added esp:mount/umount atomvm#1289

Signed-off-by: Peter M <[email protected]>
bettio added a commit that referenced this pull request Oct 12, 2024
Add esp:mount/umount to programmers-guide.md

Adds a Storage section to the Programmers guide Esp section
https://www.atomvm.net/doc/main/programmers-guide.html#esp32-specific-apis ,
with the recently added esp:mount/umount
#1289

Bit unsure of the Internal flash part, but should be good.

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants