Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ jobs:
target: esp32
- path: 'components/byte90/example'
target: esp32s3
- path: 'components/canopen/can_bridge_example'
target: esp32s3
Comment thread
finger563 marked this conversation as resolved.
- path: 'components/canopen/example'
target: esp32
- path: 'components/chsc6x/example'
Expand Down
34 changes: 34 additions & 0 deletions components/canopen/can_bridge_example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.20)

# NOTE: the IDF component manager is intentionally left ENABLED here (unlike most
# espp examples) so it can fetch the managed `espressif/esp_tinyusb` dependency
# declared by the usb_device component. To avoid the manager scanning every espp
# component manifest (some board components declare target-specific constraints
# that would fail on esp32s3), EXTRA_COMPONENT_DIRS is narrowed to just the
# components this example uses; the in-repo espp components there satisfy the
# `espp/*` dependencies locally.
include($ENV{IDF_PATH}/tools/cmake/project.cmake)

set(EXTRA_COMPONENT_DIRS
"../../../components/base_component"
"../../../components/dispatcher"
"../../../components/format"
"../../../components/logger"
"../../../components/stream_frame"
"../../../components/task"
"../../../components/twai"
"../../../components/usb_device"
)

set(
COMPONENTS
"main esptool_py base_component dispatcher format logger stream_frame task twai usb_device esp_tinyusb"
CACHE STRING
"List of components to include"
)

project(can_bridge_example)

set(CMAKE_CXX_STANDARD 20)
78 changes: 78 additions & 0 deletions components/canopen/can_bridge_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# USB <-> CAN Bridge Example

Turns an ESP32-S3 into a **WebUSB / Web Serial CAN interface**: the hosted
[CAN console web app](https://esp-cpp.github.io/espp/apps/can_console.html)
connects over the native USB and can

- **send** CAN frames as a normal, ACK-ing bus participant ("master"), and
- **inspect** the bus — stream every received frame; in *listen-only* mode the
node is a passive sniffer that never ACKs or transmits.

It bridges the ESP32-S3 TWAI (CAN 2.0) controller to the host over USB using the
espp `stream_frame` framing and an `espp::Dispatcher` (this example owns
**module id 5**). The same framed protocol is exposed on both the USB **vendor**
interface (WebUSB) and a **CDC** interface (Web Serial), so the web app can use
either transport. The system console/logs stay on the separate built-in
USB-Serial-JTAG.

## Wiring

Connect the TWAI TX/RX GPIOs to a CAN transceiver (e.g. SN65HVD230, TJA1050) on
a properly terminated (120 Ω) bus. Defaults (change in `can_bridge_example.cpp`):

| Signal | GPIO |
|--------|------|
| TWAI TX | 17 |
| TWAI RX | 16 |

Listen-only mode monitors an existing bus without a transceiver ACKing, but a
transceiver is still required to receive the differential signal.

## Protocol (module 5)

Framed with `stream_frame` and routed by `espp::Dispatcher`. The full base
header order on the wire (all multi-byte fields little-endian) is:

```
[magic u16 "OT"][flags u8][module u8][type u8][len u32][payload…][crc32 u32]
```

The base header is 9 bytes. `module` is **5** for this bridge. `flags` bit0 =
reply (0 = host→device request, 1 = device→host reply/event), bits 4-7 =
version = 1 — so a request `flags` byte is `0x10` and a reply is `0x11`. (v2
also defines an optional correlation-id field gated by `flags` bit1, inserted
between `type` and `len`; the CAN bridge never sets it, so its frames always use
the 9-byte base header.) `crc32` covers the header + payload. Host→device
requests use type high-nibble 5; device→host replies/events use high-nibble D.

| Type | Dir | Meaning |
|------|-----|---------|
| `0x50` CAN_TX | H→D | transmit a CAN frame |
| `0x51` SET_CONFIG | H→D | `[baudrate u32][mode u8][rsv u8]` (mode 0=normal, 1=listen-only) |
| `0x52` START | H→D | bring the bus up with the current config |
| `0x53` STOP | H→D | take the bus down |
| `0x54` GET_STATUS | H→D | request a STATUS reply |
| `0xD0` CAN_RX | D→H | a received CAN frame |
| `0xD1` OK | D→H | ack |
| `0xD2` ERROR | D→H | `[code u32][utf8 message]` |
| `0xD3` STATUS | D→H | `[baudrate u32][mode u8][running u8][rx u32][tx u32][err u32]` |

A CAN frame is encoded as `[id u32][flags u8][dlc u8]` optionally followed by
`dlc` data bytes, where `flags` bit0 = extended (29-bit) and bit1 = RTR. The
data bytes are present **only for non-RTR frames**: an RTR frame is just the
6-byte header even when its `dlc` is nonzero (the DLC is the requested response
length, not a data length). A client must therefore append no data for RTR
frames — the bridge encodes and expects none — so the payload is 6 bytes for RTR
and `6 + dlc` (6..14) otherwise.

The bus starts **stopped**: the host sets baudrate/mode with `SET_CONFIG`, then
`START`. `SET_CONFIG` is rejected while the bus is running (stop first).

## Build & flash

```
idf.py set-target esp32s3
idf.py build flash monitor
```

Then open the CAN console web app and Connect (WebUSB or Web Serial).
5 changes: 5 additions & 0 deletions components/canopen/can_bridge_example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES usb_device twai dispatcher stream_frame task logger
)
Loading
Loading