CAN Bus for Embedded Developers: Protocol Basics, Bit Timing, Arbitration, and Practical Implementation
CAN (Controller Area Network) is the backbone of automotive and industrial communication. Created by Bosch in the 1980s for vehicle networks, it has become the standard for reliable, real-time communication in harsh environments.
This guide provides embedded developers with practical knowledge: how CAN works, bit timing configuration, implementing CAN on microcontrollers, and debugging real-world CAN networks.
What is CAN Bus?
CAN is a multi-master, message-based serial communication protocol designed for:
- Automotive applications (engine control, ABS, airbags, infotainment)
- Industrial automation (PLCs, sensors, actuators)
- Medical devices (surgical equipment, patient monitors)
- Aerospace (avionics, flight control systems)
Key Features:
✅ Multi-master: Any node can transmit when bus is idle
✅ Message-based: No node addresses, messages identified by ID
✅ Priority arbitration: High-priority messages win bus access
✅ Error detection: 5 error detection mechanisms (CRC, ACK, bit monitoring, frame check, bit stuffing)
✅ Error confinement: Faulty nodes self-isolate from network
✅ Real-time: Deterministic latency for high-priority messages
✅ Robustness: Differential signaling, works in noisy environments
CAN Physical Layer
Differential Signaling:
CAN uses two wires: CAN_H (High) and CAN_L (Low).
Recessive bit (1):
- CAN_H = CAN_L = 2.5V
- Voltage difference = 0V
Dominant bit (0):
- CAN_H = 3.5V
- CAN_L = 1.5V
- Voltage difference = 2V
Why differential?
- Noise affects both wires equally (common-mode rejection)
- Long cable lengths (up to 40m @ 1 Mbps, 1000m @ 50 kbps)
- Works in electrically noisy environments (engines, motors, EMI)
CAN Transceiver:
Microcontrollers have CAN controllers (protocol handling), but need external transceivers to drive the physical bus.
Common transceivers:
- TJA1050 (NXP) - 1 Mbps, 3.3V/5V
- MCP2551 (Microchip) - 1 Mbps, 5V
- SN65HVD230 (TI) - 1 Mbps, 3.3V
- TCAN1051 (TI) - 5 Mbps (CAN FD capable)
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ MCU │ │ Transceiver │ │ CAN Bus │
│ │ TX──▶│ │ CANH ├────────────┤
│ CAN │ RX◀──│ TJA1050 │ CANL ├────────────┤
│ Controller │ │ │ │ │
└─────────────┘ └──────────────┘ └────────────┘
Bus Termination:
CAN bus requires 120Ω termination resistors at both ends to prevent signal reflections.
120Ω CAN_H 120Ω
┌────┐ ──────────── ┌────┐
│Node├─────────────────┬────┬─────────────────┤Node│
│ 1 │ │ │ │ N │
└────┘ │ │ └────┘
120Ω CAN_L 120Ω
Without termination: Signal reflections cause bit errors.
With termination: Clean signal edges, reliable communication.
CAN Message Frame
Standard Frame (CAN 2.0A):
┌───┬───┬───────────┬───┬───┬───┬────────┬───────┬─────┬───┬───┬───┐
│SOF│ │ ID │RTR│IDE│r0 │ DLC │ DATA │ CRC │ACK│EOF│IFS│
│ │ │ 11 bits │ │ │ │ 4 bits │0-8 B │15bits│ │ │ │
└───┴───┴───────────┴───┴───┴───┴────────┴───────┴─────┴───┴───┴───┘
Extended Frame (CAN 2.0B):
- 29-bit identifier (instead of 11-bit)
- Used when 2048 message IDs (11-bit) are insufficient
- Common in J1939 (heavy-duty vehicles)
Frame Fields:
| Field | Bits | Description |
|---|---|---|
| SOF | 1 | Start of Frame (dominant bit) |
| ID | 11/29 | Message identifier (priority) |
| RTR | 1 | Remote Transmission Request |
| IDE | 1 | Identifier Extension (0=standard, 1=extended) |
| DLC | 4 | Data Length Code (0-8 bytes) |
| DATA | 0-64 | Payload data |
| CRC | 15 | Cyclic Redundancy Check |
| ACK | 2 | Acknowledgment (receiver confirms) |
| EOF | 7 | End of Frame |
| IFS | 3 | Inter-Frame Space |
Arbitration: Priority-Based Bus Access
Key concept: Lower ID = Higher priority.
Non-Destructive Arbitration:
When multiple nodes transmit simultaneously, the node with the lowest ID wins without any frame loss.
Example:
Node A transmits ID 0x100 (binary: 00100000000)
Node B transmits ID 0x101 (binary: 00100000001)
Bit-by-bit:
Bit 10: Both send 0 (dominant) → Continue
Bit 9: Both send 0 (dominant) → Continue
...
Bit 1: Both send 0 (dominant) → Continue
Bit 0: A sends 0 (dominant), B sends 1 (recessive)
→ A's dominant bit overrides B's recessive bit
→ B detects mismatch, stops transmitting
→ A wins bus access and completes frame
Result:
- Node A (ID 0x100) wins
- Node B (ID 0x101) waits and retries
- No collision, no data loss
This is why CAN is deterministic: High-priority messages always get through quickly.
Bit Timing Configuration
Bit timing is the most critical and confusing part of CAN configuration. Get it wrong, and the bus won’t work at all.
Bit Timing Segments:
Each bit is divided into segments:
┌─────────┬────────┬─────────────┬─────────────┐
│ SYNC │ PROP │ PHASE1 │ PHASE2 │
│ 1 TQ │ 1-8TQ │ 1-8 TQ │ 1-8 TQ │
└─────────┴────────┴─────────────┴─────────────┘
▲ ▲
Sample Point Resync Point
- Time Quantum (TQ): Basic time unit
- SYNC_SEG: Synchronization segment (always 1 TQ)
- PROP_SEG: Propagation segment (compensates for bus delay)
- PHASE_SEG1: Phase buffer segment 1
- PHASE_SEG2: Phase buffer segment 2
- Sample Point: Where bit value is read (typically 75-87.5%)
Formula:
Bit Time = SYNC_SEG + PROP_SEG + PHASE_SEG1 + PHASE_SEG2
Bit Time = 1 TQ + PROP + PS1 + PS2
Baudrate = APB Clock / (Prescaler × Bit Time)
Example: 500 kbps CAN on STM32 with 42 MHz APB1 clock
Target: 500 kbps, 75% sample point
Calculation:
Desired Bit Time = 1 / 500 kbps = 2 µs
APB1 Clock = 42 MHz
Prescaler candidates: Try 6
TQ = Prescaler / APB1 Clock = 6 / 42 MHz = 142.86 ns
Bit Time in TQs = 2 µs / 142.86 ns = 14 TQ
Distribute TQs:
SYNC_SEG = 1 TQ (always)
PROP_SEG = 5 TQ
PHASE_SEG1 = 4 TQ
PHASE_SEG2 = 4 TQ
Total = 1 + 5 + 4 + 4 = 14 TQ ✓
Sample Point = (1 + 5 + 4) / 14 = 10 / 14 = 71.4% (close to 75%)
STM32 Configuration:
CAN_FilterConfTypeDef filter;
hcan.Instance = CAN1;
hcan.Init.Prescaler = 6;
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_9TQ; // PROP + PHASE1 = 5 + 4 = 9
hcan.Init.TimeSeg2 = CAN_BS2_4TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = ENABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE;
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
HAL_CAN_Init(&hcan);
Common Baudrates:
| Baudrate | Max Cable Length | Typical Use |
|---|---|---|
| 1 Mbps | 40 m | High-speed automotive (CAN-HS) |
| 500 kbps | 100 m | Standard automotive |
| 250 kbps | 250 m | Industrial automation |
| 125 kbps | 500 m | Building automation |
| 50 kbps | 1000 m | Long-distance industrial |
Rule of thumb: Baudrate × Cable Length ≈ Constant (40,000 for CAN)
CAN Filters
MCUs often receive hundreds of CAN messages. Filters allow accepting only relevant IDs, reducing interrupt load.
STM32 CAN Filter Example:
// Accept only ID 0x123
CAN_FilterTypeDef filter;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = (0x123 << 5); // Shift for 11-bit ID
filter.FilterIdLow = 0;
filter.FilterMaskIdHigh = (0x7FF << 5); // Match all bits
filter.FilterMaskIdLow = 0;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &filter);
// Accept range: IDs 0x100-0x10F
filter.FilterIdHigh = (0x100 << 5);
filter.FilterMaskIdHigh = (0x7F0 << 5); // Mask lower 4 bits
HAL_CAN_ConfigFilter(&hcan, &filter);
Sending and Receiving CAN Messages
Transmit (STM32 HAL):
#include "stm32f4xx_hal.h"
CAN_TxHeaderTypeDef txHeader;
uint8_t txData[8];
uint32_t txMailbox;
void CAN_Send_Example(void) {
txHeader.StdId = 0x123; // Message ID
txHeader.ExtId = 0;
txHeader.RTR = CAN_RTR_DATA; // Data frame (not remote)
txHeader.IDE = CAN_ID_STD; // Standard 11-bit ID
txHeader.DLC = 8; // 8 bytes of data
txHeader.TransmitGlobalTime = DISABLE;
// Prepare data
txData[0] = 0x01;
txData[1] = 0x02;
txData[2] = 0x03;
txData[3] = 0x04;
txData[4] = 0x05;
txData[5] = 0x06;
txData[6] = 0x07;
txData[7] = 0x08;
// Send message
if (HAL_CAN_AddTxMessage(&hcan, &txHeader, txData, &txMailbox) != HAL_OK) {
Error_Handler();
}
}
Receive (Interrupt-based):
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
// Receive message from FIFO0
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK) {
// Check message ID
if (rxHeader.StdId == 0x123 && rxHeader.IDE == CAN_ID_STD) {
// Process data
uint16_t value = (rxData[0] << 8) | rxData[1];
printf("Received: 0x%04X\n", value);
}
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_CAN_Init();
// Start CAN and enable interrupt
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
while (1) {
// Main loop
}
}
MCP2515: External CAN Controller
For MCUs without built-in CAN (Arduino, Raspberry Pi), use MCP2515 SPI-to-CAN controller.
Wiring:
Arduino/MCU MCP2515 CAN Transceiver
MOSI ────────▶ SI
MISO ◀──────── SO
SCK ────────▶ SCK
CS ────────▶ CS
INT ◀──────── INT
TX ────────▶ TXD
RX ◀──────── RXD
CANH ──┬── Bus
CANL ──┘
Arduino Example (using mcp2515 library):
#include <SPI.h>
#include <mcp2515.h>
MCP2515 mcp2515(10); // CS on pin 10
void setup() {
Serial.begin(115200);
SPI.begin();
mcp2515.reset();
mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ); // 500 kbps, 8 MHz crystal
mcp2515.setNormalMode();
Serial.println("CAN initialized");
}
void loop() {
// Send CAN message
struct can_frame txFrame;
txFrame.can_id = 0x123;
txFrame.can_dlc = 8;
txFrame.data[0] = 0x11;
txFrame.data[1] = 0x22;
txFrame.data[2] = 0x33;
txFrame.data[3] = 0x44;
txFrame.data[4] = 0x55;
txFrame.data[5] = 0x66;
txFrame.data[6] = 0x77;
txFrame.data[7] = 0x88;
mcp2515.sendMessage(&txFrame);
Serial.println("Message sent");
// Receive CAN message
struct can_frame rxFrame;
if (mcp2515.readMessage(&rxFrame) == MCP2515::ERROR_OK) {
Serial.print("ID: 0x");
Serial.print(rxFrame.can_id, HEX);
Serial.print(" Data: ");
for (int i = 0; i < rxFrame.can_dlc; i++) {
Serial.print(rxFrame.data[i], HEX);
Serial.print(" ");
}
Serial.println();
}
delay(1000);
}
CAN Error Detection and Handling
CAN has 5 error detection mechanisms:
1. CRC Check
- 15-bit CRC protects data and control fields
- Receiver recalculates CRC and compares
2. Frame Check
- Ensures frame format is valid
- Checks bit stuffing, fixed bits
3. ACK Error
- Transmitter expects at least one node to acknowledge
- If no ACK, frame is retried
4. Bit Monitoring
- Transmitter reads back what it sent
- Detects if a recessive bit was overwritten by dominant
5. Bit Stuffing
- After 5 consecutive identical bits, insert opposite bit
- Prevents DC bias and helps synchronization
Error States:
CAN nodes track error counters (Transmit Error Counter, Receive Error Counter):
| State | TEC/REC | Behavior |
|---|---|---|
| Error Active | < 128 | Normal operation, sends active error flags |
| Error Passive | 128-255 | Limited operation, sends passive error flags |
| Bus Off | > 255 | Node disconnects from bus (fail-safe) |
Self-healing: Nodes automatically recover when error count decreases.
CAN FD: Flexible Data Rate
CAN FD (ISO 11898-1:2015) improves classical CAN:
| Feature | Classical CAN | CAN FD |
|---|---|---|
| Max payload | 8 bytes | 64 bytes |
| Data phase speed | Same as arbitration | Up to 8 Mbps |
| Throughput | ~1 Mbps | ~10 Mbps (effective) |
| Backward compatible | N/A | No (requires CAN FD controller) |
Use cases:
- Automotive (ADAS cameras, radar, Ethernet gateway)
- Industrial (high-frequency sensor data)
- Software updates (faster flashing)
CAN FD Frame:
Arbitration Phase: 500 kbps (compatible with classical CAN)
Data Phase: 2-8 Mbps (faster transmission)
Benefit: 8x more data per frame, faster overall throughput.
Debugging CAN Bus
1. Hardware Tools:
CAN Analyzers:
- PEAK PCAN-USB ($150) - Professional tool, Windows/Linux
- CANable ($30) - Open-source USB-CAN adapter
- Kvaser Leaf Light ($400) - Industrial-grade
2. Software Tools:
CANalyzer / CANoe (Vector) - Industry standard, expensive
Wireshark with SocketCAN (Linux) - Free
PCAN-View (PEAK) - Free with PCAN hardware
Busmaster (Open-source) - Free Windows tool
3. Wireshark with SocketCAN (Linux):
# Load SocketCAN kernel module
sudo modprobe can
sudo modprobe can_raw
sudo modprobe vcan
# Create virtual CAN interface for testing
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
# Or use real hardware (e.g., CANable on slcan)
sudo slcand -o -c -s6 /dev/ttyUSB0 can0
sudo ip link set can0 up
# Capture with Wireshark
wireshark -i can0
# Or use candump
candump can0
4. Common Issues:
| Problem | Symptom | Solution |
|---|---|---|
| No termination | Bus errors, intermittent | Add 120Ω resistors at both ends |
| Wrong bit timing | No communication | Recalculate bit timing, verify clock |
| Mixed baudrates | ACK errors | Ensure all nodes use same baudrate |
| Bus-Off state | Node stops transmitting | Check error counters, reset controller |
| EMI/noise | Sporadic errors | Twisted pair cable, shielding, ferrite beads |
Real-World CAN Protocols
1. CANopen (Industrial Automation)
- Standard: CEN EN 50325-4
- Use case: Industrial machinery, robotics
- Features: Object dictionary, SDO (Service Data Objects), PDO (Process Data Objects), NMT (Network Management)
2. J1939 (Heavy-Duty Vehicles)
- Standard: SAE J1939
- Use case: Trucks, buses, agricultural equipment
- Features: 29-bit extended IDs, Parameter Group Numbers (PGN), multi-packet transport
Example J1939 ID:
29-bit ID breakdown:
- Priority (3 bits)
- Reserved (1 bit)
- Data Page (1 bit)
- PDU Format (8 bits)
- PDU Specific (8 bits)
- Source Address (8 bits)
3. OBD-II (Automotive Diagnostics)
- Standard: ISO 15765-4 (CAN)
- Use case: Vehicle diagnostics, emission testing
- IDs: 0x7DF (request), 0x7E8-0x7EF (response)
Example:
// Request engine RPM (PID 0x0C)
txData[0] = 0x02; // Length
txData[1] = 0x01; // Mode 1 (current data)
txData[2] = 0x0C; // PID 0x0C (engine RPM)
Practical Use Cases
Example 1: Engine Control Unit (ECU) Communication
Network:
- Engine ECU: Transmits RPM, temperature, throttle position
- Dashboard: Displays values
- Transmission ECU: Adjusts shift points based on RPM
Message IDs (priority-based):
- 0x100 (highest priority): Critical engine fault
- 0x200: Engine RPM (10 ms interval)
- 0x201: Coolant temperature (100 ms interval)
- 0x300: Throttle position (20 ms interval)
Example 2: Industrial Sensor Network
Setup:
- 10 temperature sensors on CAN bus
- Each sensor has unique ID
- PLC collects data and controls HVAC
Python CAN with python-can library:
import can
import time
# Initialize CAN bus
bus = can.interface.Bus(channel='can0', bustype='socketcan')
# Send request to sensor 5
msg = can.Message(arbitration_id=0x105, data=[0x01], is_extended_id=False)
bus.send(msg)
# Receive response
response = bus.recv(timeout=1.0)
if response:
temp = (response.data[0] << 8) | response.data[1]
temp_celsius = temp / 10.0
print(f"Sensor 5 temperature: {temp_celsius}°C")
Conclusion
CAN bus is essential for:
- Automotive electronics (OBD-II, ECUs, body control)
- Industrial automation (PLCs, distributed I/O)
- Medical devices (patient monitoring, surgical equipment)
- Aerospace (avionics, flight control)
Key takeaways:
- Understand arbitration (lower ID = higher priority)
- Configure bit timing correctly (use online calculators if needed)
- Use filters to reduce interrupt load
- Add 120Ω termination resistors
- Debug with CAN analyzers (PEAK, CANable, Wireshark)
For your next CAN project:
- Choose a microcontroller with built-in CAN (STM32, ESP32-C6) or use MCP2515
- Add a CAN transceiver (TJA1050, SN65HVD230)
- Calculate bit timing for your target baudrate
- Configure filters for relevant message IDs
- Test with a CAN analyzer before deploying
Further Reading
- Communication Protocols and Interfaces Explained
- OBD-II Protocol Explained
- ISO 11898-1 CAN Specification
- Kvaser CAN Bus Tutorial
- CAN Bit Timing Calculator
Are you using CAN bus in your projects? What challenges have you faced? Share your experience in the comments.