ILI9341 CAN Display - Gauge rendering on display

Status
Not open for further replies.
I will have to sample the data twice in each loop, correct?
No, just once (gauge_value).

Think of gauge_shown as the needle in the gauge, dragged around by the sampled gauge_value , but limited to a maximum rate of change.



If the sampled value jitters, and you want to smooth that out, you'll need to instead implement a smoothing filter.

The two most commonly used filtering schemes for this sort of stuff are exponential and windowed.

Exponential is the simplest, but may take some experimentation to find the optimum coefficient. The code itself is trivial:
Code:
#define  GAUGE_COEFFICIENT  0.75  /* between 0.5 and 1 */

gauge_shown = GAUGE_COEFFICIENT * gauge_value + (1.0f - GAUGE_COEFFICIENT) * gauge_shown;
This is called exponential (often exponential smoothing) because the shown value is essentially a mix of samples thus far, with exponentially decaying weights.

The optimum GAUGE_COEFFICIENT depends on the amount of jitter, and how rapidly the shown gauge should react to large changes. The smaller, the smoother; the closer to one, the faster it reacts.

Windowed filtering involves keeping a short queue of sampled values, always replacing the oldest one with the new sample, and showing their average:
Code:
#define  GAUGE_SAMPLES  20

float  gauge_sample[GAUGE_SAMPLES];
unsigned int  gauge_sample_index = 0;
int i;

/* Add a new sample */
gauge_sample[gauge_sample_index++] = gauge_value;
if (gauge_sample_index >= GAUGE_SAMPLES)
    gauge_sample_index = 0;

/* Calculate average */
gauge_shown = 0.0f;
for (i = 0; i < GAUGE_SAMPLES; i++)
    gauge_shown += gauge_sample[i];
gauge_shown /= GAUGE_SAMPLES;
The optimum window size (GAUGE_SAMPLES) depends on the jitter in gauge_value samples.

The window size also determines the amount of latency (delay in registering a change) the shown gauge has.
For example, if the samples are first all zeroes, and then suddenly change to one, it takes GAUGE_SAMPLES updates before the shown gauge gets to one. It will rise there linearly.

Sometimes the window is also weighted, but I would guess that in this kind of application the exponential filter will work better (than a weighted windowing filter).
 
Perhaps this illustration of 100 samples (a sine wave plus noise) gives you some idea:
plot-optimized.jpg
The same data, but with different coefficient and window size:
plot-optimized2.jpg
 
Great info!. A lot of cloth to cut!.

This is my approach to smoothing readings. The key is the data acquisition interval; I think that 1 second is more than acceptable to have real-time information from any of the sensors that you want to monitor.

Code:
float Lect, Lect1, Lect2, DeltaL, VelL, LectA;

long previousMillis=0, updateMillis=1000;
void Sensor1()
{
  unsigned long currentMillis = millis(); 
  if(currentMillis - previousMillis > updateMillis)
  {
    previousMillis = currentMillis; 
    Lect=random(0,100);

    Lect2= Lect;
    DeltaL=Lect2-Lect1;
    if(DeltaL>0){VelL=1;}else{if(DeltaL<0){VelL=-1;}else{VelL=0;}}
  }

   if(LectA==Lect2){LectA=Lect2;}else{LectA = LectA + VelL;}

   GD.cmd_gauge(50, 180, 40, 0, 4, 7, LectA, 100);
   GD.cmd_gauge(GD.w-50, 180, 40, 0, 4, 7, Lect, 100);
   Lect1= LectA;
}
 
So, I never got around to testing this - just haven't had time, and I've also been working on getting the 2nd gauge to display as I want. As well as other things in parallel.

I went with a very simple approach that seems to have really smoothed out the reading, especially on the gauge..
In my loop, after sampling the data, I do an average of the last reading and the current reading like so:
Code:
boost_pressure = canData [0]; // Data from the CAN Bus
boost_pressure = (boost_pressure + lastBoostPressure) / 2; // merge and average current value with last value
lastBoostPressure = boost_pressure; // Save the current value to use in the next calculation

And here you can see the smoother response on the gauge in this video

Alternatively, I could also do this:
Code:
boost_pressure = canData [0]; // Data from the CAN Bus
boost_pressure = (boost_pressure + lastBoostPressure) / 2; // merge and average current value with last value
lastBoostPressure = canData [0]; // Save the current live value to use in the next calculation

Which would be more accurate?
 
I need to update my gage library--yours look like a million times better that mine!
Please do share what you have built! Happy to provide the graphics code if you’d like. But just note that its a work in progress and I am no expert. I learn more C++ with every new challenge I tackle on this project.
 
Which would be more accurate?
First one is exponential filtering with coefficient 0.5, the second is windowed filter with 2 sample window. They are equally valid.

How to define "accuracy"? The main purpose is to smooth out noise, so the needle doesn't jitter like a ferret on crack.
(You should use (boost_pressue+lastBoostPressure+1)/2 in both cases to get correct rounding, though, since the values are unsigned integers.)

A step change takes seven updates with the exponential filtering. With the two-sample window, two updates. The final result is the same, so both are equally accurate; they just smooth the jitter differently.

Love the gauges, by the way. I might have made the right-side one with two black lines surrounding a green/yellow/red block or line instead of the light circle on white, for better visibility, but the left-side one is just about perfect in my opinion!
 
(You should use (boost_pressue+lastBoostPressure+1)/2 in both cases to get correct rounding, though, since the values are unsigned integers.
I shall try that later on today
EDIT: actually, both variables are floats, so I don't think I'll need to add +1 for the corrections.

Love the gauges, by the way. I might have made the right-side one with two black lines surrounding a green/yellow/red block or line instead of the light circle on white, for better visibility, but the left-side one is just about perfect in my opinion!

Thank you! And I've already started working on what you mentioned earlier this week. I hate that white background - so it's going to be filled black with a white outline - will be much easier to read.
I'm really impressed with how well this whole thing works. It runs just as well with the T4 set to at lower clock speed (<400Mhz) as the main bottle neck is the SPI bus speed (20Mhz) and the sample rate from the CAN network (every 12ms) - but still providing great performance
 
Last edited:
Rezo, the CAN library is using a public circular buffer which includes variance, deviation and mean(average) built in. Since it's already included in your code you don't need to add function code to do the same work :)

Only one constructor for a circular queue
Only one call to queue a value (oldest item removed)
Only one call to retrieve the variance/deviation/mean result

CAN is using circular arrays, but circular buffers exist within same library
 
Rezo, the CAN library is using a public circular buffer which includes variance, deviation and mean(average) built in. Since it's already included in your code you don't need to add function code to do the same work :)

Only one constructor for a circular queue
Only one call to queue a value (oldest item removed)
Only one call to retrieve the variance/deviation/mean result

CAN is using circular arrays, but circular buffers exist within same library

Hey Tony, is there and documentation on this? Sounds really interesting!
 
So it would look like this more or less?
Obviously it will take 4 initial pushes to fill the buffer and get a proper average

Code:
Circular_Buffer<float, 4> boostPressure;
float canData [3]; // placing 3 different PID data types but only using 1st in this case
float boostAvrg;

void loop(){
boostPressure.push_front(canData [0]);
boostAvrg = (boostPressure.mean(), 4);  // or just boostPressure.mean(); ?
}
 
Just mean(). A float value would be returned and boostAvrg would be set with it

Normally we push_back, or write(val), push_front was made for a priority queue system but it'll still work pushing things in queue from opposite end :)

If you share the queue within interrupts and loop(), it's recommended to write in back and read from front
 
Alright, so I implemented it. Works well with the constructor setting the circle at 2,4,8 (didn't try anything higher). 1,5,6,7 messed up the readings.
TBH the responsiveness looks almost the same as my previous implementation, but I might reuse the circular buffer to smooth out some other reading as well, perhaps the 2nd gauge too.
 
Last edited:
Yeah the buffer must be a power of 2: 1, 2, 4, 8, 16, 32, etc or undefined behaviour would result and responses would be wrong
 
EDIT: actually, both variables are floats, so I don't think I'll need to add +1 for the corrections.
Correct.

(You could replace the division by a power of two (2, 4, 8, 16) by a multiplication with the exact reciprocal (0.5f, 0.25f, 0.125f, 0.0625f). I've actually expanded on this in this answer I wrote at StackOverflow, which shows how to check how correct the Markstein approach -- replacing the division by a constant with two multiply-adds, as multiplication of floating-point values is much faster than division -- if you happen to need the efficiency. You do not, as you have ample computing power to even reduce the system clock, so better keep the code easily maintained and understandable instead.)

it's going to be filled black with a white outline - will be much easier to read.
Even better than my suggestion! :D

Note that the circular buffer implementation is really trivial. For example:
Code:
#define  BOOST_SAMPLES  5  // Minimum is 1

static float  boost_sample[BOOST_SAMPLES];
static unsigned int  boost_sample_index;

// Call this to initialize or reset the boost sample buffer to a fixed value it starts from.
void boost_init(float value)
{
    for (int i = 0; i < BOOST_SAMPLES; i++) {
        boost_sample[i] = value;
    }
    boost_sample_index = 0;
}

float boost_update(float sample)
{
    float  average = sample;

    // Increment and wraparound the sample index.
    boost_sample_index = (boost_sample_index + 1) % BOOST_SAMPLES;

    // Trick: we use one extra sample by doing the average (sum) now.
    for (int i = 0; i < BOOST_SAMPLES; i++) {
        avg += boost_sample[i];
    }
    // Note: avg = sample plus sum of BOOST_SAMPLES in boost_sample[]

    // Update sample buffer.
    boost_sample[boost_sample_index] = sample;

    // Return average.
    return avg / (BOOST_SAMPLES + 1.0f);
}
Then, in your code, you just call boost_init(0.0f) in setup(), then in loop(), boost_pressure=boost_update(can[0]); and you have the BOOST_SAMPLES+1 -averaged boost pressure value. Just change the macro definition on the first line to see which sample count works best.

If you were tight on processing power, we could do something about the division and the modulo, but since they are only done when the display is updated, they really are utterly, completely neglible.

All this said, I still cannot say whether I like the exponential or the window filter approach better!

The best description for their difference that I can think of, is that the exponential reacts faster to large changes, but takes longer to arrive at the steady-state value, as it slows its approach as it comes closer; whereas windowed takes a linear path to the target value. If you want large changes to be visible faster, but filter out smaller noise, use exponential; just find the proper coefficient for your use case. The windowed gives a much more calm, stately progression to different values; its behaviour is completely dependable and unsurprising, trust-inspiring. Both are correct; it's just that they take a different path to the steady state. Exponential is like a revving engine, whereas windowed is the dependable, well-behaved one. It really depends which feel you want your gauge to have!
 
I think there might be an even quicker way to maintain a running average that *should* be the same as:
Code:
float boost_update(float sample)
{
    float  average = sample;

    // Increment and wraparound the sample index.
    boost_sample_index = (boost_sample_index + 1) % BOOST_SAMPLES;

    // Trick: we use one extra sample by doing the average (sum) now.
    for (int i = 0; i < BOOST_SAMPLES; i++) {
        avg += boost_sample[i];
    }
    // Note: avg = sample plus sum of BOOST_SAMPLES in boost_sample[]

    // Update sample buffer.
    boost_sample[boost_sample_index] = sample;

    // Return average.
    return avg / (BOOST_SAMPLES + 1.0f);
}

Instead of polling every element of BOOST_SAMPLES each time the average is updated (with that 'for' loop), you can update it by simply using the newest and oldest BOOST_SAMPLES entries. The benefit here might be negligible on powerful processors, but it means the performance shouldn't change with the size of the data array... maybe helpful if you wanted a very slow moving average (i.e. large array), like if you have a noisy sensor on a stable system (like your coolant temp). I've also included a similar strategy for building the initial array.
Code:
#define  BOOST_SAMPLES  5  // Minimum is 2
static float  boost_sample[BOOST_SAMPLES];
static unsigned int  boost_sample_index = 0;
float average = 0.0f;
int n = 0; //used for initial filling of the data array, indicates number of elements in array (actually one less...)

void boost_init(float value) {   //keeps accurate initial average for first few samples
    boost_sample_index = n;
    boost_sample[n] = value;
    average += (value - average) / (n + 1);
    n++;
}

float boost_update(float value) {   //discards oldest sample from average and updates with newest
    boost_sample_index = (boost_sample_index + 1) % BOOST_SAMPLES;   //go to oldest index
    average += (value - boost_sample[boost_sample_index]) / (BOOST_SAMPLES);   //i.e. avg_new = avg_old + (newest_sample - oldest_sample) / BOOST_SAMPLES
    boost_sample[boost_sample_index] = value; //set oldest index to newest value
}

void loop() {
    if(n < BOOST_SAMPLES) {   //only run for first BOOST_SAMPLES iterations
        boost_init(can[0]);
    }
    else {   //after BOOST_SAMPLES iterations, use update function instead
        boost_update(can[0]);
    }
    updateDisplay(average);
}
 
@mikey.antonakakis It's all good, but you shouldn't use floats or you'll end up having float rounding issues. i.e. over time the average will drift away from the real average of the values. Convert to use fixed point instead and just maintain sum of the values, and divide by the total number of samples when querying the average.
 
@mikey.antonakakis It's all good, but you shouldn't use floats or you'll end up having float rounding issues. i.e. over time the average will drift away from the real average of the values. Convert to use fixed point instead and just maintain sum of the values, and divide by the total number of samples when querying the average.
Good advice, thanks!
 
With recent events of the COVID-19 virus, I've been working from home for the past week.
I found some spare time to continue working on this project in between.

I want to thank anyone and everyone that has assisted me with code and guidance so far - I know I can be a bug at times but I just love this stuff and the people here!

This is what the current UI layout looks like so far:
IMG_2651_S.jpg

All readings in red are peaks, white is realtime.
Readings with a decimal point are aligned to the decimal point., those that are integers are aligned to the right (lots of if else statements to change curser position).
Screen updates take roughly 20ms in the loop and it responds quite well for live data - this is thanks to the optimised HXD8357D_t3n library using a frame buffer and clipping areas to update portions of the screen.

Parts:
T4.0 @ 450mhz
Adafruit HXD8357D 3.5" display (480x320)
Waveshare SN65HVD230 CAN transceiver @ 500kbps.


Next step is design a PCB to mount the T4, SN65HVD230 and a DC-DC power supply behind the display to keep the assembly as slim as possible. Will probably do this via OSHPark when I find some spare time, as well as 3D print a small case.

I still want to enhance my CAN code, as I am currently requesting each data set individually, but I can do up to 6 at the time, which should increase data refresh rate by quite a bit.

Overall this is an ongoing learning experience for me, and an enjoyable one too!
 
I finally got the PCB's from OSHPark in last week after a 2 month wait (due to shipping restrictions) and assembled one.
IMG_3958.jpgIMG_3959.jpg
The PCB has a pull down buck converter to reduce 12v to 5v, which is fed to the T4 and the HX8357 display
A CAN transceiver powered by the T4's 3v3 regulator, PWM control for the display backlight and Touch screen capabilities.
It also has Snooze implemented so the entire unit goes into hibernate mode once the car is off - just waiting for duff to fix a bug with hibernate restarts & SPI.

I redesigned the layout and made the Boost gauge bigger (320x320) and I've already placed it in the car for a few test drives - it works REALLY well!
IMG_3964.jpg

I'll upload a video at a later stage, but here's a GIF @ 25fps as a teaser
ezgif.com-video-to-gife7d5b1e903e2ac9e.gif



The next update is to migrate to a T4.1 with SD card data logging capabilities, migrate to an ILI9488 bare screen with capacitive touch and redesign the whole PCB for SMD parts. I want to make this as slim as possible.
 
I finally got the PCB's from OSHPark in last week after a 2 month wait (due to shipping restrictions) and assembled one.
View attachment 20730View attachment 20731
The PCB has a pull down buck converter to reduce 12v to 5v, which is fed to the T4 and the HX8357 display
A CAN transceiver powered by the T4's 3v3 regulator, PWM control for the display backlight and Touch screen capabilities.
It also has Snooze implemented so the entire unit goes into hibernate mode once the car is off - just waiting for duff to fix a bug with hibernate restarts & SPI.

I redesigned the layout and made the Boost gauge bigger (320x320) and I've already placed it in the car for a few test drives - it works REALLY well!
View attachment 20732

I'll upload a video at a later stage, but here's a GIF @ 25fps as a teaser
ezgif.com-video-to-gife7d5b1e903e2ac9e.gif



The next update is to migrate to a T4.1 with SD card data logging capabilities, migrate to an ILI9488 bare screen with capacitive touch and redesign the whole PCB for SMD parts. I want to make this as slim as possible.

Nice!
Here's a display I put together with a T4.0 and a Nextion display with a 3D printed housing/bezel to fit the spot for the original on-board-computer of my 1987 BMW 325. Also working great, although not as smooth with the needle movements as yours!
 
Nice!
Here's a display I put together with a T4.0 and a Nextion display with a 3D printed housing/bezel to fit the spot for the original on-board-computer of my 1987 BMW 325. Also working great, although not as smooth with the needle movements as yours!

dude.... well done!


Thank you both! A big shout out to Tony and Kurt who have been very helpful and provided me with a great basis and lots of tips.

Mikey, regarding the Nextion display - for your application I think it's perfect as you have it integrated into the dash.
Only reason I didn't go that route is it's price and the fact that I would have two units to update if I make any core changes. With the next update to move to the T4.1 and ILI9488 display, I'lll be able to fit everything onto one PCB.

Its a true plug and play, it connects to the OBD port and is ready to work on many models of the VW-Audi MQB platform.
 
Status
Not open for further replies.
Back
Top