CAN Bus for Makers: ESP32 TWAI and MCP2515 Wiring, Termination, and Message Design
CAN bus (Controller Area Network) has a reputation as "the thing in cars," but it's a genuinely useful protocol on the workbench too: it's differential (noise-resistant over long cable runs), multi-drop (many nodes on two wires, no addressing conflicts to manage manually), and built for exactly the kind of noisy, electrically messy environment a robotics platform, CNC gantry, or multi-board sensor rig lives in. If you've hit the practical limits of I2C (too short, too fragile with more than a couple of devices) or RS-485/Modbus (works, but master-polled and comparatively slow), CAN is worth adding to your toolkit — independent of anything automotive.
Why CAN Over I2C or RS-485 for Multi-Board Projects
ProtocolTopologyNoise ImmunityTypical Use Here I2CBus, single-endedPoor over more than ~30cm without bufferingOn-board sensors, short breakout runs RS-485 / ModbusBus, differential, master-polledGoodIndustrial sensors, PLC integration CANBus, differential, multi-master, arbitration-basedExcellentRobotics nodes, distributed motor controllers, gantry-spanning sensor networksThe feature that sets CAN apart is bus arbitration: any node can transmit at any time, and if two nodes transmit simultaneously, a built-in priority scheme resolves the collision without corrupting either message or requiring a retry from a polling master. That makes CAN a good fit for a robot or CNC machine with several independently-acting boards (a motor controller, a limit switch monitor, a temperature sensor node) that all need to talk without a central coordinator babysitting a poll cycle.
Hardware: ESP32's Built-In TWAI Controller
Confusingly, Espressif calls their CAN peripheral "TWAI" (Two-Wire Automotive Interface) rather than CAN, for licensing reasons — but it's a standard CAN 2.0 controller and interoperates with any other CAN device. Most ESP32 variants (original ESP32, S2, S3, C3, C6) include a TWAI controller on-chip. What the chip does not include is a CAN transceiver — the TWAI peripheral only outputs logic-level TX/RX signals, which need a transceiver chip to convert to the differential CAN_H/CAN_L signal pair that actually goes on the bus.
ESP32 TWAI PinTransceiver (SN65HVD230) Pin GPIO 4 (example, TX)D (driver input) GPIO 5 (example, RX)R (receiver output) 3.3VVCC GNDGNDThe SN65HVD230 is the easiest transceiver to pair with an ESP32 because it's natively 3.3V — no level shifting needed. The more common MCP2551 transceiver is 5V-only and requires level-shifted TX/RX lines if driven directly from ESP32 GPIO.
Hardware: MCP2515 for Arduino and Anything Without Built-In CAN
For an Arduino Uno, Nano, or any microcontroller without a native CAN peripheral, the MCP2515 SPI-to-CAN controller module (nearly always sold bundled with a TJA1050 transceiver on the same breakout board) is the standard choice. It talks to the host over SPI and handles CAN framing, filtering, and arbitration in hardware.
MCP2515 Module PinArduino Uno Pin VCC5V GNDGND CSD10 SO / SI / SCKD12 / D11 / D13 (hardware SPI) INTD2 (optional, for interrupt-driven reads)Bus Wiring and Termination
This is where most first CAN builds go wrong. CAN requires 120Ω termination resistors at each physical end of the bus — not at every node, only the two ends. Most transceiver breakout boards include a jumper or solder pad to enable an onboard 120Ω resistor; enable it on exactly the two nodes at the physical ends of your cable run and leave it disabled everywhere else. Missing termination causes signal reflections that show up as intermittent, hard-to-diagnose frame errors under load; double termination (every node enabled) drops bus impedance too low and causes similar symptoms. Use twisted-pair cable for CAN_H/CAN_L — off-the-shelf CAT5 works well for short runs — and keep a common ground reference across all nodes.
Software: Sending and Receiving Frames
On ESP32 with ESP-IDF or Arduino, the built-in driver/twai.h API (or the Arduino ESP32-TWAI-CAN library) configures a bit timing rate — 500 kbps and 250 kbps are common choices for hobby robotics buses — installs the driver, and starts it. Sending a frame means populating a twai_message_t struct with an 11-bit or 29-bit identifier and up to 8 bytes of payload, then calling twai_transmit(). Receiving is a blocking or polled call to twai_receive() that returns the next frame matching your acceptance filter.
On Arduino with an MCP2515 module, the well-established arduino-CAN or mcp_can libraries provide a similar send/receive API over SPI. A basic node loop reads a sensor, packs the value into a CAN frame with a unique ID for that sensor type, and transmits; other nodes on the bus set up filters to only wake on IDs they care about.
Designing Your Message IDs
Unlike I2C's device-address model, CAN identifiers double as both message priority (lower numeric ID wins arbitration) and message type. For a small hobby network, a simple convention works well: reserve a block of IDs per node or per message category (e.g., 0x100-0x10F for temperature readings, 0x200-0x20F for motor commands) and document it once in a spreadsheet or header file shared across all your firmware projects. This avoids the classic mistake of two nodes accidentally transmitting on the same ID with different meanings.
Where This Actually Helps
Good candidates for a CAN backbone: a multi-axis robot with a motor controller board per joint, a CNC or 3D printer with distributed temperature/endstop monitoring across a large gantry where a single control board's wiring harness would be unwieldy, or a sensor mesh spanning a workshop where RS-485's master-polling model feels like overkill. It's more setup than I2C for a two-board project, but it scales to a dozen nodes on one twisted pair far more gracefully than either I2C or point-to-point UART ever will.
🔧 Related tool: Wire Gauge / Ampacity Calculator