ELM327 to RS232 in Linux

I have been having car problems recently. However, these inspired me to dig a little into the car ECU. ECUs (engine control units) are able to give out a myriad of information, like systems’ fault codes, real-time RPM, air-flow, fuel consumption, voltage, exhaust state, and lots more. Most modern cars tap into this information and display it on the dashboard via on-board computers.

OBD2 ELM327 to RS232 Linux

I finally received the long-awaited OBD II (OBD 2) (on-board diagnostics) interpreter based on the ELM327 chip yesterday and all I can say is that it’s been a long night.

Update September 9th, 2014: turns out, unsurprisingly, that the board I had was a fake one, containing no ELM327 chip inside, but rather PIC microcontroller that emulated the expensive proprietary genuine ELM327 chip. This is probably why I never managed to get it to work on my car.

The ELM327 interpreter chip provides an RS-232 (serial) interface to ODBII data. The device that I got further provides a UART interface via its FT232R FTDI chip which provides a USB interface. So, in short, the package provides a USB interface on one end, and an OBDII interface on the other.

Ubuntu comes with FTDI support out of the box, and the ELM327 device was detected without problems.

usb 5-1: new full speed USB device using uhci_hcd and address 2
usb 5-1: configuration #1 chosen from 1 choice
usbcore: registered new interface driver usbserial
USB Serial support registered for generic
usbcore: registered new interface driver usbserial_generic
usbserial: USB Serial Driver core
USB Serial support registered for FTDI USB Serial Device
ftdi_sio 5-1:1.0: FTDI USB Serial Device converter detected
usb 5-1: Detected FT232RL
usb 5-1: Number of endpoints 2
usb 5-1: Endpoint 1 MaxPacketSize 64
usb 5-1: Endpoint 2 MaxPacketSize 64
usb 5-1: Setting MaxPacketSize 64
usb 5-1: FTDI USB Serial Device converter now attached to ttyUSB0
usbcore: registered new interface driver ftdi_sio
ftdi_sio: v1.5.0:USB FTDI Serial Converters Driver

Bus 005 Device 003: ID 0403:6001 Future Technology Devices International, Ltd FT232 USB-Serial (UART) IC

The device becomes available on a ttyUSB# terminal and can be connected to and read from. There aren’t many tools out there for Linux, and although Wine runs most Windows OBDII software they don’t give any insight into how to do things work down the wire (unless you hook up a serial sniffer).

The screen manager, terminal emulator, allows one to connect to the device at a specific baud setting. The hardest part is usually getting the bauds right, otherwise the chip will not respond or will respond with gibberish. The ELM327 datasheet says that in a normal state the baud rate is usually set to 38400 Bd (9600 Bd in sleep mode). The setting is reset when the device is left without power.

Speaking of power, some devices might not power the ELM327 chip via USB and have to be connected to the OBDII port of the car in order to respond. My particular device package work without being connected to an OBDII port.

And by the way, I am not to be held responsible for whatever happens to your device or your car, proceed at your own risk.

screen /dev/ttyUSB# 38400 should work (replace the # with the registered number of the device). ELM327 is an ASCII interpreter, so it can just be typed to. All commands start with “AT” (standing for ATtention and end with a carriage return (0x0d, Ctrl+V Ctrl+M, sent automatically by the screen terminal). By default echoing is ON, line feeds are OFF. The short AT commands list is of much help.

# these are comments for clarification, do not type in terminal
AT L1 # enable line feed
OK
> AT I # information
ELM327 v1.4
>AT @1 # device description
OBDII to RS232 Interpreter
> AT DP # current protocol
AUTO
> AT RV # car is obviously not connected, no voltage on OBDII port
0.0V

One wouldn’t want to remember the ELM327 command set, to say nothing of the ISO 15765-4 CAN, SAE J1850 PWM, SAE J1850 VPW, ISO 9141-2, ISO 14230-4 and SAE J1939 protocols that it speaks. You don’t see car diagnosticians pounding away inside a terminal, cross-referencing hex responses with car manufacturer data sheets, regardless of how #geek that would be.

There are a lot of so called scanning tools, diagnostic software packages for Windows. But only a couple that should work under Linux.

  • Ross Tech’s VAG COM, VCDS, both appear to be free and work well under Wine. Don’t forget to ln -sf /dev/ttyUSB# ~/.wine/dosdevices/com1 and select COM port 1.
  • pyOBD, free, open-source and cross-platform, uses pySerial. Does not work with ttyUSB# out of the box, requires very minor bugfixing. If interested let me know in the comments.
  • freediag – Vehicle Diagnostics Suite, open-source, coded in C. Couldn’t compile version 1.0c, other versions refused to work.
  • Rolling your own

    I did want to play around with it from code, so the ELM327 would actually respond to automated requests, etc. I chose good old C for experiments over Python’s pySerial, or PHP’s php-serial, or even Haskell’s serialport. Linux has a serial C library declared in termios.h which should work, however I chose to try out ftdi.h (although scarcely documented, some examples are available), which is part of the libftdi-dev package.

    #include <stdio.h>
    #include <ftdi.h>
    #include <string.h>
    
    int main(int argc, char * argv[]) {
      
      struct ftdi_context ftdic; /* FTDI context */
      /* line properties */
      enum ftdi_bits_type bits = BITS_8; // 8 data bits
      enum ftdi_stopbits_type sbit = STOP_BIT_1; // 1 stop bit
      enum ftdi_parity_type parity = NONE; // no parity
      enum ftdi_break_type lineend = BREAK_OFF;
    
      int ret;
      unsigned short modem_status = 0;
      unsigned char * tx;
      unsigned char * rx;
      int i,j; /* general purpose indices */
    
      if ((tx = malloc(128)) == NULL || (rx = malloc(128)) == NULL) {
        fprintf(stderr, "Error: could not allocate memory\n");
        return EXIT_FAILURE;
      }
    
      if (ftdi_init(&ftdic) < 0) {
        fprintf(stderr, "Error: could not initialize FTDI\n");
        return EXIT_FAILURE;
      }
    
      if ((ret = ftdi_usb_open(&ftdic, 0x0403, 0x6001)) < 0) {
        fprintf(stderr, "unable to open ftdi device: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;
      }
    
      if ((ret = ftdi_usb_reset(&ftdic)) < 0) {
        fprintf(stderr, "unable to reset ftdi device: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;
      }
    
      if ((ret = ftdi_usb_purge_buffers(&ftdic)) < 0) {
        fprintf(stderr, "unable to purge buffers on ftdi device: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;
      }
    
      if ((ret = ftdi_set_baudrate(&ftdic, 38400)) < 0) {
        fprintf(stderr, "unable to set ftdi device baud rate: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;
      }
    
      if ((ret = ftdi_set_line_property2(&ftdic, bits, sbit, parity, lineend)) < 0) {
        fprintf(stderr, "unable to set ftdi device properties: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;                
      }
    
      ftdi_poll_modem_status(&ftdic, &modem_status);
      printf("R232 status: 0x%x\n", modem_status);
      
      memcpy(tx, "AT E0\r", 6); /* turn echoing off */
      if (ftdi_write_data(&ftdic, tx, sizeof(unsigned char) * 6) < 0) {
        fprintf(stderr, "unable to send ftdi device data: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;             
      }
    
      while (1) {
        j = 0;
    
        while ( ( ret = getchar() ) != 0x0a && j < 128) tx[j++] = ret;
        if (!j) continue;
        tx[j++] = 0x0d; /* end */
        ret = 0;
        ftdi_usb_purge_tx_buffer(&ftdic);
        ftdi_usb_purge_rx_buffer(&ftdic);
        for (i = 0; i < j; i++) {      
          ret += ftdi_write_data(&ftdic, tx+i, sizeof(unsigned char) * 1);      
        }
    
        printf("Written %d bytes of data: ", ret);
        for (i = 0; i < ret; i++) printf("0x%x ", tx[i]);  
        
        printf("\n");
      
        while (1) {
          ret = ftdi_read_data(&ftdic, rx, 128);
          if (ret > 0) {
            printf("Read %d bytes of data\n", ret);
            ret -= 3; /* remove > prompt */
            printf("\tDATA: ");
            for (i = 0; i < ret; i++) printf("0x%x ",rx[i]);
            printf("\n\t(");
            for (i = 0; i < ret; i++) printf("%c",rx[i]);
            printf(")\n");
            break;
          } else if (ret < 0) {
            fprintf(stderr, "unable to read from ftdi device: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
            return EXIT_FAILURE;
          }
        }
      }
    
      if ((ret = ftdi_usb_close(&ftdic)) < 0) {
        fprintf(stderr, "unable to close ftdi device: %d (%s)\n", ret, ftdi_get_error_string(&ftdic));
        return EXIT_FAILURE;
      }
    
      ftdi_deinit(&ftdic);
      
      free(tx);
      free(rx);
    
      return EXIT_SUCCESS;
    }
    

    This code is very primitive and you’ll find it horrible (especially that buffer overflow ;)), but it works. Compile by linking to libftdi. Note the libftdi requires root permissions to run, which is one of its disadvantages.

    ~/tmp/obd$ make
    gcc -Wall -ggdb -lftdi obdtest.c -o odb
    ~/tmp/obd$ sudo ./odb 
    R232 status: 0x6001
    AT I
    Written 5 bytes of data: 0x41 0x54 0x20 0x49 0xd 
    Read 14 bytes of data
    	DATA: 0x45 0x4c 0x4d 0x33 0x32 0x37 0x20 0x76 0x31 0x2e 0x34 
    	(ELM327 v1.4)
    AT @1
    Written 6 bytes of data: 0x41 0x54 0x20 0x40 0x31 0xd 
    Read 29 bytes of data
    	DATA: 0x4f 0x42 0x44 0x49 0x49 0x20 0x74 0x6f 0x20 0x52 0x53 0x32 0x33 0x32 0x20 0x49 0x6e 0x74 0x65 0x72 0x70 0x72 0x65 0x74 0x65 0x72 
    	(OBDII to RS232 Interpreter)
    

    This will probably end up as either Python code or C++ code, haven’t decided yet which route to take. I’ll be further experimenting in C for now, maybe with termios.h instead of libftdi to see whether it works just as well without requiring root permissions. This is just the beginning; parsing actual ODB II data will be where the real fun begins. I’ll be aiming for a command line interface first and see where I can take it from there.