From f59f02574adaf4b636c1f915f4beca68f72017f1 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Tue, 17 Feb 2026 17:33:51 +0100 Subject: [PATCH 1/5] Use official STM32 SDK for stm32 builds instead of libopencm3 AtomVM now supports the following STM32 families: - stm32f2xx - stm32f4xx (was supported by libopencm3) - stm32f7xx (was supported by libopencm3) - stm32g0xx - stm32g4xx - stm32h5xx - stm32h7xx - stm32l4xx - stm32l5xx - stm32u3xx - stm32wbxx Signed-off-by: Paul Guyot --- .github/workflows/stm32-build.yaml | 176 ++++-- CHANGELOG.md | 1 + CMakeLists.txt | 1 + README.Md | 2 +- doc/src/build-instructions.md | 145 +++-- doc/src/getting-started-guide.md | 14 +- examples/erlang/CMakeLists.txt | 1 + examples/erlang/stm32/CMakeLists.txt | 31 ++ examples/erlang/stm32/blink_nucleo144.erl | 68 +++ examples/erlang/stm32/blink_nucleo64.erl | 41 ++ .../stm32/blink_weact_studio_blackpill.erl | 42 ++ .../erlang/stm32/blink_weact_studio_h562.erl | 42 ++ .../erlang/stm32/blink_weact_studio_h743.erl | 42 ++ .../erlang/stm32/blink_weact_studio_u585.erl | 42 ++ .../erlang/stm32/blink_weact_studio_wb55.erl | 42 ++ src/libAtomVM/posix_nifs.c | 3 - src/platforms/stm32/.gitignore | 1 - src/platforms/stm32/CMakeLists.txt | 82 ++- .../stm32/clock_configs/clock_config.h | 26 + .../stm32/clock_configs/clock_config_f2.c | 83 +++ .../stm32/clock_configs/clock_config_f4.c | 163 ++++++ .../stm32/clock_configs/clock_config_f7.c | 91 ++++ .../stm32/clock_configs/clock_config_g0.c | 76 +++ .../stm32/clock_configs/clock_config_g4.c | 88 +++ .../stm32/clock_configs/clock_config_h5.c | 104 ++++ .../stm32/clock_configs/clock_config_h7.c | 113 ++++ .../stm32/clock_configs/clock_config_l4.c | 104 ++++ .../stm32/clock_configs/clock_config_l5.c | 78 +++ .../stm32/clock_configs/clock_config_u3.c | 74 +++ .../stm32/clock_configs/clock_config_u5.c | 70 +++ .../stm32/clock_configs/clock_config_wb.c | 90 +++ src/platforms/stm32/cmake/arm-toolchain.cmake | 7 +- .../stm32/cmake/atomvm_dev_config.cmake | 27 +- src/platforms/stm32/cmake/compile-flags.cmake | 12 +- src/platforms/stm32/cmake/libopencm3.cmake | 194 ------- src/platforms/stm32/cmake/picolibc.cmake | 122 +++++ src/platforms/stm32/cmake/stm32_device.cmake | 127 +++++ src/platforms/stm32/cmake/stm32_hal_conf.h.in | 215 ++++++++ src/platforms/stm32/cmake/stm32_linker.ld.in | 146 +++++ src/platforms/stm32/cmake/stm32_sdk.cmake | 206 +++++++ src/platforms/stm32/src/CMakeLists.txt | 97 +++- src/platforms/stm32/src/lib/CMakeLists.txt | 16 +- src/platforms/stm32/src/lib/avm_devcfg.h | 96 ++-- src/platforms/stm32/src/lib/gpio_driver.c | 511 ++++++++++++------ .../stm32/src/lib/stm32_hal_platform.h | 93 ++++ src/platforms/stm32/src/lib/stm_sys.h | 36 +- src/platforms/stm32/src/lib/sys.c | 126 +++-- src/platforms/stm32/src/main.c | 279 ++++++---- .../stm32/tests/renode/stm32_boot_test.robot | 42 ++ .../stm32/tests/renode/stm32_gpio_test.robot | 46 ++ .../stm32/tests/renode/stm32g0b1.repl | 60 ++ .../stm32/tests/renode/stm32h743.repl | 125 +++++ .../stm32/tests/renode/stm32l562.repl | 109 ++++ .../tests/test_erl_sources/CMakeLists.txt | 24 + .../tests/test_erl_sources/test_boot.erl | 25 + .../tests/test_erl_sources/test_gpio.erl | 31 ++ .../stm32/tools/atomvm_stm32_config_query.erl | 450 ++++++++++++--- src/platforms/stm32/tools/device_config.hrl | 344 ++++++++++-- 58 files changed, 4680 insertions(+), 822 deletions(-) create mode 100644 examples/erlang/stm32/CMakeLists.txt create mode 100644 examples/erlang/stm32/blink_nucleo144.erl create mode 100644 examples/erlang/stm32/blink_nucleo64.erl create mode 100644 examples/erlang/stm32/blink_weact_studio_blackpill.erl create mode 100644 examples/erlang/stm32/blink_weact_studio_h562.erl create mode 100644 examples/erlang/stm32/blink_weact_studio_h743.erl create mode 100644 examples/erlang/stm32/blink_weact_studio_u585.erl create mode 100644 examples/erlang/stm32/blink_weact_studio_wb55.erl create mode 100644 src/platforms/stm32/clock_configs/clock_config.h create mode 100644 src/platforms/stm32/clock_configs/clock_config_f2.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_f4.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_f7.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_g0.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_g4.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_h5.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_h7.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_l4.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_l5.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_u3.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_u5.c create mode 100644 src/platforms/stm32/clock_configs/clock_config_wb.c delete mode 100644 src/platforms/stm32/cmake/libopencm3.cmake create mode 100644 src/platforms/stm32/cmake/picolibc.cmake create mode 100644 src/platforms/stm32/cmake/stm32_device.cmake create mode 100644 src/platforms/stm32/cmake/stm32_hal_conf.h.in create mode 100644 src/platforms/stm32/cmake/stm32_linker.ld.in create mode 100644 src/platforms/stm32/cmake/stm32_sdk.cmake create mode 100644 src/platforms/stm32/src/lib/stm32_hal_platform.h create mode 100644 src/platforms/stm32/tests/renode/stm32_boot_test.robot create mode 100644 src/platforms/stm32/tests/renode/stm32_gpio_test.robot create mode 100644 src/platforms/stm32/tests/renode/stm32g0b1.repl create mode 100644 src/platforms/stm32/tests/renode/stm32h743.repl create mode 100644 src/platforms/stm32/tests/renode/stm32l562.repl create mode 100644 src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt create mode 100644 src/platforms/stm32/tests/test_erl_sources/test_boot.erl create mode 100644 src/platforms/stm32/tests/test_erl_sources/test_gpio.erl mode change 100755 => 100644 src/platforms/stm32/tools/atomvm_stm32_config_query.erl diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index aed84aeb81..440c1743e1 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -14,6 +14,7 @@ on: - 'CMakeModules/**' - 'src/platforms/stm32/**' - 'src/libAtomVM/**' + - 'libs/**' pull_request: paths: - '.github/workflows/stm32-build.yaml' @@ -21,6 +22,7 @@ on: - 'CMakeModules/**' - 'src/platforms/stm32/**' - 'src/libAtomVM/**' + - 'libs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/main' && github.ref || github.run_id }} @@ -34,17 +36,74 @@ jobs: contents: read security-events: write - steps: - - uses: actions/cache@v4 - id: builddeps-cache - with: - path: | - /home/runner/libopencm3 - key: ${{ runner.os }}-build-deps + strategy: + fail-fast: false + matrix: + device: + - stm32f407vgt6 + - stm32f411ceu6 + - stm32f429zit6 + - stm32h743vit6 + - stm32h743zit6 + - stm32u585ait6q + - stm32wb55rg + - stm32h562rgt6 + - stm32f746zgt6 + - stm32g474ret6 + - stm32l476rgt6 + - stm32l562qei6 + - stm32f207zgt6 + - stm32u375rgt6 + - stm32g0b1ret6 + include: + - device: stm32f407vgt6 + max_size: 524288 + renode_platform: stm32f4.repl + avm_address: "0x08080000" + - device: stm32f411ceu6 + max_size: 393216 + renode_platform: stm32f4.repl + avm_address: "0x08060000" + - device: stm32f429zit6 + max_size: 524288 + - device: stm32h743vit6 + max_size: 524288 + renode_platform: stm32h743.repl + avm_address: "0x08080000" + - device: stm32h743zit6 + max_size: 524288 + - device: stm32u585ait6q + max_size: 524288 + - device: stm32wb55rg + max_size: 524288 + - device: stm32h562rgt6 + max_size: 524288 + - device: stm32f746zgt6 + max_size: 524288 + renode_platform: stm32f746.repl + avm_address: "0x08080000" + - device: stm32g474ret6 + max_size: 393216 + - device: stm32l476rgt6 + max_size: 524288 + - device: stm32l562qei6 + max_size: 393216 + renode_platform: stm32l562.repl + avm_address: "0x08060000" + - device: stm32f207zgt6 + max_size: 524288 + - device: stm32u375rgt6 + max_size: 524288 + - device: stm32g0b1ret6 + max_size: 393216 + renode_platform: stm32g0b1.repl + avm_address: "0x08060000" + steps: - uses: erlef/setup-beam@v1 with: - otp-version: "27" + otp-version: "28" + rebar3-version: "3.25.1" hexpm-mirrors: | https://builds.hex.pm https://repo.hex.pm @@ -54,42 +113,91 @@ jobs: run: sudo apt update - name: "Install deps" - run: sudo apt install -y cmake gperf gcc-arm-none-eabi + run: sudo apt install -y cmake ninja-build gperf python3-pip && pip3 install meson - - name: Checkout and build libopencm3 - if: ${{ steps.builddeps-cache.outputs.cache-hit != 'true' }} - working-directory: /home/runner + - name: "Install ARM GNU Toolchain" run: | - set -euo pipefail - cd /home/runner - test -d libopencm3 && rm -fr libopencm3 - git clone https://github.com/libopencm3/libopencm3.git -b v0.8.0 - cd libopencm3 - make + wget -q https://developer.arm.com/-/media/Files/downloads/gnu/15.2.rel1/binrel/arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-eabi.tar.xz + tar xJf arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-eabi.tar.xz -C /opt + echo "/opt/arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-eabi/bin" >> $GITHUB_PATH - name: Checkout repo uses: actions/checkout@v4 - - name: "Git config safe.directory for codeql" - run: git config --global --add safe.directory /__w/AtomVM/AtomVM - - - name: "Initialize CodeQL" - uses: github/codeql-action/init@v4 - with: - languages: 'cpp' - build-mode: manual - queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql - - - name: Build + - name: "Build for ${{ matrix.device }}" shell: bash working-directory: ./src/platforms/stm32/ run: | set -euo pipefail mkdir build cd build - # -DAVM_WARNINGS_ARE_ERRORS=ON - cmake .. -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DLIBOPENCM3_DIR=/home/runner/libopencm3 - make -j + cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DDEVICE=${{ matrix.device }} + cmake --build . + + - name: "Check firmware size for ${{ matrix.device }}" + shell: bash + working-directory: ./src/platforms/stm32/build + run: | + set -euo pipefail + ELF="AtomVM-${{ matrix.device }}.elf" + SIZES=$(arm-none-eabi-size "$ELF" | tail -1) + TEXT=$(echo "$SIZES" | awk '{print $1}') + DATA=$(echo "$SIZES" | awk '{print $2}') + FLASH_USED=$((TEXT + DATA)) + MAX=${{ matrix.max_size }} + echo "Firmware flash usage: ${FLASH_USED} bytes ($(( FLASH_USED / 1024 )) KB)" + echo "Flash limit: ${MAX} bytes ($(( MAX / 1024 )) KB)" + if [ "$FLASH_USED" -gt "$MAX" ]; then + echo "::error::Firmware too large: ${FLASH_USED} bytes exceeds ${MAX} byte limit for ${{ matrix.device }}" + exit 1 + fi + echo "OK: ${FLASH_USED} / ${MAX} bytes ($(( FLASH_USED * 100 / MAX ))% used)" + + - name: Build host AtomVM and test AVMs + if: matrix.renode_platform + run: | + set -euo pipefail + mkdir build-host + cd build-host + cmake .. -G Ninja + cmake --build . -t stm32_boot_test stm32_gpio_test + + - name: Install Renode + if: matrix.renode_platform + run: | + set -euo pipefail + mkdir -p renode-portable + wget -qO- https://builds.renode.io/renode-latest.linux-portable.tar.gz \ + | tar -xzf - -C renode-portable --strip-components=1 + echo "$PWD/renode-portable" >> $GITHUB_PATH + pip install -r renode-portable/tests/requirements.txt - - name: "Perform CodeQL Analysis" - uses: github/codeql-action/analyze@v4 + - name: Run Renode boot test + if: matrix.renode_platform + run: | + LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" + if [ -f "$LOCAL_REPL" ]; then + PLATFORM="@$PWD/$LOCAL_REPL" + else + PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" + fi + renode-test src/platforms/stm32/tests/renode/stm32_boot_test.robot \ + --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ + --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_boot_test.avm \ + --variable AVM_ADDRESS:${{ matrix.avm_address }} \ + --variable PLATFORM:$PLATFORM + + - name: Run Renode GPIO test + if: matrix.renode_platform + run: | + LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" + if [ -f "$LOCAL_REPL" ]; then + PLATFORM="@$PWD/$LOCAL_REPL" + else + PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" + fi + renode-test src/platforms/stm32/tests/renode/stm32_gpio_test.robot \ + --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ + --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_gpio_test.avm \ + --variable AVM_ADDRESS:${{ matrix.avm_address }} \ + --variable PLATFORM:$PLATFORM diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d5facada..4441ee1330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added initial support for ESP32C5 and ESP32C61 - Added `Range:size/1` - Added missing `ledc` functions for esp32 platform +- Added support for 10 new STM32 families by switching to STM32 official SDK ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index 286a269342..b950dc3f5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ add_subdirectory(tools/uf2tool) if (NOT "${CMAKE_GENERATOR}" MATCHES "Xcode") add_custom_target(dialyzer COMMENT "Run dialyzer") add_subdirectory(libs) + add_subdirectory(src/platforms/stm32/tests/test_erl_sources) if(NOT AVM_BUILD_RUNTIME_ONLY) add_subdirectory(examples) add_subdirectory(doc) diff --git a/README.Md b/README.Md index bdf2d05da0..484364f5ac 100644 --- a/README.Md +++ b/README.Md @@ -17,7 +17,7 @@ Supported Platforms * Linux, macOS, FreeBSD, DragonFly ([generic_unix](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-generic-unix-platform)) * ESP32 SoC (with IDF/FreeRTOS, see [esp32](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-esp32-platform)) -* STM32 MCUs (with LibOpenCM3, see [stm32](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-stm32-platform)) +* STM32 MCUs (with official ST HAL/LL SDK, see [stm32](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-stm32-platform)) * Raspberry Pi Pico and Pico 2 (see [rp2](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-raspberry-pi-pico-platform)) * Browsers and NodeJS with WebAssembly (see [emscripten](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-with-atomvm-webassembly)) diff --git a/doc/src/build-instructions.md b/doc/src/build-instructions.md index 803167c52b..44b84de664 100644 --- a/doc/src/build-instructions.md +++ b/doc/src/build-instructions.md @@ -707,39 +707,21 @@ To add support for a new peripheral or protocol using an AtomVM port, you need t ## Building for STM32 +This section describes building AtomVM using the official ST HAL/LL SDK, which is downloaded automatically via CMake FetchContent. This platform supports STM32F2, STM32F4, STM32F7, STM32G0, STM32G4, STM32H5, STM32H7, STM32L4, STM32L5, STM32U3, STM32U5, and STM32WB families. + ### STM32 Prerequisites The following software is required to build AtomVM for the STM32 platform: -* [11.3 ARM toolchain](https://developer.arm.com/-/media/Files/downloads/gnu/11.3.rel1/binrel/arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi.tar.xz) (or compatible with your system) -* [libopencm3](https://github.com/libopencm3/libopencm3.git) version 0.8.0 -* `cmake` -* `make` +* ARM toolchain (`arm-none-eabi-gcc`, e.g. 11.3 or later) +* `cmake` (3.13 or later) +* `meson` +* `ninja` * `git` -* `python` * Erlang/OTP `escript` ```{note} -AtomVM tests this build on the latest Ubuntu github runner. -``` - -### Setup libopencm3 - -Before building for the first time you need to have a compiled clone of the libopencm3 libraries, from inside the AtomVM/src/platforms/stm32 directory: - -```shell -$ git clone -b v0.8.0 https://github.com/libopencm3/libopencm3.git -$ cd libopencm3 && make -j4 && cd .. -``` - -```{tip} -You can put libopencm3 wherever you want on your PC as long as you update LIBOPENCM3_DIR to point to it. This -example assumes it has been cloned into /opt/libopencm3 and built. From inside the AtomVM/src/platforms/stm32 -directory: - - $ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake \ - -DLIBOPENCM3_DIR=/opt/libopencm3 .. - +No external SDK download is required. The STM32 HAL/LL drivers and CMSIS headers are fetched automatically by CMake during the build. AtomVM is built with `picolibc` which is also downloaded as part of the build and requires `meson` and `ninja`. ``` ### Build AtomVM with cmake toolchain file @@ -749,14 +731,39 @@ $ cd $ cd src/platforms/stm32 $ mkdir build $ cd build -$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake .. -$ make +$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake -DDEVICE=stm32f407vgt6 .. +$ ninja ``` ### Changing the target device -The default build is based on the STM32F4Discovery board chip (`stm32f407vgt6`). If you want to target a different -chip, pass the `-DDEVICE` flag when invoking cmake. For example, to use the BlackPill V2.0, pass `-DDEVICE=stm32f411ceu6`. At this time any `STM32F4` or `STM32F7` device with 512KB or more of on package flash should work with AtomVM. If an unsupported device is passed with the `DEVICE` parameter the configuration will fail. For devices with either 512KB or 768KB of flash the available application flash space will be limited to 128KB. Devices with only 512KB of flash may also suffer from slightly reduced performance because the compiler must optimize for size rather than performance. +The default build targets the STM32F407 Discovery (`stm32f407vgt6`). Pass the `-DDEVICE` flag to select a different device. Supported families and example devices: + +| Family | Example Devices | Max Clock | +|--------|----------------|-----------| +| STM32F2 | `stm32f205rgt6`, `stm32f207zgt6` | 120 MHz | +| STM32F4 | `stm32f407vgt6`, `stm32f429zit6`, `stm32f411ceu6`, `stm32f401ceu6` | 84-180 MHz | +| STM32F7 | `stm32f746zgt6`, `stm32f767zit6` | 216 MHz | +| STM32G0 | `stm32g0b1ret6` | 64 MHz | +| STM32G4 | `stm32g474ret6`, `stm32g491ret6` | 170 MHz | +| STM32H5 | `stm32h562rgt6` | 250 MHz | +| STM32H7 | `stm32h743vit6`, `stm32h743zit6` | 480 MHz | +| STM32L4 | `stm32l476rgt6`, `stm32l4r5zit6` | 80-120 MHz | +| STM32L5 | `stm32l552ret6` | 110 MHz | +| STM32U3 | `stm32u375rgt6`, `stm32u385rgt6` | 96 MHz | +| STM32U5 | `stm32u585ait6q` | 160 MHz | +| STM32WB | `stm32wb55rg` | 64 MHz | + +If an unsupported device is passed with the `DEVICE` parameter the configuration will fail. + +Example to build for a BlackPill (F411) board, which uses a 25 MHz HSE crystal: + +```shell +$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake -DDEVICE=stm32f411ceu6 \ + -DAVM_HSE_VALUE=25000000 .. +``` + +For devices with 512KB or less of flash, application flash space will be limited and the compiler optimizes for size rather than performance. ```{attention} For devices with only 512KB of flash the application address is different and must be adjusted when flashing your @@ -766,7 +773,9 @@ devices is `0x8060000`. ### Configuring the Console -The default build for any `DEVICE` will use `USART2` and output will be on `PA2`. This default will work well for most `Discovery` and generic boards that do not have an on-board TTL to USB-COM support (including the `stm32f411ceu6` A.K.A. `BlackPill V2.0`). For `Nucleo` boards that do have on board UART to USB-COM support you may pass the `cmake` parameter `-DBOARD=nucleo` to have the correct USART and TX pins configured automatically. The `Nucleo-144` series use `USART3` and `PD8`, while the supported `Nucleo-64` boards use `USART2`, but passing the `BOARD` parameter along with `DEVICE` will configure the correct `USART` for your model. If any other boards are discovered to have on board USB UART support pull requests, or opening issues with the details, are more than welcome. +By default, stdout and stderr are printed on the configured console USART. Baudrate is 115200 and serial transmission is 8N1 with no flow control. + +The default console is `USART1` on `PA9`. For `Nucleo` boards, pass `-DBOARD=nucleo` to automatically select the correct USART for your board. Example to configure a `NUCLEO-F429ZI`: @@ -777,16 +786,44 @@ $ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake -DDEVICE=stm32f429zi The AtomVM system console `USART` may also be configured to a specific uart peripheral. Pass one of the parameters from the chart below with the `cmake` option `-DAVM_CFG_CONSOLE=CONSOLE_#`, using the desired console parameter in place of `CONSOLE_#`. Not all UARTs are available on every supported board, but most will have several options that are not already used by other on board peripherals. Consult your data sheets for your device to select an appropriate console. -| Parameter | USART | TX Pin | AtomVM Default | Nucleo-144 | Nucleo-64 | -|-----------|-------|--------|----------------|------------|-----------| -| `CONSOLE_1` | `USART1` | `PA9` | | | | -| `CONSOLE_2` | `USART2` | `PA2` | ✅ | | ✅ | +| Parameter | USART | TX Pin | Default | Nucleo-144 | Nucleo-64 | +|-----------|-------|--------|---------|------------|-----------| +| `CONSOLE_1` | `USART1` | `PA9` | ✅ | | | +| `CONSOLE_2` | `USART2` | `PA2` | | | ✅ | | `CONSOLE_3` | `USART3` | `PD8` | | ✅ | | | `CONSOLE_4` | `UART4` | `PC10` | | | | | `CONSOLE_5` | `UART5` | `PC12` | | | | | `CONSOLE_6` | `USART6` | `PC6` | | | | -| `CONSOLE_7` | `UART7` | `PF7` | | | | -| `CONSOLE_8` | `UART8` | `PJ8` | | | | + +### Configuring the HSE frequency + +The external oscillator (HSE) frequency varies between boards. Set it at build time with `-DAVM_HSE_VALUE=`: + +```shell +$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake -DDEVICE=stm32f411ceu6 \ + -DAVM_HSE_VALUE=25000000 .. +``` + +Default HSE values per family (set in the family HAL configuration header): + +| Family | Default HSE | Typical Boards | +|--------|-------------|----------------| +| STM32F2 | 8 MHz | Nucleo boards | +| STM32F4 | 8 MHz | Discovery/Nucleo boards (BlackPill boards use 25 MHz) | +| STM32F7 | 8 MHz | Nucleo boards (ST-Link MCO bypass) | +| STM32G0 | 8 MHz | | +| STM32G4 | 8 MHz | Nucleo boards | +| STM32H5 | 8 MHz | Nucleo boards (ST-Link MCO bypass), WeAct Studio H562 | +| STM32H7 | 8 MHz | Nucleo boards (ST-Link MCO bypass), WeAct Studio H743 (25 MHz) | +| STM32L4 | 8 MHz | Nucleo boards | +| STM32L5 | N/A (uses MSI) | Nucleo boards | +| STM32U3 | 16 MHz | Nucleo boards | +| STM32U5 | 16 MHz | Nucleo-U585AI-Q, WeAct Studio U585 (25 MHz) | +| STM32WB | 32 MHz | Nucleo-WB55, WeAct Studio WB55 (required for BLE radio) | + +```{note} +Not all STM32 families have been tested on hardware. The F4, H5, H7, U5, and WB families have been tested on actual boards. The F2, F7, G0, G4, L4, L5, and U3 families are supported in the build system but have not yet been validated on hardware. If you encounter issues with an untested family, please open an [issue on GitHub](https://github.com/atomvm/AtomVM/issues). +``` ### Configure STM32 logging with `cmake` @@ -803,17 +840,37 @@ For log entries colorized by log level pass `-DAVM_ENABLE_LOG_COLOR=on` to cmake By default only `ERROR` messages contain file and line number information. This can be included with all log entries by passing `-DAVM_ENABLE_LOG_LINES=on` to cmake, but it does incur a significant performance penalty and is only suggested for debugging during development. -### Console Printing on STM32 +### Using local source checkouts for STM32 dependencies -AtomVM is built with standard `newlib` to support `long long` integers (`signed` and `unsigned`). If you are building for a device with extremely limited flash space the `nano` version of `newlib` can be used instead. This may be done by passing `-DAVM_NEWLIB_NANO=on`. If the `nano newlib` is used logs will be automatically disabled, this is because many of the VM low level log messages will include `%ull` formatting and will cause buffer overflows and crash the VM if logging is not disabled for `nano newlib` builds. The total flash savings of using `nano newlib` and disabling logs is just under 40kB. +By default, the STM32 build downloads picolibc and the STM32 SDK components (CMSIS headers and HAL/LL drivers) automatically. You can override this to use local checkouts by setting the following environment variables or CMake variables: -By default, stdout and stderr are printed on USART2. On the STM32F4Discovery board, you can see them -using a TTL-USB with the TX pin connected to board's pin PA2 (USART2 RX). Baudrate is 115200 and serial transmission -is 8N1 with no flow control. +| Variable | Description | +|----------|-------------| +| `PICOLIBC_PATH` | Path to a local [picolibc](https://github.com/picolibc/picolibc) source tree | +| `STM32_CMSIS_CORE_PATH` | Path to a local [CMSIS Core](https://github.com/STMicroelectronics/cmsis_core) checkout | +| `STM32_CMSIS_DEVICE__PATH` | Path to a local CMSIS Device checkout for the target family (e.g. `STM32_CMSIS_DEVICE_F4_PATH`) | +| `STM32_HAL_DRIVER__PATH` | Path to a local HAL/LL driver checkout for the target family (e.g. `STM32_HAL_DRIVER_F4_PATH`) | -```{seealso} -If building for a different target USART may be configure as explained above in -[Configuring the Console](#configuring-the-console). +The `` placeholder is the uppercase short family code: `F2`, `F4`, `F7`, `G0`, `G4`, `H5`, `H7`, `L4`, `L5`, `U3`, `U5`, or `WB`. + +Example using environment variables: + +```shell +$ export PICOLIBC_PATH=/home/user/src/picolibc +$ export STM32_CMSIS_CORE_PATH=/home/user/src/cmsis_core +$ export STM32_CMSIS_DEVICE_F4_PATH=/home/user/src/cmsis_device_f4 +$ export STM32_HAL_DRIVER_F4_PATH=/home/user/src/stm32f4xx_hal_driver +$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake -DDEVICE=stm32f407vgt6 .. +``` + +Alternatively, pass them as CMake variables with `-D`: + +```shell +$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-toolchain.cmake -DDEVICE=stm32f407vgt6 \ + -DPICOLIBC_PATH=/home/user/src/picolibc \ + -DFETCHCONTENT_SOURCE_DIR_CMSIS_CORE=/home/user/src/cmsis_core \ + -DFETCHCONTENT_SOURCE_DIR_CMSIS_DEVICE=/home/user/src/cmsis_device_f4 \ + -DFETCHCONTENT_SOURCE_DIR_HAL_DRIVER=/home/user/src/stm32f4xx_hal_driver .. ``` ### Configuring deployment builds for STM32 diff --git a/doc/src/getting-started-guide.md b/doc/src/getting-started-guide.md index 6225241fed..7822eec781 100644 --- a/doc/src/getting-started-guide.md +++ b/doc/src/getting-started-guide.md @@ -197,14 +197,22 @@ For information about how to flash your application to your ESP32, see the [Atom ## Getting Started on the STM32 platform -AtomVM can run on a wide variety of STM32 chip-sets available from [STMicroelectronics](https://www.st.com). The support is not nearly as mature as for the ESP32 platform, but work is ongoing, and pull requests are always welcome. At this time AtomVM will work on any board with a minimum of around 128KB ram and 512KB (1M recommended) flash. Simple applications and tests have been successfully run on a stm32f411ceu6 (A.K.A. Black Pill V2). These minimum requirements may need to be raised as platform support matures. +AtomVM can run on a wide variety of STM32 chip-sets available from [STMicroelectronics](https://www.st.com). The STM32 platform uses the official ST HAL/LL SDK and supports STM32F2, F4, F7, G0, G4, H5, H7, L4, L5, U3, U5, and WB families. The SDK is downloaded automatically during the build. + +The firmware reports `stm32` from `atomvm:platform/0` and uses the GPIO pin tuple format `{Bank :: atom(), Pin :: non_neg_integer()}`. + +At this time AtomVM will work on any board with a minimum of around 128KB RAM and 512KB (1M recommended) flash. Typical boards include ST Nucleo and Discovery boards, as well as WeAct Studio boards (H562, U585, WB55) and BlackPill (F411) boards. + +```{note} +Not all STM32 families have been tested on hardware. The F4, H5, H7, U5, and WB families have been tested on actual boards. The F2, F7, G0, G4, L4, L5, and U3 families are supported in the build system but have not yet been validated on hardware. If you encounter issues with an untested family, please open an [issue on GitHub](https://github.com/atomvm/AtomVM/issues). +``` ### STM32 Requirements Deployment of AtomVM on the STM32 platform requires the following components: * A computer running MacOS or Linux (Windows is not currently supported); -* An stm32 board and a USB/UART connector (these are built into some boards such as the Nucleo product line) and a minimum of 512k (1M recommended) of flash and a recommended minimum of 100k RAM; +* An STM32 board with a minimum of 512KB (1M recommended) of flash and a recommended minimum of 128K RAM; * A USB cable capable of connecting the STM32 module or board to your development machine (laptop or PC); * `st-flash` via [stlink](https://github.com/stlink-org/stlink), to flash both AtomVM and your packed AVM applications. Make sure to follow its [installation procedure](https://github.com/stlink-org/stlink#installation) before proceeding further. * A [st-link v2](https://www.st.com/en/development-tools/st-link-v2.html) or [st-link v3](https://www.st.com/en/development-tools/stlink-v3set.html) device (typically already included on Nucleo and Discovery boards), is needed for flashing and optional jtag debugging. @@ -245,7 +253,7 @@ of flash the application address is 0x8060000, leaving 128KB of application flas #### Console Printing -By default, stdout and stderr are printed on USART2. On the STM32F4Discovery board, you can see them using a TTL-USB with the TX pin connected to board's pin PA2 (USART2 RX). Baudrate is 115200 and serial transmission is 8N1 with no flow control. +By default, stdout and stderr are printed on the configured console USART. Baudrate is 115200 and serial transmission is 8N1 with no flow control. For Nucleo boards the on board USB-COM to USART may be used by configuring your build with a BOARD parameter, see the [STM32 Build Instructions](./build-instructions.md#building-for-stm32) for [Configuring the Console](./build-instructions.md#configuring-the-console). diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index b3bb1dbda7..fd6fd85e62 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -24,6 +24,7 @@ include(BuildErlang) add_subdirectory(esp32) add_subdirectory(rp2) +add_subdirectory(stm32) pack_runnable(hello_world hello_world estdlib) pack_runnable(udp_server udp_server estdlib eavmlib) diff --git a/examples/erlang/stm32/CMakeLists.txt b/examples/erlang/stm32/CMakeLists.txt new file mode 100644 index 0000000000..cac029a7eb --- /dev/null +++ b/examples/erlang/stm32/CMakeLists.txt @@ -0,0 +1,31 @@ +# +# 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(examples_erlang_stm32) + +include(BuildErlang) + +pack_runnable(blink_weact_studio_blackpill blink_weact_studio_blackpill eavmlib) +pack_runnable(blink_weact_studio_h562 blink_weact_studio_h562 eavmlib) +pack_runnable(blink_weact_studio_h743 blink_weact_studio_h743 eavmlib) +pack_runnable(blink_weact_studio_u585 blink_weact_studio_u585 eavmlib) +pack_runnable(blink_weact_studio_wb55 blink_weact_studio_wb55 eavmlib) +pack_runnable(blink_nucleo64 blink_nucleo64 eavmlib) +pack_runnable(blink_nucleo144 blink_nucleo144 eavmlib) diff --git a/examples/erlang/stm32/blink_nucleo144.erl b/examples/erlang/stm32/blink_nucleo144.erl new file mode 100644 index 0000000000..8892bd4d5d --- /dev/null +++ b/examples/erlang/stm32/blink_nucleo144.erl @@ -0,0 +1,68 @@ +% +% 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 +% + +-module(blink_nucleo144). +-export([start/0]). + +% Select board revision: mb1137 (default), mb1312, or mb1364. +-ifndef(NUCLEO144_BOARD). +-define(NUCLEO144_BOARD, mb1137). +-endif. + +-if(?NUCLEO144_BOARD =:= mb1137). +% MB1137: NUCLEO-F429ZI, NUCLEO-F746ZG, NUCLEO-H743ZI, etc. +% LD1 (green) = PB0, LD2 (blue) = PB7, LD3 (red) = PB14 +-define(LEDS, [{b, [0, 7, 14]}]). +-elif(?NUCLEO144_BOARD =:= mb1312). +% MB1312: NUCLEO-L496ZG-P, NUCLEO-L4R5ZI, etc. +% LD1 (green) = PC7, LD2 (blue) = PB7, LD3 (red) = PB14 +-define(LEDS, [{b, [7, 14]}, {c, 7}]). +-elif(?NUCLEO144_BOARD =:= mb1364). +% MB1364: NUCLEO-H743ZI2 +% LD1 (green) = PB0, LD2 (yellow) = PE1, LD3 (red) = PB14 +-define(LEDS, [{b, [0, 14]}, {e, 1}]). +-endif. + +start() -> + set_outputs(?LEDS), + loop(?LEDS). + +loop(LEDs) -> + write_all(LEDs, high), + receive + after 500 -> ok + end, + write_all(LEDs, low), + receive + after 500 -> ok + end, + loop(LEDs). + +set_outputs([Pin | Rest]) -> + gpio:set_pin_mode(Pin, output), + set_outputs(Rest); +set_outputs([]) -> + ok. + +write_all([Pin | Rest], Level) -> + gpio:digital_write(Pin, Level), + write_all(Rest, Level); +write_all([], _Level) -> + ok. diff --git a/examples/erlang/stm32/blink_nucleo64.erl b/examples/erlang/stm32/blink_nucleo64.erl new file mode 100644 index 0000000000..9aa81cee33 --- /dev/null +++ b/examples/erlang/stm32/blink_nucleo64.erl @@ -0,0 +1,41 @@ +% +% 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 +% + +-module(blink_nucleo64). +-export([start/0]). + +% Nucleo-64 boards: NUCLEO-F411RE, NUCLEO-L476RG, NUCLEO-G0B1RE, etc. +% Single user LED (LD2, green) = PA5 + +start() -> + Pin = {a, 5}, + gpio:set_pin_mode(Pin, output), + loop(Pin). + +loop(Pin) -> + gpio:digital_write(Pin, high), + receive + after 500 -> ok + end, + gpio:digital_write(Pin, low), + receive + after 500 -> ok + end, + loop(Pin). diff --git a/examples/erlang/stm32/blink_weact_studio_blackpill.erl b/examples/erlang/stm32/blink_weact_studio_blackpill.erl new file mode 100644 index 0000000000..13c9818eec --- /dev/null +++ b/examples/erlang/stm32/blink_weact_studio_blackpill.erl @@ -0,0 +1,42 @@ +% +% 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 +% + +-module(blink_weact_studio_blackpill). +-export([start/0]). + +% WeAct Studio BlackPill (STM32F401/F411) +% On-board LED is on PC13, active low. + +start() -> + Pin = {c, 13}, + gpio:init(Pin), + gpio:set_pin_mode(Pin, output), + loop(Pin). + +loop(Pin) -> + gpio:digital_write(Pin, low), + receive + after 500 -> ok + end, + gpio:digital_write(Pin, high), + receive + after 500 -> ok + end, + loop(Pin). diff --git a/examples/erlang/stm32/blink_weact_studio_h562.erl b/examples/erlang/stm32/blink_weact_studio_h562.erl new file mode 100644 index 0000000000..9635c54881 --- /dev/null +++ b/examples/erlang/stm32/blink_weact_studio_h562.erl @@ -0,0 +1,42 @@ +% +% 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 +% + +-module(blink_weact_studio_h562). +-export([start/0]). + +% WeAct Studio STM32H562RGT6 +% On-board LED is on PB2, active high. + +start() -> + Pin = {b, 2}, + gpio:init(Pin), + gpio:set_pin_mode(Pin, output), + loop(Pin). + +loop(Pin) -> + gpio:digital_write(Pin, high), + receive + after 500 -> ok + end, + gpio:digital_write(Pin, low), + receive + after 500 -> ok + end, + loop(Pin). diff --git a/examples/erlang/stm32/blink_weact_studio_h743.erl b/examples/erlang/stm32/blink_weact_studio_h743.erl new file mode 100644 index 0000000000..b1e5f5c9d7 --- /dev/null +++ b/examples/erlang/stm32/blink_weact_studio_h743.erl @@ -0,0 +1,42 @@ +% +% 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 +% + +-module(blink_weact_studio_h743). +-export([start/0]). + +% WeAct Studio STM32H743VIT6 +% On-board LED is on PE3, active high. + +start() -> + Pin = {e, 3}, + gpio:init(Pin), + gpio:set_pin_mode(Pin, output), + loop(Pin). + +loop(Pin) -> + gpio:digital_write(Pin, high), + receive + after 500 -> ok + end, + gpio:digital_write(Pin, low), + receive + after 500 -> ok + end, + loop(Pin). diff --git a/examples/erlang/stm32/blink_weact_studio_u585.erl b/examples/erlang/stm32/blink_weact_studio_u585.erl new file mode 100644 index 0000000000..b09b7a1ce3 --- /dev/null +++ b/examples/erlang/stm32/blink_weact_studio_u585.erl @@ -0,0 +1,42 @@ +% +% 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 +% + +-module(blink_weact_studio_u585). +-export([start/0]). + +% WeAct Studio STM32U585CIU6 +% On-board LED is on PC13, active low. + +start() -> + Pin = {c, 13}, + gpio:init(Pin), + gpio:set_pin_mode(Pin, output), + loop(Pin). + +loop(Pin) -> + gpio:digital_write(Pin, low), + receive + after 500 -> ok + end, + gpio:digital_write(Pin, high), + receive + after 500 -> ok + end, + loop(Pin). diff --git a/examples/erlang/stm32/blink_weact_studio_wb55.erl b/examples/erlang/stm32/blink_weact_studio_wb55.erl new file mode 100644 index 0000000000..e9e373f8b8 --- /dev/null +++ b/examples/erlang/stm32/blink_weact_studio_wb55.erl @@ -0,0 +1,42 @@ +% +% 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 +% + +-module(blink_weact_studio_wb55). +-export([start/0]). + +% WeAct Studio STM32WB55CGU6 +% On-board LED is on PE4, active high. + +start() -> + Pin = {e, 4}, + gpio:init(Pin), + gpio:set_pin_mode(Pin, output), + loop(Pin). + +loop(Pin) -> + gpio:digital_write(Pin, high), + receive + after 500 -> ok + end, + gpio:digital_write(Pin, low), + receive + after 500 -> ok + end, + loop(Pin). diff --git a/src/libAtomVM/posix_nifs.c b/src/libAtomVM/posix_nifs.c index 252cc21b83..d82f8d2eb0 100644 --- a/src/libAtomVM/posix_nifs.c +++ b/src/libAtomVM/posix_nifs.c @@ -40,10 +40,7 @@ #include #endif -#if HAVE_OPEN && HAVE_CLOSE || defined(HAVE_CLOCK_SETTIME) || defined(HAVE_SETTIMEOFDAY) \ - || HAVE_OPENDIR && HAVE_READDIR && HAVE_CLOSEDIR || defined(HAVE_GETCWD) && defined(HAVE_PATH_MAX) #include -#endif #if HAVE_OPENDIR && HAVE_READDIR && HAVE_CLOSEDIR #include diff --git a/src/platforms/stm32/.gitignore b/src/platforms/stm32/.gitignore index 43afe094ee..c1854c1267 100644 --- a/src/platforms/stm32/.gitignore +++ b/src/platforms/stm32/.gitignore @@ -5,4 +5,3 @@ build/ *.elf *.bin -generated.*.ld diff --git a/src/platforms/stm32/CMakeLists.txt b/src/platforms/stm32/CMakeLists.txt index 76b5e847a3..24fa027562 100644 --- a/src/platforms/stm32/CMakeLists.txt +++ b/src/platforms/stm32/CMakeLists.txt @@ -20,6 +20,7 @@ cmake_minimum_required (VERSION 3.13) project(AtomVM) +enable_language(ASM) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../CMakeModules") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) @@ -29,23 +30,22 @@ option(AVM_ENABLE_OLD_OTP_VERSIONS "Enable OTP version < 26" OFF) option(AVM_USE_32BIT_FLOAT "Use 32 bit floats." ON) option(AVM_VERBOSE_ABORT "Print module and line number on VM abort" OFF) option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) -option(AVM_NEWLIB_NANO "Use 'nano' newlib. Saves 46kB, no `long long` support" OFF) option(AVM_LOG_DISABLE "Disable log output" OFF) option(AVM_ENABLE_LOG_COLOR "Use color log output" OFF) -option(AVM_ENABLE_LOG_LINES "Include source and line info for all enbled levels" OFF) +option(AVM_ENABLE_LOG_LINES "Include source and line info for all enabled levels" OFF) option(AVM_CONFIG_REBOOT_ON_NOT_OK "Reboot when application exits with non 'ok' return" OFF) option(AVM_DISABLE_GPIO_NIFS "Disable GPIO nifs (input and output)" OFF) option(AVM_DISABLE_GPIO_PORT_DRIVER "Disable GPIO 'port' driver (input, output, and interrupts)" OFF) option(AVM_PRINT_PROCESS_CRASH_DUMPS "Print crash reports when processes die with non-standard reasons" ON) +set(AVM_HSE_VALUE "" CACHE STRING "HSE crystal frequency in Hz (e.g. 25000000). Overrides family default.") +if (AVM_HSE_VALUE) + add_compile_definitions(HSE_VALUE=${AVM_HSE_VALUE}U) +endif() + set(AVM_DISABLE_SMP ON FORCE) set(AVM_DISABLE_TASK_DRIVER ON FORCE) -if (AVM_NEWLIB_NANO) - set(LINKER_FLAGS "${LINKER_FLAGS} -specs=nano.specs") - set(AVM_LOG_DISABLE ON FORCE) -endif() - if (AVM_CONFIG_REBOOT_ON_NOT_OK) add_compile_definitions(CONFIG_REBOOT_ON_NOT_OK) endif() @@ -102,20 +102,72 @@ if (NOT DEVICE) set(DEVICE stm32f407vgt6) endif () -# mkfifo may be defined in some newlib header but not implemented +# Bare-metal: nosys.specs provides stubs that make check_function_exists pass, +# but these functions are not actually available. Force-disable all of them. set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) -# we don't want unlink either set(HAVE_UNLINK "" CACHE INTERNAL "Have symbol unlink" FORCE) -# nor EXECVE set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) - -# Include auto-device configuration +set(HAVE_OPEN "" CACHE INTERNAL "Have symbol open" FORCE) +set(HAVE_CLOSE "" CACHE INTERNAL "Have symbol close" FORCE) +set(HAVE_OPENDIR "" CACHE INTERNAL "Have symbol opendir" FORCE) +set(HAVE_CLOSEDIR "" CACHE INTERNAL "Have symbol closedir" FORCE) +set(HAVE_READDIR "" CACHE INTERNAL "Have symbol readdir" FORCE) +set(HAVE_CLOSEFROM "" CACHE INTERNAL "Have symbol closefrom" FORCE) +set(HAVE_GETCWD "" CACHE INTERNAL "Have symbol getcwd" FORCE) +set(HAVE_CLOCK_SETTIME "" CACHE INTERNAL "Have symbol clock_settime" FORCE) +set(HAVE_SETTIMEOFDAY "" CACHE INTERNAL "Have symbol settimeofday" FORCE) +set(HAVE_SOCKET "" CACHE INTERNAL "Have symbol socket" FORCE) +set(HAVE_SELECT "" CACHE INTERNAL "Have symbol select" FORCE) + +# Include device detection (sets STM32_FAMILY, CPU flags, HAL defines, etc.) +include(cmake/stm32_device.cmake) + +# Include auto-device configuration (escript query for clock, ROM, etc.) include(cmake/atomvm_dev_config.cmake) -# Include libopencm3 -include(cmake/libopencm3.cmake) +# Include SDK fetch (CMSIS + HAL/LL) +include(cmake/stm32_sdk.cmake) -# Include additional compilation flags +# Include additional compilation flags (must come after dev_config sets CMAKE_FLASH_SIZE) include(cmake/compile-flags.cmake) +# Build picolibc from source (replaces toolchain newlib to avoid wide char bloat) +include(cmake/picolibc.cmake) + +# Use picolibc headers instead of toolchain newlib headers for ALL targets. +# The -isystem path is added globally so libAtomVM also picks up picolibc's +# stdio.h/errno.h instead of newlib's (which pulls in _impure_ptr). +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -isystem ${PICOLIBC_INCLUDE_DIR}") + +# Compute linker RAM size (after both stm32_device.cmake and atomvm_dev_config.cmake) +if (LINKER_RAM_SIZE_OVERRIDE) + set(LINKER_RAM_SIZE "${LINKER_RAM_SIZE_OVERRIDE}K") +else() + set(LINKER_RAM_SIZE "${RAM_SIZE_KB}K") +endif() + +# Generate linker script from template +set(LINKER_SCRIPT_IN "${CMAKE_CURRENT_SOURCE_DIR}/cmake/stm32_linker.ld.in") +set(LINKER_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/stm32_linker.ld") +configure_file(${LINKER_SCRIPT_IN} ${LINKER_SCRIPT} @ONLY) + +# Generate family-specific HAL configuration header from template +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/stm32_hal_conf.h.in + ${CMAKE_CURRENT_BINARY_DIR}/generated/${STM32_FAMILY}_hal_conf.h + @ONLY +) + +# Set linker flags +string(REPLACE ";" " " _arch_flags_str "${STM32_ARCH_FLAGS}") +set(LINKER_FLAGS "${LINKER_FLAGS} -nostdlib -nostartfiles -Wl,--gc-sections -T${LINKER_SCRIPT} ${_arch_flags_str}") + +message("-----------Linker Info-----------") +message(STATUS "Linker Script : ${LINKER_SCRIPT}") +message(STATUS "Linker Flags : ${LINKER_FLAGS}") + +# Use linker flags to detect symbols +set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE) +set(CMAKE_REQUIRED_FLAGS ${LINKER_FLAGS}) + add_subdirectory(src) diff --git a/src/platforms/stm32/clock_configs/clock_config.h b/src/platforms/stm32/clock_configs/clock_config.h new file mode 100644 index 0000000000..4d2552f6ae --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config.h @@ -0,0 +1,26 @@ +/* + * 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 + */ + +#ifndef _CLOCK_CONFIG_H_ +#define _CLOCK_CONFIG_H_ + +void SystemClock_Config(void); + +#endif /* _CLOCK_CONFIG_H_ */ diff --git a/src/platforms/stm32/clock_configs/clock_config_f2.c b/src/platforms/stm32/clock_configs/clock_config_f2.c new file mode 100644 index 0000000000..7b0449cc71 --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_f2.c @@ -0,0 +1,83 @@ +/* + * 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 "stm32f2xx_hal.h" + +/** + * System Clock Configuration for STM32F2 family. + * + * Configures HSE -> PLL -> SYSCLK at 120 MHz. + * + * PLL formula: SYSCLK = (HSE / PLLM) * PLLN / PLLP + * USB requires (HSE / PLLM) * PLLN / PLLQ = 48 MHz + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + __HAL_RCC_PWR_CLK_ENABLE(); + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + +#if AVM_CLOCK_HZ == 120000000 && HSE_VALUE == 8000000U + /* 8MHz / 8 = 1 MHz * 240 = 240 MHz VCO / 2 = 120 MHz + * USB: 240/5 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 240; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 5; +#elif AVM_CLOCK_HZ == 120000000 && HSE_VALUE == 25000000U + /* 25MHz / 25 = 1 MHz * 240 = 240 MHz VCO / 2 = 120 MHz + * USB: 240/5 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 25; + rcc_osc_init.PLL.PLLN = 240; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 5; +#else +#error "Unsupported clock frequency / HSE combination for STM32F2" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* Select PLL as system clock source and configure HCLK, PCLK1, PCLK2 + * SYSCLK = 120 MHz + * AHB = 120 MHz (HPRE = /1) + * APB1 = 30 MHz (PPRE1 = /4, max 30 MHz) + * APB2 = 60 MHz (PPRE2 = /2, max 60 MHz) + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV4; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV2; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_3) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_f4.c b/src/platforms/stm32/clock_configs/clock_config_f4.c new file mode 100644 index 0000000000..49c49b8142 --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_f4.c @@ -0,0 +1,163 @@ +/* + * 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 "stm32f4xx_hal.h" +#include + +/** + * System Clock Configuration for STM32F4 family. + * + * Configures HSE -> PLL -> SYSCLK at the target frequency. + * HSE_VALUE is defined in stm32f4xx_hal_conf.h (default 8MHz for + * Discovery/Nucleo boards, 25MHz for Blackpill boards). + * + * The PLL configuration depends on the target clock frequency: + * - 84 MHz: for STM32F401 (Blackpill with 25MHz HSE) + * - 100 MHz: for STM32F411 (Blackpill with 25MHz HSE) + * - 168 MHz: for STM32F407 (Discovery with 8MHz HSE) + * - 180 MHz: for STM32F4x9 (with 8MHz HSE) + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + __HAL_RCC_PWR_CLK_ENABLE(); + + /* The voltage scaling allows optimizing the power consumption when the + * device is clocked below the maximum system frequency. */ + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + + /* + * PLL formula: SYSCLK = (HSE / PLLM) * PLLN / PLLP + * USB requires (HSE / PLLM) * PLLN / PLLQ = 48 MHz + */ +#if AVM_CLOCK_HZ == 84000000 && HSE_VALUE == 25000000U + /* Blackpill F401: 25MHz / 25 * 336 / 4 = 84 MHz, USB: 336/7 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 25; + rcc_osc_init.PLL.PLLN = 336; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV4; + rcc_osc_init.PLL.PLLQ = 7; +#elif AVM_CLOCK_HZ == 100000000 && HSE_VALUE == 25000000U + /* Blackpill F411: 25MHz / 25 * 400 / 4 = 100 MHz + * Note: USB clock = 400/9 = 44.4 MHz (not exactly 48 MHz). + * Exact 48 MHz is impossible at 100 MHz SYSCLK with integer dividers. */ + rcc_osc_init.PLL.PLLM = 25; + rcc_osc_init.PLL.PLLN = 400; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV4; + rcc_osc_init.PLL.PLLQ = 9; +#elif AVM_CLOCK_HZ == 168000000 && HSE_VALUE == 8000000U + /* Discovery F407: 8MHz / 8 * 336 / 2 = 168 MHz, USB: 336/7 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 336; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 7; +#elif AVM_CLOCK_HZ == 180000000 && HSE_VALUE == 8000000U + /* F4x9: 8MHz / 8 * 360 / 2 = 180 MHz + * Note: USB clock = 360/8 = 45 MHz (not 48 MHz). USB may not work reliably. */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 360; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 8; +#elif AVM_CLOCK_HZ == 84000000 && HSE_VALUE == 8000000U + /* F401 with 8MHz HSE: 8MHz / 8 * 336 / 4 = 84 MHz */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 336; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV4; + rcc_osc_init.PLL.PLLQ = 7; +#elif AVM_CLOCK_HZ == 100000000 && HSE_VALUE == 8000000U + /* F411 with 8MHz HSE: 8MHz / 8 * 400 / 4 = 100 MHz + * Note: USB clock = 400/9 = 44.4 MHz (not exactly 48 MHz). + * Exact 48 MHz is impossible at 100 MHz SYSCLK with integer dividers. */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 400; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV4; + rcc_osc_init.PLL.PLLQ = 9; +#elif AVM_CLOCK_HZ == 168000000 && HSE_VALUE == 16000000U + /* F407 with 16MHz HSE: 16MHz / 8 * 168 / 2 = 168 MHz, USB: 336/7 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 168; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 7; +#elif AVM_CLOCK_HZ == 180000000 && HSE_VALUE == 16000000U + /* F4x9 with 16MHz HSE: 16MHz / 8 * 180 / 2 = 180 MHz + * Note: USB clock = 360/8 = 45 MHz (not 48 MHz). USB may not work reliably. */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 180; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 8; +#elif AVM_CLOCK_HZ == 168000000 && HSE_VALUE == 25000000U + /* F407 with 25MHz HSE: 25MHz / 25 * 336 / 2 = 168 MHz */ + rcc_osc_init.PLL.PLLM = 25; + rcc_osc_init.PLL.PLLN = 336; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 7; +#else +#error "Unsupported clock frequency / HSE combination for STM32F4" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + +#if AVM_CLOCK_HZ == 180000000 + if (HAL_PWREx_EnableOverDrive() != HAL_OK) { + while (1) { + } + } +#endif + + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + +#if AVM_CLOCK_HZ == 84000000 || AVM_CLOCK_HZ == 100000000 + /* APB1 max 50 MHz, APB2 max 100 MHz for F401/F411 */ + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV2; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_3) != HAL_OK) { + while (1) { + } + } +#elif AVM_CLOCK_HZ == 168000000 + /* APB1 max 42 MHz, APB2 max 84 MHz */ + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV4; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV2; + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_5) != HAL_OK) { + while (1) { + } + } +#elif AVM_CLOCK_HZ == 180000000 + /* APB1 max 45 MHz, APB2 max 90 MHz */ + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV4; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV2; + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_5) != HAL_OK) { + while (1) { + } + } +#endif +} diff --git a/src/platforms/stm32/clock_configs/clock_config_f7.c b/src/platforms/stm32/clock_configs/clock_config_f7.c new file mode 100644 index 0000000000..edeca8b1e0 --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_f7.c @@ -0,0 +1,91 @@ +/* + * 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 "stm32f7xx_hal.h" + +/** + * System Clock Configuration for STM32F7 family. + * + * Configures HSE -> PLL -> SYSCLK at 216 MHz. + * + * PLL formula: SYSCLK = (HSE / PLLM) * PLLN / PLLP + * USB requires (HSE / PLLM) * PLLN / PLLQ = 48 MHz + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + __HAL_RCC_PWR_CLK_ENABLE(); + + /* Voltage scaling: Scale 1 with over-drive for 216 MHz */ + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + +#if AVM_CLOCK_HZ == 216000000 && (HSE_VALUE == 8000000U) + /* Nucleo F746/F767: 8MHz / 8 = 1 MHz * 432 = 432 MHz VCO / 2 = 216 MHz + * USB: 432/9 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 8; + rcc_osc_init.PLL.PLLN = 432; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 9; +#elif AVM_CLOCK_HZ == 216000000 && (HSE_VALUE == 25000000U) + /* 25MHz / 25 = 1 MHz * 432 = 432 MHz VCO / 2 = 216 MHz + * USB: 432/9 = 48 MHz */ + rcc_osc_init.PLL.PLLM = 25; + rcc_osc_init.PLL.PLLN = 432; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = 9; +#else +#error "Unsupported clock frequency / HSE combination for STM32F7" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + if (HAL_PWREx_EnableOverDrive() != HAL_OK) { + while (1) { + } + } + + /* Select PLL as system clock source and configure HCLK, PCLK1, PCLK2 + * SYSCLK = 216 MHz + * AHB = 216 MHz (HPRE = /1) + * APB1 = 54 MHz (PPRE1 = /4, max 54 MHz) + * APB2 = 108 MHz (PPRE2 = /2, max 108 MHz) + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV4; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV2; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_7) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_g0.c b/src/platforms/stm32/clock_configs/clock_config_g0.c new file mode 100644 index 0000000000..26b91652f7 --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_g0.c @@ -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 + */ + +#include "stm32g0xx_hal.h" + +/** + * System Clock Configuration for STM32G0 family. + * + * Configures HSE -> PLL -> SYSCLK at 64 MHz. + * + * PLL formula: SYSCLK = (HSE / PLLM) * PLLN / PLLR + * 8 / 1 * 16 / 2 = 64 MHz + * + * Flash latency: 2 wait states at 64 MHz. + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + +#if AVM_CLOCK_HZ == 64000000 && (HSE_VALUE == 8000000U) + /* 8 MHz / 1 = 8 MHz * 16 = 128 MHz VCO / 2 = 64 MHz */ + rcc_osc_init.PLL.PLLM = RCC_PLLM_DIV1; + rcc_osc_init.PLL.PLLN = 16; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; +#else +#error "Unsupported clock frequency / HSE combination for STM32G0" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: + * SYSCLK = 64 MHz + * AHB = 64 MHz + * APB1 = 64 MHz + * Flash latency: 2 wait states at 64 MHz. + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_g4.c b/src/platforms/stm32/clock_configs/clock_config_g4.c new file mode 100644 index 0000000000..4c1ec3288f --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_g4.c @@ -0,0 +1,88 @@ +/* + * 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 "stm32g4xx_hal.h" + +/** + * System Clock Configuration for STM32G4 family. + * + * Configures HSE -> PLL -> SYSCLK at 170 MHz. + * + * PLL formula: SYSCLK = (HSE / PLLM) * PLLN / PLLR + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + /* Voltage scaling: Range 1 boost mode for 170 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST) != HAL_OK) { + while (1) { + } + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + +#if AVM_CLOCK_HZ == 170000000 && (HSE_VALUE == 8000000U) + /* 8MHz / 2 = 4 MHz * 85 = 340 MHz VCO / 2 = 170 MHz */ + rcc_osc_init.PLL.PLLM = RCC_PLLM_DIV2; + rcc_osc_init.PLL.PLLN = 85; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; +#elif AVM_CLOCK_HZ == 170000000 && (HSE_VALUE == 24000000U) + /* 24MHz / 6 = 4 MHz * 85 = 340 MHz VCO / 2 = 170 MHz */ + rcc_osc_init.PLL.PLLM = RCC_PLLM_DIV6; + rcc_osc_init.PLL.PLLN = 85; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; +#else +#error "Unsupported clock frequency / HSE combination for STM32G4" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: + * SYSCLK = 170 MHz + * AHB = 170 MHz + * APB1 = 170 MHz (G4 APBs can run at full speed) + * APB2 = 170 MHz + * Flash latency: 4 wait states at 170 MHz (boost mode). + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_4) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_h5.c b/src/platforms/stm32/clock_configs/clock_config_h5.c new file mode 100644 index 0000000000..788ad991fa --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_h5.c @@ -0,0 +1,104 @@ +/* + * 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 "stm32h5xx_hal.h" + +/** + * System Clock Configuration for STM32H5 family. + * + * Configures HSE -> PLL1 -> SYSCLK at 250 MHz. + * All bus clocks (AHB, APB1, APB2, APB3) can run at 250 MHz. + * + * PLL1 formula: SYSCLK = (HSE / PLLM) * PLLN / PLLP + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + /* Voltage scaling: Scale 0 for maximum frequency (250 MHz) */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE0) != HAL_OK) { + while (1) { + } + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + rcc_osc_init.PLL.PLLFRACN = 0; + + /* + * PLL1: HSE / PLLM * PLLN => VCO (192-836 MHz wide), / PLLP => SYSCLK + */ +#if AVM_CLOCK_HZ == 250000000 && (HSE_VALUE == 8000000U) + /* Nucleo H562: 8MHz / 2 = 4 MHz * 125 = 500 MHz VCO / 2 = 250 MHz */ + rcc_osc_init.PLL.PLLM = 2; + rcc_osc_init.PLL.PLLN = 125; + rcc_osc_init.PLL.PLLP = 2; + rcc_osc_init.PLL.PLLQ = 2; + rcc_osc_init.PLL.PLLR = 2; + rcc_osc_init.PLL.PLLRGE = RCC_PLL1_VCIRANGE_1; /* 2-4 MHz input */ + rcc_osc_init.PLL.PLLVCOSEL = RCC_PLL1_VCORANGE_WIDE; +#elif AVM_CLOCK_HZ == 250000000 && (HSE_VALUE == 25000000U) + /* 25MHz / 5 = 5 MHz * 100 = 500 MHz VCO / 2 = 250 MHz */ + rcc_osc_init.PLL.PLLM = 5; + rcc_osc_init.PLL.PLLN = 100; + rcc_osc_init.PLL.PLLP = 2; + rcc_osc_init.PLL.PLLQ = 2; + rcc_osc_init.PLL.PLLR = 2; + rcc_osc_init.PLL.PLLRGE = RCC_PLL1_VCIRANGE_2; /* 4-8 MHz input */ + rcc_osc_init.PLL.PLLVCOSEL = RCC_PLL1_VCORANGE_WIDE; +#elif AVM_CLOCK_HZ == 250000000 && (HSE_VALUE == 24000000U) + /* 24MHz / 6 = 4 MHz * 125 = 500 MHz VCO / 2 = 250 MHz */ + rcc_osc_init.PLL.PLLM = 6; + rcc_osc_init.PLL.PLLN = 125; + rcc_osc_init.PLL.PLLP = 2; + rcc_osc_init.PLL.PLLQ = 2; + rcc_osc_init.PLL.PLLR = 2; + rcc_osc_init.PLL.PLLRGE = RCC_PLL1_VCIRANGE_1; /* 2-4 MHz input */ + rcc_osc_init.PLL.PLLVCOSEL = RCC_PLL1_VCORANGE_WIDE; +#else +#error "Unsupported clock frequency / HSE combination for STM32H5" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: all at 250 MHz (no prescaling). + * Flash latency: 5 wait states at 250 MHz. + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 + | RCC_CLOCKTYPE_PCLK3; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB3CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_5) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_h7.c b/src/platforms/stm32/clock_configs/clock_config_h7.c new file mode 100644 index 0000000000..bed6682808 --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_h7.c @@ -0,0 +1,113 @@ +/* + * 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 "stm32h7xx_hal.h" + +/** + * System Clock Configuration for STM32H7 family. + * + * Configures HSE -> PLL1 -> SYSCLK at the target frequency. + * The H7 has multiple clock domains (D1/D2/D3) and requires + * power supply and voltage scaling configuration. + * + * Target: 480 MHz SYSCLK, 240 MHz AHB, 120 MHz APBx. + * + * PLL1 formula: SYSCLK = (HSE / PLLM) * PLLN / PLLP + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + /* Configure power supply: LDO mode */ + if (HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY) != HAL_OK) { + while (1) { + } + } + + /* Voltage scaling: Scale 0 for maximum frequency (480 MHz) */ + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0); + while (!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) { + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + + /* + * PLL1: HSE / PLLM * PLLN => VCO, then / PLLP => SYSCLK + * VCO must be 192-960 MHz (wide range) + * VCO input (HSE/PLLM) must be 1-16 MHz + */ +#if AVM_CLOCK_HZ == 480000000 && (HSE_VALUE == 8000000U) + /* Nucleo H743: 8MHz / 4 = 2 MHz * 480 = 960 MHz VCO / 2 = 480 MHz */ + rcc_osc_init.PLL.PLLM = 4; + rcc_osc_init.PLL.PLLN = 480; + rcc_osc_init.PLL.PLLP = 2; + rcc_osc_init.PLL.PLLQ = 20; /* 960/20 = 48 MHz for USB */ + rcc_osc_init.PLL.PLLR = 2; + rcc_osc_init.PLL.PLLRGE = RCC_PLL1VCIRANGE_1; /* 2-4 MHz input */ + rcc_osc_init.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE; + rcc_osc_init.PLL.PLLFRACN = 0; +#elif AVM_CLOCK_HZ == 480000000 && (HSE_VALUE == 25000000U) + /* 25MHz / 5 = 5 MHz * 192 = 960 MHz VCO / 2 = 480 MHz */ + rcc_osc_init.PLL.PLLM = 5; + rcc_osc_init.PLL.PLLN = 192; + rcc_osc_init.PLL.PLLP = 2; + rcc_osc_init.PLL.PLLQ = 20; /* 960/20 = 48 MHz for USB */ + rcc_osc_init.PLL.PLLR = 2; + rcc_osc_init.PLL.PLLRGE = RCC_PLL1VCIRANGE_2; /* 4-8 MHz input */ + rcc_osc_init.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE; + rcc_osc_init.PLL.PLLFRACN = 0; +#else +#error "Unsupported clock frequency / HSE combination for STM32H7" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: + * SYSCLK = 480 MHz (D1CPRE = /1) + * AHB = 240 MHz (HPRE = /2) + * APB3 = 120 MHz (D1PPRE = /2) + * APB1 = 120 MHz (D2PPRE1 = /2) + * APB2 = 120 MHz (D2PPRE2 = /2) + * APB4 = 120 MHz (D3PPRE = /2) + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 + | RCC_CLOCKTYPE_D1PCLK1 | RCC_CLOCKTYPE_D3PCLK1; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.SYSCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.AHBCLKDivider = RCC_HCLK_DIV2; + rcc_clk_init.APB3CLKDivider = RCC_APB3_DIV2; + rcc_clk_init.APB1CLKDivider = RCC_APB1_DIV2; + rcc_clk_init.APB2CLKDivider = RCC_APB2_DIV2; + rcc_clk_init.APB4CLKDivider = RCC_APB4_DIV2; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_4) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_l4.c b/src/platforms/stm32/clock_configs/clock_config_l4.c new file mode 100644 index 0000000000..30a446c23e --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_l4.c @@ -0,0 +1,104 @@ +/* + * 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 "stm32l4xx_hal.h" + +/** + * System Clock Configuration for STM32L4 family. + * + * Configures MSI -> PLL -> SYSCLK at 80 MHz (standard L4) + * or 120 MHz (L4R/L4S L4+ devices). + * + * PLL formula: SYSCLK = (source / PLLM) * PLLN / PLLR + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + +#if AVM_CLOCK_HZ == 120000000 + /* L4+ Range 1 Boost for 120 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST) != HAL_OK) { + while (1) { + } + } +#else + /* Range 1 for up to 80 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { + while (1) { + } + } +#endif + +#if AVM_CLOCK_HZ == 80000000 + /* Configure MSI at 4 MHz -> PLL -> 80 MHz + * MSI 4 MHz / 1 * 40 = 160 MHz VCO / 2 = 80 MHz */ + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_MSI; + rcc_osc_init.MSIState = RCC_MSI_ON; + rcc_osc_init.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; + rcc_osc_init.MSIClockRange = RCC_MSIRANGE_6; /* 4 MHz */ + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_MSI; + rcc_osc_init.PLL.PLLM = 1; + rcc_osc_init.PLL.PLLN = 40; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV7; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; +#elif AVM_CLOCK_HZ == 120000000 + /* L4+ (L4R/L4S): MSI 4 MHz / 1 * 60 = 240 MHz VCO / 2 = 120 MHz */ + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_MSI; + rcc_osc_init.MSIState = RCC_MSI_ON; + rcc_osc_init.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; + rcc_osc_init.MSIClockRange = RCC_MSIRANGE_6; /* 4 MHz */ + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_MSI; + rcc_osc_init.PLL.PLLM = 1; + rcc_osc_init.PLL.PLLN = 60; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV7; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; +#else +#error "Unsupported clock frequency for STM32L4" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + +#if AVM_CLOCK_HZ == 80000000 + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_4) != HAL_OK) { + while (1) { + } + } +#elif AVM_CLOCK_HZ == 120000000 + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_5) != HAL_OK) { + while (1) { + } + } +#endif +} diff --git a/src/platforms/stm32/clock_configs/clock_config_l5.c b/src/platforms/stm32/clock_configs/clock_config_l5.c new file mode 100644 index 0000000000..9ad3182f47 --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_l5.c @@ -0,0 +1,78 @@ +/* + * 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 "stm32l5xx_hal.h" + +/** + * System Clock Configuration for STM32L5 family. + * + * Configures MSI -> PLL -> SYSCLK at 110 MHz. + * + * PLL formula: SYSCLK = (MSI / PLLM) * PLLN / PLLR + * 4 MHz / 1 * 55 = 220 MHz VCO / 2 = 110 MHz + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + /* Voltage scaling: Range 0 for 110 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE0) != HAL_OK) { + while (1) { + } + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_MSI; + rcc_osc_init.MSIState = RCC_MSI_ON; + rcc_osc_init.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; + rcc_osc_init.MSIClockRange = RCC_MSIRANGE_6; /* 4 MHz */ + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_MSI; + rcc_osc_init.PLL.PLLM = 1; + rcc_osc_init.PLL.PLLN = 55; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV7; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: + * SYSCLK = 110 MHz + * AHB = 110 MHz + * APB1 = 110 MHz + * APB2 = 110 MHz + * Flash latency: 5 wait states at 110 MHz. + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_5) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_u3.c b/src/platforms/stm32/clock_configs/clock_config_u3.c new file mode 100644 index 0000000000..2390e5ec8f --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_u3.c @@ -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 + */ + +#include "stm32u3xx_hal.h" + +/** + * System Clock Configuration for STM32U3 family. + * + * Configures MSIS (MSIRC0) at 96 MHz as SYSCLK directly. + * STM32U3 has no PLL. MSIRC0 provides 96 MHz with DIV1. + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + __HAL_RCC_PWR_CLK_ENABLE(); + + /* Voltage scaling: Range 1 for up to 96 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { + while (1) { + } + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_MSIS; + rcc_osc_init.MSISState = RCC_MSI_ON; + rcc_osc_init.MSISSource = RCC_MSI_RC0; /* MSIRC0: 96 MHz base */ + rcc_osc_init.MSISDiv = RCC_MSI_DIV1; /* 96 / 1 = 96 MHz */ + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: + * SYSCLK = MSIS = 96 MHz + * AHB = 96 MHz + * APB1 = 96 MHz + * APB2 = 96 MHz + * APB3 = 96 MHz + * Flash latency: 2 wait states at 96 MHz. + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 + | RCC_CLOCKTYPE_PCLK3; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_MSIS; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB3CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_u5.c b/src/platforms/stm32/clock_configs/clock_config_u5.c new file mode 100644 index 0000000000..ad7bdee2ce --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_u5.c @@ -0,0 +1,70 @@ +/* + * 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 "stm32u5xx_hal.h" + +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + /* Enable PWR peripheral clock (not enabled by default on U5) */ + __HAL_RCC_PWR_CLK_ENABLE(); + + /* Voltage scaling: Range 1 for up to 160 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { + while (1) { + } + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_MSI; + rcc_osc_init.MSIState = RCC_MSI_ON; + rcc_osc_init.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; + rcc_osc_init.MSIClockRange = RCC_MSIRANGE_4; /* 4 MHz */ + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_MSI; + rcc_osc_init.PLL.PLLMBOOST = RCC_PLLMBOOST_DIV1; + rcc_osc_init.PLL.PLLM = 1; + rcc_osc_init.PLL.PLLN = 80; + rcc_osc_init.PLL.PLLP = 2; + rcc_osc_init.PLL.PLLQ = 2; + rcc_osc_init.PLL.PLLR = 2; /* 4 * 80 / 2 = 160 MHz */ + rcc_osc_init.PLL.PLLRGE = RCC_PLLVCIRANGE_0; /* 4-8 MHz input */ + rcc_osc_init.PLL.PLLFRACN = 0; + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 + | RCC_CLOCKTYPE_PCLK3; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB3CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_4) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/clock_configs/clock_config_wb.c b/src/platforms/stm32/clock_configs/clock_config_wb.c new file mode 100644 index 0000000000..b418ca13bf --- /dev/null +++ b/src/platforms/stm32/clock_configs/clock_config_wb.c @@ -0,0 +1,90 @@ +/* + * 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 "stm32wbxx_hal.h" + +/** + * System Clock Configuration for STM32WB family. + * + * Configures HSE (32 MHz, required for BLE radio) -> PLL -> SYSCLK at 64 MHz. + * The WB has two CPU cores: M4 (CPU1) and M0+ (CPU2, radio stack). + * + * PLL formula: SYSCLK = (HSE / PLLM) * PLLN / PLLR + * 32 / 2 * 8 / 2 = 64 MHz + * + * CPU2 (M0+) runs at HCLK2 = SYSCLK / 2 = 32 MHz. + * Shared SRAM clock (HCLK4) = SYSCLK / 1 = 64 MHz. + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef rcc_osc_init = { 0 }; + RCC_ClkInitTypeDef rcc_clk_init = { 0 }; + + /* Voltage scaling: Range 1 for 64 MHz */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { + while (1) { + } + } + + rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; + rcc_osc_init.HSEState = RCC_HSE_ON; + rcc_osc_init.PLL.PLLState = RCC_PLL_ON; + rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; + +#if AVM_CLOCK_HZ == 64000000 && (HSE_VALUE == 32000000U) + /* 32MHz / 2 = 16 MHz * 8 = 128 MHz VCO / 2 = 64 MHz */ + rcc_osc_init.PLL.PLLM = 2; + rcc_osc_init.PLL.PLLN = 8; + rcc_osc_init.PLL.PLLP = RCC_PLLP_DIV2; + rcc_osc_init.PLL.PLLQ = RCC_PLLQ_DIV2; + rcc_osc_init.PLL.PLLR = RCC_PLLR_DIV2; +#else +#error "Unsupported clock frequency / HSE combination for STM32WB (HSE must be 32 MHz)" +#endif + + if (HAL_RCC_OscConfig(&rcc_osc_init) != HAL_OK) { + while (1) { + } + } + + /* + * Configure bus clocks: + * HCLK1 (M4 AHB) = SYSCLK / 1 = 64 MHz + * HCLK2 (M0+ AHB) = SYSCLK / 2 = 32 MHz + * HCLK4 (shared) = SYSCLK / 1 = 64 MHz + * PCLK1 (APB1) = HCLK / 1 = 64 MHz + * PCLK2 (APB2) = HCLK / 1 = 64 MHz + * Flash latency: 3 wait states at 64 MHz. + */ + rcc_clk_init.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK + | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 + | RCC_CLOCKTYPE_HCLK2 | RCC_CLOCKTYPE_HCLK4; + rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; + rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; + rcc_clk_init.AHBCLK2Divider = RCC_SYSCLK_DIV2; /* CPU2 M0+ at 32 MHz */ + rcc_clk_init.AHBCLK4Divider = RCC_SYSCLK_DIV1; /* Shared SRAM at 64 MHz */ + + if (HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_3) != HAL_OK) { + while (1) { + } + } +} diff --git a/src/platforms/stm32/cmake/arm-toolchain.cmake b/src/platforms/stm32/cmake/arm-toolchain.cmake index 23ab2d4de3..6d0dfeadbc 100644 --- a/src/platforms/stm32/cmake/arm-toolchain.cmake +++ b/src/platforms/stm32/cmake/arm-toolchain.cmake @@ -27,9 +27,14 @@ find_program(ARM_CC arm-none-eabi-gcc) find_program(ARM_CXX arm-none-eabi-g++) find_program(ARM_OBJCOPY arm-none-eabi-objcopy) find_program(ARM_SIZE arm-none-eabi-size) +find_program(ARM_AR arm-none-eabi-ar) +find_program(ARM_AS arm-none-eabi-as) +find_program(ARM_NM arm-none-eabi-nm) +find_program(ARM_STRIP arm-none-eabi-strip) -mark_as_advanced(ARM_CC ARM_CXX ARM_OBJCOPY ARM_SIZE) +mark_as_advanced(ARM_CC ARM_CXX ARM_OBJCOPY ARM_SIZE ARM_AR ARM_AS ARM_NM ARM_STRIP) SET(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) set(CMAKE_C_COMPILER ${ARM_CC}) set(CMAKE_CXX_COMPILER ${ARM_CXX}) +set(CMAKE_ASM_COMPILER ${ARM_CC}) diff --git a/src/platforms/stm32/cmake/atomvm_dev_config.cmake b/src/platforms/stm32/cmake/atomvm_dev_config.cmake index 3dad744cb1..d3ec20d364 100644 --- a/src/platforms/stm32/cmake/atomvm_dev_config.cmake +++ b/src/platforms/stm32/cmake/atomvm_dev_config.cmake @@ -34,15 +34,29 @@ execute_process( COMMAND "${ESCRIPT}" "${DEVCONFIG_SCRIPT}" "${DEVICE}" "clock" OUTPUT_VARIABLE DEVCONFIG_CLOCK_HZ ) -add_compile_definitions(${DEVCONFIG_CLOCK_HZ}) +add_compile_definitions(AVM_CLOCK_HZ=${DEVCONFIG_CLOCK_HZ}U) execute_process( COMMAND "${ESCRIPT}" "${DEVCONFIG_SCRIPT}" "${DEVICE}" "rom" OUTPUT_VARIABLE DEVCONFIG_FLASH_SIZE ) add_compile_definitions(${DEVCONFIG_FLASH_SIZE}) -## also needs to be set for correct optomization flags to be used. +## also needs to be set for correct optimization flags to be used. set(CMAKE_FLASH_SIZE "${DEVCONFIG_FLASH_SIZE}") +# Query memory sizes for linker script +execute_process( + COMMAND "${ESCRIPT}" "${DEVCONFIG_SCRIPT}" "${DEVICE}" "flash_size_kb" + OUTPUT_VARIABLE DEVCONFIG_FLASH_SIZE_KB + OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND "${ESCRIPT}" "${DEVCONFIG_SCRIPT}" "${DEVICE}" "ram_size_kb" + OUTPUT_VARIABLE DEVCONFIG_RAM_SIZE_KB + OUTPUT_STRIP_TRAILING_WHITESPACE +) +set(FLASH_SIZE_KB "${DEVCONFIG_FLASH_SIZE_KB}") +set(RAM_SIZE_KB "${DEVCONFIG_RAM_SIZE_KB}") + if (AVM_CFG_CONSOLE) set(AVM_CFG_CONSOLE ${AVM_CFG_CONSOLE} CACHE STRING "AtomVM system console uart") set_property(CACHE AVM_CFG_CONSOLE PROPERTY STRINGS CONSOLE_1 CONSOLE_2 CONSOLE_3 CONSOLE_4 CONSOLE_5 CONSOLE_6 CONSOLE_7 CONSOLE_8) @@ -57,17 +71,18 @@ elseif (BOARD) set(BOARD ${BOARD} CACHE STRING "Board variant configuration") set_property(CACHE AVM_CFG_CONSOLE PROPERTY STRINGS CONSOLE_1 CONSOLE_2 CONSOLE_3 CONSOLE_4 CONSOLE_5 CONSOLE_6 CONSOLE_7 CONSOLE_8) else() - add_compile_definitions(CONSOLE_2) - set(AVM_CFG_CONSOLE CONSOLE_2 CACHE STRING "AtomVM system console uart") + add_compile_definitions(CONSOLE_1) + set(AVM_CFG_CONSOLE CONSOLE_1 CACHE STRING "AtomVM system console uart") set_property(CACHE AVM_CFG_CONSOLE PROPERTY STRINGS CONSOLE_1 CONSOLE_2 CONSOLE_3 CONSOLE_4 CONSOLE_5 CONSOLE_6 CONSOLE_7 CONSOLE_8) endif() -message("----------------------------------------") +message("--------Device Configuration Info-------") message(STATUS "Device : ${DEVICE}") if (BOARD) message(STATUS "Board : ${BOARD}") endif() -message("--------Device Configuration Info-------") message(STATUS "Clock Hz : ${DEVCONFIG_CLOCK_HZ}") message(STATUS "Flash Size : ${DEVCONFIG_FLASH_SIZE}") +message(STATUS "Flash KB : ${FLASH_SIZE_KB}") +message(STATUS "RAM KB : ${RAM_SIZE_KB}") message(STATUS "Console : ${AVM_CFG_CONSOLE}") diff --git a/src/platforms/stm32/cmake/compile-flags.cmake b/src/platforms/stm32/cmake/compile-flags.cmake index 759c0a2f71..00907fea96 100644 --- a/src/platforms/stm32/cmake/compile-flags.cmake +++ b/src/platforms/stm32/cmake/compile-flags.cmake @@ -33,17 +33,17 @@ set(COMMON_WARN_FLAGS "${COMMON_WARN_FLAGS} -pedantic -Wall -Wextra") set(C_WARN_FLAGS "${COMMON_WARN_FLAGS}") set(CXX_WARN_FLAGS "${COMMON_WARN_FLAGS}") -# Use C and C++ compiler optimizatons for size and speed. +# Use C and C++ compiler optimizations for size and speed. if (${CMAKE_FLASH_SIZE} STREQUAL "ROM_512K") -set(OPTIMIZE_FLAG "-Os") -set(LINKER_FLAGS "${LINKER_FLAGS} -specs=nano.specs") + set(OPTIMIZE_FLAG "-Os") else() -set(OPTIMIZE_FLAG "-O2") + set(OPTIMIZE_FLAG "-O2") endif() # Pass them back to the CMake variable -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_WARN_FLAGS} ${OPTIMIZE_FLAG}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CXX_WARN_FLAGS} ${OPTIMIZE_FLAG}") +# -ffunction-sections -fdata-sections enable --gc-sections to remove unused code +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_WARN_FLAGS} ${OPTIMIZE_FLAG} -ffunction-sections -fdata-sections") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CXX_WARN_FLAGS} ${OPTIMIZE_FLAG} -ffunction-sections -fdata-sections") if (LOG_VERBOSE) message("--------------Build Flags---------------") diff --git a/src/platforms/stm32/cmake/libopencm3.cmake b/src/platforms/stm32/cmake/libopencm3.cmake deleted file mode 100644 index 6e0ad658c6..0000000000 --- a/src/platforms/stm32/cmake/libopencm3.cmake +++ /dev/null @@ -1,194 +0,0 @@ -# -# This file is part of AtomVM. -# -# Copyright 2018 Riccardo Binetti -# -# 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 -# - -# Utility - -function(JOIN VALUES GLUE OUTPUT) - string(REGEX REPLACE "([^\\]|^);" "\\1${GLUE}" _TMP_STR "${VALUES}") - string(REGEX REPLACE "[\\](.)" "\\1" _TMP_STR "${_TMP_STR}") #fixes escaping - set(${OUTPUT} "${_TMP_STR}" PARENT_SCOPE) -endfunction() - -# ----------- - -# LibOpenCM3 Stuff -if (NOT LIBOPENCM3_DIR) - set(LIBOPENCM3_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libopencm3/) -endif () - -if (NOT EXISTS ${LIBOPENCM3_DIR}/Makefile) - message(FATAL_ERROR "libopencm3 does not exist or LIBOPENCM3_DIR not set to checkout " ${LIBOPENCM3_DIR}) -endif () - -add_custom_target( - libopencm3 make - WORKING_DIRECTORY ${LIBOPENCM3_DIR} -) -set(OPENCM3_LIB ${LIBOPENCM3_DIR}/lib) -set(OPENCM3_INCLUDE ${LIBOPENCM3_DIR}/include) - -# Generate linker information for device, based on libopencm3/mk/genlink-config.mk -if (NOT DEVICE) - message(FATAL_ERROR "No DEVICE specified for linker script generator") -endif () - -find_program(PYTHON python) -if (NOT PYTHON) - message(FATAL_ERROR "python is required to generate the linker script, please install it.") -endif () -mark_as_advanced(PYTHON) - -set(GENLINK_SCRIPT "${LIBOPENCM3_DIR}/scripts/genlink.py") -set(DEVICES_DATA "${LIBOPENCM3_DIR}/ld/devices.data") -execute_process( - COMMAND "${PYTHON}" "${GENLINK_SCRIPT}" "${DEVICES_DATA}" "${DEVICE}" "FAMILY" - OUTPUT_VARIABLE GENLINK_FAMILY -) -execute_process( - COMMAND "${PYTHON}" "${GENLINK_SCRIPT}" "${DEVICES_DATA}" "${DEVICE}" "SUBFAMILY" - OUTPUT_VARIABLE GENLINK_SUBFAMILY -) -execute_process( - COMMAND "${PYTHON}" "${GENLINK_SCRIPT}" "${DEVICES_DATA}" "${DEVICE}" "CPU" - OUTPUT_VARIABLE GENLINK_CPU -) -execute_process( - COMMAND "${PYTHON}" "${GENLINK_SCRIPT}" "${DEVICES_DATA}" "${DEVICE}" "FPU" - OUTPUT_VARIABLE GENLINK_FPU -) -execute_process( - COMMAND "${PYTHON}" "${GENLINK_SCRIPT}" "${DEVICES_DATA}" "${DEVICE}" "CPPFLAGS" - OUTPUT_VARIABLE GENLINK_CPPFLAGS -) -execute_process( - COMMAND "${PYTHON}" "${GENLINK_SCRIPT}" "${DEVICES_DATA}" "${DEVICE}" "DEFS" - OUTPUT_VARIABLE GENLINK_DEFS -) - -message("-----------Device Linker Info-----------") -message(STATUS "Family : ${GENLINK_FAMILY}") -message(STATUS "Sub-family : ${GENLINK_SUBFAMILY}") -message(STATUS "CPU : ${GENLINK_CPU}") -message(STATUS "FPU : ${GENLINK_FPU}") -message(STATUS "CPP Flags : ${GENLINK_CPPFLAGS}") -message(STATUS "Linker Defs : ${GENLINK_DEFS}") - -# Generate flags -set(ARCH_FLAGS "-mcpu=${GENLINK_CPU}") - -# Check CPU -set(CORTEX_CPU cortex-m0 cortex-m0plus cortex-m3 cortex-m4 cortex-m7) -list(FILTER CORTEX_CPU INCLUDE REGEX ${GENLINK_CPU}) -if (GENLINK_CPU STREQUAL CORTEX_CPU) - list(APPEND ARCH_FLAGS "-mthumb -mthumb-interwork") -endif () - -# Check FPU -if (GENLINK_FPU STREQUAL "soft") - list(APPEND ARCH_FLAGS "-msoft-float") -elseif (GENLINK_FPU STREQUAL "hard-fpv4-sp-d16") - list(APPEND ARCH_FLAGS "-mfloat-abi=hard -mfpu=fpv4-sp-d16") -elseif (GENLINK_FPU STREQUAL "hard-fpv5-sp-d16") - list(APPEND ARCH_FLAGS "-mfloat-abi=hard -mfpu=fpv5-sp-d16") -else () - message(WARNING "No match for the FPU flags") -endif () - -# Check family -if (NOT GENLINK_FAMILY) - message(WARNING "${DEVICE} not found in ${DEVICES_DATA}") -endif () - -# Linker stuff -set(LINKER_SCRIPT "generated.${DEVICE}.ld") - -if (EXISTS "${LIBOPENCM3_DIR}/lib/libopencm3_${GENLINK_FAMILY}.a") - set(OPENCM3_RUNTIME "opencm3_${GENLINK_FAMILY}") -else () - if (EXISTS "${LIBOPENCM3_DIR}/lib/libopencm3_${GENLINK_SUBFAMILY}.a") - set(OPENCM3_RUNTIME "opencm3_${GENLINK_SUBFAMILY}") - else () - message(WARNING "${LIBOPENCM3_DIR}/lib/libopencm3_${GENLINK_FAMILY}. A library variant for the selected device does not exist.") - endif () -endif () - -# ARCH_FLAGS and GENLINK_DEFS has to be passed as a list here -string(REPLACE " " ";" GENLINK_DEFS ${GENLINK_DEFS}) -# Get rid of any spaces and turn the thing into a list -JOIN("${ARCH_FLAGS}" " " ARCH_FLAGS) -string(REPLACE " " ";" ARCH_FLAGS ${ARCH_FLAGS}) -# ------------------ -execute_process( - COMMAND ${ARM_CXX} ${ARCH_FLAGS} ${GENLINK_DEFS} "-P" "-E" "${LIBOPENCM3_DIR}/ld/linker.ld.S" - OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/${LINKER_SCRIPT}" -) -message("-----------Target Specific Info---------") -message(STATUS "Generated Linker File : ${CMAKE_CURRENT_BINARY_DIR}/${LINKER_SCRIPT}") - -# ARCH_FLAGS has to be passed as a string here -JOIN("${ARCH_FLAGS}" " " ARCH_FLAGS) -# Set linker flags -set(LINKER_FLAGS "${LINKER_FLAGS} -specs=nosys.specs -nostartfiles -Wl,--undefined,_printf_float -Wl,--undefined,_scanf_float -T${CMAKE_CURRENT_BINARY_DIR}/${LINKER_SCRIPT} ${ARCH_FLAGS}") -message(STATUS "Linker Flags : ${LINKER_FLAGS}") - -# Compiler flags -set(TARGET_SPECIFIC_FLAGS "${GENLINK_CPPFLAGS} ${ARCH_FLAGS}") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_SPECIFIC_FLAGS}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TARGET_SPECIFIC_FLAGS}") - -# Use linker flags to detect symbols -set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE) -set(CMAKE_REQUIRED_FLAGS ${LINKER_FLAGS}) - -message(STATUS "Target Specific Flags : ${TARGET_SPECIFIC_FLAGS}") - -# Replace `add_executable` with custom macro with same name that adds libopencm3 as a linking target -macro(add_executable _name) - # invoke built-in add_executable - if ("${ARGV1}" STREQUAL "") - message(FATAL_ERROR "No source files added to executable") - endif () - _add_executable(${ARGV}) - - if (TARGET ${_name} AND NOT ${_name} MATCHES ".*Doxygen.*") - # Set target properties - set_target_properties(${_name} PROPERTIES LINK_FLAGS ${LINKER_FLAGS}) - set_target_properties(${_name} PROPERTIES OUTPUT_NAME "${PROJECT_EXECUTABLE}") - - # Set output file locations - string(REGEX MATCH "^(.*)\\.[^.]*$" dummy "${_name}") - set(bin ${CMAKE_MATCH_1}.bin) - set(elf ${_name}) - set(bin_out ${CMAKE_MATCH_1}.bin) - get_target_property(elf_in ${PROJECT_EXECUTABLE} OUTPUT_NAME) - - # Add target - add_custom_target( - ${bin} ALL - COMMAND ${ARM_OBJCOPY} -Obinary ${elf_in} ${bin_out} - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - DEPENDS ${elf_in} - ) - - message("------------Output Locations------------") - message(STATUS "BIN: ${CMAKE_BINARY_DIR}/${bin_out}") - message(STATUS "ELF: ${CMAKE_BINARY_DIR}/${elf_in}") - endif () -endmacro() diff --git a/src/platforms/stm32/cmake/picolibc.cmake b/src/platforms/stm32/cmake/picolibc.cmake new file mode 100644 index 0000000000..4a8768c92a --- /dev/null +++ b/src/platforms/stm32/cmake/picolibc.cmake @@ -0,0 +1,122 @@ +# +# 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 +# + +# Build picolibc from source using meson, configured without wide char/multibyte +# support but with long-long and C99 format support. + +include(ExternalProject) + +find_program(MESON_EXECUTABLE meson REQUIRED) +find_program(NINJA_EXECUTABLE ninja REQUIRED) + +# Allow using a local picolibc source checkout via environment variable or CMake variable. +if (DEFINED ENV{PICOLIBC_PATH} AND (NOT PICOLIBC_PATH)) + set(PICOLIBC_PATH $ENV{PICOLIBC_PATH}) + message("Using PICOLIBC_PATH from environment ('${PICOLIBC_PATH}')") +endif () + +set(PICOLIBC_VERSION "1.8.11") +set(PICOLIBC_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/_deps/picolibc") +if (PICOLIBC_PATH) + get_filename_component(PICOLIBC_SOURCE_DIR "${PICOLIBC_PATH}" REALPATH) +else() + set(PICOLIBC_SOURCE_DIR "${PICOLIBC_PREFIX}/src/picolibc") +endif() +set(PICOLIBC_BUILD_DIR "${PICOLIBC_PREFIX}/build") +set(PICOLIBC_INSTALL_DIR "${PICOLIBC_PREFIX}/install") + +# Build the arch flags string for the meson cross-file c_args +# STM32_ARCH_FLAGS is a CMake list; we need to format it as a meson array +set(_meson_c_args "") +foreach(_flag ${STM32_ARCH_FLAGS}) + if (_meson_c_args) + string(APPEND _meson_c_args ", ") + endif() + string(APPEND _meson_c_args "'${_flag}'") +endforeach() + +# Generate meson cross-file +set(PICOLIBC_CROSS_FILE "${PICOLIBC_PREFIX}/cross-file.txt") +file(WRITE "${PICOLIBC_CROSS_FILE}" "\ +[binaries] +c = '${ARM_CC}' +ar = '${ARM_AR}' +as = '${ARM_AS}' +nm = '${ARM_NM}' +strip = '${ARM_STRIP}' + +[host_machine] +system = 'none' +cpu_family = 'arm' +cpu = '${STM32_CPU}' +endian = 'little' + +[built-in options] +c_args = [${_meson_c_args}] +") + +if (PICOLIBC_PATH) + set(_picolibc_download_args DOWNLOAD_COMMAND "") + message(STATUS "Using local picolibc source: ${PICOLIBC_SOURCE_DIR}") +else() + set(_picolibc_download_args + URL "https://github.com/picolibc/picolibc/releases/download/${PICOLIBC_VERSION}/picolibc-${PICOLIBC_VERSION}.tar.xz" + URL_HASH SHA256=b4671ddeecbe427b77fa9a863be92c74a9059e0373a5b31343c791ff2337db64 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + LOG_DOWNLOAD TRUE + ) + if (NOT EXISTS "${PICOLIBC_SOURCE_DIR}/meson.build") + message("Downloading and building picolibc ${PICOLIBC_VERSION}") + endif() +endif() + +ExternalProject_Add(picolibc + ${_picolibc_download_args} + PREFIX "${PICOLIBC_PREFIX}" + SOURCE_DIR "${PICOLIBC_SOURCE_DIR}" + BINARY_DIR "${PICOLIBC_BUILD_DIR}" + INSTALL_DIR "${PICOLIBC_INSTALL_DIR}" + CONFIGURE_COMMAND ${MESON_EXECUTABLE} setup + --cross-file ${PICOLIBC_CROSS_FILE} + --prefix + -Dmultilib=false + -Dformat-default=l + -Dio-c99-formats=true + -Dio-long-long=true + -Dmb-capable=false + -Dpicocrt=false + -Dsemihost=false + -Dtests=false + -Dposix-console=true + -Dsingle-thread=true + -Dthread-local-storage=false + -Dnewlib-global-errno=true + -Dspecsdir=none + + + BUILD_COMMAND ${NINJA_EXECUTABLE} -C + INSTALL_COMMAND ${NINJA_EXECUTABLE} -C install + BUILD_BYPRODUCTS + "${PICOLIBC_INSTALL_DIR}/lib/libc.a" +) + +# Export variables for other CMake files to use +set(PICOLIBC_INCLUDE_DIR "${PICOLIBC_INSTALL_DIR}/include" CACHE INTERNAL "") +set(PICOLIBC_LIB_DIR "${PICOLIBC_INSTALL_DIR}/lib" CACHE INTERNAL "") diff --git a/src/platforms/stm32/cmake/stm32_device.cmake b/src/platforms/stm32/cmake/stm32_device.cmake new file mode 100644 index 0000000000..471ac3691d --- /dev/null +++ b/src/platforms/stm32/cmake/stm32_device.cmake @@ -0,0 +1,127 @@ +# +# 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 +# + +# Parse DEVICE string to determine STM32 family, CPU flags, HAL defines, memory sizes. +# Input: DEVICE (e.g. "stm32f401ccu6", "stm32h743vit6", "stm32u585ait6q", "stm32wb55rg", "stm32h562rgt6") +# Output variables: +# STM32_FAMILY - full family string (e.g. "stm32f4xx") +# STM32_FAMILY_UPPER - upper case (e.g. "STM32F4xx") +# STM32_FAMILY_UC - all upper case (e.g. "STM32F4XX") +# STM32_FAMILY_SHORT - two-char family (e.g. "f4") +# STM32_CPU - CPU core (e.g. "cortex-m4") +# STM32_FPU - FPU type (e.g. "fpv4-sp-d16") or empty +# STM32_ARCH_FLAGS - list of architecture flags + +if (NOT DEVICE) + message(FATAL_ERROR "No DEVICE specified") +endif() + +# Normalize to lowercase +string(TOLOWER ${DEVICE} DEVICE_LOWER) + +# Extract family: e.g. stm32f401ccu6 -> "f4" +string(SUBSTRING ${DEVICE_LOWER} 5 2 STM32_FAMILY_SHORT) + +# Family data: CPU, FPU, float ABI +# Format: set(_FAM_ ";;") +set(_FAM_f2 "cortex-m3;;soft") +set(_FAM_f4 "cortex-m4;fpv4-sp-d16;hard") +set(_FAM_f7 "cortex-m7;fpv5-d16;hard") +set(_FAM_g0 "cortex-m0plus;;soft") +set(_FAM_g4 "cortex-m4;fpv4-sp-d16;hard") +set(_FAM_h5 "cortex-m33;fpv5-sp-d16;hard") +set(_FAM_h7 "cortex-m7;fpv5-d16;hard") +set(_FAM_l4 "cortex-m4;fpv4-sp-d16;hard") +set(_FAM_l5 "cortex-m33;fpv5-sp-d16;hard") +set(_FAM_u3 "cortex-m33;fpv5-sp-d16;hard") +set(_FAM_u5 "cortex-m33;fpv5-sp-d16;hard") +set(_FAM_wb "cortex-m4;fpv4-sp-d16;hard") + +if (NOT DEFINED _FAM_${STM32_FAMILY_SHORT}) + message(FATAL_ERROR "Unsupported STM32 family: ${STM32_FAMILY_SHORT} (from device ${DEVICE})") +endif() + +# Unpack family data +list(GET _FAM_${STM32_FAMILY_SHORT} 0 STM32_CPU) +list(GET _FAM_${STM32_FAMILY_SHORT} 1 STM32_FPU) +list(GET _FAM_${STM32_FAMILY_SHORT} 2 STM32_FLOAT_ABI) + +# Derive family name variants from the short code +string(TOUPPER ${STM32_FAMILY_SHORT} _fam_upper) +set(STM32_FAMILY "stm32${STM32_FAMILY_SHORT}xx") +set(STM32_FAMILY_UPPER "STM32${_fam_upper}xx") +set(STM32_FAMILY_UC "STM32${_fam_upper}XX") + +# Build architecture flags +set(STM32_ARCH_FLAGS "-mcpu=${STM32_CPU}" "-mthumb" "-mthumb-interwork") +if (STM32_FPU) + list(APPEND STM32_ARCH_FLAGS "-mfloat-abi=${STM32_FLOAT_ABI}" "-mfpu=${STM32_FPU}") +else() + list(APPEND STM32_ARCH_FLAGS "-msoft-float") +endif() + +# Apply architecture flags to compiler +string(REPLACE ";" " " _arch_flags_str "${STM32_ARCH_FLAGS}") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_arch_flags_str}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_arch_flags_str}") + +# Add family-level define for stm32_hal_platform.h header selection +add_compile_definitions(${STM32_FAMILY_UC}) +add_compile_definitions(USE_HAL_DRIVER) + +# Linker script variables — defaults for most families +set(LINKER_RAM_ORIGIN "0x20000000") +set(LINKER_RAM_SIZE_OVERRIDE "") +set(LINKER_EXTRA_MEMORY "") +set(LINKER_EXTRA_SYMBOLS "") +set(LINKER_EXTRA_SECTIONS "") + +# Family-specific linker overrides +if (STM32_FAMILY_SHORT STREQUAL "f7") + set(LINKER_RAM_ORIGIN "0x20010000") + set(LINKER_EXTRA_MEMORY "DTCMRAM (rw) : ORIGIN = 0x20000000, LENGTH = 64K") +elseif (STM32_FAMILY_SHORT STREQUAL "h7") + set(LINKER_RAM_ORIGIN "0x24000000") + set(LINKER_EXTRA_MEMORY "DTCMRAM (rw) : ORIGIN = 0x20000000, LENGTH = 128K") +elseif (STM32_FAMILY_SHORT STREQUAL "u3") + set(LINKER_EXTRA_SYMBOLS "_sstack = _estack - _Min_Stack_Size;") +elseif (STM32_FAMILY_SHORT STREQUAL "wb") + set(LINKER_RAM_SIZE_OVERRIDE "192") + set(LINKER_EXTRA_MEMORY "RAM_SHARED (rwx) : ORIGIN = 0x20030000, LENGTH = 10K") + set(LINKER_EXTRA_SECTIONS [=[ + /* Mailbox shared memory (SRAM2a) for CPU1/CPU2 IPC */ + _siMB_MEM2 = LOADADDR(.MB_MEM2); + + .MB_MEM2 : + { + . = ALIGN(4); + _sMB_MEM2 = .; + *(MB_MEM2) + . = ALIGN(4); + _eMB_MEM2 = .; + } >RAM_SHARED AT> FLASH]=]) +endif() + +message("-----------Device Info-----------") +message(STATUS "Device : ${DEVICE}") +message(STATUS "Family : ${STM32_FAMILY}") +message(STATUS "CPU : ${STM32_CPU}") +message(STATUS "FPU : ${STM32_FPU}") +message(STATUS "Arch Flags : ${_arch_flags_str}") diff --git a/src/platforms/stm32/cmake/stm32_hal_conf.h.in b/src/platforms/stm32/cmake/stm32_hal_conf.h.in new file mode 100644 index 0000000000..94f3a6ed4b --- /dev/null +++ b/src/platforms/stm32/cmake/stm32_hal_conf.h.in @@ -0,0 +1,215 @@ +/* + * 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 + * + * Template for family-specific HAL configuration. + * STM32_FAMILY and STM32_FAMILY_UPPER are substituted by CMake configure_file(). + * Family-dependent values use preprocessor #if defined() guards. + */ + +#ifndef __@STM32_FAMILY_UPPER@_HAL_CONF_H +#define __@STM32_FAMILY_UPPER@_HAL_CONF_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------- Module Selection --------------------------- */ +#define HAL_MODULE_ENABLED +#define HAL_RCC_MODULE_ENABLED +#define HAL_GPIO_MODULE_ENABLED +#define HAL_CORTEX_MODULE_ENABLED +#define HAL_UART_MODULE_ENABLED +#define HAL_FLASH_MODULE_ENABLED +#define HAL_PWR_MODULE_ENABLED +#define HAL_EXTI_MODULE_ENABLED +#define HAL_DMA_MODULE_ENABLED +#if defined(STM32H5XX) || defined(STM32L5XX) || defined(STM32U3XX) || defined(STM32U5XX) +#define HAL_ICACHE_MODULE_ENABLED +#endif + +/* ------------------------- Oscillator Values -------------------------- */ +#if !defined(HSE_VALUE) +#if defined(STM32F4XX) +#define HSE_VALUE 8000000U /* Default: 8 MHz for Discovery/Nucleo boards */ +#elif defined(STM32U3XX) || defined(STM32U5XX) +#define HSE_VALUE 16000000U +#elif defined(STM32WBXX) +#define HSE_VALUE 32000000U /* 32 MHz HSE required for BLE radio */ +#else +#define HSE_VALUE 8000000U +#endif +#endif + +#if !defined(HSE_STARTUP_TIMEOUT) +#define HSE_STARTUP_TIMEOUT 100U +#endif + +#if !defined(HSI_VALUE) +#if defined(STM32H5XX) || defined(STM32H7XX) +#define HSI_VALUE 64000000U +#else +#define HSI_VALUE 16000000U +#endif +#endif + +#if defined(STM32H5XX) || defined(STM32H7XX) +#if !defined(CSI_VALUE) +#define CSI_VALUE 4000000U +#endif +#endif + +#if defined(STM32G4XX) || defined(STM32H5XX) || defined(STM32L4XX) || defined(STM32L5XX) \ + || defined(STM32U3XX) || defined(STM32U5XX) +#if !defined(HSI48_VALUE) +#define HSI48_VALUE 48000000U +#endif +#endif + +#if defined(STM32L4XX) || defined(STM32L5XX) || defined(STM32U3XX) || defined(STM32U5XX) \ + || defined(STM32WBXX) +#if !defined(MSI_VALUE) +#define MSI_VALUE 4000000U +#endif +#endif + +#if defined(STM32WBXX) +#if !defined(LSI1_VALUE) +#define LSI1_VALUE 32000U +#endif +#if !defined(LSI2_VALUE) +#define LSI2_VALUE 32000U +#endif +#else +#if !defined(LSI_VALUE) +#define LSI_VALUE 32000U +#endif +#endif + +#if !defined(LSE_VALUE) +#define LSE_VALUE 32768U +#endif + +#if !defined(LSE_STARTUP_TIMEOUT) +#define LSE_STARTUP_TIMEOUT 5000U +#endif + +#if !defined(EXTERNAL_CLOCK_VALUE) +#define EXTERNAL_CLOCK_VALUE 12288000U +#endif + +#if defined(STM32G0XX) +#if !defined(EXTERNAL_I2S1_CLOCK_VALUE) +#define EXTERNAL_I2S1_CLOCK_VALUE 12288000U +#endif +#if !defined(EXTERNAL_I2S2_CLOCK_VALUE) +#define EXTERNAL_I2S2_CLOCK_VALUE 12288000U +#endif +#endif + +#if defined(STM32G4XX) || defined(STM32L4XX) || defined(STM32L5XX) || defined(STM32U3XX) \ + || defined(STM32U5XX) || defined(STM32WBXX) +#if !defined(EXTERNAL_SAI1_CLOCK_VALUE) +#define EXTERNAL_SAI1_CLOCK_VALUE 48000U +#endif +#endif + +#if defined(STM32L4XX) || defined(STM32L5XX) +#if !defined(EXTERNAL_SAI2_CLOCK_VALUE) +#define EXTERNAL_SAI2_CLOCK_VALUE 48000U +#endif +#endif + +/* ------------------------- System Configuration ----------------------- */ +#define VDD_VALUE 3300U +#define TICK_INT_PRIORITY ((1UL<<__NVIC_PRIO_BITS) - 1UL) +#define USE_RTOS 0U + +#if defined(STM32H7XX) +#define USE_SD_TRANSCEIVER 0U +#endif + +#if defined(STM32F2XX) || defined(STM32F4XX) || defined(STM32F7XX) || defined(STM32G0XX) \ + || defined(STM32G4XX) || defined(STM32U3XX) || defined(STM32U5XX) || defined(STM32WBXX) +#define PREFETCH_ENABLE 1U +#elif defined(STM32L4XX) || defined(STM32L5XX) +#define PREFETCH_ENABLE 0U +#endif + +#if defined(STM32F7XX) +#define ART_ACCELERATOR_ENABLE 1U +#endif + +#if defined(STM32F2XX) || defined(STM32F4XX) || defined(STM32F7XX) || defined(STM32G0XX) \ + || defined(STM32G4XX) || defined(STM32L4XX) || defined(STM32WBXX) +#define INSTRUCTION_CACHE_ENABLE 1U +#define DATA_CACHE_ENABLE 1U +#endif + +/* ------------------------- Include HAL modules ------------------------ */ +#ifdef HAL_RCC_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_rcc.h" +#endif + +#ifdef HAL_EXTI_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_exti.h" +#endif + +#ifdef HAL_GPIO_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_gpio.h" +#endif + +#ifdef HAL_DMA_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_dma.h" +#endif + +#ifdef HAL_CORTEX_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_cortex.h" +#endif + +#ifdef HAL_FLASH_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_flash.h" +#endif + +#ifdef HAL_PWR_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_pwr.h" +#endif + +#ifdef HAL_UART_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_uart.h" +#endif + +#ifdef HAL_ICACHE_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_icache.h" +#endif + +/* ------------------------- Assert Configuration ----------------------- */ +/* #define USE_FULL_ASSERT 1U */ + +#ifdef USE_FULL_ASSERT +#define assert_param(expr) ((expr) ? (void) 0U : assert_failed((uint8_t *) __FILE__, __LINE__)) +void assert_failed(uint8_t *file, uint32_t line); +#else +#define assert_param(expr) ((void) 0U) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __@STM32_FAMILY_UPPER@_HAL_CONF_H */ diff --git a/src/platforms/stm32/cmake/stm32_linker.ld.in b/src/platforms/stm32/cmake/stm32_linker.ld.in new file mode 100644 index 0000000000..fb3f310998 --- /dev/null +++ b/src/platforms/stm32/cmake/stm32_linker.ld.in @@ -0,0 +1,146 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + * + * STM32 linker script template. + * Variables substituted by CMake configure_file(): + * FLASH_SIZE_KB, LINKER_RAM_ORIGIN, LINKER_RAM_SIZE, + * LINKER_EXTRA_MEMORY, LINKER_EXTRA_SYMBOLS, LINKER_EXTRA_SECTIONS + */ + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = @FLASH_SIZE_KB@K + @LINKER_EXTRA_MEMORY@ + RAM (rwx) : ORIGIN = @LINKER_RAM_ORIGIN@, LENGTH = @LINKER_RAM_SIZE@ +} + +/* Entry point */ +ENTRY(Reset_Handler) + +/* Highest address of the user mode stack */ +_estack = ORIGIN(RAM) + LENGTH(RAM); +_stack = _estack; +@LINKER_EXTRA_SYMBOLS@ + +/* Generate a link error if heap and stack don't fit into RAM */ +_Min_Heap_Size = 0x200; +_Min_Stack_Size = 0x400; + +SECTIONS +{ + /* Interrupt vector table */ + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) + . = ALIGN(4); + } >FLASH + + /* Program code */ + .text : + { + . = ALIGN(4); + *(.text) + *(.text*) + *(.glue_7) + *(.glue_7t) + *(.eh_frame) + + KEEP(*(.init)) + KEEP(*(.fini)) + + . = ALIGN(4); + _etext = .; + } >FLASH + + /* Read-only data */ + .rodata : + { + . = ALIGN(4); + *(.rodata) + *(.rodata*) + . = ALIGN(4); + } >FLASH + + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } >FLASH + + .ARM : + { + __exidx_start = .; + *(.ARM.exidx*) + __exidx_end = .; + } >FLASH + + .preinit_array : + { + PROVIDE_HIDDEN(__preinit_array_start = .); + PROVIDE_HIDDEN(__bothinit_array_start = .); + KEEP(*(.preinit_array*)) + PROVIDE_HIDDEN(__preinit_array_end = .); + } >FLASH + + .init_array : + { + PROVIDE_HIDDEN(__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array*)) + PROVIDE_HIDDEN(__init_array_end = .); + PROVIDE_HIDDEN(__bothinit_array_end = .); + } >FLASH + + .fini_array : + { + PROVIDE_HIDDEN(__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) + KEEP(*(.fini_array*)) + PROVIDE_HIDDEN(__fini_array_end = .); + } >FLASH + + _sidata = LOADADDR(.data); + + /* Initialized data */ + .data : + { + . = ALIGN(4); + _sdata = .; + *(.data) + *(.data*) + . = ALIGN(4); + _edata = .; + } >RAM AT> FLASH + + /* Uninitialized data */ + .bss : + { + _sbss = .; + __bss_start__ = _sbss; + *(.bss) + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + __bss_end__ = _ebss; + } >RAM + + /* User_heap_stack section: check minimum heap and stack */ + ._user_heap_stack : + { + . = ALIGN(8); + PROVIDE(end = .); + PROVIDE(_end = .); + . = . + _Min_Heap_Size; + . = . + _Min_Stack_Size; + . = ALIGN(8); + } >RAM + + @LINKER_EXTRA_SECTIONS@ + + .ARM.attributes 0 : { *(.ARM.attributes) } +} diff --git a/src/platforms/stm32/cmake/stm32_sdk.cmake b/src/platforms/stm32/cmake/stm32_sdk.cmake new file mode 100644 index 0000000000..c5a65981ea --- /dev/null +++ b/src/platforms/stm32/cmake/stm32_sdk.cmake @@ -0,0 +1,206 @@ +# +# 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 +# + +# Fetch STM32 SDK components via FetchContent: +# 1. CMSIS core (ARM CMSIS headers, shared across all families) +# 2. CMSIS device (family-specific device headers + startup files) +# 3. HAL driver (family-specific HAL + LL drivers) + +include(FetchContent) + +# Allow using local source checkouts via environment variables or CMake variables. +# CMSIS Core is shared across all families; CMSIS Device and HAL Driver are +# family-specific, so env var names include the family (e.g. STM32_CMSIS_DEVICE_F4_PATH). +if (DEFINED ENV{STM32_CMSIS_CORE_PATH} AND (NOT FETCHCONTENT_SOURCE_DIR_CMSIS_CORE)) + set(FETCHCONTENT_SOURCE_DIR_CMSIS_CORE $ENV{STM32_CMSIS_CORE_PATH}) + message("Using STM32_CMSIS_CORE_PATH from environment ('${FETCHCONTENT_SOURCE_DIR_CMSIS_CORE}')") +endif () + +string(TOUPPER ${STM32_FAMILY_SHORT} _FAMILY_UPPER) + +set(_cmsis_device_env_var "STM32_CMSIS_DEVICE_${_FAMILY_UPPER}_PATH") +if (DEFINED ENV{${_cmsis_device_env_var}} AND (NOT FETCHCONTENT_SOURCE_DIR_CMSIS_DEVICE)) + set(FETCHCONTENT_SOURCE_DIR_CMSIS_DEVICE "$ENV{${_cmsis_device_env_var}}") + message("Using ${_cmsis_device_env_var} from environment ('${FETCHCONTENT_SOURCE_DIR_CMSIS_DEVICE}')") +endif () + +set(_hal_driver_env_var "STM32_HAL_DRIVER_${_FAMILY_UPPER}_PATH") +if (DEFINED ENV{${_hal_driver_env_var}} AND (NOT FETCHCONTENT_SOURCE_DIR_HAL_DRIVER)) + set(FETCHCONTENT_SOURCE_DIR_HAL_DRIVER "$ENV{${_hal_driver_env_var}}") + message("Using ${_hal_driver_env_var} from environment ('${FETCHCONTENT_SOURCE_DIR_HAL_DRIVER}')") +endif () + +# CMSIS Core - common ARM headers +FetchContent_Declare( + cmsis_core + GIT_REPOSITORY https://github.com/STMicroelectronics/cmsis_core.git + GIT_TAG v5.9.0 + GIT_SHALLOW TRUE +) +FetchContent_GetProperties(cmsis_core) +if (NOT cmsis_core_POPULATED) + message("Downloading CMSIS Core headers") +endif() +FetchContent_MakeAvailable(cmsis_core) + +# Family-specific SDK versions. +# Format: set(_SDK_ ";") +# Repos follow the naming convention: +# CMSIS device: https://github.com/STMicroelectronics/cmsis_device_.git +# HAL driver: https://github.com/STMicroelectronics/stm32xx_hal_driver.git +# Exceptions are handled with _SDK__CMSIS_REPO / _SDK__HAL_REPO overrides. +set(_SDK_f2 "v2.2.6;v1.2.9") +set(_SDK_f4 "v2.6.10;v1.8.3") +set(_SDK_f7 "v1.2.10;v1.3.3") +set(_SDK_g0 "v1.4.4;v1.4.6") +set(_SDK_g4 "v1.2.6;v1.2.6") +set(_SDK_h5 "v1.2.0;v1.2.0") +set(_SDK_h7 "v1.10.4;v1.11.3") +set(_SDK_l4 "v1.7.5;v1.13.6") +set(_SDK_l5 "v1.0.7;v1.0.7") +set(_SDK_u3 "v1.3.0;v1.2.0") +set(_SDK_u5 "v1.4.0;v1.6.0") +set(_SDK_wb "v1.12.2;v1.14.3") + +# U3 uses hyphens instead of underscores in repo names +set(_SDK_u3_CMSIS_REPO "https://github.com/STMicroelectronics/cmsis-device-u3.git") +set(_SDK_u3_HAL_REPO "https://github.com/STMicroelectronics/stm32u3xx-hal-driver.git") + +if (NOT DEFINED _SDK_${STM32_FAMILY_SHORT}) + message(FATAL_ERROR "No SDK configuration for family: ${STM32_FAMILY_SHORT}") +endif() + +list(GET _SDK_${STM32_FAMILY_SHORT} 0 _CMSIS_DEVICE_TAG) +list(GET _SDK_${STM32_FAMILY_SHORT} 1 _HAL_DRIVER_TAG) + +# Use override repos if defined, otherwise use naming convention +if (DEFINED _SDK_${STM32_FAMILY_SHORT}_CMSIS_REPO) + set(_CMSIS_DEVICE_REPO "${_SDK_${STM32_FAMILY_SHORT}_CMSIS_REPO}") +else() + set(_CMSIS_DEVICE_REPO "https://github.com/STMicroelectronics/cmsis_device_${STM32_FAMILY_SHORT}.git") +endif() + +if (DEFINED _SDK_${STM32_FAMILY_SHORT}_HAL_REPO) + set(_HAL_DRIVER_REPO "${_SDK_${STM32_FAMILY_SHORT}_HAL_REPO}") +else() + set(_HAL_DRIVER_REPO "https://github.com/STMicroelectronics/stm32${STM32_FAMILY_SHORT}xx_hal_driver.git") +endif() + +FetchContent_Declare( + cmsis_device + GIT_REPOSITORY ${_CMSIS_DEVICE_REPO} + GIT_TAG ${_CMSIS_DEVICE_TAG} + GIT_SHALLOW TRUE +) +FetchContent_GetProperties(cmsis_device) +if (NOT cmsis_device_POPULATED) + message("Downloading CMSIS Device headers for ${STM32_FAMILY_SHORT}") +endif() +FetchContent_MakeAvailable(cmsis_device) + +FetchContent_Declare( + hal_driver + GIT_REPOSITORY ${_HAL_DRIVER_REPO} + GIT_TAG ${_HAL_DRIVER_TAG} + GIT_SHALLOW TRUE +) +FetchContent_GetProperties(hal_driver) +if (NOT hal_driver_POPULATED) + message("Downloading HAL/LL drivers for ${STM32_FAMILY_SHORT}") +endif() +FetchContent_MakeAvailable(hal_driver) + +# Export include directories for use by targets +set(CMSIS_CORE_INCLUDE "${cmsis_core_SOURCE_DIR}/Include") +set(CMSIS_DEVICE_INCLUDE "${cmsis_device_SOURCE_DIR}/Include") +set(HAL_DRIVER_INCLUDE "${hal_driver_SOURCE_DIR}/Inc") +set(HAL_DRIVER_SRC_DIR "${hal_driver_SOURCE_DIR}/Src") + +# Find startup file for the device. +# Startup files use varied naming conventions: +# startup_stm32f411xe.s — wildcard package, specific flash code +# startup_stm32f410cx.s — specific package, wildcard flash +# startup_stm32f407xx.s — fully generic +# Extract the device line (4 chars from pos 5), package (pos 9), and flash code (pos 10). +if (NOT DEFINED DEVICE_LOWER) + message(FATAL_ERROR "DEVICE_LOWER is not defined. It must be set by stm32_device.cmake before including stm32_sdk.cmake.") +endif() +string(SUBSTRING ${DEVICE_LOWER} 5 4 _dev_line) +string(LENGTH ${DEVICE_LOWER} _dev_len) +set(_startup_dir "${cmsis_device_SOURCE_DIR}/Source/Templates/gcc") +set(STM32_STARTUP_FILE "") +if (_dev_len GREATER 10) + string(SUBSTRING ${DEVICE_LOWER} 9 1 _dev_package) + string(SUBSTRING ${DEVICE_LOWER} 10 1 _dev_flash) + # Try: wildcard package + specific flash (most common) + file(GLOB STM32_STARTUP_FILE "${_startup_dir}/startup_stm32${_dev_line}x${_dev_flash}.s") + # Try: specific package + wildcard flash + if (NOT STM32_STARTUP_FILE) + file(GLOB STM32_STARTUP_FILE "${_startup_dir}/startup_stm32${_dev_line}${_dev_package}x.s") + endif() +endif() +# Try: fully generic (xx) +if (NOT STM32_STARTUP_FILE) + file(GLOB STM32_STARTUP_FILE "${_startup_dir}/startup_stm32${_dev_line}xx.s") +endif() +# Last resort: any matching startup file for this device line +if (NOT STM32_STARTUP_FILE) + file(GLOB STM32_STARTUP_FILE "${_startup_dir}/startup_stm32${_dev_line}*.s") + if (STM32_STARTUP_FILE) + list(GET STM32_STARTUP_FILE 0 STM32_STARTUP_FILE) + endif() +endif() +if (NOT STM32_STARTUP_FILE) + message(FATAL_ERROR "Startup file not found for ${DEVICE} in ${_startup_dir}/") +endif() + +# Derive HAL device define from the startup filename. +# E.g. startup_stm32f411xe.s -> STM32F411xE +# Convention: 'x' stays lowercase (wildcard), other chars are uppercase. +get_filename_component(_startup_base ${STM32_STARTUP_FILE} NAME_WE) +string(REPLACE "startup_" "" _device_id ${_startup_base}) +# Uppercase the device family/line part (first 9 chars: stm32 + 4-char line) +string(SUBSTRING ${_device_id} 0 9 _dev_prefix) +string(TOUPPER ${_dev_prefix} _dev_prefix) +# Handle the 2 variant chars: uppercase non-x, keep x lowercase +string(SUBSTRING ${_device_id} 9 1 _var1) +string(SUBSTRING ${_device_id} 10 1 _var2) +if (NOT _var1 STREQUAL "x") + string(TOUPPER ${_var1} _var1) +endif() +if (NOT _var2 STREQUAL "x") + string(TOUPPER ${_var2} _var2) +endif() +set(STM32_HAL_DEVICE "${_dev_prefix}${_var1}${_var2}") +add_compile_definitions(${STM32_HAL_DEVICE}) + +# System init template +set(STM32_SYSTEM_FILE "${cmsis_device_SOURCE_DIR}/Source/Templates/system_${STM32_FAMILY}.c") +if (NOT EXISTS ${STM32_SYSTEM_FILE}) + message(FATAL_ERROR "System file not found: ${STM32_SYSTEM_FILE}") +endif() + +message("-----------SDK Info-----------") +message(STATUS "HAL Device : ${STM32_HAL_DEVICE}") +message(STATUS "CMSIS Core : ${cmsis_core_SOURCE_DIR}") +message(STATUS "CMSIS Device : ${cmsis_device_SOURCE_DIR}") +message(STATUS "HAL Driver : ${hal_driver_SOURCE_DIR}") +message(STATUS "Startup File : ${STM32_STARTUP_FILE}") +message(STATUS "System File : ${STM32_SYSTEM_FILE}") diff --git a/src/platforms/stm32/src/CMakeLists.txt b/src/platforms/stm32/src/CMakeLists.txt index 832cd89715..c4f64ea648 100644 --- a/src/platforms/stm32/src/CMakeLists.txt +++ b/src/platforms/stm32/src/CMakeLists.txt @@ -1,7 +1,7 @@ # # This file is part of AtomVM. # -# Copyright 2022 Paul Guyot +# Copyright 2022-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. @@ -22,14 +22,85 @@ cmake_minimum_required (VERSION 3.13) # Specify output executable set(PROJECT_EXECUTABLE ${PROJECT_NAME}-${DEVICE}.elf) -add_executable(${PROJECT_EXECUTABLE} main.c) + +# Collect HAL/LL source files we need +set(HAL_SOURCES + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_rcc.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_rcc_ex.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_gpio.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_cortex.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_uart.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_flash.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_flash_ex.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_pwr.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_pwr_ex.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_exti.c +) + +# DMA HAL (name varies: regular DMA for F4/H7/WB, GPDMA for U5/H5) +if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_dma.c") + list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_dma.c) +endif() +if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_gpdma.c") + list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_gpdma.c) +endif() + +# ICACHE HAL for families that have it (U5, H5) +if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_icache.c") + list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_icache.c) +endif() + +# LL drivers for GPIO +set(LL_SOURCES + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_ll_gpio.c + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_ll_exti.c +) + +# Check if ll_rcc exists (needed by some families) +if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_ll_rcc.c") + list(APPEND LL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_ll_rcc.c) +endif() + +# Clock config for this family +set(CLOCK_CONFIG_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/../clock_configs/clock_config_${STM32_FAMILY_SHORT}.c") + +add_executable(${PROJECT_EXECUTABLE} + main.c + ${CLOCK_CONFIG_SOURCE} + ${STM32_SYSTEM_FILE} + ${STM32_STARTUP_FILE} + ${HAL_SOURCES} + ${LL_SOURCES} +) target_compile_features(${PROJECT_EXECUTABLE} PUBLIC c_std_11) if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(${PROJECT_EXECUTABLE} PUBLIC -Wall -pedantic -Wextra -ggdb -std=gnu11) endif() +# Startup file is assembly +set_source_files_properties(${STM32_STARTUP_FILE} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp") + +# Include directories for HAL and CMSIS +target_include_directories(${PROJECT_EXECUTABLE} SYSTEM PUBLIC + ${CMSIS_CORE_INCLUDE} + ${CMSIS_DEVICE_INCLUDE} + ${HAL_DRIVER_INCLUDE} +) + +# Include the generated hal_conf.h directory and clock_config header +target_include_directories(${PROJECT_EXECUTABLE} PUBLIC + "${CMAKE_CURRENT_BINARY_DIR}/../generated" + "${CMAKE_CURRENT_SOURCE_DIR}/../clock_configs" +) + +# Ensure picolibc is built before we compile any target (headers must exist) +add_dependencies(${PROJECT_EXECUTABLE} picolibc) + +# Link libAtomVM add_subdirectory(../../../libAtomVM libAtomVM) +add_dependencies(libAtomVM picolibc) target_link_libraries(${PROJECT_EXECUTABLE} PUBLIC libAtomVM) set( @@ -40,6 +111,24 @@ set( add_subdirectory(lib) target_link_libraries(${PROJECT_EXECUTABLE} PRIVATE libAtomVM${PLATFORM_LIB_SUFFIX}) +# Link picolibc and libgcc (must come after application libs that reference libc symbols) +target_link_libraries(${PROJECT_EXECUTABLE} PRIVATE + ${PICOLIBC_LIB_DIR}/libc.a + gcc + ${PICOLIBC_LIB_DIR}/libc.a +) + +# Set linker flags +set_target_properties(${PROJECT_EXECUTABLE} PROPERTIES LINK_FLAGS "${LINKER_FLAGS}") + +# Create binary output +add_custom_command( + TARGET ${PROJECT_EXECUTABLE} + POST_BUILD + COMMAND ${ARM_OBJCOPY} -Obinary ${PROJECT_EXECUTABLE} ${PROJECT_NAME}-${DEVICE}.bin + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} +) + # Output elf file size add_custom_command( TARGET ${PROJECT_EXECUTABLE} @@ -47,3 +136,7 @@ add_custom_command( COMMAND ${ARM_SIZE} ${PROJECT_EXECUTABLE} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ) + +message("------------Output Locations------------") +message(STATUS "BIN: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_NAME}-${DEVICE}.bin") +message(STATUS "ELF: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_EXECUTABLE}") diff --git a/src/platforms/stm32/src/lib/CMakeLists.txt b/src/platforms/stm32/src/lib/CMakeLists.txt index 536d21cc88..570bbcf3c3 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -1,7 +1,7 @@ # # This file is part of AtomVM. # -# Copyright 2022 Paul Guyot +# Copyright 2022-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. @@ -53,6 +53,14 @@ endif() target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC libAtomVM) target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${LINK_OPTIONS} -Wl,--whole-archive ${CMAKE_CURRENT_BINARY_DIR}/liblibAtomVMGeneric-arm.a -Wl,--no-whole-archive) -target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} SYSTEM PUBLIC ${OPENCM3_INCLUDE}) -target_link_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${OPENCM3_LIB}) -target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ${OPENCM3_RUNTIME}) +# Include HAL/CMSIS headers +target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} SYSTEM PUBLIC + ${CMSIS_CORE_INCLUDE} + ${CMSIS_DEVICE_INCLUDE} + ${HAL_DRIVER_INCLUDE} +) + +# Include the generated hal_conf.h directory +target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC + "${CMAKE_BINARY_DIR}/generated" +) diff --git a/src/platforms/stm32/src/lib/avm_devcfg.h b/src/platforms/stm32/src/lib/avm_devcfg.h index 8271d91090..1e38865e32 100644 --- a/src/platforms/stm32/src/lib/avm_devcfg.h +++ b/src/platforms/stm32/src/lib/avm_devcfg.h @@ -20,13 +20,9 @@ #ifndef _AVM_DEVCFG_H_ #define _AVM_DEVCFG_H_ -#include -#include -#include +#include "stm32_hal_platform.h" -#if (defined(STM32F4) || defined(STM32F7)) #define FLASH_START_ADDRESS (0x08000000U) -#endif #if defined(ROM_512K) #define CFG_FLASH_END (FLASH_START_ADDRESS + 0x80000U) @@ -47,65 +43,49 @@ #define AVM_APP_ADDRESS (FLASH_START_ADDRESS + 0x80000U) #endif -#if defined(MHZ_84) -#define AVM_CLOCK_HZ (84000000U) -#define AVM_CLOCK_CONFIGURATION (&rcc_hse_25mhz_3v3[(RCC_CLOCK_3V3_84MHZ)]) -#elif defined(MHZ_100) -#define AVM_CLOCK_HZ (100000000U) -#define AVM_CLOCK_CONFIGURATION (&rcc_hse_25mhz_3v3[(RCC_CLOCK_3V3_84MHZ)]) -#elif defined(MHZ_168) -#define AVM_CLOCK_HZ (168000000U) -#define AVM_CLOCK_CONFIGURATION (&rcc_hse_8mhz_3v3[(RCC_CLOCK_3V3_168MHZ)]) -#elif defined(MHZ_180) -#define AVM_CLOCK_HZ (180000000U) -#define AVM_CLOCK_CONFIGURATION (&rcc_hse_8mhz_3v3[(RCC_CLOCK_3V3_180MHZ)]) -#elif defined(MHZ_216) -#define AVM_CLOCK_HZ (216000000U) -#define AVM_CLOCK_CONFIGURATION (&rcc_hse_8mhz_3v3[(RCC_CLOCK_3V3_216MHZ)]) -#endif - +/* USART console configuration using HAL peripheral defines. */ #if defined(CONSOLE_1) -#define AVM_CONSOLE (USART1) -#define AVM_CONSOLE_TX (GPIO9) -#define AVM_CONSOLE_GPIO (GPIOA) -#define AVM_CONSOLE_RCC (RCC_USART1) +#define AVM_CONSOLE_USART USART1 +#define AVM_CONSOLE_TX_PIN GPIO_PIN_9 +#define AVM_CONSOLE_TX_PORT GPIOA +#if defined(STM32G0XX) +#define AVM_CONSOLE_TX_AF GPIO_AF1_USART1 +#else +#define AVM_CONSOLE_TX_AF GPIO_AF7_USART1 +#endif #elif defined(CONSOLE_2) -#define AVM_CONSOLE (USART2) -#define AVM_CONSOLE_TX (GPIO2) -#define AVM_CONSOLE_GPIO (GPIOA) -#define AVM_CONSOLE_RCC (RCC_USART2) +#define AVM_CONSOLE_USART USART2 +#define AVM_CONSOLE_TX_PIN GPIO_PIN_2 +#define AVM_CONSOLE_TX_PORT GPIOA +#if defined(STM32G0XX) +#define AVM_CONSOLE_TX_AF GPIO_AF1_USART2 +#else +#define AVM_CONSOLE_TX_AF GPIO_AF7_USART2 +#endif #elif defined(CONSOLE_3) -#define AVM_CONSOLE (USART3) -#define AVM_CONSOLE_TX (GPIO8) -#define AVM_CONSOLE_GPIO (GPIOD) -#define AVM_CONSOLE_RCC (RCC_USART3) +#define AVM_CONSOLE_USART USART3 +#define AVM_CONSOLE_TX_PIN GPIO_PIN_8 +#define AVM_CONSOLE_TX_PORT GPIOD +#if defined(STM32G0XX) +#define AVM_CONSOLE_TX_AF GPIO_AF4_USART3 +#else +#define AVM_CONSOLE_TX_AF GPIO_AF7_USART3 +#endif #elif defined(CONSOLE_4) -#define AVM_CONSOLE (UART4) -#define AVM_CONSOLE_TX (GPIO10) -#define AVM_CONSOLE_GPIO (GPIOC) -#define AVM_CONSOLE_RCC (RCC_UART4) +#define AVM_CONSOLE_USART UART4 +#define AVM_CONSOLE_TX_PIN GPIO_PIN_10 +#define AVM_CONSOLE_TX_PORT GPIOC +#define AVM_CONSOLE_TX_AF GPIO_AF8_UART4 #elif defined(CONSOLE_5) -#define AVM_CONSOLE (UART5) -#define AVM_CONSOLE_TX (GPIO12) -#define AVM_CONSOLE_GPIO (GPIOC) -#define AVM_CONSOLE_RCC (RCC_UART5) -#ifdef LIBOPENCM3_USART_COMMON_F24_H +#define AVM_CONSOLE_USART UART5 +#define AVM_CONSOLE_TX_PIN GPIO_PIN_12 +#define AVM_CONSOLE_TX_PORT GPIOC +#define AVM_CONSOLE_TX_AF GPIO_AF8_UART5 #elif defined(CONSOLE_6) -#define AVM_CONSOLE (USART6) -#define AVM_CONSOLE_TX (GPIO6) -#define AVM_CONSOLE_GPIO (GPIOC) -#define AVM_CONSOLE_RCC (RCC_USART6) -#elif defined(CONSOLE_7) -#define AVM_CONSOLE (UART7) -#define AVM_CONSOLE_TX (GPIO7) -#define AVM_CONSOLE_GPIO (GPIOF) -#define AVM_CONSOLE_RCC (RCC_UART7) -#elif defined(CONSOLE_8) -#define AVM_CONSOLE (UART8) -#define AVM_CONSOLE_TX (GPIO8) -#define AVM_CONSOLE_GPIO (GPIOJ) -#define AVM_CONSOLE_RCC (RCC_UART8) -#endif /* LIBOPENCM3_USART_COMMON_F24_H */ +#define AVM_CONSOLE_USART USART6 +#define AVM_CONSOLE_TX_PIN GPIO_PIN_6 +#define AVM_CONSOLE_TX_PORT GPIOC +#define AVM_CONSOLE_TX_AF GPIO_AF8_USART6 #endif #endif /* _AVM_DEVCFG_H_ */ diff --git a/src/platforms/stm32/src/lib/gpio_driver.c b/src/platforms/stm32/src/lib/gpio_driver.c index 2c37d7d52b..0ca541dd52 100644 --- a/src/platforms/stm32/src/lib/gpio_driver.c +++ b/src/platforms/stm32/src/lib/gpio_driver.c @@ -23,9 +23,12 @@ #include #include -#include -#include -#include +#include "stm32_hal_platform.h" + +/* Normalize GPIO_PIN_All vs GPIO_PIN_ALL across HAL versions */ +#if !defined(GPIO_PIN_All) && defined(GPIO_PIN_ALL) +#define GPIO_PIN_All GPIO_PIN_ALL +#endif #include #include @@ -48,7 +51,6 @@ #define TAG "gpio_driver" -#define GPIO_MODE_OUTPUT_OD 0x4 // Error that cannot be used for these registers #define GPIOInvalidBank 0x0000U #define GPIO_INVALID_MODE 0xE @@ -103,9 +105,9 @@ enum gpio_cmd }; static const AtomStringIntPair exti_trigger_table[] = { - { ATOM_STR("\x6", "rising"), EXTI_TRIGGER_RISING }, - { ATOM_STR("\x7", "falling"), EXTI_TRIGGER_FALLING }, - { ATOM_STR("\x4", "both"), EXTI_TRIGGER_BOTH }, + { ATOM_STR("\x6", "rising"), GPIO_MODE_IT_RISING }, + { ATOM_STR("\x7", "falling"), GPIO_MODE_IT_FALLING }, + { ATOM_STR("\x4", "both"), GPIO_MODE_IT_RISING_FALLING }, SELECT_INT_DEFAULT(INVALID_EXTI_TRIGGER) }; @@ -129,27 +131,37 @@ enum gpio_pin_state }; static const AtomStringIntPair gpio_bank_table[] = { - { ATOM_STR("\x1", "a"), GPIOA }, - { ATOM_STR("\x1", "b"), GPIOB }, - { ATOM_STR("\x1", "c"), GPIOC }, - { ATOM_STR("\x1", "d"), GPIOD }, - { ATOM_STR("\x1", "e"), GPIOE }, - { ATOM_STR("\x1", "f"), GPIOF }, - { ATOM_STR("\x1", "g"), GPIOG }, - { ATOM_STR("\x1", "h"), GPIOH }, -#ifdef LIBOPENCM3_GPIO_COMMON_F24_H - { ATOM_STR("\x1", "i"), GPIOI }, - { ATOM_STR("\x1", "j"), GPIOJ }, - { ATOM_STR("\x1", "k"), GPIOK }, -#endif /* defined LIBOPENCM3_GPIO_COMMON_F24_H */ + { ATOM_STR("\x1", "a"), (int) GPIOA }, + { ATOM_STR("\x1", "b"), (int) GPIOB }, + { ATOM_STR("\x1", "c"), (int) GPIOC }, + { ATOM_STR("\x1", "d"), (int) GPIOD }, + { ATOM_STR("\x1", "e"), (int) GPIOE }, +#ifdef GPIOF + { ATOM_STR("\x1", "f"), (int) GPIOF }, +#endif +#ifdef GPIOG + { ATOM_STR("\x1", "g"), (int) GPIOG }, +#endif +#ifdef GPIOH + { ATOM_STR("\x1", "h"), (int) GPIOH }, +#endif +#ifdef GPIOI + { ATOM_STR("\x1", "i"), (int) GPIOI }, +#endif +#ifdef GPIOJ + { ATOM_STR("\x1", "j"), (int) GPIOJ }, +#endif +#ifdef GPIOK + { ATOM_STR("\x1", "k"), (int) GPIOK }, +#endif SELECT_INT_DEFAULT(GPIOInvalidBank) }; static const AtomStringIntPair output_mhz_table[] = { - { ATOM_STR("\x5", "mhz_2"), GPIO_OSPEED_2MHZ }, - { ATOM_STR("\x6", "mhz_25"), GPIO_OSPEED_25MHZ }, - { ATOM_STR("\x6", "mhz_50"), GPIO_OSPEED_50MHZ }, - { ATOM_STR("\x7", "mhz_100"), GPIO_OSPEED_100MHZ }, + { ATOM_STR("\x5", "mhz_2"), GPIO_SPEED_FREQ_LOW }, + { ATOM_STR("\x6", "mhz_25"), GPIO_SPEED_FREQ_MEDIUM }, + { ATOM_STR("\x6", "mhz_50"), GPIO_SPEED_FREQ_HIGH }, + { ATOM_STR("\x7", "mhz_100"), GPIO_SPEED_FREQ_VERY_HIGH }, SELECT_INT_DEFAULT(INVALID_GPIO_OSPEED) }; @@ -161,59 +173,101 @@ static const AtomStringIntPair pin_level_table[] = { static const AtomStringIntPair pin_mode_table[] = { { ATOM_STR("\x5", "input"), GPIO_MODE_INPUT }, - { ATOM_STR("\x6", "output"), GPIO_MODE_OUTPUT }, + { ATOM_STR("\x6", "output"), GPIO_MODE_OUTPUT_PP }, { ATOM_STR("\x9", "output_od"), GPIO_MODE_OUTPUT_OD }, - { ATOM_STR("\x2", "af"), GPIO_MODE_AF }, + { ATOM_STR("\x2", "af"), GPIO_MODE_AF_PP }, { ATOM_STR("\x6", "analog"), GPIO_MODE_ANALOG }, SELECT_INT_DEFAULT(GPIO_INVALID_MODE) }; static const AtomStringIntPair pull_mode_table[] = { - { ATOM_STR("\x2", "up"), GPIO_PUPD_PULLUP }, - { ATOM_STR("\x4", "down"), GPIO_PUPD_PULLDOWN }, - { ATOM_STR("\x8", "floating"), GPIO_PUPD_NONE }, - SELECT_INT_DEFAULT(GPIO_PUPD_NONE) + { ATOM_STR("\x2", "up"), GPIO_PULLUP }, + { ATOM_STR("\x4", "down"), GPIO_PULLDOWN }, + { ATOM_STR("\x8", "floating"), GPIO_NOPULL }, + SELECT_INT_DEFAULT(GPIO_NOPULL) }; #ifndef AVM_DISABLE_GPIO_PORT_DRIVER // Obtain the IRQ interrupt associated with a pin number -static uint8_t pin_num_to_exti_irq(uint16_t pin_num) +static IRQn_Type pin_num_to_exti_irq(uint16_t pin_num) { switch (pin_num) { +#if defined(STM32G0XX) + /* G0: grouped IRQs for lines 0-1, 2-3, 4-15 */ case 0: - return NVIC_EXTI0_IRQ; case 1: - return NVIC_EXTI1_IRQ; + return EXTI0_1_IRQn; case 2: - return NVIC_EXTI2_IRQ; case 3: - return NVIC_EXTI3_IRQ; + return EXTI2_3_IRQn; case 4: - return NVIC_EXTI4_IRQ; case 5: - return NVIC_EXTI9_5_IRQ; case 6: - return NVIC_EXTI9_5_IRQ; case 7: - return NVIC_EXTI9_5_IRQ; case 8: - return NVIC_EXTI9_5_IRQ; case 9: - return NVIC_EXTI9_5_IRQ; case 10: - return NVIC_EXTI15_10_IRQ; case 11: - return NVIC_EXTI15_10_IRQ; case 12: - return NVIC_EXTI15_10_IRQ; case 13: - return NVIC_EXTI15_10_IRQ; case 14: - return NVIC_EXTI15_10_IRQ; case 15: - return NVIC_EXTI15_10_IRQ; + return EXTI4_15_IRQn; +#else + case 0: + return EXTI0_IRQn; + case 1: + return EXTI1_IRQn; + case 2: + return EXTI2_IRQn; + case 3: + return EXTI3_IRQn; + case 4: + return EXTI4_IRQn; +#if defined(STM32F4XX) || defined(STM32H7XX) || defined(STM32WBXX) || defined(STM32F7XX) \ + || defined(STM32G4XX) || defined(STM32L4XX) || defined(STM32F2XX) + /* Classic EXTI: lines 5-9 and 10-15 share IRQs */ + case 5: + case 6: + case 7: + case 8: + case 9: + return EXTI9_5_IRQn; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return EXTI15_10_IRQn; +#else + /* Per-line EXTI: each line has its own IRQ (U5, H5, L5, U3) */ + case 5: + return EXTI5_IRQn; + case 6: + return EXTI6_IRQn; + case 7: + return EXTI7_IRQn; + case 8: + return EXTI8_IRQn; + case 9: + return EXTI9_IRQn; + case 10: + return EXTI10_IRQn; + case 11: + return EXTI11_IRQn; + case 12: + return EXTI12_IRQn; + case 13: + return EXTI13_IRQn; + case 14: + return EXTI14_IRQn; + case 15: + return EXTI15_IRQn; +#endif +#endif /* STM32G0XX */ default: - return 0; + return (IRQn_Type) -1; } } @@ -285,6 +339,7 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) free(bank_string); return error_tuple_str_maybe_gc(ctx, invalid_bank_atom); } + GPIO_TypeDef *gpio_port = (GPIO_TypeDef *) gpio_bank; term pin_term = term_get_tuple_element(gpio_pin_tuple, 1); uint16_t gpio_pin_mask = 0x0000U; @@ -319,17 +374,16 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) if (pin_term != ALL_ATOM) { return error_tuple_str_maybe_gc(ctx, invalid_pin_atom); } - gpio_pin_mask = GPIO_ALL; + gpio_pin_mask = GPIO_PIN_All; } else { return error_tuple_str_maybe_gc(ctx, invalid_pin_atom); } term mode_atom; - uint8_t gpio_mode; + uint32_t gpio_mode; bool setup_output = false; - uint8_t pull_up_down; - uint8_t out_type; - uint8_t output_speed; + uint32_t pull_up_down; + uint32_t output_speed; term mhz_atom = term_invalid_term(); term pull_atom = term_invalid_term(); if (term_is_tuple(mode_term)) { @@ -338,20 +392,14 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) AVM_LOGE(TAG, "GPIO Mode must be an atom ('input', 'output', 'output_od')."); return error_tuple_str_maybe_gc(ctx, invalid_mode_atom); } - gpio_mode = ((uint8_t) interop_atom_term_select_int(pin_mode_table, mode_atom, ctx->global)); + gpio_mode = ((uint32_t) interop_atom_term_select_int(pin_mode_table, mode_atom, ctx->global)); if (UNLIKELY(gpio_mode == GPIO_INVALID_MODE)) { char *mode_string = interop_atom_to_string(ctx, mode_atom); AVM_LOGE(TAG, "Invalid gpio mode: %s", mode_string); free(mode_string); return error_tuple_str_maybe_gc(ctx, invalid_mode_atom); } - if ((gpio_mode == GPIO_MODE_OUTPUT) || (gpio_mode == GPIO_MODE_OUTPUT_OD)) { - if (gpio_mode == GPIO_MODE_OUTPUT_OD) { - gpio_mode = GPIO_MODE_OUTPUT; - out_type = GPIO_OTYPE_OD; - } else { - out_type = GPIO_OTYPE_PP; - } + if ((gpio_mode == GPIO_MODE_OUTPUT_PP) || (gpio_mode == GPIO_MODE_OUTPUT_OD)) { setup_output = true; } @@ -361,7 +409,7 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) return error_tuple_str_maybe_gc(ctx, invalid_pull_atom); } - pull_up_down = ((uint8_t) interop_atom_term_select_int(pull_mode_table, pull_atom, ctx->global)); + pull_up_down = ((uint32_t) interop_atom_term_select_int(pull_mode_table, pull_atom, ctx->global)); if ((setup_output) && (term_get_tuple_arity(mode_term) == 3)) { mhz_atom = term_get_tuple_element(mode_term, 2); if (UNLIKELY(!term_is_atom(mhz_atom))) { @@ -369,15 +417,15 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) error_tuple_str_maybe_gc(ctx, invalid_rate_atom); } - output_speed = (uint8_t) interop_atom_term_select_int(output_mhz_table, mhz_atom, ctx->global); + output_speed = (uint32_t) interop_atom_term_select_int(output_mhz_table, mhz_atom, ctx->global); if (output_speed == INVALID_GPIO_OSPEED) { - output_speed = GPIO_OSPEED_2MHZ; + output_speed = GPIO_SPEED_FREQ_LOW; char *mhz_string = interop_atom_to_string(ctx, mhz_atom); AVM_LOGW(TAG, "Invalid output speed '%s' given, falling back to 2 Mhz default.", mhz_string); free(mhz_string); } } else if (setup_output) { - output_speed = GPIO_OSPEED_2MHZ; + output_speed = GPIO_SPEED_FREQ_LOW; AVM_LOGW(TAG, "No output speed given, falling back to 2 Mhz default."); } } else { @@ -386,33 +434,38 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) AVM_LOGE(TAG, "GPIO Mode must be an atom ('input', 'output', 'output_od')."); return error_tuple_str_maybe_gc(ctx, invalid_mode_atom); } - gpio_mode = ((uint8_t) interop_atom_term_select_int(pin_mode_table, mode_atom, ctx->global)); + gpio_mode = ((uint32_t) interop_atom_term_select_int(pin_mode_table, mode_atom, ctx->global)); if (UNLIKELY(gpio_mode == GPIO_INVALID_MODE)) { char *mode_string = interop_atom_to_string(ctx, mode_atom); AVM_LOGE(TAG, "Invalid gpio mode: %s", mode_string); free(mode_string); return error_tuple_str_maybe_gc(ctx, invalid_mode_atom); } - pull_up_down = GPIO_PUPD_NONE; - if ((gpio_mode == GPIO_MODE_OUTPUT) || (gpio_mode == GPIO_MODE_OUTPUT_OD)) { - if (gpio_mode == GPIO_MODE_OUTPUT_OD) { - gpio_mode = GPIO_MODE_OUTPUT; - out_type = GPIO_OTYPE_OD; - } else { - out_type = GPIO_OTYPE_PP; - } - output_speed = GPIO_OSPEED_2MHZ; + pull_up_down = GPIO_NOPULL; + if ((gpio_mode == GPIO_MODE_OUTPUT_PP) || (gpio_mode == GPIO_MODE_OUTPUT_OD)) { + output_speed = GPIO_SPEED_FREQ_LOW; setup_output = true; } } - gpio_mode_setup(gpio_bank, gpio_mode, pull_up_down, gpio_pin_mask); + GPIO_InitTypeDef gpio_init = { 0 }; + gpio_init.Pin = gpio_pin_mask; + gpio_init.Pull = pull_up_down; + + gpio_init.Mode = gpio_mode; if (setup_output) { - gpio_set_output_options(gpio_bank, out_type, output_speed, gpio_pin_mask); - AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X output mode 0x%02X, output speed 0x%04X, pull mode 0x%02X", gpio_bank, gpio_pin_mask, gpio_mode, output_speed, pull_up_down); - } else { - AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X input mode 0x%02X, pull mode 0x%02X", gpio_bank, gpio_pin_mask, gpio_mode, pull_up_down); + gpio_init.Speed = output_speed; + AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X output mode 0x%02lX, output speed 0x%04lX, pull mode 0x%02lX", gpio_bank, gpio_pin_mask, gpio_mode, output_speed, pull_up_down); + } else if (gpio_mode == GPIO_MODE_INPUT) { + AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X input mode 0x%02lX, pull mode 0x%02lX", gpio_bank, gpio_pin_mask, gpio_mode, pull_up_down); + } else if (gpio_mode == GPIO_MODE_AF_PP) { + gpio_init.Speed = GPIO_SPEED_FREQ_HIGH; + AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X AF mode", gpio_bank, gpio_pin_mask); + } else if (gpio_mode == GPIO_MODE_ANALOG) { + AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X analog mode", gpio_bank, gpio_pin_mask); } + + HAL_GPIO_Init(gpio_port, &gpio_init); return OK_ATOM; } @@ -436,6 +489,7 @@ static term gpio_digital_write(Context *ctx, term gpio_pin_tuple, term level_ter free(bank_string); return error_tuple_str_maybe_gc(ctx, invalid_bank_atom); } + GPIO_TypeDef *gpio_port = (GPIO_TypeDef *) gpio_bank; term pin_term = term_get_tuple_element(gpio_pin_tuple, 1); uint16_t gpio_pin_mask = 0x0000U; @@ -471,7 +525,7 @@ static term gpio_digital_write(Context *ctx, term gpio_pin_tuple, term level_ter AVM_LOGE(TAG, "Pin number must be between 0 and 15!"); return error_tuple_str_maybe_gc(ctx, invalid_pin_atom); } - gpio_pin_mask = GPIO_ALL; + gpio_pin_mask = GPIO_PIN_All; } else { return error_tuple_str_maybe_gc(ctx, invalid_pin_atom); } @@ -494,11 +548,7 @@ static term gpio_digital_write(Context *ctx, term gpio_pin_tuple, term level_ter } } - if (level != 0) { - gpio_set(gpio_bank, gpio_pin_mask); - } else { - gpio_clear(gpio_bank, gpio_pin_mask); - } + HAL_GPIO_WritePin(gpio_port, gpio_pin_mask, level ? GPIO_PIN_SET : GPIO_PIN_RESET); TRACE("Write: bank: 0x%08lX, pin mask: 0x%04X, level: %i\n", gpio_bank, gpio_pin_mask, level); return OK_ATOM; } @@ -523,6 +573,8 @@ static term gpio_digital_read(Context *ctx, term gpio_pin_tuple) free(bank_string); return error_tuple_str_maybe_gc(ctx, invalid_bank_atom); } + GPIO_TypeDef *gpio_port = (GPIO_TypeDef *) gpio_bank; + // TODO: Add support for reading list, or all input pins on port? uint16_t gpio_pin_num = ((uint16_t) term_to_int32(term_get_tuple_element(gpio_pin_tuple, 1))); if (UNLIKELY(gpio_pin_num > 15)) { @@ -530,8 +582,8 @@ static term gpio_digital_read(Context *ctx, term gpio_pin_tuple) return error_tuple_str_maybe_gc(ctx, invalid_pin_atom); } - uint16_t pin_levels = gpio_get(gpio_bank, (1U << gpio_pin_num)); - uint16_t level = (pin_levels >> gpio_pin_num); + GPIO_PinState state = HAL_GPIO_ReadPin(gpio_port, (uint16_t) (1U << gpio_pin_num)); + uint16_t level = (state == GPIO_PIN_SET) ? 1 : 0; TRACE("Read: Bank 0x%08lX Pin %u. RESULT: %u\n", gpio_bank, gpio_pin_num, level); return level_to_atom(ctx, level); @@ -584,10 +636,10 @@ static term gpiodriver_close(Context *ctx) if (!list_is_empty(&gpio_data->gpio_listeners)) { MUTABLE_LIST_FOR_EACH (item, tmp, &gpio_data->gpio_listeners) { struct GPIOListenerData *gpio_listener = GET_LIST_ENTRY(item, struct GPIOListenerData, gpio_listener_list_head); - uint32_t exti = gpio_listener->exti; - uint8_t irqn = pin_num_to_exti_irq(gpio_listener->gpio_pin); - nvic_disable_irq(irqn); - exti_disable_request(exti); + uint32_t exti_line = gpio_listener->exti; + IRQn_Type irqn = pin_num_to_exti_irq(gpio_listener->gpio_pin); + HAL_NVIC_DisableIRQ(irqn); + __HAL_GPIO_EXTI_CLEAR_IT(exti_line); list_remove(&gpio_listener->gpio_listener_list_head); free(gpio_listener); } @@ -631,7 +683,7 @@ void gpio_interrupt_callback(Context *ctx, uint32_t exti) } // This function is used to store the local Context pointer during gpiodriver_set_int and to relay messages to the interrupt_callback -// from the exti#_isr interrupt handlers (defined in libopencm3/stm32/CHIP-SERIES/nvic.h) that have no access to the context. +// from the exti#_isr interrupt handlers that have no access to the context. void isr_handler(Context *ctx, uint32_t exti) { static Context *local_ctx; @@ -651,85 +703,159 @@ void isr_error_handler(const char *isr_name) AVM_LOGE(TAG, "%s triggered, but no match found!", isr_name); } -void exti0_isr() +/* Compatibility macros for families with separate rising/falling EXTI + * registers (U5, H5) where __HAL_GPIO_EXTI_GET_IT may not be defined */ +#ifndef __HAL_GPIO_EXTI_GET_IT +#define __HAL_GPIO_EXTI_GET_IT(__EXTI_LINE__) \ + (__HAL_GPIO_EXTI_GET_RISING_IT(__EXTI_LINE__) || __HAL_GPIO_EXTI_GET_FALLING_IT(__EXTI_LINE__)) +#define __HAL_GPIO_EXTI_CLEAR_IT(__EXTI_LINE__) \ + do { \ + __HAL_GPIO_EXTI_CLEAR_RISING_IT(__EXTI_LINE__); \ + __HAL_GPIO_EXTI_CLEAR_FALLING_IT(__EXTI_LINE__); \ + } while (0) +#endif + +/* EXTI ISR handlers - HAL provides HAL_GPIO_EXTI_Callback but we handle + * them directly for more control over the interrupt handling */ + +#if defined(STM32G0XX) +/* G0: grouped handlers for lines 0-1, 2-3, 4-15 */ +void EXTI0_1_IRQHandler(void) { - exti_reset_request(EXTI0); - isr_handler(NULL, EXTI0); + for (uint16_t pin = 0; pin <= 1; pin++) { + uint16_t pin_mask = (uint16_t) (1U << pin); + if (__HAL_GPIO_EXTI_GET_IT(pin_mask)) { + __HAL_GPIO_EXTI_CLEAR_IT(pin_mask); + isr_handler(NULL, pin_mask); + } + } } -void exti1_isr() +void EXTI2_3_IRQHandler(void) { - exti_reset_request(EXTI1); - isr_handler(NULL, EXTI1); + for (uint16_t pin = 2; pin <= 3; pin++) { + uint16_t pin_mask = (uint16_t) (1U << pin); + if (__HAL_GPIO_EXTI_GET_IT(pin_mask)) { + __HAL_GPIO_EXTI_CLEAR_IT(pin_mask); + isr_handler(NULL, pin_mask); + } + } } -void exti2_isr() +void EXTI4_15_IRQHandler(void) { - exti_reset_request(EXTI2); - isr_handler(NULL, EXTI2); + for (uint16_t pin = 4; pin <= 15; pin++) { + uint16_t pin_mask = (uint16_t) (1U << pin); + if (__HAL_GPIO_EXTI_GET_IT(pin_mask)) { + __HAL_GPIO_EXTI_CLEAR_IT(pin_mask); + isr_handler(NULL, pin_mask); + } + } } -void exti3_isr() +#else /* !STM32G0XX */ + +void EXTI0_IRQHandler(void) { - exti_reset_request(EXTI3); - isr_handler(NULL, EXTI3); + if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) { + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); + isr_handler(NULL, GPIO_PIN_0); + } } -void exti4_isr() +void EXTI1_IRQHandler(void) { - exti_reset_request(EXTI4); - isr_handler(NULL, EXTI4); + if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_1)) { + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_1); + isr_handler(NULL, GPIO_PIN_1); + } } -void exti9_5_isr() +void EXTI2_IRQHandler(void) { - if (exti_get_flag_status(EXTI5) == EXTI5) { - exti_reset_request(EXTI5); - isr_handler(NULL, EXTI5); - } else if (exti_get_flag_status(EXTI6) == EXTI6) { - exti_reset_request(EXTI6); - isr_handler(NULL, EXTI6); - } else if (exti_get_flag_status(EXTI7) == EXTI7) { - exti_reset_request(EXTI7); - isr_handler(NULL, EXTI7); - } else if (exti_get_flag_status(EXTI8) == EXTI8) { - exti_reset_request(EXTI8); - isr_handler(NULL, EXTI8); - } else if (exti_get_flag_status(EXTI9) == EXTI9) { - exti_reset_request(EXTI9); - isr_handler(NULL, EXTI9); - } else { - static const char *const isr_name = "exti9_5_isr"; - isr_error_handler(isr_name); + if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_2)) { + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_2); + isr_handler(NULL, GPIO_PIN_2); } } -void exti15_10_isr() +void EXTI3_IRQHandler(void) { - if (exti_get_flag_status(EXTI10)) { - exti_reset_request(EXTI10); - isr_handler(NULL, EXTI10); - } else if (exti_get_flag_status(EXTI11)) { - exti_reset_request(EXTI11); - isr_handler(NULL, EXTI11); - } else if (exti_get_flag_status(EXTI12)) { - exti_reset_request(EXTI12); - isr_handler(NULL, EXTI12); - } else if (exti_get_flag_status(EXTI13)) { - exti_reset_request(EXTI13); - isr_handler(NULL, EXTI13); - } else if (exti_get_flag_status(EXTI14)) { - exti_reset_request(EXTI14); - isr_handler(NULL, EXTI14); - } else if (exti_get_flag_status(EXTI15)) { - exti_reset_request(EXTI15); - isr_handler(NULL, EXTI15); - } else { - static const char *const isr_name = "exti15_10_isr"; - isr_error_handler(isr_name); + if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_3)) { + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_3); + isr_handler(NULL, GPIO_PIN_3); + } +} + +void EXTI4_IRQHandler(void) +{ + if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_4)) { + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_4); + isr_handler(NULL, GPIO_PIN_4); } } +#if defined(STM32F4XX) || defined(STM32H7XX) || defined(STM32WBXX) || defined(STM32F7XX) \ + || defined(STM32G4XX) || defined(STM32L4XX) || defined(STM32F2XX) +/* Classic EXTI: shared handlers for lines 5-9 and 10-15 */ +void EXTI9_5_IRQHandler(void) +{ + bool handled = false; + for (uint16_t pin = 5; pin <= 9; pin++) { + uint16_t pin_mask = (uint16_t) (1U << pin); + if (__HAL_GPIO_EXTI_GET_IT(pin_mask)) { + __HAL_GPIO_EXTI_CLEAR_IT(pin_mask); + isr_handler(NULL, pin_mask); + handled = true; + } + } + if (!handled) { + isr_error_handler("EXTI9_5_IRQHandler"); + } +} + +void EXTI15_10_IRQHandler(void) +{ + bool handled = false; + for (uint16_t pin = 10; pin <= 15; pin++) { + uint16_t pin_mask = (uint16_t) (1U << pin); + if (__HAL_GPIO_EXTI_GET_IT(pin_mask)) { + __HAL_GPIO_EXTI_CLEAR_IT(pin_mask); + isr_handler(NULL, pin_mask); + handled = true; + } + } + if (!handled) { + isr_error_handler("EXTI15_10_IRQHandler"); + } +} +#else +/* Per-line EXTI: each line has its own handler (U5, H5, L5, U3) */ +#define DEFINE_EXTI_HANDLER(n) \ + void EXTI##n##_IRQHandler(void) \ + { \ + if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_##n)) { \ + __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_##n); \ + isr_handler(NULL, GPIO_PIN_##n); \ + } \ + } + +DEFINE_EXTI_HANDLER(5) +DEFINE_EXTI_HANDLER(6) +DEFINE_EXTI_HANDLER(7) +DEFINE_EXTI_HANDLER(8) +DEFINE_EXTI_HANDLER(9) +DEFINE_EXTI_HANDLER(10) +DEFINE_EXTI_HANDLER(11) +DEFINE_EXTI_HANDLER(12) +DEFINE_EXTI_HANDLER(13) +DEFINE_EXTI_HANDLER(14) +DEFINE_EXTI_HANDLER(15) + +#undef DEFINE_EXTI_HANDLER +#endif +#endif /* !STM32G0XX */ + static term gpiodriver_set_level(Context *ctx, term cmd) { term gpio_pin_tuple = term_get_tuple_element(cmd, 1); @@ -801,7 +927,7 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) AVM_LOGE(TAG, "GPIO interrupt trigger must be an atom ('rising', 'falling', or 'both')."); return error_tuple_str_maybe_gc(ctx, invalid_trigger_atom); } - enum exti_trigger_type interrupt_type = interop_atom_term_select_int(exti_trigger_table, trigger, ctx->global); + int interrupt_type = interop_atom_term_select_int(exti_trigger_table, trigger, ctx->global); if (UNLIKELY(interrupt_type == INVALID_EXTI_TRIGGER)) { char *trigger_string = interop_atom_to_string(ctx, trigger); AVM_LOGE(TAG, "Interrupt type %s not supported on stm32 platform.", trigger_string); @@ -829,13 +955,13 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) target_local_pid = target_pid; } - uint32_t exti = 1U << gpio_pin; + uint32_t exti_line = 1U << gpio_pin; if (!list_is_empty(&gpio_data->gpio_listeners)) { struct ListHead *item; LIST_FOR_EACH (item, &gpio_data->gpio_listeners) { struct GPIOListenerData *gpio_listener = GET_LIST_ENTRY(item, struct GPIOListenerData, gpio_listener_list_head); - if (gpio_listener->exti == exti) { + if (gpio_listener->exti == exti_line) { char *bank_string = interop_atom_to_string(ctx, gpio_bank_atom); AVM_LOGE(TAG, "Cannot set interrupt for pin %s%u, exti%u device already in use!", bank_string, gpio_pin, gpio_pin); free(bank_string); @@ -844,8 +970,8 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) } } - uint8_t exti_irq = pin_num_to_exti_irq(gpio_pin); - if (UNLIKELY(exti_irq == 0)) { + IRQn_Type exti_irq = pin_num_to_exti_irq(gpio_pin); + if (UNLIKELY(exti_irq == (IRQn_Type) -1)) { AVM_LOGE(TAG, "BUG: No valid exti irq found!"); return error_tuple_str_maybe_gc(ctx, invalid_irq_atom); } @@ -859,21 +985,23 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) data->target_local_pid = target_local_pid; data->bank_atom = gpio_bank_atom; data->gpio_pin = gpio_pin; - data->exti = exti; - data->exti_irq = exti_irq; + data->exti = exti_line; + data->exti_irq = (uint8_t) exti_irq; AVM_LOGD(TAG, "Installing interrupt type 0x%02X with exti%u for bank 0x%08lX pin %u.", interrupt_type, gpio_pin, gpio_bank, gpio_pin); - exti_disable_request(exti); - exti_select_source(exti, gpio_bank); - exti_set_trigger(exti, interrupt_type); + GPIO_TypeDef *gpio_port = (GPIO_TypeDef *) gpio_bank; + GPIO_InitTypeDef gpio_init = { 0 }; + gpio_init.Pin = exti_line; + gpio_init.Pull = GPIO_NOPULL; + gpio_init.Mode = interrupt_type; + HAL_GPIO_Init(gpio_port, &gpio_init); + // Store the Context pointer in the isr_handler isr_handler(ctx, 0x0000U); - exti_enable_request(exti); - if (!nvic_get_irq_enabled(exti_irq)) { - nvic_enable_irq(exti_irq); - } - nvic_set_priority(exti_irq, 1); + + HAL_NVIC_SetPriority(exti_irq, 1, 0); + HAL_NVIC_EnableIRQ(exti_irq); return OK_ATOM; } @@ -898,7 +1026,11 @@ static term gpiodriver_remove_int(Context *ctx, term cmd) return error_tuple_str_maybe_gc(ctx, invalid_bank_atom); } uint16_t target_num = (uint16_t) term_to_int32(term_get_tuple_element(gpio_tuple, 1)); - uint8_t target_irq = pin_num_to_exti_irq(target_num); + if (UNLIKELY(target_num > 15)) { + AVM_LOGE(TAG, "Pin number must be between 0 and 15!"); + return error_tuple_str_maybe_gc(ctx, invalid_pin_atom); + } + IRQn_Type target_irq = pin_num_to_exti_irq(target_num); bool stop_irq = true; bool int_removed = false; @@ -909,20 +1041,20 @@ static term gpiodriver_remove_int(Context *ctx, term cmd) MUTABLE_LIST_FOR_EACH (item, tmp, &gpio_data->gpio_listeners) { struct GPIOListenerData *gpio_listener = GET_LIST_ENTRY(item, struct GPIOListenerData, gpio_listener_list_head); if ((gpio_listener->gpio_pin == target_num) && (gpio_listener->bank_atom == target_bank_atom)) { - uint16_t exti = gpio_listener->exti; - uint8_t irqn = pin_num_to_exti_irq(gpio_listener->gpio_pin); - nvic_disable_irq(irqn); + uint32_t exti_line = gpio_listener->exti; + IRQn_Type irqn = pin_num_to_exti_irq(gpio_listener->gpio_pin); + HAL_NVIC_DisableIRQ(irqn); list_remove(&gpio_listener->gpio_listener_list_head); - exti_disable_request(exti); + __HAL_GPIO_EXTI_CLEAR_IT(exti_line); free(gpio_listener); int_removed = true; // some pins share irqs - don't stop the irq if another pin is still using it. - } else if (gpio_listener->exti_irq == target_irq) { + } else if (gpio_listener->exti_irq == (uint8_t) target_irq) { stop_irq = false; } } if (stop_irq) { - nvic_disable_irq(target_irq); + HAL_NVIC_DisableIRQ(target_irq); } if (int_removed == false) { AVM_LOGW(TAG, "No interrupt removed, match not found for bank 0x%08lX pin %u.", target_bank, target_num); @@ -1016,6 +1148,23 @@ REGISTER_PORT_DRIVER(gpio, gpiodriver_init, NULL, gpio_driver_create_port) #ifndef AVM_DISABLE_GPIO_NIFS +static term nif_gpio_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(ctx); + UNUSED(argc); + UNUSED(argv); + /* GPIO clocks are already enabled at startup in sys_enable_core_periph_clocks() */ + return OK_ATOM; +} + +static term nif_gpio_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(ctx); + UNUSED(argc); + UNUSED(argv); + return OK_ATOM; +} + static term nif_gpio_set_pin_mode(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -1070,6 +1219,16 @@ static term nif_gpio_digital_read(Context *ctx, int argc, term argv[]) return ret; } +static const struct Nif gpio_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_gpio_init +}; + +static const struct Nif gpio_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_gpio_deinit +}; + static const struct Nif gpio_set_pin_mode_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_gpio_set_pin_mode @@ -1092,6 +1251,16 @@ static const struct Nif gpio_digital_read_nif = { const struct Nif *gpio_nif_get_nif(const char *nifname) { + if (strcmp("gpio:init/1", nifname) == 0 || strcmp("Elixir.GPIO:init/1", nifname) == 0) { + AVM_LOGD(TAG, "Resolved platform nif %s ...", nifname); + return &gpio_init_nif; + } + + if (strcmp("gpio:deinit/1", nifname) == 0 || strcmp("Elixir.GPIO:deinit/1", nifname) == 0) { + AVM_LOGD(TAG, "Resolved platform nif %s ...", nifname); + return &gpio_deinit_nif; + } + if (strcmp("gpio:set_pin_mode/2", nifname) == 0 || strcmp("Elixir.GPIO:set_pin_mode/2", nifname) == 0) { AVM_LOGD(TAG, "Resolved platform nif %s ...", nifname); return &gpio_set_pin_mode_nif; diff --git a/src/platforms/stm32/src/lib/stm32_hal_platform.h b/src/platforms/stm32/src/lib/stm32_hal_platform.h new file mode 100644 index 0000000000..18875b9c23 --- /dev/null +++ b/src/platforms/stm32/src/lib/stm32_hal_platform.h @@ -0,0 +1,93 @@ +/* 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 + */ + +#ifndef _STM32_HAL_PLATFORM_H_ +#define _STM32_HAL_PLATFORM_H_ + +/* + * Single include point for STM32 HAL headers. + * The correct family-specific hal.h is selected via the family define + * (e.g. STM32F4XX) which is set by cmake/stm32_device.cmake. + */ + +#if defined(STM32F4XX) +#include "stm32f4xx_hal.h" +#include "stm32f4xx_ll_exti.h" +#include "stm32f4xx_ll_gpio.h" + +#elif defined(STM32H7XX) +#include "stm32h7xx_hal.h" +#include "stm32h7xx_ll_exti.h" +#include "stm32h7xx_ll_gpio.h" + +#elif defined(STM32U5XX) +#include "stm32u5xx_hal.h" +#include "stm32u5xx_ll_exti.h" +#include "stm32u5xx_ll_gpio.h" + +#elif defined(STM32WBXX) +#include "stm32wbxx_hal.h" +#include "stm32wbxx_ll_exti.h" +#include "stm32wbxx_ll_gpio.h" + +#elif defined(STM32H5XX) +#include "stm32h5xx_hal.h" +#include "stm32h5xx_ll_exti.h" +#include "stm32h5xx_ll_gpio.h" + +#elif defined(STM32F7XX) +#include "stm32f7xx_hal.h" +#include "stm32f7xx_ll_exti.h" +#include "stm32f7xx_ll_gpio.h" + +#elif defined(STM32G0XX) +#include "stm32g0xx_hal.h" +#include "stm32g0xx_ll_exti.h" +#include "stm32g0xx_ll_gpio.h" + +#elif defined(STM32G4XX) +#include "stm32g4xx_hal.h" +#include "stm32g4xx_ll_exti.h" +#include "stm32g4xx_ll_gpio.h" + +#elif defined(STM32L4XX) +#include "stm32l4xx_hal.h" +#include "stm32l4xx_ll_exti.h" +#include "stm32l4xx_ll_gpio.h" + +#elif defined(STM32L5XX) +#include "stm32l5xx_hal.h" +#include "stm32l5xx_ll_exti.h" +#include "stm32l5xx_ll_gpio.h" + +#elif defined(STM32F2XX) +#include "stm32f2xx_hal.h" +#include "stm32f2xx_ll_exti.h" +#include "stm32f2xx_ll_gpio.h" + +#elif defined(STM32U3XX) +#include "stm32u3xx_hal.h" +#include "stm32u3xx_ll_exti.h" +#include "stm32u3xx_ll_gpio.h" + +#else +#error "Unsupported STM32 family. Define the appropriate STM32 family macro (e.g. STM32F4XX)." +#endif + +#endif /* _STM32_HAL_PLATFORM_H_ */ diff --git a/src/platforms/stm32/src/lib/stm_sys.h b/src/platforms/stm32/src/lib/stm_sys.h index d40a39690f..e18c803c7d 100644 --- a/src/platforms/stm32/src/lib/stm_sys.h +++ b/src/platforms/stm32/src/lib/stm_sys.h @@ -24,25 +24,9 @@ #include #include -#define STM32_ATOM globalcontext_make_atom(ctx->global, ATOM_STR("\x5", "stm32")) - -/* Only on ARMv7EM and above - * TODO: These definitions are back-ported from libopencm3 `master`, if they ever release a new version this section should - * be removed, along with the included headers and replaced with only the following inclusion: - * `#include ` - */ -#if defined(__ARM_ARCH_7EM__) - -#include -#include -#include +#include "stm32_hal_platform.h" -/** ICIALLU: I-cache invalidate all to Point of Unification */ -#define SCB_ICIALLU MMIO32(SCB_BASE + 0x250) -/** BPIALL: Branch predictor invalidate all */ -#define SCB_BPIALL MMIO32(SCB_BASE + 0x278) - -#endif /* __ARM_ARCH_7EM__ */ +#define STM32_ATOM globalcontext_make_atom(ctx->global, ATOM_STR("\x5", "stm32")) /* Define macros for data and instruction barriers for sys_init_icache() * See: ARM V7-M Architecture Reference Manual :: https://static.docs.arm.com/ddi0403/eb/DDI0403E_B_armv7m_arm.pdf */ @@ -53,18 +37,6 @@ #define __isb asm __volatile__("isb" :: \ : "memory"); -#ifdef LIBOPENCM3_GPIO_COMMON_F24_H -#define GPIO_CLOCK_LIST \ - { \ - RCC_GPIOA, RCC_GPIOB, RCC_GPIOC, RCC_GPIOD, RCC_GPIOE, RCC_GPIOF, RCC_GPIOG, RCC_GPIOH, RCC_GPIOI, RCC_GPIOJ, RCC_GPIOK \ - } -#else -#define GPIO_CLOCK_LIST \ - { \ - RCC_GPIOA, RCC_GPIOB, RCC_GPIOC, RCC_GPIOD, RCC_GPIOE, RCC_GPIOF, RCC_GPIOG, RCC_GPIOH \ - } -#endif - struct LockedPin { struct ListHead locked_pins_list_head; @@ -79,10 +51,10 @@ struct STM32PlatformData void sys_init_icache(void); void sys_enable_flash_cache(void); -void *_sbrk_r(struct _reent *, ptrdiff_t); +void *sbrk(ptrdiff_t); // This function may be defined to relocate the heap. void local_heap_setup(uint8_t **start, uint8_t **end); -void sys_enable_core_periph_clocks(); +void sys_enable_core_periph_clocks(void); bool sys_lock_pin(GlobalContext *glb, uint32_t gpio_bank, uint16_t pin_num); #endif /* _STM_SYS_H_ */ diff --git a/src/platforms/stm32/src/lib/sys.c b/src/platforms/stm32/src/lib/sys.c index 262a2be8e1..9471abf063 100644 --- a/src/platforms/stm32/src/lib/sys.c +++ b/src/platforms/stm32/src/lib/sys.c @@ -18,8 +18,8 @@ * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ #include -#include #include +#include #include #include @@ -30,10 +30,7 @@ // #define ENABLE_TRACE #include -#include -#include -#include -#include +#include "stm32_hal_platform.h" #include "avm_log.h" #include "stm_sys.h" @@ -65,7 +62,7 @@ static void __local_ram(uint8_t **start, uint8_t **end) *end = (uint8_t *) (((uintptr_t) &_stack - RESERVE_STACK_SIZE)); } -void *_sbrk_r(struct _reent *reent, ptrdiff_t diff) +void *sbrk(ptrdiff_t diff) { uint8_t *_old_brk; @@ -75,19 +72,18 @@ void *_sbrk_r(struct _reent *reent, ptrdiff_t diff) _old_brk = _cur_brk; if (_cur_brk + diff > _heap_end) { - reent->_errno = ENOMEM; + errno = ENOMEM; return (void *) -1; } _cur_brk += diff; return _old_brk; } -// Monotonically increasing number of milliseconds from reset static volatile uint64_t system_millis; -// Called when systick fires -void sys_tick_handler() +void SysTick_Handler(void) { + HAL_IncTick(); system_millis++; } @@ -107,15 +103,37 @@ void platform_defaultatoms_init(GlobalContext *glb) UNUSED(glb); } -void sys_enable_core_periph_clocks() +void sys_enable_core_periph_clocks(void) { - uint32_t list[] = GPIO_CLOCK_LIST; - for (size_t i = 0; i < sizeof(list) / sizeof(list[0]); i++) { - rcc_periph_clock_enable((enum rcc_periph_clken) list[i]); - } + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_GPIOB_CLK_ENABLE(); + __HAL_RCC_GPIOC_CLK_ENABLE(); + __HAL_RCC_GPIOD_CLK_ENABLE(); + __HAL_RCC_GPIOE_CLK_ENABLE(); +#ifdef __HAL_RCC_GPIOF_CLK_ENABLE + __HAL_RCC_GPIOF_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_GPIOG_CLK_ENABLE + __HAL_RCC_GPIOG_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_GPIOH_CLK_ENABLE + __HAL_RCC_GPIOH_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_GPIOI_CLK_ENABLE + __HAL_RCC_GPIOI_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_GPIOJ_CLK_ENABLE + __HAL_RCC_GPIOJ_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_GPIOK_CLK_ENABLE + __HAL_RCC_GPIOK_CLK_ENABLE(); +#endif + #ifndef AVM_DISABLE_GPIO_PORT_DRIVER - // This clock enables the syscfg manger for external gpio interupts & ethernet PHY interface. - rcc_periph_clock_enable(RCC_SYSCFG); + /* Enable SYSCFG clock for EXTI configuration (not all families need it) */ +#ifdef __HAL_RCC_SYSCFG_CLK_ENABLE + __HAL_RCC_SYSCFG_CLK_ENABLE(); +#endif #endif } @@ -216,9 +234,16 @@ void sys_monotonic_time(struct timespec *t) sys_clock_gettime(t); } -uint64_t sys_monotonic_time_u64() +uint64_t sys_monotonic_time_u64(void) { - return system_millis; + /* 64-bit read is not atomic on 32-bit ARM; disable interrupts briefly + * to prevent SysTick_Handler from updating system_millis mid-read. + * Save/restore PRIMASK so this is safe if called with IRQs already disabled. */ + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + uint64_t val = system_millis; + __set_PRIMASK(primask); + return val; } uint64_t sys_monotonic_time_ms_to_u64(uint64_t ms) @@ -268,33 +293,52 @@ term sys_get_info(Context *ctx, term key) return UNDEFINED_ATOM; } -void sys_enable_flash_cache() +void sys_enable_flash_cache(void) { - flash_unlock_option_bytes(); - flash_set_ws(FLASH_ACR_LATENCY_5WS); - flash_prefetch_enable(); + /* Enable flash prefetch and set appropriate wait states. + * The exact wait states depend on clock frequency and voltage range, + * but HAL_Init() and SystemClock_Config() will set them correctly. + * Here we just enable prefetch and instruction cache early. */ +#ifdef __HAL_FLASH_PREFETCH_BUFFER_ENABLE + __HAL_FLASH_PREFETCH_BUFFER_ENABLE(); +#endif +#ifdef __HAL_FLASH_INSTRUCTION_CACHE_ENABLE + __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); +#endif +#ifdef __HAL_FLASH_DATA_CACHE_ENABLE + __HAL_FLASH_DATA_CACHE_ENABLE(); +#endif } /* See: ARM V7-M Architecture Reference Manual :: https://static.docs.arm.com/ddi0403/eb/DDI0403E_B_armv7m_arm.pdf */ -void sys_init_icache() +void sys_init_icache(void) +{ + __DSB(); + __ISB(); +#if defined(__ICACHE_PRESENT) && (__ICACHE_PRESENT == 1U) + SCB_EnableICache(); +#endif +#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U) + SCB_EnableDCache(); +#endif + /* STM32H5 and STM32U5 (Cortex-M33) have a dedicated ICACHE peripheral. + * Invalidate then enable after SystemClock_Config() has set flash latency. */ +#ifdef HAL_ICACHE_MODULE_ENABLED + HAL_ICACHE_Invalidate(); + HAL_ICACHE_Enable(); +#endif + __DSB(); + __ISB(); +} + +/* Empty _init/_fini stubs required by __libc_init_array when + * linking with -nostartfiles (which skips crti.o/crtn.o). */ +void _init(void) +{ +} + +void _fini(void) { - // Synchronize data and instruction barriers - __dsb; - __isb; - // Invalidate the instruction cache - SCB_ICIALLU = 0UL; - // Invalidate all branch predictors - SCB_BPIALL = 0UL; - // Re-synchronize - __dsb; - __isb; - // Enable the I-cache - SCB_CCR |= (1 << 17); - // Enable branch prediction - SCB_CCR |= (1 << 18); - // Force a final resync and clear of the instruction pipeline - __dsb; - __isb; } #ifndef AVM_NO_JIT diff --git a/src/platforms/stm32/src/main.c b/src/platforms/stm32/src/main.c index cc2fe7009e..cda86fc2e3 100644 --- a/src/platforms/stm32/src/main.c +++ b/src/platforms/stm32/src/main.c @@ -24,14 +24,6 @@ #include #include -#include -#include -#include -#include -#include -#include -#include - #include #include #include @@ -40,17 +32,13 @@ #include #include +#include "clock_config.h" #include "lib/avm_devcfg.h" #include "lib/avm_log.h" #include "lib/stm_sys.h" -#define USART_CONSOLE (AVM_CONSOLE) -#define USART_TX (AVM_CONSOLE_TX) -#define USART_GPIO (AVM_CONSOLE_GPIO) -#define USART_RCC (AVM_CONSOLE_RCC) #define AVM_ADDRESS (AVM_APP_ADDRESS) #define AVM_FLASH_END (CFG_FLASH_END) -#define CLOCK_FREQUENCY (AVM_CLOCK_HZ) #define TAG "AtomVM" @@ -69,76 +57,73 @@ " ###########################################################\n" \ "\n" -int _write(int file, char *ptr, int len); +static UART_HandleTypeDef huart_console; + pid_t _getpid(void); int _kill(pid_t pid, int sig); -// setup errno to work with newlib -#undef errno -extern int errno; - -static void clock_setup() -{ - // Setup external clock, set divider for device clock frequency - rcc_clock_setup_pll(AVM_CLOCK_CONFIGURATION); -} - static void usart_setup(GlobalContext *glb) { - // Enable clock for USART - rcc_periph_clock_enable(USART_RCC); - - // Setup GPIO pins for USART transmit - gpio_mode_setup(USART_GPIO, GPIO_MODE_AF, GPIO_PUPD_NONE, USART_TX); - sys_lock_pin(glb, USART_GPIO, USART_TX); - - // Setup USART TX pin as alternate function - gpio_set_af(USART_GPIO, GPIO_AF7, USART_TX); - - usart_set_baudrate(USART_CONSOLE, 115200); - usart_set_databits(USART_CONSOLE, 8); - usart_set_stopbits(USART_CONSOLE, USART_STOPBITS_1); - usart_set_mode(USART_CONSOLE, USART_MODE_TX); - usart_set_parity(USART_CONSOLE, USART_PARITY_NONE); - usart_set_flow_control(USART_CONSOLE, USART_FLOWCONTROL_NONE); - - // Finally enable the USART - usart_enable(USART_CONSOLE); -} +#if defined(CONSOLE_1) + __HAL_RCC_USART1_CLK_ENABLE(); +#elif defined(CONSOLE_2) + __HAL_RCC_USART2_CLK_ENABLE(); +#elif defined(CONSOLE_3) + __HAL_RCC_USART3_CLK_ENABLE(); +#elif defined(CONSOLE_4) + __HAL_RCC_UART4_CLK_ENABLE(); +#elif defined(CONSOLE_5) + __HAL_RCC_UART5_CLK_ENABLE(); +#elif defined(CONSOLE_6) + __HAL_RCC_USART6_CLK_ENABLE(); +#endif -// Set up a timer to create 1ms ticks -// The handler is in sys.c -static void systick_setup() -{ - // ((clock rate / 1000) - 1) to get 1ms interrupt rate - systick_set_clocksource(STK_CSR_CLKSOURCE_AHB); - systick_set_reload((CLOCK_FREQUENCY / 1000U) - 1U); - systick_clear(); - systick_counter_enable(); - systick_interrupt_enable(); + GPIO_InitTypeDef gpio_init = { 0 }; + gpio_init.Pin = AVM_CONSOLE_TX_PIN; + gpio_init.Mode = GPIO_MODE_AF_PP; + gpio_init.Pull = GPIO_NOPULL; + gpio_init.Speed = GPIO_SPEED_FREQ_HIGH; + gpio_init.Alternate = AVM_CONSOLE_TX_AF; + HAL_GPIO_Init(AVM_CONSOLE_TX_PORT, &gpio_init); + + sys_lock_pin(glb, (uint32_t) AVM_CONSOLE_TX_PORT, AVM_CONSOLE_TX_PIN); + + huart_console.Instance = AVM_CONSOLE_USART; + huart_console.Init.BaudRate = 115200; + huart_console.Init.WordLength = UART_WORDLENGTH_8B; + huart_console.Init.StopBits = UART_STOPBITS_1; + huart_console.Init.Parity = UART_PARITY_NONE; + huart_console.Init.Mode = UART_MODE_TX; + huart_console.Init.HwFlowCtl = UART_HWCONTROL_NONE; + huart_console.Init.OverSampling = UART_OVERSAMPLING_16; + if (HAL_UART_Init(&huart_console) != HAL_OK) { + while (1) { + } + } } -// Use USART_CONSOLE as a console. -// This is a syscall for newlib -int _write(int file, char *ptr, int len) +// picolibc posix-console syscall +int write(int file, const void *buf, size_t len) { - int i; + const char *ptr = (const char *) buf; + size_t i; if (file == STDOUT_FILENO || file == STDERR_FILENO) { for (i = 0; i < len; i++) { if (ptr[i] == '\n') { - usart_send_blocking(USART_CONSOLE, '\r'); + uint8_t cr = '\r'; + HAL_UART_Transmit(&huart_console, &cr, 1, HAL_MAX_DELAY); } - usart_send_blocking(USART_CONSOLE, ptr[i]); + HAL_UART_Transmit(&huart_console, (uint8_t *) &ptr[i], 1, HAL_MAX_DELAY); } - return i; + return (int) i; } errno = EIO; return -1; } -// newlib stubs to support AVM_ABORT -pid_t _getpid() +// picolibc stubs to support AVM_ABORT +pid_t _getpid(void) { return 1; } @@ -155,39 +140,147 @@ int _kill(pid_t pid, int sig) return -1; } -// Redefine weak linked while(1) loop from libopencm3/cm3/nvic.h. -void hard_fault_handler() +/* HardFault handler that dumps Cortex-M fault status registers. + * The naked attribute + asm extracts the correct stack pointer (MSP or PSP) + * and passes it to the C diagnostic function. + * Note: Cortex-M0/M0+ does not have CFSR/HFSR/MMFAR/BFAR or IT instructions. */ +#if (__CORTEX_M >= 3) +void HardFault_Diagnostic(uint32_t *stack_frame) +{ + volatile uint32_t cfsr = SCB->CFSR; + volatile uint32_t hfsr = SCB->HFSR; + volatile uint32_t mmfar = SCB->MMFAR; + volatile uint32_t bfar = SCB->BFAR; + + uint32_t stacked_r0 = stack_frame[0]; + uint32_t stacked_r1 = stack_frame[1]; + uint32_t stacked_r2 = stack_frame[2]; + uint32_t stacked_r3 = stack_frame[3]; + uint32_t stacked_r12 = stack_frame[4]; + uint32_t stacked_lr = stack_frame[5]; + uint32_t stacked_pc = stack_frame[6]; + uint32_t stacked_psr = stack_frame[7]; + + fprintf(stderr, "\n--- Hard Fault ---\n"); + fprintf(stderr, "CFSR: 0x%08lx\n", (unsigned long) cfsr); + fprintf(stderr, "HFSR: 0x%08lx\n", (unsigned long) hfsr); + fprintf(stderr, "MMFAR: 0x%08lx\n", (unsigned long) mmfar); + fprintf(stderr, "BFAR: 0x%08lx\n", (unsigned long) bfar); + fprintf(stderr, "PC: 0x%08lx\n", (unsigned long) stacked_pc); + fprintf(stderr, "LR: 0x%08lx\n", (unsigned long) stacked_lr); + fprintf(stderr, "R0: 0x%08lx\n", (unsigned long) stacked_r0); + fprintf(stderr, "R1: 0x%08lx\n", (unsigned long) stacked_r1); + fprintf(stderr, "R2: 0x%08lx\n", (unsigned long) stacked_r2); + fprintf(stderr, "R3: 0x%08lx\n", (unsigned long) stacked_r3); + fprintf(stderr, "R12: 0x%08lx\n", (unsigned long) stacked_r12); + fprintf(stderr, "PSR: 0x%08lx\n", (unsigned long) stacked_psr); + + /* Decode CFSR bits */ + if (cfsr & 0x8000) { + fprintf(stderr, " BFAR valid\n"); + } + if (cfsr & 0x2000) { + fprintf(stderr, " Lazy FP stacking BusFault\n"); + } + if (cfsr & 0x1000) { + fprintf(stderr, " Stacking BusFault\n"); + } + if (cfsr & 0x0800) { + fprintf(stderr, " Unstacking BusFault\n"); + } + if (cfsr & 0x0400) { + fprintf(stderr, " Imprecise BusFault\n"); + } + if (cfsr & 0x0200) { + fprintf(stderr, " Precise BusFault\n"); + } + if (cfsr & 0x0100) { + fprintf(stderr, " Instruction BusFault\n"); + } + if (cfsr & 0x80) { + fprintf(stderr, " MMFAR valid\n"); + } + if (cfsr & 0x20) { + fprintf(stderr, " Lazy FP stacking MemManage\n"); + } + if (cfsr & 0x10) { + fprintf(stderr, " Stacking MemManage\n"); + } + if (cfsr & 0x08) { + fprintf(stderr, " Unstacking MemManage\n"); + } + if (cfsr & 0x02) { + fprintf(stderr, " Data access MemManage\n"); + } + if (cfsr & 0x01) { + fprintf(stderr, " Instruction access MemManage\n"); + } + if (cfsr & 0x02000000) { + fprintf(stderr, " Divide by zero UsageFault\n"); + } + if (cfsr & 0x01000000) { + fprintf(stderr, " Unaligned access UsageFault\n"); + } + if (cfsr & 0x00080000) { + fprintf(stderr, " No coprocessor UsageFault\n"); + } + if (cfsr & 0x00040000) { + fprintf(stderr, " Invalid PC UsageFault\n"); + } + if (cfsr & 0x00020000) { + fprintf(stderr, " Invalid state UsageFault\n"); + } + if (cfsr & 0x00010000) { + fprintf(stderr, " Undefined instruction UsageFault\n"); + } + + while (1) { + } +} + +__attribute__((naked)) void HardFault_Handler(void) { - fprintf(stderr, "\nHard Fault detected!\n"); - AVM_ABORT(); + __asm volatile( + "tst lr, #4 \n" + "ite eq \n" + "mrseq r0, msp \n" + "mrsne r0, psp \n" + "b HardFault_Diagnostic \n"); } -/* define newlib weakly defined functions to prevent compiler warnings - * Reference: https://sourceware.org/newlib/libc.html#Stubs - * These are minimal non-functional implementations that can be modified as needed to - * implement functionality. - */ +#else /* Cortex-M0/M0+ */ -int _close(int file) +void HardFault_Handler(void) +{ + fprintf(stderr, "\n--- Hard Fault ---\n"); + while (1) { + } +} + +#endif /* __CORTEX_M >= 3 */ + +/* Stubs for picolibc's POSIX console and libc requirements */ + +int close(int file) { UNUSED(file); return -1; } -int _fstat_r(int file, struct stat *st) +int fstat(int file, struct stat *st) { UNUSED(file); st->st_mode = S_IFCHR; return 0; } -int _isatty(int file) +int isatty(int file) { UNUSED(file); return 1; } -off_t _lseek_r(int file, off_t ptr, int dir) +off_t lseek(int file, off_t ptr, int dir) { UNUSED(file); UNUSED(ptr); @@ -195,15 +288,7 @@ off_t _lseek_r(int file, off_t ptr, int dir) return 0; } -int open(const char *name, int flags, int mode) -{ - UNUSED(name); - UNUSED(flags); - UNUSED(mode); - return -1; -} - -int _read_r(int file, void *ptr, size_t len) +int read(int file, void *ptr, size_t len) { UNUSED(file); UNUSED(ptr); @@ -211,30 +296,30 @@ int _read_r(int file, void *ptr, size_t len) return 0; } -int unlink(const char *name) +void _exit(int status) { - UNUSED(name); - errno = ENOENT; - return -1; + UNUSED(status); + while (1) { + } } -int main() +int main(void) { - // Flash cache must be enabled before system clock is activated + HAL_Init(); sys_enable_flash_cache(); + SystemClock_Config(); sys_init_icache(); - clock_setup(); - systick_setup(); - // Start core peripheral clocks now so there are no accidental resets of peripherals that share a clock later. sys_enable_core_periph_clocks(); GlobalContext *glb = globalcontext_new(); usart_setup(glb); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + fprintf(stdout, "%s", ATOMVM_BANNER); AVM_LOGI(TAG, "Starting AtomVM revision " ATOMVM_VERSION); - AVM_LOGD(TAG, "Using usart mapped at register 0x%x for stdout/stderr.", USART_CONSOLE); const void *flashed_avm = (void *) AVM_ADDRESS; uint32_t size = (AVM_FLASH_END - AVM_ADDRESS); @@ -281,11 +366,11 @@ int main() #endif if (reboot_on_not_ok && result != RUN_SUCCESS) { AVM_LOGE(TAG, "AtomVM application terminated with non-ok return value. Rebooting ..."); - scb_reset_system(); + NVIC_SystemReset(); } else { AVM_LOGI(TAG, "AtomVM application terminated. Going to sleep forever ..."); // Disable all interrupts - cm_disable_interrupts(); + __disable_irq(); while (1) { ; } diff --git a/src/platforms/stm32/tests/renode/stm32_boot_test.robot b/src/platforms/stm32/tests/renode/stm32_boot_test.robot new file mode 100644 index 0000000000..735cc87961 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32_boot_test.robot @@ -0,0 +1,42 @@ +# +# 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 +# + +*** Settings *** +Suite Setup Setup +Suite Teardown Teardown +Test Setup Reset Emulation +Resource ${RENODEKEYWORDS} + +*** Variables *** +${UART} sysbus.usart1 +${PLATFORM} @platforms/cpus/stm32f4.repl +${ELF} REQUIRED +${AVM} REQUIRED +${AVM_ADDRESS} 0x08060000 + +*** Test Cases *** +AtomVM Should Boot And Print Platform + Execute Command mach create + Execute Command machine LoadPlatformDescription ${PLATFORM} + Execute Command sysbus LoadELF ${ELF} + Execute Command sysbus LoadBinary ${AVM} ${AVM_ADDRESS} + Create Terminal Tester ${UART} + Start Emulation + Wait For Line On Uart stm32 timeout=60 diff --git a/src/platforms/stm32/tests/renode/stm32_gpio_test.robot b/src/platforms/stm32/tests/renode/stm32_gpio_test.robot new file mode 100644 index 0000000000..bfa8f24b87 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32_gpio_test.robot @@ -0,0 +1,46 @@ +# +# 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 +# + +*** Settings *** +Suite Setup Setup +Suite Teardown Teardown +Test Setup Reset Emulation +Resource ${RENODEKEYWORDS} + +*** Variables *** +${UART} sysbus.usart1 +${PLATFORM} @platforms/cpus/stm32f4.repl +${ELF} REQUIRED +${AVM} REQUIRED +${AVM_ADDRESS} 0x08060000 + +*** Test Cases *** +AtomVM Should Drive GPIO Pin + Execute Command mach create + Execute Command machine LoadPlatformDescription ${PLATFORM} + Execute Command machine LoadPlatformDescriptionFromString "led: Miscellaneous.LED @ gpioPortA" + Execute Command machine LoadPlatformDescriptionFromString "gpioPortA: { 5 -> led@0 }" + Execute Command sysbus LoadELF ${ELF} + Execute Command sysbus LoadBinary ${AVM} ${AVM_ADDRESS} + Create Terminal Tester ${UART} + Create LED Tester sysbus.gpioPortA.led + Start Emulation + Wait For Line On Uart gpio_done timeout=60 + Assert LED State true timeout=1 diff --git a/src/platforms/stm32/tests/renode/stm32g0b1.repl b/src/platforms/stm32/tests/renode/stm32g0b1.repl new file mode 100644 index 0000000000..4d3a5ea5b8 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32g0b1.repl @@ -0,0 +1,60 @@ +// +// 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 +// + +using "platforms/cpus/stm32g0.repl" + +flash: + size: 0x80000 + +ram: + size: 0x24000 + +rcc: + script: ''' +if request.IsInit: + lastVal = 0 + regs = {} + data = {'hsion': 1, 'hseon': 0, 'pllon': 0, 'sw': 0, 'lsion': 0} + +if request.IsWrite: + regs[request.Offset] = request.Value + if request.Offset == 0x0: + data['hsion'] = (request.Value >> 8) & 0x1 + data['hseon'] = (request.Value >> 16) & 0x1 + data['pllon'] = (request.Value >> 24) & 0x1 + elif request.Offset == 0x8: + data['sw'] = request.Value & 0x7 + elif request.Offset == 0x60: + data['lsion'] = request.Value & 0x1 +elif request.IsRead: + lastVal = 1 - lastVal + if request.Offset == 0x0: + request.Value = (data['hsion'] << 8) | (data['hsion'] << 10) | (data['hseon'] << 16) | (data['hseon'] << 17) | (data['pllon'] << 24) | (data['pllon'] << 25) + elif request.Offset == 0x4: + request.Value = lastVal * 0xFFFFFFF8 + elif request.Offset == 0x8: + request.Value = (regs.get(0x8, 0) & ~0x38) | (data['sw'] << 3) + elif request.Offset == 0x24: + request.Value = 0x0 + elif request.Offset == 0x60: + request.Value = (data['lsion'] << 1) + else: + request.Value = regs.get(request.Offset, 0) +''' diff --git a/src/platforms/stm32/tests/renode/stm32h743.repl b/src/platforms/stm32/tests/renode/stm32h743.repl new file mode 100644 index 0000000000..96cce98596 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32h743.repl @@ -0,0 +1,125 @@ +// +// 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 +// + +dtcmram: Memory.MappedMemory @ sysbus 0x20000000 + size: 0x20000 + +axiram: Memory.MappedMemory @ sysbus 0x24000000 + size: 0x80000 + +flash: Memory.MappedMemory @ sysbus 0x08000000 + size: 0x200000 + +nvic: IRQControllers.NVIC @ sysbus 0xE000E000 + priorityMask: 0xF0 + systickFrequency: 480000000 + IRQ -> cpu@0 + +cpu: CPU.CortexM @ sysbus + cpuType: "cortex-m7" + nvic: nvic + +usart1: UART.STM32F7_USART @ sysbus <0x40011000, +0x100> + frequency: 120000000 + IRQ -> nvic@37 + +// RCC Python peripheral at 0x58024400. +// H7 RCC register layout: +// CR (0x00): HSION[0], HSIRDY[2], CSION[7], CSIRDY[8], +// HSEON[16], HSERDY[17], PLL1ON[24], PLL1RDY[25], +// PLL2ON[26], PLL2RDY[27], PLL3ON[28], PLL3RDY[29] +// CFGR (0x10): SW[2:0], SWS[5:3] +rcc: Python.PythonPeripheral @ sysbus 0x58024400 + size: 0x400 + initable: true + script: ''' +if request.IsInit: + regs = {0x0: 0x83} + +if request.IsWrite: + regs[request.Offset] = request.Value + if request.Offset == 0x10: + sw = request.Value & 0x7 + regs[0x10] = (request.Value & ~0x38) | (sw << 3) +elif request.IsRead: + if request.Offset == 0x0: + cr = regs.get(0x0, 0x83) + hsion = cr & 0x1 + csion = (cr >> 7) & 0x1 + hseon = (cr >> 16) & 0x1 + pll1on = (cr >> 24) & 0x1 + pll2on = (cr >> 26) & 0x1 + pll3on = (cr >> 28) & 0x1 + cr &= ~((1 << 2) | (1 << 8) | (1 << 17) | (1 << 25) | (1 << 27) | (1 << 29)) + request.Value = cr | (hsion << 2) | (csion << 8) | (hseon << 17) | (pll1on << 25) | (pll2on << 27) | (pll3on << 29) + else: + request.Value = regs.get(request.Offset, 0) +''' + +// PWR Python peripheral at 0x58024800. +// H7 PWR register layout: +// CR1 (0x00), CSR1 (0x04): ACTVOSRDY[13] +// CR3 (0x0C): LDOEN[1] +// D3CR (0x18): VOSRDY[13], VOS[15:14] +// CSR1 ACTVOSRDY and D3CR VOSRDY are always set (ready). +pwr: Python.PythonPeripheral @ sysbus 0x58024800 + size: 0x400 + initable: true + script: ''' +if request.IsInit: + regs = {0x0C: 0x2} + +if request.IsWrite: + regs[request.Offset] = request.Value +elif request.IsRead: + request.Value = regs.get(request.Offset, 0) + if request.Offset == 0x04: + request.Value |= (1 << 13) + elif request.Offset == 0x18: + request.Value |= (1 << 13) +''' + +// Flash controller at 0x52002000 (D1_AHB1PERIPH_BASE + 0x2000). +// HAL_RCC_ClockConfig reads back FLASH_ACR to verify latency was applied. +flash_ctrl: Python.PythonPeripheral @ sysbus 0x52002000 + size: 0x400 + initable: true + script: ''' +if request.IsInit: + regs = {} + +if request.IsWrite: + regs[request.Offset] = request.Value +elif request.IsRead: + request.Value = regs.get(request.Offset, 0) +''' + +gpioPortA: GPIOPort.STM32_GPIOPort @ sysbus <0x58020000, +0x400> + modeResetValue: 0xABFFFFFF + numberOfAFs: 16 + +sysbus: + init: + Tag <0x58000400, 0x580007FF> "SYSCFG" + Tag <0x5C001000, 0x5C0013FF> "DBGMCU" + Tag <0x51008000, 0x510083FF> "ART" + Tag <0x52004000, 0x520043FF> "OCTOSPI" + Tag <0x40020000, 0x400203FF> "DMA1" + Tag <0x40020400, 0x400207FF> "DMA2" diff --git a/src/platforms/stm32/tests/renode/stm32l562.repl b/src/platforms/stm32/tests/renode/stm32l562.repl new file mode 100644 index 0000000000..f6a1d613ec --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32l562.repl @@ -0,0 +1,109 @@ +// +// 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 +// + +using "platforms/cpus/stm32l552.repl" + +sram1: + size: 0x40000 + +flash: + size: 0x80000 + +// RCC Python peripheral to handle clock configuration. +// The base stm32l552.repl uses static Tags for RCC_CR and RCC_CFGR which +// don't support clock switching (SWS never changes), causing +// HAL_RCC_ClockConfig to hang. +// +// L5 RCC register layout: +// CR (0x00): MSION[0], MSIRDY[1], HSION[8], HSIRDY[10], +// HSEON[16], HSERDY[17], PLLON[24], PLLRDY[25], +// PLLSAI1ON[26], PLLSAI1RDY[27], PLLSAI2ON[28], PLLSAI2RDY[29] +// CFGR (0x08): SW[1:0], SWS[3:2] +rcc: Python.PythonPeripheral @ sysbus 0x40021000 + size: 0x400 + initable: true + script: ''' +if request.IsInit: + regs = {0x0: 0x63} + +if request.IsWrite: + regs[request.Offset] = request.Value + if request.Offset == 0x8: + sw = request.Value & 0x3 + regs[0x8] = (request.Value & ~0xC) | (sw << 2) +elif request.IsRead: + if request.Offset == 0x0: + cr = regs.get(0x0, 0x63) + msion = cr & 0x1 + hsion = (cr >> 8) & 0x1 + hseon = (cr >> 16) & 0x1 + pllon = (cr >> 24) & 0x1 + pllsai1on = (cr >> 26) & 0x1 + pllsai2on = (cr >> 28) & 0x1 + cr &= ~((1 << 1) | (1 << 10) | (1 << 17) | (1 << 25) | (1 << 27) | (1 << 29)) + request.Value = cr | (msion << 1) | (hsion << 10) | (hseon << 17) | (pllon << 25) | (pllsai1on << 27) | (pllsai2on << 29) + else: + request.Value = regs.get(request.Offset, 0) +''' + +// PWR Python peripheral for voltage scaling. +// Stores writes and returns them on read. +// CR1 (offset 0x00): VOS[10:9] defaults to Range 1 (0x200). +// SR1 (offset 0x10): SMPSHPRDY (bit 15) always set = SMPS high-power ready. +// SR2 (offset 0x14): VOSF (bit 10) always clear = scaling complete. +pwr: Python.PythonPeripheral @ sysbus 0x40007000 + size: 0x400 + initable: true + script: ''' +if request.IsInit: + regs = {0x0: 0x200} + +if request.IsWrite: + regs[request.Offset] = request.Value +elif request.IsRead: + request.Value = regs.get(request.Offset, 0) + if request.Offset == 0x10: + request.Value |= (1 << 15) + elif request.Offset == 0x14: + request.Value &= ~(1 << 10) +''' + +// Flash controller for FLASH_ACR (latency, prefetch, cache enable). +// HAL_RCC_ClockConfig reads back FLASH_ACR to verify latency was applied. +flash_ctrl: MTD.STM32F4_FlashController @ sysbus 0x40022000 + flash: flash + +// ICACHE peripheral stub. +// HAL_ICACHE_Invalidate polls SR (offset 0x04) for BSYENDF (bit 1) = 1 +// and BUSYF (bit 0) = 0. +icache: Python.PythonPeripheral @ sysbus 0x40030400 + size: 0x400 + initable: true + script: ''' +if request.IsInit: + regs = {} + +if request.IsWrite: + regs[request.Offset] = request.Value +elif request.IsRead: + request.Value = regs.get(request.Offset, 0) + if request.Offset == 0x04: + request.Value = (request.Value | 0x2) & ~0x1 +''' diff --git a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt new file mode 100644 index 0000000000..21932080b9 --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# 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(BuildErlang) + +pack_runnable(stm32_boot_test test_boot) +pack_runnable(stm32_gpio_test test_gpio eavmlib) diff --git a/src/platforms/stm32/tests/test_erl_sources/test_boot.erl b/src/platforms/stm32/tests/test_erl_sources/test_boot.erl new file mode 100644 index 0000000000..d03b54537e --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/test_boot.erl @@ -0,0 +1,25 @@ +% +% 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 +% + +-module(test_boot). +-export([start/0]). + +start() -> + erlang:display(atomvm:platform()). diff --git a/src/platforms/stm32/tests/test_erl_sources/test_gpio.erl b/src/platforms/stm32/tests/test_erl_sources/test_gpio.erl new file mode 100644 index 0000000000..be23be1f39 --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/test_gpio.erl @@ -0,0 +1,31 @@ +% +% 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 +% + +-module(test_gpio). +-export([start/0]). + +start() -> + gpio:set_pin_mode({a, 5}, output), + gpio:digital_write({a, 5}, 1), + erlang:display(gpio_high), + gpio:digital_write({a, 5}, 0), + erlang:display(gpio_low), + gpio:digital_write({a, 5}, 1), + erlang:display(gpio_done). diff --git a/src/platforms/stm32/tools/atomvm_stm32_config_query.erl b/src/platforms/stm32/tools/atomvm_stm32_config_query.erl old mode 100755 new mode 100644 index 0f308a4951..7dea8adaac --- a/src/platforms/stm32/tools/atomvm_stm32_config_query.erl +++ b/src/platforms/stm32/tools/atomvm_stm32_config_query.erl @@ -55,7 +55,9 @@ main(Args) -> error() -> io:format("~n Usage: ~s ~n", [?MODULE_STRING]), - io:format("~n Where is one of: clock | rom | ram | ccm~n"), + io:format( + "~n Where is one of: clock | rom | ram | ccm | flash_size_kb | ram_size_kb~n" + ), erlang:halt(255). get_dev_config(Device) -> @@ -71,6 +73,42 @@ get_dev_config(Device) -> _ -> Config end; + "stm32h7" -> + Config = get_h7dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32u5" -> + Config = get_u5dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32wb" -> + Config = get_wbdev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32h5" -> + Config = get_h5dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; "stm32f7" -> Config = get_f7dev_config(Lookup), case Config of @@ -80,6 +118,60 @@ get_dev_config(Device) -> _ -> Config end; + "stm32g0" -> + Config = get_g0dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32g4" -> + Config = get_g4dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32l4" -> + Config = get_l4dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32l5" -> + Config = get_l5dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32f2" -> + Config = get_f2dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; + "stm32u3" -> + Config = get_u3dev_config(Lookup), + case Config of + unsupported -> + io:format("Error! Unsupported device ~s.~n", [Device]), + erlang:halt(255); + _ -> + Config + end; _ -> io:format("Error! Unsupported device ~s.~n", [Device]), erlang:halt(255) @@ -243,121 +335,321 @@ get_f4dev_config(Lookup) -> unsupported end. -get_f7dev_config(Lookup) -> +get_h7dev_config(Lookup) -> Line = string:slice(Lookup, 0, 2), Flash = string:slice(Lookup, 3, 1), case Line of - "22" -> + "43" -> case Flash of - "e" -> - ?STM32F7_23_23_E; + "i" -> + ?STM32H743_I; + "g" -> + ?STM32H743_G; _ -> unsupported end; - "23" -> + _ -> + unsupported + end. + +get_u5dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "85" -> case Flash of - "e" -> - ?STM32F7_23_23_E; + "i" -> + ?STM32U585_I; _ -> unsupported end; - "32" -> + _ -> + unsupported + end. + +get_wbdev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "55" -> case Flash of + "g" -> + ?STM32WB55_G; "e" -> - ?STM32F7_23_23_E; + ?STM32WB55_E; _ -> unsupported end; - "33" -> + _ -> + unsupported + end. + +get_h5dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "62" -> case Flash of - "e" -> - ?STM32F7_23_23_E; + "g" -> + ?STM32H562_G; + "i" -> + ?STM32H562_I; _ -> unsupported end; - "45" -> + _ -> + unsupported + end. + +get_f7dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "22" -> f7_72_73(Flash); + "23" -> f7_72_73(Flash); + "32" -> f7_72_73(Flash); + "33" -> f7_72_73(Flash); + "30" -> f7_72_73(Flash); + "50" -> f7_72_73(Flash); + "45" -> f7_74_75(Flash); + "46" -> f7_74_75(Flash); + "56" -> f7_74_75(Flash); + "65" -> f7_76_77(Flash); + "67" -> f7_76_77(Flash); + "69" -> f7_76_77(Flash); + "77" -> f7_76_77(Flash); + "79" -> f7_76_77(Flash); + _ -> unsupported + end. + +f7_72_73(Flash) -> + case Flash of + "e" -> ?STM32F72_73_E; + _ -> unsupported + end. + +f7_74_75(Flash) -> + case Flash of + "e" -> ?STM32F74_75_E; + "g" -> ?STM32F74_75_G; + _ -> unsupported + end. + +f7_76_77(Flash) -> + case Flash of + "e" -> ?STM32F76_77_E; + "g" -> ?STM32F76_77_G; + "i" -> ?STM32F76_77_I; + _ -> unsupported + end. + +get_g0dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "b1" -> + case Flash of + "e" -> ?STM32G0B1_E; + _ -> unsupported + end; + "71" -> case Flash of - "e" -> - ?STM32F745_E; - "g" -> - ?STM32F745_G; - _ -> - unsupported + "b" -> ?STM32G071_B; + _ -> unsupported end; - "46" -> + _ -> + unsupported + end. + +get_g4dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "73" -> case Flash of - "e" -> - ?STM32F7_45_6_E; - "g" -> - ?STM32F7_45_6_G; - _ -> - unsupported + "e" -> ?STM32G47_48_E; + _ -> unsupported end; - "56" -> + "74" -> case Flash of - "e" -> - ?STM32F7_45_6_E; - "g" -> - ?STM32F7_45_6_G; - _ -> - unsupported + "e" -> ?STM32G47_48_E; + _ -> unsupported end; - "65" -> + "83" -> case Flash of - "g" -> - ?STM32F765_G; - "i" -> - ?STM32F765_I; - _ -> - unsupported + "e" -> ?STM32G47_48_E; + _ -> unsupported end; - "67" -> + "84" -> case Flash of - "g" -> - ?STM32F7_67_7_G; - "i" -> - ?STM32F7_67_7_I; - _ -> - unsupported + "e" -> ?STM32G47_48_E; + _ -> unsupported end; - "68" -> + "91" -> case Flash of - "i" -> - ?STM32F7_67_89_I; - _ -> - unsupported + "e" -> ?STM32G49_4A_E; + _ -> unsupported end; - "69" -> + "a1" -> case Flash of - "g" -> - ?STM32F769_G; - "i" -> - ?STM32F7_67_89_I; - _ -> - unsupported + "e" -> ?STM32G49_4A_E; + _ -> unsupported end; - "77" -> + _ -> + unsupported + end. + +get_l4dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "51" -> case Flash of - "g" -> - ?STM32F7_67_7_G; - "i" -> - ?STM32F7_67_7_I; - _ -> - unsupported + "e" -> ?STM32L45_46_E; + _ -> unsupported end; - "78" -> + "52" -> case Flash of - "i" -> - ?STM32F7_67_89_I; - _ -> - unsupported + "e" -> ?STM32L45_46_E; + _ -> unsupported end; - "79" -> + "62" -> case Flash of - "i" -> - ?STM32F7_67_89_I; - _ -> - unsupported + "e" -> ?STM32L45_46_E; + _ -> unsupported + end; + "71" -> + case Flash of + "e" -> ?STM32L47_E; + "g" -> ?STM32L47_48_G; + _ -> unsupported + end; + "75" -> + case Flash of + "e" -> ?STM32L47_E; + "g" -> ?STM32L47_48_G; + _ -> unsupported + end; + "76" -> + case Flash of + "e" -> ?STM32L47_E; + "g" -> ?STM32L47_48_G; + _ -> unsupported + end; + "85" -> + case Flash of + "e" -> ?STM32L47_E; + "g" -> ?STM32L47_48_G; + _ -> unsupported + end; + "86" -> + case Flash of + "e" -> ?STM32L47_E; + "g" -> ?STM32L47_48_G; + _ -> unsupported + end; + "96" -> + case Flash of + "g" -> ?STM32L49_4A_G; + _ -> unsupported + end; + "a6" -> + case Flash of + "g" -> ?STM32L49_4A_G; + _ -> unsupported + end; + "r5" -> + case Flash of + "g" -> ?STM32L4R_4S_G; + "i" -> ?STM32L4R_4S_I; + _ -> unsupported + end; + "r7" -> + case Flash of + "g" -> ?STM32L4R_4S_G; + "i" -> ?STM32L4R_4S_I; + _ -> unsupported + end; + "r9" -> + case Flash of + "g" -> ?STM32L4R_4S_G; + "i" -> ?STM32L4R_4S_I; + _ -> unsupported + end; + "s5" -> + case Flash of + "g" -> ?STM32L4R_4S_G; + "i" -> ?STM32L4R_4S_I; + _ -> unsupported + end; + "s7" -> + case Flash of + "g" -> ?STM32L4R_4S_G; + "i" -> ?STM32L4R_4S_I; + _ -> unsupported + end; + "s9" -> + case Flash of + "g" -> ?STM32L4R_4S_G; + "i" -> ?STM32L4R_4S_I; + _ -> unsupported + end; + _ -> + unsupported + end. + +get_l5dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "52" -> + case Flash of + "e" -> ?STM32L55_56_E; + _ -> unsupported + end; + "62" -> + case Flash of + "e" -> ?STM32L55_56_E; + _ -> unsupported + end; + _ -> + unsupported + end. + +get_f2dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "05" -> f2_flash(Flash); + "07" -> f2_flash(Flash); + "15" -> f2_flash(Flash); + "17" -> f2_flash(Flash); + _ -> unsupported + end. + +f2_flash(Flash) -> + case Flash of + "e" -> ?STM32F2_E; + "f" -> ?STM32F2_F; + "g" -> ?STM32F2_G; + _ -> unsupported + end. + +get_u3dev_config(Lookup) -> + Line = string:slice(Lookup, 0, 2), + Flash = string:slice(Lookup, 3, 1), + case Line of + "75" -> + case Flash of + "e" -> ?STM32U37_38_E; + "g" -> ?STM32U37_38_G; + _ -> unsupported + end; + "85" -> + case Flash of + "e" -> ?STM32U37_38_E; + "g" -> ?STM32U37_38_G; + _ -> unsupported end; _ -> unsupported @@ -385,6 +677,10 @@ get_param_key(Parameter) -> ccm; ["clock"] -> clock; + ["flash_size_kb"] -> + flash_size_kb; + ["ram_size_kb"] -> + ram_size_kb; _ -> io:format("Error: ~p is not a valid configuration parameter.~n", [Parameter]), erlang:halt(255) diff --git a/src/platforms/stm32/tools/device_config.hrl b/src/platforms/stm32/tools/device_config.hrl index 1e3a311d06..392f12af50 100644 --- a/src/platforms/stm32/tools/device_config.hrl +++ b/src/platforms/stm32/tools/device_config.hrl @@ -19,63 +19,343 @@ % %% STM32F4 Family configurations --define(STM32F401_E, #{rom => "ROM_512K", ram => "RAM_64K", clock => "MHZ_84"}). +-define(STM32F401_E, #{ + rom => "ROM_512K", + ram => "RAM_64K", + clock => "84000000", + flash_size_kb => "512", + ram_size_kb => "64" +}). -define(STM32F4_01_57_E, #{ - rom => "ROM_512K", ram => "RAM_128K", ccm => "CCM_64K", clock => "MHZ_168" + rom => "ROM_512K", + ram => "RAM_128K", + ccm => "CCM_64K", + clock => "168000000", + flash_size_kb => "512", + ram_size_kb => "128" }). -define(STM32F4_01_57_G, #{ - rom => "ROM_1024K", ram => "RAM_128K", ccm => "CCM_64K", clock => "MHZ_168" + rom => "ROM_1024K", + ram => "RAM_128K", + ccm => "CCM_64K", + clock => "168000000", + flash_size_kb => "1024", + ram_size_kb => "128" +}). +-define(STM32F411_E, #{ + rom => "ROM_512K", + ram => "RAM_128K", + clock => "100000000", + flash_size_kb => "512", + ram_size_kb => "128" +}). +-define(STM32F412_E, #{ + rom => "ROM_512K", + ram => "RAM_256K", + clock => "100000000", + flash_size_kb => "512", + ram_size_kb => "256" +}). +-define(STM32F412_G, #{ + rom => "ROM_1024K", + ram => "RAM_256K", + clock => "100000000", + flash_size_kb => "1024", + ram_size_kb => "256" }). --define(STM32F411_E, #{rom => "ROM_512K", ram => "RAM_128K", clock => "MHZ_100"}). --define(STM32F412_E, #{rom => "ROM_512K", ram => "RAM_256K", clock => "MHZ_100"}). --define(STM32F412_G, #{rom => "ROM_1024K", ram => "RAM_256K", clock => "MHZ_100"}). -define(STM32F4_12_3_G, #{ - rom => "ROM_1024K", ram => "RAM_256K", ccm => "CCM_64K", clock => "MHZ_100" + rom => "ROM_1024K", + ram => "RAM_256K", + ccm => "CCM_64K", + clock => "100000000", + flash_size_kb => "1024", + ram_size_kb => "256" }). -define(STM32F4_12_3_H, #{ - rom => "ROM_1536K", ram => "RAM_256K", ccm => "CCM_64K", clock => "MHZ_100" + rom => "ROM_1536K", + ram => "RAM_256K", + ccm => "CCM_64K", + clock => "100000000", + flash_size_kb => "1536", + ram_size_kb => "256" }). -define(STM32F4_23_79_E, #{ - rom => "ROM_512K", ram => "RAM_192K", ccm => "CCM_64K", clock => "MHZ_180" + rom => "ROM_512K", + ram => "RAM_192K", + ccm => "CCM_64K", + clock => "180000000", + flash_size_kb => "512", + ram_size_kb => "192" }). -define(STM32F4_23_79_G, #{ - rom => "ROM_1024K", ram => "RAM_192K", ccm => "CCM_64K", clock => "MHZ_180" + rom => "ROM_1024K", + ram => "RAM_192K", + ccm => "CCM_64K", + clock => "180000000", + flash_size_kb => "1024", + ram_size_kb => "192" }). -define(STM32F4_23_79_I, #{ - rom => "ROM_2048K", ram => "RAM_192K", ccm => "CCM_64K", clock => "MHZ_180" + rom => "ROM_2048K", + ram => "RAM_192K", + ccm => "CCM_64K", + clock => "180000000", + flash_size_kb => "2048", + ram_size_kb => "192" +}). +-define(STM32F446_E, #{ + rom => "ROM_512K", + ram => "RAM_128K", + clock => "180000000", + flash_size_kb => "512", + ram_size_kb => "128" }). --define(STM32F446_E, #{rom => "ROM_512K", ram => "RAM_128K", clock => "MHZ_180"}). -define(STM32F4_67_9_E, #{ - rom => "ROM_512K", ram => "RAM_320K", ccm => "CCM_64K", clock => "MHZ_180" + rom => "ROM_512K", + ram => "RAM_320K", + ccm => "CCM_64K", + clock => "180000000", + flash_size_kb => "512", + ram_size_kb => "320" }). -define(STM32F4_67_9_G, #{ - rom => "ROM_1024K", ram => "RAM_320K", ccm => "CCM_64K", clock => "MHZ_180" + rom => "ROM_1024K", + ram => "RAM_320K", + ccm => "CCM_64K", + clock => "180000000", + flash_size_kb => "1024", + ram_size_kb => "320" }). -define(STM32F4_67_9_I, #{ - rom => "ROM_2048K", ram => "RAM_320K", ccm => "CCM_64K", clock => "MHZ_180" + rom => "ROM_2048K", + ram => "RAM_320K", + ccm => "CCM_64K", + clock => "180000000", + flash_size_kb => "2048", + ram_size_kb => "320" +}). + +%% STM32H7 Family configurations +-define(STM32H743_I, #{ + rom => "ROM_2048K", + ram => "RAM_512K", + clock => "480000000", + flash_size_kb => "2048", + ram_size_kb => "512" +}). +-define(STM32H743_G, #{ + rom => "ROM_1024K", + ram => "RAM_512K", + clock => "480000000", + flash_size_kb => "1024", + ram_size_kb => "512" +}). + +%% STM32U5 Family configurations +-define(STM32U585_I, #{ + rom => "ROM_2048K", + ram => "RAM_768K", + clock => "160000000", + flash_size_kb => "2048", + ram_size_kb => "768" +}). + +%% STM32WB Family configurations +-define(STM32WB55_G, #{ + rom => "ROM_1024K", + ram => "RAM_256K", + clock => "64000000", + flash_size_kb => "1024", + ram_size_kb => "256" +}). +-define(STM32WB55_E, #{ + rom => "ROM_512K", + ram => "RAM_256K", + clock => "64000000", + flash_size_kb => "512", + ram_size_kb => "256" +}). + +%% STM32H5 Family configurations +-define(STM32H562_G, #{ + rom => "ROM_1024K", + ram => "RAM_640K", + clock => "250000000", + flash_size_kb => "1024", + ram_size_kb => "640" +}). +-define(STM32H562_I, #{ + rom => "ROM_2048K", + ram => "RAM_640K", + clock => "250000000", + flash_size_kb => "2048", + ram_size_kb => "640" }). %% STM32F7 Family configurations --define(STM32F7_23_23_E, #{ - rom => "ROM_512K", ram => "RAM_192K", ccm => "CCM_64K", clock => "MHZ_216" +-define(STM32F72_73_E, #{ + rom => "ROM_512K", + ram => "RAM_256K", + clock => "216000000", + flash_size_kb => "512", + ram_size_kb => "256" +}). +-define(STM32F74_75_E, #{ + rom => "ROM_512K", + ram => "RAM_320K", + clock => "216000000", + flash_size_kb => "512", + ram_size_kb => "320" +}). +-define(STM32F74_75_G, #{ + rom => "ROM_1024K", + ram => "RAM_320K", + clock => "216000000", + flash_size_kb => "1024", + ram_size_kb => "320" +}). +-define(STM32F76_77_E, #{ + rom => "ROM_512K", + ram => "RAM_512K", + clock => "216000000", + flash_size_kb => "512", + ram_size_kb => "512" }). --define(STM32F745_E, #{rom => "ROM_512K", ram => "RAM_256K", ccm => "CCM_64K", clock => "MHZ_216"}). --define(STM32F745_G, #{rom => "ROM_1024K", ram => "RAM_256K", ccm => "CCM_64K", clock => "MHZ_216"}). --define(STM32F765_G, #{rom => "ROM_1024K", ram => "RAM_384K", ccm => "CCM_128K", clock => "MHZ_216"}). --define(STM32F765_I, #{rom => "ROM_2048K", ram => "RAM_384K", ccm => "CCM_128K", clock => "MHZ_216"}). --define(STM32F7_45_6_E, #{ - rom => "ROM_512K", ram => "RAM_256K", ccm => "CCM_64K", clock => "MHZ_216" +-define(STM32F76_77_G, #{ + rom => "ROM_1024K", + ram => "RAM_512K", + clock => "216000000", + flash_size_kb => "1024", + ram_size_kb => "512" }). --define(STM32F7_45_6_G, #{ - rom => "ROM_1024K", ram => "RAM_256K", ccm => "CCM_64K", clock => "MHZ_216" +-define(STM32F76_77_I, #{ + rom => "ROM_2048K", + ram => "RAM_512K", + clock => "216000000", + flash_size_kb => "2048", + ram_size_kb => "512" }). --define(STM32F7_67_7_G, #{ - rom => "ROM_1024K", ram => "RAM_384K", ccm => "CCM_128K", clock => "MHZ_216" + +%% STM32G0 Family configurations +-define(STM32G0B1_E, #{ + rom => "ROM_512K", + ram => "RAM_144K", + clock => "64000000", + flash_size_kb => "512", + ram_size_kb => "144" +}). +-define(STM32G071_B, #{ + rom => "ROM_128K", + ram => "RAM_36K", + clock => "64000000", + flash_size_kb => "128", + ram_size_kb => "36" +}). + +%% STM32G4 Family configurations +-define(STM32G47_48_E, #{ + rom => "ROM_512K", + ram => "RAM_128K", + clock => "170000000", + flash_size_kb => "512", + ram_size_kb => "128" +}). +-define(STM32G49_4A_E, #{ + rom => "ROM_512K", + ram => "RAM_112K", + clock => "170000000", + flash_size_kb => "512", + ram_size_kb => "112" +}). + +%% STM32L4 Family configurations +-define(STM32L45_46_E, #{ + rom => "ROM_512K", + ram => "RAM_160K", + clock => "80000000", + flash_size_kb => "512", + ram_size_kb => "160" +}). +-define(STM32L47_E, #{ + rom => "ROM_512K", + ram => "RAM_128K", + clock => "80000000", + flash_size_kb => "512", + ram_size_kb => "128" +}). +-define(STM32L47_48_G, #{ + rom => "ROM_1024K", + ram => "RAM_128K", + clock => "80000000", + flash_size_kb => "1024", + ram_size_kb => "128" +}). +-define(STM32L49_4A_G, #{ + rom => "ROM_1024K", + ram => "RAM_320K", + clock => "80000000", + flash_size_kb => "1024", + ram_size_kb => "320" }). --define(STM32F7_67_7_I, #{ - rom => "ROM_2048K", ram => "RAM_384K", ccm => "CCM_128K", clock => "MHZ_216" +-define(STM32L4R_4S_G, #{ + rom => "ROM_1024K", + ram => "RAM_640K", + clock => "120000000", + flash_size_kb => "1024", + ram_size_kb => "640" +}). +-define(STM32L4R_4S_I, #{ + rom => "ROM_2048K", + ram => "RAM_640K", + clock => "120000000", + flash_size_kb => "2048", + ram_size_kb => "640" +}). + +%% STM32L5 Family configurations +-define(STM32L55_56_E, #{ + rom => "ROM_512K", + ram => "RAM_256K", + clock => "110000000", + flash_size_kb => "512", + ram_size_kb => "256" +}). + +%% STM32F2 Family configurations +-define(STM32F2_E, #{ + rom => "ROM_512K", + ram => "RAM_128K", + clock => "120000000", + flash_size_kb => "512", + ram_size_kb => "128" +}). +-define(STM32F2_F, #{ + rom => "ROM_768K", + ram => "RAM_128K", + clock => "120000000", + flash_size_kb => "768", + ram_size_kb => "128" +}). +-define(STM32F2_G, #{ + rom => "ROM_1024K", + ram => "RAM_128K", + clock => "120000000", + flash_size_kb => "1024", + ram_size_kb => "128" +}). + +%% STM32U3 Family configurations +-define(STM32U37_38_E, #{ + rom => "ROM_512K", + ram => "RAM_256K", + clock => "96000000", + flash_size_kb => "512", + ram_size_kb => "256" }). --define(STM32F769_G, #{rom => "ROM_1024K", ram => "RAM_384K", ccm => "CCM_128K", clock => "MHZ_216"}). --define(STM32F7_67_89_I, #{ - rom => "ROM_2048K", ram => "RAM_384K", ccm => "CCM_128K", clock => "MHZ_216" +-define(STM32U37_38_G, #{ + rom => "ROM_1024K", + ram => "RAM_256K", + clock => "96000000", + flash_size_kb => "1024", + ram_size_kb => "256" }). From 1028e7c843b556bc208f078466e32fd821261538 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Tue, 17 Feb 2026 17:35:26 +0100 Subject: [PATCH 2/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 fd6fd85e62..4920832fd3 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -35,11 +35,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 99914bbaebb40540ab5a9c4df35f17d52cd4d287 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 21 Feb 2026 19:43:41 +0100 Subject: [PATCH 3/5] Add avm_stm32 as a dependency for dialyzer Signed-off-by: Paul Guyot --- examples/erlang/stm32/CMakeLists.txt | 14 +++++++------- .../stm32/tests/test_erl_sources/CMakeLists.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/erlang/stm32/CMakeLists.txt b/examples/erlang/stm32/CMakeLists.txt index cac029a7eb..cec4c45e43 100644 --- a/examples/erlang/stm32/CMakeLists.txt +++ b/examples/erlang/stm32/CMakeLists.txt @@ -22,10 +22,10 @@ project(examples_erlang_stm32) include(BuildErlang) -pack_runnable(blink_weact_studio_blackpill blink_weact_studio_blackpill eavmlib) -pack_runnable(blink_weact_studio_h562 blink_weact_studio_h562 eavmlib) -pack_runnable(blink_weact_studio_h743 blink_weact_studio_h743 eavmlib) -pack_runnable(blink_weact_studio_u585 blink_weact_studio_u585 eavmlib) -pack_runnable(blink_weact_studio_wb55 blink_weact_studio_wb55 eavmlib) -pack_runnable(blink_nucleo64 blink_nucleo64 eavmlib) -pack_runnable(blink_nucleo144 blink_nucleo144 eavmlib) +pack_runnable(blink_weact_studio_blackpill blink_weact_studio_blackpill eavmlib avm_stm32) +pack_runnable(blink_weact_studio_h562 blink_weact_studio_h562 eavmlib avm_stm32) +pack_runnable(blink_weact_studio_h743 blink_weact_studio_h743 eavmlib avm_stm32) +pack_runnable(blink_weact_studio_u585 blink_weact_studio_u585 eavmlib avm_stm32) +pack_runnable(blink_weact_studio_wb55 blink_weact_studio_wb55 eavmlib avm_stm32) +pack_runnable(blink_nucleo64 blink_nucleo64 eavmlib avm_stm32) +pack_runnable(blink_nucleo144 blink_nucleo144 eavmlib avm_stm32) diff --git a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt index 21932080b9..aff32393c9 100644 --- a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -21,4 +21,4 @@ include(BuildErlang) pack_runnable(stm32_boot_test test_boot) -pack_runnable(stm32_gpio_test test_gpio eavmlib) +pack_runnable(stm32_gpio_test test_gpio eavmlib avm_stm32) From c51558096ba57863938f2b2284d9c81a4e1989d4 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Wed, 18 Feb 2026 13:38:40 +0100 Subject: [PATCH 4/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 530dbb35a64acd1c29a73310ef682eee887646df Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Fri, 20 Feb 2026 23:32:44 +0100 Subject: [PATCH 5/5] Add I2C support to STM32 platform Signed-off-by: Paul Guyot --- .github/workflows/stm32-build.yaml | 27 +- examples/erlang/stm32/CMakeLists.txt | 1 + examples/erlang/stm32/stm32_lis3dh.erl | 122 ++++ libs/avm_stm32/src/CMakeLists.txt | 1 + libs/avm_stm32/src/i2c.erl | 434 ++++++++++++ src/platforms/stm32/cmake/stm32_hal_conf.h.in | 5 + src/platforms/stm32/src/CMakeLists.txt | 6 + src/platforms/stm32/src/lib/CMakeLists.txt | 1 + src/platforms/stm32/src/lib/i2c_driver.c | 616 ++++++++++++++++++ .../stm32/tests/renode/stm32_i2c_test.robot | 43 ++ .../stm32/tests/renode/stm32h743.repl | 7 + .../tests/test_erl_sources/CMakeLists.txt | 1 + .../stm32/tests/test_erl_sources/test_i2c.erl | 29 + 13 files changed, 1292 insertions(+), 1 deletion(-) create mode 100644 examples/erlang/stm32/stm32_lis3dh.erl create mode 100644 libs/avm_stm32/src/i2c.erl create mode 100644 src/platforms/stm32/src/lib/i2c_driver.c create mode 100644 src/platforms/stm32/tests/renode/stm32_i2c_test.robot create mode 100644 src/platforms/stm32/tests/test_erl_sources/test_i2c.erl diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index 440c1743e1..2ed88811d5 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -60,10 +60,16 @@ jobs: max_size: 524288 renode_platform: stm32f4.repl avm_address: "0x08080000" + # Renode's STM32F4_I2C model has a bug: it never calls + # FinishTransmission() on the I2C slave when a STOP condition + # occurs, causing the BME280 sensor to get stuck in Reading + # state and ignore subsequent writes. + skip_i2c_test: true - device: stm32f411ceu6 max_size: 393216 renode_platform: stm32f4.repl avm_address: "0x08060000" + skip_i2c_test: true - device: stm32f429zit6 max_size: 524288 - device: stm32h743vit6 @@ -90,6 +96,10 @@ jobs: max_size: 393216 renode_platform: stm32l562.repl avm_address: "0x08060000" + # Renode's built-in stm32l552.repl uses STM32F4_I2C (legacy I2C + # register layout) but the L5 HAL uses the newer I2C registers + # (TIMINGR, ISR, etc.), causing a complete register mismatch. + skip_i2c_test: true - device: stm32f207zgt6 max_size: 524288 - device: stm32u375rgt6 @@ -160,7 +170,7 @@ jobs: mkdir build-host cd build-host cmake .. -G Ninja - cmake --build . -t stm32_boot_test stm32_gpio_test + cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test - name: Install Renode if: matrix.renode_platform @@ -201,3 +211,18 @@ jobs: --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_gpio_test.avm \ --variable AVM_ADDRESS:${{ matrix.avm_address }} \ --variable PLATFORM:$PLATFORM + + - name: Run Renode I2C test + if: matrix.renode_platform && !matrix.skip_i2c_test + run: | + LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" + if [ -f "$LOCAL_REPL" ]; then + PLATFORM="@$PWD/$LOCAL_REPL" + else + PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" + fi + renode-test src/platforms/stm32/tests/renode/stm32_i2c_test.robot \ + --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ + --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_i2c_test.avm \ + --variable AVM_ADDRESS:${{ matrix.avm_address }} \ + --variable PLATFORM:$PLATFORM diff --git a/examples/erlang/stm32/CMakeLists.txt b/examples/erlang/stm32/CMakeLists.txt index cec4c45e43..bb6fc21111 100644 --- a/examples/erlang/stm32/CMakeLists.txt +++ b/examples/erlang/stm32/CMakeLists.txt @@ -29,3 +29,4 @@ pack_runnable(blink_weact_studio_u585 blink_weact_studio_u585 eavmlib avm_stm32) pack_runnable(blink_weact_studio_wb55 blink_weact_studio_wb55 eavmlib avm_stm32) pack_runnable(blink_nucleo64 blink_nucleo64 eavmlib avm_stm32) pack_runnable(blink_nucleo144 blink_nucleo144 eavmlib avm_stm32) +pack_runnable(stm32_lis3dh stm32_lis3dh eavmlib avm_stm32) diff --git a/examples/erlang/stm32/stm32_lis3dh.erl b/examples/erlang/stm32/stm32_lis3dh.erl new file mode 100644 index 0000000000..d68c264797 --- /dev/null +++ b/examples/erlang/stm32/stm32_lis3dh.erl @@ -0,0 +1,122 @@ +% +% 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 STM32. +%% +%% Reads X, Y, Z acceleration from a LIS3DH connected via I2C and prints +%% the values every second. +%% +%% Default wiring (I2C1 on most STM32 boards): +%% SCL -> PB6 +%% SDA -> PB7 +%% +%% The alternate function number defaults to 4, which is correct for I2C1 +%% on most STM32 families. Adjust `af' if your chip uses a different mapping. +%% @end +%%----------------------------------------------------------------------------- +-module(stm32_lis3dh). +-export([start/0]). + +%% I2C1 pins (PB6/PB7 are common I2C1 pins across STM32 families) +-define(SCL_PIN, {b, 6}). +-define(SDA_PIN, {b, 7}). + +%% 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, 1} + ]), + 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_stm32/src/CMakeLists.txt b/libs/avm_stm32/src/CMakeLists.txt index d85f481d4c..5169705bdb 100644 --- a/libs/avm_stm32/src/CMakeLists.txt +++ b/libs/avm_stm32/src/CMakeLists.txt @@ -24,6 +24,7 @@ include(BuildErlang) set(ERLANG_MODULES gpio + i2c ) pack_archive(avm_stm32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_stm32/src/i2c.erl b/libs/avm_stm32/src/i2c.erl new file mode 100644 index 0000000000..a7672e6733 --- /dev/null +++ b/libs/avm_stm32/src/i2c.erl @@ -0,0 +1,434 @@ +% +% 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 STM32 +%% +%% This module provides an interface to the I2C hardware on STM32 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API +%% {@link init/1}, {@link deinit/1}, {@link write/4}, {@link read/4}, +%% {@link mem_read/5}, {@link mem_write/5}. +%% These operate on a bare resource reference returned by {@link init/1}. +%% +%% 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 and I2C initialization 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 (STM32 HAL) +-export([ + init/1, + deinit/1, + write/4, + read/4, + mem_read/5, + mem_write/5 +]). + +-type pin() :: {gpio_bank(), 0..15}. +-type gpio_bank() :: a | b | c | d | e | f | g | h | i | j | k. +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 1..4. +-type param() :: + {scl, pin()} + | {sda, pin()} + | {clock_speed_hz, freq_hz()} + | {peripheral, peripheral()} + | {af, non_neg_integer()} + | {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_SEND_TIMEOUT_MS, 500). + +%% --------------------------------------------------------------------------- +%% High-level API +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns I2C handle +%% @doc Open a connection to the I2C driver +%% +%% This function configures the GPIO pins for I2C alternate function +%% with open-drain and pull-up, and initializes the I2C peripheral. +%% +%% Supported parameters: +%%
    +%%
  • `{scl, {Bank, Pin}}' - the SCL pin (required)
  • +%%
  • `{sda, {Bank, Pin}}' - the SDA pin (required)
  • +%%
  • `{clock_speed_hz, Hz}' - clock speed in Hz (default: 100000)
  • +%%
  • `{peripheral, 1..4}' - I2C peripheral number (default: 1)
  • +%%
  • `{af, N}' - GPIO alternate function number (default: 4)
  • +%%
  • `{send_timeout_ms, Ms | infinity}' - send timeout (default: 500)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: params()) -> i2c(). +open(Params) -> + SendTimeoutMs = get_value(send_timeout_ms, Params, ?DEFAULT_SEND_TIMEOUT_MS), + {ok, Resource} = ?MODULE:init(Params), + erlang:spawn_opt(fun() -> loop(Resource, SendTimeoutMs, undefined) end, [link]). + +%%----------------------------------------------------------------------------- +%% @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 uses HAL_I2C_Mem_Read which performs an I2C combined +%% write-then-read transaction (register address write followed by +%% repeated start and read). +%% @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. +%% +%% This uses HAL_I2C_Mem_Write which handles the register address +%% and data in a single I2C transaction. +%% @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 (STM32 HAL NIFs) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Opts Initialization options proplist +%% @returns `{ok, Resource}' +%% @doc Initialize the I2C peripheral. +%% +%% Configures GPIO pins and initializes the I2C hardware. +%% Options: `peripheral', `scl', `sda', `clock_speed_hz', `af'. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Opts :: params()) -> {ok, Resource :: i2c_resource()}. +init(_Opts) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/1' +%% @returns `ok' +%% @doc Deinitialize the I2C peripheral. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: i2c_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/1' +%% @param Addr 7-bit I2C device address +%% @param Data Binary data to write +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns Number of bytes written, or `{error, Reason}' +%% @doc Write to I2C device (master transmit). +%% @end +%%----------------------------------------------------------------------------- +-spec write( + Resource :: i2c_resource(), Addr :: address(), Data :: binary(), TimeoutMs :: timeout() +) -> + non_neg_integer() | {error, Reason :: term()}. +write(_Resource, _Addr, _Data, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/1' +%% @param Addr 7-bit I2C device address +%% @param Count Number of bytes to read +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read from I2C device (master receive). +%% @end +%%----------------------------------------------------------------------------- +-spec read( + Resource :: i2c_resource(), + Addr :: address(), + Count :: non_neg_integer(), + TimeoutMs :: timeout() +) -> + {ok, binary()} | {error, Reason :: term()}. +read(_Resource, _Addr, _Count, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/1' +%% @param Addr 7-bit I2C device address +%% @param Register Register address to read from +%% @param Count Number of bytes to read +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read from a register of an I2C device (combined write+read). +%% @end +%%----------------------------------------------------------------------------- +-spec mem_read( + Resource :: i2c_resource(), + Addr :: address(), + Register :: register_addr(), + Count :: non_neg_integer(), + TimeoutMs :: timeout() +) -> + {ok, binary()} | {error, Reason :: term()}. +mem_read(_Resource, _Addr, _Register, _Count, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource I2C resource returned by `init/1' +%% @param Addr 7-bit I2C device address +%% @param Register Register address to write to +%% @param Data Binary data to write +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns Number of bytes written, or `{error, Reason}' +%% @doc Write to a register of an I2C device. +%% @end +%%----------------------------------------------------------------------------- +-spec mem_write( + Resource :: i2c_resource(), + Addr :: address(), + Register :: register_addr(), + Data :: binary(), + TimeoutMs :: timeout() +) -> + non_neg_integer() | {error, Reason :: term()}. +mem_write(_Resource, _Addr, _Register, _Data, _TimeoutMs) -> + erlang:nif_error(undefined). + +%% --------------------------------------------------------------------------- +%% Internal helpers +%% --------------------------------------------------------------------------- + +%% @private +get_value(_Key, [], Default) -> Default; +get_value(Key, [{Key, Value} | _], _Default) -> Value; +get_value(Key, [_ | Rest], Default) -> get_value(Key, Rest, Default). + +%% @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 ?MODULE:write(Resource, Address, Data, 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 = ?MODULE:read(Resource, Address, Count, SendTimeoutMs), + {reply, Result, TxState}; +handle_request(Resource, SendTimeoutMs, TxState, {read_bytes, Address, Register, Count}) -> + Result = ?MODULE:mem_read(Resource, Address, Register, Count, SendTimeoutMs), + {reply, Result, TxState}; +handle_request(Resource, SendTimeoutMs, TxState, {write_bytes, Address, Data}) -> + Result = + case ?MODULE:write(Resource, Address, Data, SendTimeoutMs) of + {error, _} = Error -> Error; + _N -> ok + end, + {reply, Result, TxState}; +handle_request(Resource, SendTimeoutMs, TxState, {write_bytes, Address, Register, Data}) -> + Result = + case ?MODULE:mem_write(Resource, Address, Register, Data, SendTimeoutMs) of + {error, _} = Error -> Error; + _N -> ok + end, + {reply, Result, TxState}. diff --git a/src/platforms/stm32/cmake/stm32_hal_conf.h.in b/src/platforms/stm32/cmake/stm32_hal_conf.h.in index 94f3a6ed4b..22fd0e2b6f 100644 --- a/src/platforms/stm32/cmake/stm32_hal_conf.h.in +++ b/src/platforms/stm32/cmake/stm32_hal_conf.h.in @@ -39,6 +39,7 @@ extern "C" { #define HAL_PWR_MODULE_ENABLED #define HAL_EXTI_MODULE_ENABLED #define HAL_DMA_MODULE_ENABLED +#define HAL_I2C_MODULE_ENABLED #if defined(STM32H5XX) || defined(STM32L5XX) || defined(STM32U3XX) || defined(STM32U5XX) #define HAL_ICACHE_MODULE_ENABLED #endif @@ -194,6 +195,10 @@ extern "C" { #include "@STM32_FAMILY@_hal_uart.h" #endif +#ifdef HAL_I2C_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_i2c.h" +#endif + #ifdef HAL_ICACHE_MODULE_ENABLED #include "@STM32_FAMILY@_hal_icache.h" #endif diff --git a/src/platforms/stm32/src/CMakeLists.txt b/src/platforms/stm32/src/CMakeLists.txt index c4f64ea648..9f96271202 100644 --- a/src/platforms/stm32/src/CMakeLists.txt +++ b/src/platforms/stm32/src/CMakeLists.txt @@ -46,6 +46,12 @@ if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_gpdma.c") list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_gpdma.c) endif() +# I2C HAL +list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_i2c.c) +if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_i2c_ex.c") + list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_i2c_ex.c) +endif() + # ICACHE HAL for families that have it (U5, H5) if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_icache.c") list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_icache.c) diff --git a/src/platforms/stm32/src/lib/CMakeLists.txt b/src/platforms/stm32/src/lib/CMakeLists.txt index 570bbcf3c3..f800e9d23c 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -33,6 +33,7 @@ set(HEADER_FILES set(SOURCE_FILES gpio_driver.c + i2c_driver.c jit_stream_flash.c platform_nifs.c sys.c diff --git a/src/platforms/stm32/src/lib/i2c_driver.c b/src/platforms/stm32/src/lib/i2c_driver.c new file mode 100644 index 0000000000..c2752c4a38 --- /dev/null +++ b/src/platforms/stm32/src/lib/i2c_driver.c @@ -0,0 +1,616 @@ +/* + * 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 "stm32_hal_platform.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#include "avm_log.h" +#include "stm_sys.h" + +#define TAG "i2c_driver" + +static ErlNifResourceType *i2c_resource_type; + +struct I2CResource +{ + I2C_HandleTypeDef handle; +}; + +static const AtomStringIntPair gpio_bank_table[] = { + { ATOM_STR("\x1", "a"), (int) GPIOA }, + { ATOM_STR("\x1", "b"), (int) GPIOB }, + { ATOM_STR("\x1", "c"), (int) GPIOC }, + { ATOM_STR("\x1", "d"), (int) GPIOD }, + { ATOM_STR("\x1", "e"), (int) GPIOE }, +#ifdef GPIOF + { ATOM_STR("\x1", "f"), (int) GPIOF }, +#endif +#ifdef GPIOG + { ATOM_STR("\x1", "g"), (int) GPIOG }, +#endif +#ifdef GPIOH + { ATOM_STR("\x1", "h"), (int) GPIOH }, +#endif +#ifdef GPIOI + { ATOM_STR("\x1", "i"), (int) GPIOI }, +#endif +#ifdef GPIOJ + { ATOM_STR("\x1", "j"), (int) GPIOJ }, +#endif +#ifdef GPIOK + { ATOM_STR("\x1", "k"), (int) GPIOK }, +#endif + SELECT_INT_DEFAULT(0) +}; + +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 I2C_TypeDef *peripheral_to_instance(int peripheral) +{ + switch (peripheral) { +#ifdef I2C1 + case 1: + return I2C1; +#endif +#ifdef I2C2 + case 2: + return I2C2; +#endif +#ifdef I2C3 + case 3: + return I2C3; +#endif +#ifdef I2C4 + case 4: + return I2C4; +#endif + default: + return NULL; + } +} + +static void enable_i2c_clock(int peripheral) +{ + switch (peripheral) { +#ifdef I2C1 + case 1: + __HAL_RCC_I2C1_CLK_ENABLE(); + break; +#endif +#ifdef I2C2 + case 2: + __HAL_RCC_I2C2_CLK_ENABLE(); + break; +#endif +#ifdef I2C3 + case 3: + __HAL_RCC_I2C3_CLK_ENABLE(); + break; +#endif +#ifdef I2C4 + case 4: + __HAL_RCC_I2C4_CLK_ENABLE(); + break; +#endif + default: + break; + } +} + +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 uint32_t get_timeout_ms(term timeout_term) +{ + if (term_is_atom(timeout_term) && timeout_term == INFINITY_ATOM) { + return HAL_MAX_DELAY; + } + return (uint32_t) term_to_int(timeout_term); +} + +/* + * I2C timing computation. + * F2/F4 use the legacy I2C peripheral (ClockSpeed/DutyCycle). + * All other supported families use the newer I2C with a Timing register. + */ +#if defined(STM32F2XX) || defined(STM32F4XX) + +static void configure_i2c_timing(I2C_HandleTypeDef *handle, uint32_t clock_speed_hz) +{ + handle->Init.ClockSpeed = clock_speed_hz; + handle->Init.DutyCycle = I2C_DUTYCYCLE_2; +} + +#else + +/* + * Compute a Timing register value for a target I2C baudrate. + * This is a simplified computation assuming analog filter on, digital filter off. + * For production use, STM32CubeMX-generated values are recommended. + */ +static uint32_t compute_i2c_timing(uint32_t pclk_freq, uint32_t target_freq) +{ + uint32_t presc, scll, sclh; + + if (target_freq <= 100000) { + /* Standard mode (100 kHz) */ + presc = (pclk_freq / 4000000) - 1; + if (presc > 15) { + presc = 15; + } + uint32_t t_presc = pclk_freq / (presc + 1); + uint32_t scl_period = t_presc / target_freq; + scll = scl_period / 2; + sclh = scl_period - scll; + if (scll > 0) { + scll--; + } + if (sclh > 0) { + sclh--; + } + if (scll > 255) { + scll = 255; + } + if (sclh > 255) { + sclh = 255; + } + } else { + /* Fast mode (400 kHz) */ + presc = (pclk_freq / 8000000) - 1; + if (presc > 15) { + presc = 15; + } + uint32_t t_presc = pclk_freq / (presc + 1); + uint32_t scl_period = t_presc / target_freq; + scll = (scl_period * 2) / 3; + sclh = scl_period - scll; + if (scll > 0) { + scll--; + } + if (sclh > 0) { + sclh--; + } + if (scll > 255) { + scll = 255; + } + if (sclh > 255) { + sclh = 255; + } + } + + /* SDADEL and SCLDEL: use safe conservative values */ + uint32_t sdadel = 2; + uint32_t scldel = 4; + + return (presc << 28) | (scldel << 20) | (sdadel << 16) | (sclh << 8) | scll; +} + +static void configure_i2c_timing(I2C_HandleTypeDef *handle, uint32_t clock_speed_hz) +{ + uint32_t pclk1_freq = HAL_RCC_GetPCLK1Freq(); + handle->Init.Timing = compute_i2c_timing(pclk1_freq, clock_speed_hz); +} + +#endif + +static bool parse_pin(GlobalContext *glb, term pin_term, GPIO_TypeDef **port, uint16_t *pin_mask) +{ + if (!term_is_tuple(pin_term) || term_get_tuple_arity(pin_term) != 2) { + return false; + } + term bank_atom = term_get_tuple_element(pin_term, 0); + if (!term_is_atom(bank_atom)) { + return false; + } + uint32_t gpio_bank = (uint32_t) interop_atom_term_select_int(gpio_bank_table, bank_atom, glb); + if (gpio_bank == 0) { + return false; + } + *port = (GPIO_TypeDef *) gpio_bank; + + term pin_num_term = term_get_tuple_element(pin_term, 1); + if (!term_is_integer(pin_num_term)) { + return false; + } + int pin_num = term_to_int(pin_num_term); + if (pin_num < 0 || pin_num > 15) { + return false; + } + *pin_mask = (uint16_t) (1U << pin_num); + return true; +} + +static term nif_i2c_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + + static const char *const peripheral_str = ATOM_STR("\xA", "peripheral"); + static const char *const scl_str = ATOM_STR("\x3", "scl"); + static const char *const sda_str = ATOM_STR("\x3", "sda"); + static const char *const clock_speed_hz_str = ATOM_STR("\xE", "clock_speed_hz"); + static const char *const af_str = ATOM_STR("\x2", "af"); + + GlobalContext *glb = ctx->global; + + term peripheral_term = interop_kv_get_value_default(opts, peripheral_str, term_from_int(1), glb); + term scl_term = interop_kv_get_value(opts, scl_str, glb); + term sda_term = interop_kv_get_value(opts, sda_str, glb); + term clock_speed_term = interop_kv_get_value_default(opts, clock_speed_hz_str, term_from_int(100000), glb); + term af_term = interop_kv_get_value_default(opts, af_str, term_from_int(4), glb); + + if (term_is_invalid_term(scl_term) || term_is_invalid_term(sda_term)) { + AVM_LOGE(TAG, "scl and sda pins are required"); + RAISE_ERROR(BADARG_ATOM); + } + + VALIDATE_VALUE(peripheral_term, term_is_integer); + VALIDATE_VALUE(clock_speed_term, term_is_integer); + VALIDATE_VALUE(af_term, term_is_integer); + + int peripheral = term_to_int(peripheral_term); + uint32_t clock_speed_hz = (uint32_t) term_to_int(clock_speed_term); + uint32_t af = (uint32_t) term_to_int(af_term); + + I2C_TypeDef *instance = peripheral_to_instance(peripheral); + if (IS_NULL_PTR(instance)) { + AVM_LOGE(TAG, "Invalid I2C peripheral: %d", peripheral); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *scl_port; + uint16_t scl_pin; + if (!parse_pin(glb, scl_term, &scl_port, &scl_pin)) { + AVM_LOGE(TAG, "Invalid SCL pin"); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *sda_port; + uint16_t sda_pin; + if (!parse_pin(glb, sda_term, &sda_port, &sda_pin)) { + AVM_LOGE(TAG, "Invalid SDA pin"); + RAISE_ERROR(BADARG_ATOM); + } + + enable_i2c_clock(peripheral); + + GPIO_InitTypeDef gpio_init = { 0 }; + gpio_init.Mode = GPIO_MODE_AF_OD; + gpio_init.Pull = GPIO_PULLUP; + gpio_init.Speed = GPIO_SPEED_FREQ_HIGH; + gpio_init.Alternate = af; + + gpio_init.Pin = scl_pin; + HAL_GPIO_Init(scl_port, &gpio_init); + + gpio_init.Pin = sda_pin; + HAL_GPIO_Init(sda_port, &gpio_init); + + struct I2CResource *rsrc_obj = enif_alloc_resource(i2c_resource_type, sizeof(struct I2CResource)); + if (IS_NULL_PTR(rsrc_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + memset(&rsrc_obj->handle, 0, sizeof(I2C_HandleTypeDef)); + rsrc_obj->handle.Instance = instance; + configure_i2c_timing(&rsrc_obj->handle, clock_speed_hz); + rsrc_obj->handle.Init.OwnAddress1 = 0; + rsrc_obj->handle.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; + rsrc_obj->handle.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; + rsrc_obj->handle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; + rsrc_obj->handle.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; + + HAL_StatusTypeDef status = HAL_I2C_Init(&rsrc_obj->handle); + if (status != HAL_OK) { + enif_release_resource(rsrc_obj); + AVM_LOGE(TAG, "HAL_I2C_Init failed: %d", (int) status); + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(glb, ATOM_STR("\x9", "i2c_init"))); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + HAL_I2C_DeInit(&rsrc_obj->handle); + 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); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return create_pair(ctx, OK_ATOM, obj); +} + +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); + } + + HAL_I2C_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + + return OK_ATOM; +} + +static term hal_status_to_error(Context *ctx, HAL_StatusTypeDef status) +{ + switch (status) { + case HAL_TIMEOUT: + return create_pair(ctx, ERROR_ATOM, TIMEOUT_ATOM); + case HAL_BUSY: + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "busy"))); + default: + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(ctx->global, ATOM_STR("\x3", "eio"))); + } +} + +static term nif_i2c_write(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); + + uint16_t addr = (uint16_t) (term_to_int(argv[1]) << 1); + const uint8_t *data = (const uint8_t *) term_binary_data(argv[2]); + size_t len = term_binary_size(argv[2]); + uint32_t timeout_ms = get_timeout_ms(argv[3]); + + HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&rsrc_obj->handle, addr, (uint8_t *) data, (uint16_t) len, timeout_ms); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + + return term_from_int((int) len); +} + +static term nif_i2c_read(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); + + uint16_t addr = (uint16_t) (term_to_int(argv[1]) << 1); + avm_int_t count = term_to_int(argv[2]); + uint32_t timeout_ms = get_timeout_ms(argv[3]); + + 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); + + HAL_StatusTypeDef status = HAL_I2C_Master_Receive(&rsrc_obj->handle, addr, buf, (uint16_t) count, timeout_ms); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_i2c_mem_read(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_integer); + + uint16_t addr = (uint16_t) (term_to_int(argv[1]) << 1); + uint16_t mem_addr = (uint16_t) term_to_int(argv[2]); + avm_int_t count = term_to_int(argv[3]); + uint32_t timeout_ms = get_timeout_ms(argv[4]); + + uint16_t mem_addr_size = (mem_addr > 0xFF) ? I2C_MEMADD_SIZE_16BIT : I2C_MEMADD_SIZE_8BIT; + + 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); + + HAL_StatusTypeDef status = HAL_I2C_Mem_Read(&rsrc_obj->handle, addr, mem_addr, mem_addr_size, buf, (uint16_t) count, timeout_ms); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_i2c_mem_write(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_binary); + + uint16_t addr = (uint16_t) (term_to_int(argv[1]) << 1); + uint16_t mem_addr = (uint16_t) term_to_int(argv[2]); + const uint8_t *data = (const uint8_t *) term_binary_data(argv[3]); + size_t len = term_binary_size(argv[3]); + uint32_t timeout_ms = get_timeout_ms(argv[4]); + + uint16_t mem_addr_size = (mem_addr > 0xFF) ? I2C_MEMADD_SIZE_16BIT : I2C_MEMADD_SIZE_8BIT; + + HAL_StatusTypeDef status = HAL_I2C_Mem_Write(&rsrc_obj->handle, addr, mem_addr, mem_addr_size, (uint8_t *) data, (uint16_t) len, timeout_ms); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + + return term_from_int((int) len); +} + +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->handle.Instance)) { + HAL_I2C_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + } +} + +static const ErlNifResourceTypeInit I2CResourceTypeInit = { + .members = 1, + .dtor = i2c_resource_dtor, +}; + +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_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_write +}; +static const struct Nif i2c_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_read +}; +static const struct Nif i2c_mem_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_mem_read +}; +static const struct Nif i2c_mem_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_i2c_mem_write +}; + +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/1", 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("write/4", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_write_nif; + } + if (strcmp("read/4", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_read_nif; + } + if (strcmp("mem_read/5", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_mem_read_nif; + } + if (strcmp("mem_write/5", rest) == 0) { + TRACE("Resolved i2c nif %s ...\n", nifname); + return &i2c_mem_write_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(i2c, i2c_nif_init, NULL, i2c_nif_get_nif) diff --git a/src/platforms/stm32/tests/renode/stm32_i2c_test.robot b/src/platforms/stm32/tests/renode/stm32_i2c_test.robot new file mode 100644 index 0000000000..fd6ca619eb --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32_i2c_test.robot @@ -0,0 +1,43 @@ +# +# 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 +# + +*** Settings *** +Suite Setup Setup +Suite Teardown Teardown +Test Setup Reset Emulation +Resource ${RENODEKEYWORDS} + +*** Variables *** +${UART} sysbus.usart1 +${PLATFORM} @platforms/cpus/stm32f4.repl +${ELF} REQUIRED +${AVM} REQUIRED +${AVM_ADDRESS} 0x08060000 + +*** Test Cases *** +AtomVM Should Communicate Over I2C + Execute Command mach create + Execute Command machine LoadPlatformDescription ${PLATFORM} + Execute Command machine LoadPlatformDescriptionFromString "bme280: I2C.BME280 @ i2c1 0x76" + Execute Command sysbus LoadELF ${ELF} + Execute Command sysbus LoadBinary ${AVM} ${AVM_ADDRESS} + Create Terminal Tester ${UART} + Start Emulation + Wait For Line On Uart i2c_done timeout=60 diff --git a/src/platforms/stm32/tests/renode/stm32h743.repl b/src/platforms/stm32/tests/renode/stm32h743.repl index 96cce98596..dd9aa9abc4 100644 --- a/src/platforms/stm32/tests/renode/stm32h743.repl +++ b/src/platforms/stm32/tests/renode/stm32h743.repl @@ -115,6 +115,13 @@ gpioPortA: GPIOPort.STM32_GPIOPort @ sysbus <0x58020000, +0x400> modeResetValue: 0xABFFFFFF numberOfAFs: 16 +gpioPortB: GPIOPort.STM32_GPIOPort @ sysbus <0x58020400, +0x400> + numberOfAFs: 16 + +i2c1: I2C.STM32F7_I2C @ sysbus 0x40005400 + EventInterrupt -> nvic@31 + ErrorInterrupt -> nvic@32 + sysbus: init: Tag <0x58000400, 0x580007FF> "SYSCFG" diff --git a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt index aff32393c9..dce1a05adc 100644 --- a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -22,3 +22,4 @@ include(BuildErlang) pack_runnable(stm32_boot_test test_boot) pack_runnable(stm32_gpio_test test_gpio eavmlib avm_stm32) +pack_runnable(stm32_i2c_test test_i2c eavmlib avm_stm32) diff --git a/src/platforms/stm32/tests/test_erl_sources/test_i2c.erl b/src/platforms/stm32/tests/test_erl_sources/test_i2c.erl new file mode 100644 index 0000000000..e4d908bccf --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/test_i2c.erl @@ -0,0 +1,29 @@ +% +% 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 +% + +-module(test_i2c). +-export([start/0]). + +start() -> + I2C = i2c:open([{peripheral, 1}, {scl, {b, 6}}, {sda, {b, 7}}]), + {ok, <<16#60>>} = i2c:read_bytes(I2C, 16#76, 16#D0, 1), + ok = i2c:write_bytes(I2C, 16#76, 16#F4, 16#27), + {ok, <<16#27>>} = i2c:read_bytes(I2C, 16#76, 16#F4, 1), + erlang:display(i2c_done).