CAN is a 2-wire differential bus: CAN-H and CAN-L, 120ohm termination at each end. There is no source/destination addressing at the frame level and no built-in authentication -- any node physically on the bus can send any ID it wants. That absence of addressing and auth is the entire reason this field exists: possession of the bus is authorization.
On the OBD-II connector (16-pin, SAE J1962, in-car): pin 6 = CAN-H, pin 14 = CAN-L, pins 4/5 = ground, pin 16 = battery+ (permanently powered, not switched by ignition). Most passenger cars run high-speed CAN (ISO 11898-2) at 500 kbps on the OBD-II port; some run a second bus (body/comfort) at 125 kbps.
OBD-II 16-pin connector (SAE J1962), pins as viewed on the connector face +----+----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |mfr |J850|mfr |chas|sig |CANH|K-ln|mfr | +----+----+----+----+----+----+----+----+ | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | |mfr |J850|mfr |mfr |mfr |CANL|L-ln|+12V| +----+----+----+----+----+----+----+----+ pin 4/5 chassis / signal ground pin 6/14 CAN-H / CAN-L differential pair (ISO 15765-4 / J2284), 500 kbps pin 16 battery + (~12V), permanently powered both ends of the in-vehicle bus already carry 120ohm termination -- a flying-lead tap elsewhere on the harness needs its own termination or reflections corrupt frames
A classic CAN frame carries an 11-bit (standard) or 29-bit (extended) arbitration ID plus up to 8 data bytes. Lower ID wins arbitration -- it has priority, and it wins with zero collision or retransmission delay. CAN-FD extends the payload to 64 bytes and adds a bitrate switch; newer vehicles increasingly use it. The bit-level breakdown of a frame, and exactly how arbitration works, is in Frame Anatomy.
The fundamentals in Intro said "11-bit ID plus up to 8 data bytes." Here's what that looks like at the bit level, per CAN 2.0 / ISO 11898-1.
CAN 2.0A data frame, 11-bit standard ID (bit widths, not bytes) SOF ARBITRATION CONTROL DATA CRC ACK EOF 1 bit ID[10:0] + RTR IDE + r0 + DLC[3:0] 0-8 bytes 15 bits slot+dlm 7 bits 11+1 = 12 bits 1+1+4 = 6 bits 0-64 bits +1 dlm bit 1+1 bits SOF dominant (0), every node syncs to it -- this is what "start of frame" means on the wire RTR 0 = data frame, 1 = remote (request) frame -- remote frames carry no data IDE 0 = standard 11-bit ID, 1 = extended -- next field becomes an 18-bit ID extension r0 reserved bit, sent dominant DLC 4-bit data length code, 0-8 in classic CAN (9-15 reserved / meaningful only in CAN-FD) CRC 15-bit checksum + 1 recessive delimiter bit ACK transmitter sends recessive; any receiver that got a valid CRC pulls it dominant EOF 7 recessive bits, marks the end -- no other field is ever 7 bits of recessive
| Property | Standard (CAN 2.0A) | Extended (CAN 2.0B) | CAN-FD |
|---|---|---|---|
| ID width | 11 bits | 29 bits (11-bit base + 18-bit extension) | 11 or 29 bits |
| Max payload | 8 bytes | 8 bytes | 64 bytes |
| IDE bit | 0 | 1 | 0 or 1 |
| Bit rate | single rate throughout | single rate throughout | BRS bit switches to a faster rate for control+data+CRC |
| Distinguishing bit | -- | -- | FDF (formerly r0) set = FD frame |
| CRC | 15-bit | 15-bit | 17-bit (payload ≤16B) or 21-bit (>16B) |
CAN uses non-destructive bitwise arbitration. A 0 bit is dominant, a 1 bit is recessive -- if any node drives dominant while another drives recessive, the bus physically reads dominant (a wired-AND on the differential pair). During the arbitration field, every transmitting node sends its ID bit by bit and simultaneously reads back what's actually on the wire. The instant a node sends recessive but reads dominant, it knows a node with a numerically lower ID is also transmitting, and it immediately stops driving and becomes a receiver. The node whose ID never mismatches wins outright -- no collision, no backoff, no retransmission delay. This is what makes CAN suitable for real-time control traffic instead of just a best-effort network.
What actually gets a CAN bus onto a Linux box as a SocketCAN interface, from cheapest to most polished.
| Interface | What it is | SocketCAN story |
|---|---|---|
| MCP2515 + TJA1050/TJA1051 | SPI CAN controller + transceiver, cheap breakout boards | Native kernel driver + device-tree overlay (Pi hat) or bit-banged SPI (ESP32/Arduino); shows up as can0 once the overlay probes |
| PCAN-USB (Peak-System) | Commercial USB-CAN adapter, ~$200 | Native SocketCAN interface with no extra driver -- the "just works" option |
| CANtact / candleLight dongles | Open hardware USB-CAN dongles | gs_usb driver built into mainline Linux, shows up as can0 directly, no slcand step needed. Cheap clones exist widely |
| MCP2515+CH340 slcan boards | Cheap USB-serial CAN board | Speaks the slcan (LAWICEL) serial protocol, needs slcand to bridge into SocketCAN |
| Macchina M2 | Open source dual/quad CAN board, OBD-II connector form factor | Popular in the car-hacking community for in-vehicle use |
The MCP2515 is the default first interface for a reason: it's cheap, well documented, and runs directly off an ESP32 or Raspberry Pi SPI bus with no USB layer in between. The register-level detail of talking to it -- and the ESP32's own native CAN controller -- is in ESP32 / MCP2515.
Two ways to get CAN frames in and out of a microcontroller: the ESP32's built-in TWAI controller (TWAI = Two-Wire Automotive Interface, Espressif's name for their CAN 2.0 implementation), or an MCP2515 driven over SPI from any MCU. Both need an external transceiver -- neither chip drives the differential pair directly.
The twai driver in ESP-IDF wraps the peripheral: general config picks GPIO pins and mode, timing config picks bitrate, filter config picks which IDs to accept.
esp-idf twai init + transmit #include "driver/twai.h" #define TX_GPIO GPIO_NUM_5 #define RX_GPIO GPIO_NUM_4 void twai_init(void) { twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TX_GPIO, RX_GPIO, TWAI_MODE_NORMAL); twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); ESP_ERROR_CHECK(twai_driver_install(&g_config, &t_config, &f_config)); ESP_ERROR_CHECK(twai_start()); } void twai_send_frame(uint32_t id, uint8_t *data, uint8_t len) { twai_message_t msg = {0}; msg.identifier = id; msg.data_length_code = len; msg.flags = TWAI_MSG_FLAG_NONE; // standard 11-bit ID, data frame memcpy(msg.data, data, len); twai_transmit(&msg, pdMS_TO_TICKS(100)); }
esp-idf twai receive/dump loop void twai_rx_loop(void) { twai_message_t msg; for (;;) { if (twai_receive(&msg, pdMS_TO_TICKS(1000)) == ESP_OK) { printf("id=0x%03lx dlc=%d data=", msg.identifier, msg.data_length_code); for (int i = 0; i < msg.data_length_code; i++) printf("%02x ", msg.data[i]); printf("\n"); } } }
No HAL, just the Microchip datasheet: SPI command bytes, then key registers for mode control, bit timing, and the TX/RX buffers.
mcp2515 spi opcodes + registers (microchip datasheet)// SPI instruction bytes #define MCP_RESET 0xC0 #define MCP_READ 0x03 #define MCP_WRITE 0x02 #define MCP_READ_STATUS 0xA0 #define MCP_RX_STATUS 0xB0 #define MCP_BIT_MODIFY 0x05 #define MCP_READ_RXB0 0x90 #define MCP_LOAD_TXB0 0x40 #define MCP_RTS(n) (0x80 | (1 << (n))) // n = tx buffer 0-2 // key registers #define MCP_CANCTRL 0x0F #define MCP_CANSTAT 0x0E #define MCP_CNF1 0x2A #define MCP_CNF2 0x29 #define MCP_CNF3 0x28 #define MCP_CANINTE 0x2B #define MCP_CANINTF 0x2C #define MCP_TXB0CTRL 0x30 #define MCP_TXB0SIDH 0x31 #define MCP_TXB0SIDL 0x32 #define MCP_TXB0DLC 0x35 #define MCP_TXB0D0 0x36 #define MCP_RXB0CTRL 0x60 #define MCP_RXB0SIDH 0x61 // CANCTRL[7:5] mode bits (REQOP) #define MODE_CONFIG 0x80 #define MODE_NORMAL 0x00 #define MODE_LISTENONLY 0x60 #define MODE_LOOPBACK 0x40 #define MODE_SLEEP 0x20
register read/write helpers over spi_device_transmit static void mcp_write_reg(spi_device_handle_t spi, uint8_t reg, uint8_t val) { uint8_t tx[3] = { MCP_WRITE, reg, val }; spi_transaction_t t = { .length = 8 * sizeof(tx), .tx_buffer = tx, }; spi_device_transmit(spi, &t); } static uint8_t mcp_read_reg(spi_device_handle_t spi, uint8_t reg) { uint8_t tx[3] = { MCP_READ, reg, 0x00 }; uint8_t rx[3] = {0}; spi_transaction_t t = { .length = 8 * sizeof(tx), .tx_buffer = tx, .rx_buffer = rx, }; spi_device_transmit(spi, &t); return rx[2]; } static void mcp_reset(spi_device_handle_t spi) { uint8_t cmd = MCP_RESET; spi_transaction_t t = { .length = 8, .tx_buffer = &cmd }; spi_device_transmit(spi, &t); }
config mode + bit timing for 500kbps @ 8mhz xtal static void mcp_set_bitrate_500k_8mhz(spi_device_handle_t spi) { // BIT MODIFY CANCTRL: mask REQOP[7:5], set config mode uint8_t tx[4] = { MCP_BIT_MODIFY, MCP_CANCTRL, 0xE0, MODE_CONFIG }; spi_transaction_t t = { .length = 8 * sizeof(tx), .tx_buffer = tx }; spi_device_transmit(spi, &t); while ((mcp_read_reg(spi, MCP_CANSTAT) & 0xE0) != MODE_CONFIG) ; // 8MHz crystal, 500kbps -- known-good CNF1-3 values for this combo mcp_write_reg(spi, MCP_CNF1, 0x00); mcp_write_reg(spi, MCP_CNF2, 0x90); mcp_write_reg(spi, MCP_CNF3, 0x02); // back to normal mode uint8_t tx2[4] = { MCP_BIT_MODIFY, MCP_CANCTRL, 0xE0, MODE_NORMAL }; spi_transaction_t t2 = { .length = 8 * sizeof(tx2), .tx_buffer = tx2 }; spi_device_transmit(spi, &t2); }
load txb0 + request-to-send void mcp_send_frame(spi_device_handle_t spi, uint16_t id, uint8_t *data, uint8_t len) { mcp_write_reg(spi, MCP_TXB0SIDH, id >> 3); mcp_write_reg(spi, MCP_TXB0SIDL, (id & 0x07) << 5); mcp_write_reg(spi, MCP_TXB0DLC, len & 0x0F); for (uint8_t i = 0; i < len; i++) mcp_write_reg(spi, MCP_TXB0D0 + i, data[i]); // request-to-send on TXB0 -- opcode 0x81 = RTS | (1 << 0) uint8_t rts = MCP_RTS(0); spi_transaction_t t = { .length = 8, .tx_buffer = &rts }; spi_device_transmit(spi, &t); }
SocketCAN is a Linux kernel subsystem, not a library -- a CAN interface behaves like a network interface (ip link, ifconfig) and frames move through a raw socket.
vcan setup sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set up vcan0
MCP2515 hat, PCAN-USB, CANtact/gs_usb dongle -- anything that shows up as can0 directly.
bring up can0 sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up # undo: sudo ip link set can0 down
Needs a device-tree overlay in /boot/config.txt (or /boot/firmware/config.txt on newer Raspberry Pi OS):
/boot/config.txt dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25 dtoverlay=spi-bcm2835-overlay
Then dmesg | grep -i mcp2515 to confirm probe, and bring can0 up as above.
LAWICEL protocol -- most cheap USB-CAN dongles speak this over a serial port.
slcand bridge sudo slcand -o -c -s6 /dev/ttyUSB0 can0 # -s6 = 500kbit, see slcand -h for the -sN table sudo ip link set can0 up
This is the library-free way to talk SocketCAN, and mirrors exactly what candump does under the hood: open a PF_CAN/SOCK_RAW socket, resolve the interface index, bind, then read/write fixed-size struct can_frames.
raw AF_CAN, linux <linux/can.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <net/if.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <linux/can.h> #include <linux/can/raw.h> int main(void) { int s = socket(PF_CAN, SOCK_RAW, CAN_RAW); struct ifreq ifr; strcpy(ifr.ifr_name, "can0"); ioctl(s, SIOCGIFINDEX, &ifr); struct sockaddr_can addr = {0}; addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; bind(s, (struct sockaddr *)&addr, sizeof(addr)); struct can_frame frame; frame.can_id = 0x123; frame.can_dlc = 4; memcpy(frame.data, "\xDE\xAD\xBE\xEF", 4); write(s, &frame, sizeof(frame)); for (;;) { int n = read(s, &frame, sizeof(frame)); if (n != sizeof(frame)) continue; printf("id=0x%03X dlc=%d\n", frame.can_id, frame.can_dlc); } }
Install (Debian/Ubuntu): sudo apt install can-utils. Source: linux-can/can-utils on GitHub if you need a newer build -- the isotp tools especially lag in distro packages.
| Command | What it does |
|---|---|
| candump can0 | Live dump, all IDs |
| candump can0,123:7FF | Filter: ID 0x123 exact match (mask 7FF) |
| candump -l can0 | Log to candump-<timestamp>.log |
| candump -L can0 | Log in ASC-ish long format |
| cansend can0 123#DEADBEEF | Send one frame, ID 0x123, data DE AD BE EF |
| cangen vcan0 -g 100 -I 1A0 -L 8 -D i | Generate frames every 100ms, ID 1A0, 8 bytes, incrementing data |
| cansniffer can0 | Live view, highlights changing bytes -- best RE starting point |
| canplayer -I candump.log | Replay a captured log back onto the bus |
| canbusload can0@500000 | Bus utilization %, needs the bitrate for the math |
| cangw -A -s can0 -d can1 -e | Gateway/forward frames between two CAN interfaces |
| log2asc / asc2log | Convert between candump log format and Vector ASC |
cansniffer, wiggle one thing in the car (indicator, window, throttle), and watch which bytes flip. It's the fastest way to answer "what CAN ID changes when I press this button" -- faster than diffing candump logs by hand for a first pass.
No magic here -- reverse engineering unknown CAN IDs is differential analysis.
candump -l can0 with nothing happening.cansniffer live, to isolate the ID/byte that moved.| Tool | What it's for |
|---|---|
| SavvyCAN | Cross-platform Qt GUI -- graphing, bitfield/signal discovery, comparison between captures, DBC import/export. The standard GUI tool for this. |
| cabana | comma.ai, part of the openpilot tools repo -- browser-based CAN visualizer/DBC editor, good for correlating signals against a synced video if you have one. |
| caringcaribou | Python framework with dump, send, and diagnostic (UDS/XCP) modules -- useful for scripted, repeatable RE rather than clicking through a GUI. |
A .dbc file maps raw CAN IDs/bytes to named, scaled signals (e.g. EngineRPM). Once you've reverse engineered a signal, write it into a DBC so you -- and tooling -- can decode it by name going forward.
cantools cli pip install cantools cantools decode my.dbc < candump.log # decode a candump-format log using a DBC cantools dump my.dbc # print all messages/signals defined
cantools python api import cantools db = cantools.database.load_file('my.dbc') msg = db.get_message_by_frame_id(0x123) print(msg.decode(bytes.fromhex('deadbeefdeadbeef')))
For real-world reference DBCs to cross-check IDs against (many common signals are similar across a manufacturer's lineup): commaai/opendbc has a large collection of production DBC files pulled from openpilot-supported vehicles.
zombieCraig/ICSim on GitHub -- a fake instrument cluster + control panel that talks over a vcan interface. The standard way to practice replay/fuzzing without touching a real vehicle.
ICSim setup git clone https://github.com/zombieCraig/ICSim cd ICSim && make ./icsim vcan0 # dashboard window ./controls vcan0 # keyboard-driven control panel that sends CAN frames
cangen fuzz cangen vcan0 -g 50 -I r -L r -D r # random ID, random length, random data, every 50ms cangen vcan0 -I 220 -D r -L 8 -g 20 # fixed ID, random payload -- fuzz one message's data
For anything more structured than can-utils gives you (mutation seeded from a real capture, ID-range sweeps, replay-then-fuzz), caringcaribou's dump/send modules or a short custom python-can script are the next step up.
Segments payloads >8 bytes over CAN -- this is the transport UDS diagnostics rides on. Kernel module is mainline since Linux 5.10; on older kernels build can-isotp from hartkopp/can-isotp.
isotp tools sudo modprobe can-isotp isotpsend can0 -s 7e0 -d 7e8 # send, src/dst arbitration IDs isotprecv can0 -s 7e8 -d 7e0 isotpdump can0 -s 7e0 -d 7e8 # sniff a specific ISO-TP conversation
Reach for a library rather than hand-rolling frames: python-udsoncan (pylessard/python-udsoncan) + python-can-isotp as the transport layer, or caringcaribou's uds module for quick discovery (cc.py uds discovery can0 sweeps arbitration IDs looking for a diagnostic responder).
| SID | Service |
|---|---|
| 0x10 | DiagnosticSessionControl |
| 0x27 | SecurityAccess (seed/key) |
| 0x22 | ReadDataByIdentifier |
| 0x2E | WriteDataByIdentifier |
| 0x31 | RoutineControl |
| 0x23 | ReadMemoryByAddress |
| 0x3D | WriteMemoryByAddress |
| 0x34 | RequestDownload |
| 0x36 | TransferData |
| 0x37 | RequestTransferExit |
Standard OBD-II PIDs (mode 01) are a tiny, standardized subset of what's actually on the bus -- most interesting signals are manufacturer-specific and require reverse engineering or a factory DBC/UDS spec, not OBD-II.
| PID | Signal |
|---|---|
| 0x0C | Engine RPM |
| 0x0D | Vehicle speed |
| 0x05 | Coolant temperature |
python-obd pip install obd
python-obd query import obd conn = obd.OBD("/dev/ttyUSB0") # auto-detects an ELM327-compatible adapter print(conn.query(obd.commands.RPM))
python-obd can't get a clean response, talk to the adapter directly over serial first (ATZ reset, ATE0 echo off, ATSP0 auto-protocol) to rule out the dongle before blaming the code.
| Family | Where it shows up | Debug interface |
|---|---|---|
| Infineon TriCore | TC1766, TC1796, TC2xx -- very common in Bosch/Continental/Delphi ECUs | JTAG/DAP historically needs Lauterbach or PLS commercial tools; OpenOCD TriCore support exists but is limited/community-maintained -- check current state before assuming it'll just work |
| Freescale/NXP/ST MPC55xx / SPC56x | PowerPC e200z -- common in older-generation Bosch/Delphi ECUs | BDM (background debug mode) or JTAG; P&E Micro Multilink and USBDM are the usual affordable options |
| ARM Cortex-M (STM32 etc.) | Increasingly common in simpler modules (BCM, gateways, some newer ECUs) | Standard SWD -- OpenOCD + ST-Link/J-Link/Black Magic Probe just works |
| Renesas RH850 | Shows up in newer Denso/Hitachi ECUs | Vendor debug tooling; check current OpenOCD/probe support |
$10 diagnostic session, $27 SecurityAccess (seed/key -- may require knowing the OEM's algorithm), then $34/$36/$37 (RequestDownload/TransferData/RequestTransferExit) or $23 (ReadMemoryByAddress) to pull flash contents over the bus with no physical access to the board at all. caringcaribou or a custom python-udsoncan script.first pass on a firmware image binwalk -e firmware.bin # carve out embedded filesystems/known signatures strings -n 8 firmware.bin | less
Then Ghidra (native PowerPC/ARM support; TriCore needs a community processor module -- check what's current) or IDA for actual disassembly/RE.
ip -details -statistics link show can0. Confirm bitrate (usually 500k on the OBD-II port) before assuming the interface or wiring is bad.
| Resource | What | Where |
|---|---|---|
| opengarages.org | Craig Smith's site; hosts the free Car Hacker's Handbook PDF, still the best single starting reference for this whole field | opengarages.org |
| DEF CON Car Hacking Village | Yearly talks/CTF, most content ends up on YouTube afterward | car-hacking-village.com |
| opendbc | Large real-world DBC file collection | github.com/commaai/opendbc |
| ICSim | Practice rig -- fake instrument cluster over vcan | github.com/zombieCraig/ICSim |
| SavvyCAN | GUI RE tool | github.com/collin80/SavvyCAN |
| can-utils | Upstream source for everything in the can-utils section | github.com/linux-can/can-utils |
| caringcaribou | UDS/XCP diagnostic framework | github.com/CaringCaribou/caringcaribou |
| python-udsoncan | UDS library stack | github.com/pylessard/python-udsoncan |
| python-can-isotp | ISO-TP transport layer for udsoncan | github.com/pylessard/python-can-isotp |
GitHub topic/search awesome-vehicle-security and awesome-car-hacking for continuously updated tool lists -- worth a periodic re-check, this space moves fast.