Creating trends on OLED screen 128x64

Hello everyone!

Just last week I started tinkering with my new Arduino Micro, and I have several projects in mind... but I'm stuck, and I'd really appreciate some help.

I do have some background programing in C++, altough I havent really practiced in around 12 years.

The project I am working on looks something like this:

The objective is as follows:

  • Have three simultaneos analog or digital inputs.

  • Trend the value of the input in three charts

  • On the left of the chart, display the minimum and the máximum value of each chanel, although the picture is just showing placeholders at the moment

  • At the top of the screen, two values: the "Scan rate", which measures in the Loop routine how long it takes for the program to execute, and the "Frames per second", which shows how many times per second the screen is refreshed.

Now, following several tutorials and examples I was able to show the trend of one input fairly easy... but I noticed that the time it took the program to evaluate was very, very slow. somewhere around only 25 iterations per second.

I tried removing the screen logic from the Loop, and instead only triggering to a certain framerate... and the program gain a dramatic speed up.

Now, because I'd intending to show a somewat readable trend of each value on the screen, showing in only 100 pixels wide enough readings to see the input change seemed imposible... so My approach was to keep a "Long" array of 500 samples, storing the "Raw" data, and a "Short" array storing only the value to be displayed on the trend, each dot representing the average of 5 readings.

now, so far, the code as i copied it in here, compiles, and runs... BUT you will notice many lines that I've commented, and that's where I started getting frustrated. for example, this is the code I've got so far...

fair warning, I apologize for my horrendous way of commenting... and switching back and forth between english and spanish... let me know if I should fix my comments or standardize the naming to make it more readable

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

int Boton1Pin = 8;
int Boton1State = LOW;

int maxValue,ActualValue,LongTrendSize = 500,ShortTrendSize=100;

unsigned long lastFrame = 0, LastScanMillis=0;
int fps = 15; //number of screen refreshes per second


bool TriggerFrameRate(float &TrueFPS){
  /////////////////////////////////////////////////////////////////////////
  // use this function to return the confirmation that this specific scan,
  // the routine to call for the display update can be called.
  /////////////////////////////////////////////////////////////////////////
  int RefreshDelay = 1000/fps;
  unsigned long Imillis=0;
  float Fmillis=0.0,FlastFrame=0.0,FloatFPS;
  if(millis()-lastFrame >= RefreshDelay){
    Imillis=millis();
    Fmillis=(float) Imillis;
    FlastFrame=(float) lastFrame;
    FloatFPS = 1000.0 / (Fmillis-FlastFrame);
    TrueFPS=FloatFPS;
    lastFrame = millis();
    return true;
  }
  else{
    return false;
  }
}


void  ReadInputs(){
  /////////////////////////////////////////////////////////////////////////
  //  Routine to make all the needed digital reads and analog reads,
  // so the rest of the program doesn't repeat any.
  /////////////////////////////////////////////////////////////////////////
  Boton1State = digitalRead(Boton1Pin);
}


void PushShortTrend(byte (&trend)[100], int (&Ltrend)[500]){
  /////////////////////////////////////////////////////////////////////////
  // This subrutine will calculate the values for the short arrays, the arrays
  //that will hold the value for the Y axis offset in the OLED screen.
  /////////////////////////////////////////////////////////////////////////
  int i,promedio,Total;
  for(i=100;i>=2;i--){
    trend[i-1]=trend[i-2];
  }
  // add up the newest five scans, and obtain the total
  Total = Ltrend[0]+Ltrend[1]+Ltrend[2]+Ltrend[3]+Ltrend[4];
  
  //divide by 5 to obtain the average.
  promedio = Total / 5;
  
  //because the input will be in a range from 0..1023 and the trend can
  //only be 16 bits high, divide by 64 and store as the newest value.
  trend[0]=promedio/64;
}

void PushLongTrend(int (&trend)[500], int NewValue){
  /////////////////////////////////////////////////////////////////////////
  //This subrutine will add the newest read value to the long array, and
  //offset the rest of the values.
  /////////////////////////////////////////////////////////////////////////
  int i;
  for(i=500;i>=2;i--){
    trend[i-1]=trend[i-2];
  }
  trend[0]=NewValue;
}

int GetValue(int estado, int &valor){
  /////////////////////////////////////////////////////////////////////////
  // DEBUGGING SUBRUTINE. used to "emulate" analog inputs from a button...
  // when the button is pressed, the input scales up, when it is released,
  // it scales back down.
  /////////////////////////////////////////////////////////////////////////
  int output;
  if(estado==LOW && valor>0){
    valor--;
  }
  if(estado==HIGH && valor<1023){
    valor++;
  }
  return ActualValue;
}

void DrawTrend(byte (&trend)[100], int x, int y){
  /////////////////////////////////////////////////////////////////////////
  // This routine will draw the trend based on the short array values.
  // it will receive the "x" and "y" coordinates of the top left corner where
  // the trend is to be drawn.
  /////////////////////////////////////////////////////////////////////////
  int i,TempInt;
  for (i=0;i<100;i++){
    TempInt=trend[i];
    display.drawPixel(x+100-i,y+16-TempInt, WHITE);
  }
  //Serial.print(trend[0]);  Serial.print(" ");
  //Serial.print(trend[1]);  Serial.print(" ");
  //Serial.println(trend[2]);
}

int FindMaxVal(int (&trend)[500]){
  /////////////////////////////////////////////////////////////////////////
  // subrutine to find the highest value in the long array and return it.
  /////////////////////////////////////////////////////////////////////////
  int i,MaxVal=0;
  for (i=0;i<500;i++){
    if(trend[i]>MaxVal){
      MaxVal=trend[i];
    }
  }
  return MaxVal;
}

int FindMinVal(int (&trend)[500]){
  /////////////////////////////////////////////////////////////////////////
  // subrutine to find the lowest value in the long trend and return it
  /////////////////////////////////////////////////////////////////////////
  int i,MinVal=1500;
  for (i=0;i<500;i++){
    if(trend[i]<MinVal){
      MinVal=trend[i];
    }
  }
  return MinVal;
}

void RefreshOLED(int ScanRate, float FPS, byte (&trend1)[100], byte (&trend2)[100], byte (&trend3)[100], int Min1, int Max1, int Min2, int Max2, int Min3, int Max3){
  /////////////////////////////////////////////////////////////////////////
  // Subrutine to draw the full screen. it will only run when called by the Frame rate interrupt,
  // we give this routine all the final data, it will clean the screen, write every value, draw every line and pixel,
  // and then refresh the screen.
  /////////////////////////////////////////////////////////////////////////
  int testint;
  testint = trend1[0];
  display.clearDisplay();
  display.setCursor(0,0);   display.print("Scan:      FPS:");
  display.setCursor(30,0);  display.print(ScanRate);
  display.setCursor(88,0); display.print(FPS);
  display.setCursor(0,16);  display.print(Max1);
  display.setCursor(0,24);  display.print(Min1);
  display.setCursor(0,32);  display.print(Max2);
  display.setCursor(0,40);  display.print(Min2);
  display.setCursor(0,48);  display.print(Max3);
  display.setCursor(0,56);  display.print(Min3);
  display.drawLine(26, 16, 26, 63, WHITE);    //vertical left line, 26,16,26,63
  display.drawLine(127, 16, 127, 63, WHITE);  //vertical right line, 127,16,127,63
  display.drawLine(27, 31, 126, 31, WHITE);   //horizontal top, 27,31,126,31
  display.drawLine(27, 47, 126, 47, WHITE);   //horizontal middle, 27,47,126,47
  display.drawLine(27, 63, 126, 63, WHITE);   //horizontal bottom, 27,63,126,63
  //DrawTrend(trend1, 27, 16);
  //testint = trend1[0];
  //display.drawPixel(27,25, WHITE);
  //
  //
  display.display();
}

void setup() {
  /////////////////////////////////////////////////////////////////////////
  // setup; declaring inputs, serial and text
  // 
  /////////////////////////////////////////////////////////////////////////
  int i;
  pinMode(Boton1Pin, INPUT);
  Serial.begin(9600);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
    for(;;);
  }
  delay(2000);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
}

void loop() {
  /////////////////////////////////////////////////////////////////////////
  // Loop routine,
  //
  //    VARIABLE DEFINITION AND INITIALIZATION
  //
  /////////////////////////////////////////////////////////////////////////
  byte ShortTrend1[100],ShortTrend2[100],ShortTrend3[100];
  int LongTrend1[500], LongTrend2[500], LongTrend3[500];
  int Min1=1000,Max1=1009,Min2=2000,Max2=2009,Min3=3000,Max3=3009;
  int i,ScanRate=444;
  float TrueFPS=66.6;
  int Accum1=0, Accum2=0, Accum3=0;
  for(i=0;i<100;i++){
    ShortTrend1[i]=0;
    ShortTrend2[i]=0;
    ShortTrend3[i]=0;
  }
  for(i=0;i<500;i++){
    LongTrend1[i]=0;
    LongTrend2[i]=0;
    LongTrend3[i]=0;
  }
  /////////////////////////////////////////////////////////////////////////
  //
  //    The actual Cycle starts down here.
  //
  /////////////////////////////////////////////////////////////////////////
  while (true){
    ReadInputs();
    GetValue(Boton1State,Accum1);
    PushLongTrend(LongTrend1,Accum1);
    PushShortTrend(ShortTrend1,LongTrend1);
    /////////////////////////
    Max1=LongTrend1[0];
    //delay(1);
    ////////////////////////
    //Min1 = FindMinVal(LongTrend1);    Max1 = FindMaxVal(LongTrend1);
    //Min2 = FindMinVal(LongTrend2);    Max2 = FindMaxVal(LongTrend2);
    //Min3 = FindMinVal(LongTrend3);    Max3 = FindMaxVal(LongTrend3);
    //
    //
    //
    //
    ScanRate=millis()-LastScanMillis;
    //Serial.print(LongTrend1[0]);  Serial.print(" ");
    //Serial.print(LongTrend1[1]);  Serial.print(" ");
    //Serial.println(LongTrend1[2]);
    LastScanMillis=millis();
    if(TriggerFrameRate(TrueFPS)){
        //Serial.print(ShortTrend1[0]);  Serial.print(" ");
        //Serial.print(ShortTrend1[1]);  Serial.print(" ");
        //Serial.println(ShortTrend1[2]);
      RefreshOLED(ScanRate,TrueFPS,ShortTrend1,ShortTrend2,ShortTrend3,Min1,Max1,Min2,Max2,Min3,Max3);
    }
    //delay (50);
  }
 
}

  

The part that is really confusing me, is very punctual... the code like this "runs"... it doesn't do anything yet, but it compiles, and it shows the screen correctly.

but for example, this section, inside the while cycle in Loop:

    PushShortTrend(ShortTrend1,LongTrend1);
    /////////////////////////
    Max1=LongTrend1[0];
    //delay(1);
    ////////////////////////

Given what I mentioned, that is obviously wrong... my Max value should come from an evaluation, not just the newest value from the array... correct?

well, that's just the sample... the part that is thwoing me for a loop is that if I try to do ANYTHING with that array, the whole code crashes... and I have no idea why.

easiest example? If I switch the index to 1 instead of zero, as in:
Max1=LongTrend1[1];

it still compiles, it still runs... and yet, now all you can see in the screen is a frozen image that flashes every two seconds. no live data, no action from the button.

I can't understand what kind of mistake I could have done that I'm able to use a whole array, but only address the first register...

any help or suggestions is very welcome!

Which micro controller are you using to make several multi-100's celled arrays' with?

According to the seller the microcontroller is an ATmega32u4, its a generic board, but every other project I've done with it, including several controlling the OLED screen, worked as expected.

Not sure whether its related... but I've been compiling as "Arduino Micro", although the seller labeled it "Pro Micro Leonardo compatible"

thanks for your reply!

An int takes up 2 bytes. A 500 cell int array will take up 1000 bytes of RAM. There are 3 500 cell arrays created then a few 100 cell arrays. Which may cause memory issues.

I could sacrifice some intended precision if I can gain functionality...

for example, instead of the 3 arrays 500 in lenght, I could keep refreshing the max and min values with the latest reads and keep reseting it every 200 ms, so I get a chance to read the spike value in the screen, but if I get several in a second, I can see those too... and only keep a rolling window average in the very 100 long, byte arrays...

I really can't see a way around the trend arrays, since they hold the values that will be displayed once the screen refresh routine triggers...

do you think that could help the errors? it is very weird how it fails kind of selectively...

Running out of RAM often leads to random failures like you're seeing.

I recently ran into a similar problem trying to help someone on this forum. His previous "helper" had created an array that required around 5,000 bytes and was expected to run on a Nano. Ain't gonna happen.

Easiest fix was to change hardware. Put it on an ESP or on a black pill/blue pill and you have tons of RAM for stuff like that.

BTW: the 128x64 takes 20ms to do a screen update (with the Adafruit SSD1306 libraries on an arduino Nano using the I2C interface), so keep that in mind when displaying dynamic data.

Well, what do you know! it seems it was memory issues after all... thank you! I'd have never figured that out by myself.

the program is not finished yet, but i got rid of the int[500] and made them 100 instead, and to keep some consistency i'm averaging 10 reads now... not as granularly accurate but well, at least it now works!

here's the code so far in case anyone is ever interested in doing something similar...

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

int Boton1Pin = 8;
int Boton1State = LOW;

int maxValue,ActualValue,LongTrendSize = 500,ShortTrendSize=100;

unsigned long lastFrame = 0, LastScanMillis=0;
int fps = 15; //number of screen refreshes per second


bool TriggerFrameRate(float &TrueFPS){
  /////////////////////////////////////////////////////////////////////////
  // use this function to return the confirmation that this specific scan,
  // the routine to call for the display update can be called.
  /////////////////////////////////////////////////////////////////////////
  int RefreshDelay = 1000/fps;
  unsigned long Imillis=0;
  float Fmillis=0.0,FlastFrame=0.0,FloatFPS;
  if(millis()-lastFrame >= RefreshDelay){
    Imillis=millis();
    Fmillis=(float) Imillis;
    FlastFrame=(float) lastFrame;
    FloatFPS = 1000.0 / (Fmillis-FlastFrame);
    TrueFPS=FloatFPS;
    lastFrame = millis();
    return true;
  }
  else{
    return false;
  }
}


void  ReadInputs(){
  /////////////////////////////////////////////////////////////////////////
  //  Routine to make all the needed digital reads and analog reads,
  // so the rest of the program doesn't repeat any.
  /////////////////////////////////////////////////////////////////////////
  Boton1State = digitalRead(Boton1Pin);
}


void PushShortTrend(byte (&trend)[100], int (&Ltrend)[100]){
  /////////////////////////////////////////////////////////////////////////
  // This subrutine will calculate the values for the short arrays, the arrays
  //that will hold the value for the Y axis offset in the OLED screen.
  /////////////////////////////////////////////////////////////////////////
  int i,promedio,Total;
  for(i=100;i>=2;i--){
    trend[i-1]=trend[i-2];
  }  
  //because the input will be in a range from 0..1023 and the trend can
  //only be 16 bits high, divide by 64 and store as the newest value.
  trend[0]=Ltrend[0]/64;
}

void PushLongTrend(int (&trend)[100], int NewValue){
  /////////////////////////////////////////////////////////////////////////
  //This subrutine will add the newest read value to the long array, and
  //offset the rest of the values.
  /////////////////////////////////////////////////////////////////////////
  int i;
  for(i=100;i>=2;i--){
    trend[i-1]=trend[i-2];
  }
  trend[0]=NewValue;
}

int GetValue(int estado, int &valor){
  /////////////////////////////////////////////////////////////////////////
  // DEBUGGING SUBRUTINE. used to "emulate" analog inputs from a button...
  // when the button is pressed, the input scales up, when it is released,
  // it scales back down.
  /////////////////////////////////////////////////////////////////////////
  int output;
  if(estado==LOW && valor>0){
    valor--;
  }
  if(estado==HIGH && valor<1023){
    valor++;
  }
  return ActualValue;
}

void DrawTrend(byte (&trend)[100], int x, int y){
  /////////////////////////////////////////////////////////////////////////
  // This routine will draw the trend based on the short array values.
  // it will receive the "x" and "y" coordinates of the top left corner where
  // the trend is to be drawn.
  /////////////////////////////////////////////////////////////////////////
  int i,TempInt;
  for (i=0;i<100;i++){
    TempInt=trend[i];
    display.drawPixel(x+99-i,y+15-TempInt, WHITE);
  }
  //Serial.print(trend[0]);  Serial.print(" ");
  //Serial.print(trend[1]);  Serial.print(" ");
  //Serial.println(trend[2]);
}

int FindMaxVal(int (&trend)[100]){
  /////////////////////////////////////////////////////////////////////////
  // subrutine to find the highest value in the long array and return it.
  /////////////////////////////////////////////////////////////////////////
  int i,MaxVal=0;
  for (i=0;i<100;i++){
    if(trend[i]>MaxVal){
      MaxVal=trend[i];
    }
  }
  return MaxVal;
}

int FindMinVal(int (&trend)[100]){
  /////////////////////////////////////////////////////////////////////////
  // subrutine to find the lowest value in the long trend and return it
  /////////////////////////////////////////////////////////////////////////
  int i,MinVal=1500;
  for (i=0;i<100;i++){
    if(trend[i]<MinVal){
      MinVal=trend[i];
    }
  }
  return MinVal;
}

void RefreshOLED(int ScanRate, float FPS, byte (&trend1)[100], byte (&trend2)[100], byte (&trend3)[100], int Min1, int Max1, int Min2, int Max2, int Min3, int Max3){
  /////////////////////////////////////////////////////////////////////////
  // Subrutine to draw the full screen. it will only run when called by the Frame rate interrupt,
  // we give this routine all the final data, it will clean the screen, write every value, draw every line and pixel,
  // and then refresh the screen.
  /////////////////////////////////////////////////////////////////////////
  int testint;
  testint = trend1[0];
  display.clearDisplay();
  display.setCursor(0,0);   display.print("Scan:      FPS:");
  display.setCursor(30,0);  display.print(ScanRate);
  display.setCursor(88,0); display.print(FPS);
  display.setCursor(0,16);  display.print(Max1);
  display.setCursor(0,24);  display.print(Min1);
  display.setCursor(0,32);  display.print(Max2);
  display.setCursor(0,40);  display.print(Min2);
  display.setCursor(0,48);  display.print(Max3);
  display.setCursor(0,56);  display.print(Min3);
  display.drawLine(26, 16, 26, 63, WHITE);    //vertical left line, 26,16,26,63
  display.drawLine(127, 16, 127, 63, WHITE);  //vertical right line, 127,16,127,63
  display.drawLine(27, 31, 126, 31, WHITE);   //horizontal top, 27,31,126,31
  display.drawLine(27, 47, 126, 47, WHITE);   //horizontal middle, 27,47,126,47
  display.drawLine(27, 63, 126, 63, WHITE);   //horizontal bottom, 27,63,126,63
  DrawTrend(trend1, 27, 16);
  //testint = trend1[0];
  //display.drawPixel(27,25, WHITE);
  //
  //
  display.display();
}

void setup() {
  /////////////////////////////////////////////////////////////////////////
  // setup; declaring inputs, serial and text
  // 
  /////////////////////////////////////////////////////////////////////////
  int i;
  pinMode(Boton1Pin, INPUT);
  Serial.begin(9600);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
    for(;;);
  }
  delay(2000);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
}

void loop() {
  /////////////////////////////////////////////////////////////////////////
  // Loop routine,
  //
  //    VARIABLE DEFINITION AND INITIALIZATION
  //
  /////////////////////////////////////////////////////////////////////////
  byte ShortTrend1[100],ShortTrend2[100],ShortTrend3[100];
  int LongTrend1[100], LongTrend2[100], LongTrend3[100];
  int InstTrend1[10],InstTrend2[10],InstTrend3[10];
  int AvgInst1=0,AvgInst2=0,AvgInst3=0;
  int Min1=1000,Max1=1009,Min2=2000,Max2=2009,Min3=3000,Max3=3009;
  int i,ScanRate=444;
  float TrueFPS=66.6;
  int Accum1=0, Accum2=0, Accum3=0;
  for(i=0;i<100;i++){
    ShortTrend1[i]=0;
    ShortTrend2[i]=0;
    ShortTrend3[i]=0;
  }
  for(i=0;i<100;i++){
    LongTrend1[i]=0;
    LongTrend2[i]=0;
    LongTrend3[i]=0;
  }
  for(i=0;i<10;i++){
    InstTrend1[i]=0;
    InstTrend2[i]=0;
    InstTrend3[i]=0;
  }
  /////////////////////////////////////////////////////////////////////////
  //
  //    The actual Cycle starts down here.
  //
  /////////////////////////////////////////////////////////////////////////
  while (true){
    for(i=0;i<10;i++){
      ReadInputs();
      GetValue(Boton1State,Accum1);
      InstTrend1[i]=Accum1;
    }
    AvgInst1=(InstTrend1[0]+InstTrend1[1]+
              InstTrend1[2]+InstTrend1[3]+
              InstTrend1[4]+InstTrend1[5]+
              InstTrend1[6]+InstTrend1[7]+
              InstTrend1[8]+InstTrend1[9])/10;
    PushLongTrend(LongTrend1,AvgInst1);
    PushShortTrend(ShortTrend1,LongTrend1);
    /////////////////////////
    //Max1=LongTrend1[0];
    //delay(1);
    ////////////////////////
    Min1 = FindMinVal(LongTrend1);    Max1 = FindMaxVal(LongTrend1);
    //Min2 = FindMinVal(LongTrend2);    Max2 = FindMaxVal(LongTrend2);
    //Min3 = FindMinVal(LongTrend3);    Max3 = FindMaxVal(LongTrend3);
    //
    //
    //
    //
    ScanRate=millis()-LastScanMillis;
    //Serial.print(LongTrend1[0]);  Serial.print(" ");
    //Serial.print(LongTrend1[1]);  Serial.print(" ");
    //Serial.println(LongTrend1[2]);
    LastScanMillis=millis();
    if(TriggerFrameRate(TrueFPS)){
        //Serial.print(ShortTrend1[0]);  Serial.print(" ");
        //Serial.print(ShortTrend1[1]);  Serial.print(" ");
        //Serial.println(ShortTrend1[2]);
      RefreshOLED(ScanRate,TrueFPS,ShortTrend1,ShortTrend2,ShortTrend3,Min1,Max1,Min2,Max2,Min3,Max3);
    }
    //delay (50);
  }
 
}

  

and in case anyone would rather get some visual feedback, here's what the trend looks like:
trigger2

hey, thanks! that was also why I wanted to add the FPS display on top... I found out by trial and error that any time I tried to increase the refresh rate it simply lagged the rest of the program horribly... and it still clamped the refresh rate around 25 frames per second... although unless i messed up somewhere that would be a refresh of around 40 ms, not 20... oh well, still figuring this out, but thanks!

It's been, perhaps 50 years since I programmed computing trends. Back then a trend was somewhere between -1 and +1. With the 1's being straight vertical line. Zero trend was straight horizontal line. Trend being whether the difference between readings is changing upward or downward. Basically a second derivative of the calculation, or acceleration of changes.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.