diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 253d0e5e99..ae66f833a9 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -59,6 +59,9 @@ jobs: idf-version: 'v5.5.3' - esp-idf-target: "esp32s3" idf-version: 'v5.5.3' + - esp-idf-target: "esp32s3" + idf-version: 'v5.5.3' + usb-cdc: 'ON' - esp-idf-target: "esp32" idf-version: 'v5.4.3' libsodium: 'ON' @@ -84,11 +87,26 @@ jobs: . $IDF_PATH/export.sh idf.py add-dependency "espressif/libsodium^1.0.20~4" + - name: Add ESP TinyUSB dependency + if: matrix.usb-cdc == 'ON' + working-directory: ./src/platforms/esp32/ + run: | + . $IDF_PATH/export.sh + idf.py add-dependency "espressif/esp_tinyusb" + - name: Build with idf.py shell: bash working-directory: ./src/platforms/esp32/ + env: + USB_CDC: ${{ matrix.usb-cdc || 'OFF' }} run: | . $IDF_PATH/export.sh + if [[ "${USB_CDC}" == "ON" ]]; then + printf '%s\n' \ + 'CONFIG_TINYUSB_CDC_ENABLED=y' \ + 'CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER=y' \ + >> sdkconfig.defaults.in + fi export IDF_TARGET=${{matrix.esp-idf-target}} idf.py set-target ${{matrix.esp-idf-target}} idf.py ${{ matrix.libsodium == 'ON' && '-DAVM_USE_LIBSODIUM=ON' || '' }} build diff --git a/.github/workflows/pico-build.yaml b/.github/workflows/pico-build.yaml index 531b44d395..a97c47fae9 100644 --- a/.github/workflows/pico-build.yaml +++ b/.github/workflows/pico-build.yaml @@ -106,6 +106,11 @@ jobs: platform: "-DPICO_PLATFORM=rp2350-riscv" jit: "-DAVM_DISABLE_JIT=OFF" + - board: "pico" + platform: "" + jit: "" + usb-cdc: "ON" + steps: - name: Checkout repo uses: actions/checkout@v4 @@ -172,7 +177,7 @@ jobs: set -euo pipefail mkdir build cd build - cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} ${{ matrix.platform }} ${{ matrix.jit }} + cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} ${{ matrix.platform }} ${{ matrix.jit }} ${{ matrix.usb-cdc == 'ON' && '-DAVM_USB_CDC_PORT_DRIVER_ENABLED=ON' || '' }} cmake --build . --target=AtomVM - name: "Perform CodeQL Analysis" @@ -187,7 +192,7 @@ jobs: nvm install 24 - name: Build tests (without SMP) - if: matrix.board != 'pico2' && matrix.board != 'pico2_w' + if: matrix.board != 'pico2' && matrix.board != 'pico2_w' && matrix.usb-cdc != 'ON' shell: bash working-directory: ./src/platforms/rp2/ run: | @@ -199,7 +204,7 @@ jobs: cmake --build . --target=rp2_tests - name: Run tests with rp2040js - if: matrix.board != 'pico2' && matrix.board != 'pico2_w' + if: matrix.board != 'pico2' && matrix.board != 'pico2_w' && matrix.usb-cdc != 'ON' shell: bash working-directory: ./src/platforms/rp2/tests run: | @@ -210,7 +215,7 @@ jobs: npx tsx run-tests.ts ../build.nosmp/tests/rp2_tests.uf2 ../build.nosmp/tests/test_erl_sources/rp2_test_modules.uf2 - name: Rename AtomVM and write sha256sum - if: matrix.platform == '' && matrix.jit == '' + if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' shell: bash run: | pushd src/platforms/rp2/build @@ -220,14 +225,14 @@ jobs: popd - name: Upload AtomVM artifact - if: matrix.platform == '' && matrix.jit == '' + if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' uses: actions/upload-artifact@v4 with: name: AtomVM-${{ matrix.board }}-${{env.AVM_REF_NAME}}.uf2 path: src/platforms/rp2/build/src/AtomVM-${{ matrix.board }}-*.uf2 - name: Rename atomvmlib-rp2 and write sha256sum - if: matrix.platform == '' && matrix.jit == '' + if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' shell: bash run: | pushd build/libs @@ -237,7 +242,7 @@ jobs: popd - name: Combine uf2 using uf2tool - if: matrix.platform == '' && matrix.jit == '' + if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' shell: bash run: | ATOMVM_COMBINED_FILE=AtomVM-${{ matrix.board }}-combined-${{env.AVM_REF_NAME}}.uf2 @@ -246,7 +251,7 @@ jobs: echo "ATOMVM_COMBINED_FILE=${ATOMVM_COMBINED_FILE}" >> $GITHUB_ENV - name: Upload combined AtomVM artifact - if: matrix.platform == '' && matrix.jit == '' + if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' uses: actions/upload-artifact@v4 with: name: ${{ env.ATOMVM_COMBINED_FILE }} @@ -254,7 +259,7 @@ jobs: - name: Release (Pico & Pico2) uses: softprops/action-gh-release@v3.0.0 - if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.board != 'pico2_w' && matrix.platform == '' && matrix.jit == '' + if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.board != 'pico2_w' && matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' with: draft: true fail_on_unmatched_files: true @@ -268,7 +273,7 @@ jobs: - name: Release (PicoW & Pico2W) uses: softprops/action-gh-release@v3.0.0 - if: startsWith(github.ref, 'refs/tags/') && (matrix.board == 'pico_w' || matrix.board == 'pico2_w') && matrix.platform == '' && matrix.jit == '' + if: startsWith(github.ref, 'refs/tags/') && (matrix.board == 'pico_w' || matrix.board == 'pico2_w') && matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON' with: draft: true fail_on_unmatched_files: true diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index 33488e02bd..7dc9109eb5 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -64,21 +64,21 @@ jobs: # 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 + tests: "boot gpio spi crypto uart" - device: stm32f411ceu6 max_size: 393216 renode_platform: stm32f4.repl avm_address: "0x08060000" - skip_i2c_test: true # No RNG peripheral on F411, so the firmware has no crypto NIFs # (mbedTLS is excluded by STM32_HAS_RNG in CMake). - skip_crypto_test: true + tests: "boot gpio spi uart" - device: stm32f429zit6 max_size: 524288 - device: stm32h743vit6 max_size: 524288 renode_platform: stm32h743.repl avm_address: "0x08080000" + tests: "boot gpio i2c spi crypto uart" - device: stm32h743zit6 max_size: 524288 - device: stm32u585ait6q @@ -87,10 +87,15 @@ jobs: max_size: 524288 - device: stm32h562rgt6 max_size: 524288 + usb-cdc: "OFF" + - device: stm32h562rgt6 + max_size: 524288 + usb-cdc: "ON" - device: stm32f746zgt6 max_size: 524288 renode_platform: stm32f746.repl avm_address: "0x08080000" + tests: "boot gpio i2c spi crypto uart" - device: stm32g474ret6 max_size: 393216 - device: stm32l476rgt6 @@ -102,10 +107,9 @@ jobs: # 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 # 512 KB flash with avm_address=0x08060000 leaves only 128 KB, # but the crypto AVM is 207 KB and gets truncated. - skip_crypto_test: true + tests: "boot gpio spi uart" - device: stm32f207zgt6 max_size: 524288 - device: stm32u375rgt6 @@ -117,7 +121,7 @@ jobs: # No RNG peripheral on G0B1 (only G041/G061/G081/G0C1 have one), # so the firmware has no crypto NIFs (mbedTLS is excluded by # STM32_HAS_RNG in CMake). - skip_crypto_test: true + tests: "boot gpio i2c spi uart" steps: - uses: erlef/setup-beam@v1 @@ -162,7 +166,7 @@ jobs: set -euo pipefail mkdir build cd build - cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DDEVICE=${{ matrix.device }} + cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DDEVICE=${{ matrix.device }} ${{ matrix.usb-cdc == 'ON' && '-DAVM_USB_CDC_PORT_DRIVER_ENABLED=ON' || '' }} cmake --build . - name: "Perform CodeQL Analysis" @@ -195,7 +199,7 @@ jobs: mkdir build-host cd build-host cmake .. -G Ninja - cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test stm32_spi_test stm32_crypto_test + cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test stm32_spi_test stm32_crypto_test stm32_uart_test - name: Install Renode if: matrix.renode_platform @@ -207,52 +211,7 @@ jobs: echo "$PWD/renode-portable" >> $GITHUB_PATH pip install -r renode-portable/tests/requirements.txt - - 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 - - - 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 - - - name: Run Renode SPI test + - name: Run Renode tests if: matrix.renode_platform run: | LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" @@ -261,26 +220,10 @@ jobs: else PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" fi - renode-test src/platforms/stm32/tests/renode/stm32_spi_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_spi_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM - - - name: Run Renode crypto test - # Devices without RNG hardware (F411 / G0) don't ship mbedTLS at all; - # L562 has only 512 KB of flash and the 207 KB crypto AVM is truncated - # at AVM_ADDRESS=0x08060000, which kills kernel boot. - if: matrix.renode_platform && !matrix.skip_crypto_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_crypto_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_crypto_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM + for TEST in ${{ matrix.tests }}; do + renode-test "src/platforms/stm32/tests/renode/stm32_${TEST}_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_${TEST}_test.avm \ + --variable AVM_ADDRESS:${{ matrix.avm_address }} \ + --variable PLATFORM:$PLATFORM + done diff --git a/CHANGELOG.md b/CHANGELOG.md index 2129405f44..dcef0e1623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for `nif_start`, `executable_line` and `debug_line` opcodes - Added named variable debugging support in DWARF when modules are compiled with `beam_debug_info` - Added more reset reasons and ensured `esp:reset_reason/0` doesn't return `undefined` -- Added I2C and SPI APIs to stm32 platform +- Added I2C, SPI and UART APIs to stm32 platform - Added `Transfer-Encoding: chunked` response support to `ahttp_client`, including HTTP trailers - Added `proc_lib:init_fail/2,3` - Added UART API to rp2 platform +- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms ### Changed - Updated network type db() to dbm() to reflect the actual representation of the type diff --git a/doc/src/distributed-erlang.md b/doc/src/distributed-erlang.md index 13d50a68b4..7d7c152365 100644 --- a/doc/src/distributed-erlang.md +++ b/doc/src/distributed-erlang.md @@ -18,11 +18,16 @@ Distribution over serial (UART) is also available for point-to-point connections between any two nodes, including microcontrollers without networking (e.g. STM32). See [Serial distribution](#serial-distribution). -Three examples are provided: +Distribution over USB CDC is also supported on ESP32-S2/S3, RP2040/RP2350, +and STM32, using the same `serial_dist` protocol. On Unix hosts, USB CDC +devices appear as standard serial ports. See [USB distribution](#usb-distribution). + +Four examples are provided: - disterl in `examples/erlang/disterl.erl`: distribution on Unix systems - epmd\_disterl in `examples/erlang/esp32/epmd_disterl.erl`: distribution on ESP32 devices - serial\_disterl in `examples/erlang/serial_disterl.erl`: distribution over serial (ESP32 and Unix) +- usb\_disterl in `examples/erlang/usb_disterl.erl`: distribution over USB CDC (all platforms) ## Starting and stopping distribution @@ -240,6 +245,73 @@ This creates two pseudo-terminal devices (e.g. `/dev/ttys003` and {some_registered_name, 'a@serial.local'} ! {self(), hello}. ``` +## USB distribution + +AtomVM supports distribution over USB CDC (Communications Device Class) +connections. USB CDC makes the device appear as a virtual serial port, +so it reuses the `serial_dist` module with a USB-specific HAL module. + +See the [USB CDC](./programmers-guide.md#usb-cdc) section of the Programmer's +Guide for how to enable the port driver on each platform + +### Platform support + +| Platform | Module | Notes | +|----------|--------|-------| +| ESP32 (S2/S3) | `usb_cdc` | Requires TinyUSB CDC (`CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER`) | +| RP2040/RP2350 | `usb_cdc` | Requires `AVM_USB_CDC_PORT_DRIVER_ENABLED`; disable `pico_enable_stdio_usb` | +| STM32 | `usb_cdc` | Requires TinyUSB integration and `AVM_USB_CDC_PORT_DRIVER_ENABLED` | +| Unix | `uart` | USB CDC devices appear as `/dev/ttyACMx` (Linux) or `/dev/cu.usbmodemXXXX` (macOS) | + +### Quick start, MCU side (ESP32-S3 example) + +```erlang +{ok, _} = net_kernel:start('sensor@serial.local', #{ + name_domain => longnames, + proto_dist => serial_dist, + avm_dist_opts => #{ + uart_opts => [{peripheral, "CDC0"}], + uart_module => usb_cdc + } +}). +``` + +### Quick start, Unix host side + +On Unix, USB CDC devices are standard serial ports. Use the regular +`uart` module: + +```erlang +{ok, _} = net_kernel:start('host@serial.local', #{ + name_domain => longnames, + proto_dist => serial_dist, + avm_dist_opts => #{ + uart_opts => [{peripheral, "/dev/ttyACM0"}, {speed, 115200}], + uart_module => uart + } +}). +``` + +### Multi-device topology + +USB uses a star topology: one host connects to multiple devices through +a USB hub. Each device appears as a separate `/dev/ttyACMx` on the host. +Use `uart_ports` (list of proplists) to connect to multiple devices: + +```erlang +{ok, _} = net_kernel:start('host@serial.local', #{ + name_domain => longnames, + proto_dist => serial_dist, + avm_dist_opts => #{ + uart_ports => [ + [{peripheral, "/dev/ttyACM0"}, {speed, 115200}], + [{peripheral, "/dev/ttyACM1"}, {speed, 115200}] + ], + uart_module => uart + } +}). +``` + ## Distribution features Distribution implementation is (very) partial. The most basic features are available: diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index ebb5a87523..bfb1c17a72 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -1965,6 +1965,38 @@ ok = uart:close(UART) Once the UART driver is closed, any calls to `uart` functions using a reference to the UART driver instance should return with the value `{error, noproc}`. +### USB CDC + +On platforms with a USB device controller, AtomVM can expose a USB CDC (Communications Device Class) ACM interface, which appears on the host as a virtual serial port (`/dev/ttyACMx` on Linux, `/dev/cu.usbmodemXXXX` on macOS, a COM port on Windows). The same byte-stream interface can be used as a UART replacement (logs, REPLs, custom serial protocols) or as the transport for [distribution over USB CDC](./distributed-erlang.md#usb-distribution). + +The Erlang-side API is the platform-specific `usb_cdc` module ([`libs/avm_esp32/src/usb_cdc.erl`](https://github.com/atomvm/AtomVM/blob/main/libs/avm_esp32/src/usb_cdc.erl), [`libs/avm_rp2/src/usb_cdc.erl`](https://github.com/atomvm/AtomVM/blob/main/libs/avm_rp2/src/usb_cdc.erl), [`libs/avm_stm32/src/usb_cdc.erl`](https://github.com/atomvm/AtomVM/blob/main/libs/avm_stm32/src/usb_cdc.erl)). It mirrors the `uart` API: `open/1,2`, `read/1,2`, `write/2`, `close/1`. + +#### Platform support and build configuration + +| Platform | Notes | +|----------|-------| +| ESP32 | Enable `CONFIG_USE_USB_SERIAL` and `CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER` in `menuconfig`. The ESP-IDF `esp_tinyusb` component must be installed. | +| RP2040/RP2350 | Set `-DAVM_USB_CDC_PORT_DRIVER_ENABLED=ON` in CMake. You must also disable stdio over USB (`pico_enable_stdio_usb(AtomVM 0)`) so that the CDC interface is available for the port driver. | +| STM32 | Set `-DAVM_USB_CDC_PORT_DRIVER_ENABLED=ON` in CMake. TinyUSB is fetched automatically by default; set the `TINYUSB_PATH` environment variable to use a local checkout. | + +#### USB VID/PID and string descriptors + +USB CDC ACM is a standard device class, so on chip vendors that ship their own VID with a blessed "standard CDC" PID arrangement, AtomVM uses that pair by default and no override is required for the device class to be correctly identified by the host: + +- **RP2**: `0x2E8A:0x0009` Raspberry Pi's registered "Pico SDK CDC UART" PID, chip-agnostic (covers RP2040 and RP2350), per the [raspberrypi/usb-pid registry](https://github.com/raspberrypi/usb-pid). The same pair is used by pico-sdk's `stdio_usb` and by any other firmware exposing standard CDC under Pi's VID, so host-side tooling cannot distinguish AtomVM-Pico from other CDC firmwares on this pair. Note that bootrom PIDs (`0x0003` on RP2040, `0x000F` on RP2350) must NOT be reused. +- **ESP32**: `0x303A` (Espressif VID) + TinyUSB-derived class-encoded PID, via the `esp_tinyusb` defaults (`CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID=y`, `CONFIG_TINYUSB_DESC_USE_DEFAULT_PID=y`). [Espressif's USB VID/PID guidance](https://docs.espressif.com/projects/esp-iot-solution/en/latest/usb/usb_overview/usb_vid_pid.html) explicitly states that USB standard-class devices built on TinyUSB do not need a separate PID under Espressif's VID. +- **STM32**: `0xCAFE:0x4001` TinyUSB example placeholder, **not** a vendor-issued identity. Production firmware should override this with a real VID/PID. It is possible to apply to ST for a PID. + +If you want AtomVM to be distinguishable from other standard-CDC firmwares on the same chip (so host-side udev rules, drivers, or tooling can target it specifically), override the defaults: + +- **RP2 / STM32**: `-DAVM_USB_CDC_VID=0xXXXX -DAVM_USB_CDC_PID=0xXXXX` in CMake. RP2 builds may register a project-specific PID for free under Pi's VID via the [usb-pid registry](https://github.com/raspberrypi/usb-pid). +- **ESP32**: set `CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID=n` / `CONFIG_TINYUSB_DESC_USE_DEFAULT_PID=n` and provide `CONFIG_TINYUSB_DESC_CUSTOM_VID` / `CONFIG_TINYUSB_DESC_CUSTOM_PID` in `menuconfig`. + +The string descriptors can be overridden the same way: + +- **RP2 / STM32** (CMake): `-DAVM_USB_CDC_MANUFACTURER="Your Company"`, `-DAVM_USB_CDC_PRODUCT="Your Product"`, `-DAVM_USB_CDC_INTERFACE_NAME="Your Product CDC ACM"`. +- **ESP32** (sdkconfig): `CONFIG_TINYUSB_DESC_MANUFACTURER_STRING`, `CONFIG_TINYUSB_DESC_PRODUCT_STRING`, `CONFIG_TINYUSB_DESC_CDC_STRING`. + ### LED Control The LED Control API can be used to drive LEDs, as well as generate PWM signals on GPIO pins. diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index 882d07463b..fff889277b 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -44,8 +44,9 @@ pack_runnable(logging_example logging_example estdlib eavmlib) pack_runnable(http_client http_client estdlib eavmlib avm_network) pack_runnable(disterl disterl estdlib) pack_runnable(serial_disterl serial_disterl eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_unix) +pack_runnable(usb_disterl usb_disterl eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32 avm_unix) pack_runnable(i2c_scanner i2c_scanner eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) pack_runnable(i2c_lis3dh i2c_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) pack_runnable(spi_lis3dh spi_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) -pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2) +pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) diff --git a/examples/erlang/sim800l.erl b/examples/erlang/sim800l.erl index 7c0b1eb303..427c96b36a 100644 --- a/examples/erlang/sim800l.erl +++ b/examples/erlang/sim800l.erl @@ -34,6 +34,7 @@ %% Default pins are auto-detected from the platform and chip model: %% %% Pico (UART1): TX=GP4, RX=GP5 +%% STM32 (USART1): TX=PA9, RX=PA10, AF=7 %% ESP32/S2/S3 (UART1): TX=17, RX=16 %% ESP32-C2 (UART1): TX=4, RX=5 %% ESP32-C3/C5 (UART1): TX=4, RX=5 @@ -46,13 +47,8 @@ -define(AT_TIMEOUT, 2000). start() -> - {TX, RX} = default_pins(), - io:format("Opening UART1 on TX=~B RX=~B~n", [TX, RX]), - UART = uart:open("UART1", [ - {tx, TX}, - {rx, RX}, - {speed, 115200} - ]), + Platform = atomvm:platform(), + UART = open_uart(Platform), %% SIM800L takes 3-5 seconds to boot after power-on case wait_for_module(UART, 5) of ok -> @@ -156,13 +152,33 @@ drain(UART) -> end. %%----------------------------------------------------------------------------- -%% Platform-specific default pins +%% Platform-specific UART operations %%----------------------------------------------------------------------------- -default_pins() -> - default_pins(atomvm:platform()). -%% {TX, RX} +open_uart(stm32) -> + {TX, RX} = default_pins(stm32), + io:format("Opening USART1 on TX=~p RX=~p~n", [TX, RX]), + uart:open([ + {peripheral, 1}, + {tx, TX}, + {rx, RX}, + {af, 7}, + {speed, 115200} + ]); +open_uart(Platform) -> + {TX, RX} = default_pins(Platform), + io:format("Opening UART1 on TX=~p RX=~p~n", [TX, RX]), + uart:open("UART1", [ + {tx, TX}, + {rx, RX}, + {speed, 115200} + ]). + +%%----------------------------------------------------------------------------- +%% Platform-specific default pins +%%----------------------------------------------------------------------------- default_pins(pico) -> {4, 5}; +default_pins(stm32) -> {{a, 9}, {a, 10}}; default_pins(esp32) -> esp32_default_pins(). esp32_default_pins() -> diff --git a/examples/erlang/stm32/CMakeLists.txt b/examples/erlang/stm32/CMakeLists.txt index 5ddcf3f83c..8ecbec5c14 100644 --- a/examples/erlang/stm32/CMakeLists.txt +++ b/examples/erlang/stm32/CMakeLists.txt @@ -70,3 +70,19 @@ add_custom_command( VERBATIM ) add_custom_target(stm32_spi_flash ALL DEPENDS stm32_spi_flash.avm) + +set(SIM800L_BEAM ${CMAKE_BINARY_DIR}/examples/erlang/sim800l.beam) +add_custom_command( + OUTPUT stm32_sim800l.avm + DEPENDS sim800l_main ${SIM800L_BEAM} + ${CMAKE_BINARY_DIR}/libs/estdlib/src/estdlib.avm estdlib + ${CMAKE_BINARY_DIR}/libs/avm_stm32/src/avm_stm32.avm avm_stm32 + PackBEAM + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/packbeam create ${INCLUDE_LINES} stm32_sim800l.avm + ${SIM800L_BEAM} + ${CMAKE_BINARY_DIR}/libs/estdlib/src/estdlib.avm + ${CMAKE_BINARY_DIR}/libs/avm_stm32/src/avm_stm32.avm + COMMENT "Packing runnable stm32_sim800l.avm" + VERBATIM +) +add_custom_target(stm32_sim800l ALL DEPENDS stm32_sim800l.avm) diff --git a/examples/erlang/usb_disterl.erl b/examples/erlang/usb_disterl.erl new file mode 100644 index 0000000000..faec85de95 --- /dev/null +++ b/examples/erlang/usb_disterl.erl @@ -0,0 +1,147 @@ +% +% 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 Example: distributed Erlang over USB CDC. +%% +%% This example starts distribution using USB CDC instead of TCP/IP. +%% It works on ESP32-S2/S3, RP2040/RP2350, and STM32 (MCU side) and +%% on Unix (host side, where USB CDC devices appear as /dev/ttyACMx). +%% +%%

MCU side (ESP32-S3, RP2040, or STM32)

+%% +%% Flash this example to the MCU. It will start distribution over the +%% USB CDC interface and register a process that responds to messages. +%% +%%

Unix host side

+%% +%% Set `SERIAL_DEVICE' to the device path of the MCU's USB CDC port: +%% ``` +%% SERIAL_DEVICE=/dev/ttyACM0 AtomVM usb_disterl.avm +%% ''' +%% +%%

Multi-device (Unix host with USB hub)

+%% +%% Connect multiple MCUs through a USB hub. Each appears as a separate +%% ttyACMx device. Set `SERIAL_MULTI=true' and `SERIAL_DEVICE_1' and +%% `SERIAL_DEVICE_2': +%% ``` +%% SERIAL_MULTI=true SERIAL_DEVICE_1=/dev/ttyACM0 SERIAL_DEVICE_2=/dev/ttyACM1 AtomVM usb_disterl.avm +%% ''' +%% @end +-module(usb_disterl). + +-export([start/0]). + +start() -> + {UartModule, UartConfig} = get_platform_config(), + NodeName = make_node_name(UartConfig), + io:format("Starting USB distribution as ~p~n", [NodeName]), + {ok, _} = net_kernel:start(NodeName, #{ + name_domain => longnames, + proto_dist => serial_dist, + avm_dist_opts => UartConfig#{uart_module => UartModule} + }), + ok = net_kernel:set_cookie(<<"AtomVM">>), + io:format("Distribution started. Node: ~p~n", [node()]), + register(usb_disterl, self()), + loop(). + +loop() -> + receive + {From, ping} -> + io:format("Got ping from ~p~n", [From]), + From ! {self(), pong}, + loop(); + {From, {echo, Msg}} -> + io:format("Echo ~p from ~p~n", [Msg, From]), + From ! {self(), {echo_reply, Msg}}, + loop(); + stop -> + io:format("Stopping~n"), + net_kernel:stop(), + ok; + Other -> + io:format("Unknown message: ~p~n", [Other]), + loop() + end. + +%% @private +get_platform_config() -> + case atomvm:platform() of + esp32 -> + {usb_cdc, #{uart_opts => [{peripheral, "CDC0"}]}}; + pico -> + {usb_cdc, #{uart_opts => []}}; + stm32 -> + {usb_cdc, #{uart_opts => []}}; + generic_unix -> + get_unix_config() + end. + +%% @private +get_unix_config() -> + case os:getenv("SERIAL_MULTI") of + "true" -> + Dev1 = getenv_default("SERIAL_DEVICE_1", "/dev/ttyACM0"), + Dev2 = getenv_default("SERIAL_DEVICE_2", "/dev/ttyACM1"), + {uart, #{ + uart_ports => [ + [{peripheral, Dev1}, {speed, 115200}], + [{peripheral, Dev2}, {speed, 115200}] + ] + }}; + _ -> + Device = getenv_default("SERIAL_DEVICE", "/dev/ttyACM0"), + {uart, #{ + uart_opts => [{peripheral, Device}, {speed, 115200}] + }} + end. + +%% @private +getenv_default(Name, Default) -> + case os:getenv(Name) of + false -> Default; + Value -> Value + end. + +%% @private +make_node_name(#{uart_ports := [FirstPort | _]}) -> + Peripheral = proplists:get_value(peripheral, FirstPort, "usb"), + name_from_peripheral(Peripheral); +make_node_name(#{uart_opts := Opts}) -> + Peripheral = proplists:get_value(peripheral, Opts, "usb"), + name_from_peripheral(Peripheral); +make_node_name(_) -> + 'usb@serial.local'. + +%% @private +name_from_peripheral(Peripheral) when is_list(Peripheral) -> + Base = basename(Peripheral), + Lower = string:to_lower(Base), + list_to_atom(Lower ++ "@serial.local"); +name_from_peripheral(_) -> + 'usb@serial.local'. + +%% @private +basename(Path) -> + case lists:last(string:split(Path, "/", all)) of + [] -> Path; + Name -> Name + end. diff --git a/libs/avm_esp32/src/CMakeLists.txt b/libs/avm_esp32/src/CMakeLists.txt index 07afa6d387..f333f281dd 100644 --- a/libs/avm_esp32/src/CMakeLists.txt +++ b/libs/avm_esp32/src/CMakeLists.txt @@ -31,6 +31,7 @@ set(ERLANG_MODULES ledc spi uart + usb_cdc ) pack_archive(avm_esp32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_esp32/src/usb_cdc.erl b/libs/avm_esp32/src/usb_cdc.erl new file mode 100644 index 0000000000..62e56f3fab --- /dev/null +++ b/libs/avm_esp32/src/usb_cdc.erl @@ -0,0 +1,138 @@ +% +% 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 USB CDC implementation for ESP32 using TinyUSB. +%% +%% This module implements the {@link uart_hal} behaviour over USB CDC ACM. +%% +%% The peripheral name is the CDC interface identifier, e.g. `"CDC0"' or +%% `"USB0"'. If omitted, CDC interface 0 is used. +%% +%% This driver requires `CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER' to be +%% enabled in the ESP-IDF Kconfig and `CONFIG_USE_USB_SERIAL' for TinyUSB. +%% +%% Example: +%% ``` +%% USB = usb_cdc:open([{peripheral, "CDC0"}]), +%% ok = usb_cdc:write(USB, <<"hello">>), +%% case usb_cdc:read(USB, 1000) of +%% {ok, Data} -> io:format("Got: ~p~n", [Data]); +%% {error, timeout} -> io:format("No data~n") +%% end, +%% ok = usb_cdc:close(USB). +%% ''' +%% @end +%%----------------------------------------------------------------------------- +-module(usb_cdc). + +-behaviour(uart_hal). + +-export([open/1, open/2, close/1, read/1, read/2, write/2]). + +%%----------------------------------------------------------------------------- +%% @param Name CDC interface name, e.g. `"CDC0"' or `"USB0"' +%% @param Opts configuration options (currently unused for USB CDC) +%% @returns Port handle or error +%% @doc Open a USB CDC interface with the given name and options. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: string() | binary(), Opts :: list()) -> + port() | {error, term()}. +open(Name, Opts) -> + open([{peripheral, Name} | Opts]). + +%%----------------------------------------------------------------------------- +%% @param Opts configuration options including `{peripheral, Name}' +%% @returns Port handle or error +%% @doc Open a USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Opts :: list()) -> port() | {error, term()}. +open(Opts) -> + open_port({spawn, "usb_cdc"}, Opts). + +%%----------------------------------------------------------------------------- +%% @param Port handle returned by open +%% @returns ok or error +%% @doc Close the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec close(Port :: port()) -> ok | {error, term()}. +close(Port) when is_port(Port) -> + port:call(Port, close). + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data from USB CDC. Blocks until data is available. +%% @end +%%----------------------------------------------------------------------------- +-spec read(Port :: port()) -> {ok, binary()} | {error, term()}. +read(Port) when is_port(Port) -> + port:call(Port, read). + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @param Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, timeout}' or `{error, Reason}' +%% @doc Read data from USB CDC with timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read(Port :: port(), Timeout :: pos_integer()) -> + {ok, binary()} | {error, term()}. +read(Port, Timeout) when is_port(Port) -> + Self = self(), + MonitorRef = monitor(port, Port), + Port ! {'$call', {Self, MonitorRef}, read}, + Result = + receive + {MonitorRef, Reply} -> + Reply; + {'DOWN', MonitorRef, port, Port, normal} -> + {error, noproc}; + {'DOWN', MonitorRef, port, Port, Reason} -> + {error, Reason} + after Timeout -> + % Pass MonitorRef so the driver only clears its slot if it + % still belongs to us; otherwise a cancel racing with a + % just-completed read would wipe a freshly-installed reader. + port:call(Port, {cancel_read, MonitorRef}), + % Drop any reply that landed between the timeout and the + % cancel so it doesn't sit in the mailbox forever. + receive + {MonitorRef, _LateReply} -> ok + after 0 -> ok + end, + {error, timeout} + end, + demonitor(MonitorRef, [flush]), + Result. + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @param Data to write +%% @returns ok or error +%% @doc Write data to USB CDC. +%% @end +%%----------------------------------------------------------------------------- +-spec write(Port :: port(), Data :: iodata()) -> ok | {error, term()}. +write(Port, Data) when is_port(Port) -> + port:call(Port, {write, Data}). diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt index dce842b829..a0dc926e36 100644 --- a/libs/avm_rp2/src/CMakeLists.txt +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -28,6 +28,7 @@ set(ERLANG_MODULES pico spi uart + usb_cdc ) pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_rp2/src/usb_cdc.erl b/libs/avm_rp2/src/usb_cdc.erl new file mode 100644 index 0000000000..efb7bac635 --- /dev/null +++ b/libs/avm_rp2/src/usb_cdc.erl @@ -0,0 +1,137 @@ +% +% 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 USB CDC implementation for RP2040/RP2350 using TinyUSB. +%% +%% This module implements the {@link uart_hal} behaviour over USB CDC ACM. +%% +%% The RP2 platform uses TinyUSB for USB device support. When using this +%% driver, `pico_enable_stdio_usb' should be disabled in the CMake +%% configuration so that the USB CDC interface is available for data transfer +%% instead of console output. +%% +%% Example: +%% ``` +%% USB = usb_cdc:open([]), +%% ok = usb_cdc:write(USB, <<"hello">>), +%% case usb_cdc:read(USB, 1000) of +%% {ok, Data} -> io:format("Got: ~p~n", [Data]); +%% {error, timeout} -> io:format("No data~n") +%% end, +%% ok = usb_cdc:close(USB). +%% ''' +%% @end +%%----------------------------------------------------------------------------- +-module(usb_cdc). + +-behaviour(uart_hal). + +-export([open/1, open/2, close/1, read/1, read/2, write/2]). + +%%----------------------------------------------------------------------------- +%% @param Name peripheral name (ignored for RP2, single CDC interface) +%% @param Opts configuration options +%% @returns Port handle or error +%% @doc Open the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: string() | binary(), Opts :: list()) -> + port() | {error, term()}. +open(Name, Opts) -> + open([{peripheral, Name} | Opts]). + +%%----------------------------------------------------------------------------- +%% @param Opts configuration options +%% @returns Port handle or error +%% @doc Open the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Opts :: list()) -> port() | {error, term()}. +open(Opts) -> + open_port({spawn, "usb_cdc"}, Opts). + +%%----------------------------------------------------------------------------- +%% @param Port handle returned by open +%% @returns ok or error +%% @doc Close the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec close(Port :: port()) -> ok | {error, term()}. +close(Port) when is_port(Port) -> + port:call(Port, close). + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data from USB CDC. Blocks until data is available. +%% @end +%%----------------------------------------------------------------------------- +-spec read(Port :: port()) -> {ok, binary()} | {error, term()}. +read(Port) when is_port(Port) -> + port:call(Port, read). + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @param Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, timeout}' or `{error, Reason}' +%% @doc Read data from USB CDC with timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read(Port :: port(), Timeout :: pos_integer()) -> + {ok, binary()} | {error, term()}. +read(Port, Timeout) when is_port(Port) -> + Self = self(), + MonitorRef = monitor(port, Port), + Port ! {'$call', {Self, MonitorRef}, read}, + Result = + receive + {MonitorRef, Reply} -> + Reply; + {'DOWN', MonitorRef, port, Port, normal} -> + {error, noproc}; + {'DOWN', MonitorRef, port, Port, Reason} -> + {error, Reason} + after Timeout -> + % Pass MonitorRef so the driver only clears its slot if it + % still belongs to us; otherwise a cancel racing with a + % just-completed read would wipe a freshly-installed reader. + port:call(Port, {cancel_read, MonitorRef}), + % Drop any reply that landed between the timeout and the + % cancel so it doesn't sit in the mailbox forever. + receive + {MonitorRef, _LateReply} -> ok + after 0 -> ok + end, + {error, timeout} + end, + demonitor(MonitorRef, [flush]), + Result. + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @param Data to write +%% @returns ok or error +%% @doc Write data to USB CDC. +%% @end +%%----------------------------------------------------------------------------- +-spec write(Port :: port(), Data :: iodata()) -> ok | {error, term()}. +write(Port, Data) when is_port(Port) -> + port:call(Port, {write, Data}). diff --git a/libs/avm_stm32/src/CMakeLists.txt b/libs/avm_stm32/src/CMakeLists.txt index 7437d2a1e9..5b40e14130 100644 --- a/libs/avm_stm32/src/CMakeLists.txt +++ b/libs/avm_stm32/src/CMakeLists.txt @@ -26,6 +26,8 @@ set(ERLANG_MODULES gpio i2c spi + uart + usb_cdc ) pack_archive(avm_stm32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_stm32/src/uart.erl b/libs/avm_stm32/src/uart.erl new file mode 100644 index 0000000000..980129db26 --- /dev/null +++ b/libs/avm_stm32/src/uart.erl @@ -0,0 +1,396 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +%%----------------------------------------------------------------------------- +%% @doc AtomVM UART interface for STM32 +%% +%% This module provides an interface to the UART hardware on STM32 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API (STM32 HAL) +%% {@link init/1}, {@link deinit/1}, {@link write/3}, {@link read/3}, +%% {@link abort/1}, {@link get_state/1}, {@link get_error/1}, +%% {@link halfduplex_init/1}, {@link halfduplex_enable_tx/1}, +%% {@link halfduplex_enable_rx/1}. +%% These map directly to the corresponding HAL_UART_* functions. +%% +%% High-level API (`uart_hal' behavior) +%% {@link open/1}, {@link open/2}, {@link close/1}, {@link read/1}, +%% {@link read/2}, {@link write/2}. +%% @end +%%----------------------------------------------------------------------------- +-module(uart). +-behaviour(uart_hal). + +%% High-level API (uart_hal behaviour) +-export([open/1, open/2, close/1, read/1, read/2, write/2]). + +%% Low-level API (STM32 HAL) +-export([ + init/1, + deinit/1, + write/3, + read/3, + abort/1, + get_state/1, + get_error/1, + halfduplex_init/1, + halfduplex_enable_tx/1, + halfduplex_enable_rx/1 +]). + +-type peripheral() :: 1..9. +-type peripheral_name() :: string() | binary(). +-type uart_resource() :: reference(). +-type uart() :: pid(). +-type uart_state() :: reset | ready | busy | busy_tx | busy_rx | busy_tx_rx | error | timeout. +-type pin() :: {Bank :: atom(), PinNum :: 0..15}. + +-type uart_config() :: #{ + peripheral => peripheral(), + tx => pin(), + rx => pin(), + speed => pos_integer(), + data_bits => 7 | 8 | 9, + stop_bits => 1 | 2, + parity => none | odd | even, + af => non_neg_integer() +}. + +-type halfduplex_config() :: #{ + peripheral => peripheral(), + tx => pin(), + speed => pos_integer(), + data_bits => 7 | 8 | 9, + stop_bits => 1 | 2, + parity => none | odd | even, + af => non_neg_integer() +}. + +-export_type([ + uart/0, uart_resource/0, peripheral/0, uart_state/0, pin/0, uart_config/0, halfduplex_config/0 +]). + +-define(DEFAULT_SPEED, 115200). +-define(DEFAULT_DATA_BITS, 8). +-define(DEFAULT_STOP_BITS, 1). +-define(DEFAULT_PARITY, none). +-define(DEFAULT_AF, 7). +-define(DEFAULT_PERIPHERAL, 1). +-define(DEFAULT_TIMEOUT_MS, 5000). + +%%----------------------------------------------------------------------------- +%% High-level API (uart_hal behaviour) +%%----------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Opts UART configuration options +%% @returns UART handle (pid) +%% @doc Open a UART connection with the given options. +%% +%% Options: +%%
    +%%
  • `{tx, Pin}' - TX pin (required for full-duplex)
  • +%%
  • `{rx, Pin}' - RX pin (required for full-duplex)
  • +%%
  • `{speed, 115200}' - Baud rate (default: 115200)
  • +%%
  • `{data_bits, 8}' - Data bits: 7, 8, or 9 (default: 8)
  • +%%
  • `{stop_bits, 1}' - Stop bits: 1 or 2 (default: 1)
  • +%%
  • `{parity, none}' - Parity: none, odd, or even (default: none)
  • +%%
  • `{peripheral, 1}' - UART peripheral number (default: 1)
  • +%%
  • `{af, 7}' - Alternate function number (default: 7)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Opts :: [{atom(), term()}]) -> uart(). +open(Opts) -> + Config = parse_opts(Opts), + case ?MODULE:init(Config) of + {ok, Resource} -> + spawn_link(fun() -> loop(Resource) end); + {error, Reason} -> + error(Reason) + end. + +%%----------------------------------------------------------------------------- +%% @param Name UART peripheral name +%% @param Opts UART configuration options +%% @returns UART handle (pid) +%% @doc Open a UART connection with the given options. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: peripheral_name(), Opts :: [{atom(), term()}]) -> uart(). +open(Name, Opts) -> + open([{peripheral, Name} | Opts]). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @returns `ok' +%% @doc Close the UART connection. +%% @end +%%----------------------------------------------------------------------------- +-spec close(UART :: uart()) -> ok. +close(Pid) when is_pid(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read currently available data from the UART. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart()) -> {ok, binary()} | {error, term()}. +read(Pid) when is_pid(Pid) -> + call(Pid, read). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @param Timeout Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data from the UART with the specified timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart(), Timeout :: pos_integer()) -> {ok, binary()} | {error, term()}. +read(Pid, Timeout) when is_pid(Pid), is_integer(Timeout), Timeout > 0 -> + call(Pid, {read, Timeout}). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @param Data Data to write (binary or iolist) +%% @returns `ok' or `{error, Reason}' +%% @doc Write data to the UART. +%% @end +%%----------------------------------------------------------------------------- +-spec write(UART :: uart(), Data :: iodata()) -> ok | {error, term()}. +write(Pid, Data) when is_pid(Pid) -> + call(Pid, {write, Data, ?DEFAULT_TIMEOUT_MS}). + +%%----------------------------------------------------------------------------- +%% Low-level API (STM32 HAL) +%%----------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Config UART configuration map +%% @returns `{ok, Resource}' +%% @doc Initialize the UART HW block (HAL_UART_Init). +%% +%% The Config map should contain: +%%
    +%%
  • `{tx, Pin}' - TX pin (required)
  • +%%
  • `{rx, Pin}' - RX pin (required)
  • +%%
  • `{peripheral, 1}' - UART peripheral number (default: 1)
  • +%%
  • `{speed, 115200}' - Baud rate (default: 115200)
  • +%%
  • `{data_bits, 8}' - Data bits: 7, 8, or 9 (default: 8)
  • +%%
  • `{stop_bits, 1}' - Stop bits: 1 or 2 (default: 1)
  • +%%
  • `{parity, none}' - Parity: none, odd, or even (default: none)
  • +%%
  • `{af, 7}' - Alternate function number (default: 7)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec init(Config :: uart_config() | [{atom(), term()}]) -> {ok, uart_resource()}. +init(_Config) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns `ok' +%% @doc Disable the UART HW block (HAL_UART_DeInit). +%% @end +%%----------------------------------------------------------------- +-spec deinit(Resource :: uart_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @param Data Data to write +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns Number of bytes written or `{error, Reason}' +%% @doc Write data (HAL_UART_Transmit). +%% @end +%%----------------------------------------------------------------- +-spec write(Resource :: uart_resource(), Data :: binary(), TimeoutMs :: timeout()) -> + non_neg_integer() | {error, term()}. +write(_Resource, _Data, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @param Count Number of bytes to read +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data (HAL_UART_Receive). +%% @end +%%----------------------------------------------------------------- +-spec read(Resource :: uart_resource(), Count :: non_neg_integer(), TimeoutMs :: timeout()) -> + {ok, binary()} | {error, term()}. +read(_Resource, _Count, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns `ok' or `{error, Reason}' +%% @doc Abort ongoing UART transfer (HAL_UART_Abort). +%% @end +%%----------------------------------------------------------------- +-spec abort(Resource :: uart_resource()) -> ok | {error, term()}. +abort(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns UART state atom +%% @doc Get the UART state (HAL_UART_GetState). +%% @end +%%----------------------------------------------------------------- +-spec get_state(Resource :: uart_resource()) -> uart_state(). +get_state(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns Error code as integer +%% @doc Get the UART error (HAL_UART_GetError). +%% @end +%%----------------------------------------------------------------- +-spec get_error(Resource :: uart_resource()) -> non_neg_integer(). +get_error(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Config Half-duplex UART configuration +%% @returns `{ok, Resource}' +%% @doc Initialize UART in half-duplex mode (HAL_HalfDuplex_Init). +%% @end +%%----------------------------------------------------------------- +-spec halfduplex_init(Config :: halfduplex_config() | [{atom(), term()}]) -> {ok, uart_resource()}. +halfduplex_init(_Config) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource from `halfduplex_init/1' +%% @returns `ok' or `error' +%% @doc Enable transmitter (HAL_HalfDuplex_EnableTransmitter). +%% @end +%%----------------------------------------------------------------- +-spec halfduplex_enable_tx(Resource :: uart_resource()) -> ok | error. +halfduplex_enable_tx(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource from `halfduplex_init/1' +%% @returns `ok' or `error' +%% @doc Enable receiver (HAL_HalfDuplex_EnableReceiver). +%% @end +%%----------------------------------------------------------------- +-spec halfduplex_enable_rx(Resource :: uart_resource()) -> ok | error. +halfduplex_enable_rx(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% Internal helpers +%%----------------------------------------------------------------- + +call(Pid, Request) -> + MRef = erlang:monitor(process, Pid), + Ref = make_ref(), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> + erlang:demonitor(MRef, [flush]), + Reply; + {'DOWN', MRef, process, Pid, Reason} -> + {error, {server_died, Reason}} + end. + +loop(Resource) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource) + end + end. + +handle_request(Resource, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(Resource, read) -> + case ?MODULE:read(Resource, 1, 0) of + {ok, Byte} -> + {reply, {ok, read_available(Resource, [Byte])}}; + {error, _} = Error -> + {reply, Error} + end; +handle_request(Resource, {read, Timeout}) -> + case ?MODULE:read(Resource, 1, Timeout) of + {ok, Byte} -> + {reply, {ok, read_available(Resource, [Byte])}}; + {error, _} = Error -> + {reply, Error} + end; +handle_request(Resource, {write, Data, Timeout}) -> + Bin = iolist_to_binary(Data), + case ?MODULE:write(Resource, Bin, Timeout) of + N when is_integer(N) -> {reply, ok}; + {error, _} = Error -> {reply, Error} + end. + +read_available(Resource, Acc) -> + case ?MODULE:read(Resource, 1, 0) of + {ok, Byte} -> + read_available(Resource, [Byte | Acc]); + {error, timeout} -> + erlang:iolist_to_binary(lists:reverse(Acc)); + {error, _} -> + erlang:iolist_to_binary(lists:reverse(Acc)) + end. + +parse_opts(Opts) -> + Tx = proplists:get_value(tx, Opts), + Rx = proplists:get_value(rx, Opts), + Tx =:= undefined andalso error({missing_required_option, tx}), + Rx =:= undefined andalso error({missing_required_option, rx}), + Speed = proplists:get_value(speed, Opts, ?DEFAULT_SPEED), + DataBits = proplists:get_value(data_bits, Opts, ?DEFAULT_DATA_BITS), + StopBits = proplists:get_value(stop_bits, Opts, ?DEFAULT_STOP_BITS), + Parity = proplists:get_value(parity, Opts, ?DEFAULT_PARITY), + Peripheral = normalize_peripheral(proplists:get_value(peripheral, Opts, ?DEFAULT_PERIPHERAL)), + AF = proplists:get_value(af, Opts, ?DEFAULT_AF), + [ + {peripheral, Peripheral}, + {tx, Tx}, + {rx, Rx}, + {af, AF}, + {speed, Speed}, + {data_bits, DataBits}, + {stop_bits, StopBits}, + {parity, parity_to_int(Parity)} + ]. + +parity_to_int(none) -> 0; +parity_to_int(odd) -> 1; +parity_to_int(even) -> 2. + +normalize_peripheral(N) when is_integer(N) -> N; +normalize_peripheral([$U, $A, $R, $T | N]) -> list_to_integer(N); +normalize_peripheral(<<"UART", N/binary>>) -> binary_to_integer(N). diff --git a/libs/avm_stm32/src/usb_cdc.erl b/libs/avm_stm32/src/usb_cdc.erl new file mode 100644 index 0000000000..c4e24c30d4 --- /dev/null +++ b/libs/avm_stm32/src/usb_cdc.erl @@ -0,0 +1,135 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +%%----------------------------------------------------------------------------- +%% @doc USB CDC implementation for STM32 using TinyUSB. +%% +%% This module implements the {@link uart_hal} behaviour over USB CDC ACM. +%% +%% Requires TinyUSB to be integrated into the STM32 build and +%% `AVM_USB_CDC_PORT_DRIVER_ENABLED' to be defined. +%% +%% Example: +%% ``` +%% USB = usb_cdc:open([]), +%% ok = usb_cdc:write(USB, <<"hello">>), +%% case usb_cdc:read(USB, 1000) of +%% {ok, Data} -> io:format("Got: ~p~n", [Data]); +%% {error, timeout} -> io:format("No data~n") +%% end, +%% ok = usb_cdc:close(USB). +%% ''' +%% @end +%%----------------------------------------------------------------------------- +-module(usb_cdc). + +-behaviour(uart_hal). + +-export([open/1, open/2, close/1, read/1, read/2, write/2]). + +%%----------------------------------------------------------------------------- +%% @param Name peripheral name (ignored) +%% @param Opts configuration options +%% @returns Port handle or error +%% @doc Open the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: string() | binary(), Opts :: list()) -> + port() | {error, term()}. +open(Name, Opts) -> + open([{peripheral, Name} | Opts]). + +%%----------------------------------------------------------------------------- +%% @param Opts configuration options +%% @returns Port handle or error +%% @doc Open the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Opts :: list()) -> port() | {error, term()}. +open(Opts) -> + open_port({spawn, "usb_cdc"}, Opts). + +%%----------------------------------------------------------------------------- +%% @param Port handle returned by open +%% @returns ok or error +%% @doc Close the USB CDC interface. +%% @end +%%----------------------------------------------------------------------------- +-spec close(Port :: port()) -> ok | {error, term()}. +close(Port) when is_port(Port) -> + port:call(Port, close). + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data from USB CDC. Blocks until data is available. +%% @end +%%----------------------------------------------------------------------------- +-spec read(Port :: port()) -> {ok, binary()} | {error, term()}. +read(Port) when is_port(Port) -> + port:call(Port, read). + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @param Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, timeout}' or `{error, Reason}' +%% @doc Read data from USB CDC with timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read(Port :: port(), Timeout :: pos_integer()) -> + {ok, binary()} | {error, term()}. +read(Port, Timeout) when is_port(Port) -> + Self = self(), + MonitorRef = monitor(port, Port), + Port ! {'$call', {Self, MonitorRef}, read}, + Result = + receive + {MonitorRef, Reply} -> + Reply; + {'DOWN', MonitorRef, port, Port, normal} -> + {error, noproc}; + {'DOWN', MonitorRef, port, Port, Reason} -> + {error, Reason} + after Timeout -> + % Pass MonitorRef so the driver only clears its slot if it + % still belongs to us; otherwise a cancel racing with a + % just-completed read would wipe a freshly-installed reader. + port:call(Port, {cancel_read, MonitorRef}), + % Drop any reply that landed between the timeout and the + % cancel so it doesn't sit in the mailbox forever. + receive + {MonitorRef, _LateReply} -> ok + after 0 -> ok + end, + {error, timeout} + end, + demonitor(MonitorRef, [flush]), + Result. + +%%----------------------------------------------------------------------------- +%% @param Port handle +%% @param Data to write +%% @returns ok or error +%% @doc Write data to USB CDC. +%% @end +%%----------------------------------------------------------------------------- +-spec write(Port :: port(), Data :: iodata()) -> ok | {error, term()}. +write(Port, Data) when is_port(Port) -> + port:call(Port, {write, Data}). diff --git a/libs/eavmlib/src/uart_hal.erl b/libs/eavmlib/src/uart_hal.erl index 35bc72f50f..5078af001d 100644 --- a/libs/eavmlib/src/uart_hal.erl +++ b/libs/eavmlib/src/uart_hal.erl @@ -26,7 +26,7 @@ %% Asynchronous Receiver-Transmitter) operations across all supported %% platforms. %% -%% ESP32 and RP2 platforms provide UART implementations. +%% ESP32, RP2, STM32 and generic unix platforms provide UART implementations. %% %%

Lifecycle

%% diff --git a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt index 321b1dc2dc..aad06d9767 100644 --- a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt @@ -31,6 +31,7 @@ set(AVM_BUILTIN_COMPONENT_SRCS "spi_driver.c" "storage_nif.c" "uart_driver.c" + "usb_cdc_driver.c" "otp_crypto_platform.c" "otp_net_platform.c" "otp_socket_platform.c" @@ -67,6 +68,12 @@ if(CONFIG_AVM_ENABLE_DAC_NIF) endif() endif() +if(CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER) + if(IDF_VERSION_MAJOR GREATER_EQUAL 6 OR (IDF_VERSION_MAJOR EQUAL 5 AND IDF_VERSION_MINOR GREATER_EQUAL 3)) + target_link_libraries(${COMPONENT_LIB} PRIVATE idf::espressif__esp_tinyusb) + endif() +endif() + if (IDF_VERSION_MAJOR EQUAL 4) idf_build_set_property( LINK_OPTIONS "-Wl,--whole-archive ${CMAKE_CURRENT_BINARY_DIR}/lib${COMPONENT_NAME}.a -Wl,--no-whole-archive" diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 2f0fd08c4c..41ad63aaba 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -112,6 +112,15 @@ config AVM_ENABLE_UART_PORT_DRIVER bool "Enable UART port driver" default y +config AVM_ENABLE_USB_CDC_PORT_DRIVER + bool "Enable USB CDC port driver" + depends on TINYUSB_CDC_ENABLED + default n + help + Enable the AtomVM usb_cdc port driver. This requires native USB CDC + support through TinyUSB and should be enabled only when the firmware + is intended to expose a USB CDC data port. + config AVM_ENABLE_OTP_CRYPTO_NIFS bool "Enable OTP Crypto NIFs" default y diff --git a/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c b/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c new file mode 100644 index 0000000000..af5701ad70 --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c @@ -0,0 +1,679 @@ +/* + * 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 +#ifdef CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "atom.h" +#include "bif.h" +#include "context.h" +#include "debug.h" +#include "defaultatoms.h" +#include "globalcontext.h" +#include "interop.h" +#include "mailbox.h" +#include "module.h" +#include "platform_defaultatoms.h" +#include "port.h" +#include "scheduler.h" +#include "smp.h" +#include "term.h" +#include "utils.h" + +#include "esp32_sys.h" +#include "sys.h" + +static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx); + +#define TAG "usb_cdc_driver" +#define USB_CDC_BUF_SIZE 512 +#define NO_REF 0 +#define NO_READER term_invalid_term() + +// Abort a write after this many ms of no progress (host stalled or unplugged). +#define USB_CDC_WRITE_TIMEOUT_MS 500 + +struct USBCDCData +{ + QueueHandle_t rxqueue; + EventListener listener; + term reader_process_pid; + uint64_t reader_ref_ticks; + tinyusb_cdcacm_itf_t itf; + bool owns_itf; +#ifndef AVM_NO_SMP + Mutex *reader_lock; +#endif +}; + +enum usb_cdc_cmd +{ + USBCDCInvalidCmd = 0, + USBCDCReadCmd = 1, + USBCDCWriteCmd = 2, + USBCDCCloseCmd = 3, + USBCDCCancelCmd = 4 +}; + +static const AtomStringIntPair cmd_table[] = { + { ATOM_STR("\x4", "read"), USBCDCReadCmd }, + { ATOM_STR("\x5", "write"), USBCDCWriteCmd }, + { ATOM_STR("\x5", "close"), USBCDCCloseCmd }, + { ATOM_STR("\xB", "cancel_read"), USBCDCCancelCmd }, + SELECT_INT_DEFAULT(USBCDCInvalidCmd) +}; + +// Singleton data for each CDC interface. Read by RX callback and +// written by create_port and do_close with s_open_lock +static _Atomic(struct USBCDCData *) s_cdc_data[CONFIG_TINYUSB_CDC_COUNT]; + +#ifndef AVM_NO_SMP +static Mutex *s_open_lock = NULL; +#endif + +static void usb_cdc_driver_init(GlobalContext *global) +{ + UNUSED(global); +#ifndef AVM_NO_SMP + s_open_lock = smp_mutex_create(); + if (IS_NULL_PTR(s_open_lock)) { + ESP_LOGE(TAG, "Failed to create open lock"); + abort(); + } +#endif +} + +static void usb_cdc_send_error_reply(Context *ctx, term pid, term ref, AtomString reason_atom_str) +{ + GlobalContext *glb = ctx->global; + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + term reason = globalcontext_make_atom(glb, reason_atom_str); + term error_tuple = port_create_error_tuple(ctx, reason); + port_send_reply(ctx, pid, ref, error_tuple); +} + +static void update_reader_data(struct USBCDCData *cdc_data, term pid, uint64_t ref_ticks) +{ + cdc_data->reader_process_pid = pid; + cdc_data->reader_ref_ticks = ref_ticks; +} + +static void usb_cdc_rx_callback(int itf, cdcacm_event_t *event) +{ + if (itf < 0 || itf >= CONFIG_TINYUSB_CDC_COUNT) { + return; + } + struct USBCDCData *cdc_data = atomic_load_explicit(&s_cdc_data[itf], memory_order_acquire); + if (cdc_data == NULL) { + return; + } + size_t rx_size = 0; + esp_err_t ret = tinyusb_cdcacm_read(itf, NULL, 0, &rx_size); + if (ret != ESP_OK || rx_size == 0) { + // Peek failed, just signal that data may be available + rx_size = 1; + } + // Post the number of available bytes to the queue + uint32_t event_val = (uint32_t) rx_size; + xQueueSendFromISR(cdc_data->rxqueue, &event_val, NULL); +} + +EventListener *usb_cdc_interrupt_callback(GlobalContext *glb, EventListener *listener) +{ + struct USBCDCData *cdc_data = GET_LIST_ENTRY(listener, struct USBCDCData, listener); + + uint32_t event_val; + if (xQueueReceive(cdc_data->rxqueue, (void *) &event_val, (TickType_t) 0)) { + SMP_MUTEX_LOCK(cdc_data->reader_lock); + term reader_pid = cdc_data->reader_process_pid; + uint64_t reader_ref_ticks = cdc_data->reader_ref_ticks; + if (reader_pid != term_invalid_term()) { + update_reader_data(cdc_data, NO_READER, NO_REF); + } + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + + if (reader_pid != term_invalid_term()) { + // Read actual available data + uint8_t buf[USB_CDC_BUF_SIZE]; + size_t rx_size = 0; + esp_err_t ret = tinyusb_cdcacm_read(cdc_data->itf, buf, sizeof(buf), &rx_size); + if (ret != ESP_OK || rx_size == 0) { + BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, err_heap) + term ref = term_from_ref_ticks(reader_ref_ticks, &err_heap); + term error_tuple = term_alloc_tuple(2, &err_heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "eagain"))); + term result_tuple = term_alloc_tuple(2, &err_heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, error_tuple); + globalcontext_send_message(glb, term_to_local_process_id(reader_pid), result_tuple); + END_WITH_STACK_HEAP(err_heap, glb) + return listener; + } + + int bin_size = term_binary_heap_size(rx_size); + + int local_pid = term_to_local_process_id(reader_pid); + Heap heap; + if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return listener; + } + + term bin = term_create_uninitialized_binary(rx_size, &heap, glb); + memcpy((void *) term_binary_data(bin), buf, rx_size); + + term ok_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(ok_tuple, 0, OK_ATOM); + term_put_tuple_element(ok_tuple, 1, bin); + + term ref = term_from_ref_ticks(reader_ref_ticks, &heap); + + term result_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, ok_tuple); + + globalcontext_send_message(glb, local_pid, result_tuple); + + memory_destroy_heap(&heap, glb); + } + } + + return listener; +} + +static tinyusb_cdcacm_itf_t parse_cdc_interface(term opts, GlobalContext *global) +{ + term peripheral = interop_kv_get_value_default(opts, ATOM_STR("\xA", "peripheral"), + UNDEFINED_ATOM, global); + if (peripheral == UNDEFINED_ATOM) { + return TINYUSB_CDC_ACM_0; + } + int ok; + char *name = interop_term_to_string(peripheral, &ok); + if (!name || !ok) { + return TINYUSB_CDC_ACM_0; + } + tinyusb_cdcacm_itf_t itf = TINYUSB_CDC_ACM_0; + if (strcmp(name, "CDC0") == 0 || strcmp(name, "USB0") == 0) { + itf = TINYUSB_CDC_ACM_0; + } +#if CONFIG_TINYUSB_CDC_COUNT > 1 + else if (strcmp(name, "CDC1") == 0 || strcmp(name, "USB1") == 0) { + itf = 1; + } +#endif + free(name); + return itf; +} + +static Context *usb_cdc_driver_create_port(GlobalContext *global, term opts) +{ + tinyusb_cdcacm_itf_t itf = parse_cdc_interface(opts, global); + + if (itf >= CONFIG_TINYUSB_CDC_COUNT) { + ESP_LOGE(TAG, "Invalid CDC interface number: %d", itf); + return NULL; + } + + SMP_MUTEX_LOCK(s_open_lock); + if (atomic_load_explicit(&s_cdc_data[itf], memory_order_acquire) != NULL) { + SMP_MUTEX_UNLOCK(s_open_lock); + ESP_LOGE(TAG, "CDC interface %d already in use", itf); + return NULL; + } + + struct USBCDCData *cdc_data = malloc(sizeof(struct USBCDCData)); + if (IS_NULL_PTR(cdc_data)) { + SMP_MUTEX_UNLOCK(s_open_lock); + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + return NULL; + } + +#ifndef AVM_NO_SMP + cdc_data->reader_lock = smp_mutex_create(); + if (IS_NULL_PTR(cdc_data->reader_lock)) { + free(cdc_data); + SMP_MUTEX_UNLOCK(s_open_lock); + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + return NULL; + } +#endif + + cdc_data->rxqueue = xQueueCreate(16, sizeof(uint32_t)); + if (IS_NULL_PTR(cdc_data->rxqueue)) { + ESP_LOGE(TAG, "Failed to create RX queue"); +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + free(cdc_data); + SMP_MUTEX_UNLOCK(s_open_lock); + return NULL; + } + cdc_data->listener.handler = usb_cdc_interrupt_callback; + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + cdc_data->itf = itf; + cdc_data->owns_itf = false; + + // Initialize TinyUSB if not already done + // Note: TinyUSB init may already be done in main.c for console; + // we only initialize our specific CDC interface for data. + tinyusb_config_cdcacm_t acm_cfg = { + .cdc_port = itf, + .callback_rx = &usb_cdc_rx_callback, + .callback_rx_wanted_char = NULL, + .callback_line_state_changed = NULL, + .callback_line_coding_changed = NULL, + }; + esp_err_t err = tusb_cdc_acm_init(&acm_cfg); + if (err == ESP_OK) { + cdc_data->owns_itf = true; + } else if (err == ESP_ERR_INVALID_STATE) { + // The interface was already initialized (e.g. by the console). + // tusb_cdc_acm_init ignored our acm_cfg, so register our RX + // callback explicitly — otherwise we'd never see incoming bytes. + esp_err_t rerr = tinyusb_cdcacm_register_callback(itf, CDC_EVENT_RX, &usb_cdc_rx_callback); + if (rerr != ESP_OK) { + ESP_LOGE(TAG, "Failed to register CDC RX callback for interface %d: %s", + itf, esp_err_to_name(rerr)); + vQueueDelete(cdc_data->rxqueue); +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + free(cdc_data); + SMP_MUTEX_UNLOCK(s_open_lock); + return NULL; + } + } else { + ESP_LOGE(TAG, "Failed to init CDC ACM interface %d: %s", itf, esp_err_to_name(err)); + vQueueDelete(cdc_data->rxqueue); +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + free(cdc_data); + SMP_MUTEX_UNLOCK(s_open_lock); + return NULL; + } + + cdc_data->listener.sender = cdc_data->rxqueue; + if (xQueueAddToSet(cdc_data->rxqueue, event_set) != pdPASS) { + ESP_LOGE(TAG, "Failed to add USB CDC queue to event set."); + if (cdc_data->owns_itf) { + tusb_cdc_acm_deinit(itf); + } else { + tinyusb_cdcacm_unregister_callback(itf, CDC_EVENT_RX); + } + vQueueDelete(cdc_data->rxqueue); +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + free(cdc_data); + SMP_MUTEX_UNLOCK(s_open_lock); + return NULL; + } + + Context *ctx = context_new(global); + if (IS_NULL_PTR(ctx)) { + ESP_LOGE(TAG, "Failed to create context"); + xQueueRemoveFromSet(cdc_data->rxqueue, event_set); + if (cdc_data->owns_itf) { + tusb_cdc_acm_deinit(itf); + } else { + tinyusb_cdcacm_unregister_callback(itf, CDC_EVENT_RX); + } + vQueueDelete(cdc_data->rxqueue); +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + free(cdc_data); + SMP_MUTEX_UNLOCK(s_open_lock); + return NULL; + } + + sys_register_listener(global, &cdc_data->listener); + + atomic_store_explicit(&s_cdc_data[itf], cdc_data, memory_order_release); + SMP_MUTEX_UNLOCK(s_open_lock); + + ctx->native_handler = usb_cdc_driver_consume_mailbox; + ctx->platform_data = cdc_data; + + return ctx; +} + +static void usb_cdc_driver_do_read(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term pid = gen_message.pid; + term ref = gen_message.ref; + uint64_t ref_ticks = term_to_ref_ticks(ref); + + int local_pid = term_to_local_process_id(pid); + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + if (cdc_data->reader_process_pid != term_invalid_term()) { + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Failed to allocate space for error tuple"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + term ealready = globalcontext_make_atom(glb, ATOM_STR("\x8", "ealready")); + term error_tuple = port_create_error_tuple(ctx, ealready); + port_send_reply(ctx, pid, ref, error_tuple); + return; + } + + // Try immediate read + uint8_t buf[USB_CDC_BUF_SIZE]; + size_t rx_size = 0; + esp_err_t ret = tinyusb_cdcacm_read(cdc_data->itf, buf, sizeof(buf), &rx_size); + if (ret == ESP_OK && rx_size > 0) { + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + int bin_size = term_binary_heap_size(rx_size); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, bin_size + TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Failed to allocate space for return value"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + term bin = term_create_uninitialized_binary(rx_size, &ctx->heap, glb); + memcpy((void *) term_binary_data(bin), buf, rx_size); + + term ok_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ok_tuple, 0, OK_ATOM); + term_put_tuple_element(ok_tuple, 1, bin); + + port_send_reply(ctx, pid, ref, ok_tuple); + } else { + update_reader_data(cdc_data, pid, ref_ticks); + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + } +} + +static void usb_cdc_driver_do_cancel_read(Context *ctx, GenMessage gen_message) +{ + struct USBCDCData *cdc_data = ctx->platform_data; + term req = gen_message.req; + term pid = gen_message.pid; + term ref = gen_message.ref; + + // Two forms: bare `cancel_read` clears unconditionally (legacy), and + // `{cancel_read, ReadRef}` only clears if ReadRef matches the stored + // reader. The tuple form is what avoids the race where a cancel + // wipes a freshly-installed reader belonging to a different request. + bool clear_unconditionally = term_is_atom(req); + uint64_t target_ref_ticks = 0; + if (!clear_unconditionally) { + target_ref_ticks = term_to_ref_ticks(term_get_tuple_element(req, 1)); + } + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + if (clear_unconditionally || cdc_data->reader_ref_ticks == target_ref_ticks) { + update_reader_data(cdc_data, NO_READER, NO_REF); + } + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Failed to allocate space for return value"); + globalcontext_send_message(ctx->global, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void usb_cdc_driver_do_write(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term msg = gen_message.req; + term pid = gen_message.pid; + term ref = gen_message.ref; + + term data = term_get_tuple_element(msg, 1); + + int local_pid = term_to_local_process_id(pid); + + size_t buffer_size; + switch (interop_iolist_size(data, &buffer_size)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + case InteropBadArg: + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "badarg")); + return; + } + void *buffer = malloc(buffer_size); + if (IS_NULL_PTR(buffer)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + switch (interop_write_iolist(data, buffer)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + free(buffer); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + case InteropBadArg: + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "badarg")); + return; + } + + // If the host hasn't opened the CDC interface yet, silently drop the + // data and report success + if (!tud_cdc_n_connected(cdc_data->itf)) { + free(buffer); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); + return; + } + + // Write in chunks if needed (CDC has limited TX buffer) + size_t written = 0; + TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(USB_CDC_WRITE_TIMEOUT_MS); + while (written < buffer_size) { + if (!tud_cdc_n_connected(cdc_data->itf)) { + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "closed")); + return; + } + size_t to_write = buffer_size - written; + size_t chunk = tinyusb_cdcacm_write_queue(cdc_data->itf, + (const uint8_t *) buffer + written, to_write); + tinyusb_cdcacm_write_flush(cdc_data->itf, pdMS_TO_TICKS(100)); + if (chunk > 0) { + written += chunk; + // Reset the deadline on any progress so a slow but moving + // host can drain a buffer larger than the FIFO without tripping. + deadline = xTaskGetTickCount() + pdMS_TO_TICKS(USB_CDC_WRITE_TIMEOUT_MS); + } else { + if ((int32_t) (xTaskGetTickCount() - deadline) >= 0) { + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x7", "timeout")); + return; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + + free(buffer); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Failed to allocate space for return value"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term pid = gen_message.pid; + term ref = gen_message.ref; + + // Stop the RX path BEFORE deleting the queue: NULL-publish so any + // future callback short-circuits, then unregister our callback so + // esp_tinyusb won't dispatch new ones at all, then unhook from the + // event set so xQueueDelete sees no consumers. + SMP_MUTEX_LOCK(s_open_lock); + atomic_store_explicit(&s_cdc_data[cdc_data->itf], NULL, memory_order_release); + SMP_MUTEX_UNLOCK(s_open_lock); + + if (cdc_data->owns_itf) { + tusb_cdc_acm_deinit(cdc_data->itf); + } else { + tinyusb_cdcacm_unregister_callback(cdc_data->itf, CDC_EVENT_RX); + } + sys_unregister_listener(glb, &cdc_data->listener); + xQueueRemoveFromSet(cdc_data->rxqueue, event_set); + vQueueDelete(cdc_data->rxqueue); + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + term pending_reader_pid = cdc_data->reader_process_pid; + uint64_t pending_reader_ref_ticks = cdc_data->reader_ref_ticks; + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + + if (pending_reader_pid != term_invalid_term()) { + BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + term error_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); + term reader_ref = term_from_ref_ticks(pending_reader_ref_ticks, &heap); + term reply = term_alloc_tuple(2, &heap); + term_put_tuple_element(reply, 0, reader_ref); + term_put_tuple_element(reply, 1, error_tuple); + globalcontext_send_message(glb, term_to_local_process_id(pending_reader_pid), reply); + END_WITH_STACK_HEAP(heap, glb) + } + +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Failed to allocate space for return value"); + globalcontext_send_message(glb, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); + + free(cdc_data); + ctx->platform_data = NULL; +} + +static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) +{ + GlobalContext *glb = ctx->global; + bool is_closed = false; + while (mailbox_has_next(&ctx->mailbox)) { + Message *message = mailbox_first(&ctx->mailbox); + term msg = message->message; + GenMessage gen_message; + if (UNLIKELY(port_parse_gen_message(msg, &gen_message) != GenCallMessage)) { + ESP_LOGW(TAG, "Received invalid message."); + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + continue; + } + + uint64_t ref_ticks = term_to_ref_ticks(gen_message.ref); + int local_pid = term_to_local_process_id(gen_message.pid); + + if (is_closed) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Failed to allocate space for error tuple"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + } else { + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, NOPROC_ATOM); + + term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + + term result_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, error_tuple); + + globalcontext_send_message(glb, local_pid, result_tuple); + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + continue; + } + + term req = gen_message.req; + term cmd_term = term_is_atom(req) ? req : term_get_tuple_element(req, 0); + + enum usb_cdc_cmd cmd = interop_atom_term_select_int(cmd_table, cmd_term, ctx->global); + switch (cmd) { + case USBCDCReadCmd: + usb_cdc_driver_do_read(ctx, gen_message); + break; + case USBCDCWriteCmd: + usb_cdc_driver_do_write(ctx, gen_message); + break; + case USBCDCCloseCmd: + usb_cdc_driver_do_close(ctx, gen_message); + is_closed = true; + break; + case USBCDCCancelCmd: + usb_cdc_driver_do_cancel_read(ctx, gen_message); + break; + default: + ESP_LOGW(TAG, "Unrecognized command."); + usb_cdc_send_error_reply(ctx, gen_message.pid, gen_message.ref, + ATOM_STR("\xF", "unknown_command")); + break; + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + } + return is_closed ? NativeTerminate : NativeContinue; +} + +REGISTER_PORT_DRIVER(usb_cdc, usb_cdc_driver_init, NULL, usb_cdc_driver_create_port) + +#endif diff --git a/src/platforms/rp2/CMakeLists.txt b/src/platforms/rp2/CMakeLists.txt index b2143ab4cd..1f9859bd9a 100644 --- a/src/platforms/rp2/CMakeLists.txt +++ b/src/platforms/rp2/CMakeLists.txt @@ -74,6 +74,12 @@ option(AVM_WAIT_BOOTSEL_ON_EXIT "Wait in BOOTSEL rather than shutdown on exit" O option(AVM_REBOOT_ON_NOT_OK "Reboot Pico if result is not ok" OFF) option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) option(AVM_DISABLE_JIT "Disable just in time compilation." ON) +option(AVM_USB_CDC_PORT_DRIVER_ENABLED "Enable USB CDC port driver." OFF) +set(AVM_USB_CDC_VID "0x2E8A" CACHE STRING "USB CDC vendor ID (default: Raspberry Pi VID)") +set(AVM_USB_CDC_PID "0x0009" CACHE STRING "USB CDC product ID (default: Pi standard Pico SDK CDC UART, chip-agnostic)") +set(AVM_USB_CDC_MANUFACTURER "AtomVM" CACHE STRING "USB CDC iManufacturer string descriptor") +set(AVM_USB_CDC_PRODUCT "AtomVM CDC" CACHE STRING "USB CDC iProduct string descriptor") +set(AVM_USB_CDC_INTERFACE_NAME "AtomVM CDC ACM" CACHE STRING "USB CDC interface string descriptor") option(AVM_PRINT_PROCESS_CRASH_DUMPS "Print crash reports when processes die with non-standard reasons" ON) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^cortex-m0") # Cortex-M0/M0+ (e.g. RP2040): ARMv6-M, Thumb-1 only diff --git a/src/platforms/rp2/src/CMakeLists.txt b/src/platforms/rp2/src/CMakeLists.txt index fe5f7fb2ac..f6d0fbdf2f 100644 --- a/src/platforms/rp2/src/CMakeLists.txt +++ b/src/platforms/rp2/src/CMakeLists.txt @@ -61,7 +61,7 @@ if (AVM_DISABLE_SMP) target_compile_definitions(AtomVM PRIVATE PICO_FLASH_ASSUME_CORE1_SAFE) endif() -if (AVM_WAIT_BOOTSEL_ON_EXIT) +if (AVM_WAIT_BOOTSEL_ON_EXIT AND NOT AVM_USB_CDC_PORT_DRIVER_ENABLED) target_compile_definitions(AtomVM PRIVATE WAIT_BOOTSEL_ON_EXIT) endif() @@ -73,9 +73,15 @@ set( add_subdirectory(lib) target_link_libraries(AtomVM PRIVATE libAtomVM${PLATFORM_LIB_SUFFIX}) -# enable usb output, disable uart output -pico_enable_stdio_usb(AtomVM 1) -pico_enable_stdio_uart(AtomVM 0) +# Console routing: when our USB CDC port driver owns the TinyUSB device, +# the Pico SDK's stdio_usb cannot share it -- send stdio to UART instead. +if (AVM_USB_CDC_PORT_DRIVER_ENABLED) + pico_enable_stdio_usb(AtomVM 0) + pico_enable_stdio_uart(AtomVM 1) +else() + pico_enable_stdio_usb(AtomVM 1) + pico_enable_stdio_uart(AtomVM 0) +endif() # create map/bin/hex/uf2 file in addition to ELF. pico_add_extra_outputs(AtomVM) diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index 7410586f04..c301f145b9 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -44,6 +44,10 @@ set(SOURCE_FILES ../../../../libAtomVM/portnifloader.c ) +if (AVM_USB_CDC_PORT_DRIVER_ENABLED) + list(APPEND SOURCE_FILES usb_cdc_driver.c usb_descriptors.c) +endif() + set( PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR} @@ -129,3 +133,16 @@ if (NOT AVM_DISABLE_JIT) endif() target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,i2c_nif -Wl,-u -Wl,spi_nif -Wl,-u -Wl,uart_nif -Wl,-u -Wl,otp_crypto_nif") + +if (AVM_USB_CDC_PORT_DRIVER_ENABLED) + target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE + AVM_USB_CDC_PORT_DRIVER_ENABLED + USBD_VID=${AVM_USB_CDC_VID} + USBD_PID=${AVM_USB_CDC_PID} + "USBD_MANUFACTURER=\"${AVM_USB_CDC_MANUFACTURER}\"" + "USBD_PRODUCT=\"${AVM_USB_CDC_PRODUCT}\"" + "USBD_INTERFACE_NAME=\"${AVM_USB_CDC_INTERFACE_NAME}\"" + ) + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC tinyusb_device) + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,usb_cdcregister_port_driver") +endif() diff --git a/src/platforms/rp2/src/lib/rp2_sys.h b/src/platforms/rp2/src/lib/rp2_sys.h index a7cd4413bc..77b2918c88 100644 --- a/src/platforms/rp2/src/lib/rp2_sys.h +++ b/src/platforms/rp2/src/lib/rp2_sys.h @@ -82,6 +82,10 @@ struct RP2PlatformData #endif queue_t event_queue; +#if defined(AVM_USB_CDC_PORT_DRIVER_ENABLED) && !defined(AVM_NO_SMP) + mutex_t tinyusb_mutex; +#endif + #ifndef AVM_NO_SMP Mutex *entropy_mutex; #endif @@ -95,4 +99,18 @@ struct RP2PlatformData bool random_is_initialized; }; +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED +/** + * @brief Acquire the lock guarding TinyUSB device APIs. + * @details TinyUSB is not safe to call from multiple cores concurrently + * (see TinyUSB docs/reference/concurrency.rst). All non-callback uses of + * tud_xxx and tusb_init must be wrapped with this lock. Callbacks invoked + * by tud_task already run under the lock and must not relock. + * No-op when AVM_NO_SMP is defined. + * @end + */ +void sys_tinyusb_lock(GlobalContext *global); +void sys_tinyusb_unlock(GlobalContext *global); +#endif + #endif diff --git a/src/platforms/rp2/src/lib/sys.c b/src/platforms/rp2/src/lib/sys.c index a2cd038832..0894d0bf1d 100644 --- a/src/platforms/rp2/src/lib/sys.c +++ b/src/platforms/rp2/src/lib/sys.c @@ -37,6 +37,10 @@ #include #endif +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED +#include +#endif + #pragma GCC diagnostic pop #ifdef LIB_PICO_CYW43_ARCH @@ -83,6 +87,10 @@ void sys_init_platform(GlobalContext *glb) #endif queue_init(&platform->event_queue, sizeof(queue_t *), EVENT_QUEUE_LEN); +#if defined(AVM_USB_CDC_PORT_DRIVER_ENABLED) && !defined(AVM_NO_SMP) + mutex_init(&platform->tinyusb_mutex); +#endif + #ifdef HAVE_PLATFORM_ATOMIC_H atomic_init(); #endif @@ -186,6 +194,13 @@ bool sys_try_post_listener_event_from_isr(GlobalContext *glb, listener_event_t l void sys_poll_events(GlobalContext *glb, int timeout_ms) { struct RP2PlatformData *platform = glb->platform_data; +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + sys_tinyusb_lock(glb); + if (tud_inited()) { + tud_task(); + } + sys_tinyusb_unlock(glb); +#endif #ifndef AVM_NO_SMP if (timeout_ms != 0) { mutex_enter_blocking(&platform->event_poll_mutex); @@ -406,6 +421,28 @@ void sys_unregister_listener_from_event(GlobalContext *global, listener_event_t synclist_unlock(&global->listeners); } +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED +void sys_tinyusb_lock(GlobalContext *global) +{ +#ifndef AVM_NO_SMP + struct RP2PlatformData *platform = global->platform_data; + mutex_enter_blocking(&platform->tinyusb_mutex); +#else + (void) global; +#endif +} + +void sys_tinyusb_unlock(GlobalContext *global) +{ +#ifndef AVM_NO_SMP + struct RP2PlatformData *platform = global->platform_data; + mutex_exit(&platform->tinyusb_mutex); +#else + (void) global; +#endif +} +#endif + // TODO: enable mbedtls threading support by defining MBEDTLS_THREADING_ALT // and remove this function. int sys_mbedtls_entropy_func(void *entropy, unsigned char *buf, size_t size) diff --git a/src/platforms/rp2/src/lib/tusb_config.h b/src/platforms/rp2/src/lib/tusb_config.h new file mode 100644 index 0000000000..af89fbd7ee --- /dev/null +++ b/src/platforms/rp2/src/lib/tusb_config.h @@ -0,0 +1,56 @@ +/* + * 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 + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _ATOMVM_TUSB_CONFIG_H +#define _ATOMVM_TUSB_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined by the build system (Pico SDK sets this automatically) +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_PICO +#endif + +#ifndef CFG_TUSB_RHPORT0_MODE +#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +#define CFG_TUD_ENABLED 1 + +#define CFG_TUD_ENDPOINT0_SIZE 64 + +#define CFG_TUD_CDC 1 +#define CFG_TUD_MSC 0 +#define CFG_TUD_HID 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 0 + +#define CFG_TUD_CDC_RX_BUFSIZE 256 +#define CFG_TUD_CDC_TX_BUFSIZE 256 +#define CFG_TUD_CDC_EP_BUFSIZE 64 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/rp2/src/lib/usb_cdc_driver.c b/src/platforms/rp2/src/lib/usb_cdc_driver.c new file mode 100644 index 0000000000..89a11a2bee --- /dev/null +++ b/src/platforms/rp2/src/lib/usb_cdc_driver.c @@ -0,0 +1,563 @@ +/* + * 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 + */ + +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#include +#include +#pragma GCC diagnostic pop + +#include "atom.h" +#include "bif.h" +#include "context.h" +#include "debug.h" +#include "defaultatoms.h" +#include "globalcontext.h" +#include "interop.h" +#include "mailbox.h" +#include "module.h" +#include "port.h" +#include "scheduler.h" +#include "smp.h" +#include "term.h" +#include "utils.h" + +#include "rp2_sys.h" + +static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx); + +#define USB_CDC_BUF_SIZE 256 +#define NO_REF 0 +#define NO_READER term_invalid_term() + +// Abort a write after this many ms of no progress (host stalled or unplugged). +#define USB_CDC_WRITE_TIMEOUT_MS 500 + +struct USBCDCData +{ + queue_t rxqueue; + struct EventListener listener; + GlobalContext *global; + term reader_process_pid; + uint64_t reader_ref_ticks; + uint8_t itf; +#ifndef AVM_NO_SMP + Mutex *reader_lock; +#endif +}; + +enum usb_cdc_cmd +{ + USBCDCInvalidCmd = 0, + USBCDCReadCmd = 1, + USBCDCWriteCmd = 2, + USBCDCCloseCmd = 3, + USBCDCCancelCmd = 4 +}; + +static const AtomStringIntPair cmd_table[] = { + { ATOM_STR("\x4", "read"), USBCDCReadCmd }, + { ATOM_STR("\x5", "write"), USBCDCWriteCmd }, + { ATOM_STR("\x5", "close"), USBCDCCloseCmd }, + { ATOM_STR("\xB", "cancel_read"), USBCDCCancelCmd }, + SELECT_INT_DEFAULT(USBCDCInvalidCmd) +}; + +// Singleton driver data, protected by s_open_lock +static struct USBCDCData *s_cdc_data = NULL; + +#ifndef AVM_NO_SMP +static Mutex *s_open_lock = NULL; +#endif + +static void usb_cdc_driver_init(GlobalContext *global) +{ + UNUSED(global); +#ifndef AVM_NO_SMP + s_open_lock = smp_mutex_create(); + if (IS_NULL_PTR(s_open_lock)) { + fprintf(stderr, "usb_cdc: failed to create open lock\n"); + AVM_ABORT(); + } +#endif +} + +// TinyUSB CDC receive callback -- invoked by tud_task() in USB core task context. +// The TinyUSB lock is already held by the caller of tud_task(); do not relock. +void tud_cdc_rx_cb(uint8_t itf) +{ + if (s_cdc_data == NULL || itf != s_cdc_data->itf) { + return; + } + uint32_t available = tud_cdc_n_available(itf); + if (available > 0) { + sys_try_post_listener_event_from_isr(s_cdc_data->global, &s_cdc_data->rxqueue, &available); + } +} + +static EventListener *usb_cdc_listener_handler(GlobalContext *glb, EventListener *base_listener) +{ + struct USBCDCData *cdc_data = GET_LIST_ENTRY(base_listener, struct USBCDCData, listener); + + uint32_t event_val; + if (!queue_try_remove(&cdc_data->rxqueue, &event_val)) { + return base_listener; + } + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + if (cdc_data->reader_process_pid == term_invalid_term()) { + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + return base_listener; + } + + uint8_t buf[USB_CDC_BUF_SIZE]; + sys_tinyusb_lock(glb); + uint32_t rx_size = tud_cdc_n_read(cdc_data->itf, buf, sizeof(buf)); + sys_tinyusb_unlock(glb); + if (rx_size == 0) { + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + return base_listener; + } + + term reader_pid = cdc_data->reader_process_pid; + uint64_t reader_ref_ticks = cdc_data->reader_ref_ticks; + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + + int bin_size = term_binary_heap_size(rx_size); + + Heap heap; + if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + AVM_ABORT(); + } + + term bin = term_create_uninitialized_binary(rx_size, &heap, glb); + memcpy((void *) term_binary_data(bin), buf, rx_size); + + term ok_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(ok_tuple, 0, OK_ATOM); + term_put_tuple_element(ok_tuple, 1, bin); + + term ref = term_from_ref_ticks(reader_ref_ticks, &heap); + + term result_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, ok_tuple); + + int local_pid = term_to_local_process_id(reader_pid); + globalcontext_send_message(glb, local_pid, result_tuple); + + memory_destroy_heap(&heap, glb); + + return base_listener; +} + +static Context *usb_cdc_driver_create_port(GlobalContext *global, term opts) +{ + UNUSED(opts); + + SMP_MUTEX_LOCK(s_open_lock); + if (s_cdc_data != NULL) { + SMP_MUTEX_UNLOCK(s_open_lock); + fprintf(stderr, "usb_cdc: CDC interface already in use\n"); + return NULL; + } + + Context *ctx = context_new(global); + + struct USBCDCData *cdc_data = malloc(sizeof(struct USBCDCData)); + if (IS_NULL_PTR(cdc_data)) { + SMP_MUTEX_UNLOCK(s_open_lock); + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + AVM_ABORT(); + } + + queue_init(&cdc_data->rxqueue, sizeof(uint32_t), EVENT_QUEUE_LEN); + cdc_data->listener.handler = usb_cdc_listener_handler; + cdc_data->listener.queue = &cdc_data->rxqueue; + cdc_data->global = global; + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + cdc_data->itf = 0; + +#ifndef AVM_NO_SMP + cdc_data->reader_lock = smp_mutex_create(); +#endif + + // TinyUSB should already be initialized by Pico SDK when + // pico_enable_stdio_usb is set. For distribution use, stdio_usb + // should be disabled and this driver takes over the CDC interface. + // Ensure TinyUSB device stack is running + sys_tinyusb_lock(global); + if (!tud_inited()) { + tusb_init(); + } + sys_tinyusb_unlock(global); + + sys_register_listener(global, &cdc_data->listener); + + s_cdc_data = cdc_data; + SMP_MUTEX_UNLOCK(s_open_lock); + + ctx->native_handler = usb_cdc_driver_consume_mailbox; + ctx->platform_data = cdc_data; + + return ctx; +} + +static void usb_cdc_driver_do_read(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term pid = gen_message.pid; + term ref = gen_message.ref; + uint64_t ref_ticks = term_to_ref_ticks(ref); + + int local_pid = term_to_local_process_id(pid); + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + if (cdc_data->reader_process_pid != term_invalid_term()) { + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + term ealready = globalcontext_make_atom(glb, ATOM_STR("\x8", "ealready")); + term error_tuple = port_create_error_tuple(ctx, ealready); + port_send_reply(ctx, pid, ref, error_tuple); + return; + } + + // Try immediate read + uint8_t buf[USB_CDC_BUF_SIZE]; + sys_tinyusb_lock(glb); + uint32_t rx_size = tud_cdc_n_read(cdc_data->itf, buf, sizeof(buf)); + sys_tinyusb_unlock(glb); + if (rx_size > 0) { + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + int bin_size = term_binary_heap_size(rx_size); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, bin_size + TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate return value\n"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + term bin = term_create_uninitialized_binary(rx_size, &ctx->heap, glb); + memcpy((void *) term_binary_data(bin), buf, rx_size); + + term ok_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ok_tuple, 0, OK_ATOM); + term_put_tuple_element(ok_tuple, 1, bin); + + port_send_reply(ctx, pid, ref, ok_tuple); + } else { + cdc_data->reader_process_pid = pid; + cdc_data->reader_ref_ticks = ref_ticks; + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + } +} + +static void usb_cdc_driver_do_cancel_read(Context *ctx, GenMessage gen_message) +{ + struct USBCDCData *cdc_data = ctx->platform_data; + term req = gen_message.req; + term pid = gen_message.pid; + term ref = gen_message.ref; + + // Two forms: bare `cancel_read` clears unconditionally (legacy), and + // `{cancel_read, ReadRef}` only clears if ReadRef matches the stored + // reader. The tuple form is what avoids the race where a cancel + // wipes a freshly-installed reader belonging to a different request. + bool clear_unconditionally = term_is_atom(req); + uint64_t target_ref_ticks = 0; + if (!clear_unconditionally) { + target_ref_ticks = term_to_ref_ticks(term_get_tuple_element(req, 1)); + } + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + if (clear_unconditionally || cdc_data->reader_ref_ticks == target_ref_ticks) { + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + } + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate return value\n"); + globalcontext_send_message(ctx->global, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void usb_cdc_send_error_reply(Context *ctx, term pid, term ref, AtomString reason_atom_str) +{ + GlobalContext *glb = ctx->global; + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + term reason = globalcontext_make_atom(glb, reason_atom_str); + term error_tuple = port_create_error_tuple(ctx, reason); + port_send_reply(ctx, pid, ref, error_tuple); +} + +static void usb_cdc_driver_do_write(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term msg = gen_message.req; + term pid = gen_message.pid; + term ref = gen_message.ref; + + term data = term_get_tuple_element(msg, 1); + int local_pid = term_to_local_process_id(pid); + + size_t buffer_size; + switch (interop_iolist_size(data, &buffer_size)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + case InteropBadArg: + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "badarg")); + return; + } + void *buffer = malloc(buffer_size); + if (IS_NULL_PTR(buffer)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + switch (interop_write_iolist(data, buffer)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + free(buffer); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + case InteropBadArg: + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "badarg")); + return; + } + + /* + * If the host hasn't opened the CDC interface yet, silently drop + * the data and report success. This matches typical CDC ACM + * behavior on Linux/macOS (writes to a not-yet-opened tty are + * absorbed) and is what serial_dist needs at boot: the link + * manager blasts SYNC bytes hoping a peer is there, and a not- + * yet-connected host should be a no-op rather than crash. + */ + sys_tinyusb_lock(glb); + bool initially_connected = tud_cdc_n_connected(cdc_data->itf); + sys_tinyusb_unlock(glb); + if (!initially_connected) { + free(buffer); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); + return; + } + + size_t written = 0; + uint32_t deadline = to_ms_since_boot(get_absolute_time()) + USB_CDC_WRITE_TIMEOUT_MS; + bool host_gone = false; + bool timed_out = false; + while (written < buffer_size) { + sys_tinyusb_lock(glb); + bool connected = tud_cdc_n_connected(cdc_data->itf); + uint32_t chunk = 0; + if (connected) { + chunk = tud_cdc_n_write(cdc_data->itf, + (const uint8_t *) buffer + written, + buffer_size - written); + tud_cdc_n_write_flush(cdc_data->itf); + if (chunk == 0) { + tud_task(); + } + } + sys_tinyusb_unlock(glb); + + if (!connected) { + host_gone = true; + break; + } + if (chunk > 0) { + written += chunk; + // Reset the deadline on any progress so a slow but moving + // host can drain a buffer larger than the FIFO without tripping. + deadline = to_ms_since_boot(get_absolute_time()) + USB_CDC_WRITE_TIMEOUT_MS; + } else { + if ((int32_t) (to_ms_since_boot(get_absolute_time()) - deadline) >= 0) { + timed_out = true; + break; + } + sleep_ms(1); + } + } + + free(buffer); + + if (host_gone) { + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "closed")); + return; + } + if (timed_out) { + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x7", "timeout")); + return; + } + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term pid = gen_message.pid; + term ref = gen_message.ref; + + SMP_MUTEX_LOCK(s_open_lock); + sys_tinyusb_lock(glb); + s_cdc_data = NULL; + sys_tinyusb_unlock(glb); + SMP_MUTEX_UNLOCK(s_open_lock); + + sys_unregister_listener(glb, &cdc_data->listener); + queue_free(&cdc_data->rxqueue); + + SMP_MUTEX_LOCK(cdc_data->reader_lock); + term pending_reader_pid = cdc_data->reader_process_pid; + uint64_t pending_reader_ref_ticks = cdc_data->reader_ref_ticks; + cdc_data->reader_process_pid = term_invalid_term(); + SMP_MUTEX_UNLOCK(cdc_data->reader_lock); + + if (pending_reader_pid != term_invalid_term()) { + BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + term error_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); + term reader_ref = term_from_ref_ticks(pending_reader_ref_ticks, &heap); + term reply = term_alloc_tuple(2, &heap); + term_put_tuple_element(reply, 0, reader_ref); + term_put_tuple_element(reply, 1, error_tuple); + globalcontext_send_message(glb, term_to_local_process_id(pending_reader_pid), reply); + END_WITH_STACK_HEAP(heap, glb) + } + +#ifndef AVM_NO_SMP + smp_mutex_destroy(cdc_data->reader_lock); +#endif + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + } else { + port_send_reply(ctx, pid, ref, OK_ATOM); + } + + free(cdc_data); + ctx->platform_data = NULL; +} + +static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) +{ + GlobalContext *glb = ctx->global; + bool is_closed = false; + while (mailbox_has_next(&ctx->mailbox)) { + Message *message = mailbox_first(&ctx->mailbox); + term msg = message->message; + GenMessage gen_message; + if (UNLIKELY(port_parse_gen_message(msg, &gen_message) != GenCallMessage)) { + fprintf(stderr, "usb_cdc: Received invalid message.\n"); + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + continue; + } + + uint64_t ref_ticks = term_to_ref_ticks(gen_message.ref); + int local_pid = term_to_local_process_id(gen_message.pid); + + if (is_closed) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + } else { + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, NOPROC_ATOM); + + term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + + term result_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, error_tuple); + + globalcontext_send_message(glb, local_pid, result_tuple); + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + continue; + } + + term req = gen_message.req; + term cmd_term = term_is_atom(req) ? req : term_get_tuple_element(req, 0); + + enum usb_cdc_cmd cmd = interop_atom_term_select_int(cmd_table, cmd_term, ctx->global); + switch (cmd) { + case USBCDCReadCmd: + usb_cdc_driver_do_read(ctx, gen_message); + break; + case USBCDCWriteCmd: + usb_cdc_driver_do_write(ctx, gen_message); + break; + case USBCDCCloseCmd: + usb_cdc_driver_do_close(ctx, gen_message); + is_closed = true; + break; + case USBCDCCancelCmd: + usb_cdc_driver_do_cancel_read(ctx, gen_message); + break; + default: + fprintf(stderr, "usb_cdc: unrecognized command\n"); + usb_cdc_send_error_reply(ctx, gen_message.pid, gen_message.ref, ATOM_STR("\xF", "unknown_command")); + break; + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + } + return is_closed ? NativeTerminate : NativeContinue; +} + +REGISTER_PORT_DRIVER(usb_cdc, usb_cdc_driver_init, NULL, usb_cdc_driver_create_port) + +#endif diff --git a/src/platforms/rp2/src/lib/usb_descriptors.c b/src/platforms/rp2/src/lib/usb_descriptors.c new file mode 100644 index 0000000000..21c57dfb10 --- /dev/null +++ b/src/platforms/rp2/src/lib/usb_descriptors.c @@ -0,0 +1,126 @@ +/* + * 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 + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + +#include +#include + +#ifndef USBD_VID +#define USBD_VID 0x2E8A +#endif +#ifndef USBD_PID +#define USBD_PID 0x0009 +#endif +#ifndef USBD_MANUFACTURER +#define USBD_MANUFACTURER "AtomVM" +#endif +#ifndef USBD_PRODUCT +#define USBD_PRODUCT "AtomVM CDC" +#endif +#ifndef USBD_INTERFACE_NAME +#define USBD_INTERFACE_NAME "AtomVM CDC ACM" +#endif + +#define USBD_ITF_CDC 0 +#define USBD_ITF_MAX 2 + +#define USBD_CDC_EP_CMD 0x81 +#define USBD_CDC_EP_OUT 0x02 +#define USBD_CDC_EP_IN 0x82 +#define USBD_CDC_CMD_MAX_SIZE 8 +#define USBD_CDC_IN_OUT_MAX_SIZE 64 + +#define USBD_STR_LANG 0 +#define USBD_STR_MANUF 1 +#define USBD_STR_PRODUCT 2 +#define USBD_STR_SERIAL 3 +#define USBD_STR_CDC 4 + +#define USBD_DESC_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN) + +static const tusb_desc_device_t usbd_desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = USBD_VID, + .idProduct = USBD_PID, + .bcdDevice = 0x0100, + .iManufacturer = USBD_STR_MANUF, + .iProduct = USBD_STR_PRODUCT, + .iSerialNumber = USBD_STR_SERIAL, + .bNumConfigurations = 1, +}; + +static const uint8_t usbd_desc_cfg[USBD_DESC_LEN] = { + TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_MAX, USBD_STR_LANG, USBD_DESC_LEN, 0, 250), + TUD_CDC_DESCRIPTOR(USBD_ITF_CDC, USBD_STR_CDC, USBD_CDC_EP_CMD, + USBD_CDC_CMD_MAX_SIZE, USBD_CDC_EP_OUT, USBD_CDC_EP_IN, USBD_CDC_IN_OUT_MAX_SIZE), +}; + +static char usbd_serial_str[PICO_UNIQUE_BOARD_ID_SIZE_BYTES * 2 + 1]; + +static const char *const usbd_desc_str[] = { + [USBD_STR_MANUF] = USBD_MANUFACTURER, + [USBD_STR_PRODUCT] = USBD_PRODUCT, + [USBD_STR_SERIAL] = usbd_serial_str, + [USBD_STR_CDC] = USBD_INTERFACE_NAME, +}; + +const uint8_t *tud_descriptor_device_cb(void) +{ + return (const uint8_t *) &usbd_desc_device; +} + +const uint8_t *tud_descriptor_configuration_cb(uint8_t index) +{ + (void) index; + return usbd_desc_cfg; +} + +const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void) langid; + static uint16_t desc_str[32]; + + if (!usbd_serial_str[0]) { + pico_get_unique_board_id_string(usbd_serial_str, sizeof(usbd_serial_str)); + } + + uint8_t len; + if (index == USBD_STR_LANG) { + desc_str[1] = 0x0409; + len = 1; + } else { + if (index >= sizeof(usbd_desc_str) / sizeof(usbd_desc_str[0])) { + return NULL; + } + const char *str = usbd_desc_str[index]; + if (!str) { + return NULL; + } + for (len = 0; len < 31 && str[len]; ++len) { + desc_str[1 + len] = (uint16_t) str[len]; + } + } + + desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * len + 2)); + return desc_str; +} + +#endif diff --git a/src/platforms/stm32/CMakeLists.txt b/src/platforms/stm32/CMakeLists.txt index d46c682c26..d56861915d 100644 --- a/src/platforms/stm32/CMakeLists.txt +++ b/src/platforms/stm32/CMakeLists.txt @@ -144,6 +144,16 @@ include(cmake/atomvm_dev_config.cmake) # Include SDK fetch (CMSIS + HAL/LL) include(cmake/stm32_sdk.cmake) +option(AVM_USB_CDC_PORT_DRIVER_ENABLED "Enable USB CDC port driver" OFF) +set(AVM_USB_CDC_VID "0xCAFE" CACHE STRING "USB CDC vendor ID (default: TinyUSB example placeholder; override for distribution)") +set(AVM_USB_CDC_PID "0x4001" CACHE STRING "USB CDC product ID (default: TinyUSB example placeholder; override for distribution)") +set(AVM_USB_CDC_MANUFACTURER "AtomVM" CACHE STRING "USB CDC iManufacturer string descriptor") +set(AVM_USB_CDC_PRODUCT "AtomVM CDC" CACHE STRING "USB CDC iProduct string descriptor") +set(AVM_USB_CDC_INTERFACE_NAME "AtomVM CDC ACM" CACHE STRING "USB CDC interface string descriptor") +if (AVM_USB_CDC_PORT_DRIVER_ENABLED) + include(cmake/stm32_tinyusb.cmake) +endif() + # Include additional compilation flags (must come after dev_config sets CMAKE_FLASH_SIZE) include(cmake/compile-flags.cmake) diff --git a/src/platforms/stm32/cmake/stm32_tinyusb.cmake b/src/platforms/stm32/cmake/stm32_tinyusb.cmake new file mode 100644 index 0000000000..5afedeed27 --- /dev/null +++ b/src/platforms/stm32/cmake/stm32_tinyusb.cmake @@ -0,0 +1,95 @@ +# +# 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 +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +# Fetch TinyUSB and create an interface library for STM32 USB CDC support. +# Only the device-side CDC class on the synopsys/dwc2 controller is wired up. +# Selects MCU define + DCD port based on STM32_FAMILY_SHORT. + +include(FetchContent) + +if (DEFINED ENV{TINYUSB_PATH} AND (NOT FETCHCONTENT_SOURCE_DIR_TINYUSB)) + set(FETCHCONTENT_SOURCE_DIR_TINYUSB $ENV{TINYUSB_PATH}) + message("Using TINYUSB_PATH from environment ('${FETCHCONTENT_SOURCE_DIR_TINYUSB}')") +endif() + +FetchContent_Declare( + tinyusb + GIT_REPOSITORY https://github.com/hathach/tinyusb.git + GIT_TAG 0.18.0 + GIT_SHALLOW TRUE +) +FetchContent_GetProperties(tinyusb) +if (NOT tinyusb_POPULATED) + message("Downloading TinyUSB") +endif() +FetchContent_MakeAvailable(tinyusb) + +set(TINYUSB_SRC_DIR "${tinyusb_SOURCE_DIR}/src") + +# Map STM32 family -> TinyUSB MCU define + DCD source files. +# H7/F2/F4/F7: synopsys dwc2 OTG controller. +# H5: STM32 USB_DRD_FS device controller (the "stm32_fsdev" port in TinyUSB). +set(_DWC2_SOURCES + "${TINYUSB_SRC_DIR}/portable/synopsys/dwc2/dcd_dwc2.c" + "${TINYUSB_SRC_DIR}/portable/synopsys/dwc2/dwc2_common.c" +) +set(_FSDEV_SOURCES + "${TINYUSB_SRC_DIR}/portable/st/stm32_fsdev/dcd_stm32_fsdev.c" +) + +if (STM32_FAMILY_SHORT STREQUAL "h7") + set(STM32_TINYUSB_MCU "OPT_MCU_STM32H7") + set(STM32_TINYUSB_DCD_SOURCES ${_DWC2_SOURCES}) +elseif (STM32_FAMILY_SHORT STREQUAL "h5") + set(STM32_TINYUSB_MCU "OPT_MCU_STM32H5") + set(STM32_TINYUSB_DCD_SOURCES ${_FSDEV_SOURCES}) +else() + message(FATAL_ERROR "AVM_USB_CDC_PORT_DRIVER_ENABLED: TinyUSB MCU mapping not configured for STM32 family ${STM32_FAMILY_SHORT}") +endif() + +# Core TinyUSB sources required for device CDC, plus the family-specific DCD. +set(TINYUSB_SOURCES + "${TINYUSB_SRC_DIR}/tusb.c" + "${TINYUSB_SRC_DIR}/common/tusb_fifo.c" + "${TINYUSB_SRC_DIR}/device/usbd.c" + "${TINYUSB_SRC_DIR}/device/usbd_control.c" + "${TINYUSB_SRC_DIR}/class/cdc/cdc_device.c" + ${STM32_TINYUSB_DCD_SOURCES} +) + +add_library(tinyusb_device STATIC ${TINYUSB_SOURCES}) +target_include_directories(tinyusb_device PUBLIC + "${TINYUSB_SRC_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/src/lib" # for tusb_config.h +) +target_compile_definitions(tinyusb_device PUBLIC + "CFG_TUSB_MCU=${STM32_TINYUSB_MCU}" + "${STM32_HAL_DEVICE}" +) +target_include_directories(tinyusb_device SYSTEM PUBLIC + ${CMSIS_CORE_INCLUDE} + ${CMSIS_DEVICE_INCLUDE} + ${HAL_DRIVER_INCLUDE} + "${CMAKE_BINARY_DIR}/generated" +) +# TinyUSB has plenty of casts / fall-through that aren't worth hardening here. +target_compile_options(tinyusb_device PRIVATE + -Wno-unused-parameter + -Wno-implicit-fallthrough + -Wno-cast-align + -Wno-sign-compare +) + +message(STATUS "TinyUSB MCU : ${STM32_TINYUSB_MCU}") +message(STATUS "TinyUSB DCD : ${STM32_TINYUSB_DCD_PORT}") diff --git a/src/platforms/stm32/src/lib/CMakeLists.txt b/src/platforms/stm32/src/lib/CMakeLists.txt index 98cf713f85..b8b7f84237 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -55,6 +55,7 @@ set(SOURCE_FILES gpio_driver.c i2c_driver.c spi_driver.c + uart_driver.c jit_stream_flash.c platform_nifs.c sys.c @@ -69,6 +70,10 @@ if (STM32_HAS_RNG) otp_crypto_platform.c) endif() +if (AVM_USB_CDC_PORT_DRIVER_ENABLED) + list(APPEND SOURCE_FILES usb_cdc_driver.c usb_descriptors.c usb_irq_handlers.c usb_hw_init.c) +endif() + set( PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR} @@ -100,3 +105,16 @@ target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} SYSTEM PUBLIC target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "${CMAKE_BINARY_DIR}/generated" ) + +if (AVM_USB_CDC_PORT_DRIVER_ENABLED) + target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE + AVM_USB_CDC_PORT_DRIVER_ENABLED + USBD_VID=${AVM_USB_CDC_VID} + USBD_PID=${AVM_USB_CDC_PID} + "USBD_MANUFACTURER=\"${AVM_USB_CDC_MANUFACTURER}\"" + "USBD_PRODUCT=\"${AVM_USB_CDC_PRODUCT}\"" + "USBD_INTERFACE_NAME=\"${AVM_USB_CDC_INTERFACE_NAME}\"" + ) + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC tinyusb_device) + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,usb_cdcregister_port_driver") +endif() diff --git a/src/platforms/stm32/src/lib/sys.c b/src/platforms/stm32/src/lib/sys.c index 030b339c7b..5f60c6eeb0 100644 --- a/src/platforms/stm32/src/lib/sys.c +++ b/src/platforms/stm32/src/lib/sys.c @@ -47,6 +47,11 @@ #include "stm32_device_atoms.h" #include "stm_sys.h" +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED +#include "usb_cdc_driver.h" +#include +#endif + #define TAG "sys" #define RESERVE_STACK_SIZE 4096U @@ -260,6 +265,11 @@ void sys_poll_events(GlobalContext *glb, int timeout_ms) { UNUSED(glb); UNUSED(timeout_ms); +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + if (tud_inited()) { + usb_cdc_driver_poll(); + } +#endif } void sys_listener_destroy(struct ListHead *item) diff --git a/src/platforms/stm32/src/lib/tusb_config.h b/src/platforms/stm32/src/lib/tusb_config.h new file mode 100644 index 0000000000..13bb0f8a3c --- /dev/null +++ b/src/platforms/stm32/src/lib/tusb_config.h @@ -0,0 +1,63 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _ATOMVM_TUSB_CONFIG_STM32_H +#define _ATOMVM_TUSB_CONFIG_STM32_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined by the build system +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_RHPORT0_MODE +#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +// On STM32H7/H5/F4/F7 with dwc2: place USB buffers in DMA-capable RAM. +// Default to no special placement; user can override with -DCFG_TUSB_MEM_SECTION +// in their build if their linker script requires it. +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4))) +#endif + +#define CFG_TUD_ENABLED 1 + +// dwc2 OTG_FS endpoint 0 size +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +#define CFG_TUD_CDC 1 +#define CFG_TUD_MSC 0 +#define CFG_TUD_HID 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 0 + +#define CFG_TUD_CDC_RX_BUFSIZE 256 +#define CFG_TUD_CDC_TX_BUFSIZE 256 +#define CFG_TUD_CDC_EP_BUFSIZE 64 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/stm32/src/lib/uart_driver.c b/src/platforms/stm32/src/lib/uart_driver.c new file mode 100644 index 0000000000..41f68c8e0c --- /dev/null +++ b/src/platforms/stm32/src/lib/uart_driver.c @@ -0,0 +1,892 @@ +/* + * 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 "stm32_hal_platform.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#include "avm_log.h" +#include "stm_sys.h" + +#define TAG "uart_driver" + +static ErlNifResourceType *uart_resource_type; + +struct UARTResource +{ + UART_HandleTypeDef handle; + bool is_half_duplex; +}; + +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 bool get_uart_resource(Context *ctx, term resource_term, struct UARTResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, uart_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct UARTResource *) rsrc_obj_ptr; + return true; +} + +static bool get_timeout_ms(term timeout_term, uint32_t *out) +{ + if (term_is_atom(timeout_term)) { + if (timeout_term == INFINITY_ATOM) { + *out = HAL_MAX_DELAY; + return true; + } + return false; + } + if (!term_is_integer(timeout_term)) { + return false; + } + avm_int_t val = term_to_int(timeout_term); + if (val < 0) { + return false; + } + *out = (uint32_t) val; + return true; +} + +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 USART_TypeDef *peripheral_to_instance(int peripheral) +{ + switch (peripheral) { +#ifdef USART1 + case 1: + return USART1; +#endif +#ifdef USART2 + case 2: + return USART2; +#endif +#ifdef USART3 + case 3: + return USART3; +#endif +#ifdef UART4 + case 4: + return UART4; +#endif +#ifdef UART5 + case 5: + return UART5; +#endif +#ifdef USART6 + case 6: + return USART6; +#endif +#ifdef UART7 + case 7: + return UART7; +#endif +#ifdef UART8 + case 8: + return UART8; +#endif +#ifdef LPUART1 + case 9: + return LPUART1; +#endif + default: + return NULL; + } +} + +static void enable_uart_clock(int peripheral) +{ + switch (peripheral) { +#ifdef USART1 + case 1: + __HAL_RCC_USART1_CLK_ENABLE(); + break; +#endif +#ifdef USART2 + case 2: + __HAL_RCC_USART2_CLK_ENABLE(); + break; +#endif +#ifdef USART3 + case 3: + __HAL_RCC_USART3_CLK_ENABLE(); + break; +#endif +#ifdef UART4 + case 4: + __HAL_RCC_UART4_CLK_ENABLE(); + break; +#endif +#ifdef UART5 + case 5: + __HAL_RCC_UART5_CLK_ENABLE(); + break; +#endif +#ifdef USART6 + case 6: + __HAL_RCC_USART6_CLK_ENABLE(); + break; +#endif +#ifdef UART7 + case 7: + __HAL_RCC_UART7_CLK_ENABLE(); + break; +#endif +#ifdef UART8 + case 8: + __HAL_RCC_UART8_CLK_ENABLE(); + break; +#endif +#ifdef LPUART1 + case 9: + __HAL_RCC_LPUART1_CLK_ENABLE(); + break; +#endif + default: + break; + } +} + +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_uart_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + + GlobalContext *glb = ctx->global; + + static const char *const peripheral_str = ATOM_STR("\xA", "peripheral"); + static const char *const tx_str = ATOM_STR("\x2", "tx"); + static const char *const rx_str = ATOM_STR("\x2", "rx"); + static const char *const speed_str = ATOM_STR("\x5", "speed"); + static const char *const data_bits_str = ATOM_STR("\x9", "data_bits"); + static const char *const stop_bits_str = ATOM_STR("\x9", "stop_bits"); + static const char *const parity_str = ATOM_STR("\x6", "parity"); + static const char *const af_str = ATOM_STR("\x2", "af"); + + term peripheral_term = interop_kv_get_value_default(opts, peripheral_str, term_from_int(1), glb); + term tx_term = interop_kv_get_value(opts, tx_str, glb); + term rx_term = interop_kv_get_value(opts, rx_str, glb); + term speed_term = interop_kv_get_value_default(opts, speed_str, term_from_int(115200), glb); + term data_bits_term = interop_kv_get_value_default(opts, data_bits_str, term_from_int(8), glb); + term stop_bits_term = interop_kv_get_value_default(opts, stop_bits_str, term_from_int(1), glb); + term parity_term = interop_kv_get_value_default(opts, parity_str, term_from_int(0), glb); + term af_term = interop_kv_get_value_default(opts, af_str, term_from_int(7), glb); + + if (term_is_invalid_term(tx_term) || term_is_invalid_term(rx_term)) { + AVM_LOGE(TAG, "tx and rx pins are required"); + RAISE_ERROR(BADARG_ATOM); + } + + VALIDATE_VALUE(peripheral_term, term_is_integer); + VALIDATE_VALUE(speed_term, term_is_integer); + VALIDATE_VALUE(data_bits_term, term_is_integer); + VALIDATE_VALUE(stop_bits_term, term_is_integer); + VALIDATE_VALUE(parity_term, term_is_integer); + VALIDATE_VALUE(af_term, term_is_integer); + + int peripheral = term_to_int(peripheral_term); + uint32_t speed = (uint32_t) term_to_int(speed_term); + int data_bits = term_to_int(data_bits_term); + int stop_bits = term_to_int(stop_bits_term); + int parity = term_to_int(parity_term); + uint32_t af = (uint32_t) term_to_int(af_term); + + USART_TypeDef *instance = peripheral_to_instance(peripheral); + if (IS_NULL_PTR(instance)) { + AVM_LOGE(TAG, "Invalid UART peripheral: %d", peripheral); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *tx_port; + uint16_t tx_pin; + if (!parse_pin(glb, tx_term, &tx_port, &tx_pin)) { + AVM_LOGE(TAG, "Invalid TX pin"); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *rx_port; + uint16_t rx_pin; + if (!parse_pin(glb, rx_term, &rx_port, &rx_pin)) { + AVM_LOGE(TAG, "Invalid RX pin"); + RAISE_ERROR(BADARG_ATOM); + } + + enable_uart_clock(peripheral); + + GPIO_InitTypeDef gpio_init = { + .Mode = GPIO_MODE_AF_PP, + .Pull = GPIO_PULLUP, + .Speed = GPIO_SPEED_FREQ_HIGH, + .Alternate = af, + }; + gpio_init.Pin = tx_pin; + HAL_GPIO_Init(tx_port, &gpio_init); + gpio_init.Pin = rx_pin; + gpio_init.Mode = GPIO_MODE_AF_PP; + HAL_GPIO_Init(rx_port, &gpio_init); + + struct UARTResource *rsrc_obj = enif_alloc_resource(uart_resource_type, sizeof(struct UARTResource)); + if (IS_NULL_PTR(rsrc_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + memset(&rsrc_obj->handle, 0, sizeof(UART_HandleTypeDef)); + rsrc_obj->handle.Instance = instance; + rsrc_obj->is_half_duplex = false; + + rsrc_obj->handle.Init.BaudRate = speed; + + switch (data_bits) { +#ifdef UART_WORDLENGTH_7B + case 7: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_7B; + break; +#endif + case 8: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + case 9: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_9B; + break; + default: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + } + + switch (stop_bits) { + case 1: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + case 2: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_2; + break; + default: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + } + + switch (parity) { + case 0: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + case 1: + rsrc_obj->handle.Init.Parity = UART_PARITY_ODD; + break; + case 2: + rsrc_obj->handle.Init.Parity = UART_PARITY_EVEN; + break; + default: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + } + + rsrc_obj->handle.Init.Mode = UART_MODE_TX_RX; + rsrc_obj->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; + rsrc_obj->handle.Init.OverSampling = UART_OVERSAMPLING_16; + + HAL_StatusTypeDef status = HAL_UART_Init(&rsrc_obj->handle); + if (status != HAL_OK) { + enif_release_resource(rsrc_obj); + AVM_LOGE(TAG, "HAL_UART_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", "uart_init"))); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + HAL_UART_DeInit(&rsrc_obj->handle); + enif_release_resource(rsrc_obj); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term obj = term_from_resource(rsrc_obj, &ctx->heap); + 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_uart_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + return OK_ATOM; + } + HAL_UART_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + rsrc_obj->is_half_duplex = false; + return OK_ATOM; +} + +static term nif_uart_write(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *data = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + uint32_t timeout_ms; + if (UNLIKELY(!get_timeout_ms(argv[2], &timeout_ms))) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(len > UINT16_MAX)) { + RAISE_ERROR(BADARG_ATOM); + } + + HAL_StatusTypeDef status = HAL_UART_Transmit(&rsrc_obj->handle, (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_uart_read(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + avm_int_t count = term_to_int(argv[1]); + uint32_t timeout_ms; + if (UNLIKELY(!get_timeout_ms(argv[2], &timeout_ms))) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(count < 0 || count > UINT16_MAX)) { + 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_UART_Receive(&rsrc_obj->handle, 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_uart_abort(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + HAL_StatusTypeDef status = HAL_UART_Abort(&rsrc_obj->handle); + 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 OK_ATOM; +} + +static term nif_uart_get_state(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + + HAL_UART_StateTypeDef state = HAL_UART_GetState(&rsrc_obj->handle); + + static const char *const ready_str = ATOM_STR("\x5", "ready"); + static const char *const busy_str = ATOM_STR("\x4", "busy"); + static const char *const busy_tx_str = ATOM_STR("\x7", "busy_tx"); + static const char *const busy_rx_str = ATOM_STR("\x7", "busy_rx"); + static const char *const busy_tx_rx_str = ATOM_STR("\xA", "busy_tx_rx"); + static const char *const reset_str = ATOM_STR("\x5", "reset"); + + const char *state_str; + switch (state) { + case HAL_UART_STATE_READY: + state_str = ready_str; + break; + case HAL_UART_STATE_BUSY: + state_str = busy_str; + break; + case HAL_UART_STATE_BUSY_TX: + state_str = busy_tx_str; + break; + case HAL_UART_STATE_BUSY_RX: + state_str = busy_rx_str; + break; + case HAL_UART_STATE_BUSY_TX_RX: + state_str = busy_tx_rx_str; + break; + case HAL_UART_STATE_ERROR: + return ERROR_ATOM; + case HAL_UART_STATE_TIMEOUT: + return TIMEOUT_ATOM; + default: + state_str = reset_str; + break; + } + return globalcontext_make_atom(ctx->global, state_str); +} + +static term nif_uart_get_error(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t err = HAL_UART_GetError(&rsrc_obj->handle); + return term_from_int((int) err); +} + +static term nif_uart_halfduplex_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + + GlobalContext *glb = ctx->global; + + static const char *const peripheral_str = ATOM_STR("\xA", "peripheral"); + static const char *const tx_str = ATOM_STR("\x2", "tx"); + static const char *const speed_str = ATOM_STR("\x5", "speed"); + static const char *const data_bits_str = ATOM_STR("\x9", "data_bits"); + static const char *const stop_bits_str = ATOM_STR("\x9", "stop_bits"); + static const char *const parity_str = ATOM_STR("\x6", "parity"); + static const char *const af_str = ATOM_STR("\x2", "af"); + + term peripheral_term = interop_kv_get_value_default(opts, peripheral_str, term_from_int(1), glb); + term tx_term = interop_kv_get_value(opts, tx_str, glb); + term speed_term = interop_kv_get_value_default(opts, speed_str, term_from_int(115200), glb); + term data_bits_term = interop_kv_get_value_default(opts, data_bits_str, term_from_int(8), glb); + term stop_bits_term = interop_kv_get_value_default(opts, stop_bits_str, term_from_int(1), glb); + term parity_term = interop_kv_get_value_default(opts, parity_str, term_from_int(0), glb); + term af_term = interop_kv_get_value_default(opts, af_str, term_from_int(7), glb); + + if (term_is_invalid_term(tx_term)) { + AVM_LOGE(TAG, "tx pin is required for half-duplex"); + RAISE_ERROR(BADARG_ATOM); + } + + VALIDATE_VALUE(peripheral_term, term_is_integer); + VALIDATE_VALUE(speed_term, term_is_integer); + VALIDATE_VALUE(data_bits_term, term_is_integer); + VALIDATE_VALUE(stop_bits_term, term_is_integer); + VALIDATE_VALUE(parity_term, term_is_integer); + VALIDATE_VALUE(af_term, term_is_integer); + + int peripheral = term_to_int(peripheral_term); + uint32_t speed = (uint32_t) term_to_int(speed_term); + int data_bits = term_to_int(data_bits_term); + int stop_bits = term_to_int(stop_bits_term); + int parity = term_to_int(parity_term); + uint32_t af = (uint32_t) term_to_int(af_term); + + USART_TypeDef *instance = peripheral_to_instance(peripheral); + if (IS_NULL_PTR(instance)) { + AVM_LOGE(TAG, "Invalid UART peripheral: %d", peripheral); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *tx_port; + uint16_t tx_pin; + if (!parse_pin(glb, tx_term, &tx_port, &tx_pin)) { + AVM_LOGE(TAG, "Invalid TX pin"); + RAISE_ERROR(BADARG_ATOM); + } + + enable_uart_clock(peripheral); + + GPIO_InitTypeDef gpio_init = { + .Mode = GPIO_MODE_AF_OD, + .Pull = GPIO_NOPULL, + .Speed = GPIO_SPEED_FREQ_HIGH, + .Alternate = af, + }; + gpio_init.Pin = tx_pin; + HAL_GPIO_Init(tx_port, &gpio_init); + + struct UARTResource *rsrc_obj = enif_alloc_resource(uart_resource_type, sizeof(struct UARTResource)); + if (IS_NULL_PTR(rsrc_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + memset(&rsrc_obj->handle, 0, sizeof(UART_HandleTypeDef)); + rsrc_obj->handle.Instance = instance; + rsrc_obj->is_half_duplex = true; + + rsrc_obj->handle.Init.BaudRate = speed; + + switch (data_bits) { +#ifdef UART_WORDLENGTH_7B + case 7: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_7B; + break; +#endif + case 8: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + case 9: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_9B; + break; + default: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + } + + switch (stop_bits) { + case 1: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + case 2: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_2; + break; + default: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + } + + switch (parity) { + case 0: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + case 1: + rsrc_obj->handle.Init.Parity = UART_PARITY_ODD; + break; + case 2: + rsrc_obj->handle.Init.Parity = UART_PARITY_EVEN; + break; + default: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + } + + rsrc_obj->handle.Init.Mode = UART_MODE_TX_RX; + rsrc_obj->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; + rsrc_obj->handle.Init.OverSampling = UART_OVERSAMPLING_16; + + HAL_StatusTypeDef status = HAL_HalfDuplex_Init(&rsrc_obj->handle); + if (status != HAL_OK) { + enif_release_resource(rsrc_obj); + AVM_LOGE(TAG, "HAL_HalfDuplex_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("\xf", "halfduplex_init"))); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + HAL_UART_DeInit(&rsrc_obj->handle); + enif_release_resource(rsrc_obj); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term obj = term_from_resource(rsrc_obj, &ctx->heap); + 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_uart_halfduplex_enable_tx(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + if (!rsrc_obj->is_half_duplex) { + return ERROR_ATOM; + } + HAL_StatusTypeDef status = HAL_HalfDuplex_EnableTransmitter(&rsrc_obj->handle); + return (status == HAL_OK) ? OK_ATOM : ERROR_ATOM; +} + +static term nif_uart_halfduplex_enable_rx(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + if (!rsrc_obj->is_half_duplex) { + return ERROR_ATOM; + } + HAL_StatusTypeDef status = HAL_HalfDuplex_EnableReceiver(&rsrc_obj->handle); + return (status == HAL_OK) ? OK_ATOM : ERROR_ATOM; +} + +static void uart_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct UARTResource *rsrc_obj = (struct UARTResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->handle.Instance)) { + HAL_UART_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + } +} + +static const ErlNifResourceTypeInit UARTResourceTypeInit = { + .members = 1, + .dtor = uart_resource_dtor, +}; + +static const struct Nif uart_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_init +}; + +static const struct Nif uart_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_deinit +}; + +static const struct Nif uart_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_write +}; + +static const struct Nif uart_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_read +}; + +static const struct Nif uart_abort_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_abort +}; + +static const struct Nif uart_get_state_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_get_state +}; + +static const struct Nif uart_get_error_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_get_error +}; + +static const struct Nif uart_halfduplex_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_halfduplex_init +}; + +static const struct Nif uart_halfduplex_enable_tx_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_halfduplex_enable_tx +}; + +static const struct Nif uart_halfduplex_enable_rx_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_halfduplex_enable_rx +}; + +static void uart_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + uart_resource_type = enif_init_resource_type(&env, "uart_resource", &UARTResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *uart_nif_get_nif(const char *nifname) +{ + if (strncmp("uart:", nifname, 5) != 0) { + return NULL; + } + const char *rest = nifname + 5; + if (strcmp("init/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_deinit_nif; + } + if (strcmp("write/3", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_write_nif; + } + if (strcmp("read/3", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_read_nif; + } + if (strcmp("abort/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_abort_nif; + } + if (strcmp("get_state/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_get_state_nif; + } + if (strcmp("get_error/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_get_error_nif; + } + if (strcmp("halfduplex_init/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_halfduplex_init_nif; + } + if (strcmp("halfduplex_enable_tx/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_halfduplex_enable_tx_nif; + } + if (strcmp("halfduplex_enable_rx/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_halfduplex_enable_rx_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(uart, uart_nif_init, NULL, uart_nif_get_nif) diff --git a/src/platforms/stm32/src/lib/usb_cdc_driver.c b/src/platforms/stm32/src/lib/usb_cdc_driver.c new file mode 100644 index 0000000000..fe53f87431 --- /dev/null +++ b/src/platforms/stm32/src/lib/usb_cdc_driver.c @@ -0,0 +1,491 @@ +/* + * 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 + */ + +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + +#include + +#include + +#include "stm32_hal_platform.h" + +#include "atom.h" +#include "bif.h" +#include "context.h" +#include "debug.h" +#include "defaultatoms.h" +#include "globalcontext.h" +#include "interop.h" +#include "mailbox.h" +#include "module.h" +#include "port.h" +#include "scheduler.h" +#include "term.h" +#include "utils.h" + +#include "stm_sys.h" +#include "usb_hw_init.h" + +static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx); + +#define USB_CDC_BUF_SIZE 256 + +// Abort a write after this many ms of no progress (host stalled or unplugged). +#define USB_CDC_WRITE_TIMEOUT_MS 500 + +struct USBCDCData +{ + GlobalContext *global; + term reader_process_pid; + uint64_t reader_ref_ticks; + volatile bool rx_pending; + uint8_t itf; +}; + +enum usb_cdc_cmd +{ + USBCDCInvalidCmd = 0, + USBCDCReadCmd = 1, + USBCDCWriteCmd = 2, + USBCDCCloseCmd = 3, + USBCDCCancelCmd = 4 +}; + +static const AtomStringIntPair cmd_table[] = { + { ATOM_STR("\x4", "read"), USBCDCReadCmd }, + { ATOM_STR("\x5", "write"), USBCDCWriteCmd }, + { ATOM_STR("\x5", "close"), USBCDCCloseCmd }, + { ATOM_STR("\xB", "cancel_read"), USBCDCCancelCmd }, + SELECT_INT_DEFAULT(USBCDCInvalidCmd) +}; + +// Singleton driver data. Written by the main thread. +static struct USBCDCData *s_cdc_data = NULL; + +void tud_cdc_rx_cb(uint8_t itf) +{ + if (s_cdc_data != NULL && itf == s_cdc_data->itf) { + s_cdc_data->rx_pending = true; + } +} + +static void usb_cdc_send_error_reply(Context *ctx, term pid, term ref, AtomString reason_atom_str) +{ + GlobalContext *glb = ctx->global; + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + term reason = globalcontext_make_atom(glb, reason_atom_str); + term error_tuple = port_create_error_tuple(ctx, reason); + port_send_reply(ctx, pid, ref, error_tuple); +} + +static void usb_cdc_check_rx(struct USBCDCData *cdc_data) +{ + if (!cdc_data->rx_pending) { + return; + } + cdc_data->rx_pending = false; + + if (cdc_data->reader_process_pid == term_invalid_term()) { + return; + } + + uint8_t buf[USB_CDC_BUF_SIZE]; + uint32_t rx_size = tud_cdc_n_read(cdc_data->itf, buf, sizeof(buf)); + if (rx_size == 0) { + return; + } + + GlobalContext *glb = cdc_data->global; + int bin_size = term_binary_heap_size(rx_size); + + Heap heap; + if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + AVM_ABORT(); + } + + term bin = term_create_uninitialized_binary(rx_size, &heap, glb); + memcpy((void *) term_binary_data(bin), buf, rx_size); + + term ok_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(ok_tuple, 0, OK_ATOM); + term_put_tuple_element(ok_tuple, 1, bin); + + term ref = term_from_ref_ticks(cdc_data->reader_ref_ticks, &heap); + + term result_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, ok_tuple); + + int local_pid = term_to_local_process_id(cdc_data->reader_process_pid); + globalcontext_send_message(glb, local_pid, result_tuple); + + memory_destroy_heap(&heap, glb); + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; +} + +static Context *usb_cdc_driver_create_port(GlobalContext *global, term opts) +{ + UNUSED(opts); + + if (s_cdc_data != NULL) { + fprintf(stderr, "usb_cdc: CDC interface already in use\n"); + return NULL; + } + + // Initialize TinyUSB (clocks, GPIO, NVIC, then the device stack) + // Done before allocating any AtomVM resources so a HW failure has no cleanup. + if (!tud_inited()) { + if (!usb_cdc_hw_init()) { + fprintf(stderr, "usb_cdc: USB hardware init failed\n"); + return NULL; + } + tusb_init(); + } + + struct USBCDCData *cdc_data = malloc(sizeof(struct USBCDCData)); + if (IS_NULL_PTR(cdc_data)) { + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + return NULL; + } + + Context *ctx = context_new(global); + if (IS_NULL_PTR(ctx)) { + fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + free(cdc_data); + return NULL; + } + + cdc_data->global = global; + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + cdc_data->rx_pending = false; + cdc_data->itf = 0; + + s_cdc_data = cdc_data; + + ctx->native_handler = usb_cdc_driver_consume_mailbox; + ctx->platform_data = cdc_data; + + return ctx; +} + +static void usb_cdc_driver_do_read(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term pid = gen_message.pid; + term ref = gen_message.ref; + uint64_t ref_ticks = term_to_ref_ticks(ref); + + int local_pid = term_to_local_process_id(pid); + + if (cdc_data->reader_process_pid != term_invalid_term()) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + term ealready = globalcontext_make_atom(glb, ATOM_STR("\x8", "ealready")); + term error_tuple = port_create_error_tuple(ctx, ealready); + port_send_reply(ctx, pid, ref, error_tuple); + return; + } + + // Process USB tasks and try immediate read + tud_task(); + uint8_t buf[USB_CDC_BUF_SIZE]; + uint32_t rx_size = tud_cdc_n_read(cdc_data->itf, buf, sizeof(buf)); + if (rx_size > 0) { + int bin_size = term_binary_heap_size(rx_size); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, bin_size + TUPLE_SIZE(2) * 2, 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate return value\n"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + term bin = term_create_uninitialized_binary(rx_size, &ctx->heap, glb); + memcpy((void *) term_binary_data(bin), buf, rx_size); + + term ok_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ok_tuple, 0, OK_ATOM); + term_put_tuple_element(ok_tuple, 1, bin); + + port_send_reply(ctx, pid, ref, ok_tuple); + } else { + cdc_data->reader_process_pid = pid; + cdc_data->reader_ref_ticks = ref_ticks; + } +} + +static void usb_cdc_driver_do_cancel_read(Context *ctx, GenMessage gen_message) +{ + struct USBCDCData *cdc_data = ctx->platform_data; + term req = gen_message.req; + term pid = gen_message.pid; + term ref = gen_message.ref; + + // Two forms: bare `cancel_read` clears unconditionally (legacy), and + // `{cancel_read, ReadRef}` only clears if ReadRef matches the stored + // reader. The tuple form is what avoids the race where a cancel + // wipes a freshly-installed reader belonging to a different request. + bool clear_unconditionally = term_is_atom(req); + uint64_t target_ref_ticks = 0; + if (!clear_unconditionally) { + target_ref_ticks = term_to_ref_ticks(term_get_tuple_element(req, 1)); + } + + if (clear_unconditionally || cdc_data->reader_ref_ticks == target_ref_ticks) { + cdc_data->reader_process_pid = term_invalid_term(); + cdc_data->reader_ref_ticks = 0; + } + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate return value\n"); + globalcontext_send_message(ctx->global, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void usb_cdc_driver_do_write(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term msg = gen_message.req; + term pid = gen_message.pid; + term ref = gen_message.ref; + + term data = term_get_tuple_element(msg, 1); + int local_pid = term_to_local_process_id(pid); + + size_t buffer_size; + switch (interop_iolist_size(data, &buffer_size)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + case InteropBadArg: + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "badarg")); + return; + } + void *buffer = malloc(buffer_size); + if (IS_NULL_PTR(buffer)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + switch (interop_write_iolist(data, buffer)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + free(buffer); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + case InteropBadArg: + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "badarg")); + return; + } + + // If the host hasn't opened the CDC interface yet, silently drop data + if (!tud_cdc_n_connected(cdc_data->itf)) { + free(buffer); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); + return; + } + + size_t written = 0; + uint32_t deadline = HAL_GetTick() + USB_CDC_WRITE_TIMEOUT_MS; + while (written < buffer_size) { + if (!tud_cdc_n_connected(cdc_data->itf)) { + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x6", "closed")); + return; + } + uint32_t chunk = tud_cdc_n_write(cdc_data->itf, + (const uint8_t *) buffer + written, + buffer_size - written); + tud_cdc_n_write_flush(cdc_data->itf); + if (chunk > 0) { + written += chunk; + // Reset the deadline on any progress so a slow but moving + // host can drain a buffer larger than the FIFO without tripping. + deadline = HAL_GetTick() + USB_CDC_WRITE_TIMEOUT_MS; + } else { + if ((int32_t) (HAL_GetTick() - deadline) >= 0) { + free(buffer); + usb_cdc_send_error_reply(ctx, pid, ref, ATOM_STR("\x7", "timeout")); + return; + } + tud_task(); + } + } + + free(buffer); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) +{ + GlobalContext *glb = ctx->global; + struct USBCDCData *cdc_data = ctx->platform_data; + term pid = gen_message.pid; + term ref = gen_message.ref; + + term pending_reader_pid = cdc_data->reader_process_pid; + uint64_t pending_reader_ref_ticks = cdc_data->reader_ref_ticks; + cdc_data->reader_process_pid = term_invalid_term(); + + if (pending_reader_pid != term_invalid_term()) { + BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + term error_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); + term reader_ref = term_from_ref_ticks(pending_reader_ref_ticks, &heap); + term reply = term_alloc_tuple(2, &heap); + term_put_tuple_element(reply, 0, reader_ref); + term_put_tuple_element(reply, 1, error_tuple); + globalcontext_send_message(glb, term_to_local_process_id(pending_reader_pid), reply); + END_WITH_STACK_HEAP(heap, glb) + } + + s_cdc_data = NULL; + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &ref, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + globalcontext_send_message(glb, term_to_local_process_id(pid), OUT_OF_MEMORY_ATOM); + free(cdc_data); + ctx->platform_data = NULL; + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); + + free(cdc_data); + ctx->platform_data = NULL; +} + +/* + * Public hook called from sys_poll_events: drain any RX delivered by + * the OTG IRQ since the last poll. Without this, once the link manager + * parks waiting on UartMod:read/2 the port stops receiving mailbox + * messages, and even though tud_task processes the IRQ events, no one + * delivers the resulting bytes to the parked reader. + */ +void usb_cdc_driver_poll(void) +{ + if (s_cdc_data == NULL) { + return; + } + tud_task(); + usb_cdc_check_rx(s_cdc_data); +} + +static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) +{ + struct USBCDCData *cdc_data = ctx->platform_data; + bool is_closed = false; + + // Drain any pending USB RX before processing the mailbox so a fresh + // {read, ...} request can return immediately if data is already here. + if (cdc_data != NULL) { + tud_task(); + usb_cdc_check_rx(cdc_data); + } + + while (mailbox_has_next(&ctx->mailbox)) { + Message *message = mailbox_first(&ctx->mailbox); + term msg = message->message; + GenMessage gen_message; + if (UNLIKELY(port_parse_gen_message(msg, &gen_message) != GenCallMessage)) { + fprintf(stderr, "usb_cdc: Received invalid message.\n"); + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + continue; + } + + uint64_t ref_ticks = term_to_ref_ticks(gen_message.ref); + int local_pid = term_to_local_process_id(gen_message.pid); + + if (is_closed) { + GlobalContext *glb = ctx->global; + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); + globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); + } else { + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, NOPROC_ATOM); + + term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + + term result_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result_tuple, 0, ref); + term_put_tuple_element(result_tuple, 1, error_tuple); + + globalcontext_send_message(glb, local_pid, result_tuple); + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + continue; + } + + term req = gen_message.req; + term cmd_term = term_is_atom(req) ? req : term_get_tuple_element(req, 0); + + enum usb_cdc_cmd cmd = interop_atom_term_select_int(cmd_table, cmd_term, ctx->global); + switch (cmd) { + case USBCDCReadCmd: + usb_cdc_driver_do_read(ctx, gen_message); + break; + case USBCDCWriteCmd: + usb_cdc_driver_do_write(ctx, gen_message); + break; + case USBCDCCloseCmd: + usb_cdc_driver_do_close(ctx, gen_message); + is_closed = true; + break; + case USBCDCCancelCmd: + usb_cdc_driver_do_cancel_read(ctx, gen_message); + break; + default: + fprintf(stderr, "usb_cdc: unrecognized command\n"); + usb_cdc_send_error_reply(ctx, gen_message.pid, gen_message.ref, ATOM_STR("\xF", "unknown_command")); + break; + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + } + return is_closed ? NativeTerminate : NativeContinue; +} + +REGISTER_PORT_DRIVER(usb_cdc, NULL, NULL, usb_cdc_driver_create_port) + +#endif diff --git a/src/platforms/stm32/src/lib/usb_cdc_driver.h b/src/platforms/stm32/src/lib/usb_cdc_driver.h new file mode 100644 index 0000000000..a27e0f42b5 --- /dev/null +++ b/src/platforms/stm32/src/lib/usb_cdc_driver.h @@ -0,0 +1,24 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef ATOMVM_USB_CDC_DRIVER_H +#define ATOMVM_USB_CDC_DRIVER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Drive TinyUSB's task pump and deliver any newly-arrived bytes to a + * parked reader. Safe to call when no port is open. */ +void usb_cdc_driver_poll(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/stm32/src/lib/usb_descriptors.c b/src/platforms/stm32/src/lib/usb_descriptors.c new file mode 100644 index 0000000000..aaf41d34b4 --- /dev/null +++ b/src/platforms/stm32/src/lib/usb_descriptors.c @@ -0,0 +1,114 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + +#include +#include + +#ifndef USBD_VID +#define USBD_VID 0xCAFE +#endif +#ifndef USBD_PID +#define USBD_PID 0x4001 +#endif +#ifndef USBD_MANUFACTURER +#define USBD_MANUFACTURER "AtomVM" +#endif +#ifndef USBD_PRODUCT +#define USBD_PRODUCT "AtomVM CDC" +#endif +#ifndef USBD_INTERFACE_NAME +#define USBD_INTERFACE_NAME "AtomVM CDC ACM" +#endif + +#define USBD_ITF_CDC 0 +#define USBD_ITF_MAX 2 + +#define USBD_CDC_EP_CMD 0x81 +#define USBD_CDC_EP_OUT 0x02 +#define USBD_CDC_EP_IN 0x82 +#define USBD_CDC_CMD_MAX_SIZE 8 +#define USBD_CDC_IN_OUT_MAX_SIZE 64 + +#define USBD_STR_LANG 0 +#define USBD_STR_MANUF 1 +#define USBD_STR_PRODUCT 2 +#define USBD_STR_SERIAL 3 +#define USBD_STR_CDC 4 + +#define USBD_DESC_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN) + +static const tusb_desc_device_t usbd_desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = USBD_VID, + .idProduct = USBD_PID, + .bcdDevice = 0x0100, + .iManufacturer = USBD_STR_MANUF, + .iProduct = USBD_STR_PRODUCT, + .iSerialNumber = USBD_STR_SERIAL, + .bNumConfigurations = 1, +}; + +static const uint8_t usbd_desc_cfg[USBD_DESC_LEN] = { + TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_MAX, USBD_STR_LANG, USBD_DESC_LEN, 0, 250), + TUD_CDC_DESCRIPTOR(USBD_ITF_CDC, USBD_STR_CDC, USBD_CDC_EP_CMD, + USBD_CDC_CMD_MAX_SIZE, USBD_CDC_EP_OUT, USBD_CDC_EP_IN, USBD_CDC_IN_OUT_MAX_SIZE), +}; + +static const char *const usbd_desc_str[] = { + [USBD_STR_MANUF] = USBD_MANUFACTURER, + [USBD_STR_PRODUCT] = USBD_PRODUCT, + [USBD_STR_SERIAL] = "STM32", + [USBD_STR_CDC] = USBD_INTERFACE_NAME, +}; + +const uint8_t *tud_descriptor_device_cb(void) +{ + return (const uint8_t *) &usbd_desc_device; +} + +const uint8_t *tud_descriptor_configuration_cb(uint8_t index) +{ + (void) index; + return usbd_desc_cfg; +} + +const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void) langid; + static uint16_t desc_str[32]; + + uint8_t len; + if (index == USBD_STR_LANG) { + desc_str[1] = 0x0409; + len = 1; + } else { + if (index >= sizeof(usbd_desc_str) / sizeof(usbd_desc_str[0])) { + return NULL; + } + const char *str = usbd_desc_str[index]; + if (!str) { + return NULL; + } + for (len = 0; len < 31 && str[len]; ++len) { + desc_str[1 + len] = (uint16_t) str[len]; + } + } + + desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * len + 2)); + return desc_str; +} + +#endif diff --git a/src/platforms/stm32/src/lib/usb_hw_init.c b/src/platforms/stm32/src/lib/usb_hw_init.c new file mode 100644 index 0000000000..ee7f4a7203 --- /dev/null +++ b/src/platforms/stm32/src/lib/usb_hw_init.c @@ -0,0 +1,85 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +/* Family-specific USB peripheral hardware initialization (clocks, GPIO, NVIC). + * Called once from usb_cdc_driver_create_port before tusb_init(). + */ + +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + +#include "usb_hw_init.h" +#include "stm32_hal_platform.h" + +#include + +#if defined(STM32H7XX) || defined(STM32H5XX) + +bool usb_cdc_hw_init(void) +{ + RCC_OscInitTypeDef osc = { 0 }; + RCC_PeriphCLKInitTypeDef pclk = { 0 }; + + osc.OscillatorType = RCC_OSCILLATORTYPE_HSI48; + osc.HSI48State = RCC_HSI48_ON; + osc.PLL.PLLState = RCC_PLL_NONE; + if (HAL_RCC_OscConfig(&osc) != HAL_OK) { + fprintf(stderr, "usb_cdc: HAL_RCC_OscConfig failed (HSI48 unavailable)\n"); + return false; + } + + pclk.PeriphClockSelection = RCC_PERIPHCLK_USB; + pclk.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; + if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) { + fprintf(stderr, "usb_cdc: HAL_RCCEx_PeriphCLKConfig failed\n"); + return false; + } + +#if defined(STM32H7XX) + if (HAL_PWREx_EnableUSBVoltageDetector() != HAL_OK) { + fprintf(stderr, "usb_cdc: HAL_PWREx_EnableUSBVoltageDetector failed\n"); + return false; + } + + __HAL_RCC_USB2_OTG_FS_CLK_ENABLE(); +#else + HAL_PWREx_EnableVddUSB(); + + __HAL_RCC_USB_CLK_ENABLE(); +#endif + + __HAL_RCC_GPIOA_CLK_ENABLE(); + GPIO_InitTypeDef gpio = { 0 }; + gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12; + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Pull = GPIO_NOPULL; + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + +#if defined(STM32H7XX) + gpio.Alternate = GPIO_AF10_OTG1_FS; +#else + gpio.Alternate = GPIO_AF10_USB; +#endif + HAL_GPIO_Init(GPIOA, &gpio); + +#if defined(STM32H7XX) + HAL_NVIC_SetPriority(OTG_FS_IRQn, 6, 0); + HAL_NVIC_EnableIRQ(OTG_FS_IRQn); +#else + HAL_NVIC_SetPriority(USB_DRD_FS_IRQn, 6, 0); + HAL_NVIC_EnableIRQ(USB_DRD_FS_IRQn); +#endif + return true; +} + +#else + +#error AVM_USB_CDC_PORT_DRIVER_ENABLED: usb_cdc_hw_init not implemented for this STM32 family. Add the family-specific clock/GPIO/NVIC setup (see the H7/H5 branch above) or disable the option. + +#endif + +#endif diff --git a/src/platforms/stm32/src/lib/usb_hw_init.h b/src/platforms/stm32/src/lib/usb_hw_init.h new file mode 100644 index 0000000000..be5e301ffb --- /dev/null +++ b/src/platforms/stm32/src/lib/usb_hw_init.h @@ -0,0 +1,28 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _ATOMVM_USB_HW_INIT_H +#define _ATOMVM_USB_HW_INIT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Family-specific USB peripheral hardware initialization. + * @returns true on success, false if HAL clock configuration failed. + */ +bool usb_cdc_hw_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/stm32/src/lib/usb_irq_handlers.c b/src/platforms/stm32/src/lib/usb_irq_handlers.c new file mode 100644 index 0000000000..6b3fc069ba --- /dev/null +++ b/src/platforms/stm32/src/lib/usb_irq_handlers.c @@ -0,0 +1,43 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifdef AVM_USB_CDC_PORT_DRIVER_ENABLED + +#include + +/* TinyUSB DCD interrupt entry point. The exact ISR symbol is family-specific: + * - H7 / F2 / F4 / F7: OTG_FS_IRQHandler (synopsys dwc2 OTG controller) + * - H5: USB_DRD_FS_IRQHandler (STM32 USB_DRD_FS device IP) + * + * Both forward to dcd_int_handler(0). For H7 with OTG_HS in use, also wire + * OTG_HS_IRQHandler -> dcd_int_handler(1) (not enabled here by default). + */ + +#if defined(STM32H7XX) || defined(STM32F4XX) || defined(STM32F7XX) || defined(STM32F2XX) + +void OTG_FS_IRQHandler(void); +void OTG_FS_IRQHandler(void) +{ + tud_int_handler(0); +} + +#elif defined(STM32H5XX) + +void USB_DRD_FS_IRQHandler(void); +void USB_DRD_FS_IRQHandler(void) +{ + tud_int_handler(0); +} + +#else + +#error "AVM_USB_CDC_PORT_DRIVER_ENABLED: no IRQ handler wiring for this STM32 family" + +#endif + +#endif diff --git a/src/platforms/stm32/tests/renode/stm32_uart_test.robot b/src/platforms/stm32/tests/renode/stm32_uart_test.robot new file mode 100644 index 0000000000..a8ec875293 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32_uart_test.robot @@ -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 +# + +*** Settings *** +Suite Setup Setup +Suite Teardown Teardown +Test Setup Reset Emulation +Resource ${RENODEKEYWORDS} + +*** Variables *** +${UART} sysbus.usart1 +${TEST_UART} sysbus.usart2 +${PLATFORM} @platforms/cpus/stm32f4.repl +${ELF} REQUIRED +${AVM} REQUIRED +${AVM_ADDRESS} 0x08060000 + +*** Test Cases *** +AtomVM Should Communicate Over UART + 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 the Erlang test to signal it is ready to receive, then inject + # "hello" (104 101 108 108 111) into USART2 one byte at a time. + Wait For Line On Uart uart_waiting timeout=30 + Execute Command ${TEST_UART} WriteChar 104 + Execute Command ${TEST_UART} WriteChar 101 + Execute Command ${TEST_UART} WriteChar 108 + Execute Command ${TEST_UART} WriteChar 108 + Execute Command ${TEST_UART} WriteChar 111 + Wait For Line On Uart uart_done timeout=30 diff --git a/src/platforms/stm32/tests/renode/stm32h743.repl b/src/platforms/stm32/tests/renode/stm32h743.repl index 6e2d7d2238..05f1ba2d7c 100644 --- a/src/platforms/stm32/tests/renode/stm32h743.repl +++ b/src/platforms/stm32/tests/renode/stm32h743.repl @@ -40,6 +40,11 @@ usart1: UART.STM32F7_USART @ sysbus <0x40011000, +0x100> frequency: 120000000 IRQ -> nvic@37 +// USART2 for loopback testing (PA2/PA3) +usart2: UART.STM32F7_USART @ sysbus <0x40004400, +0x100> + frequency: 120000000 + IRQ -> nvic@38 + // RCC Python peripheral at 0x58024400. // H7 RCC register layout: // CR (0x00): HSION[0], HSIRDY[2], CSION[7], CSIRDY[8], diff --git a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt index fd2ac4f920..92d2593e41 100644 --- a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -25,3 +25,4 @@ pack_runnable(stm32_gpio_test test_gpio eavmlib avm_stm32) pack_runnable(stm32_i2c_test test_i2c eavmlib avm_stm32) pack_runnable(stm32_spi_test test_spi eavmlib avm_stm32) pack_runnable(stm32_crypto_test test_crypto estdlib) +pack_runnable(stm32_uart_test test_uart eavmlib avm_stm32) diff --git a/src/platforms/stm32/tests/test_erl_sources/test_uart.erl b/src/platforms/stm32/tests/test_erl_sources/test_uart.erl new file mode 100644 index 0000000000..543287b61c --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/test_uart.erl @@ -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 +% + +-module(test_uart). + +-export([start/0]). + +start() -> + erlang:display(test_uart_start), + % Initialize USART2: PA2 (TX), PA3 (RX), AF7, 115200 baud + % All test platforms use USART2 on PA2/PA3 with AF7 (AF is not checked by Renode) + {ok, UART} = uart:init([ + {peripheral, 2}, + {tx, {a, 2}}, + {rx, {a, 3}}, + {af, 7}, + {speed, 115200} + ]), + % Test TX: write "hello" and verify byte count + 5 = uart:write(UART, <<"hello">>, 5000), + ready = uart:get_state(UART), + 0 = uart:get_error(UART), + % Signal the Renode test that we are ready to receive. + % The robot will inject 5 bytes into USART2 upon seeing this. + erlang:display(uart_waiting), + % Test RX: read back the 5 bytes injected by Renode + {ok, <<"hello">>} = uart:read(UART, 5, 5000), + ok = uart:deinit(UART), + erlang:display(uart_done).