It’s here! Part 1 how to Display Data Over a CAN bus From Your Megasquirt or Microsquirt

An alternative to tablets and PCs

One of the cool things about the Megasquirt fuel injection system is its ability to broadcast data over a CAN bus. This is exactly how OEM automotive systems connect all the different vehicle control systems in modern vehicle and provides a very flexible platform for aftermarket connectivity as well.

1st let’s quickly go over, at a high level, what CAN bus is and how it operates. CAN is a multi-master serial bus standard for connecting vehicle subsystems (e.g. a vehicle’s ECU is a subsystem or node).  There’s physical aspects and software aspects. Wiring (a physical aspect) is generally a twisted pair with a 120Ω resistor at each end of the bus. This is important to note as improperly located or missing resistors will cause issues. The CAN protocol is one of the software aspects. The Megasquirt firmware uses an 11-bit protocol (for basic dash broadcasting) and a proprietary 29bit protocol for more in-depth communication between Mega/Microsquirt devices. For example, a Megasquirt unit controlling the engine and a Microsquirt controlling the transmission.

After soaking this information in I wanted to make use of what I feel is a very robust and reliable way to transfer data out of the Megasquirt or Microsquirt controller and display that in a useful manner. There are plenty of tablet based projects running Raspberry Pi or simply a full-blown Windows or Linux tablet, but I like the simple aspect of no operating system in between. It’s pretty much just data out of the controller and straight to the display no boot times, crashes, patching, etc., etc.

Unfortunately, it’s not as simple as broadcasting the CAN data out and picking that up with the display. There’s a few more pieces you’ll need. 1st you will need a CAN transceiver, this converts the CAN data in to a serial stream. For this I used an inexpensive Waveshare CAN transceiver from Amazon. Then you’ll need something to manipulate the data so your display can show it in an understandable format. I used a Teensy 3.2 microcontroller. The Teensy is an awesome little microcontroller that works with Arduino programming software.  And finally, you will need the display device itself so I tried out the Nextion 3.2” touch screen display. I like the Nextion as it offloads a lot of the processing load of the display from the microcontroller and is very easy to program, plus it is a touchscreen.

As you can see there’s going to be a few items with learning curves if you’ve never touched this stuff before. But the Arduino IDE and Nextion software are very easy to learn. This was my very first project with all of it!

Project in action. This is a simple video showing some of the different aspects of the operation. Visually still a work in progress, but this will help you get an idea of what you can do. You’ll find that the graphics are by far the item you’ll spend the most time on.

Here is the full parts list:

  1. Nextion 3.2″ TFT Display
  2. Waveshare CAN transceiver
  3. Teensy 3.2 Arduino compatible micro-controller
  4. Microsquirt v3 cased

Arduino sketch

/*
   Project that displays Megasquirt engine control data over CAN utilizing a Teensy 3.2 and Nextion 3.2 touch screen display.
   Thanks again to defragster on PJRC forums for the different tips and tricks!

   Hardware used is Teensy 3.2 http://www.pjrc.com/store/teensy32.html
   Nextion 3.2" resistive touch display https://www.itead.cc/nextion-nx4024t032.html
   WaveShare SN65HVD230 CAN Board http://www.amazon.com/gp/product/B00KM6XMXO?psc=1&redirect=true&ref_=od_aui_detailpages00
   Megasquirt/Microsquirt hardware from http://www.diyautotune.com/

   Thank you to xrattiracer for the inspiration here https://github.com/merkur2k/MSCan_Gauge/tree/teensy

*/


#include 
#include 

FlexCAN CANbus(500000);
static CAN_message_t rxmsg;

//Megasquirt data vars
byte indicator[7]; // where to store indicator data
float  BATTV, IAC, dwell, idle_tar, AFRtgt, AFR, newBATTV, oldBATTV;
unsigned int MAP, SPKADV, RPM, TPS, MAT, CLT, injduty, Baro, PW1, nexAFR, nexCLT;

void setup() {


  //Initialize CAN and start Serial and Serial2, Serial2 is for Nextion display
  CANbus.begin();

  //  Serial.begin(115200); //***Uncomment this section to output CAN message IDs to Serial Monitor***
  //    while (!Serial) ;
  //    Serial.println("Hello Megasquirt");
  Serial2.begin(115200);
}


void loop(void) {

  // Gauge display function
  gauge_display();

  //Look for CAN broadcasts
  if ( CANbus.read(rxmsg) ) {
    switch (rxmsg.id) { // ID's 1520+ are Megasquirt CAN broadcast frames. EAch frame represents a data group http://www.msextra.com/doc/pdf/Megasquirt_CAN_Broadcast.pdf
      case 1520: // Group 0
        RPM = (float)(word(rxmsg.buf[6], rxmsg.buf[7]));
        PW1 = (float)(word(rxmsg.buf[2], rxmsg.buf[3]));
        injduty = ((PW1 / 1000 * RPM / 120) / 10);
        break;
      case 1521: // Group 1
        SPKADV = (float)(word(rxmsg.buf[0], rxmsg.buf[1]));
        indicator[0] = rxmsg.buf[3]; // engine
        AFRtgt = (float)(word(0x00, rxmsg.buf[4]));
        break;
      case 1522: // Group 2
        Baro = (float)(word(rxmsg.buf[0], rxmsg.buf[1]));
        MAP = (float)(word(rxmsg.buf[2], rxmsg.buf[3]));
        MAT = (float)(word(rxmsg.buf[4], rxmsg.buf[5]));
        CLT = (float)(word(rxmsg.buf[6], rxmsg.buf[7]));
        nexCLT = (float)(word(rxmsg.buf[6], rxmsg.buf[7]));

        break;
      case 1523: // Group 3
        TPS = (float)(word(rxmsg.buf[0], rxmsg.buf[1]));
        BATTV = (float)(word(rxmsg.buf[2], rxmsg.buf[3]));
        AFR = (float)(word(rxmsg.buf[4], rxmsg.buf[5]));
        nexAFR = (float)(word(rxmsg.buf[4], rxmsg.buf[5]));
        break;
      case 1524: // Group 4
        break;
      case 1526: // Group 6
        IAC = (float)(word(rxmsg.buf[6], rxmsg.buf[7])); //IAC = (IAC * 49) / 125;
      case 1529: // 9
        dwell = (float)(word(rxmsg.buf[4], rxmsg.buf[5]));
        break;
      case 1530: // Group 10
        indicator[1] = rxmsg.buf[0]; // status 1
        indicator[2] = rxmsg.buf[1]; // status 2
        indicator[3] = rxmsg.buf[2]; // status 3
        indicator[6] = rxmsg.buf[6]; // status 6
        indicator[7] = rxmsg.buf[7]; // status 7
        break;
      case 1537: // Group 17

        break;
      case 1548: // Group 28
        idle_tar = (float)(word(rxmsg.buf[0], rxmsg.buf[1]));
        break;
      case 1551: // Group 31
        break;
      case 1574: // Group 54
        indicator[4] = rxmsg.buf[2]; // cel
        break;

    }


  }
}

elapsedMillis DisplayTime; //Establish a timer to prevent unnecessary screen rewrites

void gauge_display() {  //Prints captured data from above to display

  if ( DisplayTime < 150 ) return;
  DisplayTime = 0;

  //Serial.println(rxmsg.id); Any type of data you want to go to Serial Monitor place here


  // Display Spark Advance

  Serial2.print("t3.txt=");
  Serial2.write(0x22);
  Serial2.print(SPKADV / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  // Display Engine MAP

  Serial2.print("t4.txt=");
  Serial2.write(0x22);
  Serial2.print(MAP / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  // Display Engine Air Fuel Ratio

  Serial2.print("j6.val=");
  Serial2.print(nexAFR / 2); //Nextion is weird and will not display floating point data for a progress bar, so I had to massage the output for the progress bar *only*
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("t14.txt=");
  Serial2.write(0x22);
  Serial2.print(AFR / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  // Define WOT AFR Thresholds

  if ((AFR / 10 < 10.5) && (AFR / 10 > 10) && (TPS / 10 > 80))
    gauge_display_AFR_YELLOW();

  if ((AFR / 10 < 10.0) && (TPS / 10 > 80))
    gauge_display_AFR_RED();

  if ((AFR / 10 < 12.5) && (AFR / 10 > 10.5) && (TPS / 10 > 80))
    gauge_display_AFR_BLACK();

  if ((AFR / 10 > 12.5) && (TPS / 10 > 80))
    gauge_display_AFR_RED();




  // Display Injector Duty Cycle

  Serial2.print("j2.val=");
  Serial2.print(injduty);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("t15.txt=");
  Serial2.write(0x22);
  Serial2.print(injduty);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  //  Several functions to alert if duty cycle is above certain thresholds

  if (injduty < 60)
    gauge_display_injduty_black();

  if ((injduty >= 60) && (injduty < 79))

    gauge_display_injduty_yellow();

  if (injduty > 79)

    gauge_display_injduty_RED();


  // Display Throttle Position

  Serial2.print("j3.val=");
  Serial2.print(TPS / 10);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("t16.txt=");
  Serial2.write(0x22);
  Serial2.print(TPS / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  // Battery voltage generally does not change that fast, so this is trying to reduce a bit of overhead by not updating if the data has not changed
  newBATTV = BATTV / 10;
  if ( newBATTV != oldBATTV )
    gauge_display_BATTV();

  Serial2.print("t8.txt=");
  Serial2.write(0x22);
  Serial2.print(MAT / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  // Display Engine RPM

  Serial2.print("t0.txt=");
  Serial2.write(0x22);
  Serial2.print(RPM);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("j5.val=");
  Serial2.print(RPM / 100);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  // Thresholds for warning colors for Engine RPM

  if (RPM < 5000)
    gauge_display_RPM_BLACK();

  if ((RPM > 5000) && (RPM < 6000))
    gauge_display_RPM_YELLOW();

  if ((RPM > 6000))
    gauge_display_RPM_RED();

  // Display Engine Coolant Temperature

  Serial2.print("t17.txt=");
  Serial2.write(0x22);
  Serial2.print(CLT / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("j4.val=");
  Serial2.print(nexCLT / 20);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);
}

// Gauge functions for thresholds

void gauge_display_injduty_black() {

  Serial2.print("j2.pco=BLACK");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("t15.pco=BLACK");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);
}

void gauge_display_injduty_yellow() {

  Serial2.print("j2.pco=YELLOW");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_injduty_RED() {

  Serial2.print("j2.pco=RED");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_BATTV() {

  oldBATTV =
    Serial2.print("t6.txt=");
  Serial2.write(0x22);
  Serial2.print(BATTV / 10);
  Serial2.write(0x22);
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_RPM_BLACK() {


  Serial2.print("t0.pco=BLACK");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("j5.pco=BLACK");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_RPM_YELLOW() {

  Serial2.print("t0.pco=YELLOW");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

  Serial2.print("j5.pco=YELLOW");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_RPM_RED() {

  Serial2.print("t0.pco=RED");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);


  Serial2.print("j5.pco=RED");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_AFR_RED() {


  Serial2.print("j6.pco=RED");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_AFR_YELLOW() {


  Serial2.print("j6.pco=YELLOW");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}

void gauge_display_AFR_BLACK() {

  Serial2.print("j6.pco=BLACK");
  Serial2.write(0xff);
  Serial2.write(0xff);
  Serial2.write(0xff);

}