/Open-SAE-J1939

SAE J1939 protocol free to use for embedded systems or PC with CAN-bus

Primary LanguageCMIT LicenseMIT

Open SAE J1939

SAE J1939 is a protocol for shaping the CAN-bus message in a specific way that suits industrial vehicles such as tractors, machinery, trucks and more.

SAE J1939 is a very easy protocol to use, but there is a lack of information about SAE J1939, due to the cost of the protocol document, available how to shape a CAN-bus message according to SAE J1939 protocol standard. So therefore I’m writing a SAE J1939 protocol available for free to use on any embedded systems such as STM32, Arduino, AVR, PIC etc or PC.

To learn to build on this project, you need first to understand SAE J1939. I have written this project in C language because C is an industry standard. The C language dialect I have chosen is ANSI C (C89) and I don't use dynamical memory allocation in this library. So it will work with MISRA C standard.

With this library, you can communicate with valves, engines, actuators, machinery, hardware and all other things that are suitable for heavy industrial mobile applications. I have build up a basic structure of the project and I hope that other users will send pull request of their C code for extra functionality to SAE J1939 standard because SAE J1939 is a huge standard.

Looking for C CANopen library for embedded systems? https://github.com/DanielMartensson/Easy-CANopen

Looking for a C++ GUI framework that uses Open SAE J1939 over the USB? https://github.com/DanielMartensson/GoobySoft

Logging for a C STM32 project with Open SAE J1939? https://github.com/DanielMartensson/STM32-PLC

Getting started

The first thing you need to know is to read my own PDF document of Open SAE J1939 inside the Documentation folder. Learn the structure of the project, else you won't be able to understand SAE J1939. After you have got a basic understanding of the project, you are able to build on it. Keep it simple and follow the SAE J1939 standard!

After you have understand the structure of the project, then select processor choice in Hardware -> Hardware.h file. Here you can select for example STM32, Arduino, PIC, AVR etc. or if you want to run it on PC first, then select PROCESSOR_CHOICE 0 and run some examples. That's the debugging mode for internal CAN feedback.

How to use the project

  • Step 1: Download this repository
  • Step 2: Go to Hardware -> Hardware.h and select your processor, if it's not available, please write code for it and send me a pull request
  • Step 3: Copy over the Src folder to your project folder inside your IDE. Rename Src to for example Open SAE J1939. That's a good name.
  • Step 4: Use the Examples -> Open SAE J1939 -> Main.txt example as your initial starting code for a SAE J1939 project.
/*
 * Main.c
 *
 *  Created on: 16 juli 2021
 *      Author: Daniel Mårtensson
 */

#include <stdio.h>

 /* Include Open SAE J1939 */
#include "Open_SAE_J1939/Open_SAE_J1939.h"

/* Include ISO 11783 */
#include "ISO_11783/ISO_11783-7_Application_Layer/Application_Layer.h"

void Callback_Function_Send(uint32_t ID, uint8_t DLC, uint8_t data[]) {
	/* Apply your transmit layer here, e.g:
	 * uint32_t TxMailbox;
	 * static CAN_HandleTypeDef can_handler;
	 * This function transmit ID, DLC and data[] as the CAN-message.
	 * HardWareLayerCAN_TX(&can_handler, ID, DLC, data, &TxMailbox);
	 *
	 * You can use TCP/IP, USB, CAN etc. as hardware layers for SAE J1939
	 */
}

void Callback_Function_Read(uint32_t* ID, uint8_t data[], bool* is_new_data) {
	/* Apply your receive layer here, e.g:
	 * CAN_RxHeaderTypeDef rxHeader = {0};
	 * static CAN_HandleTypeDef can_handler;
	 * This function read CAN RX and give the data to ID and data[] as the CAN-message.
	 * if (HardWareLayerCAN_RX(can_handler, &rxHeader, ID, data) == STATUS_OK){
	 *	*is_new_data = true;
	 * }
	 *
	 * You can use TCP/IP, USB, CAN etc. as hardware layers for SAE J1939
	 */
}

/* This function reads the CAN traffic */
void Callback_Function_Traffic(uint32_t ID, uint8_t DLC, uint8_t data[], bool is_TX) {
	/* Print if it is TX or RX */
	printf("%s\t", is_TX ? "TX" : "RX");

	/* Print ID as hex */
	printf("%08X\t", ID);

	/* Print the data */
	uint8_t i;
	for (i = 0U; i < DLC; i++) {
		printf("%X\t", data[i]);
	}

	/* Print the non-data */
	for (i = DLC; i < 8U; i++) {
		printf("%X\t", 0U);
	}

	/* New line */
	printf("\n");
}

/* Apply your delay here */
void Callback_Function_Delay(uint8_t delay){
	/* Place your hardware delay here e.g HAL_Delay(delay); for STM32 */
}

int main() {

	/* Create our J1939 structure */
	J1939 j1939 = { 0 };

	/*
	 * Callbacks can be used if you want to pass a specific CAN-function into the hardware layer.
	 * All you need to do is to enable INTERNAL_CALLLBACK inside hardware.h
	 * If you don't want to have the traffic callback, just set the argument as NULL.
	 * If you don't want any callback at all, you can write your own hardware layer by selecting a specific processor choice at hardware.h
	 */
	CAN_Set_Callback_Functions(Callback_Function_Send, Callback_Function_Read, Callback_Function_Traffic, Callback_Function_Delay);

	/* Load your ECU information */
	Open_SAE_J1939_Startup_ECU(&j1939);

	/* SAE J1939 process */
	bool run = true;
	while (run) {
		/* Read incoming messages */
		Open_SAE_J1939_Listen_For_Messages(&j1939);

		/* Your application code here */

	}

	/* Save your ECU information */
	Open_SAE_J1939_Closedown_ECU(&j1939);

	return 0;
}

See the examples in Examples -> SAE J1939 how to change the address, NAME or identifications for your ECU.

The structure of the project

a

A Working example how the structure of Open SAE J1939 is done

This flow chart in code how Open SAE J1939 library is working. This example demonstrates how to send a request and get an answer.

  • Step 1: ECU X is going to send a PGN to ECU Y. Interpret PGN as a function code.
    ENUM_J1939_STATUS_CODES SAE_J1939_Send_Request_ECU_Identification(J1939 *j1939, uint8_t DA) {
    return SAE_J1939_Send_Request(j1939, DA, PGN_ECU_IDENTIFICATION);
    }
  • Step 2: ECU Y is going to read that PGN message from ECU X.
    bool is_new_message = CAN_Read_Message(&ID, data);
    if(is_new_message){
    /* Save latest */
    j1939->ID = ID;
    memcpy(j1939->data, data, 8);
    j1939->ID_and_data_is_updated = true;
    uint8_t id0 = ID >> 24;
    uint8_t id1 = ID >> 16;
    uint8_t DA = ID >> 8; /* Destination address which is this ECU. if DA = 0xFF = broadcast to all ECU. Sometimes DA can be an ID number too */
    uint8_t SA = ID; /* Source address of the ECU that we got the message from */
    /* Read request from other ECU */
    if (id0 == 0x18 && id1 == 0xEA && (DA == j1939->information_this_ECU.this_ECU_address || DA == 0xFF)){
    SAE_J1939_Read_Request(j1939, SA, data);
  • Step 3: The PGN function code will be interpreted by ECU Y.
    void SAE_J1939_Read_Request(J1939 *j1939, uint8_t SA, uint8_t data[]) {
    uint32_t PGN = (data[2] << 16) | (data[1] << 8) | data[0];
    if (PGN == PGN_ACKNOWLEDGEMENT) {
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN);
    } else if (PGN == PGN_ADDRESS_CLAIMED){
    SAE_J1939_Response_Request_Address_Claimed(j1939);
    } else if (PGN == PGN_COMMANDED_ADDRESS) {
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN);
    } else if (PGN == PGN_ADDRESS_DELETE) {
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN); /* Not SAE J1939 standard */
    } else if (PGN == PGN_DM1) {
    SAE_J1939_Response_Request_DM1(j1939, SA);
    } else if (PGN == PGN_DM2) {
    SAE_J1939_Response_Request_DM2(j1939, SA);
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN);
    } else if (PGN == PGN_DM3) {
    SAE_J1939_Response_Request_DM3(j1939, SA);
    } else if (PGN == PGN_REQUEST) {
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN);
    } else if (PGN == PGN_TP_CM) {
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN);
    } else if (PGN == PGN_TP_DT) {
    SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_ACKNOWLEDGEMENT_PGN_SUPPORTED, GROUP_FUNCTION_VALUE_NORMAL, PGN);
    } else if (PGN >= PGN_AUXILIARY_VALVE_ESTIMATED_FLOW_0 && PGN <= PGN_AUXILIARY_VALVE_ESTIMATED_FLOW_15) {
    ISO_11783_Response_Request_Auxiliary_Valve_Estimated_Flow(j1939, PGN & 0xF); /* PGN & 0xF = valve_number */
    } else if (PGN == PGN_GENERAL_PURPOSE_VALVE_ESTIMATED_FLOW){
    ISO_11783_Response_Request_General_Purpose_Valve_Estimated_Flow(j1939, SA);
    } else if (PGN >= PGN_AUXILIARY_VALVE_MEASURED_POSITION_0 && PGN <= PGN_AUXILIARY_VALVE_MEASURED_POSITION_15) {
    ISO_11783_Response_Request_Auxiliary_Valve_Measured_Position(j1939, PGN & 0xF); /* PGN & 0xF = valve_number */
    } else if (PGN == PGN_SOFTWARE_IDENTIFICATION) {
    SAE_J1939_Response_Request_Software_Identification(j1939, SA);
    } else if (PGN == PGN_ECU_IDENTIFICATION) {
    SAE_J1939_Response_Request_ECU_Identification(j1939, SA);
  • Step 4: The PGN function code is now interpreted as ECU Identification by ECU Y. Then ECU Y is going to broadcast the ECU Identification to all ECUs.
    • Step 4.1.1: For 1 package message, ECU Y is going to broadcast the ECU Identification.
      ENUM_J1939_STATUS_CODES SAE_J1939_Response_Request_ECU_Identification(J1939* j1939, uint8_t DA) {
      /* Find the length of the array fields */
      uint8_t length_of_each_field = j1939->information_this_ECU.this_identifications.ecu_identification.length_of_each_field;
      if (length_of_each_field < 2) {
      /* If each field have the length 1, then we can send ECU identification as it was a normal message */
      uint32_t ID = (0x18FDC5 << 8) | j1939->information_this_ECU.this_ECU_address;
      uint8_t data[8];
      data[0] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_part_number[0];
      data[1] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_serial_number[0];
      data[2] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_location[0];
      data[3] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_type[0];
      data[4] = 0xFF; /* Reserved */
      data[5] = 0xFF; /* Reserved */
      data[6] = 0xFF; /* Reserved */
      data[7] = 0xFF; /* Reserved */
      return CAN_Send_Message(ID, data);
      } else {
    • Step 4.1.2: ECU X read the response from ECU Y because the ECU Identification is broadcasted.
      }else if (id0 == 0x18 && id1 == 0xFD && DA == 0xC5){
      SAE_J1939_Read_Response_Request_ECU_Identification(j1939, SA, data);
    • Step 4.2.1: For Multi Package Message, the control byte can either be BAM or RTS. BAM is only used if you send to all ECUs e.g address 0xFF = 255. But if the control byte is RTS, e.g address is not 0xFF, then ECU Y is going to send a RTS and listen for a CTS response by ECU X. RTS is a question for "Let me know when I can transmit the message?" and CTS is response Now you can transmit the message to me.
      j1939->this_ecu_tp_cm.total_message_size = 0;
      uint8_t i;
      for(i = 0; i < length_of_each_field; i++) {
      j1939->this_ecu_tp_dt.data[i] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_part_number[i];
      j1939->this_ecu_tp_dt.data[length_of_each_field + i] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_serial_number[i];
      j1939->this_ecu_tp_dt.data[length_of_each_field*2 + i] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_location[i];
      j1939->this_ecu_tp_dt.data[length_of_each_field*3 + i] = j1939->information_this_ECU.this_identifications.ecu_identification.ecu_type[i];
      j1939->this_ecu_tp_cm.total_message_size += 4;
      }
      /* Send TP CM */
      j1939->this_ecu_tp_cm.number_of_packages = j1939->this_ecu_tp_cm.total_message_size % 8 > 0 ? j1939->this_ecu_tp_cm.total_message_size/8 + 1 : j1939->this_ecu_tp_cm.total_message_size/8; /* Rounding up */
      j1939->this_ecu_tp_cm.PGN_of_the_packeted_message = PGN_ECU_IDENTIFICATION;
      j1939->this_ecu_tp_cm.control_byte = DA == 0xFF ? CONTROL_BYTE_TP_CM_BAM : CONTROL_BYTE_TP_CM_RTS; /* If broadcast, then use BAM control byte */
      ENUM_J1939_STATUS_CODES status = SAE_J1939_Send_Transport_Protocol_Connection_Management(j1939, DA);
      • Step 4.2.2: If ECU Y is sending a RTS, then ECU X will read the RTS and response with CTS back to ECU Y
        if(j1939->from_other_ecu_tp_cm.control_byte == CONTROL_BYTE_TP_CM_RTS){
        j1939->this_ecu_tp_cm = j1939->from_other_ecu_tp_cm; /* Copy - We need to have the same data */
        j1939->this_ecu_tp_cm.control_byte = CONTROL_BYTE_TP_CM_CTS; /* We only need to change the control byte from RTS to CTS */
        SAE_J1939_Send_Transport_Protocol_Connection_Management(j1939, SA);
        }
      • Step 4.2.3: Once ECU Y has received the CTS, then it going to transmit the data, in this case ECU Identification, back to ECU X.
        if(j1939->from_other_ecu_tp_cm.control_byte == CONTROL_BYTE_TP_CM_CTS){
        SAE_J1939_Send_Transport_Protocol_Data_Transfer(j1939, SA);
        }
      • Step 4.2.3: Once ECU Y is sending package after package...
        ENUM_J1939_STATUS_CODES SAE_J1939_Send_Transport_Protocol_Data_Transfer(J1939 *j1939, uint8_t DA){
        uint32_t ID = (0x1CEB << 16) | (DA << 8) | j1939->information_this_ECU.this_ECU_address;
        uint8_t i, j, package[8];
        uint16_t bytes_sent = 0;
        ENUM_J1939_STATUS_CODES status = STATUS_SEND_OK;
        for(i = 1; i <= j1939->this_ecu_tp_cm.number_of_packages; i++) {
        package[0] = i; /* Number of package */
        for(j = 0; j < 7; j++){
        if(bytes_sent < j1939->this_ecu_tp_cm.total_message_size){
        package[j+1] = j1939->this_ecu_tp_dt.data[bytes_sent++]; /* Data that we have collected */
        }else{
        package[j+1] = 0xFF; /* Reserved */
        }
        }
        status = CAN_Send_Message(ID, package);
        CAN_Delay(100); /* Important CAN delay according to standard */
        if(status != STATUS_SEND_OK){
        return status;
        }
        }
        return status;
        }
      • Step 4.2.4: Then ECU X is recieving each package and building up the message by knowing the PNG function code.
        void SAE_J1939_Read_Transport_Protocol_Data_Transfer(J1939 *j1939, uint8_t SA, uint8_t data[]) {
        /* Save the sequence data */
        j1939->from_other_ecu_tp_dt.sequence_number = data[0];
        j1939->from_other_ecu_tp_dt.from_ecu_address = SA;
        uint8_t i, j, index = data[0] - 1;
        for (i = 1; i < 8; i++){
        j1939->from_other_ecu_tp_dt.data[index*7 + i-1] = data[i]; /* For every package, we send 7 bytes of data where the first byte data[0] is the sequence number */
        }
        /* Check if we have completed our message - Return = Not completed */
        if (j1939->from_other_ecu_tp_cm.number_of_packages != j1939->from_other_ecu_tp_dt.sequence_number || j1939->from_other_ecu_tp_cm.number_of_packages == 0){
        return;
        }
        /* Our message are complete - Build it and call it complete_data[total_message_size] */
        uint32_t PGN = j1939->from_other_ecu_tp_cm.PGN_of_the_packeted_message;
        uint16_t total_message_size = j1939->from_other_ecu_tp_cm.total_message_size;
        uint8_t complete_data[MAX_TP_DT];
        uint16_t inserted_bytes = 0;
        for (i = 0; i < j1939->from_other_ecu_tp_dt.sequence_number; i++){
        for (j = 0; j < 7; j++){
        if (inserted_bytes < total_message_size){
        complete_data[inserted_bytes++] = j1939->from_other_ecu_tp_dt.data[i*7 + j];
        }
        }
        }
        /* Send an end of message ACK back */
        if(j1939->from_other_ecu_tp_cm.control_byte == CONTROL_BYTE_TP_CM_RTS){
        SAE_J1939_Send_Acknowledgement(j1939, SA, CONTROL_BYTE_TP_CM_EndOfMsgACK, GROUP_FUNCTION_VALUE_NORMAL, PGN);
        }
        And then finally implement the message.
        case PGN_ECU_IDENTIFICATION:
        SAE_J1939_Read_Response_Request_ECU_Identification(j1939, SA, complete_data);
        break;

SAE J1939 functionality

  • SAE J1939:21 Transport Layer
    • Acknowledgement
    • Request
    • Transport Protocol Connection Management with BAM, CTS, RTS and EOM
    • Transport Protocol Data Transfer
  • SAE J1939:71 Application Layer
    • Request Component Identification
    • Request ECU Identification
    • Request Software Identification
    • Request Proprietary A
  • SAE J1939:73 Diagnostics Layer
    • DM1
    • DM2
    • DM3
    • DM14
    • DM15
    • DM16
  • SAE J1939:81 Network Management Layer
    • Address Claimed
    • Commanded Address
    • Address Not Claimed
    • Delete Address

Extra functionality

  • ISO 11783 Tractors And Machinery For Agriculture And Forestry
    • ISO 11783-7 Implement Messages Application Layer
      • Auxiliary Valve Command
      • Auxiliary Valve Estimated Flow
      • Auxiliary Valve Measured Position
      • General Purpose Valve Command
      • General Purpose Valve Estimated Flow

Questions and answers

  • Q: Can this library be used with C++?
    • A: Yes it can be used with C++
  • Q: I want to build on this library, what should I do?
    • A: First you need to know ANSI C (C89) and bitwise operations. Then you need to understand the SAE J1939:21 Transport Layer structure. Don't forget to update the PDF with your new functionality.
  • Q: Can I use this on my Arduino?
    • A: Yes, this C code is 100% pure C code and only using C standard library and also the code does not take account of what hardware you are using.
  • Q: Do I need to install the library for to use the library?
    • A: No, just copy over the .c and .h files to your project and compile. I have used this with QT framework.
  • Q: This project is quite old now and not so much updates, is it still worth to use it?
    • A: Yes, this library only updates when I or other includes more functionality from SAE J1939. The reason why I wrote this in ANSI C (C89) is because it's an industry standard and will you will always be able to compile this library and use it on all systems.
  • Q: What is your plan with the library?
    • A: To make SAE J1939 available for everybody
  • Q: I don't have CAN-bus, but can I use this library anyway with UART, USB, WiFi etc?
    • A: Yes. This is only a way to shape a message in a specific way.
  • Q: Can I send data with this library, even if I don't have CAN-bus?
    • A: Yes. There are something called DM14 transmit request, DM15 status response and DM16 binary transfer. Use that if you want to transfer data in an industrial way.
  • Q: Can I send multi package messages from from multiple ECU:s to one ECU at the same time?
    • A: No. If you starting to send multipackages from multiple ECU:s to another ECU, then that ECU cannot understand the message. Transmit only multipackage messages one at the time if the destination address is the same.
  • Q: I don't want to use 'ANSI C (C89)' with Open SAE J1939. Can I use the latest C standard with Open SAE J1939?
    • Yes, you can use the latest C standard with this library.
  • Q: Is it possible to compile this library onto a Windows MS-DOS or Windows 95 machine?
    • A C89 compatible compiler and an IDE and it should not be any problem

Issues and answers

  • I: I cannot compile this library. I'm using Keil Microvision.
    • A: Keil Microvision cannot handle binary numbers such as 0b010101. Try STM32CubeIDE instead because Open SAE J1939 is made in STM32CubeIDE
  • I: Can you provide us with some hardware examples for example STM32?
    • A: Yes! There is a STM32 example how to get connection with CAN-bus including an interrupt listener for messages. Go to Examples -> Hardware folder at look for CAN_STM32.txt. Also there is a USB example as well for QT C++.