J1939 CAN FD Extensions, Implementation Walkthroughs, and Tooling
Document ID: J19-03 Series: Telematics Tutorial Series Target audience: Advanced — you should understand J1939 addressing and transport protocols (J19-01, J19-02) and have experience with Linux networking or embedded CAN development. Prerequisites: For J1939 fundamentals, see J19-01. For J1939 transport protocols, see J19-02. For CAN FD frame format, see CAN-02. For CAN FD socketCAN code, see CAN-04.
Learning Objectives
By the end of this document, you will be able to:
- Describe the Society of Automotive Engineers (SAE) J1939-22 extensions for CAN Flexible Data-rate (CAN FD) and how they affect frame formats and transport
- Configure and use the Linux socketCAN J1939 kernel module for network-layer J1939 communication
- Implement basic J1939 communication using the Open-SAE-J1939 open-source library on an embedded platform
- Select and configure J1939 analysis tools (PEAK PCAN-Explorer, Kvaser CANKing, SavvyCAN, can-utils)
- Develop a complete J1939 data logging application
1. J1939-22: CAN FD Extensions
SAE J1939-22 extends J1939 to support Controller Area Network (CAN) FD frames. Classical J1939 is limited to 8-byte CAN frames at 250 kbit/s (or 500 kbit/s with J1939-14). J1939-22 enables 64-byte payloads and higher data rates.
1.1 Key Changes in J1939-22
| Feature | J1939 (Classical CAN) | J1939-22 (CAN FD) |
|---|---|---|
| Max data bytes per frame | 8 | 64 |
| Bit rate | 250 or 500 kbit/s | Arbitration: 250/500 kbit/s, Data: up to 5 Mbit/s (commonly 2 Mbit/s in heavy-duty profiles) |
| Transport protocol needed for | Messages > 8 bytes | Messages > 64 bytes |
| Parameter Group Number (PGN) definitions | Same | Extended to use larger payloads |
| Physical layer | J1939-11 / International Organization for Standardization (ISO) 11898-2 | ISO 11898-2 transceiver rated for data-phase bit rate |
1.2 Impact on Transport Protocols
With 64-byte CAN FD frames, many messages that previously required Broadcast Announce Message (BAM) or Connection Mode Data Transfer (CMDT) transport now fit in a single frame:
- DM1 with up to 15 Diagnostic Trouble Codes (DTC) fits in 62 bytes (2 lamp + 15 × 4 bytes)
- Most Parameter Group definitions (8 bytes) fit trivially
- Only very large messages (configuration data, firmware) still need transport protocols
J1939-21 defines two transport protocol tiers:
- Transport Protocol (TP): Handles messages from 9 to 1785 bytes using BAM or CMDT. With CAN FD, the TP threshold shifts — messages from 65 to 1785 bytes require TP.
- Extended Transport Protocol (ETP): Handles messages from 1786 to 117,440,505 bytes (approximately 117 MB). ETP is used for firmware uploads, large configuration transfers, and diagnostic memory reads. ETP remains necessary even with CAN FD for messages exceeding 1785 bytes.
📝 Note: The Linux socketCAN J1939 module handles both TP and ETP reassembly automatically. The maximum buffer size for TP is 1785 bytes; for ETP it is 117,440,505 bytes.
1.3 CAN FD / Classical CAN Coexistence
Deploying J1939-22 on an existing classical J1939 network requires careful planning:
- A classical CAN node on a CAN FD bus will generate error frames when it encounters the FDF (FD Format) bit in a CAN FD frame, because the classical CAN controller interprets the FDF bit position as a form error. This can drive the classical node — and potentially the entire bus segment — into bus-off.
- All nodes on a physical bus segment must support CAN FD before any node sends CAN FD frames. There is no graceful degradation.
- Gateways are required for mixed networks. J1939-22 defines a migration architecture using separate CAN FD and classical CAN bus segments connected by a gateway Electronic Control Unit (ECU) that translates between frame formats.
- Phased migration strategy: (1) upgrade all ECUs on a bus segment to CAN FD-capable hardware, (2) enable CAN FD on the segment, (3) repeat for remaining segments. The gateway bridges segments during the transition.
⚠️ Warning: Do not enable CAN FD (`fd on`) on a bus segment that contains any classical CAN-only node. The classical node will generate error frames that disrupt communication for all nodes on the segment.
📝 Note: A CAN FD bus can carry both classical CAN frames and CAN FD frames simultaneously. Classical-only nodes will detect CAN FD frames as errors (due to the FDF bit) — this is why the coexistence strategy in Section 1.3 requires careful migration planning. All nodes on a bus segment must be CAN FD-capable before any node transmits CAN FD frames.
1.4 CAN FD Frame Format in J1939
The 29-bit CAN identifier structure is unchanged — Priority, Extended Data Page (EDP), Data Page (DP), PDU Format (PF), PDU Specific (PS), and Source Address (SA) remain the same. The only difference is the CAN FD frame’s larger data field and optional higher data-phase bit rate.
📝 Note: J1939-22 adoption is still in early stages as of 2026. Most heavy-duty vehicle networks continue to use classical CAN at 250 kbit/s. New platforms (particularly off-highway and agricultural equipment) are the first adopters of J1939-22.
1.5 CAN FD DLC Encoding — Non-Linear Mapping
CAN FD extends the Data Length Code to support payloads larger than 8 bytes. For DLC values 0–8, the mapping is linear (DLC = data length). For DLC 9–15, the mapping is non-linear:
| DLC Code | Data Length (bytes) | Hex Data Length |
|---|---|---|
| 0 | 0 | 0x00 |
| 1 | 1 | 0x01 |
| 2 | 2 | 0x02 |
| 3 | 3 | 0x03 |
| 4 | 4 | 0x04 |
| 5 | 5 | 0x05 |
| 6 | 6 | 0x06 |
| 7 | 7 | 0x07 |
| 8 | 8 | 0x08 |
| 9 | 12 | 0x0C |
| 10 | 16 | 0x10 |
| 11 | 20 | 0x14 |
| 12 | 24 | 0x18 |
| 13 | 32 | 0x20 |
| 14 | 48 | 0x30 |
| 15 | 64 | 0x40 |
Note the gaps: there is no DLC code for 9, 10, 11, 13–15, 17–19, 21–23, 25–31, 33–47, or 49–63 bytes. When sending data that doesn’t exactly fill a DLC level, pad to the next available size.
# CAN FD DLC encoding/decoding
DLC_TO_LENGTH = {
0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8,
9: 12, 10: 16, 11: 20, 12: 24, 13: 32, 14: 48, 15: 64
}
LENGTH_TO_DLC = {v: k for k, v in DLC_TO_LENGTH.items()}
def dlc_to_length(dlc: int) -> int:
"""Convert DLC code to data length in bytes."""
return DLC_TO_LENGTH.get(dlc, 0)
def length_to_dlc(length: int) -> int:
"""Find the minimum DLC code that can carry 'length' bytes.
Rounds up to the next available DLC level.
"""
for dlc in range(16):
if DLC_TO_LENGTH[dlc] >= length:
return dlc
raise ValueError(f"Data length {length} exceeds CAN FD maximum (64 bytes)")
# Verification
print("DLC → Length mapping:")
for dlc in range(16):
length = dlc_to_length(dlc)
print(f" DLC {dlc:2d} → {length:2d} bytes")
print("\nLength → DLC (with padding):")
for length in [1, 8, 9, 10, 16, 17, 24, 25, 32, 48, 64]:
dlc = length_to_dlc(length)
actual = dlc_to_length(dlc)
pad = actual - length
print(f" {length:2d} bytes → DLC {dlc:2d} ({actual} bytes, {pad} padding)")
Expected output:
DLC → Length mapping:
DLC 0 → 0 bytes
DLC 1 → 1 bytes
DLC 2 → 2 bytes
DLC 3 → 3 bytes
DLC 4 → 4 bytes
DLC 5 → 5 bytes
DLC 6 → 6 bytes
DLC 7 → 7 bytes
DLC 8 → 8 bytes
DLC 9 → 12 bytes
DLC 10 → 16 bytes
DLC 11 → 20 bytes
DLC 12 → 24 bytes
DLC 13 → 32 bytes
DLC 14 → 48 bytes
DLC 15 → 64 bytes
Length → DLC (with padding):
1 bytes → DLC 1 (1 bytes, 0 padding)
8 bytes → DLC 8 (8 bytes, 0 padding)
9 bytes → DLC 9 (12 bytes, 3 padding)
10 bytes → DLC 9 (12 bytes, 2 padding)
16 bytes → DLC 10 (16 bytes, 0 padding)
17 bytes → DLC 11 (20 bytes, 3 padding)
24 bytes → DLC 12 (24 bytes, 0 padding)
25 bytes → DLC 13 (32 bytes, 7 padding)
32 bytes → DLC 13 (32 bytes, 0 padding)
48 bytes → DLC 14 (48 bytes, 0 padding)
64 bytes → DLC 15 (64 bytes, 0 padding)
2. Linux socketCAN J1939 Module
Linux kernel 5.4+ includes a native J1939 protocol module that provides a socket-based Application Programming Interface (API) for J1939 communication. This module operates at the J1939 network layer — it handles address claiming, PGN-based addressing, and transport protocols automatically.
graph BT
subgraph HW["Hardware Layer"]
PHY["CAN Transceiver + Physical Bus
(J1939-11 / ISO 11898-2)"]
CAN_HW["CAN Controller Hardware
━━━━━━━━━━━━━━━━━━
SJA1000 | MCP251xFD
STM32 bxCAN/FDCAN | PEAK USB"]
PHY --> |"Differential
CAN_H / CAN_L"| CAN_HW
end
subgraph KDRV["Kernel CAN Driver"]
DRV["CAN Hardware Driver
━━━━━━━━━━━━━━━━━━
peak_usb, mcp251xfd,
gs_usb, kvaser_usb, stm32_can"]
end
subgraph SCAN["socketCAN Subsystem (net/can/)"]
direction LR
RAW["CAN Raw Protocol
(can_raw)
AF_CAN, CAN_RAW"]
BCM["CAN BCM Protocol
(can_bcm)
AF_CAN, CAN_BCM"]
end
subgraph J1939["J1939 Protocol Module (net/can/j1939/)"]
direction TB
subgraph J1939_TOP[" "]
direction LR
AC["Address Claiming (AC)
━━━━━━━━━━━━━━━━
PGN 60928
NAME comparison
EADDRINUSE on conflict"]
TP["Transport Protocol (TP)
━━━━━━━━━━━━━━━━
BAM / CMDT reassembly
1785-byte max (TP)
750ms T1 / 1250ms T2"]
end
subgraph J1939_BOT[" "]
direction LR
PGN_R["PGN Routing
━━━━━━━━━━━━━━━━
Demux by PGN + SA
to bound sockets"]
FLOW["Priority & Flow Control
━━━━━━━━━━━━━━━━
TP.CM_CTS pacing
Sequence numbering"]
end
end
SYSCALL["━━━ System Call Boundary ━━━
socket() / bind() / sendto() / recvfrom()
socket(PF_CAN, SOCK_DGRAM, CAN_J1939)"]
subgraph USPACE["User Space Applications"]
direction LR
APP_C["C/C++ Application
━━━━━━━━━━━━━━━━
Direct J1939
socket calls"]
APP_PY["python-j1939
python-can
━━━━━━━━━━━━━━━━
High-level
Python wrapper"]
APP_UTIL["can-utils J1939
━━━━━━━━━━━━━━━━
j1939acd
j1939cat
j1939spy"]
end
CAN_HW -->|"IRQ / DMA / USB"| DRV
DRV -->|"struct can_frame
via netdev_rx()"| RAW
DRV -->|"struct can_frame
via netdev_rx()"| BCM
RAW -->|"Filtered
CAN frames"| AC
RAW -->|"Filtered
CAN frames"| TP
BCM -->|"Filtered
CAN frames"| PGN_R
AC --> PGN_R
TP --> PGN_R
FLOW --> TP
PGN_R -->|"Reassembled J1939 msgs
(up to 1785 bytes)
PGN + SA metadata"| SYSCALL
SYSCALL -->|"PGN-addressed
datagrams"| APP_C
SYSCALL -->|"PGN-addressed
datagrams"| APP_PY
SYSCALL -->|"PGN-addressed
datagrams"| APP_UTIL
style PHY fill:#555,stroke:#333,color:#fff
style CAN_HW fill:#666,stroke:#333,color:#fff
style DRV fill:#4a7fb5,stroke:#333,color:#fff
style RAW fill:#5a8fc5,stroke:#333,color:#fff
style BCM fill:#5a8fc5,stroke:#333,color:#fff
style AC fill:#d4a030,stroke:#333,color:#000
style TP fill:#d4a030,stroke:#333,color:#000
style PGN_R fill:#d4a030,stroke:#333,color:#000
style FLOW fill:#d4a030,stroke:#333,color:#000
style SYSCALL fill:#ff6b6b,stroke:#c00,color:#fff
style APP_C fill:#6abf69,stroke:#333,color:#000
style APP_PY fill:#6abf69,stroke:#333,color:#000
style APP_UTIL fill:#6abf69,stroke:#333,color:#000
Figure: J1903 01 linux j1939 stack
2.1 Loading the J1939 Module
# Load the J1939 module
sudo modprobe can_j1939
# Configure the CAN interface for classical J1939 (250 kbit/s)
sudo ip link set can0 type can bitrate 250000
sudo ip link set up can0
For J1939-22 CAN FD, configure the interface with a data-phase bit rate and enable FD mode:
# Configure for J1939-22 CAN FD (250 kbit/s arbitration, 2 Mbit/s data phase)
sudo ip link set can0 type can bitrate 250000 dbitrate 2000000 fd on
sudo ip link set up can0
📝 Note: The `dbitrate` and `fd on` parameters require a CAN FD-capable interface (e.g., PEAK PCAN-USB FD, Kvaser Leaf Pro HS v2). Classical CAN interfaces will reject these parameters.
2.2 Sending and Receiving with j1939 Sockets (C)
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/j1939.h>
int main(void) {
int sock;
struct sockaddr_can addr;
/* Create a J1939 socket */
sock = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
if (sock < 0) {
perror("socket");
return 1;
}
/* Bind to can0, source address 0x80, all PGNs */
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = if_nametoindex("can0");
if (addr.can_ifindex == 0) {
fprintf(stderr, "Interface can0 not found\n");
close(sock);
return 1;
}
addr.can_addr.j1939.name = J1939_NO_NAME;
addr.can_addr.j1939.addr = 0x80; /* Source address */
addr.can_addr.j1939.pgn = J1939_NO_PGN; /* Receive all PGNs */
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(sock);
return 1;
}
/* Send PGN 65262 (Engine Temperature 1) */
struct sockaddr_can dest;
memset(&dest, 0, sizeof(dest));
dest.can_family = AF_CAN;
dest.can_ifindex = addr.can_ifindex;
dest.can_addr.j1939.name = J1939_NO_NAME;
dest.can_addr.j1939.addr = J1939_NO_ADDR; /* Broadcast */
dest.can_addr.j1939.pgn = 0xFEEE; /* PGN 65262 */
uint8_t data[8] = {0x80, 0xC8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
/* Byte 0 (Suspect Parameter Number (SPN) 110): Engine Coolant Temp = 0x80 → (128-40) = 88°C */
/* Byte 1 (SPN 174): Fuel Temp = 0xC8 → (200-40) = 160°C */
if (sendto(sock, data, 8, 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
perror("sendto");
close(sock);
return 1;
}
printf("Sent PGN 0xFEEE (Engine Temperature 1)\n");
/* Receive a J1939 message (blocks until data arrives) */
uint8_t buf[1785]; /* Max TP message size (ETP supports up to 117,440,505 bytes) */
struct sockaddr_can src;
socklen_t src_len = sizeof(src);
ssize_t nbytes = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&src, &src_len);
if (nbytes > 0) {
printf("Received PGN 0x%05X from SA 0x%02X, %zd bytes\n",
src.can_addr.j1939.pgn,
src.can_addr.j1939.addr,
nbytes);
}
close(sock);
return 0;
}
Compile with: gcc -o j1939_example j1939_example.c
Expected output:
Sent PGN 0xFEEE (Engine Temperature 1)
Received PGN 0x0F004 from SA 0x00, 8 bytes
The key difference from raw CAN sockets: you work with PGNs and source addresses instead of raw CAN IDs. The kernel handles the 29-bit ID decomposition, transport protocol reassembly, and address management.
Setting a Receive Timeout
By default, recvfrom() blocks indefinitely if no frames arrive. Set a receive timeout to prevent hangs:
struct timeval tv = {.tv_sec = 1, .tv_usec = 0};
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
Setting a receive timeout prevents recvfrom() from blocking indefinitely if no frames arrive. A 1-second timeout is reasonable for most J1939 applications. When the timeout expires, recvfrom() returns -1 with errno set to EAGAIN or EWOULDBLOCK.
Socket Buffer Overflow Under Load
⚠️ Warning: Under heavy bus load (>80% utilization), the kernel socket receive buffer may overflow, causing frame drops. Increase the buffer with `setsockopt(s, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize))` where `bufsize` is at least 1 MB for high-traffic buses. Monitor drops via `ip -statistics link show can0`.
int bufsize = 1048576; /* 1 MB receive buffer */
setsockopt(s, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
2.3 J1939 with Python (can-j1939)
Install the required libraries:
pip install python-can python-j1939 cantools
import j1939
import time
class MyListener(j1939.ControllerApplication):
"""Example J1939 application that listens for engine data."""
def __init__(self, name, device_address):
super().__init__(name, device_address)
def on_message(self, priority, pgn, sa, timestamp, data):
"""Called when a J1939 message is received.
Args:
priority: Message priority (0-7)
pgn: Parameter Group Number
sa: Source address of the sending node
timestamp: Message timestamp
data: Raw message data bytes
"""
if pgn == 0xF004: # PGN 61444 — EEC1
engine_speed_raw = data[3] | (data[4] << 8)
engine_speed = engine_speed_raw * 0.125
actual_torque = data[2] - 125
print(f"EEC1 (SA 0x{sa:02X}): Speed={engine_speed:.1f} RPM, Torque={actual_torque}%")
elif pgn == 0xFEEE: # PGN 65262 — ET1
coolant_temp = data[0] - 40
fuel_temp = data[1] - 40
print(f"ET1 (SA 0x{sa:02X}): Coolant={coolant_temp}°C, Fuel={fuel_temp}°C")
# Create the ECU and add the listener
ecu = j1939.ElectronicControlUnit()
ecu.connect(interface='socketcan', channel='can0', bitrate=250000)
# Create a NAME for our application
name = j1939.Name(
arbitrary_address_capable=1, # AAC (Arbitrary Address Capable) — enables dynamic address negotiation
industry_group=j1939.Name.IndustryGroup.Global,
vehicle_system_instance=0,
vehicle_system=0,
function=0,
function_instance=0,
ecu_instance=0,
manufacturer_code=0,
identity_number=1
)
app = MyListener(name, 128) # Source address 0x80
ecu.add_ca(controller_application=app)
app.start()
# Listen for 30 seconds
time.sleep(30)
app.stop()
ecu.disconnect()
Expected output:
EEC1 (SA 0x00): Speed=1500.0 RPM, Torque=42%
ET1 (SA 0x00): Coolant=88°C, Fuel=60°C
EEC1 (SA 0x00): Speed=1512.5 RPM, Torque=43%
2.4 Command-Line J1939 with can-utils
# Listen for J1939 messages using candump with 29-bit ID decoding
candump can0 -e # -e for extended frame format
# Send a J1939 message using cansend
# PGN 0xFECA (DM1), Priority 6, SA 0x00
# 29-bit CAN ID = 0x18FECA00
cansend can0 18FECA00#04FF9C051100FFFF
# Use j1939acd for address claiming
# (part of can-utils J1939 tools)
j1939acd -r 128 can0 # Claim address 128 on can0
# Use j1939cat to read/write J1939 data by PGN
j1939cat can0:0x80 -R 0xFEEE # Read PGN 65262 from address 0x80
2.5 J1939-22 CAN FD with socketCAN
When the CAN interface is configured with fd on and a dbitrate, the J1939 kernel module handles CAN FD frames transparently. You send and receive using the same J1939 socket API — the only difference is that single-frame messages can be up to 64 bytes instead of 8.
📝 Note: For CAN FD physical layer considerations (transceiver selection, bus length limits), see CAN-01 Section 2. For CAN FD bit timing configuration, see CAN-03.
/* Sending a J1939-22 CAN FD message (up to 64 bytes in a single frame) */
/* Example: DM1 with 12 active DTCs — 50 bytes total
2 lamp bytes + 12 DTCs × 4 bytes = 50 bytes
With CAN FD, this fits in a single frame (no TP needed) */
uint8_t dm1_data[50];
dm1_data[0] = 0x04; /* Lamp status: MIL off, RSL off, AWL off, PL off */
dm1_data[1] = 0xFF; /* Reserved */
/* Fill DTC bytes: each DTC = SPN (19 bits) + Failure Mode Identifier (FMI, 5 bits) + CM (1 bit) + OC (7 bits) */
for (int i = 0; i < 12; i++) {
/* Placeholder DTCs for illustration */
dm1_data[2 + i*4 + 0] = 0x00; /* SPN low byte */
dm1_data[2 + i*4 + 1] = 0x00; /* SPN mid byte + FMI low bits */
dm1_data[2 + i*4 + 2] = 0x00; /* FMI high bits + CM */
dm1_data[2 + i*4 + 3] = 0x00; /* OC */
}
struct sockaddr_can dest;
memset(&dest, 0, sizeof(dest));
dest.can_family = AF_CAN;
dest.can_ifindex = if_nametoindex("can0");
dest.can_addr.j1939.name = J1939_NO_NAME;
dest.can_addr.j1939.addr = J1939_NO_ADDR; /* Broadcast */
dest.can_addr.j1939.pgn = 0xFECA; /* PGN 65226 (DM1) */
/* sendto() with >8 bytes — kernel uses CAN FD frame if interface supports it */
sendto(sock, dm1_data, 50, 0, (struct sockaddr *)&dest, sizeof(dest));
📝 Note: The J1939 kernel module automatically selects CAN FD or classical CAN framing based on the payload size and interface capabilities. If the payload exceeds 8 bytes and the interface supports CAN FD, a single CAN FD frame is used. If the payload exceeds 64 bytes (or the interface is classical CAN), the kernel falls back to transport protocol (TP/ETP) segmentation.
3. Open-SAE-J1939 Library
Open-SAE-J1939 is an open-source, platform-independent C library for J1939 protocol implementation on embedded systems. It supports address claiming, PGN handling, transport protocols (BAM/CMDT), and DM messages.
3.1 Library Architecture
The library uses a Hardware Abstraction Layer (HAL) to decouple J1939 protocol logic from platform-specific CAN drivers:
3.2 Basic Usage
#include "Open_SAE_J1939.h"
/* Define the J1939 structure */
J1939 j1939;
void setup(void) {
/* Initialize J1939 with our source address */
j1939.SA = 0x80; /* Source address */
/* Set our 64-bit NAME */
j1939.name.identity_number = 1;
j1939.name.manufacturer_code = 0;
j1939.name.function = 0;
j1939.name.arbitrary_address_capable = 1; /* AAC bit — required for address claiming */
/* Start address claiming (see J19-01 for address claim protocol details) */
SAE_J1939_Send_Address_Claimed(&j1939);
}
void loop(void) {
/* Process incoming CAN frames */
uint32_t can_id;
uint8_t data[8];
uint8_t dlc;
if (CAN_Receive(&can_id, data, &dlc)) {
/* Feed the frame to the J1939 stack */
SAE_J1939_Read_Message(&j1939, can_id, data, dlc);
}
/* Check for received PGNs */
if (j1939.received_pgn == 0xF004) { /* EEC1 */
/* Process engine data */
uint16_t rpm_raw = j1939.eec1.engine_speed;
float rpm = rpm_raw * 0.125;
}
/* Send DM1 periodically */
static uint32_t last_dm1 = 0;
if (millis() - last_dm1 > 1000) {
SAE_J1939_Send_DM1(&j1939);
last_dm1 = millis();
}
}
Complete Open-SAE-J1939 Example (Compilable)
/**
* Complete Open-SAE-J1939 example with HAL stubs.
*
* Build: gcc -o j1939_example j1939_example.c -I./Open-SAE-J1939/Src
* -L./Open-SAE-J1939/build -lsaej1939
*
* Or with Makefile (see below).
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
/* --- HAL Stubs (replace with your platform's CAN driver) --- */
typedef struct {
uint32_t id;
uint8_t data[8];
uint8_t dlc;
bool is_extended;
} CAN_Frame_t;
static CAN_Frame_t rx_queue[16];
static int rx_head = 0, rx_tail = 0;
bool HAL_CAN_Transmit(const CAN_Frame_t *frame) {
printf("TX: ID=0x%08X DLC=%d Data=", frame->id, frame->dlc);
for (int i = 0; i < frame->dlc; i++)
printf("%02X ", frame->data[i]);
printf("\n");
return true;
}
bool HAL_CAN_Receive(CAN_Frame_t *frame) {
if (rx_head == rx_tail)
return false; /* No frames available */
*frame = rx_queue[rx_tail];
rx_tail = (rx_tail + 1) % 16;
return true;
}
uint32_t HAL_GetTick_ms(void) {
/* Replace with your platform's millisecond timer */
static uint32_t tick = 0;
return tick++;
}
/* --- End HAL Stubs --- */
/* J1939 application configuration */
#define MY_SOURCE_ADDRESS 0x80
#define MY_NAME_IDENTITY 0x000001
/* Simulate receiving an EEC1 message */
void simulate_eec1_reception(void) {
CAN_Frame_t eec1 = {
.id = 0x0CF00400, /* PGN 61444 (EEC1), SA=0x00 (engine) */
.dlc = 8,
.is_extended = true,
.data = {0xFF, 0xFF, 0xFF, 0xE8, 0x03, 0xFF, 0xFF, 0xFF}
};
/* Engine speed = (0x03E8) * 0.125 = 125 RPM */
rx_queue[rx_head] = eec1;
rx_head = (rx_head + 1) % 16;
}
int main(void) {
printf("Open-SAE-J1939 Example\n");
printf("======================\n\n");
/* Step 1: Claim our address */
printf("Claiming address 0x%02X...\n", MY_SOURCE_ADDRESS);
/* In a real implementation, call J1939_Address_Claim() */
/* Step 2: Simulate receiving an EEC1 frame */
simulate_eec1_reception();
/* Step 3: Process received frame */
CAN_Frame_t frame;
if (HAL_CAN_Receive(&frame)) {
/* Simplified PGN extraction — correct for PDU2 (PF >= 240).
For PDU1 (PF < 240), mask out PS byte: see extract_pgn() in Section 4.3. */
uint32_t pgn = (frame.id >> 8) & 0x3FFFF;
uint8_t sa = frame.id & 0xFF;
printf("Received: PGN=0x%04X SA=0x%02X\n", pgn, sa);
if (pgn == 0xF004) { /* EEC1 */
uint16_t rpm_raw = frame.data[3] | (frame.data[4] << 8);
float rpm = rpm_raw * 0.125;
printf("Engine Speed: %.1f RPM\n", rpm);
}
}
return 0;
}
Makefile:
# Makefile for Open-SAE-J1939 example
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
INCLUDES = -I./Open-SAE-J1939/Src
TARGET = j1939_example
SRCS = j1939_example.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^
clean:
rm -f $(TARGET)
.PHONY: clean
Expected output:
Open-SAE-J1939 Example
======================
Claiming address 0x80...
Received: PGN=0xF004 SA=0x00
Engine Speed: 125.0 RPM
3.3 Platform Ports
Open-SAE-J1939 has been ported to:
| Platform | CAN Driver | Notes |
|---|---|---|
| STM32 (HAL) | bxCAN / FDCAN | Most common embedded platform |
| Arduino + MCP2515 | Serial Peripheral Interface (SPI) | Popular for prototyping |
| PIC18/PIC32 | Internal CAN module | Microchip Microcontroller Unit (MCU) support |
| Linux (socketCAN) | Raw CAN sockets | For testing and simulation |
| ESP32 (TWAI) | Two-Wire Automotive Interface (TWAI) | WiFi-enabled J1939 node |
⚠️ Warning: As of 2026, Open-SAE-J1939 supports classical CAN only. CAN FD frame handling (64-byte payloads, **Bit Rate Switch (BRS)** flag) is not implemented. For CAN FD J1939, use python-j1939 with python-can's CAN FD support, or implement directly using socketCAN's `canfd_frame` structure.
4. Tooling Ecosystem
4.1 Commercial Tools
| Tool | Vendor | J1939 support | CAN FD | Platform | Price range |
|---|---|---|---|---|---|
| PCAN-Explorer 6 | PEAK-System | Full (PGN/SPN decode, DM) | Yes | Windows | $500–$1,500 |
| CANalyzer | Vector | Full (with J1939 option) | Yes | Windows | $3,000+ |
| CANoe | Vector | Full (simulation + analysis) | Yes | Windows | $10,000+ |
| BusMaster | Open-source | Partial (basic PGN decode) | No (classical CAN only; CAN FD frames are dropped or cause errors) | Windows | Free |
4.2 Open-Source Tools
| Tool | J1939 support | CAN FD | Platform | License |
|---|---|---|---|---|
| can-utils | Basic (candump with J1939 ID decode) | Yes | Linux | GPL |
| SavvyCAN | PGN/SPN decode with DBC | Partial (supports CAN FD frame reception but not transmission, or supports CAN FD at limited data rates) | Cross-platform | GPL |
| python-can | Raw CAN (J1939 via python-j1939) | Yes | Cross-platform | LGPL |
| python-j1939 | Full (address claiming, TP, PGN) | No (classical CAN only; CAN FD frames are dropped or cause errors) | Cross-platform | MIT |
| Open-SAE-J1939 | Full (embedded) | No (classical CAN only; CAN FD frames are dropped or cause errors) | Embedded | MIT |
4.3 J1939 Database CAN (DBC) Files
J1939 PGN/SPN definitions can be loaded into CAN analysis tools via DBC files. Several sources provide J1939 DBC databases:
- CSS Electronics — Free J1939 DBC file with common PGNs
- PEAK-System — J1939 symbol file included with PCAN-Symbol Editor
- Community DBC files — Available on GitHub (search “J1939 DBC”)
- Official SAE digital annex — Machine-readable SPN definitions (requires SAE purchase)
Install the required libraries (if not already installed):
pip install python-can cantools
# Loading a J1939 DBC and decoding traffic
import cantools
import can
def extract_pgn(can_id):
"""Extract PGN from a 29-bit J1939 CAN ID.
PDU1 (PF < 240): PGN = DP + PF (PS is destination address, not part of PGN)
PDU2 (PF >= 240): PGN = DP + PF + PS (PS is group extension)
"""
pf = (can_id >> 16) & 0xFF
ps = (can_id >> 8) & 0xFF
dp = (can_id >> 24) & 0x01
edp = (can_id >> 25) & 0x01
if pf < 240:
return (edp << 17) | (dp << 16) | (pf << 8)
else:
return (edp << 17) | (dp << 16) | (pf << 8) | ps
def mask_sa(can_id, dbc_sa=0xFE):
"""Replace the Source Address byte with the DBC wildcard SA.
J1939 DBC files typically use SA=0xFE as a wildcard. To match
frames from any SA, replace the SA byte before DBC lookup.
"""
return (can_id & 0xFFFFFF00) | dbc_sa
db = cantools.database.load_file('j1939.dbc')
bus = can.interface.Bus(channel='can0', interface='socketcan', bitrate=250000)
for msg in bus:
if msg.is_extended_id:
try:
# Mask SA to 0xFE to match J1939 DBC convention
lookup_id = mask_sa(msg.arbitration_id)
decoded = db.decode_message(lookup_id, msg.data)
pgn = extract_pgn(msg.arbitration_id)
sa = msg.arbitration_id & 0xFF
print(f"PGN 0x{pgn:04X} SA 0x{sa:02X}: {decoded}")
except KeyError:
pass # PGN not in DBC
Expected output:
PGN 0xF004 SA 0x00: {'EngineSpeed': 1500.0, 'ActualEngTorque': 42, 'DriverDemandTorque': 0, 'EngTorqueMode': 0}
PGN 0xFEEE SA 0x00: {'EngineCoolantTemp': 88, 'FuelTemp': 60, 'EngineOilTemp': -273}
PGN 0xFEF1 SA 0x00: {'WheelBasedVehicleSpeed': 65.2, 'CruiseCtrlActive': 0, 'CruiseCtrlEnable': 0, 'BrakeSwitch': 0}
Sample J1939 DBC File
The data logger in Section 5.2 references j1939.dbc. Below is a minimal J1939 DBC file covering the most common PGNs. Save this as j1939.dbc in the same directory as your scripts.
VERSION ""
NS_ :
BS_:
BU_: Engine Transmission Brakes Instrument
BO_ 2364540158 EEC1: 8 Engine
SG_ EngineSpeed : 24|16@1+ (0.125,0) [0|8031.875] "rpm" Instrument,Transmission
SG_ ActualEngTorque : 8|8@1+ (1,-125) [-125|125] "%" Transmission
SG_ DriverDemandTorque : 0|8@1+ (1,-125) [-125|125] "%" Vector__XXX
SG_ EngTorqueMode : 40|4@1+ (1,0) [0|15] "" Vector__XXX
BO_ 2566844158 ET1: 8 Engine
SG_ EngineCoolantTemp : 0|8@1+ (1,-40) [-40|210] "degC" Instrument
SG_ FuelTemp : 8|8@1+ (1,-40) [-40|210] "degC" Vector__XXX
SG_ EngineOilTemp : 16|16@1+ (0.03125,-273) [-273|1734.96875] "degC" Vector__XXX
BO_ 2566844926 CCVS: 8 Instrument
SG_ WheelBasedVehicleSpeed : 8|16@1+ (0.00390625,0) [0|250.996] "km/h" Vector__XXX
SG_ CruiseCtrlActive : 0|2@1+ (1,0) [0|3] "" Vector__XXX
SG_ CruiseCtrlEnable : 2|2@1+ (1,0) [0|3] "" Vector__XXX
SG_ BrakeSwitch : 4|2@1+ (1,0) [0|3] "" Vector__XXX
BO_ 2566845182 LFE: 8 Engine
SG_ FuelRate : 0|16@1+ (0.05,0) [0|3212.75] "L/h" Vector__XXX
SG_ InstFuelEconomy : 16|16@1+ (0.001953125,0) [0|125.5] "km/L" Vector__XXX
BO_ 2566834942 DM1: 8 Engine
SG_ ProtectLamp : 0|2@1+ (1,0) [0|3] "" Instrument
SG_ AmberWarningLamp : 2|2@1+ (1,0) [0|3] "" Instrument
SG_ RedStopLamp : 4|2@1+ (1,0) [0|3] "" Instrument
SG_ MalfunctionLamp : 6|2@1+ (1,0) [0|3] "" Instrument
SG_ SPN_LSB : 16|8@1+ (1,0) [0|255] "" Vector__XXX
SG_ SPN_2ndByte : 24|8@1+ (1,0) [0|255] "" Vector__XXX
SG_ SPN_MSBits : 37|3@1+ (1,0) [0|7] "" Vector__XXX
SG_ FMI : 32|5@1+ (1,0) [0|31] "" Vector__XXX
SG_ OccurrenceCount : 40|7@1+ (1,0) [0|126] "" Vector__XXX
📝 Note: J1939 CAN IDs in DBC files use the full 29-bit extended ID with the `0x80000000` bit set to indicate an extended frame. The CAN ID for EEC1 is `0x8CF004FE` (decimal 2364540158): priority 3, PGN 0xF004, SA 0xFE. The SA 0xFE convention is common in DBC files as a wildcard — actual Source Addresses vary by ECU. ET1, CCVS, LFE, and DM1 use priority 6 (e.g., ET1 = `0x98FEEEFE`, decimal 2566844158).
📝 Note: Because J1939 DBC files use a fixed SA (typically 0xFE), you must mask the SA byte in received CAN IDs before calling `decode_message()`. The `mask_sa()` helper in Sections 4.3 and 5.2 handles this by replacing the lowest byte of the CAN ID with 0xFE before lookup.
5. Complete Implementation Walkthrough: J1939 Data Logger
This section walks through building a complete J1939 data logger that captures engine data and writes it to a CSV file.
5.1 Requirements
- Linux system with socketCAN-compatible CAN interface
- Python 3.8+ with
python-canandcantools - J1939 DBC file with PGN/SPN definitions
5.2 Implementation
Install dependencies:
pip install python-can cantools
#!/usr/bin/env python3
"""J1939 Data Logger — captures engine data to CSV."""
import can
import cantools
import csv
import time
import signal
import sys
from datetime import datetime
# Configuration
CAN_INTERFACE = 'can0'
CAN_BITRATE = 250000
DBC_FILE = 'j1939.dbc'
OUTPUT_FILE = f'j1939_log_{datetime.now():%Y%m%d_%H%M%S}.csv'
# PGNs to log
PGNS_OF_INTEREST = {
0xF004: 'EEC1', # Electronic Engine Controller 1
0xFEEE: 'ET1', # Engine Temperature 1
0xFEF1: 'CCVS', # Cruise Control / Vehicle Speed
0xFEF2: 'LFE', # Fuel Economy
0xFECA: 'DM1', # Active DTCs
}
class J1939Logger:
def __init__(self, interface, bitrate, dbc_path, output_path):
self.db = cantools.database.load_file(dbc_path)
self.bus = can.interface.Bus(
channel=interface,
interface='socketcan',
bitrate=bitrate
)
self.output_path = output_path
self.running = True
self.frame_count = 0
def extract_pgn(self, can_id):
"""Extract PGN from a 29-bit J1939 CAN ID."""
pf = (can_id >> 16) & 0xFF
ps = (can_id >> 8) & 0xFF
dp = (can_id >> 24) & 0x01
edp = (can_id >> 25) & 0x01
if pf < 240:
return (edp << 17) | (dp << 16) | (pf << 8)
else:
return (edp << 17) | (dp << 16) | (pf << 8) | ps
@staticmethod
def mask_sa(can_id, dbc_sa=0xFE):
"""Replace SA byte with DBC wildcard SA for message lookup."""
return (can_id & 0xFFFFFF00) | dbc_sa
def run(self):
"""Main logging loop."""
with open(self.output_path, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Timestamp', 'PGN', 'PGN_Name', 'SA',
'Signal', 'Value', 'Unit'])
print(f"Logging J1939 data to {self.output_path}")
print("Press Ctrl+C to stop...")
while self.running:
msg = self.bus.recv(timeout=1.0)
if msg is None or not msg.is_extended_id:
continue
pgn = self.extract_pgn(msg.arbitration_id)
sa = msg.arbitration_id & 0xFF
if pgn not in PGNS_OF_INTEREST:
continue
pgn_name = PGNS_OF_INTEREST[pgn]
try:
lookup_id = self.mask_sa(msg.arbitration_id)
decoded = self.db.decode_message(lookup_id, msg.data)
timestamp = datetime.now().isoformat()
for signal_name, value in decoded.items():
signal_def = self.db.get_message_by_frame_id(
lookup_id
).get_signal_by_name(signal_name)
unit = signal_def.unit if signal_def else ''
writer.writerow([
timestamp, f'0x{pgn:04X}', pgn_name,
f'0x{sa:02X}', signal_name, value, unit
])
self.frame_count += 1
if self.frame_count % 100 == 0:
print(f" {self.frame_count} frames logged...")
csvfile.flush()
except (KeyError, cantools.database.DecodeError):
pass # PGN not in DBC or decode error
def stop(self):
"""Stop the logger."""
self.running = False
self.bus.shutdown()
print(f"\nStopped. Total frames logged: {self.frame_count}")
if __name__ == '__main__':
logger = J1939Logger(CAN_INTERFACE, CAN_BITRATE, DBC_FILE, OUTPUT_FILE)
def signal_handler(sig, frame):
logger.stop()
sys.stdout.flush() # Flush output buffers before exiting
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
logger.run()
📝 Note: Always flush output buffers before exiting in a signal handler to prevent data loss in logged/piped output. The `sys.stdout.flush()` call ensures all buffered console output is written before the process terminates.
Expected output:
Logging J1939 data to j1939_log_20260316_143022.csv
Press Ctrl+C to stop...
100 frames logged...
200 frames logged...
300 frames logged...
^C
Stopped. Total frames logged: 342
This logger extracts PGNs from 29-bit CAN IDs, filters for PGNs of interest, decodes signal values using a DBC file, and writes timestamped records to a CSV file.
Troubleshooting
| # | Symptom | Likely cause | Diagnostic step | Resolution |
|---|---|---|---|---|
| 1 | modprobe can_j1939 fails |
Kernel version < 5.4, or module not built | Check kernel version: uname -r; check module availability: modinfo can_j1939 |
Upgrade kernel to 5.4+ or build the can_j1939 module from source |
| 2 | J1939 socket bind() fails |
Interface not up, or wrong address family | Check ip link show can0; verify AF_CAN and CAN_J1939 protocol |
Bring up interface; verify kernel J1939 support |
| 3 | python-j1939 address claim fails | Another device already holds the address with a lower NAME | Capture address claim messages (PGN 60928); compare NAMEs | Use a different address or ensure your NAME has lower numeric value |
| 4 | J1939 DBC decode gives wrong SPN values | DBC file uses wrong byte order or start bit for J1939 SPNs | Compare decoded values against manual calculation from raw bytes | Verify DBC signal definitions match SAE J1939-71 SPN specifications; J1939 predominantly uses little-endian (Intel) byte order for multi-byte SPNs, as specified in J1939-71. However, some manufacturer-specific PGNs may use big-endian ordering — always verify against the specific PGN definition in your DBC file or the J1939 digital annex |
| 5 | Data logger misses multi-packet messages | Using raw CAN sockets instead of J1939 sockets — TP reassembly not handled | Switch to J1939 sockets (kernel handles TP) or implement TP in application | Use socket(PF_CAN, SOCK_DGRAM, CAN_J1939) for automatic TP handling |
| 6 | CAN FD frames from J1939-22 device not decoded | CAN interface not configured for CAN FD mode | Check interface config: ip -d link show can0 |
Configure with fd on: sudo ip link set can0 type can bitrate 250000 dbitrate 2000000 fd on |
| 7 | Open-SAE-J1939 sends frames but gets no responses | Address not claimed, or CAN transceiver wiring issue | Check if Address Claimed (PGN 60928) was sent and acknowledged | Ensure address claiming completes before sending application PGNs |
| 8 | Multi-packet PGN transfer starts but data never arrives in application | TP.DT sequence interrupted — T1 (750 ms), T2 (1250 ms), or T3 (1250 ms) timeout exceeded due to bus congestion or slow sender | Monitor TP.CM and TP.DT frames with candump; check for missing sequence numbers or TP.Conn_Abort frames |
Investigate bus load (keep below 70% for reliable TP); increase sender TP.DT transmission rate; check for higher-priority traffic starving TP frames |
| 9 | CAN FD frames visible in arbitration but rejected with CRC errors; classical frames work normally | Data-phase bit rate (dbitrate) mismatch between nodes on the bus segment |
Compare dbitrate settings on all nodes: ip -d link show can0; check CAN error counters for Receive Error Counter (REC) increase during CAN FD frames only |
Ensure all nodes on the bus segment use the same dbitrate value (e.g., 2000000 for 2 Mbit/s) |
References
-
SAE J1939-22:2020 — Data Link Layer for CAN FD. Defines J1939 extensions for CAN FD.
-
SAE J1939-21:2018 — Data Link Layer. Transport protocols and network management.
-
SAE J1939-71:2020 — Vehicle Application Layer. PGN and SPN definitions.
-
Linux Kernel Documentation — J1939 networking. Available at kernel.org/doc/html/latest/networking/j1939.html.
-
Open-SAE-J1939 — Open-source J1939 library. Repository: github.com/DanielMartensson/Open-SAE-J1939.
-
python-j1939 — Python J1939 library. Repository: github.com/milhead2/python-j1939.
-
python-can — Python CAN bus interface. Repository: github.com/hardbyte/python-can.
-
cantools — Python CAN database tools. Repository: github.com/cantools/cantools.
-
PEAK-System — PCAN-Explorer 6 with J1939 support. J1939 PGN/SPN decoding and analysis.
Changelog
| Version | Date | Author | Summary of changes |
|---|---|---|---|
| 1.0 | 2026-03-16 | Telematics Tutorial Series | Initial publication |
| 1.1 | 2026-03-16 | Telematics Tutorial Series | Agent review iteration 1 fixes: corrected CAN FD bit rate to 5 Mbit/s max, added CAN FD coexistence section, added ETP discussion, added CAN FD socketCAN configuration and 64-byte example, fixed C code error checking, fixed python-j1939 callback signature, fixed PGN extraction in DBC decode, added TP timeout and dbitrate mismatch troubleshooting rows, corrected Open-SAE-J1939 URL |
| 1.2 | 2026-03-16 | Telematics Tutorial Series | Agent review iteration 2 fixes: fixed garbled Open-SAE-J1939 GitHub URL, corrected DTC bit-field comment order (CM before OC), clarified J1939 byte order nuance, added pip install commands, added expected output blocks, noted Open-SAE-J1939 CAN FD limitation, added recvfrom timeout and socket buffer overflow guidance, added signal handler flush note, added CAN FD cross-references, expanded Partial/No CAN FD labels in tool tables, added mixed classical/FD frames note, standardized Note/Warning callout formatting |
| 1.3 | 2026-03-16 | Telematics Tutorial Series | Added compilable Open-SAE-J1939 example with HAL stubs and Makefile (Section 3.2), sample J1939 DBC file for common PGNs (Section 4.3), CAN FD DLC non-linear encoding table with Python encoder/decoder (Section 1.5) |
| 1.4 | 2026-03-16 | Telematics Tutorial Series | Final polish: fixed DBC CAN IDs for ET1/CCVS/LFE/DM1 (wrong PGNs, now correct with 0x80000000 extended bit and SA=0xFE wildcard), added mask_sa() helper for DBC lookup (Sections 4.3 and 5.2), expanded acronyms on first use (SAE, ISO, BRS, FMI, SPN, HAL, SPI, MCU, AAC), added cross-references to CAN-02/CAN-04/J19-01/J19-02 in Prerequisites, connected AAC bit to address claiming, fixed expected output signal names to match DBC, added HAL introduction in Section 3.1, added #include |
```