diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 6854d8ce8b..020b4891dc 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -67,6 +67,10 @@ jobs: idf-version: 'v5.5.3' usb-serial: 'ON' jit: false + - esp-idf-target: "esp32s3" + idf-version: 'v5.5.3' + usb-cdc: 'ON' + jit: false - esp-idf-target: "esp32" idf-version: 'v5.4.3' libsodium: 'ON' @@ -104,7 +108,7 @@ jobs: idf.py add-dependency "espressif/libsodium^1.0.20~4" - name: Add ESP TinyUSB dependency - if: matrix.usb-serial == 'ON' + if: matrix.usb-serial == 'ON' || matrix.usb-cdc == 'ON' working-directory: ./src/platforms/esp32/ run: | . $IDF_PATH/export.sh @@ -115,11 +119,18 @@ jobs: working-directory: ./src/platforms/esp32/ env: USB_SERIAL: ${{ matrix.usb-serial || 'OFF' }} + USB_CDC: ${{ matrix.usb-cdc || 'OFF' }} run: | . $IDF_PATH/export.sh if [[ "${USB_SERIAL}" == "ON" ]]; then echo 'CONFIG_USE_USB_SERIAL=y' >> sdkconfig.defaults.in fi + 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}} if [ "${{ matrix.jit }}" = "true" ]; then SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.jit" idf.py set-target ${{matrix.esp-idf-target}} 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 de3c7cfb7f..7dc9109eb5 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -87,6 +87,10 @@ 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 @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index e24a91384f..5349f6c7d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added xtensa JIT backend for esp32 platform - Added support for configuring pins and width for sdmmc on ESP32 - Added support for map comprehensions +- 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 8135f0cd8a..7d282e1127 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -2012,6 +2012,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 2c10ab5a02..fff889277b 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -44,6 +44,7 @@ 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) 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 4922100dac..8419133648 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_precompiled_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 0267964b7f..5b40e14130 100644 --- a/libs/avm_stm32/src/CMakeLists.txt +++ b/libs/avm_stm32/src/CMakeLists.txt @@ -27,6 +27,7 @@ set(ERLANG_MODULES 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/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/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 b9830c60b9..b8b7f84237 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -70,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} @@ -101,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/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