May 2023

Feather Pressure Sender + CAN bus


Functional Requirements

  • Replace existing transmission oil pressure monitor display (Feather)
  • Measure pressure from 2x 0 to 30 psi pressure sensors mounted to the air intake and air outlet of the boat diesel engine aftercooler (charge air cooler) and calculate the pressure differential.   > 7 psi difference indicates air restriction.  
  • Automatically cycle display between the pressure readings, and include a labels of "trans [psig]", "CAC in [psig]", "CAC out [psig]", "CAC delta [psig]".  
  • 12 to 14 VDC power input

 

Hardware

Pressure Measurement

The 2x pressure sensors for the air intake and outlet on the diesel engine aftercooler are 0 to 30 psig / 5V senders.  

 

Display

 

Software

CAN Tx device attached to the pressure sensors:


/*
  Adafruit Feather M4 CAN (SAMD51)


  In Arduino IDE, set board to 'Adafruit Feather M4 Express (SAMD51)'

  If bootloading frozen, click RST button twice quickly. 

  The red LED will pulse and the RGB LED will be green when you are
  in bootloader mode. 

  NeoPixel = green if OK, RED on USB failure. 

  The yellow “charging” LED flickers constantly whenever the Feather is powered by USB

  Arduino C++ code or CircuitPython 

  6x analog input pins:  A0,A1,A2,A3,A4,A5  (0 to 3.3V)
  


  WARNING:  The Arduino IDE serial monitor causes setup() to wait until 
  the serial monitor IDE is run. 

  In Arduino IDE:
    Set the board to:  "Adafruit Feather M4 CAN (SAME51)"
  
*/

/////////////////////////////////////////////////////////////////////////////\/
// Show serial messages when DEBUG = true, otherwise minimize them.
#define DEBUG false
//////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////
// Built in LED(s)

const uint8_t pinBuiltInLED = 13;  

const byte pinLiPoly = A6;  // #20

/////////////////////////////////////////////////////////////////////////
// Built-in NeoPixel (RGB LED) 
//
// set it up as a single-LED strand on pin 8

const uint8_t pinBuiltInNeoPixel = 8;  

#include <Adafruit_NeoPixel.h>
// https://github.com/adafruit/Adafruit_NeoPixel
#define NUMPIXELS  1
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, pinBuiltInNeoPixel, NEO_GRB + NEO_KHZ800);
// Argument 1 = Number of pixels in NeoPixel strip
// Argument 2 = Arduino pin number (most are valid)
// Argument 3 = Pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)

/////////////////////////////////////////////////////////////////////////
// CAN bus

// Install library: https://github.com/adafruit/arduino-CAN
// arduino-CAN-master.zip
// Examples -> CAN Adafruit Fork -> 
#include <CAN.h>

unsigned char canMsg[8] = {0, 0, 0, 0, 0, 0, 0, 0};

// These functions are written around the concept of using at
// least two bytes to represent a float or integer value.  
// This allows you to send up to four separate float or integer
// values within a CAN message.  

void updateCanMsgFromFloat(unsigned char *CANmsg, float floatVal, int startBit, int length, int msgOffset, float factor) {
  // Update msg[8] within CANmsg[] with the passed parameters
  // Assumes length = 16;
  // unsigned int floatToIntCANbus(float myFloat, int msgOffset, float factor) {
  unsigned int myInt = floatToIntCANbus(floatVal, msgOffset, factor);
  byte msb = getMsbFromInt(myInt);
  byte lsb = getLsbFromInt(myInt);
  switch (startBit) {
  case 0:
    CANmsg[0] = msb;
    CANmsg[1] = lsb;      
    break;
  case 16:
    CANmsg[2] = msb;
    CANmsg[3] = lsb;      
    break;
  case 32:
    CANmsg[4] = msb;
    CANmsg[5] = lsb;      
    break;
  case 48:
    CANmsg[6] = msb;
    CANmsg[7] = lsb;      
    break;
  }
}


unsigned int floatToIntCANbus(float myFloat, int msgOffset, float factor) {
  // float myFloat = 2128.5;
  // unsigned int myInt = floatToIntCANbus(myFloat, 0, 0.125);
  //
  // Beginning with float of 2128.5, convert to CAN signal
  // values.
  // (int val) = ((float val) - msgOffset) / factor;
  // (int val) = ((2128.5) - 0.0) / 0.125;
  // (int val) = 17028
   
  myFloat = myFloat - (float)msgOffset;
  myFloat = myFloat / factor;
  unsigned int myInt = (unsigned int) myFloat;
  return myInt;
}

byte getMsbFromInt(int myInt) {
  // int myInt = 17028;
  // byte msb = getMsbFromInt(myInt); 
  byte msb = myInt & 0xff;
  return msb;
}

byte getLsbFromInt(int myInt) {
  // int myInt = 17028;
  // byte lsb = getLsbFromInt(myInt);
  byte lsb = (myInt >>8) & 0xff;
  return lsb;
}

//////////////////////////////////////////////////////////////////////////////
// TimerA
const unsigned long timerA = 5000;  
unsigned long timerAlap = millis();  // timer

/////////////////////////////////////////////////////////////////////////
// pressure sensors

#define PIN_TRANS A0
#define PIN_CAC_IN A1
#define PIN_CAC_OUT A2

uint32_t trans_press_adc = 0;
uint32_t eng_cac_in_adc = 0;
uint32_t eng_cac_out_adc = 0;
uint32_t samples = 0;

/////////////////////////////////////////////////////////////////////////


void setup() {

  // For ATSAMD21 and ATSAMD51:
  //  ARef pin, use analogReference(AR_EXTERNAL)
  //  Pin with pullup:
  //    Use: pinMode(pin, INPUT_PULLUP)
  //    NOT: pinMode(pin, INPUT)
  //         digitalWrite(pin, HIGH)

  #if DEBUG
  Serial.begin(115200);
  while (!Serial) {
    delay(1);
  }
  Serial.println("\nSerial ready");
  #endif

  pinMode(PIN_TRANS, INPUT);
  pinMode(PIN_CAC_IN, INPUT);
  pinMode(PIN_CAC_OUT, INPUT);

  //Below is not necessary
  //pinMode(pinBuiltInLED, OUTPUT);
  //digitalWrite(pinBuiltInLED, LOW);

  // Initialize the built-in NeoPixel (initially set to green)
  pixels.begin();
  pixels.show();  

  // turn on both the transceiver and booster:
  pinMode(PIN_CAN_STANDBY, OUTPUT);   // pin #40
  digitalWrite(PIN_CAN_STANDBY, false); // turn off STANDBY
  pinMode(PIN_CAN_BOOSTEN, OUTPUT); // #41
  digitalWrite(PIN_CAN_BOOSTEN, true); // turn on booster  
  // start the CAN bus at 250 kbps
  if (!CAN.begin(250E3)) {
    #if DEBUG
    Serial.println("CAN init failed!");
    #endif
    pixels.setPixelColor(0, 255, 0, 0, 4);  // red
    pixels.show();
    while (1);
  }

  #if DEBUG
  Serial.println("\nSetup complete\n");
  #endif
} // setup()


void loop() {

  // Continuously read the voltage as an ADC value on pins 
  // PIN_TRANS, PIN_CAC_IN, PIN_CAC_OUT and 
  // add them to trans_press_adc, eng_cac_in_adc, eng_cac_out_adc.
  // Update 'samples' as well, so the average value can be
  // calculated within the publishing timer. 
  //
  // Feather M4 CAN SAME51 analog pins are 12-bit (0 to 4095)
  // BUT for some reason the range is only 0 to 1023. 
  // AREF is tied to 3.3V due to a silicon v0 bug.
  uint32_t max_samples = 4294967295 / 1023;
  //Serial.print("max samples: "); Serial.println(max_samples); // 4198404
  if (samples >= max_samples) {
    trans_press_adc = 0;
    eng_cac_in_adc = 0;
    eng_cac_out_adc = 0;
    samples = 0;
    #if DEBUG
    Serial.println("max_samples exceeded!");
    #endif
  }
  uint16_t adc = analogRead(PIN_TRANS); // 0 to 1023
  trans_press_adc += adc;
  
  adc = analogRead(PIN_CAC_IN); // 0 to 1023
  eng_cac_in_adc += adc;
  
  adc = analogRead(PIN_CAC_OUT); // 0 to 1023
  eng_cac_out_adc += adc;
  samples++;

  //  Timer A
  if (timerAlap > millis())  timerAlap = millis();
  if (millis() - timerAlap > timerA) { 
    pixels.setPixelColor(0, 0, 0, 255, 4);  // blue
    pixels.show();
    
    // clear out CANmsg[]
    for (int i=0; i<8; i++) {
      canMsg[i] = 0;
    }

    // Send a CAN msg with 11 bit ID and 8 bytes of data

    CAN.beginPacket(0x91);    // 0x91 = 145

    // updateCanMsgFromFloat(float floatVal, int startBit, int Length, int msgOffset, float factor) 
    // transmission pressure 0 to 400 psi
    float f = (float)trans_press_adc / float(samples) * (3.3/1023.0); 
    // PSI = (VDC) * 167.364 - 36.82
    f = f * 167.364 - 36.82;
    updateCanMsgFromFloat(canMsg, f, 0, 16, 0, 0.1);
    
    // CAC in [psi]     PSIG = (VDC) * 10.24 - 3.788
    f = (float)eng_cac_in_adc / float(samples)  * (3.3/1023.0); 
    f = f * 10.24 - 3.788;
    updateCanMsgFromFloat(canMsg, f, 16, 16, 0, 0.01);
    
    // CAC out [psi]    PSIG = (VDC) * 10.24 - 3.788
    f = (float)eng_cac_out_adc / float(samples)  * (3.3/1023.0); 
    f = f * 10.24 - 3.788;
    updateCanMsgFromFloat(canMsg, f, 32, 16, 0, 0.01);
    
    // CAC delta [psi]  PSIG = (VDC) * 10.24 - 3.788
    f = ((float)eng_cac_out_adc - (float)eng_cac_in_adc) / float(samples) * ((3.3/1023.0)); 
    f = f * 10.24 - 3.788;
    updateCanMsgFromFloat(canMsg, f, 48, 16, 0, 0.01);

    trans_press_adc = 0;
    eng_cac_in_adc = 0;
    eng_cac_out_adc = 0;
    samples = 0;
    
    for (int i = 0; i < 8; i++) {
      CAN.write(canMsg[i]);
    }
    CAN.endPacket();

    #if DEBUG
    Serial.print("Sending to ID 0x91");
    Serial.print("\t");
    for (int i = 0; i < 8; i++) {
      Serial.print(canMsg[i], HEX);
      Serial.print("\t");
    }
    Serial.println("\n");    
    #endif  
    
    pixels.setPixelColor(0, 0, 0, 0, 4);  // black
    pixels.show();
    timerAlap = millis(); // reset the timer
  }
  
} // loop()

 

CAN bus Rx device that displays the pressure values on an OLED:


/* 
  Adafruit Feather M4 CAN (SAMD51)

  Adafruit 128x32 mono OLED FeatherWing

  In Arduino IDE, set board to 'Adafruit Feather M4 Express (SAMD51)'

  If bootloading frozen, click RST button twice quickly. 

  The red LED will pulse and the RGB LED will be green when you are
  in bootloader mode. 

  NeoPixel = green if OK, RED on USB failure. 

  The yellow “charging” LED flickers constantly whenever the Feather is powered by USB

  Arduino C++ code or CircuitPython 

  6x analog input pins:  A0,A1,A2,A3,A4,A5  (0 to 3.3V)
  


  WARNING:  The Arduino IDE serial monitor causes setup() to wait until 
  the serial monitor IDE is run. 

  In Arduino IDE:
    Set the board to:  "Adafruit Feather M4 CAN (SAME51)"
  
*/

/////////////////////////////////////////////////////////////////////////////\/
// Show serial messages when DEBUG = true, otherwise minimize them.
#define DEBUG false
//////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////
// Built in LED(s)

const uint8_t pinBuiltInLED = 13;  

const byte pinLiPoly = A6;  // #20

/////////////////////////////////////////////////////////////////////////
// Built-in NeoPixel (RGB LED) 
//
// set it up as a single-LED strand on pin 8

const uint8_t pinBuiltInNeoPixel = 8;  

#include <Adafruit_NeoPixel.h>
// https://github.com/adafruit/Adafruit_NeoPixel
#define NUMPIXELS  1
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, pinBuiltInNeoPixel, NEO_GRB + NEO_KHZ800);
// Argument 1 = Number of pixels in NeoPixel strip
// Argument 2 = Arduino pin number (most are valid)
// Argument 3 = Pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)


/////////////////////////////////////////////////////////////////////////
// CAN bus

// Install library: https://github.com/adafruit/arduino-CAN
// arduino-CAN-master.zip
// Examples -> CAN Adafruit Fork -> 
#include <CAN.h>

float getFloatFromCanMsg(unsigned char *CANmsg, int startBit, int msgLen, int msgOffset, float factor) {
  // Read the data from msg[8] within CANmsg[] 
  // convert it with the passed parameters, and return a float value.
  // Assumes msgLen = 16
  byte msb;
  byte lsb;
  switch (startBit) {
  case 0:
    msb = CANmsg[0];
    lsb = CANmsg[1];
    break;
  case 16:
    msb = CANmsg[2];
    lsb = CANmsg[3];
    break;
  case 32:
    msb = CANmsg[4];
    lsb = CANmsg[5];
    break;
  case 48:
    msb = CANmsg[6];
    lsb = CANmsg[7];
    break;
  }
  int myInt = (lsb << 8) | msb;
  // float CANbusIntToFloat(unsigned int myInt, int msgOffset, float factor) {
  float myFloat = CANbusIntToFloat(myInt, msgOffset, factor);
  return myFloat;
}


float CANbusIntToFloat(unsigned int myInt, int msgOffset, float factor) {
  // value in decimal = (CAN DEC value) * factor + offset
  // 17500 * 0.125 + 0 = 2187.5 rpm
  
  float myFloat = (float) myInt * factor + (float) msgOffset;
  return myFloat;
}

/////////////////////////////////////////////////////////////////////////
//  Adafruit 128x32 mono OLED FeatherWing

#include "SPI.h"
#include "Wire.h"
#include "Adafruit_GFX.h"
#include "Adafruit_SSD1306.h"
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Configure the buttons, based on the microcontroller core
#define BUTTON_A 9
#define BUTTON_B 6
#define BUTTON_C 5
#define LED      13

#if (SSD1306_LCDHEIGHT != 32)
 #error("Height incorrect, please fix Adafruit_SSD1306.h!");
#endif
//Adafruit_SSD1306 display = Adafruit_SSD1306();

/////////////////////////////////////////////////////////////////////////

uint8_t last_val = 0;


void setup() {

  // For ATSAMD21 and ATSAMD51:
  //  ARef pin, use analogReference(AR_EXTERNAL)
  //  Pin with pullup:
  //    Use: pinMode(pin, INPUT_PULLUP)
  //    NOT: pinMode(pin, INPUT)
  //         digitalWrite(pin, HIGH)

  #if DEBUG
  Serial.begin(115200);
  while (!Serial) {
    delay(1);
  }
  Serial.println("\nSerial ready");
  #endif
  
  // Initialize the built-in NeoPixel (initially set to green)
  pixels.begin();
  pixels.show();  

  // initialize with the I2C addr 0x3C (for the 128x32)
  // The OLED I2C address is 0x3C and cannot be changed
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    #if DEBUG
    Serial.println(F("SSD1306 allocation failed"));
    #endif
    while(true) {
      pixels.setPixelColor(0, 255, 0, 0, 4);  // red
      pixels.show();
      delay(500);
      pixels.setPixelColor(0, 255, 255, 255, 4);  // white
      pixels.show();
      delay(500);
    }
    for(;;); // Don't proceed, loop forever
  }  
  
  // Configure the three buttons on the OLED FeatherWing
  // BUTTON_A: 9, BUTTON_B: 6, BUTTON_C: 5
  pinMode(BUTTON_B, INPUT_PULLUP);
  pinMode(BUTTON_C, INPUT_PULLUP);
  
  // Clear the display buffer.
  display.clearDisplay();
  //display.setTextColor();  BLACK,BLUE,RED,GREEN,CYAN,MAGENTA,YELLOW,WHITE
  //display.setTextColor(text color, background color);
  //Sets the text color and background color the text will print on.
  //For monochrome (single-color) displays, colors are always specified as simply 1 (set) or 0 (clear).
  display.setTextColor(WHITE);
  //  .setTextSize(2) will change the character sizes as follows:
  //    2*6 x 2*8 = 12x16 pixels.
  //  For a 128x32 OLED, 128/12 = 10 chars wide; 32/16 = 2 characters tall
  display.setTextSize(2);
  display.setCursor(0,0);
  //display.print("0123456789");
  display.print("Knot");
  //
  display.setCursor(0,16);
  //display.print("0123456789");
  display.print("    Workin");
  display.display();
  
  //Below is not necessary
  //pinMode(pinBuiltInLED, OUTPUT);
  //digitalWrite(pinBuiltInLED, LOW);

  // turn on both the transceiver and booster:
  //Serial.print("PIN_CAN_STANDBY: "); Serial.println(PIN_CAN_STANDBY);
  pinMode(PIN_CAN_STANDBY, OUTPUT);   // pin #40
  digitalWrite(PIN_CAN_STANDBY, false); // turn off STANDBY
  //Serial.print("PIN_CAN_BOOSTEN: "); Serial.println(PIN_CAN_BOOSTEN);
  pinMode(PIN_CAN_BOOSTEN, OUTPUT); // #41
  digitalWrite(PIN_CAN_BOOSTEN, true); // turn on booster  
  // start the CAN bus at 250 kbps
  if (!CAN.begin(250E3)) {
    #if DEBUG
    Serial.println("CAN init failed!");
    #endif
    pixels.setPixelColor(0, 255, 0, 0, 4);  // red
    pixels.show();
    while (1);
  }

  #if DEBUG
  Serial.println("\nSetup complete\n");
  #endif
} // setup()


void loop() {

  int packetSize = CAN.parsePacket();
  if (packetSize) {
    pixels.setPixelColor(0, 0, 0, 255, 4);  // blue
    pixels.show();

    #if DEBUG
    // received a packet
    Serial.print("Received ");

    if (CAN.packetExtended()) {
      Serial.print("ext 29-bit ");
    } else {
      Serial.print("std 11-bit ");
    }

    if (CAN.packetRtr()) {
      // Remote transmission request (RTR), packet contains no data
      Serial.print("RTR ");
    }

    Serial.print("packet with id 0x");
    Serial.print(CAN.packetId(), HEX);
    #endif

    unsigned char buf[8];
    if (CAN.packetRtr()) {
      #if DEBUG
      Serial.print(" and requested length ");
      Serial.println(CAN.packetDlc());
      #endif
    } else {
      #if DEBUG
      Serial.print(" and length ");
      Serial.println(packetSize);
      #endif

      // Get the data for non-RTR packets 
      int i = 0;
      while (CAN.available()) {
        buf[i] = (char)CAN.read();
        i++;
      }

      // Decode the CAN message if the ID is recognized and it is not an extended packet.
      float val1 = 0.0; float val2 = 0.0; float val3=0.0; float val4=0.0;
      switch (CAN.packetId()){
        case 0x91:
          // transmission pressure 0 to 400 psi
          //msgValue = getFloatFromCanMsg(msg, msgStartBit, msgLength, msgOffset, msgFactor);
          val1 = getFloatFromCanMsg(buf, 0, 16, 0, 0.1);
          // CAC in [psi]
          val2 = getFloatFromCanMsg(buf, 16, 16, 0, 0.01);
          // CAC out [psi]
          val3 = getFloatFromCanMsg(buf, 32, 16, 0, 0.01);
          // CAC delta [psi]
          val4 = getFloatFromCanMsg(buf, 48, 16, 0, 0.01);
          #if DEBUG
          Serial.print("\tTrans [psi]: "); Serial.println(val1, 1);
          Serial.print("\tCAC in [psi]: "); Serial.println(val2, 1);
          Serial.print("\tCAC out [psi]: "); Serial.println(val3, 1);
          Serial.print("\tCAC delta [psi]: "); Serial.println(val4, 1);
          #endif
          switch (last_val) {
            case 0:
              last_val = 1;
              display.clearDisplay();
              display.setCursor(0,0);
              //display.print("0123456789");
              display.print("Trans Oil");
              display.setCursor(0,16);
              display.print(val1,1);
              display.print(" psi");
              display.display();
              break;
            case 1:
              last_val = 2;
              display.clearDisplay();
              display.setCursor(0,0);
              //display.print("0123456789");
              display.print("CAC IN");
              display.setCursor(0,16);
              display.print(val2,1);
              display.print(" psi");
              display.display();
              break;
            case 2:
              last_val = 3;
              display.clearDisplay();
              display.setCursor(0,0);
              //display.print("0123456789");
              display.print("CAC OUT");
              display.setCursor(0,16);
              display.print(val3,1);
              display.print(" psi");
              display.display();
              break;
            case 3:
              last_val = 0;
              display.clearDisplay();
              display.setCursor(0,0);
              //display.print("0123456789");
              display.print("CAC Delta");
              display.setCursor(0,16);
              display.print(val4,1);
              display.print(" psi");
              display.display();
              break;           
          } // switch (last_val)
          break;
        default:
          #if DEBUG
          Serial.print("Packet raw contents: '");
          for (i = 0; i<8; i++) {
            Serial.print("0x");
            Serial.print(buf[i], HEX);
            Serial.print("\t");
          }
          Serial.println("");
          #endif
          break;
      } // switch()
    }
    #if DEBUG
    Serial.println();
    #endif
    pixels.setPixelColor(0, 0, 0, 0, 4);  // black
    pixels.show();
    
  } // packetSize

  
} // loop()

 

 


Do you need help developing or customizing a IoT product for your needs?   Send me an email requesting a free one hour phone / web share consultation.  

 

The information presented on this website is for the author's use only.   Use of this information by anyone other than the author is offered as guidelines and non-professional advice only.   No liability is assumed by the author or this web site.