From be1ad2069ec2775dd26300e52a95a1119bbbe896 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Tue, 17 Feb 2026 17:35:26 +0100 Subject: [PATCH 1/5] 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 931b9d9563..102b7db91e 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 c9ead577396281edcffd40f6466b2e96e198e7a9 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Wed, 18 Feb 2026 13:38:40 +0100 Subject: [PATCH 2/5] 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) From 87231865141d1748ddeec68ff20d62db77589004 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Fri, 20 Feb 2026 22:12:01 +0100 Subject: [PATCH 3/5] Add SPI support to RP2 platform Also add a sample code, `spi_flash.erl` confirmed to work on both Pico-W and ESP32C2, using the same high level SPI API. Signed-off-by: Paul Guyot --- CMakeModules/BuildErlang.cmake | 52 ++- examples/erlang/CMakeLists.txt | 1 + examples/erlang/rp2/CMakeLists.txt | 11 + examples/erlang/spi_flash.erl | 135 +++++++ libs/avm_rp2/src/CMakeLists.txt | 1 + libs/avm_rp2/src/spi.erl | 493 +++++++++++++++++++++++ src/platforms/rp2/src/lib/CMakeLists.txt | 4 +- src/platforms/rp2/src/lib/spidriver.c | 328 +++++++++++++++ 8 files changed, 1011 insertions(+), 14 deletions(-) create mode 100644 examples/erlang/spi_flash.erl create mode 100644 libs/avm_rp2/src/spi.erl create mode 100644 src/platforms/rp2/src/lib/spidriver.c diff --git a/CMakeModules/BuildErlang.cmake b/CMakeModules/BuildErlang.cmake index 102b7db91e..d6969d7cdb 100644 --- a/CMakeModules/BuildErlang.cmake +++ b/CMakeModules/BuildErlang.cmake @@ -267,6 +267,9 @@ endmacro() macro(pack_runnable avm_name main) + set(multiValueArgs DIALYZE_AGAINST) + cmake_parse_arguments(PACK_RUNNABLE "" "" "${multiValueArgs}" ${ARGN}) + add_custom_command( OUTPUT ${main}.beam COMMAND erlc +debug_info -I ${CMAKE_SOURCE_DIR}/libs/include ${CMAKE_CURRENT_SOURCE_DIR}/${main}.erl @@ -281,9 +284,9 @@ macro(pack_runnable avm_name main) ) # Select the right PLT based on platform-specific dependencies - set(pack_runnable_${avm_name}_plt_name "atomvmlib") + set(pack_runnable_${avm_name}_plt_names "") - foreach(archive_name ${ARGN}) + foreach(archive_name ${PACK_RUNNABLE_UNPARSED_ARGUMENTS}) 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|avm_network|avm_esp32|avm_rp2|avm_stm32|avm_emscripten$") @@ -295,24 +298,47 @@ macro(pack_runnable avm_name main) 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") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-esp32") elseif(${archive_name} STREQUAL "avm_rp2") - set(pack_runnable_${avm_name}_plt_name "atomvmlib-rp2") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-rp2") elseif(${archive_name} STREQUAL "avm_stm32") - set(pack_runnable_${avm_name}_plt_name "atomvmlib-stm32") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-stm32") elseif(${archive_name} STREQUAL "avm_emscripten") - set(pack_runnable_${avm_name}_plt_name "atomvmlib-emscripten") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-emscripten") endif() endforeach() + # DIALYZE_AGAINST overrides auto-detected PLTs + if(PACK_RUNNABLE_DIALYZE_AGAINST) + set(pack_runnable_${avm_name}_plt_names "") + foreach(plt_lib IN LISTS PACK_RUNNABLE_DIALYZE_AGAINST) + if(${plt_lib} STREQUAL "avm_esp32") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-esp32") + elseif(${plt_lib} STREQUAL "avm_rp2") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-rp2") + elseif(${plt_lib} STREQUAL "avm_stm32") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-stm32") + elseif(${plt_lib} STREQUAL "avm_emscripten") + list(APPEND pack_runnable_${avm_name}_plt_names "atomvmlib-emscripten") + endif() + endforeach() + endif() + + # Default to base PLT if no platform was detected + if(NOT pack_runnable_${avm_name}_plt_names) + set(pack_runnable_${avm_name}_plt_names "atomvmlib") + endif() + if (Dialyzer_FOUND) - add_custom_target( - ${avm_name}_dialyzer - DEPENDS ${avm_name}_main - 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 ${pack_runnable_${avm_name}_plt_name}_plt ${pack_runnable_${avm_name}_archive_targets}) - add_dependencies(dialyzer ${avm_name}_dialyzer) + foreach(plt_name IN LISTS pack_runnable_${avm_name}_plt_names) + add_custom_target( + ${avm_name}_${plt_name}_dialyzer + DEPENDS ${avm_name}_main + COMMAND dialyzer --plt ${CMAKE_BINARY_DIR}/libs/${plt_name}.plt -c ${main}.beam ${${avm_name}_dialyzer_beams_opt} + ) + add_dependencies(${avm_name}_${plt_name}_dialyzer ${plt_name}_plt ${pack_runnable_${avm_name}_archive_targets}) + add_dependencies(dialyzer ${avm_name}_${plt_name}_dialyzer) + endforeach() endif() if(AVM_RELEASE) diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index 5e9a8febc1..01974dcc5a 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -42,3 +42,4 @@ 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 avm_network) pack_runnable(disterl disterl estdlib) +pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2) diff --git a/examples/erlang/rp2/CMakeLists.txt b/examples/erlang/rp2/CMakeLists.txt index b1f59c0057..418fda38c9 100644 --- a/examples/erlang/rp2/CMakeLists.txt +++ b/examples/erlang/rp2/CMakeLists.txt @@ -27,6 +27,17 @@ 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) + +set(SPI_FLASH_AVM ${CMAKE_BINARY_DIR}/examples/erlang/spi_flash.avm) +add_custom_command( + OUTPUT spi_flash.uf2 + DEPENDS ${SPI_FLASH_AVM} UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o spi_flash.uf2 -f universal -s 0x10180000 ${SPI_FLASH_AVM} + COMMENT "Creating UF2 file spi_flash.uf2" + VERBATIM +) +add_custom_target(spi_flash_uf2 ALL DEPENDS spi_flash.uf2) +add_dependencies(spi_flash_uf2 spi_flash) pack_uf2(picow_blink picow_blink) pack_uf2(picow_wifi_sta picow_wifi_sta) pack_uf2(picow_wifi_ap picow_wifi_ap) diff --git a/examples/erlang/spi_flash.erl b/examples/erlang/spi_flash.erl new file mode 100644 index 0000000000..43700f3445 --- /dev/null +++ b/examples/erlang/spi_flash.erl @@ -0,0 +1,135 @@ +% +% 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 flash JEDEC ID reader example. +%% +%% Reads the JEDEC ID and Status Register 1 from a standard SPI flash chip +%% (W25Qxx or similar) and prints the results every 5 seconds. +%% +%% Default pins are auto-detected from the platform and chip model: +%% +%% Pico (SPI0): SCK=GP2, MOSI=GP3, MISO=GP4, CS=GP5 +%% ESP32/S2/S3 (SPI2): SCK=18, MOSI=23, MISO=19, CS=5 +%% ESP32-C2/C3/C5 (SPI2): SCK=6, MOSI=7, MISO=2, CS=10 +%% ESP32-C6/C61 (SPI2): SCK=6, MOSI=7, MISO=2, CS=16 +%% +%% Note: some breakout boards label pins using QSPI convention where +%% D0 is MISO and D1 is MOSI (opposite of DI/DO). If you read all +%% zeros, try swapping D0 and D1. +%% +%% The flash command byte is sent as the "address" parameter of read_at/4, +%% using address_len_bits => 8 and command_len_bits => 0. +%% @end +%%----------------------------------------------------------------------------- +-module(spi_flash). +-export([start/0]). + +%% SPI flash commands +-define(CMD_JEDEC_ID, 16#9F). +-define(CMD_READ_STATUS1, 16#05). + +start() -> + {SCK, MOSI, MISO, CS} = default_pins(), + SPI = spi:open([ + {bus_config, [ + {sclk, SCK}, + {mosi, MOSI}, + {miso, MISO} + ]}, + {device_config, [ + {flash, [ + {cs, CS}, + {clock_speed_hz, 1000000}, + {mode, 0}, + {address_len_bits, 8}, + {command_len_bits, 0} + ]} + ]} + ]), + loop(SPI). + +loop(SPI) -> + read_jedec_id(SPI), + read_status(SPI), + timer:sleep(5000), + loop(SPI). + +read_jedec_id(SPI) -> + case spi:read_at(SPI, flash, ?CMD_JEDEC_ID, 24) of + {ok, Value} -> + Manufacturer = (Value bsr 16) band 16#FF, + MemType = (Value bsr 8) band 16#FF, + Capacity = Value band 16#FF, + io:format( + "JEDEC ID: manufacturer=0x~2.16.0B mem_type=0x~2.16.0B capacity=0x~2.16.0B~n", [ + Manufacturer, MemType, Capacity + ] + ), + case manufacturer_name(Manufacturer) of + unknown -> ok; + Name -> io:format(" Manufacturer: ~s~n", [Name]) + end; + {error, Reason} -> + io:format("JEDEC ID read error: ~p~n", [Reason]) + end. + +read_status(SPI) -> + case spi:read_at(SPI, flash, ?CMD_READ_STATUS1, 8) of + {ok, Status} -> + Busy = Status band 1, + Wel = (Status bsr 1) band 1, + io:format("Status Register 1: 0x~2.16.0B (BUSY=~B WEL=~B)~n", [ + Status, Busy, Wel + ]); + {error, Reason} -> + io:format("Status read error: ~p~n", [Reason]) + end. + +default_pins() -> + default_pins(atomvm:platform()). + +%% {SCK, MOSI, MISO, CS} +default_pins(pico) -> {2, 3, 4, 5}; +default_pins(esp32) -> esp32_default_pins(). + +esp32_default_pins() -> + #{model := Model} = erlang:system_info(esp32_chip_info), + esp32_default_pins(Model). + +%% {SCK, MOSI, MISO, CS} +esp32_default_pins(esp32) -> {18, 23, 19, 5}; +esp32_default_pins(esp32_s2) -> {18, 23, 19, 5}; +esp32_default_pins(esp32_s3) -> {18, 23, 19, 5}; +esp32_default_pins(esp32_c2) -> {6, 7, 2, 10}; +esp32_default_pins(esp32_c3) -> {6, 7, 2, 10}; +esp32_default_pins(esp32_c5) -> {6, 7, 2, 10}; +esp32_default_pins(esp32_c6) -> {6, 7, 2, 16}; +esp32_default_pins(esp32_c61) -> {6, 7, 2, 16}; +esp32_default_pins(_) -> {18, 23, 19, 5}. + +manufacturer_name(16#EF) -> "Winbond"; +manufacturer_name(16#C8) -> "GigaDevice"; +manufacturer_name(16#20) -> "Micron"; +manufacturer_name(16#01) -> "Spansion/Cypress"; +manufacturer_name(16#1F) -> "Adesto/Atmel"; +manufacturer_name(16#BF) -> "Microchip/SST"; +manufacturer_name(16#9D) -> "ISSI"; +manufacturer_name(_) -> unknown. diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt index ec5dff9098..c76d8b1675 100644 --- a/libs/avm_rp2/src/CMakeLists.txt +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -26,6 +26,7 @@ set(ERLANG_MODULES gpio i2c pico + spi ) pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_rp2/src/spi.erl b/libs/avm_rp2/src/spi.erl new file mode 100644 index 0000000000..fa260a0182 --- /dev/null +++ b/libs/avm_rp2/src/spi.erl @@ -0,0 +1,493 @@ +% +% 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 SPI interface for RP2 (Pico) +%% +%% This module provides an interface to the SPI hardware on RP2 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API +%% {@link init/2}, {@link deinit/1}, {@link set_baudrate/2}, +%% {@link set_format/4}, {@link write_blocking/2}, {@link read_blocking/3}, +%% {@link write_read_blocking/2}. +%% These operate on a bare resource reference returned by {@link init/2}. +%% Pin muxing must be done separately via `gpio:set_function/2'. +%% +%% High-level API (`spi_hal' behavior) +%% {@link open/1}, {@link close/1}, {@link read_at/4}, {@link write_at/5}, +%% {@link write/3}, {@link write_read/3}. +%% {@link open/1} handles pin setup and CS management automatically. +%% @end +%%----------------------------------------------------------------------------- +-module(spi). + +-behaviour(spi_hal). + +%% High-level API (spi_hal behaviour) +-export([ + open/1, + close/1, + read_at/4, + write_at/5, + write/3, + write_read/3 +]). + +%% Low-level API (Pico SDK) +-export([ + init/2, + deinit/1, + set_baudrate/2, + set_format/4, + write_blocking/2, + read_blocking/3, + write_read_blocking/2 +]). + +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 0 | 1. +-type spi_resource() :: reference(). +-type spi() :: pid(). +-type device_name() :: atom(). +-type address() :: non_neg_integer(). +-type transaction() :: #{ + command => integer(), + address => non_neg_integer(), + write_data => binary(), + write_bits => non_neg_integer(), + read_bits => non_neg_integer() +}. + +-export_type([spi/0, spi_resource/0, device_name/0, address/0, transaction/0]). + +-define(DEFAULT_CLOCK_SPEED_HZ, 1000000). +-define(DEFAULT_MODE, 0). +-define(DEFAULT_ADDRESS_LEN_BITS, 8). +-define(DEFAULT_COMMAND_LEN_BITS, 0). +-define(DEFAULT_PERIPHERAL, 0). + +%% --------------------------------------------------------------------------- +%% High-level API (spi_hal behaviour) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns SPI handle (pid) +%% @doc Open a connection to the SPI driver +%% +%% This function configures the GPIO pins for SPI function, +%% initializes the SPI peripheral, and sets up CS pins for +%% software chip-select management. +%% +%% Parameters use the same format as the ESP32 SPI driver: +%%
    +%%
  • `{bus_config, BusConfig}' - bus configuration (required)
  • +%%
  • `{device_config, DeviceConfigs}' - device configurations
  • +%%
+%% +%% Bus configuration: +%%
    +%%
  • `{sclk, Pin}' - the SCLK pin number (required)
  • +%%
  • `{mosi, Pin}' - the MOSI pin number
  • +%%
  • `{miso, Pin}' - the MISO pin number
  • +%%
  • `{peripheral, 0 | 1}' - the SPI peripheral to use (default: 0)
  • +%%
+%% +%% Device configuration (keyed by device name atom): +%%
    +%%
  • `{cs, Pin}' - the CS pin number
  • +%%
  • `{clock_speed_hz, Hz}' - clock speed (default: 1000000)
  • +%%
  • `{mode, 0..3}' - SPI mode (default: 0)
  • +%%
  • `{address_len_bits, Bits}' - address width (default: 8)
  • +%%
  • `{command_len_bits, Bits}' - command width (default: 0)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: [{atom(), term()}]) -> spi(). +open(Params) -> + BusConfig = get_value(bus_config, Params), + DeviceConfigList = get_value(device_config, Params, []), + SCLK = get_value(sclk, BusConfig), + MOSI = get_value(mosi, BusConfig, undefined), + MISO = get_value(miso, BusConfig, undefined), + Peripheral = get_value(peripheral, BusConfig, ?DEFAULT_PERIPHERAL), + gpio:set_function(SCLK, spi), + maybe_set_spi_function(MOSI), + maybe_set_spi_function(MISO), + {ok, {_ActualBaud, Resource}} = ?MODULE:init(Peripheral, ?DEFAULT_CLOCK_SPEED_HZ), + Devices = setup_devices(DeviceConfigList), + spawn_link(fun() -> loop(Resource, Devices) end). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @returns `ok' +%% @doc Close the SPI driver and free resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(SPI :: spi()) -> ok. +close(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Address SPI address from which to read +%% @param Len number of bits to read +%% @returns `{ok, Value}' or `{error, Reason}' +%% @doc Read a value from an address on the device. +%% +%% Sends the address (address_len_bits wide), then reads Len bits. +%% The read value is returned as a big-endian integer. +%% @end +%%----------------------------------------------------------------------------- +-spec read_at( + SPI :: spi(), DeviceName :: device_name(), Address :: address(), Len :: non_neg_integer() +) -> + {ok, integer()} | {error, term()}. +read_at(Pid, DeviceName, Address, Len) -> + call(Pid, {read_at, DeviceName, Address, Len}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Address SPI address to which to write +%% @param Len number of bits to transfer +%% @param Data value to write +%% @returns `{ok, Value}' or `{error, Reason}' +%% @doc Write a value to an address on the device. +%% +%% Sends the address with bit 7 set (write flag), followed by the +%% data. Returns the value read back during the data phase. +%% @end +%%----------------------------------------------------------------------------- +-spec write_at( + SPI :: spi(), + DeviceName :: device_name(), + Address :: address(), + Len :: non_neg_integer(), + Data :: integer() +) -> + {ok, integer()} | {error, term()}. +write_at(Pid, DeviceName, Address, Len, Data) -> + call(Pid, {write_at, DeviceName, Address bor 16#80, Len, Data}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Transaction transaction map +%% @returns `ok' or `{error, Reason}' +%% @doc Write data to the SPI device using a transaction. +%% +%% The transaction map may contain: `command', `address', +%% `write_data', and `write_bits'. +%% @end +%%----------------------------------------------------------------------------- +-spec write(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + ok | {error, term()}. +write(Pid, DeviceName, Transaction) -> + call(Pid, {write, DeviceName, Transaction}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Transaction transaction map +%% @returns `{ok, ReadData}' or `{error, Reason}' +%% @doc Write and simultaneously read from the SPI device. +%% +%% The transaction map may contain: `command', `address', +%% `write_data', `write_bits', and `read_bits'. +%% Returns the first `ceil(read_bits / 8)' bytes read during the +%% data phase. +%% @end +%%----------------------------------------------------------------------------- +-spec write_read(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + {ok, binary()} | {error, term()}. +write_read(Pid, DeviceName, Transaction) -> + call(Pid, {write_read, DeviceName, Transaction}). + +%% --------------------------------------------------------------------------- +%% Low-level API (Pico SDK) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Peripheral SPI peripheral number (0 or 1) +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, {ActualBaudrate, Resource}}' +%% @doc Initialize the SPI HW block. +%% +%% Pin muxing must be done separately via `gpio:set_function/2'. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Peripheral :: peripheral(), Baudrate :: freq_hz()) -> + {ok, {ActualBaudrate :: freq_hz(), Resource :: spi_resource()}}. +init(_Peripheral, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `ok' +%% @doc Disable the SPI HW block. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: spi_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, ActualBaudrate}' +%% @doc Set SPI baudrate. +%% @end +%%----------------------------------------------------------------------------- +-spec set_baudrate(Resource :: spi_resource(), Baudrate :: freq_hz()) -> + {ok, ActualBaudrate :: freq_hz()}. +set_baudrate(_Resource, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param DataBits Number of data bits per transfer (4..16) +%% @param CPOL Clock polarity (0 or 1) +%% @param CPHA Clock phase (0 or 1) +%% @returns `ok' +%% @doc Set SPI format. +%% +%% SPI mode mapping: mode 0 = CPOL 0, CPHA 0; mode 1 = CPOL 0, +%% CPHA 1; mode 2 = CPOL 1, CPHA 0; mode 3 = CPOL 1, CPHA 1. +%% Data order is always MSB first. +%% @end +%%----------------------------------------------------------------------------- +-spec set_format(Resource :: spi_resource(), DataBits :: 4..16, CPOL :: 0 | 1, CPHA :: 0 | 1) -> ok. +set_format(_Resource, _DataBits, _CPOL, _CPHA) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Data Binary data to write +%% @returns Number of bytes written +%% @doc Write to SPI, blocking. +%% @end +%%----------------------------------------------------------------------------- +-spec write_blocking(Resource :: spi_resource(), Data :: binary()) -> non_neg_integer(). +write_blocking(_Resource, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param RepeatedTxData Byte value to send on TX while reading +%% @param Count Number of bytes to read +%% @returns `{ok, Data}' +%% @doc Read from SPI, blocking. +%% +%% `RepeatedTxData' is repeatedly sent on TX while reading data. +%% @end +%%----------------------------------------------------------------------------- +-spec read_blocking( + Resource :: spi_resource(), RepeatedTxData :: byte(), Count :: non_neg_integer() +) -> + {ok, binary()}. +read_blocking(_Resource, _RepeatedTxData, _Count) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param WriteData Binary data to write +%% @returns `{ok, ReadData}' +%% @doc Simultaneous write and read from SPI, blocking. +%% +%% WriteData is sent on TX while the same number of bytes is read +%% into the returned binary. +%% @end +%%----------------------------------------------------------------------------- +-spec write_read_blocking(Resource :: spi_resource(), WriteData :: binary()) -> + {ok, ReadData :: binary()}. +write_read_blocking(_Resource, _WriteData) -> + erlang:nif_error(undefined). + +%% --------------------------------------------------------------------------- +%% Internal helpers +%% --------------------------------------------------------------------------- + +%% @private +call(Pid, Request) -> + Ref = make_ref(), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> Reply + end. + +%% @private +maybe_set_spi_function(undefined) -> ok; +maybe_set_spi_function(Pin) -> gpio:set_function(Pin, spi). + +%% @private +get_value(Key, Map, Default) when is_map(Map) -> + maps:get(Key, Map, Default); +get_value(Key, List, Default) when is_list(List) -> + proplists:get_value(Key, List, Default). + +%% @private +get_value(Key, MapOrList) -> + get_value(Key, MapOrList, undefined). + +%% @private +setup_devices(DeviceConfigMap) when is_map(DeviceConfigMap) -> + setup_devices(maps:to_list(DeviceConfigMap)); +setup_devices(DeviceConfigList) when is_list(DeviceConfigList) -> + lists:foldl(fun setup_device/2, #{}, DeviceConfigList). + +%% @private +setup_device({Name, Config}, Acc) -> + CS = get_value(cs, Config), + ClockSpeedHz = get_value(clock_speed_hz, Config, ?DEFAULT_CLOCK_SPEED_HZ), + Mode = get_value(mode, Config, ?DEFAULT_MODE), + AddressLenBits = get_value(address_len_bits, Config, ?DEFAULT_ADDRESS_LEN_BITS), + CommandLenBits = get_value(command_len_bits, Config, ?DEFAULT_COMMAND_LEN_BITS), + case CS of + undefined -> + ok; + _ -> + gpio:set_function(CS, sio), + gpio:set_pin_mode(CS, output), + gpio:digital_write(CS, 1) + end, + DeviceInfo = #{ + cs => CS, + clock_speed_hz => ClockSpeedHz, + mode => Mode, + address_len_bits => AddressLenBits, + command_len_bits => CommandLenBits + }, + Acc#{Name => DeviceInfo}. + +%% @private +loop(Resource, Devices) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Devices, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource, Devices) + end + end. + +%% @private +handle_request(Resource, _Devices, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(Resource, Devices, {read_at, DeviceName, Address, LenBits}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + ?MODULE:write_blocking(Resource, <>), + LenBytes = (LenBits + 7) div 8, + {ok, Data} = ?MODULE:read_blocking(Resource, 0, LenBytes), + deselect_device(DeviceInfo), + <> = Data, + {reply, {ok, Value}}; +handle_request(Resource, Devices, {write_at, DeviceName, Address, LenBits, Data}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + TxBin = <>, + {ok, RxBin} = ?MODULE:write_read_blocking(Resource, TxBin), + deselect_device(DeviceInfo), + <<_:AddressLenBits, Value:LenBits/big-unsigned>> = RxBin, + {reply, {ok, Value}}; +handle_request(Resource, Devices, {write, DeviceName, Transaction}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + PrefixBin = build_tx_prefix(DeviceInfo, Transaction), + WriteData = maps:get(write_data, Transaction, <<>>), + WriteBits = maps:get(write_bits, Transaction, byte_size(WriteData) * 8), + WriteBytes = (WriteBits + 7) div 8, + TxData = binary:part(WriteData, 0, min(WriteBytes, byte_size(WriteData))), + ?MODULE:write_blocking(Resource, <>), + deselect_device(DeviceInfo), + {reply, ok}; +handle_request(Resource, Devices, {write_read, DeviceName, Transaction}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + PrefixBin = build_tx_prefix(DeviceInfo, Transaction), + WriteData = maps:get(write_data, Transaction, <<>>), + WriteBits = maps:get(write_bits, Transaction, byte_size(WriteData) * 8), + WriteBytes = (WriteBits + 7) div 8, + ReadBits = maps:get(read_bits, Transaction, WriteBits), + ReadBytes = (ReadBits + 7) div 8, + case byte_size(PrefixBin) of + 0 -> ok; + _ -> ?MODULE:write_blocking(Resource, PrefixBin) + end, + DataLen = max(WriteBytes, ReadBytes), + TxData = pad_binary(binary:part(WriteData, 0, min(WriteBytes, byte_size(WriteData))), DataLen), + {ok, RxBin} = ?MODULE:write_read_blocking(Resource, TxData), + deselect_device(DeviceInfo), + ReadData = binary:part(RxBin, 0, ReadBytes), + {reply, {ok, ReadData}}. + +%% @private +select_device(Resource, DeviceInfo) -> + ClockSpeedHz = maps:get(clock_speed_hz, DeviceInfo), + Mode = maps:get(mode, DeviceInfo), + {CPOL, CPHA} = mode_to_cpol_cpha(Mode), + ?MODULE:set_baudrate(Resource, ClockSpeedHz), + ?MODULE:set_format(Resource, 8, CPOL, CPHA), + case maps:get(cs, DeviceInfo) of + undefined -> ok; + CS -> gpio:digital_write(CS, 0) + end. + +%% @private +deselect_device(DeviceInfo) -> + case maps:get(cs, DeviceInfo) of + undefined -> ok; + CS -> gpio:digital_write(CS, 1) + end. + +%% @private +mode_to_cpol_cpha(0) -> {0, 0}; +mode_to_cpol_cpha(1) -> {0, 1}; +mode_to_cpol_cpha(2) -> {1, 0}; +mode_to_cpol_cpha(3) -> {1, 1}. + +%% @private +build_tx_prefix(DeviceInfo, Transaction) -> + CommandLenBits = maps:get(command_len_bits, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + Command = maps:get(command, Transaction, 0), + Address = maps:get(address, Transaction, 0), + case {CommandLenBits, AddressLenBits} of + {0, 0} -> <<>>; + {0, _} -> <>; + {_, 0} -> <>; + {_, _} -> <> + end. + +%% @private +pad_binary(Bin, Len) when byte_size(Bin) >= Len -> + Bin; +pad_binary(Bin, Len) -> + PadLen = Len - byte_size(Bin), + <>. diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index a4c383fc24..fd0eed8d90 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -32,6 +32,7 @@ set(HEADER_FILES set(SOURCE_FILES gpiodriver.c i2cdriver.c + spidriver.c networkdriver.c otp_crypto_platform.c platform_defaultatoms.c @@ -59,6 +60,7 @@ target_link_libraries( PUBLIC hardware_gpio hardware_i2c + hardware_spi hardware_sync pico_float pico_mbedtls @@ -123,4 +125,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,i2c_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,spi_nif -Wl,-u -Wl,otp_crypto_nif") diff --git a/src/platforms/rp2/src/lib/spidriver.c b/src/platforms/rp2/src/lib/spidriver.c new file mode 100644 index 0000000000..7b8b2fae2f --- /dev/null +++ b/src/platforms/rp2/src/lib/spidriver.c @@ -0,0 +1,328 @@ +/* + * 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_SPI_INSTANCES 2 + +static ErlNifResourceType *spi_resource_type; + +struct SPIResource +{ + spi_inst_t *spi_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 bool get_spi_resource(Context *ctx, term resource_term, struct SPIResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, spi_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct SPIResource *) rsrc_obj_ptr; + return true; +} + +static term nif_spi_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_SPI_INSTANCES)) { + RAISE_ERROR(BADARG_ATOM); + } + + uint baudrate = (uint) term_to_int(argv[1]); + spi_inst_t *inst = (peripheral == 0) ? spi0 : spi1; + + uint actual_baudrate = spi_init(inst, baudrate); + + struct SPIResource *rsrc_obj = enif_alloc_resource(spi_resource_type, sizeof(struct SPIResource)); + if (IS_NULL_PTR(rsrc_obj)) { + spi_deinit(inst); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + rsrc_obj->spi_inst = inst; + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + spi_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); + + 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_spi_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + spi_deinit(rsrc_obj->spi_inst); + rsrc_obj->spi_inst = NULL; + + return OK_ATOM; +} + +static term nif_spi_set_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_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 = spi_set_baudrate(rsrc_obj->spi_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_spi_set_format(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_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_integer); + + uint data_bits = (uint) term_to_int(argv[1]); + spi_cpol_t cpol = (spi_cpol_t) term_to_int(argv[2]); + spi_cpha_t cpha = (spi_cpha_t) term_to_int(argv[3]); + + spi_set_format(rsrc_obj->spi_inst, data_bits, cpol, cpha, SPI_MSB_FIRST); + + return OK_ATOM; +} + +static term nif_spi_write_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *data = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + + int ret = spi_write_blocking(rsrc_obj->spi_inst, data, len); + + return term_from_int(ret); +} + +static term nif_spi_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + + uint8_t repeated_tx_data = (uint8_t) term_to_int(argv[1]); + avm_int_t count = term_to_int(argv[2]); + + if (UNLIKELY(count < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + 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); + + spi_read_blocking(rsrc_obj->spi_inst, repeated_tx_data, buf, (size_t) count); + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_spi_write_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *src = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(len), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(len, &ctx->heap, ctx->global); + uint8_t *dst = (uint8_t *) term_binary_data(data); + + spi_write_read_blocking(rsrc_obj->spi_inst, src, dst, len); + + return create_pair(ctx, OK_ATOM, data); +} + +static void spi_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct SPIResource *rsrc_obj = (struct SPIResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->spi_inst)) { + spi_deinit(rsrc_obj->spi_inst); + rsrc_obj->spi_inst = NULL; + } +} + +static const ErlNifResourceTypeInit SPIResourceTypeInit = { + .members = 1, + .dtor = spi_resource_dtor, +}; + +// +// NIF structs +// +static const struct Nif spi_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_init +}; +static const struct Nif spi_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_deinit +}; +static const struct Nif spi_set_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_baudrate +}; +static const struct Nif spi_set_format_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_format +}; +static const struct Nif spi_write_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_write_blocking +}; +static const struct Nif spi_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_read_blocking +}; +static const struct Nif spi_write_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_write_read_blocking +}; + +static void spi_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + spi_resource_type = enif_init_resource_type(&env, "spi_resource", &SPIResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *spi_nif_get_nif(const char *nifname) +{ + if (strncmp("spi:", nifname, 4) != 0) { + return NULL; + } + const char *rest = nifname + 4; + if (strcmp("init/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_deinit_nif; + } + if (strcmp("set_baudrate/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_baudrate_nif; + } + if (strcmp("set_format/4", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_format_nif; + } + if (strcmp("write_blocking/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_write_blocking_nif; + } + if (strcmp("read_blocking/3", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_read_blocking_nif; + } + if (strcmp("write_read_blocking/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_write_read_blocking_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(spi, spi_nif_init, NULL, spi_nif_get_nif) From f23223146da962de3ca49fe5ab97fac94ef7f24f Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 21 Feb 2026 22:13:58 +0100 Subject: [PATCH 4/5] Add missing declaration for binary:match/2,3 Signed-off-by: Paul Guyot --- libs/estdlib/src/binary.erl | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/libs/estdlib/src/binary.erl b/libs/estdlib/src/binary.erl index 1d7d4d10f4..8c3902e229 100644 --- a/libs/estdlib/src/binary.erl +++ b/libs/estdlib/src/binary.erl @@ -32,6 +32,7 @@ encode_hex/1, encode_hex/2, part/3, split/2, split/3, + match/2, match/3, replace/3, replace/4 ]). @@ -103,6 +104,33 @@ encode_hex(Data, uppercase) -> encode_hex(Data, lowercase) -> <<<<(hd(string:to_lower(integer_to_list(B, 16)))):8>> || <> <= Data>>. +%%----------------------------------------------------------------------------- +%% @equiv match(Binary, Pattern, []) +%% @param Binary binary to search in +%% @param Pattern pattern to search for +%% @returns `{Start, Length}' or `nomatch' +%% @doc Find the first occurrence of Pattern in Binary. +%% @end +%%----------------------------------------------------------------------------- +-spec match(Binary :: binary(), Pattern :: binary() | [binary()]) -> + {non_neg_integer(), non_neg_integer()} | nomatch. +match(_Binary, _Pattern) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Binary binary to search in +%% @param Pattern pattern to search for +%% @param Options options for the match +%% @returns `{Start, Length}' or `nomatch' +%% @doc Find the first occurrence of Pattern in Binary. +%% Options can include `{scope, {Start, Length}}'. +%% @end +%%----------------------------------------------------------------------------- +-spec match(Binary :: binary(), Pattern :: binary() | [binary()], Options :: [term()]) -> + {non_neg_integer(), non_neg_integer()} | nomatch. +match(_Binary, _Pattern, _Options) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Binary binary to extract a subbinary from %% @param Pos 0-based index of the subbinary to extract From d1d00e04c2c59fab6bcae09c9b0f16cb3842a74c Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Fri, 20 Feb 2026 23:25:47 +0100 Subject: [PATCH 5/5] Add UART support to RP2 platform Also add a sample code, `sim800l.erl` confirmed to work on both Pico-W and ESP32C2, using the same high level UART API. Signed-off-by: Paul Guyot --- examples/erlang/CMakeLists.txt | 1 + examples/erlang/rp2/CMakeLists.txt | 11 + examples/erlang/sim800l.erl | 180 ++++++++++ libs/avm_rp2/src/CMakeLists.txt | 1 + libs/avm_rp2/src/uart.erl | 433 +++++++++++++++++++++++ src/platforms/rp2/src/lib/CMakeLists.txt | 4 +- src/platforms/rp2/src/lib/uartdriver.c | 375 ++++++++++++++++++++ 7 files changed, 1004 insertions(+), 1 deletion(-) create mode 100644 examples/erlang/sim800l.erl create mode 100644 libs/avm_rp2/src/uart.erl create mode 100644 src/platforms/rp2/src/lib/uartdriver.c diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index 01974dcc5a..6e7f5c449b 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -43,3 +43,4 @@ pack_runnable(logging_example logging_example estdlib eavmlib) pack_runnable(http_client http_client estdlib eavmlib avm_network) pack_runnable(disterl disterl estdlib) pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2) +pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2) diff --git a/examples/erlang/rp2/CMakeLists.txt b/examples/erlang/rp2/CMakeLists.txt index 418fda38c9..f27ffc9b19 100644 --- a/examples/erlang/rp2/CMakeLists.txt +++ b/examples/erlang/rp2/CMakeLists.txt @@ -38,6 +38,17 @@ add_custom_command( ) add_custom_target(spi_flash_uf2 ALL DEPENDS spi_flash.uf2) add_dependencies(spi_flash_uf2 spi_flash) + +set(SIM800L_AVM ${CMAKE_BINARY_DIR}/examples/erlang/sim800l.avm) +add_custom_command( + OUTPUT sim800l.uf2 + DEPENDS ${SIM800L_AVM} UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o sim800l.uf2 -f universal -s 0x10180000 ${SIM800L_AVM} + COMMENT "Creating UF2 file sim800l.uf2" + VERBATIM +) +add_custom_target(sim800l_uf2 ALL DEPENDS sim800l.uf2) +add_dependencies(sim800l_uf2 sim800l) pack_uf2(picow_blink picow_blink) pack_uf2(picow_wifi_sta picow_wifi_sta) pack_uf2(picow_wifi_ap picow_wifi_ap) diff --git a/examples/erlang/sim800l.erl b/examples/erlang/sim800l.erl new file mode 100644 index 0000000000..ee802cd7fd --- /dev/null +++ b/examples/erlang/sim800l.erl @@ -0,0 +1,180 @@ +% +% 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 SIM800L AT command demo. +%% +%% Opens a UART connection to a SIM800L GSM module and verifies it responds +%% to basic AT commands. Prints firmware identification and signal quality +%% every 10 seconds. +%% +%% Be careful: SIM800L boards can draw up to 2A and shouldn't be powered by +%% the 3.3V of the usual Pico / ESP32 boards. It's ok for this demo but +%% do not put a SIM card in them to avoid damaging your board. +%% +%% The SIM800L communicates at 115200 baud (8N1) by default. +%% +%% Default pins are auto-detected from the platform and chip model: +%% +%% Pico (UART1): TX=GP4, RX=GP5 +%% ESP32/S2/S3 (UART1): TX=17, RX=16 +%% ESP32-C3/C5 (UART1): TX=4, RX=5 +%% ESP32-C6/C61 (UART1): TX=4, RX=5 +%% @end +%%----------------------------------------------------------------------------- +-module(sim800l). +-export([start/0]). + +-define(AT_TIMEOUT, 2000). + +start() -> + {TX, RX} = default_pins(), + io:format("Opening UART1 on TX=~B RX=~B~n", [TX, RX]), + UART = uart:open("UART1", [ + {tx, TX}, + {rx, RX}, + {speed, 115200} + ]), + %% SIM800L takes 3-5 seconds to boot after power-on + case wait_for_module(UART, 5) of + ok -> + io:format("SIM800L responding to AT commands~n"), + at_identify(UART), + loop(UART); + error -> + io:format("SIM800L not responding, giving up~n"), + uart:close(UART) + end. + +loop(UART) -> + at_signal_quality(UART), + timer:sleep(10000), + loop(UART). + +wait_for_module(_UART, 0) -> + error; +wait_for_module(UART, Retries) -> + drain(UART), + case at_command(UART, <<"AT">>) of + {ok, _} -> + ok; + {error, _} -> + timer:sleep(1000), + wait_for_module(UART, Retries - 1) + end. + +at_identify(UART) -> + case at_command(UART, <<"ATI">>) of + {ok, Response} -> + io:format("Module info: ~s~n", [Response]); + {error, Reason} -> + io:format("ATI failed: ~p~n", [Reason]) + end. + +at_signal_quality(UART) -> + case at_command(UART, <<"AT+CSQ">>) of + {ok, Response} -> + io:format("Signal quality: ~s~n", [Response]); + {error, Reason} -> + io:format("AT+CSQ failed: ~p~n", [Reason]) + end. + +%%----------------------------------------------------------------------------- +%% @private Send an AT command and collect the response until OK or ERROR. +%%----------------------------------------------------------------------------- +at_command(UART, Command) -> + uart:write(UART, [Command, <<"\r\n">>]), + collect_response(UART, []). + +collect_response(UART, Acc) -> + case uart:read(UART, ?AT_TIMEOUT) of + {ok, Data} -> + NewAcc = [Data | Acc], + Combined = erlang:iolist_to_binary(lists:reverse(NewAcc)), + case parse_response(Combined) of + {ok, Body} -> {ok, Body}; + error -> {error, Combined}; + incomplete -> collect_response(UART, NewAcc) + end; + {error, timeout} when Acc =/= [] -> + Combined = erlang:iolist_to_binary(lists:reverse(Acc)), + case parse_response(Combined) of + {ok, Body} -> {ok, Body}; + _ -> {error, {partial, Combined}} + end; + {error, timeout} -> + {error, timeout} + end. + +%% Look for OK or ERROR in the accumulated response +parse_response(Data) -> + case binary:match(Data, <<"\r\nOK\r\n">>) of + {_Pos, _Len} -> + Body = strip_status(Data), + {ok, Body}; + nomatch -> + case binary:match(Data, <<"\r\nERROR\r\n">>) of + {_Pos2, _Len2} -> error; + nomatch -> incomplete + end + end. + +%% Extract body between echo/first CRLF and final status line +strip_status(Data) -> + Trimmed = trim_leading_crlf(Data), + case binary:match(Trimmed, <<"\r\nOK\r\n">>) of + {Pos, _} -> binary:part(Trimmed, 0, Pos); + nomatch -> Trimmed + end. + +trim_leading_crlf(<<"\r\n", Rest/binary>>) -> trim_leading_crlf(Rest); +trim_leading_crlf(Data) -> Data. + +%% Discard any pending data in the UART buffer +drain(UART) -> + case uart:read(UART, 100) of + {ok, _} -> drain(UART); + {error, timeout} -> ok + end. + +%%----------------------------------------------------------------------------- +%% Platform-specific default pins +%%----------------------------------------------------------------------------- +default_pins() -> + default_pins(atomvm:platform()). + +%% {TX, RX} +default_pins(pico) -> {4, 5}; +default_pins(esp32) -> esp32_default_pins(). + +esp32_default_pins() -> + #{model := Model} = erlang:system_info(esp32_chip_info), + esp32_default_pins(Model). + +%% {TX, RX} +esp32_default_pins(esp32) -> {17, 16}; +esp32_default_pins(esp32_s2) -> {17, 16}; +esp32_default_pins(esp32_s3) -> {17, 16}; +esp32_default_pins(esp32_c2) -> {4, 5}; +esp32_default_pins(esp32_c3) -> {4, 5}; +esp32_default_pins(esp32_c5) -> {4, 5}; +esp32_default_pins(esp32_c6) -> {4, 5}; +esp32_default_pins(esp32_c61) -> {4, 5}; +esp32_default_pins(_) -> {17, 16}. diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt index c76d8b1675..dce842b829 100644 --- a/libs/avm_rp2/src/CMakeLists.txt +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -27,6 +27,7 @@ set(ERLANG_MODULES i2c pico spi + uart ) pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_rp2/src/uart.erl b/libs/avm_rp2/src/uart.erl new file mode 100644 index 0000000000..b6908a3d50 --- /dev/null +++ b/libs/avm_rp2/src/uart.erl @@ -0,0 +1,433 @@ +% +% 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 UART interface for RP2 (Pico) +%% +%% This module provides an interface to the UART hardware on RP2 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API +%% {@link init/2}, {@link deinit/1}, {@link set_baudrate/2}, +%% {@link set_format/4}, {@link set_hw_flow/3}, +%% {@link write_blocking/2}, {@link read_blocking/2}, +%% {@link is_readable/1}, {@link is_readable_within_us/2}. +%% These operate on a bare resource reference returned by {@link init/2}. +%% Pin muxing must be done separately via `gpio:set_function/2'. +%% +%% High-level API (`uart_hal' behavior) +%% {@link open/1}, {@link open/2}, {@link close/1}, +%% {@link read/1}, {@link read/2}, {@link write/2}. +%% {@link open/1} handles pin setup automatically. +%% @end +%%----------------------------------------------------------------------------- +-module(uart). + +-behaviour(uart_hal). + +%% High-level API (uart_hal behaviour) +-export([ + open/1, open/2, + close/1, + read/1, read/2, + write/2 +]). + +%% Low-level API (Pico SDK) +-export([ + init/2, + deinit/1, + set_baudrate/2, + set_format/4, + set_hw_flow/3, + write_blocking/2, + read_blocking/2, + is_readable/1, + is_readable_within_us/2 +]). + +-type pin() :: non_neg_integer(). +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 0 | 1. +-type param() :: + {tx, pin()} + | {rx, pin()} + | {rts, pin()} + | {cts, pin()} + | {speed, pos_integer()} + | {data_bits, 5..8} + | {stop_bits, 1 | 2} + | {parity, none | even | odd} + | {flow_control, none | hardware} + | {peripheral, peripheral() | string() | binary()}. +-type params() :: [param()]. +-type uart_resource() :: reference(). +-type uart() :: pid(). + +-export_type([uart/0, uart_resource/0]). + +-define(DEFAULT_SPEED, 115200). +-define(DEFAULT_DATA_BITS, 8). +-define(DEFAULT_STOP_BITS, 1). +-define(DEFAULT_PARITY, none). +-define(DEFAULT_FLOW_CONTROL, none). +-define(DEFAULT_PERIPHERAL, 0). + +%% --------------------------------------------------------------------------- +%% High-level API (uart_hal behaviour) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Name UART peripheral name (`"UART0"' or `"UART1"') +%% @param Params Initialization parameters +%% @returns UART handle (pid) +%% @doc Open a connection to the UART driver +%% +%% This function provides compatibility with the ESP32 UART driver +%% interface. The `Name' parameter is converted to a peripheral +%% number and prepended to the parameters. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: string() | binary(), Params :: params()) -> uart(). +open(Name, Params) -> + open([{peripheral, Name} | Params]). + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns UART handle (pid) +%% @doc Open a connection to the UART driver +%% +%% This function configures the GPIO pins for UART function, +%% initializes the UART peripheral, and configures data format +%% and flow control. +%% +%% Supported parameters: +%%
    +%%
  • `{tx, Pin}' - the TX pin number (required)
  • +%%
  • `{rx, Pin}' - the RX pin number (required)
  • +%%
  • `{rts, Pin}' - the RTS pin number (optional)
  • +%%
  • `{cts, Pin}' - the CTS pin number (optional)
  • +%%
  • `{speed, Baud}' - the baud rate (default: 115200)
  • +%%
  • `{data_bits, 5..8}' - data bits per character (default: 8)
  • +%%
  • `{stop_bits, 1 | 2}' - stop bits (default: 1)
  • +%%
  • `{parity, none | even | odd}' - parity (default: none)
  • +%%
  • `{flow_control, none | hardware}' - flow control (default: none)
  • +%%
  • `{peripheral, 0 | 1 | "UART0" | "UART1"}' - UART peripheral (default: 0)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: params()) -> uart(). +open(Params) -> + TX = proplists:get_value(tx, Params), + RX = proplists:get_value(rx, Params), + RTS = proplists:get_value(rts, Params, undefined), + CTS = proplists:get_value(cts, Params, undefined), + Speed = proplists:get_value(speed, Params, ?DEFAULT_SPEED), + DataBits = proplists:get_value(data_bits, Params, ?DEFAULT_DATA_BITS), + StopBits = proplists:get_value(stop_bits, Params, ?DEFAULT_STOP_BITS), + Parity = proplists:get_value(parity, Params, ?DEFAULT_PARITY), + FlowControl = proplists:get_value(flow_control, Params, ?DEFAULT_FLOW_CONTROL), + PeripheralParam = proplists:get_value(peripheral, Params, ?DEFAULT_PERIPHERAL), + Peripheral = normalize_peripheral(PeripheralParam), + gpio:set_function(TX, uart), + gpio:set_function(RX, uart), + maybe_set_uart_function(RTS), + maybe_set_uart_function(CTS), + {ok, {_ActualBaudrate, Resource}} = ?MODULE:init(Peripheral, Speed), + ?MODULE:set_format(Resource, DataBits, StopBits, Parity), + case FlowControl of + hardware -> + ?MODULE:set_hw_flow(Resource, CTS =/= undefined, RTS =/= undefined); + none -> + ok + end, + spawn_link(fun() -> loop(Resource) end). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @returns `ok' +%% @doc Close the connection to the UART driver and free resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(UART :: uart()) -> ok | {error, Reason :: term()}. +close(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @returns `{ok, Data}' or `{error, timeout}' +%% @doc Read currently available data from the UART FIFO. +%% +%% Returns `{error, timeout}' immediately if no data is available. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart()) -> {ok, binary()} | {error, Reason :: term()}. +read(Pid) -> + call(Pid, read). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @param Timeout Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, timeout}' +%% @doc Read data from the UART with a timeout. +%% +%% Waits up to `Timeout' milliseconds for data to become available. +%% Once data arrives, reads all currently available bytes from the +%% FIFO. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart(), Timeout :: pos_integer()) -> + {ok, binary()} | {error, Reason :: term()}. +read(Pid, Timeout) -> + call(Pid, {read, Timeout}). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @param Data iodata to write +%% @returns `ok' +%% @doc Write data to the UART. +%% @end +%%----------------------------------------------------------------------------- +-spec write(UART :: uart(), Data :: iodata()) -> ok | {error, Reason :: term()}. +write(Pid, Data) -> + case is_iolist(Data) of + true -> call(Pid, {write, Data}); + false -> error(badarg) + end. + +%% --------------------------------------------------------------------------- +%% Low-level API (Pico SDK) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Peripheral UART peripheral number (0 or 1) +%% @param Baudrate Baudrate in Hz (e.g. 115200) +%% @returns `{ok, {ActualBaudrate, Resource}}' +%% @doc Initialize the UART HW block. +%% +%% Pin muxing must be done separately via `gpio:set_function/2'. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Peripheral :: peripheral(), Baudrate :: freq_hz()) -> + {ok, {ActualBaudrate :: freq_hz(), Resource :: uart_resource()}}. +init(_Peripheral, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `ok' +%% @doc Disable the UART HW block. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: uart_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, ActualBaudrate}' +%% @doc Set UART baudrate. +%% @end +%%----------------------------------------------------------------------------- +-spec set_baudrate(Resource :: uart_resource(), Baudrate :: freq_hz()) -> + {ok, ActualBaudrate :: freq_hz()}. +set_baudrate(_Resource, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param DataBits Number of data bits per character (5..8) +%% @param StopBits Number of stop bits (1 or 2) +%% @param Parity Parity setting (`none', `even', or `odd') +%% @returns `ok' +%% @doc Set UART data format. +%% @end +%%----------------------------------------------------------------------------- +-spec set_format( + Resource :: uart_resource(), DataBits :: 5..8, StopBits :: 1 | 2, Parity :: none | even | odd +) -> ok. +set_format(_Resource, _DataBits, _StopBits, _Parity) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param CTS Enable CTS flow control +%% @param RTS Enable RTS flow control +%% @returns `ok' +%% @doc Set UART hardware flow control. +%% +%% The corresponding CTS/RTS pins must have been set to UART +%% function via `gpio:set_function/2' before enabling. +%% @end +%%----------------------------------------------------------------------------- +-spec set_hw_flow(Resource :: uart_resource(), CTS :: boolean(), RTS :: boolean()) -> ok. +set_hw_flow(_Resource, _CTS, _RTS) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Data Binary data to write +%% @returns `ok' +%% @doc Write to UART, blocking until all data is sent. +%% @end +%%----------------------------------------------------------------------------- +-spec write_blocking(Resource :: uart_resource(), Data :: binary()) -> ok. +write_blocking(_Resource, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Count Number of bytes to read +%% @returns `{ok, Data}' +%% @doc Read from UART, blocking until `Count' bytes have been received. +%% @end +%%----------------------------------------------------------------------------- +-spec read_blocking(Resource :: uart_resource(), Count :: non_neg_integer()) -> + {ok, binary()}. +read_blocking(_Resource, _Count) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `true' if data is available, `false' otherwise +%% @doc Check if UART has data available to read. +%% @end +%%----------------------------------------------------------------------------- +-spec is_readable(Resource :: uart_resource()) -> boolean(). +is_readable(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Us Maximum wait time in microseconds +%% @returns `true' if data became available, `false' on timeout +%% @doc Wait for UART data with a microsecond timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec is_readable_within_us(Resource :: uart_resource(), Us :: non_neg_integer()) -> boolean(). +is_readable_within_us(_Resource, _Us) -> + 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) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource) + end + end. + +%% @private +handle_request(Resource, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(Resource, read) -> + case ?MODULE:is_readable(Resource) of + true -> + Data = read_available(Resource), + {reply, {ok, Data}}; + false -> + {reply, {error, timeout}} + end; +handle_request(Resource, {read, Timeout}) -> + Deadline = erlang:system_time(millisecond) + Timeout, + case poll_readable(Resource, Deadline) of + true -> + Data = read_available(Resource), + {reply, {ok, Data}}; + false -> + {reply, {error, timeout}} + end; +handle_request(Resource, {write, Data}) -> + Bin = erlang:iolist_to_binary(Data), + ?MODULE:write_blocking(Resource, Bin), + {reply, ok}. + +%% @private +read_available(Resource) -> + read_available(Resource, []). + +%% @private +read_available(Resource, Acc) -> + {ok, Byte} = ?MODULE:read_blocking(Resource, 1), + case ?MODULE:is_readable(Resource) of + true -> + read_available(Resource, [Byte | Acc]); + false -> + erlang:iolist_to_binary(lists:reverse([Byte | Acc])) + end. + +%% @private +poll_readable(Resource, Deadline) -> + case ?MODULE:is_readable_within_us(Resource, 1000) of + true -> + true; + false -> + case erlang:system_time(millisecond) >= Deadline of + true -> false; + false -> poll_readable(Resource, Deadline) + end + end. + +%% @private +normalize_peripheral(N) when is_integer(N) -> N; +normalize_peripheral("UART0") -> 0; +normalize_peripheral("UART1") -> 1; +normalize_peripheral(<<"UART0">>) -> 0; +normalize_peripheral(<<"UART1">>) -> 1. + +%% @private +maybe_set_uart_function(undefined) -> ok; +maybe_set_uart_function(Pin) -> gpio:set_function(Pin, uart). + +%% @private +is_iolist([]) -> + true; +is_iolist(B) when is_binary(B) -> + true; +is_iolist(I) when is_integer(I) andalso 0 =< I andalso I =< 255 -> + true; +is_iolist([H | T]) -> + case is_iolist(H) of + true -> + is_iolist(T); + false -> + false + end; +is_iolist(_) -> + false. diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index fd0eed8d90..fea39a025f 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -33,6 +33,7 @@ set(SOURCE_FILES gpiodriver.c i2cdriver.c spidriver.c + uartdriver.c networkdriver.c otp_crypto_platform.c platform_defaultatoms.c @@ -61,6 +62,7 @@ target_link_libraries( hardware_gpio hardware_i2c hardware_spi + hardware_uart hardware_sync pico_float pico_mbedtls @@ -125,4 +127,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,i2c_nif -Wl,-u -Wl,spi_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,spi_nif -Wl,-u -Wl,uart_nif -Wl,-u -Wl,otp_crypto_nif") diff --git a/src/platforms/rp2/src/lib/uartdriver.c b/src/platforms/rp2/src/lib/uartdriver.c new file mode 100644 index 0000000000..9638c1810c --- /dev/null +++ b/src/platforms/rp2/src/lib/uartdriver.c @@ -0,0 +1,375 @@ +/* + * 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_UART_INSTANCES 2 + +static ErlNifResourceType *uart_resource_type; + +struct UARTResource +{ + uart_inst_t *uart_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 bool get_uart_resource(Context *ctx, term resource_term, struct UARTResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, uart_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct UARTResource *) rsrc_obj_ptr; + return true; +} + +static term nif_uart_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_UART_INSTANCES)) { + RAISE_ERROR(BADARG_ATOM); + } + + uint baudrate = (uint) term_to_int(argv[1]); + uart_inst_t *inst = uart_get_instance((uint) peripheral); + + uint actual_baudrate = uart_init(inst, baudrate); + + struct UARTResource *rsrc_obj = enif_alloc_resource(uart_resource_type, sizeof(struct UARTResource)); + if (IS_NULL_PTR(rsrc_obj)) { + uart_deinit(inst); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + rsrc_obj->uart_inst = inst; + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + uart_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); + + 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_uart_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + uart_deinit(rsrc_obj->uart_inst); + rsrc_obj->uart_inst = NULL; + + return OK_ATOM; +} + +static term nif_uart_set_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_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 = uart_set_baudrate(rsrc_obj->uart_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_uart_set_format(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_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); + + uint data_bits = (uint) term_to_int(argv[1]); + uint stop_bits = (uint) term_to_int(argv[2]); + + term parity_term = argv[3]; + uart_parity_t parity; + if (parity_term == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "none"))) { + parity = UART_PARITY_NONE; + } else if (parity_term == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "even"))) { + parity = UART_PARITY_EVEN; + } else if (parity_term == globalcontext_make_atom(ctx->global, ATOM_STR("\x3", "odd"))) { + parity = UART_PARITY_ODD; + } else { + RAISE_ERROR(BADARG_ATOM); + } + + uart_set_format(rsrc_obj->uart_inst, data_bits, stop_bits, parity); + + return OK_ATOM; +} + +static term nif_uart_set_hw_flow(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_atom); + VALIDATE_VALUE(argv[2], term_is_atom); + + bool cts = (argv[1] == TRUE_ATOM); + bool rts = (argv[2] == TRUE_ATOM); + + uart_set_hw_flow(rsrc_obj->uart_inst, cts, rts); + + return OK_ATOM; +} + +static term nif_uart_write_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *data = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + + uart_write_blocking(rsrc_obj->uart_inst, data, len); + + return OK_ATOM; +} + +static term nif_uart_is_readable(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + bool readable = uart_is_readable(rsrc_obj->uart_inst); + return readable ? TRUE_ATOM : FALSE_ATOM; +} + +static term nif_uart_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + avm_int_t count = term_to_int(argv[1]); + if (UNLIKELY(count < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + 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); + + uart_read_blocking(rsrc_obj->uart_inst, buf, (size_t) count); + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_uart_is_readable_within_us(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + uint32_t us = (uint32_t) term_to_int(argv[1]); + bool readable = uart_is_readable_within_us(rsrc_obj->uart_inst, us); + return readable ? TRUE_ATOM : FALSE_ATOM; +} + +static void uart_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct UARTResource *rsrc_obj = (struct UARTResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->uart_inst)) { + uart_deinit(rsrc_obj->uart_inst); + rsrc_obj->uart_inst = NULL; + } +} + +static const ErlNifResourceTypeInit UARTResourceTypeInit = { + .members = 1, + .dtor = uart_resource_dtor, +}; + +// +// NIF structs +// +static const struct Nif uart_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_init +}; +static const struct Nif uart_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_deinit +}; +static const struct Nif uart_set_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_baudrate +}; +static const struct Nif uart_set_format_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_format +}; +static const struct Nif uart_set_hw_flow_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_hw_flow +}; +static const struct Nif uart_write_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_write_blocking +}; +static const struct Nif uart_is_readable_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_is_readable +}; +static const struct Nif uart_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_read_blocking +}; +static const struct Nif uart_is_readable_within_us_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_is_readable_within_us +}; + +static void uart_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + uart_resource_type = enif_init_resource_type(&env, "uart_resource", &UARTResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *uart_nif_get_nif(const char *nifname) +{ + if (strncmp("uart:", nifname, 5) != 0) { + return NULL; + } + const char *rest = nifname + 5; + if (strcmp("init/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_deinit_nif; + } + if (strcmp("set_baudrate/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_baudrate_nif; + } + if (strcmp("set_format/4", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_format_nif; + } + if (strcmp("set_hw_flow/3", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_hw_flow_nif; + } + if (strcmp("write_blocking/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_write_blocking_nif; + } + if (strcmp("is_readable/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_is_readable_nif; + } + if (strcmp("read_blocking/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_read_blocking_nif; + } + if (strcmp("is_readable_within_us/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_is_readable_within_us_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(uart, uart_nif_init, NULL, uart_nif_get_nif)