From e190b09895e78ba916dbfa0cb4c36cbcd9d82e94 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 24 Jul 2026 08:34:50 +0000 Subject: [PATCH 1/4] Fix term_is_uint32 bigint handling on 32-bit term_is_uint32 was returning true on 32-bit builds for big integers whose low 64 bits fit uint32_t, so term_to_uint32 truncated them. Signed-off-by: Davide Bettio --- CHANGELOG.md | 3 +++ src/libAtomVM/term.h | 2 +- tests/erlang_tests/test_crc32.erl | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecedd437fc..d0b1717b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where negative or oversized segment sizes were not rejected in binary matching - Fixed the `network` mdns configuration to read the documented `host` key; the previously required, undocumented `hostname` key is still accepted +- Fixed `term_is_uint32` accepting big integers whose low 64 bits are within range on 32-bit + builds, which made `erlang:crc32/2`, `erlang:crc32_combine/3` and `crypto:pbkdf2_hmac/5` + silently truncate huge integer arguments instead of raising `badarg` ## [0.7.0-alpha.1] - 2026-04-06 diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 40e943b415..46038f6a1e 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -1746,7 +1746,7 @@ static inline bool term_is_uint32(term t) #endif #if BOXED_TERMS_REQUIRED_FOR_INT != BOXED_TERMS_REQUIRED_FOR_INT64 - } else { + } else if (term_boxed_size(t) == BOXED_TERMS_REQUIRED_FOR_INT64) { avm_int64_t unboxed64 = term_unbox_int64(t); return unboxed64 <= UINT32_MAX; #endif diff --git a/tests/erlang_tests/test_crc32.erl b/tests/erlang_tests/test_crc32.erl index 4b973ea374..3841495f50 100644 --- a/tests/erlang_tests/test_crc32.erl +++ b/tests/erlang_tests/test_crc32.erl @@ -110,6 +110,15 @@ test_crc32_badarg() -> ok = expect_badarg(fun() -> erlang:crc32_combine(0, 0, ?MODULE:id(-1)) end), ok = expect_badarg(fun() -> erlang:crc32_combine(?MODULE:id(16#100000000), 0, 0) end), ok = expect_badarg(fun() -> erlang:crc32_combine(0, ?MODULE:id(16#100000000), 0) end), + + % big integers whose low 64 bits are small must not pass the uint32 check + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(1 bsl 64), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id((1 bsl 64) + 7), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(-(1 bsl 64)), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(1 bsl 130), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(?MODULE:id(1 bsl 64), 0, 0) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(0, ?MODULE:id((1 bsl 64) + 7), 0) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(0, 0, ?MODULE:id(1 bsl 64)) end), ok. test_crc32_boundary() -> From 6c321aabb1c8de6a3e34950a0e6f3713fc472e9d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 24 Jul 2026 08:34:56 +0000 Subject: [PATCH 2/4] Add C unit tests for term.h integer conversions Check the term_is_* integer predicates and term_to_* conversions against per-case expected flags, and run the new test-term binary in CI. Signed-off-by: Davide Bettio --- .github/workflows/build-and-test-macos.yaml | 5 + .../workflows/build-and-test-on-freebsd.yaml | 7 + .github/workflows/build-and-test.yaml | 13 + .github/workflows/build-linux-artifacts.yaml | 3 + tests/CMakeLists.txt | 10 + tests/test-term.c | 272 ++++++++++++++++++ 6 files changed, 310 insertions(+) create mode 100644 tests/test-term.c diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index 6738be264a..1d18a21030 100644 --- a/.github/workflows/build-and-test-macos.yaml +++ b/.github/workflows/build-and-test-macos.yaml @@ -254,6 +254,11 @@ jobs: run: | ./tests/test-structs + - name: "Test: test-term" + working-directory: build + run: | + ./tests/test-term + - name: "Test: test_etest.avm" timeout-minutes: 5 working-directory: build diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index f22718ecc2..d7a3b49407 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -157,6 +157,13 @@ jobs: cd build ./tests/test-structs + - name: "Test: test-term" + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE; + cd build + ./tests/test-term + - name: "Test: test_etest.avm" shell: freebsd {0} run: | diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index f487ea0cf3..48de1aa824 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -875,6 +875,19 @@ jobs: ulimit -c unlimited ./tests/test-structs + - name: "Test: test-term with valgrind" + if: matrix.library-arch == '' + working-directory: build + run: | + ulimit -c unlimited + valgrind --error-exitcode=1 ./tests/test-term + + - name: "Test: test-term" + working-directory: build + run: | + ulimit -c unlimited + ./tests/test-term + - name: "Test: test_etest.avm with valgrind" if: matrix.library-arch == '' timeout-minutes: 5 diff --git a/.github/workflows/build-linux-artifacts.yaml b/.github/workflows/build-linux-artifacts.yaml index ff9018cc70..cd65b9cc87 100644 --- a/.github/workflows/build-linux-artifacts.yaml +++ b/.github/workflows/build-linux-artifacts.yaml @@ -236,6 +236,7 @@ jobs: make test-heap && make test-mailbox && make test-structs && + make test-term && file ./tests/test-erlang && ./tests/test-erlang -s prime_smp && file ./tests/test-enif && @@ -246,6 +247,8 @@ jobs: ./tests/test-mailbox && file ./tests/test-structs && ./tests/test-structs && + file ./tests/test-term && + ./tests/test-term && file ./src/AtomVM && ./src/AtomVM tests/libs/etest/test_etest.avm && ./src/AtomVM tests/libs/estdlib/test_estdlib.avm && diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c7f880c49..2ba0944bab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(test-heap test-heap.c) add_executable(test-jit_stream_flash test-jit_stream_flash.c ../src/libAtomVM/jit_stream_flash.c) add_executable(test-mailbox test-mailbox.c) add_executable(test-structs test-structs.c) +add_executable(test-term test-term.c) target_compile_features(test-erlang PUBLIC c_std_11) target_compile_features(test-enif PUBLIC c_std_11) @@ -34,6 +35,7 @@ target_compile_features(test-heap PUBLIC c_std_11) target_compile_features(test-jit_stream_flash PUBLIC c_std_11) target_compile_features(test-mailbox PUBLIC c_std_11) target_compile_features(test-structs PUBLIC c_std_11) +target_compile_features(test-term PUBLIC c_std_11) if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-erlang PUBLIC -Wall -pedantic -Wextra -ggdb) @@ -42,6 +44,7 @@ if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-jit_stream_flash PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-mailbox PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-structs PUBLIC -Wall -pedantic -Wextra -ggdb) + target_compile_options(test-term PUBLIC -Wall -pedantic -Wextra -ggdb) endif() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") @@ -56,6 +59,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_link_libraries(test-jit_stream_flash PRIVATE ${LIBRT}) target_link_libraries(test-mailbox PRIVATE ${LIBRT}) target_link_libraries(test-structs PRIVATE ${LIBRT}) + target_link_libraries(test-term PRIVATE ${LIBRT}) else() # might also be in libc check_library_exists(c clock_gettime "" HAVE_CLOCK_GETTIME) @@ -70,6 +74,7 @@ if (MbedTLS_FOUND) target_link_libraries(test-jit_stream_flash PRIVATE MbedTLS::mbedtls) target_link_libraries(test-mailbox PRIVATE MbedTLS::mbedtls) target_link_libraries(test-structs PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-term PRIVATE MbedTLS::mbedtls) endif() set( @@ -87,6 +92,7 @@ if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR target_include_directories(test-jit_stream_flash PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-mailbox PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-structs PRIVATE ../src/platforms/generic_unix/lib) + target_include_directories(test-term PRIVATE ../src/platforms/generic_unix/lib) else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") endif() @@ -97,6 +103,7 @@ target_include_directories(test-heap PRIVATE ../src/libAtomVM) target_include_directories(test-jit_stream_flash PRIVATE ../src/libAtomVM ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(test-mailbox PRIVATE ../src/libAtomVM) target_include_directories(test-structs PRIVATE ../src/libAtomVM) +target_include_directories(test-term PRIVATE ../src/libAtomVM) target_link_libraries(test-erlang PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-enif PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-heap PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) @@ -108,6 +115,7 @@ endif() target_link_libraries(test-jit_stream_flash PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-mailbox PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-structs PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) +target_link_libraries(test-term PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) # Except for XCode, also compile beams if (NOT "${CMAKE_GENERATOR}" MATCHES "Xcode") @@ -133,12 +141,14 @@ if (COVERAGE) append_coverage_compiler_flags_to_target(test-jit_stream_flash) append_coverage_compiler_flags_to_target(test-mailbox) append_coverage_compiler_flags_to_target(test-structs) + append_coverage_compiler_flags_to_target(test-term) append_coverage_linker_flags_to_target(test-erlang) append_coverage_linker_flags_to_target(test-enif) append_coverage_linker_flags_to_target(test-heap) append_coverage_linker_flags_to_target(test-jit_stream_flash) append_coverage_linker_flags_to_target(test-mailbox) append_coverage_linker_flags_to_target(test-structs) + append_coverage_linker_flags_to_target(test-term) if (CMAKE_COMPILER_IS_GNUCC) setup_target_for_coverage_lcov(NAME coverage EXECUTABLE test-erlang DEPENDENCIES test-erlang) endif() diff --git a/tests/test-term.c b/tests/test-term.c new file mode 100644 index 0000000000..53d2021161 --- /dev/null +++ b/tests/test-term.c @@ -0,0 +1,272 @@ +/* + * 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 + */ + +#include +#include +#include +#include + +#include "context.h" +#include "globalcontext.h" +#include "intn.h" +#include "memory.h" +#include "term.h" +#include "utils.h" + +// Enough room for all terms created by a single test function, so that no GC +// runs between term creation and the checks +#define TEST_HEAP_SIZE 256 + +enum +{ + IsIntegerOn32Bit = 1 << 0, + IsIntegerOn64Bit = 1 << 1, + IsPositive = 1 << 2, + IsNegative = 1 << 3, + IsUint8 = 1 << 4, + IsInt32 = 1 << 5, + IsUint32 = 1 << 6, + IsInt64 = 1 << 7, + IsUint64 = 1 << 8, + IsBigint = 1 << 9 +}; + +static bool flags_is_immediate(unsigned int flags) +{ +#if TERM_BITS == 32 + return (flags & IsIntegerOn32Bit) != 0; +#elif TERM_BITS == 64 + return (flags & IsIntegerOn64Bit) != 0; +#else +#error "Unsupported TERM_BITS" +#endif +} + +static void assert_integer_predicates(term t, unsigned int flags) +{ + bool is_immediate = flags_is_immediate(flags); + bool is_positive = (flags & IsPositive) != 0; + bool is_negative = (flags & IsNegative) != 0; + + assert(term_is_any_integer(t)); + assert(term_is_number(t)); + assert(term_is_int(t) == is_immediate); + assert(term_is_boxed_integer(t) == !is_immediate); + + assert(term_is_non_neg_int(t) == (is_immediate && !is_negative)); + assert(term_is_pos_int(t) == (is_immediate && is_positive)); + assert(term_is_neg_int(t) == (is_immediate && is_negative)); + + assert(term_is_pos_boxed_integer(t) == (!is_immediate && !is_negative)); + assert(term_is_neg_boxed_integer(t) == (!is_immediate && is_negative)); + if (!is_immediate) { + assert(term_boxed_integer_sign(t) + == (is_negative ? TermNegativeInteger : TermPositiveInteger)); + } + + assert(term_is_any_non_neg_integer(t) == !is_negative); + assert(term_is_any_pos_integer(t) == is_positive); + assert(term_is_any_neg_integer(t) == is_negative); + + assert(term_is_bigint(t) == ((flags & IsBigint) != 0)); +} + +static term make_bigint_term( + Context *ctx, const intn_digit_t *digits, size_t len, term_integer_sign_t sign) +{ + size_t count = intn_count_digits(digits, len); + size_t intn_data_size; + size_t rounded_num_len; + term_bigint_size_requirements(count, &intn_data_size, &rounded_num_len); + + term t = term_create_uninitialized_bigint(intn_data_size, sign, &ctx->heap); + term_initialize_bigint(t, digits, count, rounded_num_len); + return t; +} + +static void test_int64_range_checks(void) +{ + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + enum MemoryGCResult res = memory_ensure_free(ctx, TEST_HEAP_SIZE); + assert(res == MEMORY_GC_OK); + + static const struct + { + int64_t value; + unsigned int flags; + } cases[] = { + { 0, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsUint8 | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + { 1, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { -1, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { 42, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { -42, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { 255, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { 256, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + // 2^27 - 1 and -2^27: immediate boundary on 32-bit builds + { 0x07FFFFFF, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + { 0x08000000, IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 | IsUint64 }, + { -0x08000000, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { -0x08000001, IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { INT32_MAX, IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 | IsUint64 }, + { INT32_MIN, IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { (int64_t) INT32_MAX + 1, IsIntegerOn64Bit | IsPositive | IsUint32 | IsInt64 | IsUint64 }, + { (int64_t) INT32_MIN - 1, IsIntegerOn64Bit | IsNegative | IsInt64 }, + { UINT32_MAX, IsIntegerOn64Bit | IsPositive | IsUint32 | IsInt64 | IsUint64 }, + { (int64_t) UINT32_MAX + 1, IsIntegerOn64Bit | IsPositive | IsInt64 | IsUint64 }, + // 2^59 - 1 and -2^59: immediate boundary on 64-bit builds + { 0x07FFFFFFFFFFFFFF, IsIntegerOn64Bit | IsPositive | IsInt64 | IsUint64 }, + { 0x0800000000000000, IsPositive | IsInt64 | IsUint64 }, + { -0x0800000000000000, IsIntegerOn64Bit | IsNegative | IsInt64 }, + { -0x0800000000000001, IsNegative | IsInt64 }, + { INT64_MAX, IsPositive | IsInt64 | IsUint64 }, + { INT64_MIN, IsNegative | IsInt64 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int64_t value = cases[i].value; + unsigned int flags = cases[i].flags; + term t = term_make_maybe_boxed_int64(value, &ctx->heap); + + assert_integer_predicates(t, flags); + + if (flags_is_immediate(flags)) { + assert(term_is_integer(t) == true); + assert(term_to_int(t) == (avm_int_t) value); + } else { + assert(term_is_integer(t) == false); + } + if (flags & IsUint8) { + assert(term_is_uint8(t) == true); + assert(term_to_uint8(t) == (uint8_t) value); + } else { + assert(term_is_uint8(t) == false); + } + if (flags & IsInt32) { + assert(term_is_int32(t) == true); + assert(term_to_int32(t) == (int32_t) value); + } else { + assert(term_is_int32(t) == false); + } + if (flags & IsUint32) { + assert(term_is_uint32(t) == true); + assert(term_to_uint32(t) == (uint32_t) value); + } else { + assert(term_is_uint32(t) == false); + } + if (flags & IsInt64) { + assert(term_is_int64(t) == true); + assert(term_to_int64(t) == value); + } else { + assert(term_is_int64(t) == false); + } + if (flags & IsUint64) { + assert(term_is_uint64(t) == true); + assert(term_to_uint64(t) == (uint64_t) value); + } else { + assert(term_is_uint64(t) == false); + } + } + + context_destroy(ctx); + globalcontext_destroy(glb); +} + +static void test_bigint_range_checks(void) +{ + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + enum MemoryGCResult res = memory_ensure_free(ctx, TEST_HEAP_SIZE); + assert(res == MEMORY_GC_OK); + + static const struct + { + intn_digit_t digits[INTN_MAX_IN_LEN]; + size_t len; + unsigned int flags; + uint64_t uint64_value; + } cases[] = { + // 2^63 + { { 0x00000000, 0x80000000 }, 2, IsPositive | IsBigint | IsUint64, UINT64_C(1) << 63 }, + // 2^64 - 1 + { { 0xFFFFFFFF, 0xFFFFFFFF }, 2, IsPositive | IsBigint | IsUint64, UINT64_MAX }, + // 2^64: low 64 bits are 0 + { { 0x00000000, 0x00000000, 0x00000001 }, 3, IsPositive | IsBigint, 0 }, + // 2^64 + 7: low 64 bits are 7 + { { 0x00000007, 0x00000000, 0x00000001 }, 3, IsPositive | IsBigint, 0 }, + // 2^65 + 2^40 + { { 0x00000000, 0x00000100, 0x00000002 }, 3, IsPositive | IsBigint, 0 }, + // -(2^63 + 1) + { { 0x00000001, 0x80000000 }, 2, IsNegative | IsBigint, 0 }, + // -(2^64) + { { 0x00000000, 0x00000000, 0x00000001 }, 3, IsNegative | IsBigint, 0 }, + // 2^256 - 1: largest supported magnitude + { { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, + 0xFFFFFFFF }, + 8, IsPositive | IsBigint, 0 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + unsigned int flags = cases[i].flags; + term_integer_sign_t sign = (flags & IsNegative) ? TermNegativeInteger : TermPositiveInteger; + term t = make_bigint_term(ctx, cases[i].digits, cases[i].len, sign); + + assert_integer_predicates(t, flags); + + assert(term_is_uint8(t) == false); + assert(term_is_int32(t) == false); + assert(term_is_uint32(t) == false); + assert(term_is_int64(t) == false); + if (flags & IsUint64) { + assert(term_is_uint64(t) == true); + assert(term_to_uint64(t) == cases[i].uint64_value); + } else { + assert(term_is_uint64(t) == false); + } + } + + context_destroy(ctx); + globalcontext_destroy(glb); +} + +int main(int argc, char **argv) +{ + UNUSED(argc); + UNUSED(argv); + + test_int64_range_checks(); + test_bigint_range_checks(); + + return EXIT_SUCCESS; +} From c9f75b358a67fcc4d8cb4291ea59bb8a2b7f388a Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 22 Jun 2026 15:25:05 +0000 Subject: [PATCH 3/4] 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 1e434abc93933b9f6e9f2c2a80de99b050a2f342 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 22 Jun 2026 15:48:43 +0000 Subject: [PATCH 4/4] 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 | 52 ++ libs/avm_esp32/src/esp.erl | 88 ++++ .../esp32/components/avm_builtins/Kconfig | 4 + .../components/avm_builtins/storage_nif.c | 478 +++++++++++++++++- .../components/avm_sys/include/esp32_sys.h | 3 + .../test/main/test_erl_sources/CMakeLists.txt | 2 + .../main/test_erl_sources/test_sdcard.erl | 105 ++++ src/platforms/esp32/test/main/test_main.c | 8 + 9 files changed, 740 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 d0b1717b0d..4c45240685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 sending to an alias reference - 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 - Added a Linux `gpio` driver for the generic_unix port (in `avm_unix`) using sysfs diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index d3cc115b51..391a7c078e 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -1394,6 +1394,58 @@ These functions allow you to work with external storage devices or partitions on 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), +``` + +All `esp:sdcard_*` functions are synchronous: like `esp:mount/4`, they run on the scheduler +that executes the calling process and block it for the duration of the card transaction +(typically milliseconds). + +```{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. A card opened with `"sdspi"` must be closed with +`esp:sdcard_close/1` before the SPI bus is closed with `spi:close/1`: closing the bus first +leaves the SPI host unusable until the device is restarted. +``` + ### Restart and Sleep You can use the [`esp:restart/0`](./apidocs/erlang/eavmlib/esp.md#restart0) function to immediately restart the ESP32 device. This function does not return a value. diff --git a/libs/avm_esp32/src/esp.erl b/libs/avm_esp32/src/esp.erl index fbc2468ec5..a393278d77 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() :: reference(). + -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,86 @@ 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. +%% +%% A card opened with `"sdspi"' must be closed before the SPI bus is +%% closed with `spi:close/1'. +%% @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..d8fcdb5bd4 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,7 +38,11 @@ #include #include #include +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS #include +#endif +#include +#include #include #include @@ -57,6 +61,7 @@ #define SMP_UNLOCK(mounted_fs) #endif +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS // TODO: allow ro option enum mount_type { @@ -86,6 +91,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 +104,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 +112,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 @@ -252,6 +260,7 @@ static bool sdcard_config_from_source( } #endif +#ifdef CONFIG_AVM_ENABLE_SPI_PORT_DRIVER if (!strcmp(source, "sdspi")) { sdmmc_host_t host_config = SDSPI_HOST_DEFAULT(); sdspi_device_config_t spi_slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); @@ -284,10 +293,32 @@ static bool sdcard_config_from_source( cfg->slot.spi_dev = spi_slot_config; return true; } +#endif return false; } +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +struct SDCardBlockDevice +{ +#ifndef AVM_NO_SMP + Mutex *lock; +#endif + bool open; + uint32_t sector_size; + uint32_t sector_count; + sdmmc_card_t *card; +}; + +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 +520,389 @@ static void mounted_fs_dtor(ErlNifEnv *caller_env, void *obj) free(mounted_fs->base_path); } +#endif + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +// TODO: switch to the new ESP-IDF blockdev API once the API issues are fixed and it is +// proven working: https://github.com/espressif/esp-idf/issues/18875 +static esp_err_t sdcard_blockdev_read_sector( + struct SDCardBlockDevice *dev, void *dst, size_t sector) +{ + return sdmmc_read_sectors(dev->card, dst, sector, 1); +} + +static esp_err_t sdcard_blockdev_write_sector( + struct SDCardBlockDevice *dev, const void *src, size_t sector) +{ + return sdmmc_write_sectors(dev->card, src, sector, 1); +} + +// Mirrors call_host_deinit() in ESP-IDF's vfs_fat_sdmmc.c: per-device/per-slot +// teardown where the host provides it (IDF >= 5.3), forceful host deinit otherwise. +static esp_err_t sdcard_host_deinit(const sdmmc_host_t *host) +{ + if (host->flags & SDMMC_HOST_FLAG_DEINIT_ARG) { + return host->deinit_p(host->slot); + } + return host->deinit(); +} + +static esp_err_t sdcard_blockdev_close(struct SDCardBlockDevice *dev) +{ + esp_err_t ret = sdcard_host_deinit(&dev->card->host); + + 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->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; + } + sdspi_dev_handle_t spi_handle; + err = sdspi_host_init_device(&cfg->slot.spi_dev, &spi_handle); + if (UNLIKELY(err != ESP_OK)) { + // No device was added and sdspi_host_init() is stateless: nothing to + // undo here, and sdspi_host_deinit() would destroy unrelated SDSPI + // slots, including any FAT-mounted card. + free(dev->card); + dev->card = NULL; + return err; + } + cfg->host.slot = spi_handle; + err = sdmmc_card_init(&cfg->host, dev->card); + if (UNLIKELY(err != ESP_OK)) { + sdcard_host_deinit(&cfg->host); + 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)) { + if (!(cfg->host.flags & SDMMC_HOST_FLAG_DEINIT_ARG)) { + // Pre-5.3 IDF: the exclusive sdmmc_host_init() succeeded, so this + // is the only host user and the host must be released. With + // per-slot refcounting (IDF >= 5.3) no slot reference was taken: + // a deinit here could tear down another slot's user, and the + // tolerant host init recovers on the next open. + cfg->host.deinit(); + } + free(dev->card); + dev->card = NULL; + return err; + } + err = sdmmc_card_init(&cfg->host, dev->card); + if (UNLIKELY(err != ESP_OK)) { + sdcard_host_deinit(&cfg->host); + 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; + + 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); + } + dev->open = false; + dev->card = NULL; + dev->sector_size = 0; + dev->sector_count = 0; +#ifndef AVM_NO_SMP + dev->lock = smp_mutex_create(); + if (IS_NULL_PTR(dev->lock)) { + enif_release_resource(dev); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } +#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); + + 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_uint32(argv[1]))) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector = term_to_uint32(argv[1]); + + SMP_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(sector >= dev->sector_count)) { + SMP_MUTEX_UNLOCK(dev->lock); + 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_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term binary = term_create_uninitialized_binary(sector_size, &ctx->heap, ctx->global); + // term_create_uninitialized_binary returns an invalid term if the refc binary + // allocation fails; memory_ensure_free only covers the boxed header words. + if (UNLIKELY(term_is_invalid_term(binary))) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + // 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_sector( + dev, (void *) term_binary_data(binary), (size_t) sector); + SMP_MUTEX_UNLOCK(dev->lock); + + 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_uint32(argv[1]))) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector = term_to_uint32(argv[1]); + + VALIDATE_VALUE(argv[2], term_is_binary); + term data = argv[2]; + + SMP_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(term_binary_size(data) != dev->sector_size)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(sector >= dev->sector_count)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + + esp_err_t err = sdcard_blockdev_write_sector(dev, term_binary_data(data), (size_t) sector); + SMP_MUTEX_UNLOCK(dev->lock); + + 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_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector_size = dev->sector_size; + uint32_t sector_count = dev->sector_count; + SMP_MUTEX_UNLOCK(dev->lock); + + 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_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + esp_err_t err = sdcard_blockdev_close(dev); + dev->open = false; + SMP_MUTEX_UNLOCK(dev->lock); + + 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."); + } + } + +#ifndef AVM_NO_SMP + if (dev->lock) { + smp_mutex_destroy(dev->lock); + dev->lock = NULL; + } +#endif +} +#endif void storage_nif_init(GlobalContext *global) { @@ -496,10 +910,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 +930,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 a5b4106bf0..e99036fc56 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -110,6 +110,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) @@ -138,6 +139,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..82d36947a7 --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl @@ -0,0 +1,105 @@ +% +% 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_reopen(), + 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. + +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), + try + ok = esp:sdcard_write(SDCard, LastSector, Pattern), + {ok, Pattern} = esp:sdcard_read(SDCard, LastSector) + after + restore_sector(SDCard, LastSector, Original) + end, + {ok, Original} = esp:sdcard_read(SDCard, LastSector), + ok. + +restore_sector(SDCard, Sector, Data) -> + try + esp:sdcard_write(SDCard, Sector, Data) + catch + _:_ -> ok + end. + +test_bad_args(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, 0, <<0>>) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, SectorCount) end), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, SectorCount, make_pattern(SectorSize)) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, -1) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, 1 bsl 32) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, 1 bsl 64) end), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, 1 bsl 64, 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. + +%% Close must fully release the host so that a new open works. +test_reopen() -> + {ok, SDCard} = esp:sdcard_open("sdmmc", []), + {ok, _Sector0} = esp:sdcard_read(SDCard, 0), + ok = esp:sdcard_close(SDCard), + 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 1fc46f33f0..af169a4258 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -391,6 +391,14 @@ TEST_CASE("test_mount", "[test_run]") term ret_value = avm_test_case("test_mount.beam"); TEST_ASSERT(ret_value == OK_ATOM); } + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +TEST_CASE("test_sdcard", "[test_run]") +{ + term ret_value = avm_test_case("test_sdcard.beam"); + TEST_ASSERT(ret_value == OK_ATOM); +} +#endif #endif struct pipefs_global_ctx