MS2 Digital dashboard and CAN

A forum for discussing the MegaSquirt related (but non-B&G) board development, assembly, installation, and testing.

Moderators: jsmcortina, muythaibxr

Post Reply
mestarikfk
MS/Extra Newbie
Posts: 16
Joined: Sun Jul 29, 2018 3:42 am

MS2 Digital dashboard and CAN

Post by mestarikfk »

As long as I have had motored vehicles as a hobby, have been getting a lot of help from forums like this. I would like to contribute a little with a "show and tell". Long story short, after I installed MS2 to my project (2016?) I longed for a digital dashboard. I went and got a Raspberry Pi and bought a 7" screen and parsed it all together with some 3D printed parts. At the time, I still had fuel amount and oil pressure as analogue gauges so it was a mix of digital and analogue. I decided that that needs to change, so I started to vision something like this:

Image

In the dotted line was the new set of parts to be incorporated to my build. The gauge cluster was upgraded from 7" to 12.6" to fully fill the area provided. Old setup:

Image

New setup:

Image

The CAN-portion

I had an Arduino Mega 2560 and an MCP2515 CAN-controller laying around since the vision was quite fermented already. I started with a schematic like this to ease my soldering process:

Image

Hardware pictures:

Image

Image

Image

Testing:

Image

The code:

Code: Select all

#include <SPI.h>
#include <mcp2515.h> //https://github.com/autowp/arduino-mcp2515
#include <MegaCAN.h> //https://github.com/mantonakakis1/MegaCAN

//Sensor libraries

  //Ambient temperature
    #include <MBAmbientTemp.h> 
    AmbientTemp AT;
    
    #define AT_EXE_INTERVAL 200 // How often Ambient Temperature is measured (ms)
    unsigned long ATlastExecutedMillis = 0; // Variable to save the last executed time
  
  //Oil Pressure
    #include <MBOilPressure.h>
    OilPressure OP;
  
    #define OP_EXE_INTERVAL 50 // How often Oil Pressure is measured (ms)
    unsigned long OPlastExecutedMillis = 0; // Variable to save the last executed time
  
  //Fuel Amount
    #include <MBFuelAmount.h>
    FuelAmount FA;
    #define FA_EXE_INTERVAL 100 // How often Fuel Amount is measured (ms)
    #define FW_EXE_INTERVAL 100 // How often Fuel Warning is measured (ms)
    unsigned long FAlastExecutedMillis = 0; // Variable to save the last executed time
    unsigned long FWlastExecutedMillis = 0; // Variable to save the last executed time

//MegaCAN
#define CELSIUS // MegaCAN uses Fahrenheit as default. This transforms broadcasted temperatures to Celsius

//MegaCAN
const uint32_t baseID = 1512; // Must set to match Megasquirt Settings!
const uint32_t finalID = baseID + 17; // Must set to match Megasquirt Settings configured in TunerStudio! The last group of data broadcasted.

//MCP2515 related
struct can_frame receivedFrame; 
MCP2515 mcp2515(10);  //CS at D10

MegaCAN MegaCAN; // For processed Megasquirt CAN protocol messages 

MegaCAN_message_t recMsgMSC; // Stores received message from Megasquirt, Megasquirt CAN protocol
MegaCAN_message_t respMsgMSC; // Stores response message back to Megasquirt, Megasquirt CAN protocol
MegaCAN_broadcast_message_t bCastMsg; // Stores unpacked Megasquirt broadcast data, e.g. bCastMsg.rpm

struct can_frame respMsg; // Actual response message back to Megasquirt, MSCAN protocol

uint16_t GPIOADC[8] = { 0 }; // Stores values to send to Megasquirt, 4 ADCS for each message

uint16_t adc0 = 0; 
uint16_t adc1 = 0;
uint16_t adc2 = 0;
uint16_t adc3 = 0;
uint16_t adc4 = 0;
uint16_t adc5 = 0;
uint16_t adc6 = 0;
uint16_t adc7 = 0; 

//Variables for averaging of sensor values
  //AT
  const int ATnumReadings = 10;
  
  int ATreadings[ATnumReadings];    // The readings from the analog input
  int ATreadIndex = 0;              // The index of the current reading
  int ATtotal = 0;                  // The running total
  int ATaverage = 0;                // The average

  //OP
  const int OPnumReadings = 10;
  
  int OPreadings[OPnumReadings];    // The readings from the analog input
  int OPreadIndex = 0;              // The index of the current reading
  int OPtotal = 0;                  // The running total
  int OPaverage = 0;                // The average

  //FA
  const int FAnumReadings = 100;
  
  int FAreadings[FAnumReadings];    // The readings from the analog input
  int FAreadIndex = 0;              // The index of the current reading
  int FAtotal = 0;                  // The running total
  int FAaverage = 0;                // The average

  //FW
  const int FWnumReadings = 100;
  
  int FWreadings[FWnumReadings];    // The readings from the input
  int FWreadIndex = 0;              // The index of the current reading
  int FWtotal = 0;                  // The running total
  int FWaverage = 0;                // The average


void initializeCAN() {
  mcp2515.reset();
  mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ); //Megasquirt specific 500kbs
  mcp2515.setNormalMode();
  
}

void canMShandler(const can_frame &msg) {
  // For Megasquirt CAN protocol, MS is requesting data:
  if ((msg.can_id & CAN_EFF_FLAG) != 0) { //Data request from MS uses extended flag, there may be a better way to implement this with more advanced applications, works fine for sending data to MS GPIOADC
    sendDataToMS(msg); //Due to the extended flag, we assume this is a MS data request and run the function to send data to MS, passing the message received from MS to the sendDataToMS function
  }
  // For Megasquirt CAN broadcast data:
  else { //Broadcast data from MS does not use extended flag, therefore a standard message from MS will contain broadcast data
    //Unpack megasquirt broadcast data into bCastMsg:
    MegaCAN.getBCastData(msg.can_id, msg.data, bCastMsg); //baseID fixed in library based on const parameter entered for baseID above - converts the raw CAN id and buf to bCastMsg format
    if (msg.can_id == finalID) {
      /*~~~Final message for this batch of data, do stuff with the data - this is a simple example~~~*/
      /*
      Serial.print(bCastMsg.map); Serial.print(" | "); //should be kPa
      Serial.print(bCastMsg.rpm); Serial.print(" | "); //should be rpm
      Serial.println(bCastMsg.tps);                      //should be %
      */
    }
  }
}

void sendDataToMS(can_frame msg) {
  MegaCAN.processMSreq(msg.can_id, msg.data, recMsgMSC); // Unpack request message ("msg") from MS into recMsgMS

  if (recMsgMSC.core.toOffset == 2) { //For GPIOADC0-3
    GPIOADC[0] = adc0; //Ambient Temperature
    GPIOADC[1] = adc1; //Oil Pressure
    GPIOADC[2] = adc2; //Fuel Amount
    GPIOADC[3] = adc3; //Fuel Warning. ADC is not the best solution for an on/off, but will do
    MegaCAN.setMSresp(recMsgMSC, respMsgMSC, GPIOADC[0], GPIOADC[1], GPIOADC[2], GPIOADC[3]); //Packs the GPIOADC0-3 values into "respMsgMSC"
  }
  else if (recMsgMSC.core.toOffset == 10) { //For GPIOADC4-7
    GPIOADC[4] = adc4; //vacant
    GPIOADC[5] = adc5; //vacant
    GPIOADC[6] = adc6; //vacant
    GPIOADC[7] = adc7; //vacant
    MegaCAN.setMSresp(recMsgMSC, respMsgMSC, GPIOADC[4], GPIOADC[5], GPIOADC[6], GPIOADC[7]); //Packs the GPIOADC4-7 values into "respMsgMSC"
  }
  
  // Send response to Megasquirt using MSCAN protocol:
  respMsg.can_id = respMsgMSC.responseCore | CAN_EFF_FLAG; //CAN_EFF_FLAG added to the end of response message, otherwise MS will not use it
  respMsg.can_dlc = sizeof(respMsgMSC.data.response);
  for (int i = 0; i < respMsg.can_dlc; i++) {
    respMsg.data[i] = respMsgMSC.data.response[i];
  }
  
  mcp2515.sendMessage(&respMsg); //Sends the GPIOADC values stored in respMsg over CAN to Mesasquirt
  //Serial.println("Data sent to Megasquirt");
}

void setup() 
  {
  while (!Serial);
  Serial.begin(115200);
  Serial.println("MAP | RPM | TPS");
  initializeCAN();
  pinMode(33, INPUT); //Fuel warning indicator switch
//Setup Ambient Temp readings
  for (int ATthisReading = 0; ATthisReading < ATnumReadings; ATthisReading++) //As long as thisReading is smaller than numReadings, add to thisReading
    {
    ATreadings[ATthisReading] = 0;
    }
  AT.init(A2,34); //Current from D34 and measuring voltage from A2  

//Setup Oil Pressure readings
  for (int OPthisReading = 0; OPthisReading < OPnumReadings; OPthisReading++) //As long as thisReading is smaller than numReadings, add to thisReading
    {
    OPreadings[OPthisReading] = 0;
    }
  OP.init(A0,35); //Current from D35 and measuring voltage from A0  

//Setup Fuel Amount readings
  for (int FAthisReading = 0; FAthisReading < FAnumReadings; FAthisReading++) //As long as thisReading is smaller than numReadings, add to thisReading
    {
    FAreadings[FAthisReading] = 0;
    }
  FA.init(A1,32); //Current from D32 and measuring voltage from A1  
  }

void loop() {
  
onReceived(); //Read messages from CAN-bus
getAT(); //Read Ambient Temperature and handle signal
getOP(); //Read Oil Pressure and handle signal
getFA(); //Read Fuel Amount and handle signal
getFW(); //Read Fuel Warning switch and handle signal
}

void onReceived() 
//Listen to CAN-bus and run canMShandler after
{
    
  if (mcp2515.readMessage(&receivedFrame) == MCP2515::ERROR_OK) 
    {
      canMShandler(receivedFrame);
      /*
      Serial.println("Received message:");
      Serial.println("  ID: 0x" + String(receivedFrame.can_id, HEX));
      Serial.println("  DLC: " + String(receivedFrame.can_dlc));
      Serial.println("  Data: " + String(receivedFrame.data[0]) + " " +
                                  String(receivedFrame.data[1]) + " " +
                                  String(receivedFrame.data[2]) + " " +
                                  String(receivedFrame.data[3]) + " " +
                                  String(receivedFrame.data[4]) + " " +
                                  String(receivedFrame.data[5]) + " " +
                                  String(receivedFrame.data[6]) + " " +
                                  String(receivedFrame.data[7]));
    */
  }
}

void getAT()
{
  unsigned long ATcurrentMillis = millis(); // Record current time

  if (ATcurrentMillis - ATlastExecutedMillis >= AT_EXE_INTERVAL) //If elapsed time from last execution is more than the specified interval 
    {
      ATlastExecutedMillis = ATcurrentMillis; // save the last executed time
      //AT
      AT.measure(5,1024,10000,3007,3848,false); //float VCC,int _ADC, float R1, int R25, int Beta, bool prints
      ATtotal = ATtotal - ATreadings[ATreadIndex]; // subtract the last reading
      ATreadings[ATreadIndex] = AT.ambienttempvalue*10; // read from the sensor
      ATtotal = ATtotal + ATreadings[ATreadIndex]; // add the reading to the total
      ATreadIndex = ATreadIndex + 1; // advance to the next position in the array
      if (ATreadIndex >= ATnumReadings) // if we're at the end of the array...
        {
          ATreadIndex = 0; // ...wrap around to the beginning
        }
      ATaverage = ATtotal / ATnumReadings; // calculate the average
      adc0 = ATaverage; // Insert average to variable adc0
      //Serial.println(adc0);
    }  
}

void getOP()
{
  unsigned long OPcurrentMillis = millis(); // Record current time

  if (OPcurrentMillis - OPlastExecutedMillis >= OP_EXE_INTERVAL) //If elapsed time from last execution is more than the specified interval
    {
      OPlastExecutedMillis = OPcurrentMillis; // save the last executed time
      //OP
      OP.measure(5,1024,100,10,69,129,184,false); //float VCC, int _ADC, float R1, int R_0BAR, int R_1BAR, int R_2BAR, int R_3BAR, bool prints
      OPtotal = OPtotal - OPreadings[OPreadIndex]; // subtract the last reading
      OPreadings[OPreadIndex] = OP.oilpressurevalue*100; // read from the sensor
      OPtotal = OPtotal + OPreadings[OPreadIndex]; // add the reading to the total
      OPreadIndex = OPreadIndex + 1; // advance to the next position in the array
      if (OPreadIndex >= OPnumReadings) // if we're at the end of the array...
        {
          OPreadIndex = 0; // ...wrap around to the beginning:
        }
      OPaverage = OPtotal / OPnumReadings; // calculate the average
      adc1 = OPaverage; // Insert average to variable adc1
      //Serial.println(adc1);
    }  
}

void getFA()
{
  unsigned long FAcurrentMillis = millis(); // Record current time

  if (FAcurrentMillis - FAlastExecutedMillis >= FA_EXE_INTERVAL) //If elapsed time from last execution is more than the specified interval
    {
      FAlastExecutedMillis = FAcurrentMillis; // save the last executed time
      //FA
      FA.measure(5,1024,10,2,78,false); //float VCC,int _ADC, float R1, int R_Full, int R_Empty, bool prints
      FAtotal = FAtotal - FAreadings[FAreadIndex]; // subtract the last reading
      FAreadings[FAreadIndex] = FA.fuelamountvalue*100; // read from the sensor
      FAtotal = FAtotal + FAreadings[FAreadIndex]; // add the reading to the total
      FAreadIndex = FAreadIndex + 1; // advance to the next position in the array
      if (FAreadIndex >= FAnumReadings) // if we're at the end of the array...
        {
          FAreadIndex = 0; // ...wrap around to the beginning
        }
      FAaverage = FAtotal / FAnumReadings; // calculate the average
      adc2 = FAaverage; // Insert average to variable adc2
      //Serial.println(adc2);
    }  
}
void getFW()
{
 unsigned long FWcurrentMillis = millis(); // Record current time

  if (FWcurrentMillis - FWlastExecutedMillis >= FW_EXE_INTERVAL) //If elapsed time from last execution is more than the specified interval
    {
      FWlastExecutedMillis = FWcurrentMillis; // save the last executed time
      //FW
      FWtotal = FWtotal - FWreadings[FWreadIndex]; // subtract the last reading
      int sensorVal = digitalRead(33); // read port status:
      if (sensorVal = LOW) // If the switch is closed...
      {
        FWreadings[FWreadIndex] = 1; //...reading is one
      }
      else
      {
        FWreadings[FWreadIndex] = 0; //...reading is zero
      }
      FWtotal = FWtotal + FWreadings[FWreadIndex]; // add the reading to the total
      FWreadIndex = FWreadIndex + 100; // advance to the next position in the array
      // if we're at the end of the array...
      if (FWreadIndex >= FWnumReadings) 
        {
          FWreadIndex = 0; // ...wrap around to the beginning
        }
    FWaverage = FWtotal / FWnumReadings; // calculate the average
    adc3 = FWaverage; // Insert average to variable adc3
    //Serial.println(adc3);
    }  
}
Special thanks to mikey antonakakis for the CAN-communication. This code is different from mikey in a way that this code works for Arduino + CAN-controller (CAN not integrated) combo while the original works with teensy (CAN integrated). If you were to use this code, please save the "libraries" from the attachment to your Arduino library location. As a disclaimer, the code I wrote is specific to Mercedes W201/W124 sensors, but the sensor libraries are coded so that they can be used with other sensors too. The Fuel amount (FA) sensor measurement assumes linear change in resistance as the fuel amount changes. Oil pressure (OP) sensor measurement assumes a piecewise linear behaviour, where the resistance is set at different pressures and acts linearly between those points. Ambient temperature (AT) assumes a NTC-thermistor and you will need to know the R₂₅ and β values to get it working. More insight on the sensors:

Fuel amount:

Image

Oil pressure:

Image

Ambient temperature:

Image

This setup has been on my car for about 4 months now, with ~2000km driven and no problems except when the fuel tank is full to the brim, the resistance value is outside of my parameters resulting in the fuel gauge reading 0 for a while. It gets to 100 after spending some time on the happy pedal :lol: There is still some untapped potential, since I have a lot of I/O left for controlling relays and sending info to Megasquirt. Let's see what arises. Lastly, let's take a look on how the signals are handled in tunerstudio:

Image

For every sensor you need to add a custom channel. Since you cannot send decimals to the CAN-bus, you need to multiply by ten on the Arduino side and divide by ten on TS side. Adding a gauge template is a good measure too.

That is all for now, feel free to comment!
You do not have the required permissions to view the files attached to this post.
grom_e30
Super MS/Extra'er
Posts: 4461
Joined: Thu Mar 08, 2012 12:44 pm
Location: UK

Re: MS2 Digital dashboard and CAN

Post by grom_e30 »

nice project, i use an arduino as a can translator between my car and megasquirt for things like the gauges and the starter and ac requests from the car to the ecu makes the abs and electric power steering happy without the stock ecu.

whats the boot time like on tsdash?
1990 bmw 320i daily driver with m20b25 ms3 sequential fuel, 380cc injectors, d585 coil near plug, home made cam sync, launch control, fan control, vss, homebrew egt logging what's next????
Laminar
Master MS/Extra'er
Posts: 657
Joined: Wed Aug 06, 2014 7:45 am

Re: MS2 Digital dashboard and CAN

Post by Laminar »

Great work! What screen and controller did you go with?
mestarikfk
MS/Extra Newbie
Posts: 16
Joined: Sun Jul 29, 2018 3:42 am

Re: MS2 Digital dashboard and CAN

Post by mestarikfk »

grom_e30 wrote: Thu Jul 20, 2023 1:44 pm nice project, i use an arduino as a can translator between my car and megasquirt for things like the gauges and the starter and ac requests from the car to the ecu makes the abs and electric power steering happy without the stock ecu.

whats the boot time like on tsdash?
Thank you! That sounds like a good application to keep the car happy without the original ECU. The boot time is 40-50s even after optimizing by disabling services and removing software. I have the full version of TS, if I were to use TS Dash it would be in the ~20-30s region. I have managed to save some time by creating a circuit that starts the dash when I open my doors. That saves about 15s from ignition on to full screen gauges.
Laminar wrote: Thu Jul 20, 2023 1:54 pm Great work! What screen and controller did you go with?
Thanks! It's a NV126B5M-N41 with a touch screen. The controller came with the screen, but it's a 30pin dual HDMI input controller with audio input and output, which are not used in my application though. The touch screen works great and the resolution and brightness is good. the touch screen might benefit from some sort of software though, since double clicking can be a hassle and a long click should come out as a right click, but it doesn't. One good thing with this controller is that it supports sending commands through the HDMI cable. I use a LDR-sensor to read the ambient brightness and use an algorithm to define the needed screen brightness. In the code I use ddcutil to send the actual commands to the screen.
Post Reply