Correct millis() with an RTC

I’m trying to achieve 40ms accuracy over 1h on an Arduino Mega. The board is controlling a long addressable led strip via fastLED.h. This library disables interrupts so millis() doesn’t get incremented while they are disabled.
I’ve made a test program of the worst scenario that updates the strip at every cycle. With the (crappy) correction built into the library millis() is about one second behind every 36 seconds, which is way too much for what I’m trying to do.

I’ve tried to hook up another Arduino and use it only to keep time, but guess what, I2C calls I use to transfer the time variable rely on interrupts. Using a voltage change on two linked output/input ports of the Arduinos to time events is out of question since it would make programming the full routine pretty annoying.

I therefore added an RTC board but I’m having trouble figuring out a good way to implement it.
The easiest approach I thought of is waiting for a change in the time of the RTC (meaning that one second has just passed) and build another millis() counter off of that.

void setup{
    setRTC(0,0,0,0,0,0,0); //resets the RTC
    currentSec = 1;
}

void loop{
    if(RTCtimeinsec() == currentSec){
        //RTCtimeinsec() returns the time stored in the RTC converted in seconds
        compensatedMillis = currentSec*1000;
        zeroDecimalsReference = millis();
        currentSec++;
    }
}

To use the compensated time:

compensatedMillis = compensatedMillis + (millis() - zeroDecimalsReference);

This way seconds are kept with the accuracy of the RTC and the decimal resolution is provided by millis(). Decimal accuracy is not perfect but over one second this error is negligible.

The problem with this approach is that if inside the loop there is other code that takes too long to execute the detection of the RTC time change is not accurate.
In my particular case loop() contains a pre-programmed lighting routine that takes 1h to execute and uses lots of for/while loops.

I was thinking of some kind of round robin between my routine and the code that keeps compensatedMillis updated.
Any ideas on how to approach it or a better way to keep accurate time?

Thanks!

How many leds are there in the ledstrip ?
Can you tell more about timing the voltage change that you want to measure ?

A few possible solutions:

  • Make the FastLED patterns with millis() instead of delay() to let the loop() run hundreds of times per second.
  • Use another board. The FastLED library behaves differently on different boards.
  • There are other libraries for other boards, for example a library that uses DMA. They are mentioned in the Adafruit NeoPixel Überguide.
  • Put everything on a ESP32 and make use of the FreeRTOS. Replace delay() with vTaskDelay().
  • Add an extra Arduino board for the leds, or add another Arduino to measure the time critical signals.
  • Buy an other ledstrip that has data + clock signals.
  • A software I2C library does not use interrupts.
  • A Raspberry Pi Pico can use one of its programmable blocks to create the NeoPixel signal. Then the rest of the code is not influenced at all anymore. This is still in development, but if you can wait half a year, than this would be a good solution.

Trying to compensate millis() does not feel right. If you want the time, then use a RTC. Does your Arduino board have a crystal or a resonator for the 16MHz ? That makes a big difference.
The TimeLib uses millis() to keep the time going, but how often the time is updated from the RTC can be set with setSyncInterval().

Use a REAL RTC! There is no real way to keep accurate time with the 328 due to the effects of temperature on the clock source. While using a Crystal is the most accurate, there is still a small amount of drift, and a Resonator WILL drift with temperature. You can buy temperature compensated/stabalised Clock souces, these are very expensive compared to a crystal or resinator.

I’m using a DS3231SN, isn’t it accurate enough over just one hour? I read that it should be accurate down to a few minutes per year…

I need accurate time over 1h with millisecond resolution, the RTC only goes down to seconds. I can’t use only millis() because it’s accuracy is compromised by fastLED library.

WS2812B, 507 leds.

It has a crystal, I’ve tested it and in a sketch that only keeps track of time, but only in this case is accurate enough.

Patters are done with millis(), but are used in a pre-programmed sequence synced to music. Something like this: run this animation for 2 seconds, blink 5 times, dim to black over 3 seconds, etc for a total length of about 1 hour. I think optimizing and making it so that loop() can run a hundred times a second wouldn’t be easy.

This was just my first idea I discarded.

Then there was something else sketchy going on, transmitting millis() of the second Arduino via I2C was making it inaccurate as well.

This is basically what I was trying to do, I’ll look into that.
My only concern is if my routine is halted when time gets updated from the RTC, and if so for how long.

Dunno if this is useful but you can take the seconds from the RTC and add the ms modulus from millis:

uint32_t rtc_millis() { return (RTCtimeinsec() * 1000) + (millis() % 1000); }

This will be more accurate, but not 100% reliable.

Millis cannot be used with any expectation of accuracy when using FastLED. Interrupts are disabled while updating the LEDs, stopping the millis update. FastLED tries to correct the millis counter but this is only an approximate amount and not highly accurate.

You may need to use another board, I’ve never worked with one, but I know there ate teensy boards that update LEDs via direct memory access in the background, so do not interfere with interrupts.

You could also use another arduino, but instead of I2C or SPI, transfer an 8-bit synchronizing count over the I/O pins so it can be read rapidly.

@alemonti, you’ve chosen to ignore the two best (IMO) alternatives presented to you:

  1. Switch to a board that supports driving NeoPixels via DMA.
  2. Switch to SPI-based LED strips that work with Clock / Data signals and don’t require interrupts to be disabled (i.e. APA102 or DotStar).

Also, isn’t this post a duplicate of Your Previous One?

More like a continuation, I’ve tried the approaches that were explained to me there but as you can see I still haven’t managed to sort the problem out.

As far as I know most of these boards (like the Arduino Due) run on 3.3V [I could be wrong] and since my project also uses regular led strips controlled via MOSFETs with 5V logic I would prefer not having to change all my circuitry to run at 3.3V. Also I’ve never worked with teensy boards.

I already have the WS2812B strips wired in place, changing them is my very last option.

This sounds great. Can you please elaborate more on how to do it?

You could use a barebone ATtiny as external clock source, that would eliminate the issue with disabled interrupts on the primary controller.

You could also use an ESP32 with a logic level shifter. The RMT module of the ESP is very easy to work with and I’m pretty sure that FastLED supports it which means that you only have to change very little in your code.

The processors on Teensy boards do run at 3.3V. However, they contain an on-board voltage regulator that can accept a 5V supply.

For driving the WS2812B and APA102 inputs with 3.3V logic, I always level shift with a 74HCT125. It’s very fast, uses VCC = 5V, and shifts 3.3V inputs up to 5V. Works great.

You could also drive the MOSFET gates with 74HCT125. However, I’ve never had a problem using 3.3V logic directly from the processor for this.

Better idea, if this is related to your other post for synchronizing to a video. Use one of the hardware timers in the mega to generate an interrupt at the frame rate of the video (24 fps?). Synchronize this to the one second interrupt from an RTC. Update the LEDs with FastLED ONLY immediately after you receive the timer interrupt (but not inside the ISR). This should give you plenty of time to update the LEDs before the next interrupt occurs.

You could write your own version of millis() with Timer1 and a prescale of 8. Since it is a 16-bit timer you would get interrupts only 30-ish times a second instead of 980. That means you can have interrupts disabled for MUCH longer before you lose time ticks. The resolution would be 1/2 microsecond.

With a larger prescale, like 64, you would get about 3.8 interrupts per second and still have 4-microsecond resolution.

1 Like

Thought about it a bit more.

Forget about using millis for timing. You are syncing to a video, likely at 24fps, so create your own frame counter and time off of that.

The DS3231 RTC has a SQW output that can be set to output a 32kHz square wave. Use that along with a 16-bit counter/timer to generate an interrupt every 1/24 of a second to increment the frame counter.

The LEDs are timed from the frame counter, so as long as you can complete the update in a bit less than 1/24 second there will be no interference with the timer interrupt, and you get the accuracy of the RTC.

The video is actually 25fps, it shouldn’t be a problem though.

It sounds great. I get the idea, I’ll try to implement it.
May I ask why the 32kHz wave in particular?

The theoretical update time for the whole strip is about 15ms, so well within margin.

I’m using 32kHz because that is the highest frequency the DS3231 will produce on the SQW output. There is also an output with 32768Hz, a more common clock crystal frequency, but 32kHz works nicely with 25fps because 25 divides evenly into 32000.

I’m not very experienced with using the counter/timers. Had a look at the mega and only timers 0 and 5 have the external clock input available on the headers, with timer 5 using digital pin 47. Here is a test sketch that seems to work for generating a frame counter at a rate of 25fps, I’m sure someone will correct any gross errors I’m made:

volatile uint32_t frameCounter = 0;
uint32_t frameCount;
uint32_t frameCountPrev;

void setup() {
  Serial.begin(115200);
  Serial.println("startup");
  
  TCCR5A = 0; //clear control register A
  TCCR5B = 0; //clear control register B
  TCNT5 = 0;  //clear counter
  OCR5A = 1279; //set value for output compare register A  (32000Hz * 1/25 second) - 1 = 1279
  TCCR5B |= (1 << WGM52); //Set CTC mode (WGM5 = 0100);
  TCCR5B |= (1 << CS52) | (1 << CS51) | (1 << CS50) ; //External Clock mode using D47 as input
  TIMSK5 |= (1 << OCIE5A); //Set the interrupt request
  sei(); //Enable interrupt
}

void loop() {
  //temporarily disable interrupts while making a copy of frameCounter
  noInterrupts();
  frameCount = frameCounter;
  interrupts();
  
  if (frameCount != frameCountPrev){
    frameCountPrev = frameCount;
    Serial.print(frameCount);
    //print millis every 25 frames to test timing
    if ((frameCount % 25) == 0){ 
      Serial.print('\t');
      Serial.print(millis());
    }
    Serial.println();
  }
}

ISR(TIMER5_COMPA_vect) {   //This is the interrupt request
  frameCounter++;
}

Need to make some corrections to my previous posts.

The DS3231 SQW output pin can be configured to produce a square wave output of 1Hz, 1.024kHz, 4.096kHz, or 8.192kHz, there is no option for 32kHz from this pin. The 32K output pin produces a square wave of 32.678kHz.

A bit more difficult to produce the 25fps count with 32.678kHz because it does not divide evenly. A single cycle at 32.678kHz is 30.518 microSeconds, a discrepancy of that amount between one frame and the next will hopefully be tolerable, as long as the long-term timing is correct.

This code seems to work correctly for maintaining correct timing while using FastLED:

#include <FastLED.h>
#include "RTClib.h" //RTClib library

RTC_DS3231 rtc;

#define NUM_LEDS 507
#define DATA_PIN 3
CRGB leds[NUM_LEDS];

CRGB testimage[NUM_LEDS]; //for testing purposes only

volatile uint32_t frameCounter = 0;
uint32_t frameCount;
uint32_t frameCountPrev;

void setup() {
  //create some random LED data to ensure compiler does not remove the FastLED code
  for (size_t i = 0; i < NUM_LEDS; i++) {
    testimage[i] = CRGB(random(256), random(256), random(256));
  }
  
  Serial.begin(115200);
  Serial.println("startup");
  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }
  if (rtc.lostPower()) {
    Serial.println("RTC lost power, setting time");
    // If the RTC have lost power it will sets the RTC to the date & time this sketch was compiled
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
  
  if (!rtc.isEnabled32K()) { //verify 32K output is enabled
    rtc.enable32K();
  }
  
  TCCR5A = 0; //clear control register A
  TCCR5B = 0; //clear control register B
  TCNT5 = 0;  //clear counter
  OCR5A = 1309; //set value for output compare register A  (32768Hz * 1/25 second) - 1 = 1309
  TCCR5B |= (1 << WGM52); //Set CTC mode (WGM5 = 0100);
  TCCR5B |= (1 << CS52) | (1 << CS51) | (1 << CS50) ; //External Clock mode using D47 as input
  TIMSK5 |= (1 << OCIE5A); //Set the interrupt request
  sei(); //Enable interrupt

  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);  // GRB ordering is assumed
}



void loop() {
  //temporarily disable interrupts while making a copy of frameCounter
  noInterrupts();
  frameCount = frameCounter;
  interrupts();

  if (frameCount != frameCountPrev) { //only execute once per frame
    if ((frameCount - frameCountPrev) != 1) {
      Serial.println("missed frame");
      while (1) {};
    }
    frameCountPrev = frameCount;
    for (size_t i = 0; i < NUM_LEDS; i++) { //copy testimage to LED array
      leds[i] = testimage[i];
    }
    FastLED.show();
    Serial.print(frameCount);
    //print millis every 25 frames to test timing
    if ((frameCount % 25) == 0) {
      Serial.print('\t');
      Serial.print(millis());
    }
    Serial.println();
  }
}

ISR(TIMER5_COMPA_vect) {   //This is the interrupt request
  static byte cycleCount = 24;
  //adjustment to compensate for 32768 not being evenly divisible by 25
  //counter will count ( (7 * 1310) + (18 * 1311) ) = 32768 pulses over 25 frames
  if ((cycleCount & 0x03) == 0) {
    OCR5A = 1309; //1310 clock pulses
  } else {
    OCR5A = 1310; //1311 clock pulses
  }
  if (cycleCount == 0) {
    cycleCount = 25;
  }
  cycleCount--;

  frameCounter++; //actual frame counter
}
1 Like

@david_2018 thank you so much for providing code, I’ve take a look at it and it seems very good (and well made!). I’ll try it in the next few days.
Thanks again!

@david_2018 I’ve tried your code but I’m not able to get it working. It looks like frameCounter never increments so if the interrupt routine is correct I suspect that SQW is not working properly. Have you tried it? My wiring is as follows:
GND - GND
VCC - 5V
SDA - D20
SCL - D21
SQW - D47
I’ve also tried adding a 10k pullup between SQW and VCC, but no luck. The SQW output on the module could be broken as it was used in the past by someone else. I’ve ordered a new one and I’m going to try it tomorrow.
Am I missing out on something obvious?

Sorry, forgot to point out that the code uses the 32K output pin of the DS3231, not SQW.

Opss I saw “enable32K()” and assumed it was referred to the SQW output set at 32KHz (which now that I look back at it you wrote it can’t be), my bad. I had my mind set on the SQW pin, didn’t even think about the 32K pin being capable of outputting a square wave, I should have read more accurately the datasheet and your posts.
The code now runs fine, tomorrow I’ll test its accuracy on the long run.
Thanks for your help :slight_smile: