M0AGX / LB9MG

Amateur radio and embedded systems

Reading OBD2 data without ELM327, part 1 - CAN

All modern cars have an OBD2 diagnostic connector that allows reading many engine and drivetrain parameters like RPM, vehicle speed, temperatures etc.

Most of car interfaces use a special protocol translating chip like ELM327 or STN1110 to convert different vehicle protocols (that depend on the age and brand of the car) into an easier to use serial protocol with AT-commands.

I wanted to build a datalogger that would fit into a OBD2 connector. There was no space to fit my microcontroller and another chip to do protocol conversion so I investigated and reverse-engineered the most common OBD2 protocols to be able to implement them directly on my MCU.

This is the first post in series about OBD2. The second one about K-Line is here.

Basics

Because the OBD2 standards are not freely available I decided to buy an OBD2 emulator from Freematics, connect it to a Chinese OBD2 USB cable and sniff the traffic with a protocol analyzer. Later I have found out that the emulator prints out all traffic to the terminal so the analyzer was not necessary.

There are several physical layer (think "the wires") standards (CAN, K-Line, VPV, PWM) and several protocols that run over those wires. This post is about one of them - the CAN bus used for OBD2 (formally standarized as ISO 15765-4).

CAN bus

CAN bus standard allows many devices to send and receive messages over a single (twisted) pair of wires. Each message has a message identifier (11 or 29 bit long), length field and up to 8 payload bytes (+some other flags that are not needed for OBD2). There is no notion of sender or receiver. The message ID is used to distinguish the source (or destination).

Devices connected to a CAN bus must have a CAN transceiver (can be thought of similar to an RS-485 driver) and a CAN controller. The transceiver is always a separate chip. The controller may be integrated into a microcontroller or be a separate chip.

OBD2 modes and PIDs

OBD2 defines several modes (which can for example deliver live data, freeze frame data, diagnostic trouble codes etc.). Each of the modes support many PIDs (Parameter IDs). For example mode 01 PID 0x0C is current engine RPM. A PID can have up to 4 bytes and requires a formula to convert those bytes to a meaningful reading. Wikipedia has an excellent list of PIDs. Not all PIDs are available on every car.

OBD2 and CAN

The CAN-flavor of OBD2 comes in 4 variants:

  • 500kbps with standard (11 bit) identifiers
  • 250kbps with standard (11 bit) identifiers
  • 500kbps with extended (29 bit) identifiers
  • 250kbps with extended (29 bit) identifiers

It is hard to predict which one is used by the car. I have chosen to simply request mode 01 PID 0x00 (that is always available) using different variants.

Requesting a PID

The message is always 8 bytes long, even if less information is required. The first byte specifies the length within payload (in this case the first byte is 2 because only the mode and PID bytes are used).

PID message request (bytes): 0x02 <mode> <pid> 0x00 0x00 0x00 0x00 0x00

For example to request mode 0x01 PID 0x0C (RPM) simply send: 0x02 0x01 0x0C 0x00 0x00 0x00 0x00 0x00

Message ID must be 0x7DF for standard (11 bit) addressing and 0x18DB33F1 for extended (29 bit) addressing. Each message will be acknowledged by the car.

PID response

The response will carry a message ID of 0x7E8 (standard addressing) or 0x18DAF111 (extended) so the CAN controller receive filter must pass those message IDs. Example: 0x04 0x41 0x0C 0x31 0x64 0x00 0x00 0x00

First byte is the length of the payload field (in this case 4 bytes are valid), second byte - it is a response to mode 0x01 PID, third byte is the PID (0x0C), bytes 0x31 and 0x64 are byes A and B of the PID.

To get RPM you have to use a formula from table of PIDs. In this case the engine speed is (256 * 0x31 + 0x64)/4 = 3161 rpm.

Summary

Everything you need to get live OBD2 data from your car is to send a simple CAN frame and wait for the response. You don't need a protocol translator like ELM327 or STN1110 (though they will support more protocols and vehicle types, including all communication quirks found through the years).

I have described only getting data for mode 01 (mode 02 is identical). Other modes may require different data formats. For example getting the VIN number (due to its length) is more complicated because it has to be split between many CAN frames.

Example driver code

This is a piece of code from my upcoming OBD2 datalogger project. It runs on a Kinetis MKE06Z128 and uses the built-in CAN controller. The driver requires FreeRTOS and some other files but still it provides an easy reference on implementing OBD2 communications with MSCAN peripheral.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
#include <FreeRTOS/include/FreeRTOS.h>
#include <FreeRTOS/include/task.h>
#include "misc.h"
#include <MKE06Z4.h>
#include "obd_can.h"
#include <string.h>

#define DEBUG_ID DEBUG_ID_OBD_CAN
#include <debug.h>

#if DEFAULT_BUS_CLOCK == 20000000
//Values were calculated using the spreadsheet in dev_documentation directory
#define CANBTR0_500KBAUD 0xC3
#define CANBTR1_500KBAUD 0x34
#define CANBTR0_250KBAUD 0xC7
#define CANBTR1_250KBAUD 0x34
#else
#error "CAN baud registers not defined for this bus clock!"
#endif

#define CAN_PID_RESPONSE_TIMEOUT_ms 50
//ISO 15765-4 identifiers, reverse engineered from ELM327 communication ;)
#define CAN_OBD2_STD_ID_ECU_REQ_ID 0x7DF
#define CAN_OBD2_STD_ID_ECU_RESPONSE_ID 0x7E8
#define CAN_OBD2_STD_ID_ECU_RESPONSE_FILTER_MASK 0x7FF //simply all 11 bits must match
#define CAN_OBD2_EXT_ID_ECU_REQ_ID 0x18DB33F1
#define CAN_OBD2_EXT_ID_ECU_RESPONSE_ID 0x18DAF111
#define CAN_OBD2_EXT_ID_ECU_RESPONSE_FILTER_MASK 0x1FEFFFFF //simply all 29 bits must match *except* the RSRR

typedef struct {
    uint32_t identifier;
    bool identifier_is_extended;
    uint8_t length;
    uint8_t payload[8];
    uint8_t padding1[2];
} can_rx_frame_t;

static TaskHandle_t _local_task_handle = NULL;
static volatile can_rx_frame_t _rx_frame;
static bool _use_extended_id;
static uint32_t _obd_id_request;
static uint32_t _obd_id_response;

static void obd_can_transmit(uint32_t identifier, bool identifier_is_extended,
        const uint8_t *payload, uint8_t payload_length);
static void obd_can_tx_abort(void);

void obd_can_init(can_speed_t speed, bool use_extended_id){
    _local_task_handle = xTaskGetCurrentTaskHandle();

    portENTER_CRITICAL();
    SIM->SCGC |= SIM_SCGC_MSCAN_MASK;

    SIM->PINSEL1 |= SIM_PINSEL1_MSCANPS_MASK; //CAN_TX PTE7, CAN_RX PTH2

    portEXIT_CRITICAL();

    MSCAN->CANCTL0 |= MSCAN_CANCTL0_INITRQ_MASK; //enter controller initialization mode
    while (!(MSCAN->CANCTL1 & MSCAN_CANCTL1_INITAK_MASK)) {
        //wait for the controller to enter initialization mode
        vTaskDelay(2);
    }

    MSCAN->CANCTL1 = MSCAN_CANCTL1_CLKSRC_MASK /*use bus clock*/
            | MSCAN_CANCTL1_CANE_MASK; //enable CAN module*/

    //  MSCAN->CANCTL1 |= MSCAN_CANCTL1_LOOPB_MASK; //enable loopback for testing

    //set baud
    if (speed == can_speed_500kbaud){
        MSCAN->CANBTR0 = CANBTR0_500KBAUD;
        MSCAN->CANBTR1 = CANBTR1_500KBAUD;
        debugf("500k baud init");
    } else {
        MSCAN->CANBTR0 = CANBTR0_250KBAUD;
        MSCAN->CANBTR1 = CANBTR1_250KBAUD;
        debugf("250k baud init");
    }

    _use_extended_id = use_extended_id;
    if (_use_extended_id){
        debugf("Using extended 29-bit IDs");
        _obd_id_request = CAN_OBD2_EXT_ID_ECU_REQ_ID;
        _obd_id_response = CAN_OBD2_EXT_ID_ECU_RESPONSE_ID;
    } else {
        debugf("Using standard 11-bit IDs");
        _obd_id_request = CAN_OBD2_STD_ID_ECU_REQ_ID;
        _obd_id_response = CAN_OBD2_STD_ID_ECU_RESPONSE_ID;
    }

    MSCAN->CANRIER = MSCAN_CANRIER_RXFIE_MASK; //enable RX interrupt

    //RX filter - standard ID
    MSCAN->CANIDAR_BANK_1[0] = (uint8_t) (CAN_OBD2_STD_ID_ECU_RESPONSE_ID >> 3); //this register holds bits 10-3 of the ID
    MSCAN->CANIDAR_BANK_1[1] = (CAN_OBD2_STD_ID_ECU_RESPONSE_ID & 0x7) << MSCAN_TSIDR1_TSID2_TSID0_SHIFT;
    //MSCAN->CANIDAR_BANK_1[2] and [3] - don't care
    MSCAN->CANIDMR_BANK_1[0] = (uint8_t) ~((CAN_OBD2_STD_ID_ECU_RESPONSE_FILTER_MASK >> 3));
    MSCAN->CANIDMR_BANK_1[1] = (uint8_t) ~((CAN_OBD2_STD_ID_ECU_RESPONSE_FILTER_MASK & 0x7) << MSCAN_TSIDR1_TSID2_TSID0_SHIFT);
    MSCAN->CANIDMR_BANK_1[2] = 0xFF;
    MSCAN->CANIDMR_BANK_1[3] = 0xFF;

    debugf("%02X%02X %02X%02X%02X%02X",
            MSCAN->CANIDAR_BANK_1[0],
            MSCAN->CANIDAR_BANK_1[1],
            MSCAN->CANIDMR_BANK_1[0],
            MSCAN->CANIDMR_BANK_1[1],
            MSCAN->CANIDMR_BANK_1[2],
            MSCAN->CANIDMR_BANK_1[3]
    );

    MSCAN->CANIDAR_BANK_2[0] = CAN_OBD2_EXT_ID_ECU_RESPONSE_ID >> 21;
    MSCAN->CANIDAR_BANK_2[1] = ((CAN_OBD2_EXT_ID_ECU_RESPONSE_ID >> (20/*source bit position*/- 7/*destination bit position*/))
            & MSCAN_TEIDR1_TEID20_TEID18_MASK)
            | ((CAN_OBD2_EXT_ID_ECU_RESPONSE_ID >> (17 - 2)) & MSCAN_TEIDR1_TEID17_TEID15_MASK)
            | MSCAN_TEIDR1_TEIDE_MASK;
    MSCAN->CANIDAR_BANK_2[2] = (uint8_t) (CAN_OBD2_EXT_ID_ECU_RESPONSE_ID >> 7);
    MSCAN->CANIDAR_BANK_2[3] = (uint8_t) (CAN_OBD2_EXT_ID_ECU_RESPONSE_ID << 1);

    MSCAN->CANIDMR_BANK_2[0] = (uint8_t) ~(CAN_OBD2_EXT_ID_ECU_RESPONSE_FILTER_MASK >> 24);
    MSCAN->CANIDMR_BANK_2[1] = (uint8_t) ~(CAN_OBD2_EXT_ID_ECU_RESPONSE_FILTER_MASK >> 16);
    MSCAN->CANIDMR_BANK_2[2] = (uint8_t) ~(CAN_OBD2_EXT_ID_ECU_RESPONSE_FILTER_MASK >> 8);
    MSCAN->CANIDMR_BANK_2[3] = (uint8_t) ~(CAN_OBD2_EXT_ID_ECU_RESPONSE_FILTER_MASK);

    debugf("%02X%02X%02X%02X %02X%02X%02X%02X",
            MSCAN->CANIDAR_BANK_2[0],
            MSCAN->CANIDAR_BANK_2[1],
            MSCAN->CANIDAR_BANK_2[2],
            MSCAN->CANIDAR_BANK_2[3],
            MSCAN->CANIDMR_BANK_2[0],
            MSCAN->CANIDMR_BANK_2[1],
            MSCAN->CANIDMR_BANK_2[2],
            MSCAN->CANIDMR_BANK_2[3]
    );

    MSCAN->CANIDAC = MSCAN_CANIDAC_IDAM(0); //use two 32-bit acceptance filters

    NVIC_SetPriority(MSCAN_RX_IRQn, 5);
    NVIC_EnableIRQ(MSCAN_RX_IRQn);

    MSCAN->CANCTL0 &= ~MSCAN_CANCTL0_INITRQ_MASK; //exit initialization mode

    while (MSCAN->CANCTL1 & MSCAN_CANCTL1_INITAK_MASK) {
        //wait for the controller to exit initialization mode
        vTaskDelay(2);
    }

    MSCAN->CANRIER = MSCAN_CANRIER_RXFIE_MASK; //enable RX interrupt

    debugf("OBD CAN initialized");
}

void obd_can_deinit(void){
    NVIC_DisableIRQ(MSCAN_RX_IRQn);

    MSCAN->CANCTL0 |= MSCAN_CANCTL0_INITRQ_MASK; //enter controller initialization mode

    while (!(MSCAN->CANCTL1 & MSCAN_CANCTL1_INITAK_MASK)) {
            //wait for the controller to enter initialization mode
    }

    MSCAN->CANCTL1 = 0; //disable CAN module

    SIM->SCGC &= ~SIM_SCGC_MSCAN_MASK; //disable clock to module
}

void obd_can_task(void){
    //no need for keepalive messages
}

static void obd_can_tx_abort(void) {
    uint8_t busy_buffers = (~MSCAN->CANTFLG) & MSCAN_CANTFLG_TXE_MASK; //zero means a busy buffer
    MSCAN->CANTARQ = busy_buffers;          //writing one triggers abort request
    while (MSCAN->CANTAAK != busy_buffers) {
        vTaskDelay(2); //wait for abort ack
    }
}

static void obd_can_transmit(uint32_t identifier, bool identifier_is_extended,
        const uint8_t *payload, uint8_t payload_length) {

    uint8_t empty_buffer_mask = MSCAN->CANTFLG & MSCAN_CANTFLG_TXE_MASK;
    if (!empty_buffer_mask) { //this should never happen as only one buffer is used in a lockstep
        debugf("TX busy, dropping frame"); //and after a timeout all transmissions are aborted
        return;
    }

    //select transmit buffer
    MSCAN->CANTBSEL = MSCAN_CANTBSEL_TX(empty_buffer_mask);
    debugf("Buffers available %02X selected %02X, payload length %d", empty_buffer_mask, MSCAN->CANTBSEL, payload_length);

    MSCAN->TBPR = 0; //priority of this buffer

    if (identifier_is_extended) {
        MSCAN->TEIDR0 = identifier >> 21;
        MSCAN->TEIDR1 = ((identifier >> (20/*source bit position*/- 7/*destination bit position*/))
                & MSCAN_TEIDR1_TEID20_TEID18_MASK)
                                | ((identifier >> (17 - 2)) & MSCAN_TEIDR1_TEID17_TEID15_MASK)
                                | MSCAN_TEIDR1_TEIDE_MASK;
        MSCAN->TEIDR2 = identifier >> 7;
        MSCAN->TEIDR3 = identifier << 1;
    } else {
        MSCAN->TSIDR0 = (uint8_t) (identifier >> 3); //this register holds bits 10-3 of the ID
        MSCAN->TSIDR1 = (identifier & 0x7) << MSCAN_TSIDR1_TSID2_TSID0_SHIFT;
    }

    for (uint8_t i = 0; i < 8; i++){
        MSCAN->TEDSR[i] = payload[i];
    }
    MSCAN->TDLR = payload_length;

    //enable transmission of this buffer
    uint8_t transmit_flag = MSCAN->CANTBSEL & MSCAN_CANTBSEL_TX_MASK;
    debugf("transmit flag = %02X", transmit_flag);

    MSCAN->CANTFLG = transmit_flag;
}

int32_t obd_can_get_pid(pid_mode_t mode, uint8_t pid, obd_pid_response_t *target_response) {
    uint8_t data[8];
    data[0] = 2; //"real" length of the payload, CAN frame has to be 8 bytes long
    data[1] = mode;
    data[2] = pid;
    data[3] = 0x0;
    data[4] = 0x0;
    data[5] = 0x0;
    data[6] = 0x0;
    data[7] = 0x0;

    obd_can_transmit(_obd_id_request, _use_extended_id, data, sizeof(data));
    //data is transmitted - now wait for other the response or timeout
    uint32_t status = ulTaskNotifyTake(
            pdTRUE/*clear notification value when ready*/,
            pdMS_TO_TICKS(CAN_PID_RESPONSE_TIMEOUT_ms));
    if (status) {
        debugf("rx frame id=%X id_ext=%d length=%d", (unsigned int)_rx_frame.identifier,
                _rx_frame.identifier_is_extended, _rx_frame.length);

        for (uint32_t i = 0; i < _rx_frame.length; i++) {
            debugf("rx payload[%ld]=%02X", i, _rx_frame.payload[i]);
        }

        //not all PIDs return 4 bytes but higher layer will handle it
        target_response->byte_a = _rx_frame.payload[3];
        target_response->byte_b = _rx_frame.payload[4];
        target_response->byte_c = _rx_frame.payload[5];
        target_response->byte_d = _rx_frame.payload[6];

        return _rx_frame.payload[0] - 2; //length of the particular PID response (minus PID and mode bytes)
    } else { //timeout
        debugf("timeout");
        obd_can_tx_abort();
        return OBD_PID_ERR;
    }
}

extern void MSCAN_RX_IRQHandler(void);
void MSCAN_RX_IRQHandler(void) {
    if (MSCAN->REIDR1 & MSCAN_REIDR1_REIDE_MASK) { //frame has extended identifier
        _rx_frame.identifier_is_extended = true;
        //getting the ID back together is a nightmare, see MSCAN reference manual...
        _rx_frame.identifier = MSCAN->REIDR0 << 21;
        _rx_frame.identifier |=
                (MSCAN->REIDR1 & MSCAN_REIDR1_REID20_REID18_MASK) << (18 - 5);
        _rx_frame.identifier |=
                (MSCAN->REIDR1 & MSCAN_REIDR1_REID17_REID15_MASK) << 15;
        _rx_frame.identifier |= MSCAN->REIDR2 << 7;
        _rx_frame.identifier |= (MSCAN->REIDR3 & MSCAN_REIDR3_REID6_REID0_MASK) >> 1;
    } else {
        _rx_frame.identifier_is_extended = false;
        _rx_frame.identifier = ((MSCAN->RSIDR1 & MSCAN_RSIDR1_RSID2_RSID0_MASK)
                >> MSCAN_RSIDR1_RSID2_RSID0_SHIFT) | (MSCAN->RSIDR0 << 3);
    }

    if (unlikely(_rx_frame.identifier != _obd_id_response)){
        MSCAN->CANRFLG = MSCAN_CANRFLG_RXF_MASK; //clear RX interrupt flag
        return; //drop frames that are not OBD2 replies
    }

    _rx_frame.length = MSCAN->RDLR & MSCAN_RDLR_RDLC_MASK;

    for (uint32_t i = 0; i < _rx_frame.length; i++) {
        _rx_frame.payload[i] = MSCAN->REDSR[i];
    }

    MSCAN->CANRFLG = MSCAN_CANRFLG_RXF_MASK; //clear RX interrupt flag

    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xTaskNotifyFromISR(_local_task_handle, pdTRUE, eSetValueWithOverwrite,
            &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}