Varied intervals for millis

Hello everyone, I've learned a lot from these forums! I have also been very hesitant to post as I am an extremely firm believer in "figure it out your own darn self," to some extent.

I've been learning Arduino for about a month now, mostly just tinkering. However, I work in a lab and test proprietary electronic equipment that we design. This led me to my first real set of projects, creating simulators for our hardware that are far more dynamic than analog switches on a board, and in turn creating a chance to automate some tasks that are EXTREMELY time consuming when done manually.

The first project, which really lays the ground work for the next two as there will be a lot of reusable code that I can build upon, is a simulator to send PWM data to our device which interprets that into "impact" data. This allows me to bypass the normal method of getting our device out of what is called "training mode" - basically it learns what impacts are real and which can be ignored in a facility it is deployed in. In real time, on something like a forklift, this would take only 25 minutes. However, due to safety measures to prevent malicious spoofing of the system, doing this by hand can take hours to trick the system, and even that is difficult. As an idea, I've generated over 5000 impacts manually and it wasn't enough to "trick" it, but in the real world, this was achieved with only 24 impacts.

All that to bring us to the point I am now. I have designed and almost completely coded an Arduino Nano to in effect act as a forklift or vehicle. I have taken real data and programmed a sequence that does work, but like every newbie...I used the delay() function. Then worked my mind around millis(); because I need to be able to see if an interrupt comes from the screen.

I am using a Nextion Enhanced 2.4" display and an Arduino Nano (to save space and I don't need more than this for this device). Although I have read and watched and followed many tutorials, I do have a strong understanding of what is happening, and what I have written. Please know that although there are ids associated with these tutorials, I do understand them enough to have changed them to anything I wanted, but left them as is in case of troubleshooting. this is not a hodge-podge of copy paste (that drives me crazy lol)

Code below - long post....

// PINS TO CONTROL VAC
#define VAC_POS 12           //red positive 12v
#define VAC_SEAT 11         //black common negative
#define VAC_IGN 10           //orange seatswitch output
#define VAC_MOT 9           //brown ignition output
#define VAC_IMP_P 8                //blue motion output
#define VAC_IMP_Y 7         //green impact Y axis
#define VAC_IMP_X 6         //white impact X axis


// VARIABLES
int xG = 84;                 //Defines the integer for the X Axis impact Force
int xY = 0;                 //Defines the integer fo rht X Axis impact duration
int yG = 84;               //Defines the integer for the Y Axis impact Force
int yD = 0;                 //Defines the integer for the Y Axis impact duration
int long Gap = 0;           //Defines the integer for the Gap between impacts (ms)
int progressValue = 0;       //Defines the integer for updating the progress bar on display
//int runCal = no;
int X = 0;                   //Defines the integer for the X Axis Algorithm
int Y = 0;                   //Defines the interger for the Y Axis Algorithm
int cP = 4;           //Defines integer to count the progress bar and holds value to apply to progress bar on each successful impact generation
int i = 0;


void setup() { //Runs 1 time when Arduino is powered up
Serial.begin(115200);     //Set Baud Rate to communicate with Nextion Display

Serial.println("PowerFleet Impact Sensor Calibration Tool"); 
  
pinMode(VAC_POS, OUTPUT);
pinMode(VAC_SEAT, OUTPUT);
pinMode(VAC_IGN, OUTPUT);
pinMode(VAC_MOT, OUTPUT);
pinMode(VAC_IMP_P, INPUT);
pinMode(VAC_IMP_Y, OUTPUT);
pinMode(VAC_IMP_X, OUTPUT);
pinMode(CAL_INDICATOR, OUTPUT);
pinMode(CAL_FOCUS, OUTPUT);
}

void loop() { //Runs continuously while Arduino is on
  if(Serial.available()){                         //Check to see if Serial command is available
    String data_from_display="";                 //Empty String to collect serial data
    delay(30);                                     //30ms delay to catch entire string without double touch
    while(Serial.available()){
      data_from_display += char(Serial.read());   //Collect data and append to itself
      
    }
    Serial.println(data_from_display);             //Debugging Test - should send data from the display to the serial monitor – See primaryControls funtion
    primaryControl(data_from_display);             //Call the control functions
    trainingMode(data_from_display);               //Call trainingMode function
    //progressTest(data_from_display); //Testing and troubleshooting function – not listed here
  }

}


//FUNCTIONS TO BE CALLED IN VOID LOOP

/*CONTROLS FROM NEXTION DISPLAY – SENT VIA SERIAL TO ARDUINO*/
void primaryControl(String data_from_display){
  //POWER CONTROLS
  if(data_from_display == "PWR ON"){
    digitalWrite(VAC_POS, HIGH);                 //Turns ON relay to supply 12v+ to VAC
  }
  if(data_from_display == "PWR OFF"){
    digitalWrite(VAC_POS, LOW);                 //Turns OFF relay to supply 12V+ to VAC
  }
  
  //SEAT SWITCH CONTROLS
  if(data_from_display == "SEAT ON"){
    analogWrite(VAC_SEAT,153);                     //Turns ON Seat Switch Input to VAC
  }
  if(data_from_display == "SEAT OFF"){
    analogWrite(VAC_SEAT,0);                       //Turns OFF Seat Switch Input to VAC
  }
  
  //ENGINE CONTROLS //Additional Logic to be written for Electric vs Gas
  if(data_from_display == "ENG 00"){
    analogWrite(VAC_IGN, 153);                     //Turns ON Ignition Electric x2
    Serial.println("VAC ONE: Electric        VAC TWO: Electric");
  }
  else if(data_from_display == "ENG 01"){
    analogWrite(VAC_IGN,0);                       //Turns ON Ignition for Electric - Gas
    Serial.println("VAC ONE: Electric        VAC TWO: Gas");
  }
  else if(data_from_display == "ENG 10"){
    analogWrite(VAC_IGN,0);                       //Turns ON Ignition for Gas - Electric
    Serial.println("VAC ONE: Gas        VAC TWO: Electric");
  }
  else if(data_from_display == "ENG 11"){
    analogWrite(VAC_IGN,0);                       //Turns ON Ignition for Gas x2
    Serial.println("VAC ONE: Gas        VAC TWO: Gas");
  }
  else if(data_from_display == "ENG OFF"){
    analogWrite(VAC_IGN,0);                       //Turns OFF Ignition
  }

  //MOTION CONTROLS
  if(data_from_display == "MOT ON"){
    analogWrite(VAC_MOT, 153);                     //Turns ON Motion Input to VAC
  }
  if(data_from_display == "ENG OFF"){
    analogWrite(VAC_MOT,0);                       //Turns OFF Motion Input to VAC
  }

  //IMPACT CONTROLS //NOT BEING USED FROM HERE AT THIS TIME
  /*if(data_from_display == "CAL START"){           //Turns ON the Impact Calibration
    trainingMode();
  }
  if(data_from_display == "CAL STOP"){           //Turns OFF the Impact Calibration
    
 
}
*/
//Future write - ANALOG READ TO VERIFY IMPACT SENSOR IS READABLE
}


/* IMPACT GENERATION FUNCTION */ //This is used in the Training Mode Function. It cannot be a pure loop because the variables are not static on each impact.

void impactNow (int xG,int xD,int yG,int yD,int long Gap){ 
  //Example: impactNow(.6, 10, 0, 0, 5000); - .6G on X for 10ms and 0G on Y of 0ms with 5 seconds until next impact
  //xG  = Force on X Axis
  //xD  = Duration of Force on X Axis
  //yG  = Force on Y Axis
  //yD  = Duration of Force on X Axis
  //Gap = Time until next impact
  //84 is 1.65v - 50% Duty Cycle when mapped 0-5V = 0-255
  //6 = .1178v = 1G

X = round(xG*6)+84;   //take the G value from VP and multiply by 6 and use the internal round function to get to a whole number for X Axis
Y = round(yG*6)+84;   //take the G value from VP and multiply by 6 and use the internal round function to get to a whole number for Y Axis

analogWrite (VAC_IMP_X, X); //Impact on X Axis is written to the PWM output effectively changing the Duty Cycle
analogWrite (VAC_IMP_Y, Y); //Impact on Y Axis is written to the PWM output effectively changing the Duty Cycle

if(xD<yD){       //Compare duration of the X and Y Impacts, then set them back to zero G in the proper order
  delay(xD); //This delay is acceptable as it will always be <1000
  analogWrite(VAC_IMP_X, 84); //Adjust PWM to equate 0G
  delay(yD-xD); //This delay is acceptable as it will always be <1000
  analogWrite(VAC_IMP_Y, 84); //Adjust PWM to equate 0G
  }
else if(xD>yD){
  delay(yD); //This delay is acceptable as it will always be <1000
  analogWrite(VAC_IMP_Y, 84); //Adjust PWM to equate 0G
  delay(xD-yD); //This delay is acceptable as it will always be <1000
  analogWrite(VAC_IMP_X, 84); //Adjust PWM to equate 0G
  }
  //UPDATE PROGRESS BAR
    Serial.print("caltime.val=" + String(cP)); //Create String to send via serial to display to update the progress bar embedded in display
    Serial.write(0xff); //Must send these 3 bits to indicate end of command
    Serial.write(0xff);
    Serial.write(0xff);

    cP = cP + 4;     //Update progress bar by 4%

    delay(Gap);     //Time until the next Impact will occur – This is what I need to adjust from the “delay()” function to the “millis()” function
}


/* IMPACT TRAINING MODE FUNCTION */ 

void trainingMode (String data_from_display){
  if(data_from_display == "CAL START"){
  delay(250);
  Serial.println("Impact Calibration Initiated");
  delay(250);
  impactNow(0.6,6,0,0,20000);
  impactNow(0.6,2,0.6,16,2000);
  impactNow(0.6,22,0.6,12,2000);
  impactNow(1,251,0.6,49,212000);      
  impactNow(0.7,252,0,0,102000);       
  impactNow(0.7,17,0.6,7,192000);       
  impactNow(0.8,40,0.6,13,10000);
  impactNow(1,101,0.6,16,38000);
  impactNow(0,35,0.8,38,20000);
  impactNow(0,1,0.6,8,8000);
  impactNow(1.3,114,0,3,96000);       
  impactNow(0.8,24,0.6,25,14000);
  impactNow(0,0,0.6,18,30000);
  impactNow(0.8,16,1.1,10,10000);
  impactNow(1,28,0,0,56000);
  impactNow(0,0,0.7,20,90000);       
  impactNow(1.1,76,0.7,39,110000);       
  impactNow(1,109,0,0,12000);
  impactNow(1.2,78,0.8,20,88000);       
  impactNow(1.8,25,0.7,15,72000);       
  impactNow(1,174,0,0,12000);
  impactNow(1,62,0.7,1,42000);
  impactNow(1.6,13,0.8,19,208000);       
  impactNow(0.8,5,0.6,8,8000);
  impactNow(1,16,0.8,23,0);
  //delay(500);                //I will most likely remove this delay permanently
  //Serial.println("Impact Initialization Complete"); //Debugging Only
 
  }
  else if(data_from_display == "CAL STOP"){ //Not Working due to blocking code. “delay()” function
    Serial.print("caltime.val=" + String (0));
  }
}

So, as you can see, I've hit that wall of "blocking code." Fortunately, I wrote a function that should make it easy enough to only have to adjust the impactNow() Function.

I want to use millis() in this function rather than delay and then use the Gap integer to calculate the millis for the next impact to come. However, I need to add the times together so that the condition is not prematurely true. The time between impacts varies, and must, per the logic in our device. It knows that purely static data is not legit.

Using something like

if(currentTime - previousTime >= eventInterval){
//do stuff here
}

Seems like it will not work as I need it to. Because I somehow need to adjust the "Gap" integer automatically. Otherwise the impacts will be generated out of sync. As you can see above some are 2 seconds apart, some are several minutes apart. I adjusted my "real world data" in a way that I have an incrementing number that is a bit more static but still the same distance apart from eachother:

  impactNow(0.6,6,0,0,0);
  impactNow(0.6,2,0.6,16,20000);
  impactNow(0.6,22,0.6,12,22000);
  impactNow(1,251,0.6,49,24000);       
  impactNow(0.7,252,0,0,236000);       
  impactNow(0.7,17,0.6,7,338000);       
  impactNow(0.8,40,0.6,13,530000);
  impactNow(1,101,0.6,16,540000);
  impactNow(0,35,0.8,38,578000);
  impactNow(0,1,0.6,8,598000);
  impactNow(1.3,114,0,3,606000);       
  impactNow(0.8,24,0.6,25,702000);
  impactNow(0,0,0.6,18,716000);
  impactNow(0.8,16,1.1,10,746000);
  impactNow(1,28,0,0,756000);
  impactNow(0,0,0.7,20,812000);
  impactNow(1.1,76,0.7,39,902000);
  impactNow(1,109,0,0,1012000);
  impactNow(1.2,78,0.8,20,1024000);
  impactNow(1.8,25,0.7,15,1112000);
  impactNow(1,174,0,0,1184000);
  impactNow(1,62,0.7,1,1196000);
  impactNow(1.6,13,0.8,19,1238000);
  impactNow(0.8,5,0.6,8,1246000);
  impactNow(1,16,0.8,23,1446000);

The issue I am now running in to is == vs => as I think ==> will cause all the previous impacts to also fire as it cycles through the impacts as those will become true again. I need to go through 1 impact at a time, but also be able to have time to listen on the serial port in order to break out of the function (will write that after the timing.

I am sure this has got to be something simple I am overlooking or not understanding. I have looked around and although I still have a few ideas in my brain, I am exhausted and opted to reaching out for help rather than pounding my head against the keyboard lol.

If you need further explanation or detail, please let me know and I am more than happy to provide it.

Disclaimer - I am sure there is a lot of clean up to go through, so please be nice lol.

Using ‘String’ in sketches is not recommended as memory problems may occur, use char array strings instead.

Always reduce the sketch size to that which still exhibits any problems.

CTRL T or CMD T can help format your code.

Do you know what ‘State Machine Programming’ is all about ?

I would define a struct to hold the parameters that you pass to the impactNow function and declare an array of them, initializing it with the data you now pass to the function with all those calls.

Then keep a global to tell you which element of the array you have got to. Have trainingMode use millis to decide when it's time to move on to the next one. If it isn't, trainingMode will return to loop so it can read the serial port.

My attention span is shorter than your three posts together :cry:

You need new code to read the Serial input. I prefer processing a single character per loop() iteration, and put that character in a buffer. When the end of a line is received, then process the received command.
You need to think about how the sketch knows the end of a line. With a LineFeed or CarriageReturn at the end or both or a timeout or any combination of all of them.

When we say "use millis() instead of delay()", that means you have to rewrite the sketch. Sometimes the sketch gets two or three times bigger.

The interval can be taken from a table, as in my example: millis_rhythm.ino.

If you need a certain specific sequence with actions, you can put all of that in a table. The table would be some kind of timing-and-action-list. A "table" is an array and you probably need a struct, so this is the same thing as what wildbill writes about.

It is also possible to put the specific sequence in code with a Finite State Machine. That is a fancy name for something that is basically something simple: The Finite State Machine | Majenko Technologies. I don't know if your code needs one Finite State Machine or many.

Hello Larryd.

larryd:
Using ‘String’ in sketches is not recommended as memory problems may occur, use char array strings instead.

This may be something to circle back on. Currently the data is being sent properly to the Nextion display and all functions are working. I will definitely keep this in mind moving forward and appreciate the feedback.

larryd:
Always reduce the sketch size to that which still exhibits any problems.

CTRL T or CMD T can help format your code.

Thanks for the CTRL T pointer - I knew that had to be in there somewhere, similar to VisualStudio. Most was indented manually - time is of the essence so I have not bothered with some of the handy shortcuts like this... however, now that I know its there, it will be like hitting CTRL S haha.

larryd:
Do you know what ‘State Machine Programming’ is all about ?

Hypothetically...yes? I understand the idea behind it but have no experience with execution. I guess a better way to answer is yes, but do not fully know "how".

The only area I am having an issue with is replacing the delay with a millis function, unless there is an even better way that I am not aware of. In essense. Currently that gap is part of the values.

Impact (unique values for Impact)
Time Gap
Impact (unique values for Impact)
Time Gap
Impact (unique values for Impact)
Time Gap

Using millis seems to make sense, I am just hung up on the "how" of making sure that the condition of millis progresses sequentially yet still does not keep me stuck with the delay. (Hopefully I explained that correctly)

Thank you everyone for your feedback. It really helped get the wheels turning in my mind and gave me some direction.

wildbill:
I would define a struct to hold the parameters that you pass to the impactNow function and declare an array of them, initializing it with the data you now pass to the function with all those calls.

Then keep a global to tell you which element of the array you have got to. Have trainingMode use millis to decide when it's time to move on to the next one. If it isn't, trainingMode will return to loop so it can read the serial port.

I did get this working, and have rewritten a lot of my code, fortunately reducing it quite a bit. I have used 2 arrays, 1 is a single dimension and the other is multi-dimensional. I separated out the values for the "gap" and put them in their own array as it was just easier to tweak them that way rather than line by line of the other array with the remaining 4 values.

I changed my if else if statements to a switch case as well.

Most of the strings that were present were strictly for troubleshooting, but I also removed the few that were part of the functional code.

/* PINS */
#define VAC_POS 12          //red positive 12v
#define VAC_SEAT 11         //black common negative
#define VAC_IGN 10          //orange seatswitch output
#define VAC_MOT 9           //brown ignition output
#define VAC_IMP_P 8         //blue motion output
#define VAC_IMP_Y 7         //green impact Y axis
#define VAC_IMP_X 6         //white impact X axis

#define CAL_INDICATOR 5     //calibration start/stop
#define CAL_FOCUS 4         //calibration focus 

/* VARIABLES */
int count;                                      //Count impact cycles
byte cal = 0;                                 //Record the state of calibration
float xG = 84;                               //Defines the float for the X Axis impact Force
int xD;                                         //Defines the integer fo rht X Axis impact duration
float yG = 84;                              //Defines the float for the Y Axis impact Force
int yD;                                        //Defines the integer for the Y Axis impact duration
int long Gap ;                              //Defines the integer for the Gap between impacts
int cP = 0;                                  //Defines the integer for updating the progress bar on display
int X = 0;                                   //Defines the integer for the X Axis Algorithm
int Y = 0;                                   //Defines the interger for the Y Axis Algorithm
unsigned long currentTime = 0;
unsigned long previousTime = 0;      //Update and store Previous Time
unsigned long minWait = 0;             //Minimum Wait time for next impact
unsigned long maxWait = 0;            //Maximum Wait time for next impact
//byte VAC_IMP_X = 201;
//byte VAC_IMP_Y = 202;
int switches;                       //Defines the integer for the Switch Cases

/* ARRAYS */
float impWait[25] = {               //Impact Delays
  0, 1000, 1000, 2000, 1000, 1000, 2000, 2000, 2000, 1000, 1000, 1000, 5000,
  1000, 1000, 2000, 1000, 1000, 2000, 2000, 2000, 1000, 1000, 1000, 5000,
};

float impParam [25][5] = {        //Impact Parameters
  {0.6, 6, 0, 0, 20000},
  {0.6, 2, 0.6, 16, 2000},
  {0.6, 22, 0.6, 12, 2000},
  {1, 251, 0.6, 49, 212000},
  {0.7, 252, 0, 0, 102000},
  {0.7, 17, 0.6, 7, 192000},
  {0.8, 40, 0.6, 13, 10000},
  {1, 101, 0.6, 16, 38000},
  {0, 35, 0.8, 38, 20000},
  {0, 1, 0.6, 8, 8000},
  {1.3, 114, 0, 3, 96000},
  {0.8, 24, 0.6, 25, 14000},
  {0, 0, 0.6, 18, 30000},
  {0.8, 16, 1.1, 10, 10000},
  {1, 28, 0, 0, 56000},
  {0, 0, 0.7, 20, 90000},
  {1.1, 76, 0.7, 39, 110000},
  {1, 109, 0, 0, 12000},
  {1.2, 78, 0.8, 20, 88000},
  {1.8, 25, 0.7, 15, 72000},
  {1, 174, 0, 0, 12000},
  {1, 62, 0.7, 1, 42000},
  {1.6, 13, 0.8, 19, 8000},
  {0.8, 5, 0.6, 8, 200000},
  {1, 16, 0.8, 23, 0},
};

void setup() {
  Serial.begin(115200);
  Serial.println("Ready to begin serial communication");

  pinMode(VAC_POS, OUTPUT);
  pinMode(VAC_SEAT, OUTPUT);
  pinMode(VAC_IGN, OUTPUT);
  pinMode(VAC_MOT, OUTPUT);
  pinMode(VAC_IMP_P, INPUT);
  pinMode(VAC_IMP_Y, OUTPUT);
  pinMode(VAC_IMP_X, OUTPUT);
  pinMode(CAL_INDICATOR, OUTPUT);
  pinMode(CAL_FOCUS, OUTPUT);
}

void loop() {
  if (Serial.available()) {                       //Check to see if Serial command is available
    int data_from_display = 0;
    delay(30);                                    //30ms delay to catch entire string without double touch
    while (Serial.available()) {
      data_from_display += (Serial.parseInt());   //Collect data and append to itself
    }
    switches = data_from_display;
    controls();
  }
  while (cal == 1 && count < 25) {
    hitme(impParam[count][0], impParam[count][1], impParam[count][2], impParam[count][3], impWait[count]);
    break;
  }

}

Controls Code:

/* Switch Cases */
void controls() {
  switch (switches) {
    case 10:
      Serial.println("Power On");
      break;
    case 11:
      Serial.println("Power Off");
      break;
    case 20:
      Serial.println("SeatSwitch On");
      break;
    case 21:
      Serial.println("SeatSwitch Off");
      break;
    case 30:
      Serial.println("Engine On");
      break;
    case 31:
      Serial.println("VAC ONE: Electric        VAC TWO: Electric");
      break;
    case 32:
      Serial.println("VAC ONE: Electric        VAC TWO: Gas");
      break;
    case 33:
      Serial.println("VAC ONE: Gas        VAC TWO: Electric");
      break;
    case 34:
      Serial.println("VAC ONE: Gas       VAC TWO: Gas");
      break;
    case 35:
      Serial.println("VAC ONE: Shutdown       VAC TWO: Shutdown");
      break;
    case 40:
      Serial.println("Motion On");
      break;
    case 41:
      Serial.println("Motion Off");
      break;
    case 50:
      Serial.println("Calibration On");
      cal = 1;
      break;
    case 51:
      Serial.println("Calibration Off");
      cal = 0;
      count =  0;
      Serial.println("Count has been reset!    " + String(count));
      break;
  }
}

Impacts Code:

void hitme(float xG , int xD , float yG , int yD , int long Gap ) {
  currentTime = millis();
  minWait = currentTime - previousTime;
  maxWait = Gap + 10;

  /*Start Math*/
  X = round(xG * 6) + 84; //take the G value from VP and multiply by 6 and use the internal round function to get to a whole number for X Axis
  Y = round(yG * 6) + 84; //take the G value from VP and multiply by 6 and use the internal round function to get to a whole number for Y Axis

  if (minWait > Gap ) {
    if (minWait < maxWait) {
      analogWrite (VAC_IMP_X, X); //Impact on X Axis is written to the PWM 
      analogWrite (VAC_IMP_Y, Y); //Impact on Y Axis is written to the PWM 
      if (xD < yD) {                      //Compare duration , then set them back to zero G in the proper order
        delay(xD);
        analogWrite(VAC_IMP_X, 84);
        delay(yD - xD);
        analogWrite(VAC_IMP_Y, 84);
      }
      else if (xD > yD) {
        delay(yD);
        analogWrite(VAC_IMP_Y, 84);
        delay(xD - yD);
        analogWrite(VAC_IMP_X, 84);

      }
       /*Update Progress Bar */
       Serial.print("caltime.val=" + String(cP));
        Serial.write(0xff);
        Serial.write(0xff);
        Serial.write(0xff);

      cP = cP + 4;    //Update progress bar by 4%
    }
    previousTime = currentTime;
    count++;

  }

}

You sketch is still based on delay() and not on millis(). When using millis() you have to start all over and make a completely different sketch.

didnt have time you read through your whole project but i can answer the headline. it's good that you put effort into your question but it's a little too much info and difficult to narrow down exactly what question you are asking. first of all if you where using delay() for timing before you will probobly need to rewrite your entire sketch from the start. once you start using millis() there is much more complex things you can do with timing. However, as the door opens there are many different methods and coding techniques that you can use depending on your needs. obviosly button presses need to be outside the millis() timer brackets. then probobly sent to functions.

dont forget...

you can use more than one millis() timer.
a millis() timer be used to trigger something just once.
you can put counter varibles inside a millis() timer to trigger things at those numbers.
you can change the speed of a millis() timer at any time.

if you are changing over to millis() in your sketch and are stuck on a timing issue please specify where you are stuck and we can help.

And remember there is micros()

If you're interested in a simpler interface that also handles millis/micros overflow, you might find this useful.