Skip to content

Commit eae0c9d

Browse files
committed
Add USB CDC port drivers for ESP32, RP2 and STM32
Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent 36e2117 commit eae0c9d

36 files changed

Lines changed: 3207 additions & 17 deletions

.github/workflows/esp32-build.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ jobs:
6767
idf-version: 'v5.5.3'
6868
usb-serial: 'ON'
6969
jit: false
70+
- esp-idf-target: "esp32s3"
71+
idf-version: 'v5.5.3'
72+
usb-cdc: 'ON'
73+
jit: false
7074
- esp-idf-target: "esp32"
7175
idf-version: 'v5.4.3'
7276
libsodium: 'ON'
@@ -104,7 +108,7 @@ jobs:
104108
idf.py add-dependency "espressif/libsodium^1.0.20~4"
105109
106110
- name: Add ESP TinyUSB dependency
107-
if: matrix.usb-serial == 'ON'
111+
if: matrix.usb-serial == 'ON' || matrix.usb-cdc == 'ON'
108112
working-directory: ./src/platforms/esp32/
109113
run: |
110114
. $IDF_PATH/export.sh
@@ -115,11 +119,18 @@ jobs:
115119
working-directory: ./src/platforms/esp32/
116120
env:
117121
USB_SERIAL: ${{ matrix.usb-serial || 'OFF' }}
122+
USB_CDC: ${{ matrix.usb-cdc || 'OFF' }}
118123
run: |
119124
. $IDF_PATH/export.sh
120125
if [[ "${USB_SERIAL}" == "ON" ]]; then
121126
echo 'CONFIG_USE_USB_SERIAL=y' >> sdkconfig.defaults.in
122127
fi
128+
if [[ "${USB_CDC}" == "ON" ]]; then
129+
printf '%s\n' \
130+
'CONFIG_TINYUSB_CDC_ENABLED=y' \
131+
'CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER=y' \
132+
>> sdkconfig.defaults.in
133+
fi
123134
export IDF_TARGET=${{matrix.esp-idf-target}}
124135
if [ "${{ matrix.jit }}" = "true" ]; then
125136
SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.jit" idf.py set-target ${{matrix.esp-idf-target}}

.github/workflows/pico-build.yaml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ jobs:
106106
platform: "-DPICO_PLATFORM=rp2350-riscv"
107107
jit: "-DAVM_DISABLE_JIT=OFF"
108108

109+
- board: "pico"
110+
platform: ""
111+
jit: ""
112+
usb-cdc: "ON"
113+
109114
steps:
110115
- name: Checkout repo
111116
uses: actions/checkout@v4
@@ -172,7 +177,7 @@ jobs:
172177
set -euo pipefail
173178
mkdir build
174179
cd build
175-
cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} ${{ matrix.platform }} ${{ matrix.jit }}
180+
cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} ${{ matrix.platform }} ${{ matrix.jit }} ${{ matrix.usb-cdc == 'ON' && '-DAVM_USB_CDC_PORT_DRIVER_ENABLED=ON' || '' }}
176181
cmake --build . --target=AtomVM
177182
178183
- name: "Perform CodeQL Analysis"
@@ -187,7 +192,7 @@ jobs:
187192
nvm install 24
188193
189194
- name: Build tests (without SMP)
190-
if: matrix.board != 'pico2' && matrix.board != 'pico2_w'
195+
if: matrix.board != 'pico2' && matrix.board != 'pico2_w' && matrix.usb-cdc != 'ON'
191196
shell: bash
192197
working-directory: ./src/platforms/rp2/
193198
run: |
@@ -199,7 +204,7 @@ jobs:
199204
cmake --build . --target=rp2_tests
200205
201206
- name: Run tests with rp2040js
202-
if: matrix.board != 'pico2' && matrix.board != 'pico2_w'
207+
if: matrix.board != 'pico2' && matrix.board != 'pico2_w' && matrix.usb-cdc != 'ON'
203208
shell: bash
204209
working-directory: ./src/platforms/rp2/tests
205210
run: |
@@ -210,7 +215,7 @@ jobs:
210215
npx tsx run-tests.ts ../build.nosmp/tests/rp2_tests.uf2 ../build.nosmp/tests/test_erl_sources/rp2_test_modules.uf2
211216
212217
- name: Rename AtomVM and write sha256sum
213-
if: matrix.platform == '' && matrix.jit == ''
218+
if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
214219
shell: bash
215220
run: |
216221
pushd src/platforms/rp2/build
@@ -220,14 +225,14 @@ jobs:
220225
popd
221226
222227
- name: Upload AtomVM artifact
223-
if: matrix.platform == '' && matrix.jit == ''
228+
if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
224229
uses: actions/upload-artifact@v4
225230
with:
226231
name: AtomVM-${{ matrix.board }}-${{env.AVM_REF_NAME}}.uf2
227232
path: src/platforms/rp2/build/src/AtomVM-${{ matrix.board }}-*.uf2
228233

229234
- name: Rename atomvmlib-rp2 and write sha256sum
230-
if: matrix.platform == '' && matrix.jit == ''
235+
if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
231236
shell: bash
232237
run: |
233238
pushd build/libs
@@ -237,7 +242,7 @@ jobs:
237242
popd
238243
239244
- name: Combine uf2 using uf2tool
240-
if: matrix.platform == '' && matrix.jit == ''
245+
if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
241246
shell: bash
242247
run: |
243248
ATOMVM_COMBINED_FILE=AtomVM-${{ matrix.board }}-combined-${{env.AVM_REF_NAME}}.uf2
@@ -246,15 +251,15 @@ jobs:
246251
echo "ATOMVM_COMBINED_FILE=${ATOMVM_COMBINED_FILE}" >> $GITHUB_ENV
247252
248253
- name: Upload combined AtomVM artifact
249-
if: matrix.platform == '' && matrix.jit == ''
254+
if: matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
250255
uses: actions/upload-artifact@v4
251256
with:
252257
name: ${{ env.ATOMVM_COMBINED_FILE }}
253258
path: ${{ env.ATOMVM_COMBINED_FILE }}
254259

255260
- name: Release (Pico & Pico2)
256261
uses: softprops/action-gh-release@v3.0.0
257-
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.board != 'pico2_w' && matrix.platform == '' && matrix.jit == ''
262+
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.board != 'pico2_w' && matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
258263
with:
259264
draft: true
260265
fail_on_unmatched_files: true
@@ -268,7 +273,7 @@ jobs:
268273
269274
- name: Release (PicoW & Pico2W)
270275
uses: softprops/action-gh-release@v3.0.0
271-
if: startsWith(github.ref, 'refs/tags/') && (matrix.board == 'pico_w' || matrix.board == 'pico2_w') && matrix.platform == '' && matrix.jit == ''
276+
if: startsWith(github.ref, 'refs/tags/') && (matrix.board == 'pico_w' || matrix.board == 'pico2_w') && matrix.platform == '' && matrix.jit == '' && matrix.usb-cdc != 'ON'
272277
with:
273278
draft: true
274279
fail_on_unmatched_files: true

.github/workflows/stm32-build.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ jobs:
8787
max_size: 524288
8888
- device: stm32h562rgt6
8989
max_size: 524288
90+
usb-cdc: "OFF"
91+
- device: stm32h562rgt6
92+
max_size: 524288
93+
usb-cdc: "ON"
9094
- device: stm32f746zgt6
9195
max_size: 524288
9296
renode_platform: stm32f746.repl
@@ -162,7 +166,7 @@ jobs:
162166
set -euo pipefail
163167
mkdir build
164168
cd build
165-
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DDEVICE=${{ matrix.device }}
169+
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DDEVICE=${{ matrix.device }} ${{ matrix.usb-cdc == 'ON' && '-DAVM_USB_CDC_PORT_DRIVER_ENABLED=ON' || '' }}
166170
cmake --build .
167171
168172
- name: "Perform CodeQL Analysis"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
USB-Serial-JTAG controller (C3/C5/C6/C61/H2/H21/H4/P4/S3)
2222
- Added support for the `safe` option in `erlang:binary_to_term/2`
2323
- Added xtensa JIT backend for esp32 platform
24+
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
2425

2526
### Changed
2627
- Updated network type db() to dbm() to reflect the actual representation of the type

doc/src/distributed-erlang.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ Distribution over serial (UART) is also available for point-to-point
1818
connections between any two nodes, including microcontrollers without
1919
networking (e.g. STM32). See [Serial distribution](#serial-distribution).
2020

21-
Three examples are provided:
21+
Distribution over USB CDC is also supported on ESP32-S2/S3, RP2040/RP2350,
22+
and STM32, using the same `serial_dist` protocol. On Unix hosts, USB CDC
23+
devices appear as standard serial ports. See [USB distribution](#usb-distribution).
24+
25+
Four examples are provided:
2226

2327
- disterl in `examples/erlang/disterl.erl`: distribution on Unix systems
2428
- epmd\_disterl in `examples/erlang/esp32/epmd_disterl.erl`: distribution on ESP32 devices
2529
- serial\_disterl in `examples/erlang/serial_disterl.erl`: distribution over serial (ESP32 and Unix)
30+
- usb\_disterl in `examples/erlang/usb_disterl.erl`: distribution over USB CDC (all platforms)
2631

2732
## Starting and stopping distribution
2833

@@ -240,6 +245,73 @@ This creates two pseudo-terminal devices (e.g. `/dev/ttys003` and
240245
{some_registered_name, 'a@serial.local'} ! {self(), hello}.
241246
```
242247

248+
## USB distribution
249+
250+
AtomVM supports distribution over USB CDC (Communications Device Class)
251+
connections. USB CDC makes the device appear as a virtual serial port,
252+
so it reuses the `serial_dist` module with a USB-specific HAL module.
253+
254+
See the [USB CDC](./programmers-guide.md#usb-cdc) section of the Programmer's
255+
Guide for how to enable the port driver on each platform
256+
257+
### Platform support
258+
259+
| Platform | Module | Notes |
260+
|----------|--------|-------|
261+
| ESP32 (S2/S3) | `usb_cdc` | Requires TinyUSB CDC (`CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER`) |
262+
| RP2040/RP2350 | `usb_cdc` | Requires `AVM_USB_CDC_PORT_DRIVER_ENABLED`; disable `pico_enable_stdio_usb` |
263+
| STM32 | `usb_cdc` | Requires TinyUSB integration and `AVM_USB_CDC_PORT_DRIVER_ENABLED` |
264+
| Unix | `uart` | USB CDC devices appear as `/dev/ttyACMx` (Linux) or `/dev/cu.usbmodemXXXX` (macOS) |
265+
266+
### Quick start, MCU side (ESP32-S3 example)
267+
268+
```erlang
269+
{ok, _} = net_kernel:start('sensor@serial.local', #{
270+
name_domain => longnames,
271+
proto_dist => serial_dist,
272+
avm_dist_opts => #{
273+
uart_opts => [{peripheral, "CDC0"}],
274+
uart_module => usb_cdc
275+
}
276+
}).
277+
```
278+
279+
### Quick start, Unix host side
280+
281+
On Unix, USB CDC devices are standard serial ports. Use the regular
282+
`uart` module:
283+
284+
```erlang
285+
{ok, _} = net_kernel:start('host@serial.local', #{
286+
name_domain => longnames,
287+
proto_dist => serial_dist,
288+
avm_dist_opts => #{
289+
uart_opts => [{peripheral, "/dev/ttyACM0"}, {speed, 115200}],
290+
uart_module => uart
291+
}
292+
}).
293+
```
294+
295+
### Multi-device topology
296+
297+
USB uses a star topology: one host connects to multiple devices through
298+
a USB hub. Each device appears as a separate `/dev/ttyACMx` on the host.
299+
Use `uart_ports` (list of proplists) to connect to multiple devices:
300+
301+
```erlang
302+
{ok, _} = net_kernel:start('host@serial.local', #{
303+
name_domain => longnames,
304+
proto_dist => serial_dist,
305+
avm_dist_opts => #{
306+
uart_ports => [
307+
[{peripheral, "/dev/ttyACM0"}, {speed, 115200}],
308+
[{peripheral, "/dev/ttyACM1"}, {speed, 115200}]
309+
],
310+
uart_module => uart
311+
}
312+
}).
313+
```
314+
243315
## Distribution features
244316

245317
Distribution implementation is (very) partial. The most basic features are available:

doc/src/programmers-guide.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,38 @@ ok = uart:close(UART)
19851985
19861986
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}`.
19871987
1988+
### USB CDC
1989+
1990+
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).
1991+
1992+
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`.
1993+
1994+
#### Platform support and build configuration
1995+
1996+
| Platform | Notes |
1997+
|----------|-------|
1998+
| 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. |
1999+
| 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. |
2000+
| 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. |
2001+
2002+
#### USB VID/PID and string descriptors
2003+
2004+
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:
2005+
2006+
- **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.
2007+
- **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.
2008+
- **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.
2009+
2010+
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:
2011+
2012+
- **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).
2013+
- **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`.
2014+
2015+
The string descriptors can be overridden the same way:
2016+
2017+
- **RP2 / STM32** (CMake): `-DAVM_USB_CDC_MANUFACTURER="Your Company"`, `-DAVM_USB_CDC_PRODUCT="Your Product"`, `-DAVM_USB_CDC_INTERFACE_NAME="Your Product CDC ACM"`.
2018+
- **ESP32** (sdkconfig): `CONFIG_TINYUSB_DESC_MANUFACTURER_STRING`, `CONFIG_TINYUSB_DESC_PRODUCT_STRING`, `CONFIG_TINYUSB_DESC_CDC_STRING`.
2019+
19882020
### LED Control
19892021
19902022
The LED Control API can be used to drive LEDs, as well as generate PWM signals on GPIO pins.

examples/erlang/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pack_runnable(logging_example logging_example estdlib eavmlib)
4444
pack_runnable(http_client http_client estdlib eavmlib avm_network)
4545
pack_runnable(disterl disterl estdlib)
4646
pack_runnable(serial_disterl serial_disterl eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_unix)
47+
pack_runnable(usb_disterl usb_disterl eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32 avm_unix)
4748
pack_runnable(i2c_scanner i2c_scanner eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32)
4849
pack_runnable(i2c_lis3dh i2c_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32)
4950
pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32)

0 commit comments

Comments
 (0)