RX8 Project – Part 21, Canbus #6, Working Code

Following a couple requests recently from people I’ve decided to post my code as it currently stands. I’ve been meaning to tidy it up and crop out all the extraneous bits but I’ve just not had time so here we go. I describe this as “working code” simply because it’s the one I’m still working on!

There’s a lot going on here so don’t expect it to be an immediate plug and play and additionally there are extra variables and things that I’ve used for testing with no purpose otherwise so don’t be surprised if you can’t work out what all of it is for. One trick bit I’ve added is if a specified digital input is tied to 0V when the Arduino powers up it starts in a listen mode where if the ECU is still connected it logs the exchange between it an the immobiliser and stores the data to the internal EEPROM memory. If you then disconnect the ECU and remove the 0V jumper it will wait for the immobiliser to try to initialise by matching its code to the one logged and sent the stored response. I don’t know if this will work correctly on all cars but it should. As per one of my previous posts you can actually just write random data in this exchange as long as the packet structure is right and it’ll work.

Similarly the code also includes the update for the ODO and trip meters based on ABS speed data so that should all work ok hopefully.

The latest section I was working on when other things started taking all my time again is to decode CAN packets from a Megasquirt ECU to control. Generally this should work but you might want to modify this to either not overwrite certain if you are getting them from elsewhere such as temperature for the cluster reading from an analogue input rather than CAN. There is an enable boolean for this (MSCAN) at the top of variable declaration but it’s defaulted to false to stop it messing with anything normally.

As ever if anyone wants to know any more about what’s going on just post a comment at the bottom. Sometimes it takes me a while to respond but I try to answer everyone.

My only other request is if you link to this page when sharing this elsewhere, mostly because I find it really interesting to see how it’s being used!

// Code modified by Jonathan Coe (www.chamberofunderstanding.co.uk) 2021 with the following:
//
// Fixed variable rollover issue with speeds over 163
// Added new definitions to allow switching to Leonardo CAN hardware
// Added new definitions to allow use of Seeed CAN library rather than MCP_CAN clones
// Added startup LED blink to confirm unit powered
// Added two short LED blinks when can chip started successfully
// Added slow LED blinking when CAN chip failed to start
// Added function to pull immobiliser challenge/response packets from existing vehicle
// Added EEPROM functions to store config data
// Added Code to check immobiliser requests against data from previous scans (retained through power cycle) and respond with stored answer
// Added Code to increment the Odometer/Trip based on live speed from ABS system
// Added Code to decode Megasquirt CAN data for engine.
//
// This code is a development from the work done by Dave Blackhurst (details below) which in itself was based 
// on earlier work from this website which in itself included research done by others before on the ID's
//

//  **************************************************************************************

// Arduino Leonardo code for replacing the PCM within a Mark 1 RX8
// This code allows you to leave the CANBUS in place, just removing the PCM
// There are plenty of ID's on the CANBUS I do not understand, and any use of this code is strictly at your own risk
//
// Features turned on, possibly working - at least they do not complain on the dashboard,
//    ABS / DSC
//    Traction Control
//    Immobiliser Deactivated
//    Power Steering Enabled
//    RPM Controllable
//    Vehicle Speed set by Wheel Sensors
//    Warning lights turned off (you can turn them on in the code if you wish)
//
//    Written by David Blackhurst, dave@blackhurst.co.uk 06/10/2019
//
//    Some parts of this code have been dissected from other sources - in researching this project I copied a lot of code to play with
//    Sorry if some of this is yours - just let me know and I will attribute it to you.
//
//    I have throttle pedal code in here to translate the output of the primary throttle signal to the range my controller required. This is unlikely to be helpful to anyone else 
//    unless you happen to have the dodgy chinese controller I have.
//
//    Again use AT YOUR OWN RISK

#include <Arduino.h>


/// ********************* Option Selection *********************
// JC 21/01/20 - Updates to select hardware version to allow support for Leonardo CAN
//  and preferred CAN library (either the standard MCP-CAN versions or SEEED version) for compiler

// Comment out to select correct hardware
#define LEO_CAN         
// #define Seeed_CAN

// Comment out to select correct CAN library
#define Seeed_Library
//#define MCP_Library


#ifdef Seeed_CAN      // Configure Pins for Seeed CAN Shield
  #define CANint          2
  #define LED             13
  #define CAN_CS          10
  #define Set_Immobiliser 1
#endif

#ifdef LEO_CAN        // Configure Pins for Leonardo CAN
  #define CANint          7
  #define LED             23
  #define CAN_CS          17
  #define Set_Immobiliser 4
#endif

#ifdef Seeed_Library
  #include "mcp2515_can.h"
  mcp2515_can CAN0(CAN_CS); // Configure CAN SPI Chip Select
#endif

#ifdef MCP_Library
  #include <mcp_can.h>
  #include <mcp_can_dfs.h>
  MCP_CAN CAN0(CAN_CS); // Configure CAN SPI Chip Select
#endif

#include <EEPROM.h>       // Load EEPROM library to save configuration data

/// ********************* End of Option Selection *********************

// Enable MS_CAN Decode
bool MSCAN = false;

// Variables for Throttle Pedal
int analogPin = A1;
int outputPin = 5;

int val = 0;
int lowPedal = 0;
int highPedal = 0;
int convertThrottle = 0;
int base = 0;
int output = 0;

// Declarations for loop delays
long lastRefreshTime = 0;
long ODORefreshTime = 0;

// Variables for PCM, Only overrideable if PCM removed from CAN
bool checkEngineMIL;
bool checkEngineBL;
byte engTemp;
byte odo;
bool oilPressure;
bool lowWaterMIL;
bool batChargeMIL;
bool oilPressureMIL;

// Variables for PCM, Only overrideable if PCM removed from CAN
int engineRPM;
int vehicleSpeed;
byte throttlePedal;

// Variables for ABS/DSC, Only overrideable if ABS/DSC removed from CAN
bool dscOff;
bool absMIL;
bool brakeFailMIL;
bool etcActiveBL;
bool etcDisabled;

// Variables for Wheel Speed 
// JC 21/01/20 - changed from int to long as variable rollover was causing speeds over 163 to go negative
long frontLeft;
long frontRight;
long rearLeft;
long rearRight;

//Variables for reading in from the CANBUS
unsigned char len = 0;
unsigned char buf[8];
unsigned long ID = 0;

//Setup Array's to store bytes to send to CAN on Various ID's
byte send201[8]  = {0, 0, 255, 255, 0, 0, 0, 255};
byte send420[7]  = {0, 0, 0, 0, 0, 0, 0};
byte send212[7]  = {0, 0, 0, 0, 0, 0, 0};

//Setup PCM Status's required to fool all other CAN devices that everything is OK, just send these out continuously
byte send203[7]  = {19,19,19,19,175,3,00};                // {19,19,19,19,175,3,19} data to do with traction control
byte send215[8]  = {2,45,2,45,2,42,6,129};                // {2,45,2,45,2,42,6,129}, experimented with {2,0,2,0,2,0,0,0} but no idea
byte send231[5]  = {15,0,255,255,0};                      // {15,0,255,255,0} or {255,0,255,255,0}
byte send240[8]  = {4,0,40,0,2,55,6,129};                 // No idea what this is for
byte send620[7]  = {0,0,0,0,0,0,4}; //needed for abs light to go off, byte 7 is different on different cars, sometimes 2,3 or 4 {0,0,0,0,16,0,4}
byte send630[8]  = {8,0,0,0,0,0,106,106}; //needed for abs light to go off, AT/MT and Wheel Size
byte send650[1]  = {0};  //Cruise Light, 0 = Off, Bit 6 = Green "Cruise", Bit 7 = Yellow "Cruise Main"


//  Declarations for testing 4B0/4B1 VSS Rx on Megasquirt.
//  180 mph
//byte send4b1[8]  = {113, 40, 113, 40, 113, 40, 113, 40};
//  180 mph rear, 160mph front
//byte send4b1[8]  = {100, 149, 100, 149, 113, 40, 113, 40};
//  100 mph
//byte send4b1[8]  = {62, 221, 62, 221, 62, 221, 62, 221};
//  10 mph
//byte send4b1[8]  = {6, 73, 6, 73, 6, 73, 6, 73};

//KCM / Immobiliser replies for Dave Blackhurst
//byte send41a[8] = {7,12,48,242,23,0,0,0};                      // Reply to 47 first  : 0x 07 0C 30 F2 17 00 00 00
//byte send41b[8] = {129,127,0,0,0,0,0,0};                       // Reply to 47 second : 0x 81 7F

// Immobiliser replies for Jon Coe
byte send41a[8] = {7,120,192,226,94,0,0,0};                      // Reply to 47 first  : 0x 07 78 C0 E2 5E 00 00 00 
                                                                 // Bytes 0 is the same, bytes 3 & 4 dont seem to matter, 5,6,7 are zero
byte send41b[8] = {129,127,0,0,0,0,0,0};                         // Reply to 47 second : 0x 81 7F

// Immobiliser Blank
byte response_a[8] = {7,0,0,0,0,0,0,0};
byte response_b[8] = {129,127,0,0,0,0,0,0};                      // This always seems to be this value (0x 81 7F) so used as default
byte request_a[8] = {6,127,0,0,0,0,0,0};
byte request_b[8] = {8,0,0,0,1,0,0,0};

// Time delay for Odo
long ODOus = 4500000;                                           // Set to max 4,500,000 to keep dash MIL warning lights awake at 0 speed

void printhex( byte [], int );                                  // JC - Prototype for function to send byte array to serial monitor as HEX pairs
                                                                // Actual function is later in code - prototypes not technically required in Arduino IDE

void setup() {
  Serial.begin(115200);
  Serial.println("Start Setup");

  // Give a Wakeup Blink - Disabled to speed up boot
  /*
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH);
  delay(500);
  digitalWrite(LED, LOW);
  */
    
  pinMode(CANint, INPUT);                           // Set CAN interrupt pin as input
  pinMode(Set_Immobiliser, INPUT_PULLUP);           // Configure Button to setup immobiliser as input with pullup enabled
  
  if (CAN0.begin(CAN_500KBPS) == CAN_OK) {          // Connect to CAN chip
    Serial.println("Found High Speed CAN");
      
      // JC 21/01/20 - Added two short blinks of LED to identify CAN chip started
      // Disabled to improve speed
      /*
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);
      */
      
  } else {
    Serial.println("Failed to find High Speed CAN");
    while (1) {
      Serial.println("Loop Forever");
      
      // JC 21/01/20 - Added long blinking of LED to identify CAN chip fault
      digitalWrite(LED, HIGH);
      delay(1000);
      digitalWrite(LED, LOW);
      delay(1000);
      
    }
  }
  
  // Populates CAN buffers with meaningful initial data
  // for live use this will prevent dash lights defaulting to ON etc
  
  setDefaults(); // JC - Sets up some default values to fill CAN registers with sensible data in case nothing else is written later


//******* JC 25/01/21 Immobiliser Compatibility Mods *******

//    Check if First Run Immobiliser Code Scanning is Enabled
    if (digitalRead(Set_Immobiliser) == 0){
        immobiliserCodeSet();
    }

//    Check if stored values exist in EEPROM and if not setup from program defaults

    if (EEPROM.read(0) == 0 && EEPROM.read(1) == 0){
      writeByteArrayIntoEEPROM(0, request_a, 16);
      writeByteArrayIntoEEPROM(16, response_a, 16);
      writeByteArrayIntoEEPROM(32, request_b, 16);
    }

//    Pull Immobiliser codes from EEPROM 

      //delay(5000); delay to allow serial startup - disabled for speed
      readByteArrayFromEEPROM(0, request_a, 8);
      Serial.print("Request A from EEPROM : ");
      printhex(request_a,8);
      
      readByteArrayFromEEPROM(16, response_a, 8);
      Serial.print("Response A from EEPROM : ");
      printhex(response_a,8);
      
      readByteArrayFromEEPROM(32, request_b, 8); 
      Serial.print("Request B from EEPROM : ");
      printhex(request_b,8);
}

// ******* JC 25/01/21 Code to write Hex array to Serial *******

void printhex(byte b[], int sizeOfArray){
  Serial.print("0x ");
    for (int i=0;i<sizeOfArray;i++){
      if(b[i]<10){
        Serial.print("0");
      }
      Serial.print(b[i],HEX);
      Serial.print(" ");
      }
  Serial.println("");
}

//////////////////////////////////////////////////////////////////////////////////////
//
// ******* JC 25/01/21 Code to Scan Immobiliser codes from a working system ******* 
//          Set_Immobiliser is a digital input which is configured as 
//          INPUT_PULLUP and tied to ground to enable this mode.
//
//////////////////////////////////////////////////////////////////////////////////////


void immobiliserCodeSet(){
  
      digitalWrite(LED, HIGH);
      delay(3000);
      digitalWrite(LED, LOW);
      Serial.println("Immobiliser Code Read Mode");
    
    //immobiliserCodeSet;
      int immSet1 = 0;
      do
      if(CAN_MSGAVAIL == CAN0.checkReceive()) { // Check to see whether data is read
          CAN0.readMsgBufID(&ID, &len, buf);    // Read data
          
          if(ID == 0x47) { //71 Dec is 47 Hex - Keyless Chat
              if(buf[0] == 0x6 && buf[1] == 0x7F){
                  memcpy(request_a, buf, 8);
                  immSet1++;
                  Serial.print("Request 1 Found! - ");
                  printhex(buf,8);
                  
              }
              if (buf[0] == 0x8){
                  memcpy(request_b, buf, 8);
                  immSet1++;
                  Serial.print("Request 2 Found! - ");
                  printhex(buf,8);
              }
          }
               
          if(ID == 0x41) {
              if(buf[0] == 0x07){
                memcpy(response_a, buf, 8);
                immSet1++;
                Serial.print("Response 1 Found! - ");
                printhex(buf,8);
              }
          }
      Serial.print("CAN Data Received - Found ");
      Serial.println(immSet1);
   } while (immSet1 < 3);
   
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);

      Serial.print("All Codes Found - Saving...");
      
      writeByteArrayIntoEEPROM(0, request_a, 16);
      writeByteArrayIntoEEPROM(16, response_a, 16);
      writeByteArrayIntoEEPROM(32, request_b, 16);
      
      Serial.println("Ok!");
      Serial.println("");
      Serial.println(" Turn off ignition, reset to normal mode and reboot");
 
      while (digitalRead(Set_Immobiliser) == 0);            //    Halt here as long as input held low
  }


// ******* End of Immobiliser Scan Routine *******


void setDefaults() {
  Serial.println("Setup Started");
  // StatusMIL
  engTemp         = 145; //Roughly in the middle
  odo             = 0;
  oilPressure     = 1;   // For the gauge, 1 is OK, 0 is L
  checkEngineMIL  = 0;
  checkEngineBL   = 0;
  lowWaterMIL     = 0;
  batChargeMIL    = 0;
  oilPressureMIL  = 0;
  
  // StatusPCM
  engineRPM       = 1000;   // RPM
  vehicleSpeed    = 0;      // km/h + 10000
  throttlePedal   = 0;      // %
  
  // StatusDSC
  dscOff          = 0;
  absMIL          = 0;
  etcActiveBL     = 0;
  etcDisabled     = 0;
  brakeFailMIL    = 0;

  /*
  Serial.println("Start wait to ensure Throttle Pedal is on");
  delay(500);
  lowPedal = 341;  //analogRead(analogPin) - 40;  Temporary fixed value //read the throttle pedal, should be around 1.7v minus 40 to ensure no small throttle inputs
  highPedal = 803; //4v
  
  // Voltage to read from Pedal 1.64v - 4.04v
  // Going to use a safe range 1.7v to 4v
  // Low of 1.7v has been read above as can fluctuate
  // 1.7v = INT 341
  // 4v = INT 803
  // (highPedal - lowPedal) = RANGE FROM RX8 PEDAL
  // out for 1024 (5v max), controller wants 4.5v max = 920 (adding 40 to help stabilise)
  
  convertThrottle = 960 / (highPedal - lowPedal);
  Serial.print("Low Pedal ");
  Serial.print(lowPedal);
  Serial.print(", High Pedal ");
  Serial.println(highPedal);
  Serial.println("Setup Complete");

  */
}

//    ********** JC 25/01/21 - Add Functionality to read/write arrays from EEPROM **********
//    Taken from www.roboticsbackend.com and expanded for byte arrays

void writeIntArrayIntoEEPROM(int address, int numbers[], int arraySize)
{
  int addressIndex = address;
  for (int i = 0; i < arraySize; i++) 
  {
    EEPROM.write(addressIndex, numbers[i] >> 8);
    EEPROM.write(addressIndex + 1, numbers[i] & 0xFF);
    addressIndex += 2;
  }
}
void readIntArrayFromEEPROM(int address, int numbers[], int arraySize)
{
  int addressIndex = address;
  for (int i = 0; i < arraySize; i++)
  {
    numbers[i] = (EEPROM.read(addressIndex) << 8) + EEPROM.read(addressIndex + 1);
    addressIndex += 2;
  }
}

void writeByteArrayIntoEEPROM(int address, byte numbers[], int arraySize)
{
  int addressIndex = address;
  for (int i = 0; i < arraySize; i++) 
  {
    EEPROM.write(addressIndex, numbers[i] >> 8);
    EEPROM.write(addressIndex + 1, numbers[i] & 0xFF);
    addressIndex += 2;
  }
}
void readByteArrayFromEEPROM(int address, byte numbers[], int arraySize)
{
  int addressIndex = address;
  for (int i = 0; i < arraySize; i++)
  {
    numbers[i] = (EEPROM.read(addressIndex) << 8) + EEPROM.read(addressIndex + 1);
    addressIndex += 2;
  }
}

// ********** End of EEPROM Section **********


void updateMIL() {
  send420[0] = engTemp;
  //send420[1] = odo;     
  send420[4] = oilPressure;

  if (checkEngineMIL == 1) {
    send420[5] = send420[5] | 0b01000000;
  } else {
    send420[5] = send420[5] & 0b10111111;
  }

  if (checkEngineBL == 1) {
    send420[5] = send420[5] | 0b10000000;
  } else {
    send420[5] = send420[5] & 0b01111111;
  }

  if (lowWaterMIL == 1) {
    send420[6] = send420[6] | 0b00000010;
  } else {
    send420[6] = send420[6] & 0b11111101;
  }

  if (batChargeMIL == 1) {
    send420[6] = send420[6] | 0b01000000;
  } else {
    send420[6] = send420[6] & 0b10111111;
  }

  if (oilPressureMIL == 1) {
    send420[6] = send420[6] | 0b10000000;
  } else {
    send420[6] = send420[6] & 0b01111111;
  }
}

void updatePCM() {
  int tempEngineRPM = engineRPM * 3.85;
  int tempVehicleSpeed = (vehicleSpeed * 100) + 10000;
  
  send201[0] = highByte(tempEngineRPM);       
  send201[1] = lowByte(tempEngineRPM);        

  send201[4] = highByte(tempVehicleSpeed);    
  send201[5] = lowByte(tempVehicleSpeed);     

  send201[6] = (200 / 100) * throttlePedal;   //Pedal information is in 0.5% increments 
}

void updateDSC() {
  if (dscOff == 1) {
    send212[3] = send212[3] | 0b00000100;
  } else {
    send212[3] = send212[3] & 0b01111011;
  }

  if (absMIL == 1) {
    send212[4] = send212[4] | 0b00001000;
  } else {
    send212[4] = send212[4] & 0b11110111;
  }

  if (brakeFailMIL == 1) {
    send212[4] = send212[4] | 0b01000000;
  } else {
    send212[4] = send212[4] & 0b10111111;
  }

  if (etcActiveBL == 1) {
    send212[5] = send212[5] | 0b00100000;
  } else {
    send212[5] = send212[5] & 0b11011111;
  }

  if (etcDisabled == 1) {
    send212[5] = send212[5] | 0b00010000;
  } else {
    send212[5] = send212[5] & 0b11101111;
  }
}


long calcMicrosecODO(float speedKMH){
  long uS;
  float freq;
  float speedMPH;
   
  Serial.print("Speed = ");
  Serial.print(speedKMH/100);
  Serial.println(" km/h");
  speedMPH = speedKMH / 160.934;
  // Required frequency for timer 1 ISR
  //  1.15 is 4140 (Pulse per Mile) / 3600 (1hr in seconds)
  //  0.7146 is 2572.5 (pulse per KM) / 3600
  freq = speedMPH * 1.15; 
  Serial.print("Freq = ");
  Serial.print(freq);
  Serial.println(" Hz");
  uS = 1000000/freq;
  if(uS < 4500000 && uS > 0){
    return (uS);}
  else {
    return (4500000);
  }
  
 
}

void sendOnClock(){
  // Do not increment ODO byte when step is = 4.5s
  // slower than this updateMIL must still be called so 
  // warning lights don't turn on but speed may be zero!
  if ( ODOus < 4500000){
    send420[1]++;   
  }
  updateMIL();
  CAN0.sendMsgBuf(0x420, 0, 7, send420);
}

void sendOnTenth() {
  //PCM Status's to mimic the PCM being there, these may be different for different cars, and not all are always required, better safe and include them all.
  CAN0.sendMsgBuf(0x203, 0, 7, send203);
  CAN0.sendMsgBuf(0x215, 0, 8, send215);
  CAN0.sendMsgBuf(0x231, 0, 8, send231);
  CAN0.sendMsgBuf(0x240, 0, 8, send240);
  CAN0.sendMsgBuf(0x620, 0, 7, send620);
  CAN0.sendMsgBuf(0x630, 0, 8, send630);
  CAN0.sendMsgBuf(0x650, 0, 1, send650);
  
  updateMIL();
  CAN0.sendMsgBuf(0x420, 0, 7, send420);    //Moved to sendOnClock to update at timer ISR frequency for ODO

  updatePCM();
  CAN0.sendMsgBuf(0x201, 0, 8, send201);

  // Send to Megasquirt VSS Sim - For testing Megasquirt ABS decode
  //  CAN0.sendMsgBuf(0x4b1, 0, 8, send4b1);
  
  /* Add this section back in if you want to take control of ABS / DSC Lights.
  updateDSC();
  CAN0.sendMsgBuf(0x212, 0, 7, send212);
  */
}

void loop() {
  //Send information on the CanBus every 100ms to avoid spamming the system.
  if(micros() - lastRefreshTime >= 100000) {
		lastRefreshTime += 100000;
    sendOnTenth();
	}
  // Call function to updateMIL on variable timebase
   if(micros() - ODORefreshTime >= ODOus) {
   ODORefreshTime += ODOus;
    sendOnClock();
  }
  
  //Read the CAN and Respond if necessary or use data
  if(CAN_MSGAVAIL == CAN0.checkReceive()) { // Check to see whether data is read
    CAN0.readMsgBufID(&ID, &len, buf);    // Read data

    //digitalWrite(LED, HIGH);
    //delay(1);
    //digitalWrite(LED, LOW);
    
    if(ID == 0x212) {           // 0x212 = 530
      for(int i = 0; i<len; i++) { // Output 8 Bytes of data in Dec
        Serial.print(buf[i]);
        Serial.print("\t");
      }
      
     //Serial.print(time);   // Timestamp
      Serial.println("");
     //Serial.println(line); // Line Number
    }
    
    //Keyless Control Module and Immobiliser want to have a chat with the PCM, this deals with the conversation
    if(ID == 0x47) { //71 Dec is 47 Hex - Keyless Chat
      /*
      //***** Fixed Coding for Dave Blackhurst's Car *******
      if(buf[1] == 127 && buf[2] == 2) {                        // 0x 06 7F 02 00 00 00 00 00
        CAN0.sendMsgBuf(0x041, 0, 8, send41a);                  // 0x041 = 65
      }
      if(buf[1] == 92 && buf[2] == 244) {                       // 0x 08 5C F4 65 22 01 00 00
        CAN0.sendMsgBuf(0x041, 0, 8, send41b);                  // 0x 81 7F 00 00 00 00 00 00
      }

      //***** Fixed Coding for Jon Coe's Car *******
      // Some experimentation showed that on the initial request for my car byte 2 was a 01 not a 02
      // however all codes so far begin 06 7F for either car so this was used.
      // Similarly in the second message from the immobiliser bytes 1-4 change but byte 5 is always 01 on either vehicle
      // The negotiation happens during every start but the codes only seem to cycle whenever the battery 
      // is disconnected  

      if(buf[0] == 0x6 && buf[1] == 0x7F ) {                      // 0x 06 7F 01 00 00 00 00 32
        //printhex(buf,8);                                        // Transmit out received request on debug serial port - breaks timings on vehicle.
        CAN0.sendMsgBuf(0x041, 0, 8, send41a);                    // 0x041 = 65
      }
      if(buf[0] == 0x8 && buf[5] == 0x1 ) {                        // 0x 08 94 29 BC 91 01 00 32
        CAN0.sendMsgBuf(0x041, 0, 8, send41b);                     // 0x 81 7F 00 00 00 00 00 00  
      }
   */
     
//    ********** JC 25/01/21 - Add Functionality to use immobiliser responses stored in EEPROM **********

      //if(memcmp(buf, request_a, 8) == 0){                          // Check first request matches stored pattern (difference = 0)
      if(buf[0] == request_a[0] && buf[1] == request_a[1] ) {
        CAN0.sendMsgBuf(0x041, 0, 8, response_a);                  // Send stored response to 0x041 = 65
      }
      //if(memcmp(buf, request_b, 8) == 0) {                         // Check second request starts with "08"
      if(buf[0] == request_b[0] && buf[5] == request_b[5] ) {
        CAN0.sendMsgBuf(0x041, 0, 8, response_b);                  // Send second response - seems to always be 0x 81 7F 00 00 00 00 00 00
      }

      
    }


    
    //Read wheel speeds to update Dash
    //if(ID == 1200) { //1201 Dec is 4b1 Hex - Wheel Speeds ----> check this address. Wheel speeds for dash 4B1 -> 201

    if(ID == 0x4B0) {
      frontLeft = (buf[0] * 256) + buf[1] - 10000;
      frontRight = (buf[2] * 256) + buf[3] - 10000;
      rearLeft = (buf[4] * 256) + buf[5] - 10000;
      rearRight = (buf[6] * 256) + buf[7] - 10000;
      
      //Going to check front wheel speeds for any issues, ignoring the rears due to problems created by wheelspin
      if (frontLeft - frontRight > 500 || frontLeft - frontRight < -500) { //more than 5kph difference in wheel speed
        checkEngineMIL = 1; //light up engine warning light and set speed to zero
        vehicleSpeed = 0;
      } else {
        vehicleSpeed = (((frontLeft + frontRight) / 2) / 100); //Get average of front two wheels.
      }
      //Update timer count value with live speed for ODO
      //OCR1A = calcTimer1Count((frontLeft + frontRight) / 2);
      // delay in MS for ODO
      ODOus = calcMicrosecODO((frontLeft + frontRight) / 2);
      Serial.print("ODO Step : ");
      Serial.print(ODOus);
      Serial.println("us");
      // Dump speed to serial for debug - this is just a cropped int.
      //Serial.println(vehicleSpeed);
      
    }

    
    // Decode for Megasquirt - JC
    /* This section matches the various fields used by Megasquirt
     *  CAN send to decode for the RX8 cluster. Not all fields are
     *  converted from the Megasquirt as most are not required.
     *  The CAN start address for Megasquirt is the default 0x5F2 (1520)
     */
    
  if(MSCAN == true){
    if(ID == 0x5F0) { //1520 Dec is 5F0 Hex - Megasquirt Block 0
      // Block 0 - Seconds, PW2, PW2, RPM, 2 bytes each

      engineRPM = (buf[6] * 256) + buf[7];
    }
       
    if(ID == 0x5F2) { //1522 Dec is 5F2 Hex - Megasquirt Block 2
      // Block 2 - Baro(kPa*10), MAP(kPa*10), MAT(degF*10), CLT(degF*10)
      // Edit map to set "normal" range on cluster (in degF) where needle stays centred
      // outside of the normal range needle will rapidly increase or decrease.

      int normMin = 140;    //  60 degC
      int normMax = 220;    // 104 degC

      engTemp = map((buf[6] * 256) + buf[7],normMin*10,normMax*10,110,150);
    }
    
    if(ID == 0x5F3) { //1523 Dec is 5F3 Hex - Megasquirt Block 3
      // Block 3 - TPS (%*10), Batt (V*10), EGO1(Depricated on MS3), 
      // EGO2(Depricated on MS3), 2 bytes each

      throttlePedal = ((buf[0] * 256) + buf[1]) / 10;
    }

    if(ID == 0x624) { //1572 Dec is 624 Hex - Megasquirt Block 52
      // Block 54 - CANin_1(?),CANout_1, CANout_2, 
      // Knock_ret (deg*10, 1 byte), Fuel flow (cc/min*10, 2 byte), 
      // Fuel Consumption(l/km, 2 byte)
      // First 3 bytes appear wrong in the Megasquirt CAN documentation
      // testing shows byte 1 is CANout_1 not CANin_2
      // byte 2 is CANout_2

      byte CANout_1   = buf[1];
      byte CANout_2   = buf[2];
     
      // Read Check engine light from Megasquirt
      checkEngineMIL  = bitRead(CANout_1,0);

      // Blink traction control light - Disabled due to ABS unit
      //etcActiveBL     = bitRead(CANout_1,1);    

      // Read Oil Pressure light from Megasquirt
      oilPressureMIL     = bitRead(CANout_1,2);
      
      // Also set cluster "gauge" to match warning light
      if(oilPressureMIL == 1){
        oilPressure = 0;
      }
      else{
        oilPressure = 1;
      }
            
    }

   }  // Close MSCAN mode check
   
  } // Close CAN message receive processing

  /*  Reading throttle sensor for electric drive control - 
  
  //Throttle Pedal Work
  val = analogRead(analogPin);  // read the input pin
  
  //See Set Defaults Method for Calculations
  if (val < 110 || val > 960) { // If input is less than 0.5v or higher than 4.5v something is wrong so NO THROTTLE
    val = 0;
  }
  base = val - lowPedal;
  if (base < 0) {
    base = 0;
  }
  output = base * convertThrottle;
  if (output > 960) {
    output = 960;
  }
  throttlePedal = (100 / 960) * output;
  analogWrite(outputPin,(output/4));

  */
}

Also for completeness here is the alternate section which handles the immobiliser with random data in response_a but otherwise is much the same as the other version. The immobiliser module hashes this with the RFID code off the key and passes it back and request_b just gives the immobiliser the OK. The only issue you might get is if other versions of the car use a different packet format because both my methods rely on matching the packet structure to pick it out of the data stream.

//******* JC 25/01/21 Immobiliser Compatibility Mods *******

//    Check if First Run Immobiliser Code Scanning is Enabled
    if (digitalRead(Set_Immobiliser) == 0){
        immobiliserCodeSet();
    }

//    Check if stored values exist in EEPROM and if not setup from program defaults

    if (EEPROM.read(0) == 0 && EEPROM.read(1) == 0){
      writeByteArrayIntoEEPROM(0, request_a, 16);
      writeByteArrayIntoEEPROM(16, response_a, 16);
      writeByteArrayIntoEEPROM(32, request_b, 16);
    }

//    Pull Immobiliser codes from EEPROM 

      //delay(5000);                                          // Delay used for serial diagnostics to give the monitor time to connect before sending data
      readByteArrayFromEEPROM(0, request_a, 8);
      Serial.print("Request A from EEPROM : ");
      printhex(request_a,8);
      
      //readByteArrayFromEEPROM(16, response_a, 8);
      response_a[1] = random(255);
      response_a[2] = random(255);
      response_a[3] = random(255);
      response_a[4] = random(255);
      Serial.print("Response A - Random Filler : ");
      printhex(response_a,8);
      
      readByteArrayFromEEPROM(32, request_b, 8); 
      Serial.print("Request B from EEPROM : ");
      printhex(request_b,8);
}

The only other thing you need to do for the random data is generate some random data, I did this by using a floating input as a random seed. The data doesn’t actually need to be random and you can just set fixed values for response_a bytes 1-4 or whatever as long as the rest of the response structure is correct.

void setup() {

  //pinMode(A1, INPUT);
  randomSeed(analogRead(A1));                                   // A5 Not actually connected on Leonardo CAN - Used as seed value

As ever, your mileage may vary and you get no guarantee from me!

RX8 Project – Part 6, Canbus #2

So now we have a powered up RX8 cluster and some Canbus hardware that we can talk to we can start doing a few interesting things with our new setup.

Fire up the Arduino IDE and we can start writing our basic controller code. Canbus devices have a device address and a memory location for all the required data so we need to work all of these out. This is a lot easier for digital IO than the more complicated analog values because digital IO are just either set bit on or off whereas analog values can have funny scaling and things going on. I initially used a canbus demo program which worked because I basically cloned the Arduino Canbus shield. Plenty of starter guides and information can be found here : https://learn.sparkfun.com/tutorials/can-bus-shield-hookup-guide

My initial approach involved setting the target address for the Canbus transmission and sending patterns of bits high and low and see what happens. Google told me that the cluster Canbus interface operates on 500 kHz clock so with that information we should get a connection.

#include <Arduino.h>
#include <mcp_can.h>
#include <mcp_can_dfs.h>

#define CANint 2
#define LED2 8
#define LED3 7

MCP_CAN CAN0(10); // Set CS to pin 10

void setup() {
 // put your setup code here, to run once:
 Serial.begin(115200);
 Serial.println("Init…");

Serial.println("Setup pins");
 pinMode(LED2, OUTPUT);
 pinMode(LED3, OUTPUT);
 pinMode(CANint, INPUT);

Serial.println("Enable pullups");
 digitalWrite(LED2, LOW);
 Serial.println("CAN init:");
 
 if (CAN0.begin(CAN_500KBPS) == CAN_OK) 
 {
 Serial.println("OK!");
 } 
 else 
 {
 Serial.println("fail :-(");
 while (1) 
 {
 Serial.print("Zzz… ");
 delay(1000);
 }
 }

Serial.println("Good to go!");
}

unsigned char offarray[8] = {0, 0, 0, 0, 0, 0, 0, 0}; // Always Off Array
unsigned char onarray[8] = {255,255,255,255,255,255,255,255}; // Always On Array

void loop() {
 // put your main code here, to run repeatedly:

for (int i=0; i <= 512; i++){ 
  for (int l=0; l <= 10; l++){  
    for (int j=0; j <= 100; j++){
       CAN0.sendMsgBuf(i, 0, 8,offarray);
       delay(10);
     }

     for (int k=0; k <= 100; k++){
     CAN0.sendMsgBuf(i, 0, 8,onarray);
     delay(10);
     }
   }
  }
}

So this loop will iterate through 512 addresses strobing all 8 bytes high and low on 1 second intervals. The trick here is that canbus needs regular packets to stay active, so we can’t just send a value once. In this case each value (high or low) is send 100 times at 10ms intervals so the cluster should stay active long enough to see if anything is happening. Each address will get 10 cycles of high and low before moving onto the next address. In concept this would work and provided ‘i’ is set to increment high enough you would cover all the addresses. Bear in mind at this rate you’d have to watch it for about 90 mins…

Thankfully around the time I started running though this trying to find any useful addresses I came across this :

https://www.cantanko.com/rx-8/reverse-engineering-the-rx-8s-instrument-cluster-part-one/

This nicely tied in with the hardware I was working on and so the code and information is all exceedingly useful and saved me a lot of time. Specifically it gives the address locations for most of the indicators on the cluster which is what we really need.

After lots of trial and error I managed to come up with a block of code that can control the cluster but easily allow me to set any warning light I need on the dash, or more accurately also to turn off all the warning lights. Something that will be essential come MOT time as a warning light that stays on is an MOT fail and since most of the systems that control them will no longer be in the car (primarily the original ECU).

#include <Arduino.h>
#include <mcp_can.h>
#include <mcp_can_dfs.h>


#define COMMAND 0xFE
#define CLEAR 0x01
#define LINE0 0x80
#define LINE1 0xC0

#define CANint 2
#define LED2 8
#define LED3 7

#define NOP __asm__ ("nop\n\t")

// Variables for StatusMIL
bool checkEngineMIL;
bool checkEngineBL;
byte engTemp;
byte odo;
bool oilPressure;
bool lowWaterMIL;
bool batChargeMIL;
bool oilPressureMIL;

// Variables for PCM
byte engRPM;
byte vehicleSpeed;

// Variables for DSC
bool dscOff;
bool absMIL;
bool brakeFailMIL;
bool etcActiveBL;
bool etcDisabled;


MCP_CAN CAN0(10); // Set CS to pin 10

void setup() 
{
    
    //Serial.begin(115200);
    //Serial.println("Init…");
    //Serial.println("Setup pins");
    
    pinMode(LED2, OUTPUT);
    pinMode(LED3, OUTPUT);
    pinMode(CANint, INPUT);

    //Serial.println("Enable pullups");
    digitalWrite(LED2, LOW);
    //Serial.println("CAN init:");
    
    if (CAN0.begin(CAN_500KBPS) == CAN_OK) 
    {
        //Serial.println("OK!");
    } 
    else 
    {
        //Serial.println("fail :-(");
        while (1) 
        {
            //Serial.print("Zzz… ");
            delay(1000);
        }
     }

Serial.println("Good to go!");
}

unsigned char stmp[8]       = {0, 0, 0, 0, 0, 0, 0, 0};                         // Always Off Array
unsigned char otmp[8]       = {255,255,255,255,255,255,255,255};                // Always On Array

unsigned char statusPCM[8]  = {125,0,0,0,156,0,0,0};                            // Write to 201
unsigned char statusMIL[8]  = {140,0,0,0,0,0,0,0};                              // Write to 420
unsigned char statusDSC[8]  = {0,0,0,0,0,0,0,0};                                // Write to 212

unsigned char statusEPS1[8] = {0x00,0x00,0xFF,0xFF,0x00,0x32,0x06,0x81};        // Write to 200 0x00 00 FF FF 00 32 06 81
unsigned char statusEPS2[8] = {0x89,0x89,0x89,0x19,0x34,0x1F,0xC8,0xFF};        // Write to 202 0x89 89 89 19 34 1F C8 FF

unsigned char statusECU1[8] = {0x02,0x2D,0x02,0x2D,0x02,0x2A,0x06,0x81};        // Write to 215 - Unknown
unsigned char statusECU2[8] = {0x0F,0x00,0xFF,0xFF,0x02,0x2D,0x06,0x81};        // Write to 231 - Unknown
unsigned char statusECU3[8] = {0x04,0x00,0x28,0x00,0x02,0x37,0x06,0x81};        // Write to 240 - Unknown
unsigned char statusECU4[8] = {0x00,0x00,0xCF,0x87,0x7F,0x83,0x00,0x00};        // Write to 250 - Unknown


/*

215 02 2D 02 2D 02 2A 06 81 // Some ECU status

231 0F 00 FF FF 02 2D 06 81 // Some ECU status

240 04 00 28 00 02 37 06 81 // Some ECU status

250 00 00 CF 87 7F 83 00 00 // Some ECU status

*/


void updateMIL()
{
    statusMIL[0] = engTemp;
    statusMIL[1] = odo;
    statusMIL[4] = oilPressure;
    
  if (checkEngineMIL == 1)
  {
    statusMIL[5] = statusMIL[5] | 0b01000000;
  }
  else
  {
    statusMIL[5] = statusMIL[5] & 0b10111111;
  }
   
    if (checkEngineBL == 1)
  {
    statusMIL[5] = statusMIL[5] | 0b10000000;
  }
  else
  {
    statusMIL[5] = statusMIL[5] & 0b01111111;
  }

   if (lowWaterMIL == 1)
  {
    statusMIL[6] = statusMIL[6] | 0b00000010;
  }
  else
  {
    statusMIL[6] = statusMIL[6] & 0b11111101;
  }

     if (batChargeMIL == 1)
  {
    statusMIL[6] = statusMIL[6] | 0b01000000;
  }
  else
  {
    statusMIL[6] = statusMIL[6] & 0b10111111;
  }

       if (oilPressureMIL == 1)
  {
    statusMIL[6] = statusMIL[6] | 0b10000000;
  }
  else
  {
    statusMIL[6] = statusMIL[6] & 0b01111111;
  }
}

void updatePCM()
{
    statusPCM[0] = engRPM;
    statusPCM[4] = vehicleSpeed;
}

void updateDSC()
{       
  if (dscOff == 1)
  {
    statusDSC[3] = statusDSC[3] | 0b00000100;
  }
  else
  {
    statusDSC[3] = statusDSC[3] & 0b01111011;
  }

    if (absMIL == 1)
  {
    statusDSC[4] = statusDSC[4] | 0b00001000;
  }
  else
  {
    statusDSC[4] = statusDSC[4] & 0b11110111;
  }

      if (brakeFailMIL == 1)
  {
    statusDSC[4] = statusDSC[4] | 0b01000000;
  }
  else
  {
    statusDSC[4] = statusDSC[4] & 0b10111111;
  }

     if (etcActiveBL == 1)
  {
     statusDSC[5] = statusDSC[5] | 0b00100000;
  }
  else
  {
    statusDSC[5] = statusDSC[5] & 0b11011111;
  }

    if (etcDisabled == 1)
  {
    statusDSC[5] = statusDSC[5] | 0b00010000;
  }
  else
  {
    statusDSC[5] = statusDSC[5] & 0b11101111;
  }
 

}

void loop() 
{
    // StatusMIL
    engTemp         = 145;
    odo             = 0;
    oilPressure     = 1;    // Either 0 (fault) or >=1 (Ok)
    checkEngineMIL  = 0;
    checkEngineBL   = 0;
    lowWaterMIL     = 0;
    batChargeMIL    = 0;
    oilPressureMIL  = 0;

    updateMIL();
    CAN0.sendMsgBuf(0x420, 0, 8, statusMIL);
    delay(10);


    // StatusPCM
    engRPM          = 60;    // RPM  Value*67 gives 8500 RPM Reading Redline is 127
    vehicleSpeed    = 93;    // Speed  Value=0.63*(Speed)+38.5

    updatePCM();
    CAN0.sendMsgBuf(0x201, 0, 8, statusPCM);          //CAN0.sendMsgBuf(CAN_ID, Data Type (normally 0), length of data, Data
    delay(10);

    // StatusDSC
    dscOff          = 0;
    absMIL          = 0;
    brakeFailMIL    = 0;
    etcActiveBL     = 0;    // Only works with dscOff set
    etcDisabled     = 0;    // Only works with dscOff set

    updateDSC();
    CAN0.sendMsgBuf(0x212, 0, 8, statusDSC);
    delay(10);

/*
    CAN0.sendMsgBuf(0x200, 0, 8, statusEPS1);
    delay(10);            

    CAN0.sendMsgBuf(0x202, 0, 8, statusEPS2);
    delay(10);    
           
    
    CAN0.sendMsgBuf(0x215, 0, 8, statusECU1);
    delay(10);  

    CAN0.sendMsgBuf(0x231, 0, 8, statusECU2);
    delay(10);  

    CAN0.sendMsgBuf(0x240, 0, 8, statusECU3);
    delay(10);  
    CAN0.sendMsgBuf(0x250, 0, 8, statusECU4);
    delay(10);  

*/
            
 }

This is the current latest version of the code I have, the serial comms section at the start is handy for checking you have a working connection but if you don’t have the serial monitor open it will cause the Arduino to hang at the transmit instruction and not get as far as updating the canbus so I recommend enabling these initially but once you get a working connection comment them back out as above. There are also sections above taken directly from the Cantanko page to deal with the power steering controller and some other bits, I haven’t tried these yes as the cluster isn’t in a car but I plan do so at some stage.

This code has some limitations in this form, basically the warning lights etc are set in the code as static values so this code will just set everything normal and all the lights I have addresses for to off. One particular curiosity is the way the cluster treats the oil pressure value. The data space actually has a whole byte for a value here but the cluster only only has two values despite having an actual gauge, a value of 0 drops the needle to the bottom, a value of 1 or more sets the gauge to normal. My feeling on this is the car was originally intended to have a proper oil pressure transmitter but someone decided it was too expensive and a normal pressure switch was used instead and rather than redesign the cluster they just changed the scaling in the on board microcontroller to behave as described. long term I’d love to mod the cluster to add the analog functionality back in but without the code for the controller this would probably involve disconnecting the original drive to that gauge and adding another Arduino inside the cluster just for driving that gauge. It seems like a lot of work to know oil pressure on a scale of low to high!

The code also sets a speed and RPM however the ODO and trip meters will not increment as it stands – I purposefully left this bit of code out because it’s only really applicable if you’re on an actual car otherwise your cluster will just keep clocking up. The ODO and trips seemed to be the one bit no-one else had managed to do or identify but after quite a few hours of testing  random blocks will various combinations of high and low as well as sending incrementing / decrementing values I noticed my trip meter had actually moved up by 0.1 miles which meant something was working. I eventually managed to trace it to the address listed above. The trick to actually making it work is that the value in the byte is irrelevant to what the cluster displays, so no this doesn’t let you clock it back! What it appears to do is counts the changes to the value so I made it work by setting up a loop of code that keeps changing this value up to a certain number to give a specific distance. Turns out the cluster needs to see 4140 changes per mile. Eventually I plan to tie this in to the speedometer value so only one value needs to be set, by knowing the pulses per mile and the speed we can work out the time delay between each change. As an example at 100mph we need to send 115 changes per second, this is one every 8.7ms which is obviously faster than the loops above and so for speed and accuracy would have to be hardware timed with an interrupt to do the update at the right speed. I’ll probably do an updated version at a later date.

There’s a couple other things I need to sort out, the airbag warning light being the prime one because this is an outright MOT fail. Now in theory when it’s back on the car it should work just fine as the airbag wiring is all there but I figured it was worth working out anyway. So after working through all of the RX8 cluster wiring diagrams (1)(2)(3)(4) I pieced this together :RX8 Cluster Pinout

This covers the majority of everything someone might need to use the cluster on a simulator apart from the fuel gauge. The RX8 fuel gauge is a bit of an oddity because the car uses a saddle shaped fuel tank to allow it to clear the driveshaft and so it has two senders denoted as ‘main’ and ‘sub’.

RX8 Fuel System Senders

Each of these apparently has a resistance value of between about 325Ω at the bottom and 10Ω at the top. There seems to be some questions about how this system works out what to display based on the two sender values and whether it expects to see one remain full while the other empties or something similar. Unfortunately I’ve not played with it much because I’ve don’t actually need to make this work off the car (since the car will have the senders but I can say putting 100Ω a resistor between both 2R-2P and 2R-2T gives a value of about 1/3 of a tank and 9Ω (because I just added a 10Ω on along side the existing 100Ω) across these connections gives a full tank reading  (actually a little above full) so as long as you move both together it works fine, this is ok if all you want to do is get the fuel warning light off for a driving simulator but not that helpful for a single fuel sender conversion which is why most people are interested. If I get chance I’ll investigate further. Also for some reason it takes an absolute age to move up the scale which is a bit unfortunate but I suspect is as intended. At least it should be stable!

All of that leaved us with just one light I can’t get to turn of, the warning light for the electronic power steering. This is controlled via canbus by the look of the circuit diagram but as yet I’ve not found the address for it. If anyone knows the address please leave a comment! Else I will have to go back to either trial and error or the really dodgy backup plan which involves cutting the tracks in the cluster and doubling up on another warning light which isn’t used much! Maybe I should just add another controller in the cluster!

So hopefully that gives you (almost) everything you need to know about making the cluster work! We might get back to mechanical things in the next update…