Timing with millis() and delay()

I am using the Arduino Uno to observe the duration of a voltage change. I am sampling the voltage every 10 milliseconds by using delay(10) and I am getting duration by using millis() to obtain the time at voltage changes. With this, I expect to see durations in increments of 10 milliseconds, but I am getting values in between. When the duration of the voltage change is small (<100), I am getting values like 31, 41, 51, and rarely values like 29 and 49 or 62 and 72. When the duration is several hundreds of milliseconds, the values can have anything in the ones place.

Could this be due to the fact that the code is taking some time to run or rounding errors? or is there something with the millis() or delay() functions that I am missing?

P.S. don't bother telling me to change my code; I'm just curious as to why I'm getting these intermediate values ( sorry, a lot of people on here just criticize and don't help :confused: )

Here's my code (I am running arduino output 10 through a voltage splitter which comes back to A0)

int touchCountA = 0;
int touchCountB = 0;
#include <SPI.h>
#include <SD.h>
File myFile;

void setup() {
  // Set A0 as analog input
  pinMode(A0, INPUT);
  // Set 10 as digital output for SD reader
  pinMode(10, OUTPUT);
  // Starts the SD reader from pin 4
  SD.begin(4);
  // Starts Serial communications(with computer if connected)
  Serial.begin(9600);
  //Open(or create if non-existent) dat.txt
  myFile = SD.open("dat.txt", FILE_WRITE);
  // Prints this line whenever power is turned on
  myFile.println("TchA,TchB,Time,Dur");
  myFile.close();
  //for LEDs
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
}

void loop() {
  int voltage = analogRead(A0);
    // Serial.println(voltage);
  // Voltage splits if Lick Spout A
  if(voltage >= 300 && voltage <= 380){
    long touchTime = millis();
    while(voltage >= 300 && voltage <= 380){
      digitalWrite(7, HIGH);
      delay(10);
      voltage = analogRead(A0);
      // Serial.println(voltage);
    }
    digitalWrite(7, LOW);
    touchCountA++;
    long finalTime = millis();
    myFile = SD.open("dat.txt", FILE_WRITE);
    myFile.print(touchCountA);
    myFile.print(",0,");
    myFile.print(touchTime);
    myFile.print(",");
    myFile.println(finalTime - touchTime);
    myFile.close();
    Serial.print(touchCountA);
    Serial.print(",0,");
    Serial.print(touchTime);
    Serial.print(",");
    Serial.println(finalTime - touchTime);
  }
  // Full 3.3V read when Lick Spout B
  if(voltage >= 666){
    long touchTime = millis();
    while(voltage >= 666){
      digitalWrite(8, HIGH);
      delay(10);
      voltage = analogRead(A0);
      // Serial.println(voltage);
    }
    digitalWrite(8, LOW);
    long finalTime = millis();
    touchCountB++;
    myFile = SD.open("dat.txt", FILE_WRITE);
    myFile.print("0,");
    myFile.print(touchCountB);
    myFile.print(",");
    myFile.print(touchTime);
    myFile.print(",");
    myFile.println(finalTime - touchTime);
    myFile.close();
    Serial.print("0,");
    Serial.print(touchCountB);
    Serial.print(",");
    Serial.print(touchTime);
    Serial.print(",");
    Serial.println(finalTime - touchTime);
  }
  delay(10);
}

P.S. don't bother telling me to change my code

:zipper_mouth_face:

delay(10) does delay pretty much exactly 10 milliseconds. But then you go and do other stuff like analogRead() and incrementing your loop counter. Your loop takes longer than 10ms to run.

Then there's other stuff going on. There's interrupts continuously running in the background, keeping the milliseconds up to date. If one of them hits during the non-delay part of your code, then that one loop will take longer. Usually it's insignificant but the microseconds do add up. Your code has no way of getting back on track from this general drift.

Obviously you're happy with the performance of the code, so there's no reason to change. Personally, I'd remove the delays entirely. It won't make any difference except your timing will be more accurate without 10ms steps.

tonyu22:
Could this be due to the fact that the code is taking some time to run or rounding errors?

Yes to both.

millis() is not perfect. In particular, sometimes it jumps by 2 instead of 1.

For very short intervals, micros() might be a better choice. micros() isn't perfect either: if I remember correctly, it jumps by 4 microseconds at a time, but since there are 1000 microseconds in 1 millisecond, that isn't much of a problem.

Obviously you're happy with the performance of the code, so there's no reason to change. Personally, I'd remove the delays entirely. It won't make any difference except your timing will be more accurate without 10ms steps.

I'm with MorganS here. I don't see the advantage of quantizing your voltage durations into 10ms chunks. The While loop timers will work perfectly correctly to pick up a start and finish with out using delay.

I'm not asking you to change your code, I'm asking you to explain it. I have uses While loop timers in my code, but usually for digital signals and not with analogRead().

Is it for some sort of hysteresis control/debounce at the boundaries?

You need to change your code :stuck_out_tongue: (someone has to say that)

  • Always use "unsigned long" variables when using millis(), or else you have a rollover problem.
  • The includes normally go first and after that the variables are declared.
  • You don't have to set A0 to INPUT with pinMode(), when using analogRead().
  • What is connected to your Arduino board ? Only a SD card or also a Ethernet ? I hope only a SD card or else the SPI bus will go wrong.
  • Do you measure the voltage of an output pin ? That is the same as the 5V pin of the Arduino (or 3.3V for a 3.3V Arduino board). Did you know that an Arduino can read its own VCC internally without using any pins ? That is possible by setting the internal mux against the internal reference.

If you use millis() for timing the way it is used in Several Things at a Time (rather than using delay() ) it will take account of other activity and give you consistent timing.

...R

  // Set A0 as analog input
  pinMode(A0, INPUT);

Actually, you're setting it as a digital input.
But don't worry, analogRead will set it back to analogue before you use it.

P.S. don't bother telling me to change my code;

D'oh!

a lot of people on here just criticize and don't help

If people don't point out problems, how are you going to learn?

AWOL:
Actually, you're setting it as a digital input.
... analogRead will set it back to analogue ...

I can't see anything in the datasheet that says that there's any difference between the setup requirements for an analog input versus a digital input. I don't see anything in analogRead() that changes the pin setup - it selects the analog reference and the mux channel, and then starts a conversion.

A quick test suggests that analogRead() doesn't change anything other than the mux channel. Here's the code:

void setup() {
  Serial.begin(115200);
  Serial.println("OK");
  Serial.println();

  Serial.print("On startup: ");
  showA0Mode();
  showA0Reading();
  Serial.print("After analogRead(): ");
  showA0Mode();
  Serial.println();
  
  Serial.print("Configure as INPUT: ");
  pinMode(A0, INPUT);
  showA0Mode();
  showA0Reading();
  Serial.print("After analogRead(): ");
  showA0Mode();
  Serial.println();

  Serial.print("Configure as INPUT_PULLUP: ");
  pinMode(A0, INPUT_PULLUP);
  showA0Mode();
  showA0Reading();
  Serial.print("After analogRead(): ");
  showA0Mode();
  Serial.println();

  Serial.print("Configure as OUTPUT, LOW: ");
  pinMode(A0, OUTPUT);
  digitalWrite(A0,LOW);
  showA0Mode();
  showA0Reading();
  Serial.print("After analogRead(): ");
  showA0Mode();
  Serial.println();

  Serial.print("Configure as OUTPUT, HIGH: ");
  pinMode(A0, OUTPUT);
  digitalWrite(A0,HIGH);
  showA0Mode();
  showA0Reading();
  Serial.print("After analogRead(): ");
  showA0Mode();
  Serial.println();
}

void loop() {}

void showA0Mode() {
  Serial.print("A0 Mode = ");
  if (DDRC & 1) {
    Serial.print("OUTPUT, ");
    if (PORTC & 1) {
      Serial.print("HIGH");
    }
    else {
      Serial.print("LOW");
    }
  }
  else {
    Serial.print("INPUT");
    if (PORTC & 1) {
      Serial.print("_PULLUP");
    }
  }
  Serial.println();
}

void showA0Reading() {
  Serial.print("analogRead(A0) = ");
  Serial.println(analogRead(A0));
}

and here's the output:

OK

On startup: A0 Mode = INPUT
analogRead(A0) = 791
After analogRead(): A0 Mode = INPUT

Configure as INPUT: A0 Mode = INPUT
analogRead(A0) = 865
After analogRead(): A0 Mode = INPUT

Configure as INPUT_PULLUP: A0 Mode = INPUT_PULLUP
analogRead(A0) = 1010
After analogRead(): A0 Mode = INPUT_PULLUP

Configure as OUTPUT, LOW: A0 Mode = OUTPUT, LOW
analogRead(A0) = 0
After analogRead(): A0 Mode = OUTPUT, LOW

Configure as OUTPUT, HIGH: A0 Mode = OUTPUT, HIGH
analogRead(A0) = 1023
After analogRead(): A0 Mode = OUTPUT, HIGH

It looks like analogRead() doesn't change the pin setup, and reads the analog voltage correctly even when the pin is configured as an output. The only quirky thing looks to be the analog reading when the pin is configured as INPUT_PULLUP, likely because the resistance of the internal pullup is a lot higher than the recommended maximum of 10K.

When I want to use an analog pin as either an analog input or a digital input, I leave it as I found it on startup, or set it to input with pinMode(). Am I missing something?

tmd3, you are right. At startup the pin is analog input and digital input. Both on the same pin.
In some ATmega and ATtiny chips the digital part can be turned off, to improve the analog accuracy. I have used that in the past.

The internal pullup resistor is 50k. If you would do a dummy read and a delay, it will probably reach the 1023.

Only when the pin must be an output, then the register has to be set for output. The analog input will still be there on that pin.

I remember something with the reference. I might be wrong, but I think the Arduino library sets the reference during analogRead(), and not during analogReference().

Koepel:
... the Arduino library sets the reference during analogRead(), and not during analogReference().

Indeed. Here's the entire analogReference() function, from wiring_analog.c:

uint8_t analog_reference = DEFAULT;

void analogReference(uint8_t mode)
{
 // can't actually set the register here because the default setting
 // will connect AVCC and the AREF pin, which would cause a short if
 // there's something connected to AREF.
 analog_reference = mode;
}

All it does is set the value of variable analogReference. It doesn't change anything in any of the peripherals.

After reset, the analog voltage reference selection bits REFS1:0 are both zero, selecting no reference at all, and turning off the internal reference voltage sources. After initialization, the value of variable analogReference is set to DEFAULT, which is defined in Arduino.h as 1 for the Uno. analogRead() contains this line for the Uno:

ADMUX = (analog_reference << 6) | (pin & 0x07);

That sets bits REFS1:0 in ADMUX to the value of variable analogReference: 1, if it hasn't been changed by a call to analogReference(), or the value that analogReference() gave it, otherwise.

In summary, function analogReference() sets the value of variable analogReference, but doesn't change the operation of any peripheral. Function analogRead() writes that value to the ADC's ADMUX register. For an internal reference voltage, with REFS0 == 1, that connects the selected voltage to the analog reference pin; for an external reference, with REFS0 == 0, it doesn't connect anything to the pin.