candump
Displays received CAN frames in real time. Supports filtering by ID and masking. Part of can-utils.
candump can0,7E8:7F0OBD-II (On-Board Diagnostics, second generation) is the most widely deployed CAN bus application on the planet — every passenger vehicle sold in the US since 1996 and in the EU since 2001 carries an OBD-II port. Under the hood, OBD-II is just CAN with a set of standardized message IDs and data formats defined by ISO 15765-4 and SAE J1979. The RS485 CAN HAT speaks the same electrical protocol, which means a Raspberry Pi can participate directly on a vehicle’s diagnostic CAN bus.
This guide covers two complementary use cases:
Both modes use the same wiring and interface setup described in the CAN Bus Usage guide. The difference is entirely in the CAN message content.
Every OBD-II-compliant vehicle exposes a standardized 16-pin diagnostic link connector (DLC) defined by SAE J1962. It is usually located under the dashboard on the driver’s side. Of the 16 pins, only a handful are relevant for CAN bus communication:
| Pin | Signal | Description |
|---|---|---|
| 4 | Chassis Ground | Signal ground reference |
| 5 | Signal Ground | CAN and protocol ground |
| 6 | CAN High (CAN-H) | ISO 15765-4, dominant-high line |
| 14 | CAN Low (CAN-L) | ISO 15765-4, dominant-low line |
| 16 | Battery Positive | Permanent +12V from vehicle battery |
Pins 6 and 14 carry the CAN differential pair. Pin 5 provides the signal ground reference. The remaining pins serve legacy protocols (ISO 9141, J1850) that predate CAN-based OBD-II and are not used here.
The connection between the RS485 CAN HAT and a vehicle OBD-II port requires exactly three wires:
| HAT Terminal | OBD-II Pin | Signal |
|---|---|---|
| H | Pin 6 | CAN High |
| L | Pin 14 | CAN Low |
| GND (Pi ground) | Pin 5 | Signal Ground |
You can solder wires directly into an OBD-II breakout connector, or use a pre-made OBD-II pigtail cable with bare wire ends. Keep leads short and twisted where possible — CAN is a differential bus and benefits from balanced line impedance.
A common concern is whether the SN65HVD230 transceiver on the RS485 CAN HAT is compatible with automotive CAN buses. The short answer: yes.
The SN65HVD230 is an ISO 11898-2 compliant CAN transceiver designed for 3.3V operation. ISO 11898-2 is the same physical layer standard that automotive CAN uses. The differential voltage levels on the bus are defined by the standard (dominant: CANH ≈ 3.5V, CANL ≈ 1.5V; recessive: both ≈ 2.5V), and these levels are independent of the transceiver’s supply voltage. A 3.3V transceiver and a 5V transceiver on the same bus produce and interpret identical differential signals — that is the entire point of a differential bus standard.
The misconception that automotive CAN requires a “5V transceiver” likely stems from the fact that many automotive CAN controllers (like those inside ECUs) run from a 5V supply. But the transceiver’s job is to convert between the controller’s logic levels and the bus’s differential levels, and the bus levels are defined by the standard, not by any individual transceiver’s VCC.
OBD-II over CAN uses one of two bitrates:
There is no way to know which rate a vehicle uses without trying. The most reliable approach is to bring up the interface at 500 kbps and check for traffic:
Bring up the interface at 500 kbps.
sudo ip link set can0 up type can bitrate 500000Listen for traffic. Vehicles with active systems (ABS, traction control, HVAC) generate periodic CAN traffic even with the engine off, as long as the ignition is on.
candump can0Check for errors. If the bitrate is wrong, the MCP2515 will report framing errors instead of valid frames. Check with:
ip -details -statistics link show can0Look at the bus-error and restarts counters. If they are climbing rapidly, the bitrate is wrong.
Try 250 kbps if 500 didn’t work.
sudo ip link set can0 downsudo ip link set can0 up type can bitrate 250000candump can0Once you see clean frames in candump, you have the correct bitrate.
OBD-II uses a simple request-response model over CAN. A diagnostic tool sends a request frame, and the vehicle’s ECU replies with a response frame.
There are two addressing modes:
Response frames come back on CAN IDs 0x7E8 through 0x7EF (the request ID plus 8). The primary engine ECU typically responds on 0x7E8.
An OBD-II request frame is always 8 bytes. The first byte is the number of additional data bytes, the second byte is the mode (service), and the third byte (when applicable) is the PID (parameter ID).
Byte: [0] [1] [2] [3..7] Len Mode PID Padding (0x55 or 0x00)For example, to request engine RPM (Mode 0x01, PID 0x0C):
02 01 0C 00 00 00 00 00The 02 means “2 bytes of data follow” (the mode and PID).
The ECU responds with a frame where the mode byte has 0x40 added to it:
Byte: [0] [1] [2] [3..N] [N+1..7] Len Mode+40 PID Value PaddingFor engine RPM, the response looks like:
04 41 0C 1A F8 00 00 00Here 41 confirms it is a response to Mode 0x01, 0C echoes the PID, and 1A F8 is the RPM value. RPM is encoded as (A * 256 + B) / 4, so (0x1A * 256 + 0xF8) / 4 = 1726 RPM.
| PID | Name | Formula | Unit |
|---|---|---|---|
| 0x00 | Supported PIDs (01-20) | Bitmask | — |
| 0x05 | Coolant Temperature | A - 40 | °C |
| 0x0C | Engine RPM | (A×256 + B) / 4 | rpm |
| 0x0D | Vehicle Speed | A | km/h |
| 0x0F | Intake Air Temperature | A - 40 | °C |
| 0x11 | Throttle Position | A × 100 / 255 | % |
| 0x1F | Run Time Since Start | A×256 + B | seconds |
| 0x2F | Fuel Tank Level | A × 100 / 255 | % |
PID 0x00 is special — it returns a 4-byte bitmask indicating which of PIDs 0x01 through 0x20 are supported by the ECU. This is the standard way to discover what a vehicle supports before requesting specific data.
In scan tool mode, the Pi sends OBD-II request frames and decodes the responses. This is the classic “read your car’s data” use case.
The fastest way to verify connectivity is with cansend and candump. Open two terminals:
Terminal 1 — listen for responses:
candump can0,7E8:7F0The filter 7E8:7F0 passes CAN IDs 0x7E8 through 0x7EF (all standard OBD-II response IDs).
Terminal 2 — request engine RPM:
cansend can0 7DF#02010C0000000000In the first terminal, you should see a response like:
can0 7E8 [8] 04 41 0C 1A F8 00 00 00Decode manually: bytes 3-4 are 0x1A and 0xF8. RPM = (26 × 256 + 248) / 4 = 1726 RPM.
For real-time monitoring of changing values, cansniffer highlights bytes that change between frames:
cansniffer can0 -cThis is particularly useful for reverse-engineering proprietary CAN messages beyond the standard OBD-II PIDs.
This approach uses python-can to construct OBD-II requests directly. It gives full control over the CAN framing and is the right choice when you need to work with non-standard PIDs or proprietary extensions.
import canimport struct
bus = can.interface.Bus(channel='can0', bustype='socketcan')
def request_pid(pid, mode=0x01): """Send an OBD-II request and return the raw response bytes.""" msg = can.Message( arbitration_id=0x7DF, data=[0x02, mode, pid, 0x00, 0x00, 0x00, 0x00, 0x00], is_extended_id=False, ) bus.send(msg)
# Wait for response on 0x7E8-0x7EF while True: resp = bus.recv(timeout=2.0) if resp is None: return None if 0x7E8 <= resp.arbitration_id <= 0x7EF: return resp.data
# Engine RPM (PID 0x0C)data = request_pid(0x0C)if data: rpm = (data[3] * 256 + data[4]) / 4 print(f"RPM: {rpm:.0f}")
# Coolant temperature (PID 0x05)data = request_pid(0x05)if data: temp_c = data[3] - 40 print(f"Coolant: {temp_c} °C")
# Vehicle speed (PID 0x0D)data = request_pid(0x0D)if data: speed = data[3] print(f"Speed: {speed} km/h")
bus.shutdown()The python-obd library abstracts away the CAN framing entirely. It handles PID discovery, unit conversion, and multi-frame responses automatically. This is the faster path to a working dashboard, but it requires an ELM327-compatible interface or a SocketCAN adapter.
pip3 install obdimport obd
# Connect via SocketCANconnection = obd.OBD("/dev/pts/0") # or use ELM327 adapter path
# Query live datarpm_resp = connection.query(obd.commands.RPM)print(f"RPM: {rpm_resp.value}")
speed_resp = connection.query(obd.commands.SPEED)print(f"Speed: {speed_resp.value}")
coolant_resp = connection.query(obd.commands.COOLANT_TEMP)print(f"Coolant: {coolant_resp.value}")
# List all supported commandsfor cmd in connection.supported_commands: print(cmd.name)
connection.close()Note that python-obd expects an ELM327-style serial interface by default. Using it directly over SocketCAN requires additional configuration or a bridge like slcand. For raw SocketCAN access, the python-can approach in the other tab is more straightforward.
In emulator mode, the Pi listens for incoming OBD-II requests and responds with simulated data. This is invaluable for bench-testing scan tools, OBD-II dashboards, and data loggers without needing access to a vehicle.
The following script responds to standard Mode 0x01 PID requests on the functional address (0x7DF) and the primary physical address (0x7E0), replying on 0x7E8:
import canimport timeimport math
bus = can.interface.Bus(channel='can0', bustype='socketcan')
def simulated_values(): """Generate plausible engine data that varies over time.""" t = time.time() rpm = 800 + 400 * math.sin(t * 0.5) # Idle oscillation speed = max(0, 60 + 30 * math.sin(t * 0.2)) # Gentle cruising coolant = 85 + 5 * math.sin(t * 0.05) # Warm engine throttle = 15 + 10 * math.sin(t * 0.3) # Light throttle intake_temp = 25 + 3 * math.sin(t * 0.1) # Ambient drift return rpm, speed, coolant, throttle, intake_temp
def build_response(pid): """Build an OBD-II response for a given Mode 0x01 PID.""" rpm, speed, coolant, throttle, intake_temp = simulated_values()
if pid == 0x00: # Supported PIDs bitmask: 0x05, 0x0C, 0x0D, 0x0F, 0x11 # Bit positions (from PID 0x01): 05=bit27, 0C=bit20, 0D=bit19, 0F=bit17, 11=bit15 bitmask = 0x08198000 b = bitmask.to_bytes(4, 'big') return [0x06, 0x41, 0x00, b[0], b[1], b[2], b[3], 0x00]
elif pid == 0x05: val = int(coolant + 40) return [0x03, 0x41, 0x05, val, 0x00, 0x00, 0x00, 0x00]
elif pid == 0x0C: encoded = int(rpm * 4) a = (encoded >> 8) & 0xFF b = encoded & 0xFF return [0x04, 0x41, 0x0C, a, b, 0x00, 0x00, 0x00]
elif pid == 0x0D: return [0x03, 0x41, 0x0D, int(speed), 0x00, 0x00, 0x00, 0x00]
elif pid == 0x0F: val = int(intake_temp + 40) return [0x03, 0x41, 0x0F, val, 0x00, 0x00, 0x00, 0x00]
elif pid == 0x11: val = int(throttle * 255 / 100) return [0x03, 0x41, 0x11, val, 0x00, 0x00, 0x00, 0x00]
return None
print("ECU emulator running — listening for OBD-II requests...")
while True: msg = bus.recv(timeout=1.0) if msg is None: continue
if msg.arbitration_id not in (0x7DF, 0x7E0): continue
if msg.data[1] != 0x01: continue
pid = msg.data[2] response_data = build_response(pid)
if response_data: resp = can.Message( arbitration_id=0x7E8, data=response_data, is_extended_id=False, ) bus.send(resp)You can test the emulator entirely in software using a virtual CAN interface. The vcan kernel module creates a loopback CAN bus that requires no physical hardware:
sudo modprobe vcansudo ip link add dev vcan0 type vcansudo ip link set up vcan0Change channel='can0' to channel='vcan0' in the emulator script, then open a second terminal and issue requests:
# In terminal 1: run the emulator on vcan0python3 ecu_emulator.py
# In terminal 2: request RPMcansend vcan0 7DF#02010C0000000000candump vcan0,7E8:7FFThis is an excellent way to develop and debug OBD-II applications without a vehicle or even the CAN HAT itself.
Some OBD-II responses exceed the 8-byte CAN frame limit — most notably the Vehicle Identification Number (VIN, Mode 0x09 PID 0x02) and diagnostic trouble code lists (Mode 0x03). These use ISO 15765-2 (ISO-TP), a transport protocol that segments long messages across multiple CAN frames.
The can-isotp kernel module provides ISO-TP support at the socket level:
sudo modprobe can-isotpOnce loaded, you can use isotpsend and isotprecv from can-utils to handle multi-frame transfers:
# Receive ISO-TP messages from ECU (tx=7E0, rx=7E8)isotprecv -s 7E0 -d 7E8 can0
# Send an ISO-TP request (in another terminal)echo "09 02" | isotpsend -s 7E0 -d 7E8 can0OBD-II Mode 0x03 retrieves stored diagnostic trouble codes from the ECU. These are the codes that cause the “check engine” light to illuminate.
Send a Mode 0x03 request (no PID needed):
cansend can0 7DF#0103000000000000The response contains pairs of DTC bytes. Each 2-byte pair encodes one trouble code:
Byte pair: [High nibble] [Low 3 nibbles] ↓ ↓ Category + digit Remaining digitsThe high 2 bits of the first byte determine the category:
| Bits | Category | Prefix |
|---|---|---|
| 00 | Powertrain | P0xxx |
| 01 | Chassis | C0xxx |
| 10 | Body | B0xxx |
| 11 | Network | U0xxx |
For example, the bytes 01 07 decode as:
0x01 = 00 → P (Powertrain)0 1 0 7 → P0107 (Manifold Absolute Pressure sensor circuit low)Mode 0x04 clears stored DTCs and resets the check engine light:
cansend can0 7DF#0104000000000000candump
Displays received CAN frames in real time. Supports filtering by ID and masking. Part of can-utils.
candump can0,7E8:7F0cansend
Sends a single CAN frame. The format is ID#data with bytes separated by dots or concatenated as hex.
cansend can0 7DF#02010C0000000000cansniffer
Shows real-time changes in CAN traffic. Highlights bytes that differ between consecutive frames — useful for identifying which bytes correspond to changing sensor values.
cansniffer can0 -cisotpsend / isotprecv
Send and receive ISO-TP (multi-frame) messages. Required for VIN requests, DTC reads, and any response longer than 7 data bytes. Requires the can-isotp kernel module.
isotprecv -s 7E0 -d 7E8 can0python-can
Python library for SocketCAN. Provides a clean interface for sending and receiving CAN frames, bus management, and message filtering. Install with pip3 install python-can.
python-obd
High-level Python library that handles OBD-II PID encoding, decoding, and unit conversion. Best suited for applications that use an ELM327 adapter or need a quick path to reading vehicle data. Install with pip3 install obd.