This example exchanges CAN frames between an Ubuntu computer and an ESP32. Configure the host interface as described in CAN 02 before connecting the ESP32.
The Arduino sketch prints received frames and sends hello approximately once per second. It also sends an RTR frame requesting three data bytes, so the logger can show a second frame without a payload.
Hardware
The host connects through a USB-to-CAN adapter, while the ESP32 connects through an external CAN transceiver. The original ESP32 includes a CAN-compatible controller, documented by Espressif as the Two-Wire Automotive Interface (TWAI). It requires an external physical-layer transceiver. Other microcontrollers may also need an external CAN controller, such as the MCP2515.




Connect the ESP32 to a CAN Transceiver
Connect the ESP32's TWAI TX and RX signals to the transceiver's logic-side TXD and RXD pins. CAN-H and CAN-L connect to the differential bus. Connecting them directly to ESP32 GPIO pins can damage the board.
This setup uses GPIO 17 for TX and GPIO 16 for RX. Other pins can be selected through the ESP32 GPIO matrix, but TX must be output-capable and the library configuration must match the wiring.
The TJA1050 is a 5 V transceiver. Many inexpensive modules expose a 5 V logic-high level on RXD, while ESP32 GPIO is 3.3 V. Check the exact module schematic and transceiver datasheet before connecting it. Use a transceiver with 3.3 V-compatible logic or appropriate level shifting.
I used the Arduino IDE with the Arduino-CAN library. For explicit timing configuration, error-state handling, and recovery control, refer to Espressif's TWAI driver.
An Arduino-CAN issue reports a half-rate result for one ESP32/RTOS configuration. Start with the intended rate, such as
CAN.begin(500E3)for 500 kbit/s, and verify it with a known-good node or analyzer. If the measured rate differs, check the board clock, ESP32 revision, library version, and bit-timing implementation before changing the configured value.
Before flashing, change the historical sketch's CAN.begin(1000E3) to CAN.begin(500E3) to match this post's 500 kbit/s host. Retain TX 17 and RX 16 for the wiring above:
CAN.setPins(RX_GPIO_NUM, TX_GPIO_NUM);
if (!CAN.begin(500E3)) {
Serial.println("Starting CAN failed!");
while (1) {}
}
Flash this adjusted version of the linked example sketch to the ESP32. If the IDE reports that it cannot open /dev/ttyUSB0, add the user to the serial-device group:
sudo adduser <username> dialout
Replace <username> with the current user, then log out and back in so the new group membership takes effect. A temporary chmod can help diagnose a permission problem. Use group membership or a udev rule for persistent access.
A transmitted frame needs acknowledgement from at least one other active CAN node. Missing hardware, incorrect wiring, or incorrect termination can cause transmission failures and repeated retries. Depending on the library, CAN.endPacket() can also block.
Receive Frames from the ESP32
Assuming the USB adapter uses candleLight firmware, configure can0 for 500 kbit/s and bring it up:
sudo ip link set can0 down
sudo ip link set can0 up type can bitrate 500000
Open a terminal to display received frames:
candump can0

candump displays the payload as hexadecimal bytes. For an ASCII payload:
68 65 6C 6C 6F represents hello.
Send a Frame to the ESP32
Use cansend <interface> <CAN-ID>#<payload> to transmit a Classic CAN data frame. The following command sends hello with base identifier 0x123:
cansend can0 123#68656C6C6F
With correct wiring, termination, and an acknowledging peer, the ESP32 serial monitor should display the received payload. Repeated transmission failures can fill a host or device TX queue. Check interface error counters when the payload does not arrive; a successful application write alone does not confirm bus delivery.

Generate Test Traffic
On an isolated test bus, cangen can generate synthetic frames:
cangen can0
This command transmits continuously until it is stopped. Do not run it on a live robot or vehicle network: random identifiers and payloads can trigger unintended device behavior or saturate the bus.

The example receiver displays every byte as an ASCII character, so random payloads can produce unreadable text. To inspect the bytes in hex, replace Serial.print((char)CAN.read()); with Serial.print(CAN.read(), HEX);.