[Solved] When does millis() actually initialize and begin counting?

Good morning folks :slight_smile: ,

I'm not entirely sure if this is a hardware question or a software question, but here's the scenario:

What I'm trying to find out:
When does millis() actually start counting? Everywhere I read, people say it begins as soon as the device is powered on, but I find that answer to be a little non-technical. Is it a function of hardware, and that is why it begins as soon as power is applied? Or is it a function of software, and it's actually not occurring the instant power-on begins, but rather a short time later?

Why I'm trying to find out:
For a simple project where two arduino devices (separately and remotely with the same sketch) don't begin until a user presses a button, I'm considering using "randomSeed(millis());" to reset my RNG for the sketch at a point after manual user-interaction in loop(). I'm not super critical about this being non-deterministic. I'm fine with pseudo-random. The variation that it takes either user to manually press a button in milliseconds is fine, but this is what led to my curiosity about how millis() actually works.

If anyone has insight on the intricacies of millis(), I'd very much appreciate your two cents to add to my piggy bank! :smiley:

I don't know.

However, I do know that millis is updated by an interrupt because it ceases to increment when you are running an ISR. I believe that millis is incremented by one of the timer interrupts and I suspect that that is probably setup in the init function called from the hidden main function that the IDE provides for you.

You could dig into the init code and find out.

See: wiring.c in the Arduino core. Timer0 is started in the init() function that is called before your setup() function.

Valuable information from both of you, thank you very much!
@johnwasser has the exact answer to my question :smiley:
@wildbill has pointed out relevant information (attachInterrupt() reference) that affects millis() functionality.

I appreciate your responses, thank you again! :slight_smile:

Your goal is to get a different random seed ?

An external event (pressing a button) is a good way. Once the random() is running with a good seed, there is no need to set the seed once more. So you only need to start with a seed once. You could also combine millis() with micros().
The random() function is fast and pretty good. Even for complex math problems, it is okay.

We use the analog inputs and set the seed in the setup() function. It is possible that they all are stuck to 0V or 5V (or 3.3V), but almost in every situation at least one of them has some noise.
Just one analog input with a LDR with a resistor might be good enough.

To be sure, all the analog inputs can be combined.
In my millis_reaction_timer.ino I have this code:

void setup()
{
  // Use the noise from the analog inputs,
  // to create a different random sequence each
  // time the Arduino is started.
  // The randomSeed() function takes a 32 bit unsigned long parameter.
  // By shifting each value from analogRead() some to the left, 
  // the analog values are spread over the 32-bits, 
  // for (in theory) a little more random.
  unsigned long noise = 0;
  for( int i = A0; i <= A7; i++)  // select the analog pins that your board supports
  {
    noise += analogRead( i);
    noise <<= 2;               // for a little more random
  }
  randomSeed( noise);
}

My previous line of thought was that I would be calling randomSeed() once per "session" (roughly once per hour) after the user has finished configuring several settings and accepts them. However, upon reflection on your post I suppose it makes more sense to put the actions that set those settings into a function and call that function in setup, and then call randomSeed() from setup immediately after. That way, during runtime I can call the settings function separately and not have to call randomSeed() again, as it is nested in setup and not the settings function.

I like your idea about using micros() as well! Although, I only see value in using it as a replacement for millis() in this case rather than a supplement. If the timer for micros() and millis() begins at the same time, then the value for millis() is predictably the value of micros() rounded and divided by 1000, so any math that relies solely on the two will have the same actual utilized seed range in a given time frame as simply using micros() alone. If I have my head on straight here, haha. If my code is small enough to fit on a nano, then randomSeed(micros()) will give me 250x the expected seed range as if I had simply used randomSeed(millis()), so that's what I shall do :slight_smile:

tl;dr: randomSeed(micros()) after user input. :thumbsup:

BMooney:
then the value for millis() is predictably the value of micros() rounded and divided by 1000

It is not that abstract. That is why I used the word "combine". You can 'add' them or 'xor' them, or whatever. Any combination is better than no combination.
The micros() increments with 4µs on a Arduino Nano. That means you lost two bits of random :o

This sketch tests micros() versus millis(). I even turned off the interrupts while retrieving both values.

// Arduino IDE 1.8.13 with Arduino Uno

void setup()
{
  Serial.begin( 9600);
  Serial.println();
  Serial.println( "micros() vs millis()");
}

void loop()
{
  noInterrupts();
  unsigned long micros32 = micros();
  unsigned long millis32 = millis();
  interrupts();

  // Calculate the difference.
  // They are both unsigned 32-bit, but millis needs to be multiplied
  // by 1000 and then the difference has to be calculated.
  // I think this is only possible by converting them to signed 64 bit.
  // The difference is converted to signed long, because then Serial.println()
  // can print it.
  int64_t micros64 = (int64_t) micros32;
  int64_t millis64 = (int64_t) millis32 * 1000LL;
  int64_t diff64 = micros64 - millis64;
  long diff32 = (long) diff64;
  Serial.println( diff32);
  delay( 200);
}

This is the result:

micros() vs millis()
184
1364
588
772
948
1124
344
1524
748
932
1112
1332
556
1736
960
1144
364
1540
760
944
1124
1344
572
1748
976
1156
1384
1608
836
1012
1240
1460
688
864
1040
260
1436
664
840
20
1152
1376
600
780
964
1140
1368
588
772
948
1124
344
1524

Someone will someday press the button while the Arduino starts.
The more possible random you put in it, the higher the chance that it will be random.

Startup: Use analog inputs. The more analog inputs are combined, the better.
Button press by user: Combine everything with the motto: "if you can do it, you can overdo it".

// When the button has been pressed for the first time.

unsigned long ran = random();   // use the random from startup
ran += millis();    // should be enough
ran += micros();    // let's overdo it
ran += analogRead( A0);  // why not
randomSeed( ran);

There is more you can do. You could store the random number once per hour to EEPROM. At startup you can read that and have a flying start with random.
There is also an extra analog channel that you didn't think about. The Arduino ADC can measure the internal voltage reference versus the VCC voltage. That is almost never exactly the same due to noise and temperature differences.

And this is not even about real random, this is only about the seed to start a sequence :wink:

Very comprehensive and well explained @Koepel!

I must point out, however, that your subtraction test didn't quite fully debunk my determination that millis() is still predictable based on the value of micros(), but it certainly has put me on a tangent and shown me the value of using both millis() and micros(). Subtracting as you have, I would have expected to simply see the incrementing micros() itself, but as a number less than 1000 (or rather, I expected it to be constantly less than 1000...).

Of course, because it manages to exceed 1000 at all does prove that there is a variable in the device's actual functioning speed, which indeed adds randomness (min value in the list is 20, which implies that it can perform the incrementation to millis quite quickly after micros [or vice versa], whereas the range of (1748-20)>>1000 heavily implies that the time it takes to increment millis after micros [or vice versa] is not constant).

I'm wondering if there are numbers nearing 2000 due to the fact that if millis and micros both use interrupts to increment, then they would be in fact halting the other's count during their increments, causing drift
If it were graphed over time. If the numbers trended to get larger, perhaps it would be attributed to having one of the functions taking longer to increment during the interrupt... however, even that would add to the randomness haha.

What a tangent my mind has gone down this morning :sweat_smile:

Writing to EEPROM for next startup using a random() value, I quite like that idea as well!

And gasp, that final thought that you put out sounds pretty good too for adding a bit more! I imagine that measuring the input voltage relative to AREF is a little more complicated than simply calling an analogRead, but inputs that vary based on other external factors such as temperature are excellent in my mind.

tl;dr: Thank you for the thoughts and performing that snippet comparing micros and millis, I've definitely learned more than I expected out of this topic! :smiley:

On an Arduino UNO, Timer0 is set for a prescale of 64 so it counts at 250 kHz (16 MHz / 64) or one count every 4 microseconds. It is an 8-bit counter so it overflows every 1024 microseconds. In the overflow interrupt, 1 is added to the milliseconds counter. The 'extra' 24 microseconds get added to a separate counter. When that 'extras' counter exceeds 1000 microseconds (every 41-2/3 interrupts) another millisecond is added to the milliseconds counter and 1000 microseconds are subtracted from the 'extras' counter. Millis() runs a little slow and then every 41 to 42 milliseconds, catches up by double-counting. Just before the double-count, it will be almost 1000 microseconds behind the micros() timer.

Actually, the 'extras' counter counts by 3's (24/8), and when the counter gets to 125 (1000/8) a millisecond is added to the milliseconds counter and 125 is subtracted from the 'extras' counter. This allows the 'extras' counter to be one byte long so it doesn't need to be fetched with interrupts disabled.

The micros() function uses the raw overflow counter multiplied by 256 plus the current timer count, all multiplied by 4 (the microseconds in a clock tick). It increases by 4 microseconds every clock tick.

All this mess was to allow Timer0 to run the full 256 counts so you could get 8-bit PWM from the two PWM pins controlled by Timer0. If you don't need 8-bit PWM you can set TOP to 249 and get exactly 1000 microseconds (1 millisecond) for each timer overflow. You could even run the timer at a prescale of 8 to get 1/2 microsecond resolution in micros() instead of 4-microsecond resolution.

Ahhhhh, thank you! That explains why the range exceeded 1000 so greatly! This would also mean that snippet alone introduces a tolerance to the accuracy of a call to millis() of +0,-1.

I suppose getting 8-bit PWM would be higher priority for the majority of users than the concern of the precise accuracy of millis() at any given moment.

I'm going to have to open up more of the base Arduino files to fully understand, I see calls to functions that don't exist in wiring.c (ex.: micros() calls function clockCyclesPerMicrosecond(), which while it is named quite obviously, the function itself is not found in wiring.c, and preprocessor directives that don't originate from wiring.c either), so at a time when I can go down the rabbit hole I will have to dive in and do a bit of reading so I don't say too many dumb things on the forums :slight_smile:

BMooney:
I see calls to functions that don't exist in wiring.c (ex.: micros() calls function clockCyclesPerMicrosecond(), which while it is named quite obviously, the function itself is not found in wiring.c,

That is a macro on line 104 of Arduino.h:

#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )