Optimizing i2C for OLED display on nano.

Hi Guys,

I have made a pan controller with a stepper motor for time-lapse photography and added a small 128x32 OLED display and RTC. I am using a nano with i2C pins A4 and A5.

The code checks an alarm time against the current time and begins to rotate the stepper when the alarm time is reached. The number of degrees to rotate is entered and the desired time to rotate is also configurable.

Everything displays correctly and works almost perfectly except for some rounding errors that make the angles a few degrees off which is negligible for my application.

My real issue that I would like some help with please is when I set the rotation time to be quite fast, 2 minutes for example. The code that prints via i2c to the OLED display is very slow and causes the delay between stepper motor steps to be very low and hence I cannot achieve maximum rotation speed.

I would love some help to optimize my code so that i can accommodate for fast rotation times too.

Any advice would be greatly appreciated please :slight_smile:

NEW_SkyTracker_170420_V4.ino (11.1 KB)

1 Like

Instead of clearing the display and reprinting everything each time through the loop, print the entire display only once, then only print the data that changes by re-positioning the cursor to the appropriate location and overwriting the old data with the new. It looks like the only thing that is going to change is the time and the current step number, and if you save the previous values for the data and only print when the value changes you will not have to send hours or minutes very often either. Will work better if you format the display so that the values have leading zeros or spaces, so they always occupy the same number of characters on the display.

Also be careful of printing too much to Serial, by printing every time through the loop, even at 115200 baud the send buffer will fill up and the code will be delayed while waiting for characters to be send.

1 Like

thanks David

ill suss that out now.

The serial stuff I comment out at runtime but so far has made no significant impact when comparing it to the massive delay of the I2C communication that grind everyting to a halt.

I have also been reading into increasing the clock frequency of the i2C lines to 400Khz but haven't noticed a difference..

Anyone got any ideas if what I am doing is actually making an impact to the i2c speed?

 Wire.begin();                                           // start i2C
  Wire.setClock(400000);                                  // Crank up the i2C speed

Have you already implemented the excellent suggestions provided by @david_2018?

david_2018:
Instead of clearing the display and reprinting everything each time through the loop, print the entire display only once, then only print the data that changes by re-positioning the cursor to the appropriate location and overwriting the old data with the new. It looks like the only thing that is going to change is the time and the current step number, and if you save the previous values for the data and only print when the value changes you will not have to send hours or minutes very often either. Will work better if you format the display so that the values have leading zeros or spaces, so they always occupy the same number of characters on the display.

This is the best solution indeed.
But to implement that, OP would have to use a different OLED library. The Adafruit_SSD1306 library always writes the entire display buffer, regardless of what pixels changed.

You could also try moving the stepper code to a timer interrupt, but this could lead to all kinds of other problems ...

Personally, I'd use an SPI OLED display. SPI supports up to 8 MHz on a Nano.

Pieter

PieterP:
This is the best solution indeed.
But to implement that, OP would have to use a different OLED library. The Adafruit_SSD1306 library always writes the entire display buffer, regardless of what pixels changed.

You could also try moving the stepper code to a timer interrupt, but this could lead to all kinds of other problems ...

Personally, I'd use an SPI OLED display. SPI supports up to 8 MHz on a Nano.

Pieter

You can use display.display() without clearing the entire screen and redrawing everything. Where is the bottleneck? If it is transferring the entire display buffer, I guess you are hosed.

a7

alto777:
You can use display.display() without clearing the entire screen and redrawing everything. Where is the bottleneck? If it is transferring the entire display buffer, I guess you are hosed.

Adafruit_SSD1306::display() always transfers the entire buffer:

PieterP:
But to implement that, OP would have to use a different OLED library. The Adafruit_SSD1306 library always writes the entire display buffer, regardless of what pixels changed.

Sorry, I'm not that familiar with OLED displays, didn't think to look up if the display was buffered.

It would still help if the OP saved the values of the data that was being displayed, and only updated the display when the data changes. As it is, the display is being refreshed every time through loop(), before checking if the delay between steps has expired. You can cut that down to updating once for the new value of step, and once per second to update the clock. If you really wanted to get creative, you could measure the refresh time of the display, then check to see if there is sufficient time before the next step before refreshing the display.

hi guys,

Thanks for the feedback.

I am working on the changes now because i wanted to get the thing setup overnight for sunrise this morning. which kinda failed... details below...

I am in NZ currently in pretty tight lock down so I have literally made this thing with bits i have at home, some scrap metal and a bearing from a fidget spinner and thank god for the tube of epoxy glue I have. Luckily for me i have some arduino stuff but no SPI screens and there are no stores allowed to sell me one at the moment, so the SPI version is out until after lock down.

I have found a new issue that i cannot recreate and I would love some help to identify please.

So I set up the camera (old gopro) and motor "sky tracker" last night with the following settings:

  • Angle to rotate; 100 degrees
  • Time to rotate: 12 hours
  • Time to start Rotation: 07:00

p.s. The Gopro is configured to take a photo every 10 seconds and to start taking photos at about 06:30 but nothing to do with the arduino.

what happened is that the alarm time was reached and it seems that the motor was rotated from 0 to 400ish steps in the first 6 minutes...

I noticed because i went outside to check on it and found that it was facing the wrong direction for that early in the morning... ive attached a photo showing the time and current steps which doesnt add up.

I don't understand how it could have got through 412 steps in 5-6 minutes with a delay of 75922 milliseconds between steps.

On my workshop bench using the same power supply everything works perfectly (except the initial issue of speed). I've attached some videos showing it working with a rotation time of 1.2 minutes (0.02 hour)

Any help would be really appreciated.

NEW_SkyTracker_190420_V4_1.ino (11.5 KB)

and here is the video...

Rotation Start.zip (826 KB)

and the photo showing 412 steps in 6 minutes... with a delay of 75922 milli between steps.... ????

The rapid rotation in the first few minutes is caused by how you are doing the millis() timer for the stepper motor. The variable time_now was initialized to zero, but never gets updated until the stepper motor code starts running. You are checking if millis() is >= (time_now + DelayBetweenStep), then if true you are stepping the motor and adding DelayBetweenStep to time_now. The trouble is, millis() counts the number of milliseconds since the sketch began running, so time_now is going to take a long time to catch up to the value of millis(), causing your stepper motor to step every time through the loop.

Hi David,

Thanks for the reply.

Definitely melting my brain..

So the reason I am not seeing it on my bench testing is because I am only waiting a few minutes before I start the rotation and time_now doesn't have much of a chance to drift from the milli count ? and after waiting overnight it does?

Is the solution as simple as making time_now = millis() at the beginning of each void iteration?

void loop() {

time_now = millis();  // Like this? 

//.. everything else...

dannysprogis:
So the reason I am not seeing it on my bench testing is because I am only waiting a few minutes before I start the rotation and time_now doesn't have much of a chance to drift from the milli count ? and after waiting overnight it does?

That is essentially correct, after 8 hours millis() will have a value of 28,800,000. With a delay of 75922mS per step it would take around 379 steps before you started seeing the delay between steps.

Is the solution as simple as making time_now = millis() at the beginning of each void iteration?

No, then millis() would never be >= time_now + DelayBetweenSteps.

You need to initialize time_now when the alarm time has been reached, that will synchronize it with the current millis() value.

  if (DoneSelectedRotation == true) //only check the alarm if alarm time has not been reached
  {                                 //this prevents this code from executing more than once
    if (now.hour() == Alarm_Hour)                  // check alarm
    {
      if (now.minute() == Alarm_Minute)
      {
        Serial.println("The time is now!");        //if alarm time
        DoneSelectedRotation = false;
        time_now = millis() - DelayBetweenStep; //initialize time_now so that stepper motor steps immediately
      }
    }
  }

I would also change the if statement that is checking millis() against the timer, the way you have it written will eventually give incorrect results. You are unlikely to notice it in your application, but at around 50 days of runtime the millis() counter overflows from its maximum value to 0, and starts counting up from 0 again. Slightly before this time, the value of time_now + DelayBetweenStep will overflow, resulting in a very low number, while millis() is still near its maximum value, making the if statement true when it should not be.

    if (millis() >= time_now + DelayBetweenStep)
    {
      time_now += DelayBetweenStep;

The convention way of writing the if statement would be like this, although normally time_now would be set to millis() inside the if statement. Incrementing it by DelayBetweenStep will work, and avoids a slight drift in the time interval when you are a millisecond or two late getting to the if statement.

if ( (millis() - time_now) >= DelayBetweenStep) {
  time_now += DelayBetweenStep;
  ...
}

I would also change DelayBetweenStep to an unsigned long, I don't see anywhere you are using it that needs it to be a float, and millis() timing is all does as unsigned integer math.

thanks David,

I have only just figured out that it needs to go in the alarm.

if (now.hour()== Alarm_Hour)                            // check alarm
 {
    if(now.minute()== Alarm_Minute)
      {
       Serial.println("The time is now!");              //if alarm time 
       DoneSelectedRotation = false;  
       time_now = millis();
       Serial.print("time_now: ");              
       Serial.println(time_now);   
        
      }
 }

ill go through your other comments now and adjust my code!

thanks for the help..

Thanks for the help David.

I will see how this runs for sunrise tomorrow... fingers crossed it is a colorful one.

Here is the code that seems to be working.

ill confirm tomorrow...

thanks again!!!

NEW_SkyTracker_190420_V4_2.ino (11.8 KB)

You need to make sure your alarm checking code only runs once, otherwise it will constantly be resetting time_now for the 60 second period that Alarm_Hour and Alarm_Minute equal the current time. Conveniently, DoneSelectedRotation can be used as an indicator to tell if the alarm condition has been met. In the code I posted, I was only checking the alarm if DoneSelectedRotation = true.

  if (now.hour() == Alarm_Hour)                           // check alarm
  {
    if (now.minute() == Alarm_Minute)
    {
      Serial.println("The time is now!");              //if alarm time
      DoneSelectedRotation = false;
      time_now = millis() - DelayBetweenStep;          //initialize time_now so that stepper motor steps immediately
      Serial.print("time_now: ");
      Serial.println(time_now);

    }
  }

thanks David,

I realized this but wasn't particularly concerned because I was not worried about 1 minute accuracy. i assumed that it would work correct at 7:01 but it turns out that it failed again this morning.

at 9:38 ( 2 hours 38 minutes after 7am) it had already moved 448 of 569 steps.

The delay between steps is 75922ms

I will make the adjustment to run the alarm checking code only once but im not sure if that fixes my above issue :frowning:

and ideas?

here is the update to the alarm check. i am currently running a longer test (2 hours) to see if i can identify why it failed this morning...

if (now.hour()== Alarm_Hour)                            // check alarm
 {
    if(now.minute()== Alarm_Minute)
      {
       Serial.println("The time is now!");              //if alarm time 
        if (DoneSelectedRotation == true)               // first time  
           {          
              Serial.print("Set time_now only once per alarm time........................................ ");              
              time_now = millis() - DelayBetweenStep;          //initialize time_now so that stepper motor steps immediately
           }
       DoneSelectedRotation = false;  
       Serial.print("time_now: ");              
       Serial.println(time_now);   
        
      }
 }