Skip to content

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-can and cantools
  • 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

  1. SAE J1939-22:2020 — Data Link Layer for CAN FD. Defines J1939 extensions for CAN FD.

  2. SAE J1939-21:2018 — Data Link Layer. Transport protocols and network management.

  3. SAE J1939-71:2020 — Vehicle Application Layer. PGN and SPN definitions.

  4. Linux Kernel Documentation — J1939 networking. Available at kernel.org/doc/html/latest/networking/j1939.html.

  5. Open-SAE-J1939 — Open-source J1939 library. Repository: github.com/DanielMartensson/Open-SAE-J1939.

  6. python-j1939 — Python J1939 library. Repository: github.com/milhead2/python-j1939.

  7. python-can — Python CAN bus interface. Repository: github.com/hardbyte/python-can.

  8. cantools — Python CAN database tools. Repository: github.com/cantools/cantools.

  9. 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 to C example, updated Open-SAE-J1939 date to 2026

```