Arduino coding expertise needed for fuel consumption

Keith-i

Well-Known Member
Joined
4 Jul 2012
Messages
1,476
Location
Jersey
Visit site
I have developed an Arduino gateway to get some engine data from the engine can-bus into N2K. So far all is good with rpm, temp and boost. What I'm struggling with is fuel consumption. Below is the crux of it; any help will be appreciated. I realise this is all a bit specialist but the likes of Vas, GHA, adwuk and others seem to have their heads round this stuff more than I do.

The challenge:
To take consecutive readings from bytes 2 and 3 of frame 0x480 of can bus and calculate average fuel consumption. Bytes 2 & 3 give a continuous incremental reading of fuel consumed in microlitres that resets when it reaches 7FFFFF. (Note: the first bit of data is just a marker bit so the data is only 15 bits long).

Requirements:
To get accurate timestamp of data.
To keep a rolling average to smooth out data.
To deal with rollover of time and fuel data. e.g. as fuel consumption data is 15 bit it reaches hex 7FFFFFF (dec 32767 ) before rolling back to zero. Time is 8 bit so it rolls over much sooner.

Managed so far:
To obtain continuous output from 0x480 with flexcan timestamp

Sample data:

Code:
Timestamp-- bin-- dec
31910-- 111111111101111-- 32751
41943-- 111111111110100-- 32756
51976-- 111111111111001-- 32761
62008-- 111111111111110-- 32766
6506-- 11-- 3
16537-- 1000-- 8
26569-- 1101-- 13
36602-- 10010-- 18
46636-- 10111-- 23
56668-- 11100-- 28

Code theory:

Code:
	fuel = ((( inMsg.buf[3]&0b01111111)<<8)+inMsg.buf[2]); 

	if fuel2>fuel1 
	fuelused = fuel2-fuel1 
	else fuelused=((fuel2+32767)-fuel1)

     
	if time2>time1 
	timediff = time2-time1 
	else timediff=((time2+65535)-time1)

	fuelrate = (360/timediff)*fuelused
Questions:
How do I get sequential readings from 0x480 so that I can obtain fuel1, fuel2, time1 & time 2 to then perform my calculation?

How do I keep a rolling average of the readings to smooth the data?
 
Questions:
How do I get sequential readings from 0x480 so that I can obtain fuel1, fuel2, time1 & time 2 to then perform my calculation?

How do I keep a rolling average of the readings to smooth the data?

Keith, don't have time atm for a fuller reply, but please have a look FIRST at what type of data the various N2K PGNs dealing with fuel need.
then check which display/plotter you'll be using, see which PGNs it supports and work from there, N2K is a standard of some sorts and companies twist it at will with custom PGNs if the normal ones don't suit them!
They generally work with INSTANT consumption, the calcs are then done at the plotter (ave, etc)
PGN127489 (works on Garmin), PGN127497 iirc works on maretron, etc. Got to get my code up, read comments and come back...

rolling averages, expon. etc you just use a lib you like (or make your own function which I find a bit too much)
I was using Megunolink, but it's a bit bloated and lots and lots of warnings on compile, so last week moved to smoothed lib (embedded in the arduino IDE)

cheers

V
 
Thanks Vas. My Garmin 7012 plotter will happily display fuel consumption statistics such as mpg so long as I provide it with the instantaneous l/hr fuel rate in PGN127489 which I think gets updated at 1Hz.

My engine canbus gives me incremental readings of fuel consumed. My issues are with the coding to read sequential fuel consumed readings from the buffers in flexcan (or poll for it?) and from these calculate the l/hr average. On paper it's a simple mathematical formula of take 2 readings, calculate the difference between them and divide by the time interval to get a litres per hour result. Where I'm struggling is how to code this to get the data from the flexcan buffers, perform the calculations on a rolling basis and update the PGN. Actually, the update PGN I can do! It's everything prior that's the hurdle.

I love it when people say 'just use a library' or 'make your own function' :p I'm in that limbo between understanding the basics of Arduino and trying to get to grips with integrating libraries, function calls and other stuff which is still somewhat over my head.
 
Yes. I posted on one of the arduino forums where I’d previously got some great help but unfortunately the answers I received to this latest quest have gone over my head somewhat so I didn’t pursue it there. I just need that helping start. My biggest uncertainty is how to continuously grab sequential bytes of data with the flexcan library so that I can then process the data.
 
Ok. Don't have the necessary hardware here to test code my self but if you send me what you have got so far I'll be able to advise you.
 
Keith,

sorry just now relaxed a bit after messing and cleaning for three hours my SMEG hob at home :( need to order an igniter now!
anyway,

grabbing data from buffers is not my strong point by any stretch of imagination! needs decent understanding of programming, buffers, reading, etc and I'm self taught on that...
"incremental readings of fuel consumed" interesting, but what does it actually mean? When is it reset? everytime the engine starts I guess, right?
Anyway, looks like you have to query/poll the engine bus for the data, on set intervals (say half a sec?), subtract previous to current reading divide over time and you have an instant value.
Thing is that the output you get from your calc most likely this is going to be seriously unstable and going up and down.
If you do an exponential filtering with a smallish bias it will smooth the values and give out something that wont make you dizzy watching the screen :rolleyes:
So on the same loop that does the query/process, I'd pass the output through the smoothing filter and sent the smoothed output to the N2K bus. Yes there's going to be a delay on the relevance of the value you'll sent compared to the actual data, but tbh you don't accelerate watching the consumption gauge, you settle on a speed, and then take you time to check all sorts of values and maybe play with the tabs to improve speed and/or consumption. Hence I feel that even a few sec delay is no problem.

I've had to do lots of smoothing of data produced mainly from analogue reading voltage on pins on the teensies, et al. Works fine, just play with the bias.
I'd recommend using the smoothed lib which is included in the arduino IDE, just go CTRL+SHFT+i, type smoothed in the box top right and install it.
Apologies if you know all that already, not sure where you stand!

happy to also have a look at some code, but not the buffer bit please :D

cheers

V.
 
Vas, thanks for taking the trouble to respond. I've had some great help from Paulg25 who's kindly given me some code to slip into my existing code. It creates a buffer from which readings are taken and then produces a rolling average which will hopefully achieve what I'm looking for. Will keep you posted.
K
 
Just thought I'd close this thread as my engine to N2k gateway is all working now. Some subtle changes in my code suggested by Timo have resolved all my little issues.
 
good, well done!

if you don't mind, it would be nice for others to share the code (of course assuming it's one of the standard protocols used in el. diesel engines). I did that when I reverse engineered a couple of Maretron proprietary PGNs so now others can use EGT temps and fuel flow rates on Maretron displays. I'm sure Timo would like some more help and libs to add to the N2K libs

cheers

V.
 
Vas, I'm more than happy to share the code, although I doubt it will be of much interest to a wider audience as my circumstances were fairly specific. The engine is a VW Marine unit (based on the road going V6 Tdi) which was popular for a while on the continent in various boats but few and far between in our waters. It uses the standard VW can bus protocol which I think is TP2.0 ISO15765. My code extracts data from the relevant bytes that are being streamed over the canbus and puts them into N2k. Specifically I have boost, temp, rpm and fuel rate.

I did have one pm requesting info but that has gone quiet. Here's the working code though:

Code:
// Convert VW Marine ECU canbus data to N2k. Oct 2019
// based on Collin80 and Timo Lappalainen code and Paulg25 on ybw
// Set for Teensy 3.6 at 96Mhz

#include <Arduino.h>
#include <NMEA2000_CAN.h>  // Chooses right CAN library and creates                                                                                                                                                                                                                                                                                                               suitable NMEA2000 object
#include <N2kMessages.h>
#include <NMEA2000_teensy.h>
#include <FlexCAN.h>


#ifndef __MK66FX1M0__
#error "Teensy 3.6 with dual CAN bus is required to run this example"
#endif

// List below N2k messages your device will transmit.
const unsigned long TransmitMessages[] PROGMEM = {127488L, 127489, 0}; //engine rapid and dynamic

static CAN_message_t msg; //sets up a storage buffer?
static uint8_t hex[17] = "0123456789abcdef";

#define EngineTimeout 5000 //perhaps not in global?


//-----------------------------------------------------------------------------------------
void setup() {

  delay(5000);
  // Set Product information
  NMEA2000.SetProductInformation("000300", // Manufacturer's Model serial code
                                 200, // Manufacturer's product code
                                 "Arduino VW Gateway",  // Manufacturer's Model ID
                                 "1.1 (2019)",  // Manufacturer's Software version code
                                 "1.A (2019)" // Manufacturer's Model version
                                );
  // Det device information
  NMEA2000.SetDeviceInformation(303030, // Unique number. Use e.g. Serial number.
                                160, // Device function=engine gateway.
                                50, // Device class=propulsion.
                                443 // Just choosen free from code list - VDO
                               );

  // Uncomment 2 rows below to see, what device will send to bus. Use e.g. OpenSkipper or Actisense NMEA Reader
  //Serial.begin(115200); // uncomment to send to serial for PC use
  //NMEA2000.SetForwardStream(&Serial); // uncomment to send to serial - default is Actisense format for PC use

  // If you want to use simple ascii monitor like Arduino Serial Monitor, uncomment next line
  //NMEA2000.SetForwardType(tNMEA2000::fwdt_Text); // uncomment to send to serial as plain text - overides above Actisense format

  // If you also want to see all traffic on the bus use N2km_ListenAndNode instead of N2km_NodeOnly below
  NMEA2000.SetMode(tNMEA2000::N2km_NodeOnly, 22);

  //NMEA2000.SetDebugMode(tNMEA2000::dm_Actisense); // Uncomment this, so you can test code without CAN bus chips on Arduino Mega
  // Comment out next line for Actisense PC use
  NMEA2000.EnableForward(false); // Disable all forward messaging from N2K bus to USB (=Serial) - default is True. Comment for PC use

  NMEA2000.ExtendTransmitMessages(TransmitMessages);
  NMEA2000.Open();

  //Can0 should be automatically set at default 250kbs for N2k
  Can1.begin(500000); //set baud for VW ECU.
  Can1.setListenOnly (true); //hopefully set listen only mode. Set after begin()
}
/*Engine frames
    Byte                     data    factor                expected range
    0x280 byte 1 =           torque  (*0.39)%              (0-100% limit 493Nm)
    0x280 byte 2low&3high =  rpm     (*0.25)/min           (typ range 0-4500)
    0x288 byte 1 =           coolant ((*0.75)-48)C         (typ range 20-100)
    0x380 byte 1 =           IAT     ((*0.75)-48)C         (typ range 20-50)
    0x480 byte 2&3 =         fuel consumption ul           (idling 0.42-0.82 l/h)
    0x588 byte 4 =           boost (*0.01)bar or (*1000)Pa (1010 to 2295 mbar)
*/
//variable     data type       data                N2k expected units  resolution
double torque = N2kDoubleNA;  //torque            % (1% increment)
double rpm = N2kDoubleNA;     //rpm
double coolant = N2kDoubleNA; //coolant temp      K (0.1K)
double fuel_consumption = N2kDoubleNA;    //fuel rate         l/h (0.1 l/h)
double boost = N2kDoubleNA;   //boost pressure    Pa (100Pa) 1bar = 100,000Pa
unsigned long LastRPM = 0;
#define EngineTimeout 5000 //perhaps in global?

//for fuel consumption calculations
uint16_t fuel_last = 0; //unsigned int 16bits 0-65535
unsigned long time_last = 0; //unsigned 4 bytes
#define FUEL_AVG_BUFFER 10
double fuel_buffer[FUEL_AVG_BUFFER]; // buffer locations will be 0 - 9
size_t fuel_pointer = 0; //represents size of something in no. of bytes

//---------------------------------------------------------------------
void PollEngineCAN() { //function to poll engine data
  CAN_message_t inMsg;

  if (Can1.available() > 0) {
    Can1.read(inMsg); //read message into inMsg
    switch ( inMsg.id ) { //a type of if process that can check for multiple conditions

      case 0x280:
        torque = ( inMsg.buf[1] * 0.39 );
        rpm = ((( inMsg.buf[3] << 8) + inMsg.buf[2]) / 4);
        LastRPM = millis(); //timestamp the last rpm reading
        break;

      case 0x288:
        coolant = (( inMsg.buf[1] * 0.75) + 225.15); //convert from C to K (-48 + 273.15 = 225.15)
        break;

      case 0x480: {
          double fuel_used;
          uint16_t fuel_now = ( ( (inMsg.buf[3] & 0b01111111) << 8 ) + inMsg.buf[2] );  // get current fuel reading
          unsigned long time_now = millis();                  // get curent time

          if ( time_last != 0 ) {   //if time not equal to 0
            if ( fuel_now >= fuel_last ) {           // to allow for roll over
              fuel_used = fuel_now - fuel_last;
            } else {
              fuel_used = ( fuel_now + (32767 - fuel_last) );
            }

            double time_elapsed = time_now - time_last;

            // fuel_used is in ul, so we need devide it with 1e6 to get litres.
            // time_elapsed is ms, so divide it with 3.6e6 to get hours.
            // fuel_rate = ( (fuel_used / 1e6) / ( time_elapsed/3.6e6 ) );
            double fuel_rate = (fuel_used / time_elapsed) * 3.6;

            fuel_buffer[fuel_pointer] = fuel_rate;      // store latest reading in buffer (10 deep).
            fuel_pointer++;                             // increment buffer pointer ready for next time

            if ( fuel_pointer == FUEL_AVG_BUFFER ) fuel_pointer = 0;   // if end of buffer reached, point to start

            double fuel_average = 0;
            for ( size_t i = 0; i < FUEL_AVG_BUFFER; i++ ) {       // do this 10 times
              fuel_average += fuel_buffer[i];       // add all 10 numbers stored in buffer
            }          
            fuel_consumption = fuel_average / FUEL_AVG_BUFFER;
          }
          time_last = time_now;
          fuel_last = fuel_now;
        }
        break;

      case 0x588:
        boost = ( inMsg.buf[4] * 1000 ); //convert from bar to Pa (*0.01 x 100,000 = 1000)
        break;
    }
  }
}
//---------------------------------------------------------------------------------
void SendN2kEngineRapid() //N2k function to send data
{
  static unsigned int NextSendTime = 5000;
  if ( NextSendTime < millis() )
  {
    NextSendTime += 100;
    tN2kMsg N2kMsg;

    SetN2kEngineParamRapid(N2kMsg, 0, rpm, boost);
    NMEA2000.SendMsg(N2kMsg);
  }
}
//---------------------------------------------------------------------------------
void SendN2kEngineDynamic() //N2k function to send data
{
  static unsigned int NextSendTime = 5000;
  if ( NextSendTime < millis() )
  {
    NextSendTime += 500;
    tN2kMsg N2kMsg;

    SetN2kEngineDynamicParam(N2kMsg, 0, N2kDoubleNA, N2kDoubleNA, coolant,
                             N2kDoubleNA, fuel_consumption, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kInt8NA, torque);
    NMEA2000.SendMsg(N2kMsg);
  }
}
//-----------------------------------------------------------------------------------------
void loop()
{
  NMEA2000.ParseMessages();
  PollEngineCAN();
  if ( LastRPM + EngineTimeout > millis() ) {
    SendN2kEngineRapid();
    SendN2kEngineDynamic();
  }
}
 
thanks Keith,

I've no use personally for this code and I very much doubt how easy will be for someone to find it here, maybe ask Timo if he wants to add it in a ext lib?

cheers

V.
 
Top