Seri Protokoller
TEKNİK REHBER SERİ PROTOKOLLER SAE J1939 2026

SAE J1939
Ağır Araç CAN Protokolü

Kamyon, otobüs, traktör — PGN/SPN mimarisi, Linux J1939 socket ve Python ile motor, fren, aktarma organı verilerini oku.

00 J1939 nedir: ağır araç standart ekosistemi

SAE J1939, kamyon, otobüs, tarım makinesi ve inşaat ekipmanlarında ECU'lar arası haberleşmeyi standardize eden protokol ailesidir. CAN 2.0B (29-bit ID) üzerine inşa edilmiştir.

Uygulama alanları

Ağır ticari araçlarKamyon, otobüs, çekici — motor, şanzıman, ABS, fren, süspansiyon ECU'ları arasında standart iletişim. Freightliner, Volvo, Mercedes-Benz, MAN tüm araçlarında J1939 kullanır.
Tarım makineleriTraktör, biçerdöver, ekim makinesi. ISO 11783 (ISOBUS), J1939 üzerine kurulu tarıma özel bir üst katman protokolüdür.
İnşaat makineleriEkskavatör, yükleyici, greyder. Hidrolik sistem, tahrik ve güç yönetimi veri paylaşımı.
Deniz araçlarıNMEA 2000 (bkz. nmea2000.html) J1939'u temel alır; deniz elektroniğine özel PGN'ler ekler.
Jeneratörler ve güç sistemleriYedek güç jeneratörleri, UPS sistemleri, J1939 ile izleme ve kontrol.

J1939 standart ailesi

StandartKonu
J1939/11Fiziksel katman: kablo, terminasyon, konnektör
J1939/13Off-Board Diagnostic konnektörü
J1939/15Azaltılmış fiziksel katman (izolasyon gerektirmeyen)
J1939/21Veri katmanı: frame formatı, PGN yapısı
J1939/31Ağ katmanı: multi-packet transport protocol
J1939/71Araç uygulama katmanı: PGN ve SPN tanımları
J1939/73Teşhis uygulamaları: DM mesajları
J1939/81Adres yönetimi: NAME, address claiming
ISO 11783 (ISOBUS)

ISOBUS, tarım makineleri için J1939 üzerine inşa edilmiş bir protokoldür. Traktör ile çekilen alet (pullType/mounted implements) arasındaki haberleşmeyi standartlaştırır. Temel J1939 bilgisi ISOBUS için doğrudan uygulanabilir.

01 Adres yapısı: PGN, SPN, SA ve DA

J1939'un temel kavramları PGN (parametre grubu numarası) ve SPN (şüpheli parametre numarası) ile tanımlanır. Her mesaj bir kaynak adresten gelir ve bir hedef adrese veya global alıcılara yönelir.

Temel kavramlar

PGN — Parameter Group Number18-bit sayı, belirli bir veri grubunu tanımlar. Örneğin PGN 61444 (0xF004) Electronic Engine Controller 1 verilerini içerir. PGN, 29-bit CAN ID'den türetilir.
SPN — Suspect Parameter NumberPGN içindeki bireysel veri öğelerini tanımlar. Örneğin SPN 190, motor hızı (RPM) değeridir. Bir PGN birden fazla SPN içerir.
SA — Source Address8-bit, mesajı gönderen ECU'nun adresi (0x00–0xFD). 0xFE = null adres (claiming öncesi), 0xFF = global broadcast.
DA — Destination Address8-bit hedef adres. PDU1 formatında (PF < 240): belirli bir ECU'ya yönelir. PDU2 formatında (PF ≥ 240): broadcast mesajdır, DA kavramı yoktur.

02 Frame anatomy: 29-bit CAN ID detayı

J1939, CAN 2.0B'nin 29-bit extended ID'sini tam olarak kullanır. Her bit grubu protokol anlamı taşır.

29-bit ID yapısı

  Bit 28-26: Priority (P)     — 3-bit, 0-7 (0=en yüksek)
  Bit 25:    Reserved (R)     — 0 olmalı
  Bit 24:    Data Page (DP)   — PGN seçiminde kullanılır
  Bit 23-16: PDU Format (PF)  — 8-bit
  Bit 15-8:  PDU Specific (PS)— 8-bit: PF<240 ise DA, PF≥240 ise Group Extension
  Bit 7-0:   Source Address (SA) — 8-bit

  PGN = (DP << 16) | (PF << 8) | (PS, sadece PDU2/broadcast için)

  PDU1 (hedefli, PF < 0xF0):
    PS = Destination Address
    PGN = (R << 17) | (DP << 16) | (PF << 8) | 0x00

  PDU2 (broadcast, PF ≥ 0xF0):
    PS = Group Extension
    PGN = (R << 17) | (DP << 16) | (PF << 8) | PS
    

Pratik hesaplama örneği

j1939_id_decode.py
def decode_j1939_id(can_id_29bit: int) -> dict:
    """29-bit J1939 CAN ID'yi decode eder"""
    priority = (can_id_29bit >> 26) & 0x07
    reserved = (can_id_29bit >> 25) & 0x01
    dp       = (can_id_29bit >> 24) & 0x01
    pf       = (can_id_29bit >> 16) & 0xFF
    ps       = (can_id_29bit >>  8) & 0xFF
    sa       = (can_id_29bit >>  0) & 0xFF

    if pf < 0xF0:  # PDU1: hedefli
        pgn = (dp << 16) | (pf << 8)
        da  = ps
    else:            # PDU2: broadcast
        pgn = (dp << 16) | (pf << 8) | ps
        da  = 0xFF       # global

    return {
        'priority': priority,
        'pgn': pgn,
        'sa': sa,
        'da': da,
        'pf': pf,
        'ps': ps,
    }

# Örnek: EEC1 mesajı (PGN 61444 = 0xF004, Priority=3, SA=0x00)
# Priority=3 (011b), R=0, DP=0, PF=0xF0, PS=0x04, SA=0x00
# CAN ID = (3<<26)|(0xF0<<16)|(0x04<<8)|0x00 = 0x0CF00400
result = decode_j1939_id(0x0CF00400)
print(ff"PGN: {result['pgn']:05X} ({result['pgn']})  SA: {result['sa']:02X}")
# PGN: F004 (61444)  SA: 00

03 PGN örnekleri ve SPN tablosu

J1939/71 yüzlerce PGN tanımlar. En yaygın kullanılan PGN'ler motor yönetimi, araç hızı ve teşhis mesajlarıdır.

Önemli PGN'ler

PGN (onluk)PGN (hex)İsimPeriyot
614440xF004Electronic Engine Controller 1 (EEC1)10 ms
652620xFEEEEngine Temperature 11000 ms
652650xFEF1Cruise Control / Vehicle Speed100 ms
652700xFEF6Inlet/Exhaust Conditions 1500 ms
652710xFEF7Vehicle Electrical Power 11000 ms
652760xFEFCDash Display 11000 ms
652790xFEFFFuel Economy (Liquid)100 ms
609280xEE00Address ClaimedOn request / event
593920xE800Acknowledgment (ACK/NACK)On request
652260xFECADM1 (Active Diagnostic Trouble Codes)1000 ms

EEC1 (PGN 61444) SPN dökümü

SPNBit/ByteİsimBirimFormül
899Byte 1, bit 1-4Engine Torque ModeDurum kodu
512Byte 2Driver's Demand Engine Percent Torque%x - 125 (%)
513Byte 3Actual Engine Percent Torque%x - 125 (%)
190Byte 4-5Engine Speed (RPM)rpmx * 0.125 rpm/bit
1483Byte 6Source Address of Controlling DeviceKaynak SA
1675Byte 7, bit 1-4Engine Starter ModeDurum kodu
eec1_parse.py — EEC1 frame parse
import struct

def parse_eec1(data: bytes) -> dict:
    """PGN 61444 EEC1 frame parse (8 byte)"""
    if len(data) < 8:
        return {}

    torque_mode        = (data[0] >> 0) & 0x0F   # byte1 bit1-4
    driver_torque_pct  = data[1] - 125             # SPN 512
    actual_torque_pct  = data[2] - 125             # SPN 513
    rpm_raw            = struct.unpack_from('<H', data, 3)[0]  # byte 4-5 LE
    rpm                = rpm_raw * 0.125           # SPN 190
    src_ctrl_device    = data[5]                   # SPN 1483

    return {
        'torque_mode': torque_mode,
        'driver_demand_torque_pct': driver_torque_pct,
        'actual_torque_pct': actual_torque_pct,
        'engine_speed_rpm': rpm,
        'ctrl_device_sa': src_ctrl_device,
    }

# Örnek veri (1500 RPM, 0% torque)
sample = bytes([0x00, 0x7D, 0x7D, 0xC0, 0x2E, 0x00, 0xFF, 0xFF])
print(parse_eec1(sample))
# {'engine_speed_rpm': 1500.0, 'actual_torque_pct': 0, ...}

04 Transport Protocol: BAM ve RTS/CTS

J1939 maksimum 8 byte CAN payload kısıtını aşmak için transport protocol (TP) katmanı tanımlar. 9–1785 byte arası veriler TP ile aktarılır.

BAM — Broadcast Announce Message

BAM, belirli bir hedef olmaksızın tüm node'lara büyük veri aktarmak için kullanılır. Akış kontrolü yoktur; gönderen sabit hızda gönderir.

  Gönderen                      Tüm alıcılar
    ↓
  TP.CM_BAM (PGN 0xEC00):       Toplam byte, paket sayısı, PGN bilgisi
    ↓ (50 ms bekleme)
  TP.DT#1 (PGN 0xEB00):         7 byte veri, sekans no = 1
  TP.DT#2:                       7 byte veri, sekans no = 2
  ...
  TP.DT#N:                       Son paket (kalan veri, padding 0xFF)
    
tp_bam_frames.py
import struct

def build_tp_bam(pgn: int, data: bytes) -> list:
    """BAM paketlerini üret"""
    total_bytes = len(data)
    num_packets = (total_bytes + 6) // 7  # 7 byte/paket
    frames = []

    # TP.CM_BAM frame (PGN 0xEC00 + DA=0xFF + SA)
    cm = struct.pack('<BBHBB3s',
        0x20,               # Control Byte = BAM
        total_bytes & 0xFF,
        (total_bytes >> 8) & 0xFF,
        num_packets,
        0xFF,               # Rezerv
        pgn.to_bytes(3, 'little'))
    frames.append(('CM_BAM', bytes(cm)))

    # TP.DT frame'leri
    padded = data + b'\xFF' * (num_packets * 7 - total_bytes)
    for i in range(num_packets):
        seq_no = i + 1
        chunk  = padded[i*7:(i+1)*7]
        dt = bytes([seq_no]) + chunk
        frames.append((ff'DT#{seq_no}', dt))

    return frames

RTS/CTS — Request to Send / Clear to Send

RTS/CTS, belirli bir hedefe yönelik büyük veri aktarımında akış kontrolü sağlar. Alıcı ne kadar veri alabileceğini CTS mesajı ile bildirir.

  Gönderen          Alıcı
    → TP.CM_RTS     (toplam veri, paket sayısı, PGN)
            TP.CM_CTS ←  (kaç paket gönderebilirsin, hangi sekans'tan)
    → TP.DT#1
    → TP.DT#2
    ...
            TP.CM_CTS ←  (devam)
    → TP.DT#N
            TP.CM_EndOfMsgAck ← (başarı)
    

05 Address Claiming

J1939/81, ağa yeni katılan her ECU'nun benzersiz adres talep etmesini ve çakışma durumunda çözüm üretmesini tanımlar.

NAME alanı (64-bit)

  Bit 63:    Arbitrary Address Capable (1=dinamik adres alabilir)
  Bit 62-60: Industry Group (0=global, 2=agricultural, 3=construction...)
  Bit 59-56: Vehicle System Instance
  Bit 55-49: Vehicle System (motor=0, tahrik=1, fren=2...)
  Bit 48:    Rezerv
  Bit 47-42: Function (motor kontrolü=0, fren=5...)
  Bit 41-39: Function Instance
  Bit 38-35: ECU Instance
  Bit 34-21: Manufacturer Code (11-bit)
  Bit 20-0:  Identity Number (21-bit, üretici seri no)
    

Address Claiming süreci

  1. ECU ağa katılır, tercih ettiği adresi (SA) seçer
  2. "Address Claimed" mesajı (PGN 0xEE00) yayınlar:
     CAN ID: Priority=6, PGN=0xEE00, DA=0xFF, SA=istenen_adres
     Data: kendi NAME (8 byte)
  3. Süre: 250 ms bekler
  4. Aynı adrese sahip başka bir ECU "Address Claimed" gönderirse:
     - Düşük NAME numarası (daha yüksek öncelik) kazanır
     - Yüksek NAME numarası: başka adres dener veya SA=0xFE (null) kullanır
  5. Başarılıysa o adresle çalışmaya devam eder
    

Dinamik adres kazanımı örneği

address_claim.py
import socket, struct, time

PGN_ADDRESS_CLAIMED = 0xEE00

def build_address_claimed_id(sa: int) -> int:
    """PGN 0xEE00 için CAN ID oluştur (Priority=6, DA=0xFF)"""
    priority = 6
    dp   = 0
    pf   = 0xEE
    ps   = 0xFF   # global broadcast
    return (priority << 26) | (dp << 24) | (pf << 16) | (ps << 8) | sa

def build_name(manufacturer_code: int, identity_number: int,
               function: int = 0, industry_group: int = 0,
               arbitrary: bool = True) -> bytes:
    """64-bit J1939 NAME oluştur"""
    name = (
        ((int(arbitrary) & 1) << 63) |
        ((industry_group & 0x7) << 60) |
        ((function & 0x7F) << 41) |
        ((manufacturer_code & 0x7FF) << 21) |
        (identity_number & 0x1FFFFF)
    )
    return struct.pack('<Q', name)

# SA=0x80 talep et
desired_sa = 0x80
can_id = build_address_claimed_id(desired_sa) | 0x80000000  # EFF flag
name   = build_name(manufacturer_code=0x123, identity_number=0x00001)
print(ff"Address Claimed: CAN_ID=0x{can_id:08X}, NAME={name.hex()}")

06 Linux J1939 socket

Linux 5.4 kernel'inden itibaren J1939 socket desteği dahil edilmiştir. AF_CAN + J1939 protokol ailesi ile PGN bazlı filtreleme ve adres yönetimi kernel tarafından yapılır.

J1939 socket oluşturma

j1939_socket.c
#include <linux/can.h>
#include <linux/can/j1939.h>

/* J1939 socket — CAN_J1939 protokol ailesi */
int s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);

/* J1939 adresi yapısı */
struct sockaddr_can addr = {
    .can_family  = AF_CAN,
    .can_addr.j1939 = {
        .name = J1939_NO_NAME,   /* NAME kullanmıyoruz */
        .pgn  = J1939_NO_PGN,    /* tüm PGN'leri al */
        .addr = 0x80,            /* kendi SA = 0x80 */
    },
    .can_ifindex = if_nametoindex("can0"),
};

bind(s, (struct sockaddr *)&addr, sizeof(addr));

/* Belirli PGN'e filtrele (örn. EEC1 = 61444) */
struct j1939_filter filter = {
    .pgn      = 61444,
    .pgn_mask = 0x3FFFF,
};
setsockopt(s, SOL_CAN_J1939, SO_J1939_FILTER,
           &filter, sizeof(filter));

J1939 veri alma

j1939_recv.c
uint8_t buf[8];
struct sockaddr_can src;
socklen_t addrlen = sizeof(src);

ssize_t len = recvfrom(s, buf, sizeof(buf), 0,
                       (struct sockaddr *)&src, &addrlen);

uint32_t pgn = src.can_addr.j1939.pgn;
uint8_t  sa  = src.can_addr.j1939.addr;

printf("PGN: %u (0x%05X), SA: 0x%02X, len: %zd\n",
       pgn, pgn, sa, len);

/* EEC1: motor hızı decode */
if (pgn == 61444 && len >= 6) {
    uint16_t rpm_raw = ((uint16_t)buf[4] << 8) | buf[3];
    float rpm = rpm_raw * 0.125f;
    printf("Motor hızı: %.1f RPM\n", rpm);
}

J1939 veri gönderme

j1939_send.c
/* EEC1 benzeri mesaj gönder (simülasyon/test için) */
struct sockaddr_can dst = {
    .can_family  = AF_CAN,
    .can_addr.j1939 = {
        .name = J1939_NO_NAME,
        .pgn  = 61444,     /* EEC1 */
        .addr = J1939_NO_ADDR,  /* broadcast */
    },
    .can_ifindex = if_nametoindex("can0"),
};

/* 1500 RPM → raw = 1500 / 0.125 = 12000 = 0x2EE0 */
uint8_t data[8] = {0x00, 0x7D, 0x7D,
                   0xE0, 0x2E,  /* RPM = 12000 LE */
                   0x00, 0xFF, 0xFF};

sendto(s, data, sizeof(data), 0,
       (struct sockaddr *)&dst, sizeof(dst));

07 Python ile J1939

python-j1939 kütüphanesi, J1939 ECU simülasyonu ve veri okuma için kapsamlı bir Python API sunar.

bash — kurulum
pip install j1939 python-can

ECU simülasyonu ve PGN subscribe

j1939_listen.py
import j1939, time, logging

logging.basicConfig(level=logging.INFO)

class EngineMonitor:
    def __init__(self):
        self.ecu = j1939.ElectronicControlUnit()
        self.ecu.connect(bustype='socketcan', channel='can0',
                         bitrate=250000)
        # Tüm J1939 mesajlarını dinle
        self.ecu.subscribe(self.on_message)

    def on_message(self, priority, pgn, sa, timestamp, data):
        if pgn == 0xF004:   # EEC1 = 61444 = 0xF004
            rpm = ((data[4] << 8) | data[3]) * 0.125
            torque = data[2] - 125
            print(ff"[EEC1] SA={sa:#04x} RPM={rpm:.1f} Torque={torque}%")

        elif pgn == 0xFEEE:  # Engine Temperature 1
            coolant_temp = data[0] - 40  # SPN 110: offset -40°C
            print(ff"[Temp1] Coolant={coolant_temp}°C")

        elif pgn == 0xFECA:  # DM1 — Active DTCs
            self.parse_dm1(sa, data)

    def parse_dm1(self, sa, data):
        lamp_status = data[0]
        if lamp_status == 0x00:
            print(ff"[DM1] SA={sa:#04x} DTC yok")
            return
        # Her DTC 4 byte: SPN(19-bit) + FMI(5-bit) + CM(1-bit) + OC(7-bit)
        for i in range(2, len(data), 4):
            if i + 3 >= len(data):
                break
            spn = ((data[i+2] & 0xE0) << 11) | (data[i+1] << 8) | data[i]
            fmi = data[i+2] & 0x1F
            oc  = data[i+3] & 0x7F
            print(ff"  DTC: SPN={spn} FMI={fmi} OC={oc}")

    def run(self, duration=30):
        print(ff"J1939 dinleniyor ({duration} saniye)...")
        time.sleep(duration)
        self.ecu.disconnect()

monitor = EngineMonitor()
monitor.run()

08 Pratik: Raspberry Pi ile J1939 araç veri izleme

Raspberry Pi + MCP2515 SPI-CAN modülü ile kamyon veya iş makinesi J1939 bus'ına bağlanarak motor sıcaklığı ve hız verisi izlenebilir.

Donanım bağlantısı

  Raspberry Pi 4         MCP2515 + TJA1050
  ──────────────         ─────────────────
  GPIO 8  (CE0)  ───────→ CS
  GPIO 11 (SCLK) ───────→ SCK
  GPIO 10 (MOSI) ───────→ SI
  GPIO 9  (MISO) ───────→ SO
  GPIO 25        ───────→ INT
  3.3V           ───────→ VCC (dikkat: bazı modüller 5V)
                          CAN_H ──→ J1939 bus (pin A — OBD konnektörde)
                          CAN_L ──→ J1939 bus (pin B — OBD konnektörde)
    

Device Tree overlay ve kernel yapılandırması

/boot/firmware/config.txt
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
dtparam=spi=on
bash — J1939 arayüz başlatma
# J1939 araçlarda çoğunlukla 250 kbit/s kullanılır
sudo ip link set can0 type can bitrate 250000 restart-ms 100
sudo ip link set can0 up

# Linux J1939 modülü yükle
sudo modprobe can-j1939

# Temel test: candump ile J1939 mesajları göster
candump can0 -a
# (1716890123.456) can0  0CF00400   [8]  00 7D 7D E0 2E 00 FF FF
#     → J1939: PGN=F004 SA=00 (EEC1, 1500 RPM)

SPN 110 motor sıcaklığı izleme

engine_temp_monitor.py
import socket, struct, time

PGN_ENGINE_TEMP1 = 0xFEEE   # 65262
SPN_COOLANT_TEMP = 110       # byte 0, offset -40, 1°C/bit
SPN_OIL_TEMP     = 175       # byte 2-3, offset -273, 0.03125°C/bit

def parse_engine_temp1(data: bytes) -> dict:
    coolant_raw = data[0]
    fuel_temp   = data[1] - 40       # SPN 174, °C
    oil_temp_raw = struct.unpack_from('<H', data, 2)[0]

    return {
        'coolant_temp_c': coolant_raw - 40,      # SPN 110
        'fuel_temp_c': fuel_temp,
        'oil_temp_c': oil_temp_raw * 0.03125 - 273,  # SPN 175
    }

# J1939 socket ile Engine Temp filtrele
s = socket.socket(socket.AF_CAN, socket.SOCK_DGRAM, socket.CAN_J1939)

# Bind: SA=0xF9 (araç verisi izleyici olarak)
addr = struct.pack('=HHiQIB',
    socket.AF_CAN, 0,
    socket.if_nametoindex('can0'),
    0,                     # NAME
    PGN_ENGINE_TEMP1,      # PGN filtresi
    0xF9)                  # SA
s.bind(addr[:14])  # struct sockaddr_can boyutu

print("J1939 Engine Temp izleniyor...")
while True:
    data, _ = s.recvfrom(8)
    if len(data) >= 8:
        t = parse_engine_temp1(data)
        print(ff"Soğutucu: {t['coolant_temp_c']}°C  "
              f"Motor Yağı: {t['oil_temp_c']:.1f}°C")

ISOBUS (ISO 11783) giriş

ISOBUS, tarım makineleri için J1939'u temel alan üst katman protokolüdür. Temel farklar:

ÖzellikJ1939ISOBUS (ISO 11783)
KonnektörDeutsch 9-pinAEF ISOBUS konnektör (Deutsch + ek pin)
Bitrate250 kbit/s250 kbit/s
Güç hattıHariciKonnektörde 12V güç pini
Uygulama katmanıJ1939/71ISO 11783-7 (traktör-alet iletişimi)
VT (Virtual Terminal)YokISO 11783-6 operatör arayüzü
Task ControllerYokISO 11783-10 tarla veri yönetimi
PRATİK ARAÇ

Araç J1939 bus'ını keşfetmek için ScanTool OBDLink SX veya PEAK PCAN-USB ile candump can0 + j1939acd (J1939 Address Claiming Daemon) kombinasyonu kullanılabilir. j1939acd can-utils içinde yer alır.