160 Baud ALDL
Home Up

 

April 25th 2018

Time to check in again!

I've spent a good deal of time investigating the ALDL/OBD1 data stream emitted by the ECM in our early C4's and have focused on the 160bps data stream. Before anyone scoffs, and asks why not use the faster more data rich 8192bps data stream? Well, there's a goodly amount of useful information in the 160bps data stream and so I'm starting there. Once I'm satisfied that I've got the 160bps stream handled I'll move on to the 8192bps stream. In fact I'd like to explore the data stream that no one talks about, namely the smaller data stream that feeds data continually to the digital dash for things like the MPG readout etc.

So after reading about all the really hacky things people have done to fit the square peg of ALDL data into the round hole of RS232 UARTs I decided to take a different tack. I decided to employ an Arduino to sample the ALDL data and convert it to a stream of easily read and easily parsed ASCII 1's and 0's.

I purchased this ALDL cable from http://www.aldlcable.com/:



And I cobbled together a perf board to tap into the various ALDL connections and added a 10k resistor with an enable/disable jumper. From there the ALDL data line and ground were attached to the arduino. And finally, the Arduino is plugged into the Raspberry Pi which reads the incoming data and also powers the Arduino.



Here's an O-Scope image of the 160bps ALDL data stream coming from the ECM:



As you can see, there's a bunch of activity in between two quiet periods. All those blips at the bottom of the trace are the data bits. The amount of time the voltage stays at zero volts determines whether the bit is a 0 or a 1.

After several hours of contemplating and coding, my Arduino finally read the ALDL stream. Success! Or so I thought. When I started the engine the data became garbled. Another look at the ALDL stream with the engine running revealed that there were some small, random voltage spikes present. The scope image below shows the tiny spikes:



Since the ALDL line wasn't as clean as I had hoped, it was time to rewrite my code to allow it to ignore minor glitches on the data line. A few hours later I had a working program.

This has worked out really well. I now have an Arduino doing what it does best (which is handling real-world time-critical events). Then it transmits the data to the Raspberry Pi to do what it does best (which is processing raw data and presenting it graphically).

This video shows the ALDL data stream from the ECM being displayed on the Raspberry Pi touch-screen. If you enlarge the video, you can see that when I press the over-drive button on my shifter, one of the bits changes from a 0 to a 1. Pretty cool stuff.

The background music (Neil Young's "Computer Age") from the early 80's seems particularly fitting given the car and the project.
 


Now I have to take all those 1's and 0's (which represent things like the TPS voltage, BLM counts, MAF air flow rate, IAC position etc.) and translate them into a digital/analog/barchart/graph that can be displayed on the touch-screen.

If you're into binary, you can also see the PROMID's in the top row, 3rd and 4th bytes (fyi: the ECM always sends a start bit (0) followed by 8 data bits so the leading zero is not part of the data). These values indicate the version of the Programmable Read Only Memory chip installed in the car:


     PROMIDA   PROMIDB
    000011110 010101011
     |      |  |      |
     +-1Eh--+  +-ABh--+
     |                |
     +-----7851d------+
    
My car has a PROMID of 1EAB Hex or 7851 Decimal.

Here's a link to the Arduino source code for the 160 Baud ALDL reader:

Download: ALDL_160_Baud_Interface.ino

And here's the actual code:


// = ALDL_160_Baud_Interface ===========================================================================================
//
//  This sketch reads the ALDL (Assembly Line Diagnostic Link) data from a 1988 Chevrolet Corvette ECU (Engine Control 
//  Unit). This particular ECU requires a 10k ohm resistor between ALDL connector pins A and B to cause it to start 
//  streaming diagnostic data. The diagnostic data appears on pin E of the ALDL connector.
//  
//  The 160 bps (bits per second) ALDL data stream is received as a continuous stream of data bits. Each data bit is
//  made up of a high to low, and a low to high voltage transition within a time of 6.25 mSec (milliseconds) or 
//  6,250 uSec (microseconds).
//
//  Each data bit starts as a falling edge of the ALDL data stream. If the bit being sent is a zero (0) the data line 
//  will return to a high level after a small delay of ~300 uSec. If the bit being sent is a one (1) the data line will 
//  return to a high level after a larger delay of ~4,500 uSec.
//  
//       |----- 6,250 Microseconds ---->|----- 6,250 Microseconds ---->|----- 6,250 Microseconds ---->|--- repeat -->
//  5v --+  +---------------------------+  +---------------------------+                    +---------+  +----------
//       |  |                           |  |                           |                    |         |  |
//       |  |                           |  |                           |                    |         |  |
//  0v   +--+                           +--+                           +--------------------+         +--+
//       ^__^ = 0                       ^__^ = 0                       ^____________________^ = 1     ^__^ = 0
//
//  Both 0 and 1 bits, take exactly the same time ~6,250 uSec. There are therefore 1,000,000/6,250 or 160 bits
//  transmitted per second.
//
//  The ALDL message consists of multiple 9 bit data units. Each data unit begins with a start bit '0' followed
//  by eight data bits zero (0) or one (1).
//
//  The first data unit is always nine zero (0) bits. Followed by the actual data units. The message ends with a 
//  quiet period of approximately 50,000 uSec.
//  
//  ALDL Data Stream Map:
//
//    1           MODE WORD 2
//            0   OVERDRIVE ON                                1 = ON, 0 = OFF
//            1   MALF 14 OR 15 THIS STARTUP
//            2   REFERENCE PULSE OCCURED
//            3   1 = IN ALDL MODE, 8192 LOCKED IN, AND MODE 4
//            4   DIAGNOSTIC SWITCH IN DIAGNOSTIC POSITION
//            5   DIAGNOSTIC SWITCH IN ALDL POSITION
//            6   HIGH BATTERY VOLTAGE
//            7   SHIFT LIGHT                                 1 = ON,  0 = OFF
//    2           FIRST PROMID WORD                           PROM ID A MSB     (My 1988=00011110b   1Eh    30d)
//    3           SECOND PROMID WORD                          PROM ID B LSB     (My 1988=10101011b   ABh   171d)
//    4           IAC PRESENT MOTOR POSITION                  STEPS = N
//    5           COOLANT TEMPERATURE                         DEG C = N*192/256 - 40
//    6           MILES PER HOUR                              N = MPH
//    7           EGR DUTY CYCLE                              % DUTY CYCLE = N/2.56
//    8           ENGINE SPEED (RPM)                          RPM = N * 25
//    9           THROTTLE POSITION                           VOLTS = N * .0196
//    10          BASE PULSE CLOSED LOOP CORRECTION           N = COUNTS
//    11          OXYGEN SENSOR                               MILLIVOLTS = N*4.44
//    12          MALFUNCTION FLAG WORD 1
//            0   C23 MAT SENSOR LOW
//            1   C22 THROTTLE POSITION SENSOR LOW
//            2   C21 THROTTLE POSITION SENSOR HIGH
//            3   C16 NOT USED
//            4   C15 COOLANT SENSOR LOW TEMPERATURE
//            5   C14 COOLANT SENSOR HIGH TEMPERATURE
//            6   C13 OXYGEN SENSOR OPEN
//            7   C12 NO REFERENCE PULSES (ENG. NOT RUNNING)
//    13          MALFUNCTION FLAG WORD 2
//            0   C35 NOT USED
//            1   C34 MAF SENSOR LOW
//            2   C33 MAF SENSOR HIGH
//            3   C32 EGR DIAGNOSTIC
//            4   C31 NOT USED
//            5   C26 NOT USED
//            6   C25 MAT SENSOR HIGH
//            7   C24 VEHICLE SPEED SENSOR
//    14          MALFUNCTION FLAG WORD 3
//            0   C51 PROM ERROR
//            1   C46 VATS FAILED
//            2   C45 OXYGEN SENSOR RICH
//            3   C44 OXYGEN SENSOR LEAN
//            4   C43 ESC FAILURE
//            5   C42 EST MONITOR ERROR
//            6   C41 CYLINDER SELECT ERROR
//            7   C36 BURNOFF DIAGNOSTIC
//    15          MALF FLAG WORD 4
//            0   C63 NOT USED
//            1   C62 NOT USED
//            2   C61 NOT USED
//            3   C56 NOT USED
//            4   C54 ADU ERROR
//            5   C53 FUEL PUMP VOLTAGE
//            6   C52 OVER VOLTAGE
//            7   C51 CAL PACK MISSING
//    16          AIR/FUEL MODE WORD
//            0   NOT USED
//            1   LEARN CONTROL ENABLE FLAG                   1 = ENABLE STORED
//            2   NOT USED
//            3   NOT USED
//            4   VEHICLE SPEED SENSOR FAILURE
//            5   EECC SLOW 02 RICH/LEAN FLAG
//            6   RICH - LEAN FLAG                            1 = RICH, 0 = LEAN
//            7   CLOSED LOOP FLAG                            1 = CLOSED LOOP
//    17          MANIFOLD AIR TEMPERATURE                    SEE TABLE 1
//    18          MCU INPUT STATUS WORD
//            0   1 = IN PARK/NEUTRAL
//            1   1 = NOT IN THIRD GEAR
//            2   1 = OVERDRIVE REQUEST
//            3   NOT USED(POWER STEERING ON = 1)
//            4   1 = EGR DIAGNOSTIC SWITCH CLOSED
//            5   1 = TCC LOCKED
//            6   1 = FAN REQUEST BIT
//            7   0 = A/C REQUEST
//    19          OLDPA3 - ESC COUNTER INPUT                  N = COUNTS
//    20          BLM                                         N = COUNTS
//    21          ALDL RICH LEAN CHANGE COUNTER / TOTAL CROSSOVER COUNTS **
//    22          AIR FLOW RATE (MSB)
//    23          AIR FLOW RATE (LSB)                         G/S = (MSB) * 256 + (LSB)
//    24          INJECTOR BASE PULSE WIDTH (MSB)
//    25          INJECTOR BASE PULSE WIDTH (LSB)             WIDTH=(MSB) * 256 + (LSB)


/* = Constants ====================================================================================================== */

  // ALDL data line is connected to this pin.
  //
  const int ALDL_PIN = 2;

  // ALDL Message length: 32 * (StartBit + 8 DataBits) = 288
  //
  const int ALDL_BIT_BUFFER_SIZE = 288;

  // ALDL inter-message quiet time in microseconds.
  //
  const long ALDL_QUIET_TIME = 50000;

  // ALDL time span needed to frame a single bit.
  //
  const int ALDL_BIT_TIME_SPAN = 6250;
  
  // ALDL time span of a '0' bit in microseconds.
  //
  const int ALDL_0_BIT_TIME_SPAN = 250;

  // ALDL time span of a '1' bit in microseconds.
  //
  const int ALDL_1_BIT_TIME_SPAN = 2500;


/* = Globals ======================================================================================================== */

  // Character array to store 1's and 0's
  //
  char gALDL_Bit_Buffer[ALDL_BIT_BUFFER_SIZE];


/* = Helper Functions =============================================================================================== */

  void WaitForLineStateChange(unsigned int &oCurrentLineState, unsigned long &oStateChangeTime)
  {
    // This routine waits for an ALDL line state change. It tests any state change to see
    // if it is a valid state change vs. a state change caused by a noise spike. Most 
    // electrical interference (ignition, alternator etc.) can be filtered out by ignoring
    // state changes with a duration of less than 100 uSec.
        
    unsigned int  LineStateAtEntry;
    unsigned long TimeAtEntry;

    unsigned int  CurrentLineState;
    unsigned long CurrentTime;
    
    unsigned long StateChangeTime;

    unsigned int  TimeOut;
  
    // Read the ALDL line state.
    //
    LineStateAtEntry = digitalRead(ALDL_PIN);

    // Read the current time.
    //
    TimeAtEntry = micros();

    // Initialize time out flag.
    //
    TimeOut = false;
    
    // Start a forever loop. We will remain inside this loop until a state change
    // can be validated.
    //
    for(;;)
    {

      // Start another forever loop. We will remain inside this loop until a state
      // change is detected. If no state change is detected within 6,250 uSec we
      // fake a state change.
      //
      for(;;)
      {
        // Read the ALDL line state.
        //
        CurrentLineState = digitalRead(ALDL_PIN);

        // Has line state changed?
        //
        if(CurrentLineState != LineStateAtEntry)
        {
          StateChangeTime = micros();
          
          break;
        }

        // Read the current time.
        //
        CurrentTime = micros();

        // Check the current time.
        //
        if ((CurrentTime - TimeAtEntry) > ALDL_BIT_TIME_SPAN)
        {
          TimeOut = true;
          
          break;
        }
      }

      // Did we time out?
      //
      if (TimeOut == true)
      {
        // Invert the current line state.
        //
        oCurrentLineState = (CurrentLineState == HIGH) ? LOW : HIGH;

        // Set the state change time to now.
        //
        oStateChangeTime = micros();

        break;
      }
      
      // Wait 100uS for line state to settle.
      //
      delayMicroseconds(100);
  
      // Has line state remained unchanged?
      //
      if(digitalRead(ALDL_PIN) == CurrentLineState)
      {
        // A valid (not noise induced) line state change has occured, so
        // return the current line state and state change time to caller.
        //
        oCurrentLineState = CurrentLineState;
        oStateChangeTime = StateChangeTime;
        
        break;
      }
    }
  }


/* = Program Setup ================================================================================================== */

void setup()
{
  // Initialize built-in LED.
  //
  pinMode(LED_BUILTIN, OUTPUT);

  // Initialize serial comms.
  //
  Serial.begin(115200, SERIAL_8N1);

  // Initialize ALDL message buffer.
  //
  for(int i = 0; i < ALDL_BIT_BUFFER_SIZE; i++)
  {
    gALDL_Bit_Buffer[i] = '-';
  }
}


/* = Program Loop =================================================================================================== */

void loop()
{
  unsigned int  PreviousLineState;
  unsigned long PreviousStateChangeTime;

  unsigned int  CurrentLineState;
  unsigned long CurrentStateChangeTime;
  
  unsigned int  Interval;

  unsigned int  ALDL_Bit_Buffer_Index = 0;

  // When we arrive here we need to start collecting ALDL bit
  // data. So wait for an ALDL state change.
  //
  WaitForLineStateChange(CurrentLineState, CurrentStateChangeTime);

  // Remember the line state and state change time.
  //
  PreviousLineState = CurrentLineState;
  PreviousStateChangeTime = CurrentStateChangeTime;

  // Start collecting ALDL data bits.
  //
  for(;;)
  {
    // Wait for an ALDL state change.
    //
    WaitForLineStateChange(CurrentLineState, CurrentStateChangeTime);

    // Calculate the interval in milliseconds between this
    // state change and the last state change.
    //
    Interval = CurrentStateChangeTime - PreviousStateChangeTime;

    // Remember the line state and state change time.
    //
    PreviousLineState = CurrentLineState;
    PreviousStateChangeTime = CurrentStateChangeTime;

    // What is the current line state?
    //
    switch(CurrentLineState)
    {

      case LOW:

        // If we captured an empty bit, then we're into the quiet time. So, we 
        // can take this opportunity to transmit the bits we've gathered.
        //
        if(Interval > ALDL_BIT_TIME_SPAN)
        {
          // Is there data in the ALDL bit array?
          //
          if (gALDL_Bit_Buffer[0] == '-')
          {
            break;
          }

          // Walk the ALDL bit array and emit each '1' or '0' character.
          //
          for(int i = 0; i < ALDL_BIT_BUFFER_SIZE; i++)
          {
            // Print a space before each data unit (except the first).
            //
            if((i % 9 == 0) && (i != 0))
            {
              Serial.print(' ');
            }

            // Print array element.
            //
            Serial.print(gALDL_Bit_Buffer[i]);

            // Reset the array element to a '-' after printing it.
            //
            gALDL_Bit_Buffer[i] = '-';
          }
  
          // Add a carriage return and line feed to stream.
          //
          Serial.write(0x0D);
          Serial.write(0x0A);

          ALDL_Bit_Buffer_Index = 0;
        }
        
        break;
        
      case HIGH:

        // What kind of bit is this?
        //
        if(Interval >= ALDL_1_BIT_TIME_SPAN)
        {
          digitalWrite(LED_BUILTIN, HIGH);
          gALDL_Bit_Buffer[ALDL_Bit_Buffer_Index] = '1';
          ALDL_Bit_Buffer_Index++;
        }
        else if(Interval >= ALDL_0_BIT_TIME_SPAN)
        {
          digitalWrite(LED_BUILTIN, LOW);
          gALDL_Bit_Buffer[ALDL_Bit_Buffer_Index] = '0';
          ALDL_Bit_Buffer_Index++;
        }
  
        break;
    }

  }

}