From 67d116dbdfb094ab9b2f92785a0d0f978fec3f2c Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Tue, 17 Feb 2026 17:35:26 +0100 Subject: [PATCH 1/2] Extract platform-specific modules from eavmlib Create new avm_emscripten, avm_esp32, avm_network, avm_rp2 and avm_stm32 libraries and build atomvmlib--.avm files for each platform and jit combination. Create new `{gpio,i2c,spi,uart}_hal` behavior module that serve as abstractions for all platforms. Signed-off-by: Paul Guyot --- .github/workflows/build-libraries.yaml | 16 +- .github/workflows/pico-build.yaml | 19 +- .github/workflows/run-tests-with-beam.yaml | 2 +- .github/workflows/wasm-build.yaml | 2 +- CMakeModules/BuildErlang.cmake | 133 ++++--- examples/elixir/esp32/CMakeLists.txt | 6 +- examples/elixir/stm32/CMakeLists.txt | 2 +- examples/emscripten/CMakeLists.txt | 10 +- examples/erlang/CMakeLists.txt | 6 +- examples/erlang/esp32/CMakeLists.txt | 32 +- libs/CMakeLists.txt | 106 ++++-- libs/avm_emscripten/src/CMakeLists.txt | 51 +++ .../src/emscripten.erl | 0 .../src/websocket.erl | 0 libs/avm_esp32/src/CMakeLists.txt | 57 +++ libs/{eavmlib => avm_esp32}/src/esp.erl | 0 libs/{eavmlib => avm_esp32}/src/esp_adc.erl | 0 libs/{eavmlib => avm_esp32}/src/esp_dac.erl | 0 libs/{eavmlib => avm_esp32}/src/gpio.erl | 139 +------- libs/{eavmlib => avm_esp32}/src/i2c.erl | 3 + libs/{eavmlib => avm_esp32}/src/ledc.erl | 0 libs/{eavmlib => avm_esp32}/src/spi.erl | 2 + libs/{eavmlib => avm_esp32}/src/uart.erl | 3 + libs/avm_network/src/CMakeLists.txt | 55 +++ .../src/ahttp_client.erl | 0 libs/{eavmlib => avm_network}/src/epmd.erl | 0 .../src/http_server.erl | 0 libs/{eavmlib => avm_network}/src/mdns.erl | 0 libs/{eavmlib => avm_network}/src/network.erl | 0 .../src/network_fsm.erl | 0 libs/avm_rp2/src/CMakeLists.txt | 51 +++ libs/avm_rp2/src/gpio.erl | 309 ++++++++++++++++ libs/{eavmlib => avm_rp2}/src/pico.erl | 0 libs/avm_stm32/src/CMakeLists.txt | 50 +++ libs/avm_stm32/src/gpio.erl | 335 ++++++++++++++++++ libs/eavmlib/src/CMakeLists.txt | 21 +- libs/eavmlib/src/gpio_hal.erl | 94 +++++ libs/eavmlib/src/i2c_hal.erl | 75 ++++ libs/eavmlib/src/spi_hal.erl | 76 ++++ libs/eavmlib/src/uart_hal.erl | 53 +++ libs/esp32boot/CMakeLists.txt | 4 +- .../emscripten/tests/src/test_atomvm.html | 1 + .../emscripten/tests/src/test_call.html | 2 +- .../emscripten/tests/src/test_html5.html | 2 +- .../emscripten/tests/src/test_websockets.html | 2 +- .../test/main/test_erl_sources/CMakeLists.txt | 4 +- .../rp2/tests/test_erl_sources/CMakeLists.txt | 6 +- tests/libs/eavmlib/CMakeLists.txt | 2 +- tests/libs/estdlib/CMakeLists.txt | 2 +- 49 files changed, 1450 insertions(+), 283 deletions(-) create mode 100644 libs/avm_emscripten/src/CMakeLists.txt rename libs/{eavmlib => avm_emscripten}/src/emscripten.erl (100%) rename libs/{eavmlib => avm_emscripten}/src/websocket.erl (100%) create mode 100644 libs/avm_esp32/src/CMakeLists.txt rename libs/{eavmlib => avm_esp32}/src/esp.erl (100%) rename libs/{eavmlib => avm_esp32}/src/esp_adc.erl (100%) rename libs/{eavmlib => avm_esp32}/src/esp_dac.erl (100%) rename libs/{eavmlib => avm_esp32}/src/gpio.erl (72%) rename libs/{eavmlib => avm_esp32}/src/i2c.erl (99%) rename libs/{eavmlib => avm_esp32}/src/ledc.erl (100%) rename libs/{eavmlib => avm_esp32}/src/spi.erl (99%) rename libs/{eavmlib => avm_esp32}/src/uart.erl (99%) create mode 100644 libs/avm_network/src/CMakeLists.txt rename libs/{eavmlib => avm_network}/src/ahttp_client.erl (100%) rename libs/{eavmlib => avm_network}/src/epmd.erl (100%) rename libs/{eavmlib => avm_network}/src/http_server.erl (100%) rename libs/{eavmlib => avm_network}/src/mdns.erl (100%) rename libs/{eavmlib => avm_network}/src/network.erl (100%) rename libs/{eavmlib => avm_network}/src/network_fsm.erl (100%) create mode 100644 libs/avm_rp2/src/CMakeLists.txt create mode 100644 libs/avm_rp2/src/gpio.erl rename libs/{eavmlib => avm_rp2}/src/pico.erl (100%) create mode 100644 libs/avm_stm32/src/CMakeLists.txt create mode 100644 libs/avm_stm32/src/gpio.erl create mode 100644 libs/eavmlib/src/gpio_hal.erl create mode 100644 libs/eavmlib/src/i2c_hal.erl create mode 100644 libs/eavmlib/src/spi_hal.erl create mode 100644 libs/eavmlib/src/uart_hal.erl diff --git a/.github/workflows/build-libraries.yaml b/.github/workflows/build-libraries.yaml index 7c7d9eaaee..bb8894f83b 100644 --- a/.github/workflows/build-libraries.yaml +++ b/.github/workflows/build-libraries.yaml @@ -113,9 +113,11 @@ jobs: - name: "Rename and write sha256sum" working-directory: build run: | - ATOMVMLIB_FILE=atomvmlib-${{ github.ref_name }}.avm - mv libs/atomvmlib.avm "libs/${ATOMVMLIB_FILE}" && - sha256sum "libs/${ATOMVMLIB_FILE}" > "libs/${ATOMVMLIB_FILE}.sha256" + for variant in atomvmlib atomvmlib-esp32 atomvmlib-rp2 atomvmlib-stm32 atomvmlib-emscripten; do + VARIANT_FILE="${variant}-${{ github.ref_name }}.avm" + mv "libs/${variant}.avm" "libs/${VARIANT_FILE}" && + sha256sum "libs/${VARIANT_FILE}" > "libs/${VARIANT_FILE}.sha256" + done HELLO_WORLD_FILE=hello_world-${{ github.ref_name }}.avm mv examples/erlang/hello_world.avm "examples/erlang/${HELLO_WORLD_FILE}" sha256sum "examples/erlang/${HELLO_WORLD_FILE}" > "examples/erlang/${HELLO_WORLD_FILE}.sha256" @@ -129,5 +131,13 @@ jobs: files: | build/libs/atomvmlib-${{ github.ref_name }}.avm build/libs/atomvmlib-${{ github.ref_name }}.avm.sha256 + build/libs/atomvmlib-esp32-${{ github.ref_name }}.avm + build/libs/atomvmlib-esp32-${{ github.ref_name }}.avm.sha256 + build/libs/atomvmlib-rp2-${{ github.ref_name }}.avm + build/libs/atomvmlib-rp2-${{ github.ref_name }}.avm.sha256 + build/libs/atomvmlib-stm32-${{ github.ref_name }}.avm + build/libs/atomvmlib-stm32-${{ github.ref_name }}.avm.sha256 + build/libs/atomvmlib-emscripten-${{ github.ref_name }}.avm + build/libs/atomvmlib-emscripten-${{ github.ref_name }}.avm.sha256 build/examples/erlang/hello_world-${{ github.ref_name }}.avm build/examples/erlang/hello_world-${{ github.ref_name }}.avm.sha256 diff --git a/.github/workflows/pico-build.yaml b/.github/workflows/pico-build.yaml index 630a73398c..d4881d6f99 100644 --- a/.github/workflows/pico-build.yaml +++ b/.github/workflows/pico-build.yaml @@ -138,7 +138,7 @@ jobs: npm install npx tsx run-tests.ts ../build.nosmp/tests/rp2_tests.uf2 ../build.nosmp/tests/test_erl_sources/rp2_test_modules.uf2 - - name: Build atomvmlib.uf2 + - name: Build atomvmlib-rp2.uf2 if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.platform == '' && matrix.jit == '' shell: bash run: | @@ -146,7 +146,7 @@ jobs: mkdir build cd build cmake .. - make atomvmlib-${{ matrix.board }}.uf2 + make atomvmlib-rp2-${{ matrix.board }}.uf2 - name: Rename AtomVM and write sha256sum if: startsWith(github.ref, 'refs/tags/') && matrix.platform == '' && matrix.jit == '' @@ -157,19 +157,14 @@ jobs: mv src/AtomVM.uf2 "src/${ATOMVM_UF2}" sha256sum "src/${ATOMVM_UF2}" > "src/${ATOMVM_UF2}.sha256" popd - pushd build/libs - ATOMVMLIB_FILE=atomvmlib-${{ matrix.board }}-${{ github.ref_name }}.uf2 - mv atomvmlib.uf2 "${ATOMVMLIB_FILE}" - sha256sum "${ATOMVMLIB_FILE}" > "${ATOMVMLIB_FILE}.sha256" - popd - - name: Rename atomvmlib and write sha256sum + - name: Rename atomvmlib-rp2 and write sha256sum if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.platform == '' && matrix.jit == '' shell: bash run: | pushd build/libs - ATOMVMLIB_FILE=atomvmlib-${{ matrix.board }}-${{ github.ref_name }}.uf2 - mv atomvmlib-${{ matrix.board }}.uf2 "${ATOMVMLIB_FILE}" + ATOMVMLIB_FILE=atomvmlib-rp2-${{ matrix.board }}-${{ github.ref_name }}.uf2 + mv atomvmlib-rp2-${{ matrix.board }}.uf2 "${ATOMVMLIB_FILE}" sha256sum "${ATOMVMLIB_FILE}" > "${ATOMVMLIB_FILE}.sha256" popd @@ -182,8 +177,8 @@ jobs: files: | src/platforms/rp2/build/src/AtomVM-${{ matrix.board }}-${{ github.ref_name }}.uf2 src/platforms/rp2/build/src/AtomVM-${{ matrix.board }}-${{ github.ref_name }}.uf2.sha256 - build/libs/atomvmlib-${{ matrix.board }}-${{ github.ref_name }}.uf2 - build/libs/atomvmlib-${{ matrix.board }}-${{ github.ref_name }}.uf2.sha256 + build/libs/atomvmlib-rp2-${{ matrix.board }}-${{ github.ref_name }}.uf2 + build/libs/atomvmlib-rp2-${{ matrix.board }}-${{ github.ref_name }}.uf2.sha256 - name: Release (PicoW) uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/run-tests-with-beam.yaml b/.github/workflows/run-tests-with-beam.yaml index 41e73a7e6a..4f818a7e3d 100644 --- a/.github/workflows/run-tests-with-beam.yaml +++ b/.github/workflows/run-tests-with-beam.yaml @@ -177,7 +177,7 @@ jobs: working-directory: build run: | export PATH="${{ matrix.path_prefix }}$PATH" - erl -pa tests/libs/estdlib/ -pa tests/libs/estdlib/beams/ -pa libs/etest/src/beams -pa libs/eavmlib/src/beams -s tests -s init stop -noshell + erl -pa tests/libs/estdlib/ -pa tests/libs/estdlib/beams/ -pa libs/etest/src/beams -pa libs/eavmlib/src/beams -pa libs/avm_network/src/beams -s tests -s init stop -noshell # Test - name: "Run tests/libs/etest/test_eunit with OTP eunit" diff --git a/.github/workflows/wasm-build.yaml b/.github/workflows/wasm-build.yaml index 321b669f26..f0add5d2da 100644 --- a/.github/workflows/wasm-build.yaml +++ b/.github/workflows/wasm-build.yaml @@ -71,7 +71,7 @@ jobs: cd build cmake .. -G Ninja -DAVM_WARNINGS_ARE_ERRORS=ON # test_eavmlib does not work with wasm due to http + ssl test - ninja AtomVM atomvmlib erlang_test_modules test_etest test_alisp test_estdlib hello_world run_script call_cast html5_events wasm_webserver + ninja AtomVM atomvmlib atomvmlib-emscripten erlang_test_modules test_etest test_alisp test_estdlib hello_world run_script call_cast html5_events wasm_webserver - name: "Perform CodeQL Analysis" uses: github/codeql-action/analyze@v4 diff --git a/CMakeModules/BuildErlang.cmake b/CMakeModules/BuildErlang.cmake index d0a3388da3..ed8478dc4d 100644 --- a/CMakeModules/BuildErlang.cmake +++ b/CMakeModules/BuildErlang.cmake @@ -20,9 +20,18 @@ macro(pack_archive avm_name) - set(multiValueArgs ERLC_FLAGS MODULES) + set(multiValueArgs ERLC_FLAGS MODULES DEPENDS_ON) cmake_parse_arguments(PACK_ARCHIVE "" "" "${multiValueArgs}" ${ARGN}) list(JOIN PACK_ARCHIVE_ERLC_FLAGS " " PACK_ARCHIVE_ERLC_FLAGS) + + # Build -pa flags and file dependencies from DEPENDS_ON + set(_pack_archive_pa_flags "") + set(_pack_archive_extra_deps "") + foreach(_dep_name IN LISTS PACK_ARCHIVE_DEPENDS_ON) + list(APPEND _pack_archive_pa_flags -pa ${CMAKE_BINARY_DIR}/libs/${_dep_name}/src/beams) + list(APPEND _pack_archive_extra_deps ${CMAKE_BINARY_DIR}/libs/${_dep_name}/src/${_dep_name}.avm) + endforeach() + foreach(module_name IN LISTS ${PACK_ARCHIVE_MODULES} PACK_ARCHIVE_MODULES PACK_ARCHIVE_UNPARSED_ARGUMENTS) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/beams/${module_name}.beam @@ -32,8 +41,9 @@ macro(pack_archive avm_name) -I ${CMAKE_SOURCE_DIR}/libs/include -I ${CMAKE_SOURCE_DIR}/libs -I ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${_pack_archive_pa_flags} ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}.erl - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}.erl + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}.erl ${_pack_archive_extra_deps} COMMENT "Compiling ${module_name}.erl" VERBATIM ) @@ -63,6 +73,10 @@ macro(pack_archive avm_name) ${avm_name} ALL DEPENDS ${avm_name}_emu ) + # Add target-level dependencies from DEPENDS_ON + foreach(_dep_name IN LISTS PACK_ARCHIVE_DEPENDS_ON) + add_dependencies(${avm_name} ${_dep_name}) + endforeach() endmacro() macro(pack_precompiled_archive avm_name) @@ -127,12 +141,17 @@ macro(pack_precompiled_archive avm_name) endmacro() macro(pack_lib avm_name) + set(options UF2) + cmake_parse_arguments(PACK_LIB "${options}" "" "" ${ARGN}) + set(pack_lib_${avm_name}_archive_targets "") + set(pack_lib_${avm_name}_archives "") + set(pack_lib_${avm_name}_emu_archives "") if(NOT AVM_DISABLE_JIT) set(pack_lib_${avm_name}_archive_targets jit) endif() - foreach(archive_name ${ARGN}) + foreach(archive_name ${PACK_LIB_UNPARSED_ARGUMENTS}) if(${archive_name} STREQUAL "exavmlib") set(pack_lib_${avm_name}_archives ${pack_lib_${avm_name}_archives} ${CMAKE_BINARY_DIR}/libs/${archive_name}/lib/${archive_name}.avm) elseif(${archive_name} STREQUAL "estdlib") @@ -164,7 +183,7 @@ macro(pack_lib avm_name) foreach(jit_target_arch_variant ${AVM_PRECOMPILED_TARGETS}) # Build JIT archives list for this specific target architecture set(pack_lib_${avm_name}_jit_archives_${jit_target_arch_variant} ${CMAKE_BINARY_DIR}/libs/jit/src/jit-${jit_target_arch_variant}.avm) - foreach(archive_name ${ARGN}) + foreach(archive_name ${PACK_LIB_UNPARSED_ARGUMENTS}) if(${archive_name} STREQUAL "estdlib") set(pack_lib_${avm_name}_jit_archives_${jit_target_arch_variant} ${pack_lib_${avm_name}_jit_archives_${jit_target_arch_variant}} ${CMAKE_BINARY_DIR}/libs/${archive_name}/src/${archive_name}-${jit_target_arch_variant}.avm) endif() @@ -180,56 +199,59 @@ macro(pack_lib avm_name) set(target_deps ${target_deps} ${avm_name}-${jit_target_arch_variant}.avm) endforeach() endif() - add_custom_command( - OUTPUT ${avm_name}-pico.uf2 - DEPENDS ${avm_name}.avm UF2Tool - COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-pico.uf2 -s 0x10100000 ${avm_name}.avm - COMMENT "Creating UF2 file ${avm_name}.uf2" - VERBATIM - ) - add_custom_command( - OUTPUT ${avm_name}-pico2.uf2 - DEPENDS ${avm_name}.avm UF2Tool - COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-pico2.uf2 -f data -s 0x10100000 ${avm_name}.avm - COMMENT "Creating UF2 file ${avm_name}.uf2" - VERBATIM - ) - set(target_deps ${target_deps} ${avm_name}-pico.uf2 ${avm_name}-pico2.uf2) - if((NOT AVM_DISABLE_JIT OR AVM_ENABLE_PRECOMPILED) AND ("armv6m" IN_LIST AVM_PRECOMPILED_TARGETS)) + if(PACK_LIB_UF2) add_custom_command( - OUTPUT ${avm_name}-armv6m-pico.uf2 - DEPENDS ${avm_name}-armv6m.avm UF2Tool - COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m-pico.uf2 -s 0x10100000 ${avm_name}-armv6m.avm - COMMENT "Creating UF2 file ${avm_name}-armv6m.uf2" + OUTPUT ${avm_name}-pico.uf2 + DEPENDS ${avm_name}.avm UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-pico.uf2 -s 0x10100000 ${avm_name}.avm + COMMENT "Creating UF2 file ${avm_name}-pico.uf2" VERBATIM ) add_custom_command( - OUTPUT ${avm_name}-armv6m-pico2.uf2 - DEPENDS ${avm_name}-armv6m.avm UF2Tool - COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m-pico2.uf2 -f data -s 0x10100000 ${avm_name}-armv6m.avm - COMMENT "Creating UF2 file ${avm_name}-armv6m.uf2" + OUTPUT ${avm_name}-pico2.uf2 + DEPENDS ${avm_name}.avm UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-pico2.uf2 -f data -s 0x10100000 ${avm_name}.avm + COMMENT "Creating UF2 file ${avm_name}-pico2.uf2" VERBATIM ) - set(target_deps ${target_deps} ${avm_name}-armv6m-pico.uf2 ${avm_name}-armv6m-pico2.uf2) - endif() + set(target_deps ${target_deps} ${avm_name}-pico.uf2 ${avm_name}-pico2.uf2) - if((NOT AVM_DISABLE_JIT OR AVM_ENABLE_PRECOMPILED) AND ("armv6m+float32" IN_LIST AVM_PRECOMPILED_TARGETS)) - add_custom_command( - OUTPUT ${avm_name}-armv6m+float32-pico.uf2 - DEPENDS ${avm_name}-armv6m+float32.avm UF2Tool - COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m+float32-pico.uf2 -s 0x10100000 ${avm_name}-armv6m+float32.avm - COMMENT "Creating UF2 file ${avm_name}-armv6m+float32.uf2" - VERBATIM - ) - add_custom_command( - OUTPUT ${avm_name}-armv6m+float32-pico2.uf2 - DEPENDS ${avm_name}-armv6m+float32.avm UF2Tool - COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m+float32-pico2.uf2 -f data -s 0x10100000 ${avm_name}-armv6m+float32.avm - COMMENT "Creating UF2 file ${avm_name}-armv6m+float32.uf2" - VERBATIM - ) - set(target_deps ${target_deps} ${avm_name}-armv6m+float32-pico.uf2 ${avm_name}-armv6m+float32-pico2.uf2) + if((NOT AVM_DISABLE_JIT OR AVM_ENABLE_PRECOMPILED) AND ("armv6m" IN_LIST AVM_PRECOMPILED_TARGETS)) + add_custom_command( + OUTPUT ${avm_name}-armv6m-pico.uf2 + DEPENDS ${avm_name}-armv6m.avm UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m-pico.uf2 -s 0x10100000 ${avm_name}-armv6m.avm + COMMENT "Creating UF2 file ${avm_name}-armv6m-pico.uf2" + VERBATIM + ) + add_custom_command( + OUTPUT ${avm_name}-armv6m-pico2.uf2 + DEPENDS ${avm_name}-armv6m.avm UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m-pico2.uf2 -f data -s 0x10100000 ${avm_name}-armv6m.avm + COMMENT "Creating UF2 file ${avm_name}-armv6m-pico2.uf2" + VERBATIM + ) + set(target_deps ${target_deps} ${avm_name}-armv6m-pico.uf2 ${avm_name}-armv6m-pico2.uf2) + endif() + + if((NOT AVM_DISABLE_JIT OR AVM_ENABLE_PRECOMPILED) AND ("armv6m+float32" IN_LIST AVM_PRECOMPILED_TARGETS)) + add_custom_command( + OUTPUT ${avm_name}-armv6m+float32-pico.uf2 + DEPENDS ${avm_name}-armv6m+float32.avm UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m+float32-pico.uf2 -s 0x10100000 ${avm_name}-armv6m+float32.avm + COMMENT "Creating UF2 file ${avm_name}-armv6m+float32-pico.uf2" + VERBATIM + ) + add_custom_command( + OUTPUT ${avm_name}-armv6m+float32-pico2.uf2 + DEPENDS ${avm_name}-armv6m+float32.avm UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o ${avm_name}-armv6m+float32-pico2.uf2 -f data -s 0x10100000 ${avm_name}-armv6m+float32.avm + COMMENT "Creating UF2 file ${avm_name}-armv6m+float32-pico2.uf2" + VERBATIM + ) + set(target_deps ${target_deps} ${avm_name}-armv6m+float32-pico.uf2 ${avm_name}-armv6m+float32-pico2.uf2) + endif() endif() add_custom_target( @@ -258,25 +280,38 @@ macro(pack_runnable avm_name main) DEPENDS ${main}.beam ) + # Select the right PLT based on platform-specific dependencies + set(pack_runnable_${avm_name}_plt_name "atomvmlib") + foreach(archive_name ${ARGN}) if(NOT ${archive_name} STREQUAL "exavmlib") set(pack_runnable_${avm_name}_archives ${pack_runnable_${avm_name}_archives} ${CMAKE_BINARY_DIR}/libs/${archive_name}/src/${archive_name}.avm) - if(NOT ${archive_name} MATCHES "^eavmlib|estdlib|alisp$") + if(NOT ${archive_name} MATCHES "^eavmlib|estdlib|alisp|avm_network|avm_esp32|avm_rp2|avm_stm32|avm_emscripten$") set(${avm_name}_dialyzer_beams_opt ${${avm_name}_dialyzer_beams_opt} "-r" ${CMAKE_BINARY_DIR}/libs/${archive_name}/src/beams/) endif() else() set(pack_runnable_${avm_name}_archives ${pack_runnable_${avm_name}_archives} ${CMAKE_BINARY_DIR}/libs/${archive_name}/lib/${archive_name}.avm) endif() set(pack_runnable_${avm_name}_archive_targets ${pack_runnable_${avm_name}_archive_targets} ${archive_name}) + # Pick the platform-specific PLT if a platform library is in the dependencies + if(${archive_name} STREQUAL "avm_esp32") + set(pack_runnable_${avm_name}_plt_name "atomvmlib-esp32") + elseif(${archive_name} STREQUAL "avm_rp2") + set(pack_runnable_${avm_name}_plt_name "atomvmlib-rp2") + elseif(${archive_name} STREQUAL "avm_stm32") + set(pack_runnable_${avm_name}_plt_name "atomvmlib-stm32") + elseif(${archive_name} STREQUAL "avm_emscripten") + set(pack_runnable_${avm_name}_plt_name "atomvmlib-emscripten") + endif() endforeach() if (Dialyzer_FOUND) add_custom_target( ${avm_name}_dialyzer DEPENDS ${avm_name}_main - COMMAND dialyzer --plt ${CMAKE_BINARY_DIR}/libs/atomvmlib.plt -c ${main}.beam ${${avm_name}_dialyzer_beams_opt} + COMMAND dialyzer --plt ${CMAKE_BINARY_DIR}/libs/${pack_runnable_${avm_name}_plt_name}.plt -c ${main}.beam ${${avm_name}_dialyzer_beams_opt} ) - add_dependencies(${avm_name}_dialyzer atomvmlib_plt ${pack_runnable_${avm_name}_archive_targets}) + add_dependencies(${avm_name}_dialyzer ${pack_runnable_${avm_name}_plt_name}_plt ${pack_runnable_${avm_name}_archive_targets}) add_dependencies(dialyzer ${avm_name}_dialyzer) endif() diff --git a/examples/elixir/esp32/CMakeLists.txt b/examples/elixir/esp32/CMakeLists.txt index 9a15d37cd9..95c2f674f1 100644 --- a/examples/elixir/esp32/CMakeLists.txt +++ b/examples/elixir/esp32/CMakeLists.txt @@ -22,8 +22,8 @@ project(examples_elixir_esp32) include(BuildElixir) -pack_runnable(Blink Blink estdlib eavmlib exavmlib) -pack_runnable(Ledc_x4 Ledc_x4 estdlib eavmlib exavmlib) +pack_runnable(Blink Blink estdlib eavmlib exavmlib avm_esp32) +pack_runnable(Ledc_x4 Ledc_x4 estdlib eavmlib exavmlib avm_esp32) if(NOT (AVM_DISABLE_FP)) - pack_runnable(SHT31 SHT31 estdlib eavmlib exavmlib) + pack_runnable(SHT31 SHT31 estdlib eavmlib exavmlib avm_esp32) endif() diff --git a/examples/elixir/stm32/CMakeLists.txt b/examples/elixir/stm32/CMakeLists.txt index cf15d0b46a..12c76b73f1 100644 --- a/examples/elixir/stm32/CMakeLists.txt +++ b/examples/elixir/stm32/CMakeLists.txt @@ -22,4 +22,4 @@ project(examples_elixir_stm32) include(BuildElixir) -pack_runnable(MultiBlink MultiBlink estdlib eavmlib exavmlib) +pack_runnable(MultiBlink MultiBlink estdlib eavmlib exavmlib avm_stm32) diff --git a/examples/emscripten/CMakeLists.txt b/examples/emscripten/CMakeLists.txt index 51ce812beb..349702471a 100644 --- a/examples/emscripten/CMakeLists.txt +++ b/examples/emscripten/CMakeLists.txt @@ -22,8 +22,8 @@ project(examples_emscripten) include(BuildErlang) -pack_runnable(run_script run_script estdlib eavmlib) -pack_runnable(call_cast call_cast eavmlib) -pack_runnable(html5_events html5_events estdlib eavmlib) -pack_runnable(echo_websocket echo_websocket estdlib eavmlib) -pack_runnable(wasm_webserver wasm_webserver estdlib eavmlib) +pack_runnable(run_script run_script estdlib eavmlib avm_emscripten) +pack_runnable(call_cast call_cast eavmlib avm_emscripten) +pack_runnable(html5_events html5_events estdlib eavmlib avm_emscripten) +pack_runnable(echo_websocket echo_websocket estdlib eavmlib avm_emscripten) +pack_runnable(wasm_webserver wasm_webserver estdlib eavmlib avm_network avm_emscripten) diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index b3bb1dbda7..5e9a8febc1 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -34,11 +34,11 @@ pack_runnable(tcp_socket_client tcp_socket_client estdlib eavmlib) pack_runnable(tcp_socket_server tcp_socket_server estdlib eavmlib) pack_runnable(udp_socket_server udp_socket_server estdlib eavmlib) pack_runnable(udp_socket_client udp_socket_client estdlib eavmlib) -pack_runnable(hello_world_server hello_world_server estdlib eavmlib) -pack_runnable(system_info_server system_info_server estdlib eavmlib) +pack_runnable(hello_world_server hello_world_server estdlib eavmlib avm_network) +pack_runnable(system_info_server system_info_server estdlib eavmlib avm_network) pack_runnable(code_lock code_lock estdlib eavmlib) pack_runnable(mqtt_client mqtt_client estdlib eavmlib) pack_runnable(network_console network_console estdlib eavmlib alisp) pack_runnable(logging_example logging_example estdlib eavmlib) -pack_runnable(http_client http_client estdlib eavmlib) +pack_runnable(http_client http_client estdlib eavmlib avm_network) pack_runnable(disterl disterl estdlib) diff --git a/examples/erlang/esp32/CMakeLists.txt b/examples/erlang/esp32/CMakeLists.txt index f28afeb8fe..39e497bd55 100644 --- a/examples/erlang/esp32/CMakeLists.txt +++ b/examples/erlang/esp32/CMakeLists.txt @@ -22,19 +22,19 @@ project(examples_erlang_esp32) include(BuildErlang) -pack_runnable(blink blink eavmlib estdlib) -pack_runnable(deep_sleep deep_sleep eavmlib estdlib) -pack_runnable(morse_server morse_server estdlib eavmlib) -pack_runnable(ap_sta_network ap_sta_network eavmlib estdlib) -pack_runnable(set_network_config set_network_config eavmlib estdlib) -pack_runnable(udp_server_blink udp_server_blink eavmlib estdlib) -pack_runnable(tcp_client_esp32 tcp_client_esp32 eavmlib estdlib) -pack_runnable(tcp_server_blink tcp_server_blink eavmlib estdlib) -pack_runnable(esp_random esp_random eavmlib estdlib) -pack_runnable(esp_nvs esp_nvs eavmlib) -pack_runnable(sht31 sht31 eavmlib estdlib) -pack_runnable(sx127x sx127x eavmlib estdlib) -pack_runnable(reformat_nvs reformat_nvs eavmlib) -pack_runnable(uartecho uartecho eavmlib estdlib) -pack_runnable(ledc_example ledc_example eavmlib estdlib) -pack_runnable(epmd_disterl epmd_disterl eavmlib estdlib) +pack_runnable(blink blink eavmlib estdlib avm_esp32) +pack_runnable(deep_sleep deep_sleep eavmlib estdlib avm_esp32) +pack_runnable(morse_server morse_server estdlib eavmlib avm_network avm_esp32) +pack_runnable(ap_sta_network ap_sta_network eavmlib estdlib avm_network avm_esp32) +pack_runnable(set_network_config set_network_config eavmlib estdlib avm_esp32) +pack_runnable(udp_server_blink udp_server_blink eavmlib estdlib avm_network avm_esp32) +pack_runnable(tcp_client_esp32 tcp_client_esp32 eavmlib estdlib avm_network avm_esp32) +pack_runnable(tcp_server_blink tcp_server_blink eavmlib estdlib avm_network avm_esp32) +pack_runnable(esp_random esp_random eavmlib estdlib avm_esp32) +pack_runnable(esp_nvs esp_nvs eavmlib avm_esp32) +pack_runnable(sht31 sht31 eavmlib estdlib avm_esp32) +pack_runnable(sx127x sx127x eavmlib estdlib avm_esp32) +pack_runnable(reformat_nvs reformat_nvs eavmlib avm_esp32) +pack_runnable(uartecho uartecho eavmlib estdlib avm_esp32) +pack_runnable(ledc_example ledc_example eavmlib estdlib avm_esp32) +pack_runnable(epmd_disterl epmd_disterl eavmlib estdlib avm_network avm_esp32) diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 2f168baedd..0e6b04c3a6 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -25,6 +25,11 @@ add_subdirectory(estdlib/src) add_subdirectory(eavmlib/src) add_subdirectory(alisp/src) add_subdirectory(etest/src) +add_subdirectory(avm_network/src) +add_subdirectory(avm_esp32/src) +add_subdirectory(avm_rp2/src) +add_subdirectory(avm_stm32/src) +add_subdirectory(avm_emscripten/src) add_subdirectory(esp32boot) add_subdirectory(esp32devmode/src) # JIT compiler doesn't compile with OTP < 23 @@ -33,69 +38,96 @@ if(Erlang_VERSION VERSION_GREATER_EQUAL "23") endif() -set(ATOMVM_LIBS eavmlib estdlib alisp) +set(ATOMVM_COMMON_LIBS eavmlib estdlib alisp) find_package(Elixir) find_package(Gleam) if (Elixir_FOUND) add_subdirectory(exavmlib/lib) - list(APPEND ATOMVM_LIBS exavmlib) + list(APPEND ATOMVM_COMMON_LIBS exavmlib) else() message(WARNING "Unable to find elixirc -- skipping Elixir libs") endif() if (Gleam_FOUND) add_subdirectory(gleam_avm) - list(APPEND ATOMVM_LIBS gleam_avm) + list(APPEND ATOMVM_COMMON_LIBS gleam_avm) else() message(WARNING "Unable to find gleam -- skipping Gleam libs") endif() -pack_lib(atomvmlib ${ATOMVM_LIBS}) +# Base (generic_unix): common + network +pack_lib(atomvmlib ${ATOMVM_COMMON_LIBS} avm_network) + +# Platform-specific variants +pack_lib(atomvmlib-esp32 ${ATOMVM_COMMON_LIBS} avm_network avm_esp32) +pack_lib(atomvmlib-rp2 UF2 ${ATOMVM_COMMON_LIBS} avm_network avm_rp2) +pack_lib(atomvmlib-stm32 ${ATOMVM_COMMON_LIBS} avm_stm32) +pack_lib(atomvmlib-emscripten ${ATOMVM_COMMON_LIBS} avm_network avm_emscripten) if (Dialyzer_FOUND) - add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/estdlib_beams.txt - DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/estdlib/src/estdlib.avm PackBEAM - COMMAND - ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -l ${CMAKE_CURRENT_BINARY_DIR}/estdlib/src/estdlib.avm | sed -e 's|^|${CMAKE_CURRENT_BINARY_DIR}/estdlib/src/beams/|g' > ${CMAKE_CURRENT_BINARY_DIR}/estdlib_beams.txt - ) - add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/eavmlib_beams.txt - DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/eavmlib/src/eavmlib.avm PackBEAM - COMMAND - ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -l ${CMAKE_CURRENT_BINARY_DIR}/eavmlib/src/eavmlib.avm | sed -e 's|^|${CMAKE_CURRENT_BINARY_DIR}/eavmlib/src/beams/|g' > ${CMAKE_CURRENT_BINARY_DIR}/eavmlib_beams.txt - ) - add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/alisp_beams.txt - DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/alisp/src/alisp.avm PackBEAM - COMMAND - ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -l ${CMAKE_CURRENT_BINARY_DIR}/alisp/src/alisp.avm | sed -e 's|^|${CMAKE_CURRENT_BINARY_DIR}/alisp/src/beams/|g' > ${CMAKE_CURRENT_BINARY_DIR}/alisp_beams.txt - ) + # Helper macro to generate a beams list file from an archive + macro(dialyzer_beams_list lib_name lib_path) + get_filename_component(_beams_dir ${lib_path} DIRECTORY) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${lib_name}_beams.txt + DEPENDS ${lib_path} PackBEAM + COMMAND + ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -l ${lib_path} | sed -e 's|^|${_beams_dir}/beams/|g' > ${CMAKE_CURRENT_BINARY_DIR}/${lib_name}_beams.txt + ) + endmacro() + + dialyzer_beams_list(estdlib ${CMAKE_CURRENT_BINARY_DIR}/estdlib/src/estdlib.avm) + dialyzer_beams_list(eavmlib ${CMAKE_CURRENT_BINARY_DIR}/eavmlib/src/eavmlib.avm) + dialyzer_beams_list(alisp ${CMAKE_CURRENT_BINARY_DIR}/alisp/src/alisp.avm) + dialyzer_beams_list(avm_network ${CMAKE_CURRENT_BINARY_DIR}/avm_network/src/avm_network.avm) + dialyzer_beams_list(avm_esp32 ${CMAKE_CURRENT_BINARY_DIR}/avm_esp32/src/avm_esp32.avm) + dialyzer_beams_list(avm_rp2 ${CMAKE_CURRENT_BINARY_DIR}/avm_rp2/src/avm_rp2.avm) + dialyzer_beams_list(avm_stm32 ${CMAKE_CURRENT_BINARY_DIR}/avm_stm32/src/avm_stm32.avm) + dialyzer_beams_list(avm_emscripten ${CMAKE_CURRENT_BINARY_DIR}/avm_emscripten/src/avm_emscripten.avm) + add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/jit_beams.txt DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/jit/src/jit.avm PackBEAM COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -l ${CMAKE_CURRENT_BINARY_DIR}/jit/src/jit.avm | sed -e 's|^|${CMAKE_CURRENT_BINARY_DIR}/jit/src/beams/|g' | grep -v jit_precompile > ${CMAKE_CURRENT_BINARY_DIR}/jit_beams.txt ) - set(dialyzer_lists + + # Base beam lists shared by all PLTs + set(dialyzer_base_lists ${CMAKE_CURRENT_BINARY_DIR}/estdlib_beams.txt ${CMAKE_CURRENT_BINARY_DIR}/eavmlib_beams.txt ${CMAKE_CURRENT_BINARY_DIR}/alisp_beams.txt + ${CMAKE_CURRENT_BINARY_DIR}/avm_network_beams.txt ) if(Erlang_VERSION VERSION_GREATER_EQUAL "23") - set(dialyzer_lists ${dialyzer_lists} ${CMAKE_CURRENT_BINARY_DIR}/jit_beams.txt) + set(dialyzer_base_lists ${dialyzer_base_lists} ${CMAKE_CURRENT_BINARY_DIR}/jit_beams.txt) endif() - add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib.plt - DEPENDS ${dialyzer_lists} - COMMAND cat ${dialyzer_lists} - | xargs dialyzer --build_plt --output_plt ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib.plt - ) - add_custom_target(atomvmlib_plt - DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib.plt - ) + + # Helper macro to build a PLT from base lists + platform-specific lists + macro(build_plt plt_name) + set(plt_extra_lists ${ARGN}) + set(plt_all_lists ${dialyzer_base_lists} ${plt_extra_lists}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${plt_name}.plt + DEPENDS ${plt_all_lists} + COMMAND cat ${plt_all_lists} + | xargs dialyzer --build_plt --output_plt ${CMAKE_CURRENT_BINARY_DIR}/${plt_name}.plt + ) + add_custom_target(${plt_name}_plt + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${plt_name}.plt + ) + endmacro() + + # Base PLT (no platform-specific modules) + build_plt(atomvmlib) + + # Per-platform PLTs + build_plt(atomvmlib-esp32 ${CMAKE_CURRENT_BINARY_DIR}/avm_esp32_beams.txt) + build_plt(atomvmlib-rp2 ${CMAKE_CURRENT_BINARY_DIR}/avm_rp2_beams.txt) + build_plt(atomvmlib-stm32 ${CMAKE_CURRENT_BINARY_DIR}/avm_stm32_beams.txt) + build_plt(atomvmlib-emscripten ${CMAKE_CURRENT_BINARY_DIR}/avm_emscripten_beams.txt) else() message("Dialyzer was not found -- skipping PLT build") endif() @@ -104,6 +136,14 @@ install( FILES ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib.avm DESTINATION lib/atomvm ) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib-esp32.avm + ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib-rp2.avm + ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib-stm32.avm + ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib-emscripten.avm + DESTINATION lib/atomvm +) if(NOT AVM_DISABLE_JIT) install( FILES ${CMAKE_CURRENT_BINARY_DIR}/atomvmlib-${AVM_JIT_TARGET_ARCH}.avm diff --git a/libs/avm_emscripten/src/CMakeLists.txt b/libs/avm_emscripten/src/CMakeLists.txt new file mode 100644 index 0000000000..65bbfee9c7 --- /dev/null +++ b/libs/avm_emscripten/src/CMakeLists.txt @@ -0,0 +1,51 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# 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 +# + +project(avm_emscripten) + +include(BuildErlang) + +set(ERLANG_MODULES + emscripten + websocket +) + +pack_archive(avm_emscripten ${ERLANG_MODULES}) + +include(../../../version.cmake) + +set(AVM_EMSCRIPTEN_VERSION ${ATOMVM_BASE_VERSION}) + +install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/beams/ + DESTINATION lib/atomvm/lib/avm_emscripten-${AVM_EMSCRIPTEN_VERSION}/ebin + FILES_MATCHING PATTERN "*.beam" +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/avm_emscripten.avm + DESTINATION lib/atomvm/lib/avm_emscripten-${AVM_EMSCRIPTEN_VERSION}/ebin/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION lib/atomvm/lib/avm_emscripten-${AVM_EMSCRIPTEN_VERSION}/src + FILES_MATCHING PATTERN "*.erl" +) diff --git a/libs/eavmlib/src/emscripten.erl b/libs/avm_emscripten/src/emscripten.erl similarity index 100% rename from libs/eavmlib/src/emscripten.erl rename to libs/avm_emscripten/src/emscripten.erl diff --git a/libs/eavmlib/src/websocket.erl b/libs/avm_emscripten/src/websocket.erl similarity index 100% rename from libs/eavmlib/src/websocket.erl rename to libs/avm_emscripten/src/websocket.erl diff --git a/libs/avm_esp32/src/CMakeLists.txt b/libs/avm_esp32/src/CMakeLists.txt new file mode 100644 index 0000000000..07afa6d387 --- /dev/null +++ b/libs/avm_esp32/src/CMakeLists.txt @@ -0,0 +1,57 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# 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 +# + +project(avm_esp32) + +include(BuildErlang) + +set(ERLANG_MODULES + esp + esp_adc + esp_dac + gpio + i2c + ledc + spi + uart +) + +pack_archive(avm_esp32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) + +include(../../../version.cmake) + +set(AVM_ESP32_VERSION ${ATOMVM_BASE_VERSION}) + +install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/beams/ + DESTINATION lib/atomvm/lib/avm_esp32-${AVM_ESP32_VERSION}/ebin + FILES_MATCHING PATTERN "*.beam" +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/avm_esp32.avm + DESTINATION lib/atomvm/lib/avm_esp32-${AVM_ESP32_VERSION}/ebin/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION lib/atomvm/lib/avm_esp32-${AVM_ESP32_VERSION}/src + FILES_MATCHING PATTERN "*.erl" +) diff --git a/libs/eavmlib/src/esp.erl b/libs/avm_esp32/src/esp.erl similarity index 100% rename from libs/eavmlib/src/esp.erl rename to libs/avm_esp32/src/esp.erl diff --git a/libs/eavmlib/src/esp_adc.erl b/libs/avm_esp32/src/esp_adc.erl similarity index 100% rename from libs/eavmlib/src/esp_adc.erl rename to libs/avm_esp32/src/esp_adc.erl diff --git a/libs/eavmlib/src/esp_dac.erl b/libs/avm_esp32/src/esp_dac.erl similarity index 100% rename from libs/eavmlib/src/esp_dac.erl rename to libs/avm_esp32/src/esp_dac.erl diff --git a/libs/eavmlib/src/gpio.erl b/libs/avm_esp32/src/gpio.erl similarity index 72% rename from libs/eavmlib/src/gpio.erl rename to libs/avm_esp32/src/gpio.erl index ac5d89b3ba..26c508ed39 100644 --- a/libs/eavmlib/src/gpio.erl +++ b/libs/avm_esp32/src/gpio.erl @@ -19,18 +19,16 @@ % %%----------------------------------------------------------------------------- -%% @doc GPIO driver module +%% @doc GPIO driver module for ESP32 %% %% This module provides functions for interacting with micro-controller GPIO -%% (General Purpose Input and Output) pins. -%% -%% Note: `-type pin()' used in this driver refers to a pin number on Espressif -%% chips and normal Raspberry Pi Pico pins, or a tuple {GPIO_BANK, PIN} for STM32 -%% chips and the "extra" GPIOs available on the Pico-W. +%% (General Purpose Input and Output) pins on the ESP32 platform. %% @end %%----------------------------------------------------------------------------- -module(gpio). +-behaviour(gpio_hal). + -export([ start/0, open/0, @@ -60,27 +58,18 @@ -type gpio() :: port(). %% This is the port returned by `gpio:start/0'. --type pin() :: non_neg_integer() | pin_tuple(). -%% The pin definition for ESP32 and PR2040 is a non-negative integer. A tuple is used on the STM32 platform and for the extra "WL" pins on the Pico-W. --type pin_tuple() :: {gpio_bank(), 0..15}. -%% A pin parameter on STM32 is a tuple consisting of a GPIO bank and pin number, also used on the Pico-W for the extra "WL" pins `0..2'. --type gpio_bank() :: a | b | c | d | e | f | g | h | i | j | k | wl. -%% STM32 gpio banks vary by board, some only break out `a' thru `h'. The extra "WL" pins on Pico-W use bank `wl'. --type direction() :: input | output | output_od | mode_config(). +-type pin() :: non_neg_integer(). +%% The pin definition for ESP32 is a non-negative integer. +-type direction() :: input | output | output_od. %% The direction is used to set the mode of operation for a GPIO pin, either as an input, an output, or output with open drain. -%% On the STM32 platform pull mode and output_speed must be set at the same time as direction. See @type mode_config() --type mode_config() :: {direction(), pull()} | {output, pull(), output_speed()}. -%% Extended mode configuration options on STM32. Default pull() is `floating', default output_speed() is `mhz_2' if options are omitted. -type pull() :: up | down | up_down | floating. -%% Internal resistor pull mode. STM32 does not support `up_down'. --type output_speed() :: mhz_2 | mhz_25 | mhz_50 | mhz_100. -%% Output clock speed. Only available on STM32, default is `mhz_2'. +%% Internal resistor pull mode. -type low_level() :: low | 0. -type high_level() :: high | 1. -type level() :: low_level() | high_level(). %% Valid pin levels can be atom or binary representation. -type trigger() :: none | rising | falling | both | low | high. -%% Event type that will trigger a `gpio_interrupt'. STM32 only supports `rising', `falling', or `both'. +%% Event type that will trigger a `gpio_interrupt'. %%----------------------------------------------------------------------------- %% @returns Port | error | {error, Reason} @@ -90,8 +79,6 @@ %% port driver will be stared and registered as `gpio'. The use of %% `gpio:open/0' or `gpio:start/0' is required before using any functions %% that require a GPIO port as a parameter. -%% -%% Not currently available on rp2040 (Pico) port, use nif functions. %% @end %%----------------------------------------------------------------------------- -spec start() -> gpio() | {error, Reason :: atom()} | error. @@ -112,8 +99,6 @@ start() -> %% `gpio:start/0' the command will fail. The use of `gpio:open/0' or %% `gpio:start/0' is required before using any functions that require a %% GPIO port as a parameter. -%% -%% Not currently available on rp2040 (Pico) port, use nif functions. %% @end %%----------------------------------------------------------------------------- -spec open() -> gpio() | {error, Reason :: atom()} | error. @@ -127,8 +112,6 @@ open() -> %% %% This function disables any interrupts that are set, stops %% the listening port, and frees all of its resources. -%% -%% Not currently available on rp2040 (Pico) port, use nif functions. %% @end %%----------------------------------------------------------------------------- -spec close(GPIO :: gpio()) -> ok | {error, Reason :: atom()} | error. @@ -141,8 +124,6 @@ close(GPIO) -> %% %% This function disables any interrupts that are set, stops %% the listening port, and frees all of its resources. -%% -%% Not currently available on rp2040 (Pico) port, use nif functions. %% @end %%----------------------------------------------------------------------------- -spec stop() -> ok | {error, Reason :: atom()} | error. @@ -163,8 +144,6 @@ stop() -> %% Read if an input pin state is `high' or `low'. %% Warning: if the pin was not previously configured as an input using %% `gpio:set_direction/3' it will always read as low. -%% -%% Not supported on rp2040 (Pico), use `gpio:digital_read/1' instead. %% @end %%----------------------------------------------------------------------------- -spec read(GPIO :: gpio(), Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. @@ -179,21 +158,6 @@ read(GPIO, Pin) -> %% @doc Set the operational mode of a pin %% %% Pins can be used for input, output, or output with open drain. -%% -%% The STM32 platform has extended direction mode configuration options. -%% See @type mode_config() for details. All configuration must be set using -%% `set_direction/3', including pull() mode, unlike the ESP32 which has a -%% separate function (`set_pin_pull/2'). If you are configuring multiple pins -%% on the same GPIO `bank' with the same options the pins may be configured all -%% at the same time by giving a list of pin numbers in the pin tuple. -%% -%% Example to configure all of the leds on a Nucleo board: -%% -%%
-%%    gpio:set_direction({b, [0,7,14], output)
-%% 
-%% -%% Not supported on rp2040 (Pico), use `gpio:set_pin_mode/2' instead. %% @end %%----------------------------------------------------------------------------- -spec set_direction(GPIO :: gpio(), Pin :: pin(), Direction :: direction()) -> @@ -209,25 +173,6 @@ set_direction(GPIO, Pin, Direction) -> %% @doc Set GPIO digital output level %% %% Set a pin to `high' (1) or `low' (0). -%% -%% The STM32 is capable of setting the state for any, or all of the output pins -%% on a single bank at the same time, this is done by passing a list of pins numbers -%% in the pin tuple. -%% -%% For example, setting all of the even numbered pins to a `high' state, -%% and all of the odd numbered pins to a `low' state can be accomplished in two lines: -%% -%%
-%%    gpio:digital_write({c, [0,2,4,6,8,10,12,14]}, high}),
-%%    gpio:digital_write({c, [1,3,5,7,9,11,13,15]}, low}).
-%% 
-%% -%% To set the same state for all of the pins that have been previously configured as outputs -%% on a specific bank the atom `all' may be used, this will have no effect on any pins on the -%% same bank that have been configured as inputs, so it is safe to use with mixed direction -%% modes on a bank. -%% -%% Not supported on rp2040 (Pico), use `gpio:digital_write/2' instead. %% @end %%----------------------------------------------------------------------------- -spec set_level(GPIO :: gpio(), Pin :: pin(), Level :: level()) -> @@ -247,10 +192,6 @@ set_level(GPIO, Pin, Level) -> %% and `high'. When the interrupt is triggered it will send a tuple: %% `{gpio_interrupt, Pin}' to the process that set the interrupt. `Pin' %% will be the number of the pin that triggered the interrupt. -%% -%% The STM32 port only supports `rising', `falling', or `both'. -%% -%% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- -spec set_int(GPIO :: gpio(), Pin :: pin(), Trigger :: trigger()) -> @@ -272,10 +213,6 @@ set_int(GPIO, Pin, Trigger) -> %% `{gpio_interrupt, Pin}' %% to the process that set the interrupt. Pin will be the number %% of the pin that triggered the interrupt. -%% -%% The STM32 port only supports `rising', `falling', or `both'. -%% -%% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- -spec set_int(GPIO :: gpio(), Pin :: pin(), Trigger :: trigger(), Pid :: pid()) -> @@ -290,8 +227,6 @@ set_int(GPIO, Pin, Trigger, Pid) -> %% @doc Remove a GPIO interrupt %% %% Removes an interrupt from the specified pin. -%% -%% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- -spec remove_int(GPIO :: gpio(), Pin :: pin()) -> ok | {error, Reason :: atom()} | error. @@ -302,7 +237,7 @@ remove_int(GPIO, Pin) -> %% @param Pin number to initialize %% @returns ok %% @doc Initialize a pin to be used as GPIO. -%% This is required on RP2040 and for some pins on ESP32. +%% This may be required for some pins on ESP32. %% @end %%----------------------------------------------------------------------------- -spec init(Pin :: pin()) -> ok. @@ -313,7 +248,6 @@ init(_Pin) -> %% @param Pin number to deinitialize %% @returns ok %% @doc Reset a pin back to the NULL function. -%% Currently only implemented for RP2040 (Pico). %% @end %%----------------------------------------------------------------------------- -spec deinit(Pin :: pin()) -> ok. @@ -327,18 +261,6 @@ deinit(_Pin) -> %% @doc Set the operational mode of a pin %% %% Pins can be used for input, output, or output with open drain. -%% -%% The STM32 platform has extended direction mode configuration options. -%% See @type mode_config() for details. All configuration must be set using -%% `set_direction/3', including pull() mode, unlike the ESP32 which has a -%% separate function (`set_pin_pull/2'). If you are configuring multiple pins -%% on the same GPIO `bank' with the same options the pins may be configured all -%% at the same time by giving a list of pin numbers in the pin tuple. -%% Example to configure all of the leds on a Nucleo board: -%% -%%
-%%    gpio:set_direction({b, [0,7,14], output)
-%% 
%% @end %%----------------------------------------------------------------------------- -spec set_pin_mode(Pin :: pin(), Direction :: direction()) -> @@ -354,10 +276,6 @@ set_pin_mode(_Pin, _Mode) -> %% %% Pins can be internally pulled `up', `down', `up_down' (pulled in %% both directions), or left `floating'. -%% -%% This function is not supported on STM32, the internal resistor must -%% be configured when setting the direction mode, see `set_direction/3' -%% or `set_pin_mode/2'. %% @end %%----------------------------------------------------------------------------- -spec set_pin_pull(Pin :: pin(), Pull :: pull()) -> ok | error. @@ -382,8 +300,6 @@ set_pin_pull(_Pin, _Pull) -> %% will resume the hold function when the chip wakes up from %% Deep-sleep. If the digital gpio also needs to be held during %% Deep-sleep `gpio:deep_sleep_hold_en' should also be called. -%% -%% This function is only supported on ESP32. %% @end %%----------------------------------------------------------------------------- -spec hold_en(Pin :: pin()) -> ok | error. @@ -397,15 +313,13 @@ hold_en(_Pin) -> %% %% When the chip is woken up from Deep-sleep, the gpio will be set to %% the default mode, so, the gpio will output the default level if -%% this function is called. If you don’t want the level changes, the +%% this function is called. If you don't want the level changes, the %% gpio should be configured to a known state before this function is %% called. e.g. If you hold gpio18 high during Deep-sleep, after the %% chip is woken up and `gpio:hold_dis' is called, gpio18 will output -%% low level(because gpio18 is input mode by default). If you don’t +%% low level(because gpio18 is input mode by default). If you don't %% want this behavior, you should configure gpio18 as output mode and %% set it to hight level before calling `gpio:hold_dis'. -%% -%% This function is only supported on ESP32. %% @end %%----------------------------------------------------------------------------- -spec hold_dis(Pin :: pin()) -> ok | error. @@ -429,8 +343,6 @@ hold_dis(_Pin) -> %% Power down or call `gpio_hold_dis' will disable this function, %% otherwise, the digital gpio hold feature works as long as the chip %% enters Deep-sleep. -%% -%% This function is only supported on ESP32. %% @end %%----------------------------------------------------------------------------- -spec deep_sleep_hold_en() -> ok. @@ -440,8 +352,6 @@ deep_sleep_hold_en() -> %%----------------------------------------------------------------------------- %% @returns ok %% @doc Disable all gpio pad functions during Deep-sleep. -%% -%% This function is only supported on ESP32. %% @end %%----------------------------------------------------------------------------- -spec deep_sleep_hold_dis() -> ok. @@ -455,24 +365,6 @@ deep_sleep_hold_dis() -> %% @doc Set GPIO digital output level %% %% Set a pin to `high' (1) or `low' (0). -%% -%% The STM32 is capable of setting the state for any, or all of the output pins -%% on a single bank at the same time, this is done by passing a list of pins numbers -%% in the pin tuple. For example, setting all of the even numbered pins to a `high' state, -%% and all of the odd numbered pins to a `low' state can be accomplished in two lines: -%% -%%
-%%    gpio:digital_write({c, [0,2,4,6,8,10,12,14]}, high}),
-%%    gpio:digital_write({c, [1,3,5,7,9,11,13,15]}, low}).
-%% 
-%% -%% To set the same state for all of the pins that have been previously configured as outputs -%% on a specific bank the atom `all' may be used, this will have no effect on any pins on the -%% same bank that have been configured as inputs, so it is safe to use with mixed direction -%% modes on a bank. -%% -%% The LED pin on the Pico-W can be controlled on the extended pin `{wl, 0}', and does not -%% require or accept `set_pin_mode' or `set_pin_pull' before use. %% @end %%----------------------------------------------------------------------------- -spec digital_write(Pin :: pin(), Level :: level()) -> ok | {error, Reason :: atom()} | error. @@ -487,9 +379,6 @@ digital_write(_Pin, _Level) -> %% Read if an input pin state is high or low. %% Warning: if the pin was not previously configured as an input using %% `gpio:set_pin_mode/2' it will always read as low. -%% -%% The VBUS detect pin on the Pico-W can be read on the extended pin `{wl, 2}', -%% and does not require or accept `set_pin_mode' or `set_pin_pull' before use. %% @end %%----------------------------------------------------------------------------- -spec digital_read(Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. @@ -510,8 +399,6 @@ digital_read(_Pin) -> %% used in an application. If multiple pins are being configured with %% interrupt triggers gpio:set_int/3 should be used otherwise there is %% a race condition when start() is called internally by this function. -%% -%% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- -spec attach_interrupt(Pin :: pin(), Trigger :: trigger()) -> @@ -529,8 +416,6 @@ attach_interrupt(Pin, Trigger) -> %% %% Unlike `gpio:attach_interrupt/2' this function can be safely used %% regardless of the number of interrupt pins used in the application. -%% -%% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- -spec detach_interrupt(Pin :: pin()) -> ok | {error, Reason :: atom()} | error. diff --git a/libs/eavmlib/src/i2c.erl b/libs/avm_esp32/src/i2c.erl similarity index 99% rename from libs/eavmlib/src/i2c.erl rename to libs/avm_esp32/src/i2c.erl index 76260edd1c..7833eadf08 100644 --- a/libs/eavmlib/src/i2c.erl +++ b/libs/avm_esp32/src/i2c.erl @@ -32,6 +32,9 @@ %% @end %%----------------------------------------------------------------------------- -module(i2c). + +-behaviour(i2c_hal). + -export([ open/1, close/1, diff --git a/libs/eavmlib/src/ledc.erl b/libs/avm_esp32/src/ledc.erl similarity index 100% rename from libs/eavmlib/src/ledc.erl rename to libs/avm_esp32/src/ledc.erl diff --git a/libs/eavmlib/src/spi.erl b/libs/avm_esp32/src/spi.erl similarity index 99% rename from libs/eavmlib/src/spi.erl rename to libs/avm_esp32/src/spi.erl index 337388c672..ae9d009e09 100644 --- a/libs/eavmlib/src/spi.erl +++ b/libs/avm_esp32/src/spi.erl @@ -40,6 +40,8 @@ %% -module(spi). +-behaviour(spi_hal). + -export([open/1, close/1, read_at/4, write_at/5, write/3, write_read/3]). %% TODO remove deprecated hspi and vspi diff --git a/libs/eavmlib/src/uart.erl b/libs/avm_esp32/src/uart.erl similarity index 99% rename from libs/eavmlib/src/uart.erl rename to libs/avm_esp32/src/uart.erl index 34a3daf06f..76a51bb85e 100644 --- a/libs/eavmlib/src/uart.erl +++ b/libs/avm_esp32/src/uart.erl @@ -19,6 +19,9 @@ % -module(uart). + +-behaviour(uart_hal). + -export([open/1, open/2, close/1, read/1, read/2, write/2]). -type peripheral() :: string() | binary(). diff --git a/libs/avm_network/src/CMakeLists.txt b/libs/avm_network/src/CMakeLists.txt new file mode 100644 index 0000000000..59afdc8659 --- /dev/null +++ b/libs/avm_network/src/CMakeLists.txt @@ -0,0 +1,55 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# 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 +# + +project(avm_network) + +include(BuildErlang) + +set(ERLANG_MODULES + ahttp_client + epmd + http_server + mdns + network + network_fsm +) + +pack_archive(avm_network ${ERLANG_MODULES}) + +include(../../../version.cmake) + +set(AVM_NETWORK_VERSION ${ATOMVM_BASE_VERSION}) + +install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/beams/ + DESTINATION lib/atomvm/lib/avm_network-${AVM_NETWORK_VERSION}/ebin + FILES_MATCHING PATTERN "*.beam" +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/avm_network.avm + DESTINATION lib/atomvm/lib/avm_network-${AVM_NETWORK_VERSION}/ebin/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION lib/atomvm/lib/avm_network-${AVM_NETWORK_VERSION}/src + FILES_MATCHING PATTERN "*.erl" +) diff --git a/libs/eavmlib/src/ahttp_client.erl b/libs/avm_network/src/ahttp_client.erl similarity index 100% rename from libs/eavmlib/src/ahttp_client.erl rename to libs/avm_network/src/ahttp_client.erl diff --git a/libs/eavmlib/src/epmd.erl b/libs/avm_network/src/epmd.erl similarity index 100% rename from libs/eavmlib/src/epmd.erl rename to libs/avm_network/src/epmd.erl diff --git a/libs/eavmlib/src/http_server.erl b/libs/avm_network/src/http_server.erl similarity index 100% rename from libs/eavmlib/src/http_server.erl rename to libs/avm_network/src/http_server.erl diff --git a/libs/eavmlib/src/mdns.erl b/libs/avm_network/src/mdns.erl similarity index 100% rename from libs/eavmlib/src/mdns.erl rename to libs/avm_network/src/mdns.erl diff --git a/libs/eavmlib/src/network.erl b/libs/avm_network/src/network.erl similarity index 100% rename from libs/eavmlib/src/network.erl rename to libs/avm_network/src/network.erl diff --git a/libs/eavmlib/src/network_fsm.erl b/libs/avm_network/src/network_fsm.erl similarity index 100% rename from libs/eavmlib/src/network_fsm.erl rename to libs/avm_network/src/network_fsm.erl diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt new file mode 100644 index 0000000000..10b1e64ec7 --- /dev/null +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -0,0 +1,51 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# 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 +# + +project(avm_rp2) + +include(BuildErlang) + +set(ERLANG_MODULES + gpio + pico +) + +pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) + +include(../../../version.cmake) + +set(AVM_RP2_VERSION ${ATOMVM_BASE_VERSION}) + +install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/beams/ + DESTINATION lib/atomvm/lib/avm_rp2-${AVM_RP2_VERSION}/ebin + FILES_MATCHING PATTERN "*.beam" +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/avm_rp2.avm + DESTINATION lib/atomvm/lib/avm_rp2-${AVM_RP2_VERSION}/ebin/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION lib/atomvm/lib/avm_rp2-${AVM_RP2_VERSION}/src + FILES_MATCHING PATTERN "*.erl" +) diff --git a/libs/avm_rp2/src/gpio.erl b/libs/avm_rp2/src/gpio.erl new file mode 100644 index 0000000000..0d8b4e4e16 --- /dev/null +++ b/libs/avm_rp2/src/gpio.erl @@ -0,0 +1,309 @@ +% +% This file is part of AtomVM. +% +% Copyright 2018-2023 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 +% + +%%----------------------------------------------------------------------------- +%% @doc GPIO driver module for RP2 (Pico) +%% +%% This module provides functions for interacting with micro-controller GPIO +%% (General Purpose Input and Output) pins on the RP2 platform. +%% +%% The port-based API (start/0, open/0, read/2, set_direction/3, set_level/3) +%% is implemented as a wrapper around NIFs. Interrupt functions (set_int/3,4 +%% and remove_int/2) are not supported on this platform and return +%% `{error, not_supported}'. +%% @end +%%----------------------------------------------------------------------------- +-module(gpio). + +-behaviour(gpio_hal). + +-export([ + start/0, + open/0, + read/2, + set_direction/3, + set_level/3, + set_int/3, set_int/4, + remove_int/2, + stop/0, + close/1 +]). +-export([ + init/1, + deinit/1, + set_pin_mode/2, + set_pin_pull/2, + digital_write/2, + digital_read/1 +]). + +-type pin() :: non_neg_integer() | pin_tuple(). +%% The pin definition for RP2040 is a non-negative integer. A tuple is used for the extra "WL" pins on the Pico-W. +-type pin_tuple() :: {wl, 0..2}. +%% The extra "WL" pins on Pico-W use bank `wl'. +-type direction() :: input | output | output_od. +%% The direction is used to set the mode of operation for a GPIO pin, either as an input, an output, or output with open drain. +-type pull() :: up | down | up_down | floating. +%% Internal resistor pull mode. +-type low_level() :: low | 0. +-type high_level() :: high | 1. +-type level() :: low_level() | high_level(). +%% Valid pin levels can be atom or binary representation. +-type gpio() :: pid(). +%% This is the pid returned by `gpio:start/0'. Unlike ESP32 and STM32, this +%% is not a real port but a process wrapping NIF calls. +-type trigger() :: none | rising | falling | both | low | high. +%% Event type that will trigger a `gpio_interrupt'. + +%%----------------------------------------------------------------------------- +%% @returns Pid | error | {error, Reason} +%% @doc Start the GPIO driver +%% +%% Returns the pid of the active GPIO driver process, otherwise the GPIO +%% driver process will be started and registered as `gpio'. The use of +%% `gpio:open/0' or `gpio:start/0' is required before using any functions +%% that require a GPIO pid as a parameter. +%% @end +%%----------------------------------------------------------------------------- +-spec start() -> gpio() | {error, Reason :: atom()} | error. +start() -> + case whereis(gpio) of + undefined -> + open(); + GPIO -> + GPIO + end. + +%%----------------------------------------------------------------------------- +%% @returns Pid | error | {error, Reason} +%% @doc Start the GPIO driver +%% +%% The GPIO driver process will be started and registered as `gpio'. If the +%% process has already been started through `gpio:open/0' or +%% `gpio:start/0' the command will fail. +%% @end +%%----------------------------------------------------------------------------- +-spec open() -> gpio() | {error, Reason :: atom()} | error. +open() -> + Pid = spawn(fun gpio_loop/0), + register(gpio, Pid), + Pid. + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from gpio:start/0 +%% @returns ok | error | {error, Reason} +%% @doc Stop the GPIO driver +%% @end +%%----------------------------------------------------------------------------- +-spec close(GPIO :: gpio()) -> ok | {error, Reason :: atom()} | error. +close(GPIO) -> + Ref = make_ref(), + GPIO ! {'$call', {self(), Ref}, {close}}, + receive + {Ref, Result} -> Result + end. + +%%----------------------------------------------------------------------------- +%% @returns ok | error | {error, Reason} +%% @doc Stop the GPIO driver +%% @end +%%----------------------------------------------------------------------------- +-spec stop() -> ok | {error, Reason :: atom()} | error. +stop() -> + case whereis(gpio) of + undefined -> + ok; + Pid when is_pid(Pid) -> + close(Pid) + end. + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from gpio:start/0 +%% @param Pin number of the pin to read +%% @returns high | low | error | {error, Reason} +%% @doc Read the digital state of a GPIO pin +%% +%% Read if an input pin state is `high' or `low'. +%% Warning: if the pin was not previously configured as an input using +%% `gpio:set_direction/3' it will always read as low. +%% @end +%%----------------------------------------------------------------------------- +-spec read(GPIO :: gpio(), Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. +read(_GPIO, Pin) -> + digital_read(Pin). + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from `gpio:start/0' +%% @param Pin number of the pin to configure +%% @param Direction is `input', `output', or `output_od' +%% @returns ok | error | {error, Reason} +%% @doc Set the operational mode of a pin +%% +%% Pins can be used for input, output, or output with open drain. +%% @end +%%----------------------------------------------------------------------------- +-spec set_direction(GPIO :: gpio(), Pin :: pin(), Direction :: direction()) -> + ok | {error, Reason :: atom()} | error. +set_direction(_GPIO, Pin, Direction) -> + init(Pin), + set_pin_mode(Pin, Direction). + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from `gpio:start/0' +%% @param Pin number of the pin to write +%% @param Level the desired output level to set +%% @returns ok | error | {error, Reason} +%% @doc Set GPIO digital output level +%% +%% Set a pin to `high' (1) or `low' (0). +%% @end +%%----------------------------------------------------------------------------- +-spec set_level(GPIO :: gpio(), Pin :: pin(), Level :: level()) -> + ok | {error, Reason :: atom()} | error. +set_level(_GPIO, Pin, Level) -> + digital_write(Pin, Level). + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from `gpio:start/0' +%% @param Pin number of the pin to set the interrupt on +%% @param Trigger is the state that will trigger an interrupt +%% @returns {error, not_supported} +%% @doc Not supported on RP2. +%% @end +%%----------------------------------------------------------------------------- +-spec set_int(GPIO :: gpio(), Pin :: pin(), Trigger :: trigger()) -> + {error, not_supported}. +set_int(_GPIO, _Pin, _Trigger) -> + {error, not_supported}. + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from `gpio:start/0' +%% @param Pin number of the pin to set the interrupt on +%% @param Trigger is the state that will trigger an interrupt +%% @param Pid is the process that will receive the interrupt message +%% @returns {error, not_supported} +%% @doc Not supported on RP2. +%% @end +%%----------------------------------------------------------------------------- +-spec set_int(GPIO :: gpio(), Pin :: pin(), Trigger :: trigger(), Pid :: pid()) -> + {error, not_supported}. +set_int(_GPIO, _Pin, _Trigger, _Pid) -> + {error, not_supported}. + +%%----------------------------------------------------------------------------- +%% @param GPIO pid that was returned from `gpio:start/0' +%% @param Pin number of the pin to remove the interrupt +%% @returns {error, not_supported} +%% @doc Not supported on RP2. +%% @end +%%----------------------------------------------------------------------------- +-spec remove_int(GPIO :: gpio(), Pin :: pin()) -> {error, not_supported}. +remove_int(_GPIO, _Pin) -> + {error, not_supported}. + +%%----------------------------------------------------------------------------- +%% @param Pin number to initialize +%% @returns ok +%% @doc Initialize a pin to be used as GPIO. +%% This is required on RP2040. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Pin :: pin()) -> ok. +init(_Pin) -> + ok. + +%%----------------------------------------------------------------------------- +%% @param Pin number to deinitialize +%% @returns ok +%% @doc Reset a pin back to the NULL function. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Pin :: pin()) -> ok. +deinit(_Pin) -> + ok. + +%%----------------------------------------------------------------------------- +%% @param Pin number to set operational mode +%% @param Direction is `input', `output', or `output_od' +%% @returns ok | error | {error, Reason} +%% @doc Set the operational mode of a pin +%% +%% Pins can be used for input, output, or output with open drain. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pin_mode(Pin :: pin(), Direction :: direction()) -> + ok | {error, Reason :: atom()} | error. +set_pin_mode(_Pin, _Mode) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Pin number to set internal resistor direction +%% @param Pull is the internal resistor state +%% @returns ok | error +%% @doc Set the internal resistor of a pin +%% +%% Pins can be internally pulled `up', `down', `up_down' (pulled in +%% both directions), or left `floating'. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pin_pull(Pin :: pin(), Pull :: pull()) -> ok | error. +set_pin_pull(_Pin, _Pull) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Pin number of the pin to write +%% @param Level the desired output level to set +%% @returns ok | error | {error, Reason} +%% @doc Set GPIO digital output level +%% +%% Set a pin to `high' (1) or `low' (0). +%% +%% The LED pin on the Pico-W can be controlled on the extended pin `{wl, 0}', and does not +%% require or accept `set_pin_mode' or `set_pin_pull' before use. +%% @end +%%----------------------------------------------------------------------------- +-spec digital_write(Pin :: pin(), Level :: level()) -> ok | {error, Reason :: atom()} | error. +digital_write(_Pin, _Level) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Pin number of the pin to read +%% @returns high | low | error | {error, Reason} +%% @doc Read the digital state of a GPIO pin +%% +%% Read if an input pin state is high or low. +%% Warning: if the pin was not previously configured as an input using +%% `gpio:set_pin_mode/2' it will always read as low. +%% +%% The VBUS detect pin on the Pico-W can be read on the extended pin `{wl, 2}', +%% and does not require or accept `set_pin_mode' or `set_pin_pull' before use. +%% @end +%%----------------------------------------------------------------------------- +-spec digital_read(Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. +digital_read(_Pin) -> + erlang:nif_error(undefined). + +%% @private +gpio_loop() -> + receive + {'$call', {Pid, Ref}, {close}} -> + unregister(gpio), + Pid ! {Ref, ok} + end. diff --git a/libs/eavmlib/src/pico.erl b/libs/avm_rp2/src/pico.erl similarity index 100% rename from libs/eavmlib/src/pico.erl rename to libs/avm_rp2/src/pico.erl diff --git a/libs/avm_stm32/src/CMakeLists.txt b/libs/avm_stm32/src/CMakeLists.txt new file mode 100644 index 0000000000..d85f481d4c --- /dev/null +++ b/libs/avm_stm32/src/CMakeLists.txt @@ -0,0 +1,50 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# 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 +# + +project(avm_stm32) + +include(BuildErlang) + +set(ERLANG_MODULES + gpio +) + +pack_archive(avm_stm32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) + +include(../../../version.cmake) + +set(AVM_STM32_VERSION ${ATOMVM_BASE_VERSION}) + +install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/beams/ + DESTINATION lib/atomvm/lib/avm_stm32-${AVM_STM32_VERSION}/ebin + FILES_MATCHING PATTERN "*.beam" +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/avm_stm32.avm + DESTINATION lib/atomvm/lib/avm_stm32-${AVM_STM32_VERSION}/ebin/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION lib/atomvm/lib/avm_stm32-${AVM_STM32_VERSION}/src + FILES_MATCHING PATTERN "*.erl" +) diff --git a/libs/avm_stm32/src/gpio.erl b/libs/avm_stm32/src/gpio.erl new file mode 100644 index 0000000000..f7fe5ff092 --- /dev/null +++ b/libs/avm_stm32/src/gpio.erl @@ -0,0 +1,335 @@ +% +% This file is part of AtomVM. +% +% Copyright 2018-2023 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 +% + +%%----------------------------------------------------------------------------- +%% @doc GPIO driver module for STM32 +%% +%% This module provides functions for interacting with micro-controller GPIO +%% (General Purpose Input and Output) pins on the STM32 platform. +%% +%% Note: `-type pin()' used in this driver refers to a tuple {GPIO_BANK, PIN}. +%% @end +%%----------------------------------------------------------------------------- +-module(gpio). + +-behaviour(gpio_hal). + +-export([ + start/0, + open/0, + read/2, + set_direction/3, + set_level/3, + set_int/3, set_int/4, + remove_int/2, + stop/0, + close/1 +]). +-export([ + init/1, + deinit/1, + set_pin_mode/2, + set_pin_pull/2, + digital_write/2, + digital_read/1 +]). + +-type pin() :: {gpio_bank(), 0..15}. +%% A pin parameter on STM32 is a tuple consisting of a GPIO bank and pin number. +-type gpio_bank() :: a | b | c | d | e | f | g | h | i | j | k. +%% STM32 gpio banks vary by board, some only break out `a' thru `h'. +-type direction() :: input | output | output_od | mode_config(). +%% The direction is used to set the mode of operation for a GPIO pin, either as an input, an output, or output with open drain. +%% Pull mode and output_speed must be set at the same time as direction. See @type mode_config() +-type mode_config() :: {direction(), pull()} | {output, pull(), output_speed()}. +%% Extended mode configuration options. Default pull() is `floating', default output_speed() is `mhz_2' if options are omitted. +-type pull() :: up | down | floating. +%% Internal resistor pull mode. STM32 does not support `up_down'. +-type output_speed() :: mhz_2 | mhz_25 | mhz_50 | mhz_100. +%% Output clock speed. Default is `mhz_2'. +-type low_level() :: low | 0. +-type high_level() :: high | 1. +-type level() :: low_level() | high_level(). +%% Valid pin levels can be atom or binary representation. +-type gpio() :: port(). +%% This is the port returned by `gpio:start/0'. +-type trigger() :: none | rising | falling | both | low | high. +%% Event type that will trigger a `gpio_interrupt'. + +%%----------------------------------------------------------------------------- +%% @returns Port | error | {error, Reason} +%% @doc Start the GPIO driver port +%% +%% Returns the port of the active GPIO port driver, otherwise the GPIO +%% port driver will be stared and registered as `gpio'. The use of +%% `gpio:open/0' or `gpio:start/0' is required before using any functions +%% that require a GPIO port as a parameter. +%% @end +%%----------------------------------------------------------------------------- +-spec start() -> gpio() | {error, Reason :: atom()} | error. +start() -> + case whereis(gpio) of + undefined -> + open(); + GPIO -> + GPIO + end. + +%%----------------------------------------------------------------------------- +%% @returns Port | error | {error, Reason} +%% @doc Start the GPIO driver port +%% +%% The GPIO port driver will be stared and registered as `gpio'. If the +%% port has already been started through the `gpio:open/0' or +%% `gpio:start/0' the command will fail. The use of `gpio:open/0' or +%% `gpio:start/0' is required before using any functions that require a +%% GPIO port as a parameter. +%% @end +%%----------------------------------------------------------------------------- +-spec open() -> gpio() | {error, Reason :: atom()} | error. +open() -> + open_port({spawn, "gpio"}, []). + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from gpio:start/0 +%% @returns ok | error | {error, Reason} +%% @doc Stop the GPIO interrupt port +%% +%% This function disables any interrupts that are set, stops +%% the listening port, and frees all of its resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(GPIO :: gpio()) -> ok | {error, Reason :: atom()} | error. +close(GPIO) -> + port:call(GPIO, {close}). + +%%----------------------------------------------------------------------------- +%% @returns ok | error | {error, Reason} +%% @doc Stop the GPIO interrupt port +%% +%% This function disables any interrupts that are set, stops +%% the listening port, and frees all of its resources. +%% @end +%%----------------------------------------------------------------------------- +-spec stop() -> ok | {error, Reason :: atom()} | error. +stop() -> + case whereis(gpio) of + undefined -> + ok; + Port when is_port(Port) -> + close(Port) + end. + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from gpio:start/0 +%% @param Pin number of the pin to read +%% @returns high | low | error | {error, Reason} +%% @doc Read the digital state of a GPIO pin +%% +%% Read if an input pin state is `high' or `low'. +%% Warning: if the pin was not previously configured as an input using +%% `gpio:set_direction/3' it will always read as low. +%% @end +%%----------------------------------------------------------------------------- +-spec read(GPIO :: gpio(), Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. +read(GPIO, Pin) -> + port:call(GPIO, {read, Pin}). + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from `gpio:start/0' +%% @param Pin number of the pin to configure +%% @param Direction is `input', `output', or `output_od' +%% @returns ok | error | {error, Reason} +%% @doc Set the operational mode of a pin +%% +%% Pins can be used for input, output, or output with open drain. +%% @end +%%----------------------------------------------------------------------------- +-spec set_direction(GPIO :: gpio(), Pin :: pin(), Direction :: direction()) -> + ok | {error, Reason :: atom()} | error. +set_direction(GPIO, Pin, Direction) -> + port:call(GPIO, {set_direction, Pin, Direction}). + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from `gpio:start/0' +%% @param Pin number of the pin to write +%% @param Level the desired output level to set +%% @returns ok | error | {error, Reason} +%% @doc Set GPIO digital output level +%% +%% Set a pin to `high' (1) or `low' (0). +%% @end +%%----------------------------------------------------------------------------- +-spec set_level(GPIO :: gpio(), Pin :: pin(), Level :: level()) -> + ok | {error, Reason :: atom()} | error. +set_level(GPIO, Pin, Level) -> + port:call(GPIO, {set_level, Pin, Level}). + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from `gpio:start/0' +%% @param Pin number of the pin to set the interrupt on +%% @param Trigger is the state that will trigger an interrupt +%% @returns ok | error | {error, Reason} +%% @doc Set a GPIO interrupt +%% +%% Available triggers are `none' (which is the same as disabling an +%% interrupt), `rising', `falling', `both' (rising or falling), `low', +%% and `high'. When the interrupt is triggered it will send a tuple: +%% `{gpio_interrupt, {Bank, Pin}}' to the process that set the interrupt. +%% @end +%%----------------------------------------------------------------------------- +-spec set_int(GPIO :: gpio(), Pin :: pin(), Trigger :: trigger()) -> + ok | {error, Reason :: atom()} | error. +set_int(GPIO, Pin, Trigger) -> + port:call(GPIO, {set_int, Pin, Trigger}). + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from `gpio:start/0' +%% @param Pin number of the pin to set the interrupt on +%% @param Trigger is the state that will trigger an interrupt +%% @param Pid is the process that will receive the interrupt message +%% @returns ok | error | {error, Reason} +%% @doc Set a GPIO interrupt +%% +%% Available triggers are `none' (which is the same as disabling an +%% interrupt), `rising', `falling', `both' (rising or falling), `low', and +%% `high'. When the interrupt is triggered it will send a tuple: +%% `{gpio_interrupt, {Bank, Pin}}' +%% to the specified process. +%% @end +%%----------------------------------------------------------------------------- +-spec set_int(GPIO :: gpio(), Pin :: pin(), Trigger :: trigger(), Pid :: pid()) -> + ok | {error, Reason :: atom()} | error. +set_int(GPIO, Pin, Trigger, Pid) -> + port:call(GPIO, {set_int, Pin, Trigger, Pid}). + +%%----------------------------------------------------------------------------- +%% @param GPIO port that was returned from `gpio:start/0' +%% @param Pin number of the pin to remove the interrupt +%% @returns ok | error | {error, Reason} +%% @doc Remove a GPIO interrupt +%% +%% Removes an interrupt from the specified pin. +%% @end +%%----------------------------------------------------------------------------- +-spec remove_int(GPIO :: gpio(), Pin :: pin()) -> ok | {error, Reason :: atom()} | error. +remove_int(GPIO, Pin) -> + port:call(GPIO, {remove_int, Pin}). + +%%----------------------------------------------------------------------------- +%% @param Pin number to initialize +%% @returns ok +%% @doc Initialize a pin to be used as GPIO. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Pin :: pin()) -> ok. +init(_Pin) -> + ok. + +%%----------------------------------------------------------------------------- +%% @param Pin number to deinitialize +%% @returns ok +%% @doc Reset a pin back to the NULL function. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Pin :: pin()) -> ok. +deinit(_Pin) -> + ok. + +%%----------------------------------------------------------------------------- +%% @param Pin number to set operational mode +%% @param Direction is `input', `output', or `output_od' +%% @returns ok | error | {error, Reason} +%% @doc Set the operational mode of a pin +%% +%% Pins can be used for input, output, or output with open drain. +%% +%% All configuration must be set using `set_pin_mode/2', including +%% pull() mode. If you are configuring multiple pins on the same GPIO +%% `bank' with the same options the pins may be configured all at the +%% same time by giving a list of pin numbers in the pin tuple. +%% +%% Example to configure all of the leds on a Nucleo board: +%% +%%
+%%    gpio:set_pin_mode({b, [0,7,14]}, output)
+%% 
+%% @end +%%----------------------------------------------------------------------------- +-spec set_pin_mode(Pin :: pin(), Direction :: direction()) -> + ok | {error, Reason :: atom()} | error. +set_pin_mode(_Pin, _Mode) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Pin number to set internal resistor direction +%% @param Pull is the internal resistor state +%% @returns ok | error +%% @doc Set the internal resistor of a pin +%% +%% Pins can be internally pulled `up', `down', or left `floating'. +%% STM32 does not support `up_down'. +%% @end +%%----------------------------------------------------------------------------- +-spec set_pin_pull(Pin :: pin(), Pull :: pull()) -> ok | error. +set_pin_pull(_Pin, _Pull) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Pin number of the pin to write +%% @param Level the desired output level to set +%% @returns ok | error | {error, Reason} +%% @doc Set GPIO digital output level +%% +%% Set a pin to `high' (1) or `low' (0). +%% +%% STM32 is capable of setting the state for any, or all of the output pins +%% on a single bank at the same time, this is done by passing a list of pins numbers +%% in the pin tuple. For example, setting all of the even numbered pins to a `high' state, +%% and all of the odd numbered pins to a `low' state can be accomplished in two lines: +%% +%%
+%%    gpio:digital_write({c, [0,2,4,6,8,10,12,14]}, high),
+%%    gpio:digital_write({c, [1,3,5,7,9,11,13,15]}, low).
+%% 
+%% +%% To set the same state for all of the pins that have been previously configured as outputs +%% on a specific bank the atom `all' may be used, this will have no effect on any pins on the +%% same bank that have been configured as inputs, so it is safe to use with mixed direction +%% modes on a bank. +%% @end +%%----------------------------------------------------------------------------- +-spec digital_write(Pin :: pin(), Level :: level()) -> ok | {error, Reason :: atom()} | error. +digital_write(_Pin, _Level) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Pin number of the pin to read +%% @returns high | low | error | {error, Reason} +%% @doc Read the digital state of a GPIO pin +%% +%% Read if an input pin state is high or low. +%% Warning: if the pin was not previously configured as an input using +%% `gpio:set_pin_mode/2' it will always read as low. +%% @end +%%----------------------------------------------------------------------------- +-spec digital_read(Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. +digital_read(_Pin) -> + erlang:nif_error(undefined). diff --git a/libs/eavmlib/src/CMakeLists.txt b/libs/eavmlib/src/CMakeLists.txt index c3abbef24b..548511680c 100644 --- a/libs/eavmlib/src/CMakeLists.txt +++ b/libs/eavmlib/src/CMakeLists.txt @@ -23,31 +23,18 @@ project(eavmlib) include(BuildErlang) set(ERLANG_MODULES - ahttp_client atomvm avm_pubsub console - emscripten - epmd - esp - esp_dac - esp_adc - gpio - i2c - http_server + gpio_hal + i2c_hal json_encoder - ledc logger_manager - mdns - network - network_fsm - pico port - spi + spi_hal timer_manager timestamp_util - uart - websocket + uart_hal ) pack_archive(eavmlib ${ERLANG_MODULES}) diff --git a/libs/eavmlib/src/gpio_hal.erl b/libs/eavmlib/src/gpio_hal.erl new file mode 100644 index 0000000000..703df71789 --- /dev/null +++ b/libs/eavmlib/src/gpio_hal.erl @@ -0,0 +1,94 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc GPIO Hardware Abstraction Layer behavior +%% +%% This module defines the behavior that platform-specific GPIO modules +%% must implement. It provides a common interface for basic GPIO operations +%% across all supported platforms (ESP32, RP2, STM32). +%% @end +%%----------------------------------------------------------------------------- +-module(gpio_hal). + +-type direction() :: input | output | output_od. +%% The direction is used to set the mode of operation for a GPIO pin, +%% either as an input, an output, or output with open drain. + +-type pull() :: up | down | up_down | floating. +%% Internal resistor pull mode. + +-type low_level() :: low | 0. +-type high_level() :: high | 1. +-type level() :: low_level() | high_level(). +%% Valid pin levels can be atom or integer representation. + +-type trigger() :: none | rising | falling | both | low | high. +%% Event type that will trigger a `gpio_interrupt'. + +-type gpio() :: port() | pid(). +%% Handle returned by `start/0' or `open/0'. + +-export_type([direction/0, pull/0, level/0, trigger/0, gpio/0]). + +%% NIF-based API + +-callback init(Pin :: term()) -> ok. + +-callback deinit(Pin :: term()) -> ok. + +-callback set_pin_mode(Pin :: term(), Direction :: direction()) -> + ok | {error, Reason :: atom()} | error. + +-callback set_pin_pull(Pin :: term(), Pull :: pull()) -> ok | error. + +-callback digital_write(Pin :: term(), Level :: level()) -> + ok | {error, Reason :: atom()} | error. + +-callback digital_read(Pin :: term()) -> + high | low | {error, Reason :: atom()} | error. + +%% Port-based API + +-callback start() -> gpio() | {error, Reason :: atom()} | error. + +-callback open() -> gpio() | {error, Reason :: atom()} | error. + +-callback close(GPIO :: gpio()) -> ok | {error, Reason :: atom()} | error. + +-callback stop() -> ok | {error, Reason :: atom()} | error. + +-callback read(GPIO :: gpio(), Pin :: term()) -> + high | low | {error, Reason :: atom()} | error. + +-callback set_direction(GPIO :: gpio(), Pin :: term(), Direction :: direction()) -> + ok | {error, Reason :: atom()} | error. + +-callback set_level(GPIO :: gpio(), Pin :: term(), Level :: level()) -> + ok | {error, Reason :: atom()} | error. + +-callback set_int(GPIO :: gpio(), Pin :: term(), Trigger :: trigger()) -> + ok | {error, Reason :: atom()} | error. + +-callback set_int(GPIO :: gpio(), Pin :: term(), Trigger :: trigger(), Pid :: pid()) -> + ok | {error, Reason :: atom()} | error. + +-callback remove_int(GPIO :: gpio(), Pin :: term()) -> + ok | {error, Reason :: atom()} | error. diff --git a/libs/eavmlib/src/i2c_hal.erl b/libs/eavmlib/src/i2c_hal.erl new file mode 100644 index 0000000000..2f5c1c86de --- /dev/null +++ b/libs/eavmlib/src/i2c_hal.erl @@ -0,0 +1,75 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc I2C Hardware Abstraction Layer behavior +%% +%% This module defines the behavior that platform-specific I2C modules +%% must implement. It provides a common interface for I2C operations +%% across all supported platforms. +%% @end +%%----------------------------------------------------------------------------- +-module(i2c_hal). + +-type i2c() :: port() | pid() | term(). +%% Handle returned by `open/1'. + +-type address() :: non_neg_integer(). +%% I2C device address. + +-type register() :: non_neg_integer(). +%% Register address within an I2C device. + +-type params() :: [term()]. +%% Initialization parameters for the I2C bus. + +-export_type([i2c/0, address/0, register/0, params/0]). + +-callback open(Params :: params()) -> i2c(). + +-callback close(I2C :: i2c()) -> ok | {error, Reason :: term()}. + +-callback begin_transmission(I2C :: i2c(), Address :: address()) -> + ok | {error, Reason :: term()}. + +-callback write_byte(I2C :: i2c(), Byte :: byte()) -> + ok | {error, Reason :: term()}. + +-callback write_bytes(I2C :: i2c(), Bytes :: binary()) -> + ok | {error, Reason :: term()}. + +-callback end_transmission(I2C :: i2c()) -> + ok | {error, Reason :: term()}. + +-callback read_bytes(I2C :: i2c(), Address :: address(), Count :: non_neg_integer()) -> + {ok, Data :: binary()} | {error, Reason :: term()}. + +-callback read_bytes( + I2C :: i2c(), Address :: address(), Register :: register(), Count :: non_neg_integer() +) -> + {ok, Data :: binary()} | {error, Reason :: term()}. + +-callback write_bytes(I2C :: i2c(), Address :: address(), BinOrInt :: binary() | byte()) -> + ok | {error, Reason :: term()}. + +-callback write_bytes( + I2C :: i2c(), Address :: address(), Register :: register(), BinOrInt :: binary() | integer() +) -> + ok | {error, Reason :: term()}. diff --git a/libs/eavmlib/src/spi_hal.erl b/libs/eavmlib/src/spi_hal.erl new file mode 100644 index 0000000000..274962de9b --- /dev/null +++ b/libs/eavmlib/src/spi_hal.erl @@ -0,0 +1,76 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc SPI Hardware Abstraction Layer behavior +%% +%% This module defines the behavior that platform-specific SPI modules +%% must implement. It provides a common interface for SPI operations +%% across all supported platforms. +%% @end +%%----------------------------------------------------------------------------- +-module(spi_hal). + +-type spi() :: port() | pid() | term(). +%% Handle returned by `open/1'. + +-type device_name() :: atom(). +%% Name identifying an SPI device, as specified in the device configuration. + +-type address() :: non_neg_integer(). +%% SPI device address. + +-type params() :: [term()] | map(). +%% Initialization parameters for the SPI bus. + +-type transaction() :: #{ + command => integer(), + address => non_neg_integer(), + write_data => binary(), + write_bits => non_neg_integer(), + read_bits => non_neg_integer() +}. +%% SPI transaction map. + +-export_type([spi/0, device_name/0, address/0, params/0, transaction/0]). + +-callback open(Params :: params()) -> spi(). + +-callback close(SPI :: spi()) -> ok. + +-callback read_at( + SPI :: spi(), DeviceName :: device_name(), Address :: address(), Len :: non_neg_integer() +) -> + {ok, integer()} | {error, Reason :: term()}. + +-callback write_at( + SPI :: spi(), + DeviceName :: device_name(), + Address :: address(), + Len :: non_neg_integer(), + Data :: integer() +) -> + {ok, integer()} | {error, Reason :: term()}. + +-callback write(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + ok | {error, Reason :: term()}. + +-callback write_read(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + {ok, ReadData :: binary()} | {error, Reason :: term()}. diff --git a/libs/eavmlib/src/uart_hal.erl b/libs/eavmlib/src/uart_hal.erl new file mode 100644 index 0000000000..63a40b0d5c --- /dev/null +++ b/libs/eavmlib/src/uart_hal.erl @@ -0,0 +1,53 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc UART Hardware Abstraction Layer behavior +%% +%% This module defines the behavior that platform-specific UART modules +%% must implement. It provides a common interface for UART operations +%% across all supported platforms. +%% @end +%%----------------------------------------------------------------------------- +-module(uart_hal). + +-type uart() :: port() | pid() | term(). +%% Handle returned by `open/1' or `open/2'. + +-type peripheral() :: string() | binary(). +%% UART peripheral name. + +-type params() :: [term()]. +%% Initialization parameters for the UART bus. + +-export_type([uart/0, peripheral/0, params/0]). + +-callback open(Params :: params()) -> uart() | {error, Reason :: term()}. + +-callback open(Name :: peripheral(), Params :: params()) -> uart() | {error, Reason :: term()}. + +-callback close(UART :: uart()) -> ok | {error, Reason :: term()}. + +-callback read(UART :: uart()) -> {ok, Data :: iodata()} | {error, Reason :: term()}. + +-callback read(UART :: uart(), Timeout :: pos_integer()) -> + {ok, Data :: iodata()} | {error, Reason :: term()}. + +-callback write(UART :: uart(), Data :: iodata()) -> ok | {error, Reason :: term()}. diff --git a/libs/esp32boot/CMakeLists.txt b/libs/esp32boot/CMakeLists.txt index 23cdc8795c..316b42136e 100644 --- a/libs/esp32boot/CMakeLists.txt +++ b/libs/esp32boot/CMakeLists.txt @@ -23,7 +23,7 @@ project(esp32boot) include(BuildErlang) if (Elixir_FOUND) - pack_runnable(elixir_esp32boot esp32init esp32devmode eavmlib estdlib alisp exavmlib) + pack_runnable(elixir_esp32boot esp32init esp32devmode eavmlib estdlib alisp avm_network avm_esp32 exavmlib) endif() -pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp) +pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp avm_network avm_esp32) diff --git a/src/platforms/emscripten/tests/src/test_atomvm.html b/src/platforms/emscripten/tests/src/test_atomvm.html index dfc8e4d9bf..cde06286ff 100644 --- a/src/platforms/emscripten/tests/src/test_atomvm.html +++ b/src/platforms/emscripten/tests/src/test_atomvm.html @@ -36,6 +36,7 @@ "/tests/build/test_atomvm.beam", "/build/libs/estdlib/src/estdlib.avm", "/build/libs/eavmlib/src/eavmlib.avm", + "/build/libs/avm_emscripten/src/avm_emscripten.avm", ], }; diff --git a/src/platforms/emscripten/tests/src/test_call.html b/src/platforms/emscripten/tests/src/test_call.html index d497a6fa9f..59ae9db66d 100644 --- a/src/platforms/emscripten/tests/src/test_call.html +++ b/src/platforms/emscripten/tests/src/test_call.html @@ -33,7 +33,7 @@ // wasm_webserver serves under /tests/build/ files in src/platform/escripten/build/tests/src subdirectory // and under /build/ files in build/ subdirectory. var Module = { - arguments: ['/tests/build/test_call.beam', '/build/libs/eavmlib/src/eavmlib.avm'] + arguments: ['/tests/build/test_call.beam', '/build/libs/eavmlib/src/eavmlib.avm', '/build/libs/avm_emscripten/src/avm_emscripten.avm'] } diff --git a/src/platforms/emscripten/tests/src/test_html5.html b/src/platforms/emscripten/tests/src/test_html5.html index 407af6dd5b..9b7ed24166 100644 --- a/src/platforms/emscripten/tests/src/test_html5.html +++ b/src/platforms/emscripten/tests/src/test_html5.html @@ -34,7 +34,7 @@ // wasm_webserver serves under /tests/build/ files in src/platform/escripten/build/tests/src subdirectory // and under /build/ files in build/ subdirectory. var Module = { - arguments: ['/tests/build/test_html5.beam', '/build/libs/estdlib/src/estdlib.avm', '/build/libs/eavmlib/src/eavmlib.avm'] + arguments: ['/tests/build/test_html5.beam', '/build/libs/estdlib/src/estdlib.avm', '/build/libs/eavmlib/src/eavmlib.avm', '/build/libs/avm_emscripten/src/avm_emscripten.avm'] } diff --git a/src/platforms/emscripten/tests/src/test_websockets.html b/src/platforms/emscripten/tests/src/test_websockets.html index 8b852ee0cf..c8853b4d3a 100644 --- a/src/platforms/emscripten/tests/src/test_websockets.html +++ b/src/platforms/emscripten/tests/src/test_websockets.html @@ -35,7 +35,7 @@ const echoServer = hashParams.get('echo_server'); var Module = { - arguments: ['/tests/build/test_websockets.beam', '/build/libs/eavmlib/src/eavmlib.avm', '/build/libs/estdlib/src/estdlib.avm'], + arguments: ['/tests/build/test_websockets.beam', '/build/libs/eavmlib/src/eavmlib.avm', '/build/libs/avm_emscripten/src/avm_emscripten.avm', '/build/libs/estdlib/src/estdlib.avm'], preRun: [function() { if (echoServer) { // Set environment variable for the Erlang code to read 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 3710f05434..96dfc29835 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -29,7 +29,7 @@ endif() ExternalProject_Add(HostAtomVM SOURCE_DIR ../../../../../../../../ INSTALL_COMMAND cmake -E echo "Skipping install step." - BUILD_COMMAND cmake --build . --target=atomvmlib ${host_atomvm_jit_target} --target=PackBEAM + BUILD_COMMAND cmake --build . --target=atomvmlib-esp32 ${host_atomvm_jit_target} --target=PackBEAM ) macro(jit_precompile module_name) @@ -113,7 +113,7 @@ endif() add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/esp32_test_modules.avm" COMMAND HostAtomVM-prefix/src/HostAtomVM-build/tools/packbeam/PackBEAM -i esp32_test_modules.avm - HostAtomVM-prefix/src/HostAtomVM-build/libs/atomvmlib.avm + HostAtomVM-prefix/src/HostAtomVM-build/libs/atomvmlib-esp32.avm ${erlang_test_beams_to_package} DEPENDS HostAtomVM diff --git a/src/platforms/rp2/tests/test_erl_sources/CMakeLists.txt b/src/platforms/rp2/tests/test_erl_sources/CMakeLists.txt index 5c6526d5e2..532a75d347 100644 --- a/src/platforms/rp2/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/rp2/tests/test_erl_sources/CMakeLists.txt @@ -21,16 +21,16 @@ include(ExternalProject) if(NOT AVM_DISABLE_JIT) set(host_atomvm_jit_target "--target=jit") -set(atomvlib_name "atomvmlib-${AVM_JIT_TARGET_ARCH}.avm") +set(atomvlib_name "atomvmlib-rp2-${AVM_JIT_TARGET_ARCH}.avm") else() set(host_atomvm_jit_target "") -set(atomvlib_name "atomvmlib.avm") +set(atomvlib_name "atomvmlib-rp2.avm") endif() ExternalProject_Add(HostAtomVM SOURCE_DIR ../../../../../../ INSTALL_COMMAND cmake -E echo "Skipping install step." CMAKE_ARGS -DAVM_DISABLE_JIT=${AVM_DISABLE_JIT} - BUILD_COMMAND cmake --build . --target=atomvmlib ${host_atomvm_jit_target} --target=PackBEAM --target=UF2Tool + BUILD_COMMAND cmake --build . --target=atomvmlib-rp2 ${host_atomvm_jit_target} --target=PackBEAM --target=UF2Tool ) macro(jit_precompile module_name) diff --git a/tests/libs/eavmlib/CMakeLists.txt b/tests/libs/eavmlib/CMakeLists.txt index 98de68e338..92484b196d 100644 --- a/tests/libs/eavmlib/CMakeLists.txt +++ b/tests/libs/eavmlib/CMakeLists.txt @@ -33,4 +33,4 @@ set(ERLANG_MODULES ) pack_archive(test_eavmlib_lib ${ERLANG_MODULES}) -pack_test(test_eavmlib eavmlib estdlib etest) +pack_test(test_eavmlib eavmlib avm_network estdlib etest) diff --git a/tests/libs/estdlib/CMakeLists.txt b/tests/libs/estdlib/CMakeLists.txt index 039f5ec069..11281f9fab 100644 --- a/tests/libs/estdlib/CMakeLists.txt +++ b/tests/libs/estdlib/CMakeLists.txt @@ -59,4 +59,4 @@ set(ERLANG_MODULES ) pack_archive(test_estdlib_lib ${ERLANG_MODULES}) -pack_test(test_estdlib estdlib eavmlib etest) +pack_test(test_estdlib estdlib eavmlib avm_network etest) From 53ff0bbfdb57d239ace03a1ea2ddadd650a7f3ff Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Wed, 18 Feb 2026 13:38:40 +0100 Subject: [PATCH 2/2] Add I2C support to RP2 platform Provide low-level API (nifs) and high-level API following `i2c_hal` behavior Signed-off-by: Paul Guyot --- examples/erlang/rp2/CMakeLists.txt | 2 + examples/erlang/rp2/pico_i2c_scanner.erl | 74 ++++ examples/erlang/rp2/pico_lis3dh.erl | 121 ++++++ libs/avm_rp2/src/CMakeLists.txt | 1 + libs/avm_rp2/src/gpio.erl | 17 + libs/avm_rp2/src/i2c.erl | 463 +++++++++++++++++++++++ src/platforms/rp2/src/lib/CMakeLists.txt | 4 +- src/platforms/rp2/src/lib/gpiodriver.c | 37 ++ src/platforms/rp2/src/lib/i2cdriver.c | 394 +++++++++++++++++++ 9 files changed, 1112 insertions(+), 1 deletion(-) create mode 100644 examples/erlang/rp2/pico_i2c_scanner.erl create mode 100644 examples/erlang/rp2/pico_lis3dh.erl create mode 100644 libs/avm_rp2/src/i2c.erl create mode 100644 src/platforms/rp2/src/lib/i2cdriver.c diff --git a/examples/erlang/rp2/CMakeLists.txt b/examples/erlang/rp2/CMakeLists.txt index 2f482a9cc4..b1f59c0057 100644 --- a/examples/erlang/rp2/CMakeLists.txt +++ b/examples/erlang/rp2/CMakeLists.txt @@ -24,6 +24,8 @@ include(BuildErlang) pack_uf2(hello_pico hello_pico) pack_uf2(pico_blink pico_blink) +pack_uf2(pico_i2c_scanner pico_i2c_scanner) +pack_uf2(pico_lis3dh pico_lis3dh) pack_uf2(pico_rtc pico_rtc) pack_uf2(picow_blink picow_blink) pack_uf2(picow_wifi_sta picow_wifi_sta) diff --git a/examples/erlang/rp2/pico_i2c_scanner.erl b/examples/erlang/rp2/pico_i2c_scanner.erl new file mode 100644 index 0000000000..0304c4424d --- /dev/null +++ b/examples/erlang/rp2/pico_i2c_scanner.erl @@ -0,0 +1,74 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc I2C bus scanner example for Pico. +%% +%% Scans all valid 7-bit I2C addresses (0x08-0x77) and prints which devices +%% respond with an ACK. +%% +%% Default wiring (Pico pin numbers): +%% SDA -> GP4 (pin 6) +%% SCL -> GP5 (pin 7) +%% GND -> pin 8 +%% 3V3 -> pin 36 +%% +%% These are the default I2C0 pins on the Pico. +%% @end +%%----------------------------------------------------------------------------- +-module(pico_i2c_scanner). +-export([start/0]). + +%% I2C pins (I2C0 default on Pico) +-define(SDA_PIN, 4). +-define(SCL_PIN, 5). + +start() -> + I2C = i2c:open([ + {scl, ?SCL_PIN}, + {sda, ?SDA_PIN}, + {clock_speed_hz, 100000}, + {peripheral, 0} + ]), + console:puts("I2C bus scan (0x08-0x77):\n"), + Found = scan(I2C, 16#08, []), + case Found of + [] -> + console:puts("No devices found.\n"); + _ -> + console:puts("Done. Found "), + console:puts(erlang:integer_to_list(length(Found))), + console:puts(" device(s).\n") + end. + +scan(_I2C, Addr, Acc) when Addr > 16#77 -> + lists:reverse(Acc); +scan(I2C, Addr, Acc) -> + NewAcc = + case i2c:read_bytes(I2C, Addr, 1) of + {ok, _} -> + console:puts(" 0x"), + console:puts(erlang:integer_to_list(Addr, 16)), + console:puts(" ACK\n"), + [Addr | Acc]; + {error, _} -> + Acc + end, + scan(I2C, Addr + 1, NewAcc). diff --git a/examples/erlang/rp2/pico_lis3dh.erl b/examples/erlang/rp2/pico_lis3dh.erl new file mode 100644 index 0000000000..d92169c7b8 --- /dev/null +++ b/examples/erlang/rp2/pico_lis3dh.erl @@ -0,0 +1,121 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc LIS3DH accelerometer example for Pico. +%% +%% Reads X, Y, Z acceleration from a LIS3DH connected via I2C and prints +%% the values every second. +%% +%% Default wiring (Pico pin numbers): +%% SDA -> GP4 (pin 6) +%% SCL -> GP5 (pin 7) +%% +%% These are the default I2C0 pins on the Pico. +%% @end +%%----------------------------------------------------------------------------- +-module(pico_lis3dh). +-export([start/0]). + +%% I2C pins (I2C0 default on Pico) +-define(SDA_PIN, 4). +-define(SCL_PIN, 5). + +%% LIS3DH I2C address (0x18 when SDO/SA0 is low, 0x19 when high) +-define(LIS3DH_ADDR, 16#19). + +%% LIS3DH registers +-define(WHO_AM_I, 16#0F). +-define(CTRL_REG1, 16#20). +-define(CTRL_REG4, 16#23). +-define(OUT_X_L, 16#28). + +%% Expected WHO_AM_I response +-define(LIS3DH_ID, 16#33). + +start() -> + I2C = i2c:open([ + {scl, ?SCL_PIN}, + {sda, ?SDA_PIN}, + {clock_speed_hz, 400000}, + {peripheral, 0} + ]), + case check_who_am_i(I2C) of + ok -> + configure(I2C), + loop(I2C); + {error, Reason} -> + console:puts("LIS3DH not found: "), + console:puts(erlang:atom_to_list(Reason)), + console:puts("\n") + end. + +check_who_am_i(I2C) -> + case i2c:read_bytes(I2C, ?LIS3DH_ADDR, ?WHO_AM_I, 1) of + {ok, <>} -> + console:puts("LIS3DH detected\n"), + ok; + {ok, <>} -> + console:puts("Unexpected WHO_AM_I: "), + console:puts(erlang:integer_to_list(Other, 16)), + console:puts("\n"), + {error, unexpected_id}; + {error, _} = Error -> + Error + end. + +configure(I2C) -> + %% CTRL_REG1: 50 Hz ODR, normal mode, X/Y/Z enabled + %% Bits: ODR=0100 LPen=0 Zen=1 Yen=1 Xen=1 -> 0x47 + ok = i2c:write_bytes(I2C, ?LIS3DH_ADDR, ?CTRL_REG1, 16#47), + %% CTRL_REG4: +/- 2g full scale, high resolution + %% Bits: BDU=1 BLE=0 FS=00 HR=1 ST=00 SIM=0 -> 0x88 + ok = i2c:write_bytes(I2C, ?LIS3DH_ADDR, ?CTRL_REG4, 16#88). + +loop(I2C) -> + case read_acceleration(I2C) of + {ok, {X, Y, Z}} -> + console:puts("X="), + console:puts(erlang:integer_to_list(X)), + console:puts(" Y="), + console:puts(erlang:integer_to_list(Y)), + console:puts(" Z="), + console:puts(erlang:integer_to_list(Z)), + console:puts("\n"); + {error, Reason} -> + console:puts("Read error: "), + console:puts(erlang:atom_to_list(Reason)), + console:puts("\n") + end, + timer:sleep(1000), + loop(I2C). + +read_acceleration(I2C) -> + %% Read 6 bytes starting at OUT_X_L with auto-increment (bit 7 set) + case i2c:read_bytes(I2C, ?LIS3DH_ADDR, ?OUT_X_L bor 16#80, 6) of + {ok, <>} -> + %% 12-bit left-justified in high-resolution mode: shift right by 4 + X = ((XH bsl 8) bor XL) bsr 4, + Y = ((YH bsl 8) bor YL) bsr 4, + Z = ((ZH bsl 8) bor ZL) bsr 4, + {ok, {X, Y, Z}}; + {error, _} = Error -> + Error + end. diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt index 10b1e64ec7..ec5dff9098 100644 --- a/libs/avm_rp2/src/CMakeLists.txt +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -24,6 +24,7 @@ include(BuildErlang) set(ERLANG_MODULES gpio + i2c pico ) diff --git a/libs/avm_rp2/src/gpio.erl b/libs/avm_rp2/src/gpio.erl index 0d8b4e4e16..d6dc746943 100644 --- a/libs/avm_rp2/src/gpio.erl +++ b/libs/avm_rp2/src/gpio.erl @@ -48,6 +48,7 @@ -export([ init/1, deinit/1, + set_function/2, set_pin_mode/2, set_pin_pull/2, digital_write/2, @@ -58,6 +59,8 @@ %% The pin definition for RP2040 is a non-negative integer. A tuple is used for the extra "WL" pins on the Pico-W. -type pin_tuple() :: {wl, 0..2}. %% The extra "WL" pins on Pico-W use bank `wl'. +-type gpio_function() :: spi | uart | i2c | pwm | sio | pio0 | pio1. +%% GPIO function select values matching Pico SDK gpio_function_t. -type direction() :: input | output | output_od. %% The direction is used to set the mode of operation for a GPIO pin, either as an input, an output, or output with open drain. -type pull() :: up | down | up_down | floating. @@ -239,6 +242,20 @@ init(_Pin) -> deinit(_Pin) -> ok. +%%----------------------------------------------------------------------------- +%% @param Pin GPIO pin number +%% @param Function the function to assign to the pin +%% @returns ok +%% @doc Select the function for a GPIO pin. +%% +%% Maps to `gpio_set_function()' in the Pico SDK. +%% Common functions: `sio' (default GPIO), `i2c', `spi', `uart', `pwm'. +%% @end +%%----------------------------------------------------------------------------- +-spec set_function(Pin :: non_neg_integer(), Function :: gpio_function()) -> ok. +set_function(_Pin, _Function) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Pin number to set operational mode %% @param Direction is `input', `output', or `output_od' diff --git a/libs/avm_rp2/src/i2c.erl b/libs/avm_rp2/src/i2c.erl new file mode 100644 index 0000000000..d83a9293b1 --- /dev/null +++ b/libs/avm_rp2/src/i2c.erl @@ -0,0 +1,463 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% 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 +% + +%%----------------------------------------------------------------------------- +%% @doc AtomVM I2C interface for RP2 (Pico) +%% +%% This module provides an interface to the I2C hardware on RP2 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API +%% {@link init/2}, {@link deinit/1}, {@link set_baudrate/2}, +%% {@link write_blocking/4}, {@link read_blocking/4}, +%% {@link write_timeout_us/5}, {@link read_timeout_us/5}. +%% These operate on a bare resource reference returned by {@link init/2}. +%% Pin muxing must be done separately via `gpio:set_function/2' and +%% `gpio:set_pin_pull/2'. +%% +%% High-level API (`i2c_hal` behavior) +%% {@link open/1}, {@link close/1}, {@link read_bytes/3}, {@link read_bytes/4}, +%% {@link write_bytes/3}, {@link write_bytes/4}, +%% {@link begin_transmission/2}, {@link write_byte/2}, {@link write_bytes/2}, +%% {@link end_transmission/1}. +%% {@link open/1} handles pin setup automatically. +%% @end +%%----------------------------------------------------------------------------- +-module(i2c). + +-behaviour(i2c_hal). + +%% High-level API (i2c_hal behaviour) +-export([ + open/1, + close/1, + begin_transmission/2, + write_byte/2, + end_transmission/1, + read_bytes/3, read_bytes/4, + write_bytes/2, write_bytes/3, write_bytes/4 +]). + +%% Low-level API (Pico SDK) +-export([ + init/2, + deinit/1, + set_baudrate/2, + write_blocking/4, + read_blocking/4, + write_timeout_us/5, + read_timeout_us/5 +]). + +-type pin() :: non_neg_integer(). +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 0 | 1. +-type param() :: + {scl, pin()} + | {sda, pin()} + | {clock_speed_hz, freq_hz()} + | {peripheral, peripheral()} + | {send_timeout_ms, timeout()}. +-type params() :: [param()]. +-type i2c_resource() :: reference(). +-type i2c() :: pid(). +-type address() :: non_neg_integer(). +-type register_addr() :: non_neg_integer(). + +-export_type([ + i2c/0, i2c_resource/0, address/0, register_addr/0 +]). + +-define(DEFAULT_CLOCK_SPEED_HZ, 100000). +-define(DEFAULT_PERIPHERAL, 0). +-define(DEFAULT_SEND_TIMEOUT_MS, 500). + +%% --------------------------------------------------------------------------- +%% High-level API (ESP32-compatible) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns I2C handle +%% @doc Open a connection to the I2C driver +%% +%% This function configures the GPIO pins for I2C function, enables +%% internal pull-ups, and initializes the I2C peripheral. +%% +%% Supported parameters: +%%
    +%%
  • `{scl, Pin}' - the SCL pin number (required)
  • +%%
  • `{sda, Pin}' - the SDA pin number (required)
  • +%%
  • `{clock_speed_hz, Hz}' - the I2C clock speed in Hz (default: 100000)
  • +%%
  • `{peripheral, 0 | 1}' - the I2C peripheral to use (default: 0)
  • +%%
  • `{send_timeout_ms, Ms | infinity}' - send timeout in milliseconds (default: 500)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: params()) -> i2c(). +open(Params) -> + SCL = proplists:get_value(scl, Params), + SDA = proplists:get_value(sda, Params), + ClockSpeedHz = proplists:get_value(clock_speed_hz, Params, ?DEFAULT_CLOCK_SPEED_HZ), + Peripheral = proplists:get_value(peripheral, Params, ?DEFAULT_PERIPHERAL), + SendTimeoutMs = proplists:get_value(send_timeout_ms, Params, ?DEFAULT_SEND_TIMEOUT_MS), + gpio:set_function(SCL, i2c), + gpio:set_function(SDA, i2c), + gpio:set_pin_pull(SCL, up), + gpio:set_pin_pull(SDA, up), + {ok, {_ActualBaudrate, Resource}} = ?MODULE:init(Peripheral, ClockSpeedHz), + spawn_link(fun() -> loop(Resource, SendTimeoutMs, undefined) end). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @returns `ok' +%% @doc Closes the connection to the I2C driver and frees resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(I2C :: i2c()) -> ok | {error, Reason :: term()}. +close(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Address 7-bit I2C address of the device +%% @returns `ok' or `{error, Reason}' +%% @doc Begin a transmission of I2C commands +%% +%% This command is typically followed by one or more calls to +%% `write_byte/2' and then a call to `end_transmission/1' +%% @end +%%----------------------------------------------------------------------------- +-spec begin_transmission(I2C :: i2c(), Address :: address()) -> ok | {error, Reason :: term()}. +begin_transmission(Pid, Address) -> + call(Pid, {begin_transmission, Address}). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Byte value to write +%% @returns `ok' or `{error, Reason}' +%% @doc Write a byte to the device. +%% +%% This command must be wrapped in a `begin_transmission/2' +%% and `end_transmission/1' call. +%% @end +%%----------------------------------------------------------------------------- +-spec write_byte(I2C :: i2c(), Byte :: byte()) -> ok | {error, Reason :: term()}. +write_byte(Pid, Byte) -> + call(Pid, {write_byte, Byte}). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Bytes value to write +%% @returns `ok' or `{error, Reason}' +%% @doc Write a sequence of bytes to the device. +%% +%% This command must be wrapped in a `begin_transmission/2' +%% and `end_transmission/1' call. +%% @end +%%----------------------------------------------------------------------------- +-spec write_bytes(I2C :: i2c(), Bytes :: binary()) -> ok | {error, Reason :: term()}. +write_bytes(Pid, Bytes) -> + call(Pid, {write_bytes_tx, Bytes}). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @returns `ok' or `{error, Reason}' +%% @doc End a transmission of I2C commands +%% +%% This command is typically preceded by a call to `begin_transmission/2' +%% and one or more calls to `write_byte/2'. +%% @end +%%----------------------------------------------------------------------------- +-spec end_transmission(I2C :: i2c()) -> ok | {error, Reason :: term()}. +end_transmission(Pid) -> + call(Pid, end_transmission). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Address 7-bit I2C address of the device +%% @param Count The number of bytes to read +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read a block of bytes from the I2C device. +%% @end +%%----------------------------------------------------------------------------- +-spec read_bytes(I2C :: i2c(), Address :: address(), Count :: non_neg_integer()) -> + {ok, Data :: binary()} | {error, Reason :: term()}. +read_bytes(Pid, Address, Count) -> + call(Pid, {read_bytes, Address, Count}). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Address 7-bit I2C address of the device +%% @param Register The register address from which to read +%% @param Count The number of bytes to read +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read a block of bytes from the I2C device starting at a specified +%% register address. +%% +%% This performs a write of the register address with nostop (repeated +%% start), followed by a read of the requested number of bytes. +%% @end +%%----------------------------------------------------------------------------- +-spec read_bytes( + I2C :: i2c(), Address :: address(), Register :: register_addr(), Count :: non_neg_integer() +) -> {ok, binary()} | {error, Reason :: term()}. +read_bytes(Pid, Address, Register, Count) -> + call(Pid, {read_bytes, Address, Register, Count}). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Address 7-bit I2C address of the device +%% @param Data The binary or byte value to write +%% @returns `ok' or `{error, Reason}' +%% @doc Write a block of bytes to the I2C device. +%% @end +%%----------------------------------------------------------------------------- +-spec write_bytes(I2C :: i2c(), Address :: address(), BinOrInt :: binary() | byte()) -> + ok | {error, Reason :: term()}. +write_bytes(Pid, Address, Int) when is_integer(Int) -> + write_bytes(Pid, Address, <>); +write_bytes(Pid, Address, Data) -> + call(Pid, {write_bytes, Address, Data}). + +%%----------------------------------------------------------------------------- +%% @param I2C I2C handle created via `open/1' +%% @param Address 7-bit I2C address of the device +%% @param Register The register address to which to write +%% @param Data The binary or byte value to write +%% @returns `ok' or `{error, Reason}' +%% @doc Write a block of bytes to the I2C device starting at a specified +%% register address. +%% @end +%%----------------------------------------------------------------------------- +-spec write_bytes( + I2C :: i2c(), + Address :: address(), + Register :: register_addr(), + BinOrInt :: binary() | integer() +) -> ok | {error, Reason :: term()}. +write_bytes(Pid, Address, Register, Int) when is_integer(Int) -> + write_bytes(Pid, Address, Register, <>); +write_bytes(Pid, Address, Register, Data) -> + call(Pid, {write_bytes, Address, Register, Data}). + +%% --------------------------------------------------------------------------- +%% Low-level API (Pico SDK) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Peripheral I2C peripheral number (0 or 1) +%% @param Baudrate Baudrate in Hz (e.g. 100000 for 100kHz) +%% @returns `{ok, {ActualBaudrate, Resource}}' +%% @doc Initialize the I2C HW block. +%% +%% Pin muxing must be done separately via `gpio:set_function/2' +%% and `gpio:set_pin_pull/2'. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Peripheral :: peripheral(), Baudrate :: freq_hz()) -> + {ok, {ActualBaudrate :: freq_hz(), Resource :: i2c_resource()}}. +init(_Peripheral, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/2' +%% @returns `ok' +%% @doc Disable the I2C HW block. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: i2c_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/2' +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, ActualBaudrate}' +%% @doc Set I2C baudrate. +%% @end +%%----------------------------------------------------------------------------- +-spec set_baudrate(Resource :: i2c_resource(), Baudrate :: freq_hz()) -> + {ok, ActualBaudrate :: freq_hz()}. +set_baudrate(_Resource, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/2' +%% @param Addr 7-bit I2C device address +%% @param Data Binary data to write +%% @param Nostop If true, master retains control of the bus (no Stop issued) +%% @returns Number of bytes written, or `{error, Reason}' +%% @doc Write to I2C device, blocking. +%% @end +%%----------------------------------------------------------------------------- +-spec write_blocking( + Resource :: i2c_resource(), Addr :: address(), Data :: binary(), Nostop :: boolean() +) -> + non_neg_integer() | {error, Reason :: term()}. +write_blocking(_Resource, _Addr, _Data, _Nostop) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/2' +%% @param Addr 7-bit I2C device address +%% @param Count Number of bytes to read +%% @param Nostop If true, master retains control of the bus (no Stop issued) +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read from I2C device, blocking. +%% @end +%%----------------------------------------------------------------------------- +-spec read_blocking( + Resource :: i2c_resource(), Addr :: address(), Count :: non_neg_integer(), Nostop :: boolean() +) -> + {ok, binary()} | {error, Reason :: term()}. +read_blocking(_Resource, _Addr, _Count, _Nostop) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/2' +%% @param Addr 7-bit I2C device address +%% @param Data Binary data to write +%% @param Nostop If true, master retains control of the bus (no Stop issued) +%% @param TimeoutUs Timeout in microseconds +%% @returns Number of bytes written, or `{error, Reason}' +%% @doc Write to I2C device, with timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec write_timeout_us( + Resource :: i2c_resource(), + Addr :: address(), + Data :: binary(), + Nostop :: boolean(), + TimeoutUs :: non_neg_integer() +) -> + non_neg_integer() | {error, Reason :: term()}. +write_timeout_us(_Resource, _Addr, _Data, _Nostop, _TimeoutUs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/2' +%% @param Addr 7-bit I2C device address +%% @param Count Number of bytes to read +%% @param Nostop If true, master retains control of the bus (no Stop issued) +%% @param TimeoutUs Timeout in microseconds +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read from I2C device, with timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read_timeout_us( + Resource :: i2c_resource(), + Addr :: address(), + Count :: non_neg_integer(), + Nostop :: boolean(), + TimeoutUs :: non_neg_integer() +) -> + {ok, binary()} | {error, Reason :: term()}. +read_timeout_us(_Resource, _Addr, _Count, _Nostop, _TimeoutUs) -> + erlang:nif_error(undefined). + +%% --------------------------------------------------------------------------- +%% Internal helpers +%% --------------------------------------------------------------------------- + +%% @private +call(Pid, Request) -> + Ref = make_ref(), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> Reply + end. + +%% @private +loop(Resource, SendTimeoutMs, TxState) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, SendTimeoutMs, TxState, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply, NewTxState} -> + From ! {Ref, Reply}, + loop(Resource, SendTimeoutMs, NewTxState) + end + end. + +%% @private +handle_request(Resource, _SendTimeoutMs, _TxState, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(_Resource, _SendTimeoutMs, undefined, {begin_transmission, Address}) -> + {reply, ok, {Address, []}}; +handle_request(_Resource, _SendTimeoutMs, {_Address, _Acc}, {begin_transmission, _NewAddress}) -> + {reply, {error, transaction_already_in_progress}, {_Address, _Acc}}; +handle_request(_Resource, _SendTimeoutMs, {Address, Acc}, {write_byte, Byte}) -> + {reply, ok, {Address, [<> | Acc]}}; +handle_request(_Resource, _SendTimeoutMs, undefined, {write_byte, _Byte}) -> + {reply, {error, no_transaction}, undefined}; +handle_request(_Resource, _SendTimeoutMs, {Address, Acc}, {write_bytes_tx, Bytes}) -> + {reply, ok, {Address, [Bytes | Acc]}}; +handle_request(_Resource, _SendTimeoutMs, undefined, {write_bytes_tx, _Bytes}) -> + {reply, {error, no_transaction}, undefined}; +handle_request(Resource, SendTimeoutMs, {Address, Acc}, end_transmission) -> + Data = erlang:iolist_to_binary(lists:reverse(Acc)), + Result = + case do_write(Resource, Address, Data, false, SendTimeoutMs) of + {error, _} = Error -> Error; + _N -> ok + end, + {reply, Result, undefined}; +handle_request(_Resource, _SendTimeoutMs, undefined, end_transmission) -> + {reply, {error, no_transaction}, undefined}; +handle_request(Resource, SendTimeoutMs, TxState, {read_bytes, Address, Count}) -> + Result = do_read(Resource, Address, Count, false, SendTimeoutMs), + {reply, Result, TxState}; +handle_request(Resource, SendTimeoutMs, TxState, {read_bytes, Address, Register, Count}) -> + Result = + case do_write(Resource, Address, <>, true, SendTimeoutMs) of + {error, _} = Error -> Error; + _N -> do_read(Resource, Address, Count, false, SendTimeoutMs) + end, + {reply, Result, TxState}; +handle_request(Resource, SendTimeoutMs, TxState, {write_bytes, Address, Data}) -> + Result = + case do_write(Resource, Address, Data, false, SendTimeoutMs) of + {error, _} = Error -> Error; + _N -> ok + end, + {reply, Result, TxState}; +handle_request(Resource, SendTimeoutMs, TxState, {write_bytes, Address, Register, Data}) -> + Result = + case do_write(Resource, Address, <>, false, SendTimeoutMs) of + {error, _} = Error -> Error; + _N -> ok + end, + {reply, Result, TxState}. + +%% @private +do_read(Resource, Address, Count, Nostop, infinity) -> + ?MODULE:read_blocking(Resource, Address, Count, Nostop); +do_read(Resource, Address, Count, Nostop, TimeoutMs) -> + ?MODULE:read_timeout_us(Resource, Address, Count, Nostop, TimeoutMs * 1000). + +%% @private +do_write(Resource, Address, Data, Nostop, infinity) -> + ?MODULE:write_blocking(Resource, Address, Data, Nostop); +do_write(Resource, Address, Data, Nostop, TimeoutMs) -> + ?MODULE:write_timeout_us(Resource, Address, Data, Nostop, TimeoutMs * 1000). diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index b9e594c9df..a4c383fc24 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -31,6 +31,7 @@ set(HEADER_FILES set(SOURCE_FILES gpiodriver.c + i2cdriver.c networkdriver.c otp_crypto_platform.c platform_defaultatoms.c @@ -57,6 +58,7 @@ target_link_libraries( libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC hardware_gpio + hardware_i2c hardware_sync pico_float pico_mbedtls @@ -121,4 +123,4 @@ if (NOT AVM_DISABLE_JIT) target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,jit_stream_flash_get_nif") endif() -target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,otp_crypto_nif") +target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,i2c_nif -Wl,-u -Wl,otp_crypto_nif") diff --git a/src/platforms/rp2/src/lib/gpiodriver.c b/src/platforms/rp2/src/lib/gpiodriver.c index 7bedcb8281..38ffefe1a9 100644 --- a/src/platforms/rp2/src/lib/gpiodriver.c +++ b/src/platforms/rp2/src/lib/gpiodriver.c @@ -37,6 +37,17 @@ static const struct Nif *gpio_nif_get_nif(const char *nifname); +static const AtomStringIntPair gpio_function_table[] = { + { ATOM_STR("\x3", "spi"), GPIO_FUNC_SPI }, + { ATOM_STR("\x4", "uart"), GPIO_FUNC_UART }, + { ATOM_STR("\x3", "i2c"), GPIO_FUNC_I2C }, + { ATOM_STR("\x3", "pwm"), GPIO_FUNC_PWM }, + { ATOM_STR("\x3", "sio"), GPIO_FUNC_SIO }, + { ATOM_STR("\x4", "pio0"), GPIO_FUNC_PIO0 }, + { ATOM_STR("\x4", "pio1"), GPIO_FUNC_PIO1 }, + SELECT_INT_DEFAULT(-1) +}; + static const AtomStringIntPair pin_mode_table[] = { { ATOM_STR("\x5", "input"), GPIO_IN }, { ATOM_STR("\x6", "output"), GPIO_OUT }, @@ -99,6 +110,23 @@ static term nif_gpio_deinit(Context *ctx, int argc, term argv[]) return OK_ATOM; } +static term nif_gpio_set_function(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[0], term_is_integer); + uint gpio_num = term_to_int(argv[0]); + if (UNLIKELY(gpio_num >= NUM_BANK0_GPIOS)) { + RAISE_ERROR(BADARG_ATOM); + } + int func = interop_atom_term_select_int(gpio_function_table, argv[1], ctx->global); + if (UNLIKELY(func < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + gpio_set_function(gpio_num, (gpio_function_t) func); + return OK_ATOM; +} + static term nif_gpio_set_pin_mode(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -228,6 +256,11 @@ static const struct Nif gpio_deinit_nif = { .nif_ptr = nif_gpio_deinit }; +static const struct Nif gpio_set_function_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_gpio_set_function +}; + static const struct Nif gpio_set_pin_mode_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_gpio_set_pin_mode @@ -258,6 +291,10 @@ const struct Nif *gpio_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &gpio_deinit_nif; } + if (strcmp("gpio:set_function/2", nifname) == 0 || strcmp("Elixir.GPIO:set_function/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &gpio_set_function_nif; + } if (strcmp("gpio:set_pin_mode/2", nifname) == 0 || strcmp("Elixir.GPIO:set_pin_mode/2", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &gpio_set_pin_mode_nif; diff --git a/src/platforms/rp2/src/lib/i2cdriver.c b/src/platforms/rp2/src/lib/i2cdriver.c new file mode 100644 index 0000000000..d166c06560 --- /dev/null +++ b/src/platforms/rp2/src/lib/i2cdriver.c @@ -0,0 +1,394 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * 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 "context.h" +#include "defaultatoms.h" +#include "erl_nif.h" +#include "erl_nif_priv.h" +#include "globalcontext.h" +#include "interop.h" +#include "memory.h" +#include "nifs.h" +#include "rp2_sys.h" +#include "term.h" + +// #define ENABLE_TRACE +#include "trace.h" + +#define NUM_I2C_INSTANCES 2 + +static ErlNifResourceType *i2c_resource_type; + +struct I2CResource +{ + i2c_inst_t *i2c_inst; +}; + +static term create_pair(Context *ctx, term term1, term term2) +{ + term ret = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ret, 0, term1); + term_put_tuple_element(ret, 1, term2); + return ret; +} + +static term create_error_tuple(Context *ctx, term reason) +{ + return create_pair(ctx, ERROR_ATOM, reason); +} + +static term pico_err_to_error_tuple(Context *ctx, int err) +{ + if (err == PICO_ERROR_TIMEOUT) { + return create_error_tuple(ctx, TIMEOUT_ATOM); + } + return create_error_tuple(ctx, globalcontext_make_atom(ctx->global, ATOM_STR("\x3", "eio"))); +} + +static bool get_i2c_resource(Context *ctx, term resource_term, struct I2CResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, i2c_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct I2CResource *) rsrc_obj_ptr; + return true; +} + +static bool term_to_nostop(term t) +{ + if (UNLIKELY(!term_is_atom(t))) { + return false; + } + return t == TRUE_ATOM; +} + +static term nif_i2c_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[0], term_is_integer); + VALIDATE_VALUE(argv[1], term_is_integer); + + int peripheral = term_to_int(argv[0]); + if (UNLIKELY(peripheral < 0 || peripheral >= NUM_I2C_INSTANCES)) { + RAISE_ERROR(BADARG_ATOM); + } + + uint baudrate = (uint) term_to_int(argv[1]); + i2c_inst_t *inst = i2c_get_instance((uint) peripheral); + + uint actual_baudrate = i2c_init(inst, baudrate); + + struct I2CResource *rsrc_obj = enif_alloc_resource(i2c_resource_type, sizeof(struct I2CResource)); + if (IS_NULL_PTR(rsrc_obj)) { + i2c_deinit(inst); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + rsrc_obj->i2c_inst = inst; + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + i2c_deinit(inst); + enif_release_resource(rsrc_obj); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + + // Return {ok, {ActualBaudrate, Resource}} + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term inner = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(inner, 0, term_from_int(actual_baudrate)); + term_put_tuple_element(inner, 1, obj); + + return create_pair(ctx, OK_ATOM, inner); +} + +static term nif_i2c_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct I2CResource *rsrc_obj; + if (UNLIKELY(!get_i2c_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + i2c_deinit(rsrc_obj->i2c_inst); + rsrc_obj->i2c_inst = NULL; + + return OK_ATOM; +} + +static term nif_i2c_set_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct I2CResource *rsrc_obj; + if (UNLIKELY(!get_i2c_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + uint baudrate = (uint) term_to_int(argv[1]); + uint actual = i2c_set_baudrate(rsrc_obj->i2c_inst, baudrate); + + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return create_pair(ctx, OK_ATOM, term_from_int(actual)); +} + +static term nif_i2c_write_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct I2CResource *rsrc_obj; + if (UNLIKELY(!get_i2c_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_binary); + VALIDATE_VALUE(argv[3], term_is_atom); + + uint8_t addr = (uint8_t) term_to_int(argv[1]); + const uint8_t *data = (const uint8_t *) term_binary_data(argv[2]); + size_t len = term_binary_size(argv[2]); + bool nostop = term_to_nostop(argv[3]); + + int ret = i2c_write_blocking(rsrc_obj->i2c_inst, addr, data, len, nostop); + if (UNLIKELY(ret < 0)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return pico_err_to_error_tuple(ctx, ret); + } + + return term_from_int(ret); +} + +static term nif_i2c_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct I2CResource *rsrc_obj; + if (UNLIKELY(!get_i2c_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + VALIDATE_VALUE(argv[3], term_is_atom); + + uint8_t addr = (uint8_t) term_to_int(argv[1]); + avm_int_t count = term_to_int(argv[2]); + bool nostop = term_to_nostop(argv[3]); + + if (UNLIKELY(count < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + // Allocate {ok, Data} tuple and binary + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(count), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(count, &ctx->heap, ctx->global); + uint8_t *buf = (uint8_t *) term_binary_data(data); + + int ret = i2c_read_blocking(rsrc_obj->i2c_inst, addr, buf, (size_t) count, nostop); + if (UNLIKELY(ret < 0)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return pico_err_to_error_tuple(ctx, ret); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_i2c_write_timeout_us(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct I2CResource *rsrc_obj; + if (UNLIKELY(!get_i2c_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_binary); + VALIDATE_VALUE(argv[3], term_is_atom); + VALIDATE_VALUE(argv[4], term_is_integer); + + uint8_t addr = (uint8_t) term_to_int(argv[1]); + const uint8_t *data = (const uint8_t *) term_binary_data(argv[2]); + size_t len = term_binary_size(argv[2]); + bool nostop = term_to_nostop(argv[3]); + uint timeout_us = (uint) term_to_int(argv[4]); + + int ret = i2c_write_timeout_us(rsrc_obj->i2c_inst, addr, data, len, nostop, timeout_us); + if (UNLIKELY(ret < 0)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return pico_err_to_error_tuple(ctx, ret); + } + + return term_from_int(ret); +} + +static term nif_i2c_read_timeout_us(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct I2CResource *rsrc_obj; + if (UNLIKELY(!get_i2c_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + VALIDATE_VALUE(argv[3], term_is_atom); + VALIDATE_VALUE(argv[4], term_is_integer); + + uint8_t addr = (uint8_t) term_to_int(argv[1]); + avm_int_t count = term_to_int(argv[2]); + bool nostop = term_to_nostop(argv[3]); + uint timeout_us = (uint) term_to_int(argv[4]); + + if (UNLIKELY(count < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + // Allocate {ok, Data} tuple and binary + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(count), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(count, &ctx->heap, ctx->global); + uint8_t *buf = (uint8_t *) term_binary_data(data); + + int ret = i2c_read_timeout_us(rsrc_obj->i2c_inst, addr, buf, (size_t) count, nostop, timeout_us); + if (UNLIKELY(ret < 0)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return pico_err_to_error_tuple(ctx, ret); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static void i2c_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct I2CResource *rsrc_obj = (struct I2CResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->i2c_inst)) { + i2c_deinit(rsrc_obj->i2c_inst); + rsrc_obj->i2c_inst = NULL; + } +} + +static const ErlNifResourceTypeInit I2CResourceTypeInit = { + .members = 1, + .dtor = i2c_resource_dtor, +}; + +// +// NIF structs +// +static const struct Nif i2c_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_init +}; +static const struct Nif i2c_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_deinit +}; +static const struct Nif i2c_set_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_set_baudrate +}; +static const struct Nif i2c_write_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_write_blocking +}; +static const struct Nif i2c_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_read_blocking +}; +static const struct Nif i2c_write_timeout_us_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_write_timeout_us +}; +static const struct Nif i2c_read_timeout_us_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_read_timeout_us +}; + +static void i2c_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + i2c_resource_type = enif_init_resource_type(&env, "i2c_resource", &I2CResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *i2c_nif_get_nif(const char *nifname) +{ + if (strncmp("i2c:", nifname, 4) != 0) { + return NULL; + } + const char *rest = nifname + 4; + if (strcmp("init/2", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_deinit_nif; + } + if (strcmp("set_baudrate/2", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_set_baudrate_nif; + } + if (strcmp("write_blocking/4", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_write_blocking_nif; + } + if (strcmp("read_blocking/4", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_read_blocking_nif; + } + if (strcmp("write_timeout_us/5", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_write_timeout_us_nif; + } + if (strcmp("read_timeout_us/5", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_read_timeout_us_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(i2c, i2c_nif_init, NULL, i2c_nif_get_nif)