0

I would like to set a stopwatch timer that will determine how long an input is in a certain state before changing. I want to set it so that, depending on the output my code executes one of 2 switch cases. But my trouble comes in on setting a timer. Is there a function I could use? Or a method that somebody knows? The time that the input will be for each case is not fixed so I cannot use a delay.

1
  • 3
    As I suggested in the comments to your other question, you can also use millis() here to measure time differences (use micros() if the time to measure is smaller than a few milliseconds).
    – chrisl
    Jan 20 '20 at 12:10
1

Even though you are not running an actual (complex) Operating System, you should adhere to common practices. For an Arduino, you should, in many cases, avoid directly controlling the hardware so as to be compatible with as many existing libraries for your particular Arduino platform as possible.

Setting the timer directly (if you are using an official Arduino Uno which contains an Atmel328P processor the processor's timers are covered in section 14 of the Atmel328P Specifications) may cause unexpected results should you use a library which expects the timer to run without being altered.

Instead, consider using the millis() function built into the Arduino IDE. The function returns the current number of milliseconds since the Arduino was powered up. Record this value in your code. Then, if you want to know if One Second has elapsed, get the new value of millis and subtract this saved value from it and see if it is greater than 1000. When that is true, One Second has elapsed.

5
  • 4
    Re “consider added the code to detect this roll over”: better to just write rollover-safe code, like if (millis() - start_time >= duration). “check to see if millis() returns a number which exceeds this value” is not rollover-safe. Jan 20 '20 at 14:54
  • True Edgar. @Damon Swart is brand new to the site and I like trying, testing & vetting 1 idea at a time (using millis()) - then, after the 1st idea is working, adding to it (roll-over). Also, I rather not include code. It takes longer to post answers w/code and the code may not be correct or complete. Besides, describing the solution makes the Original Poster think through the problem.
    – st2000
    Jan 21 '20 at 12:50
  • @Damon Swart, if you try the answer and it works for you, accept the answer as correct so that others can also find and use the answer. If it doesn't work for you, post a comment here and we can try to change the answer so that it does work for you.
    – st2000
    Jan 21 '20 at 12:53
  • This answer should be edited to do things the safe way. There’s no reason anyone should have to check for rollover if you write code correctly. Always see how long it has been. Never try to predict a future time.
    – Delta_G
    Jun 18 '20 at 16:29
  • Please see my proposed edit.
    – Delta_G
    Jun 18 '20 at 18:03
1

Your title is about “setting a timer”, but your actual question is about measuring the length of a pulse. There are two functions provided by the Arduino IDE for this purpose, pulseIn() and pulseInLong():

  • pulseIn() is based on a carefully timed delay loop. It has a resolution of the order of one microsecond, but will not count the time spent servicing interrupt requests. It works best for very short pulses timed with interrupts turned off.
  • pulseInLong() is based on micros(). It has a resolution of 4 µs and will not work properly if interrupts are turned off. It works best for longer pulses where its limited resolution and interrupt latency are tolerable.

Both of these are blocking functions: they completely block your sketch while they perform the measurement. If you don't want your sketch to be unresponsive during this time, you can write a non-blocking version of pulseInLong() using a finite-state machine like this:

// Measure the length of a pulse in a non-blocking manner.
// Returns 0 if no measurement is available at the time of the call.
void get_pulse_length() {
    static enum {
        INITIAL_WAIT,    // wait for the first (partial) pulse to end
        BETWEEN_PULSES,  // wait for the pulse to start
        WITHIN_PULSE     // wait for the pulse to end
    } pulse_state = INITIAL_WAIT;
    static uint32_t pulse_start;  // when the current pulse started

    uint8_t pin_state = digitalRead(pulse_pin);
    uint32_t now = micros();
    switch (pulse_state) {
        case INITIAL_WAIT:
            if (pin_state == LOW)
                pulse_state = BETWEEN_PULSES;
            break;
        case BETWEEN_PULSES:
            if (pin_state == HIGH) {
                pulse_start = now;
                pulse_state = WITHIN_PULSE;
            }
            break;
        case WITHIN_PULSE:
            if (pin_state == LOW) {
                pulse_state = BETWEEN_PULSES;
                return now - pulse_start;
            }
            break;
    }
    return 0;
}

Note that this measures high pulses. You will have to swap HIGH and LOW if you want to measure low pulses. You would use it like this:

void loop() {
    uint32_t pulse_length = get_pulse_length();
    if (pulse_length) {
        // handle the pulse
    }
}

The resolution of the measurement is the execution time of loop(), so you have to make sure there is nothing blocking there, and specially no calls to delay(). If you need a better resolution from a non-blocking method, you can use interrupts to trigger the measuring process:

volatile uint32_t pulse_start, pulse_length;
volatile bool pulse_valid;

void on_rise() {
    pulse_start = micros();
    attachInterrupt(digitalPinToInterrupt(pin), on_fall, FALLING);
}

void on_fall() {
    pulse_length = micros() - pulse_start;
    pulse_valid = true;
    attachInterrupt(digitalPinToInterrupt(pin), on_rise, RISING);
}

uint32_t get_pulse_length()
{
    if (!pulse_valid) return 0;
    noInterrupts();
    uint32_t pulse_length_copy = pulse_length;
    pulse_valid = false;
    interrupts();
    return pulse_length_copy;
}

void setup() {
    attachInterrupt(digitalPinToInterrupt(pin), on_rise, RISING);
}

This should give you the resolution of micros(), i.e. 4 µs, but occasionally you may get results that are slightly off if interrupts happen to be disabled when the input transitions. If this is unacceptable, the only other options I see is to use a hardware timer with input capture capability. You will have to look at the datasheet of your microcontroller to see how it works, and maybe do a Web search for “Arduino input capture”.

1

Use micros() (reference page) to obtain a microsecond timestamp, and millis() (reference page) to obtain a millisecond timestamp.

Full Disclosure: I link to a library I maintain in my answer below, and I mention a few products (no links) I created with various techniques presented here in order to act as representative examples of when one approach might be preferred over another.

Example 1: simple linear (synchronous) software timing measurement, including low-resolution (micros()) and high-resolution (timer2.get_count())

In the simplest sense, let's measure how long it takes to set an output pin HIGH and then LOW again:

void setup() 
{
    Serial.begin(115200);
    // I'll just leave pin 9 as an input; but uncomment the line below to make it an output
    // pinMode(9, OUTPUT);
}

void loop()
{
    // Measure and print how many microseconds it takes just to set an output pin HIGH and then
    // LOW again. 
    uint32_t time_start_us = micros();    // <=== START TIMESTAMP
    digitalWrite(9, HIGH);
    digitalWrite(9, LOW);
    uint32_t time_end_us = micros();      // <=== END TIMESTAMP
    uint32_t time_elapsed_us = time_end_us - time_start_us; 
    Serial.print("time_elapsed_us = ");
    Serial.println(time_elapsed_us);
    delay(100);
}

The really cool ShowInfo Arduino speed profiling sketch shows that Arduino's digitalWrite() function takes about 5us each, so expect the code above to print ~10us. Let's see if that's correct. I ran this on an Arduino Nano and got this output:

time_elapsed_us = 8
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 8
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 8
time_elapsed_us = 12
time_elapsed_us = 8
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 12
time_elapsed_us = 8

That's weird. Why only 8 or 12 us? Why not 10? Or 9? Or something else? Well, it turns out the Arduino micros() function only has a resolution of 4us, so it will actually print out either 8 or 12 us since those are multiples of 4 us. To get better resolution you'd have to modify the hardware timer registers, as I've done in my eRCaGuy_Timer2_Counter library, which has 0.5us-resolution. Full Disclosure: I wrote and maintain this library. It is free and open source, but I have it on my personal website, which has ads, and I solicit donations for downloads. A fully-functional snippet is also available in code at the bottom of that web-page without downloading anything.

Here is how to do the above code with my library:

#include <eRCaGuy_Timer2_Counter.h>

// Convert timer2 clock counts, which are 0.5us each, to us.
float counts_to_us(uint32_t time_counts)
{
    float time_us = (float)time_counts/2.0; 
    return time_us;
}

void setup() 
{
    Serial.begin(115200);
    // I'll just leave pin 9 as an input; but uncomment the line below to make it an output
    // pinMode(9, OUTPUT);

    // Configure Timer2. This MUST be done before the other Timer2_Counter functions work.
    // Note: since this messes up PWM outputs on pins 3 & 11, as well as interferes with the tone()
    // library (http: arduino.cc/en/reference/tone), you can always revert Timer2 back to normal by
    // calling `timer2.unsetup()`
    timer2.setup(); 
}

void loop()
{
    // Measure and print how many microseconds it takes just to set an output pin HIGH and then
    // LOW again. 
    uint32_t time_start_counts = timer2.get_count();    // <=== START TIMESTAMP
    digitalWrite(9, HIGH);
    digitalWrite(9, LOW);
    uint32_t time_end_counts = timer2.get_count();      // <=== END TIMESTAMP
    uint32_t time_elapsed_counts = time_end_counts - time_start_counts; 
    float time_elapsed_us = counts_to_us(time_elapsed_counts);
    Serial.print("time_elapsed_us = ");
    Serial.println(time_elapsed_us);
    delay(100);
}

Now look at the output. Here's the more accurate and precise results with my eRCaGuy_Timer2_Counter library. Much better! But why those spurious 14.50us values I have marked with <===? Why are they off by 4us? I will explain below.

time_elapsed_us = 10.00
time_elapsed_us = 10.50
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 14.50  <===
time_elapsed_us = 10.50
time_elapsed_us = 10.50
time_elapsed_us = 10.50
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 14.50  <===
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 10.50
time_elapsed_us = 10.50
time_elapsed_us = 10.50
time_elapsed_us = 10.50
time_elapsed_us = 10.00

The tradeoff of doing what Im doing is that you will get a 4us jitter introduced more often. Every time the 8-bit timer2 counter overflows, an ISR (Interrupt Service Routine) is called. This counts overflows to keep track of the 32-bit software timer from an 8-bit hardware counter. Entering this ISR takes about 4us, which means that if you try to grab a timestamp but then the ISR is called, you have to wait 4+us to get that timestamp, so it is off by that much. One of the several Arduino experts I really look up to, Nick Gammon, mentions this here in his Interrupts article where he says, "There is a tweaking figure of 4 µS...". So, this 8-bit counter counts at 1 tick per 0.5us, which means it rolls over every 256 ticks * 0.5us/tick = 128us. So, every 128us you will have at least a 4us delay error introduced if you try to call the timer2.get_count() exactly when the ISR is called. If you get really unlucky you may even get this effect twice and be off by as much as 8us. When using the standard micros() function, since it rolls over only every 256 ticks * 4us/tick = 1024us, you get this 4us error effect 8x less frequently. That's the tradeoff of getting better resolution: you also get more-frequent 4+us jitter.

And just for kicks, here's a really bad one. Notice the 20.50us value--off by 10.50us!

time_elapsed_us = 15.00 <===
time_elapsed_us = 10.50
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 15.00 <===
time_elapsed_us = 10.50
time_elapsed_us = 10.00
time_elapsed_us = 20.50 <======
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 10.50
time_elapsed_us = 10.00
time_elapsed_us = 10.50
time_elapsed_us = 10.50
time_elapsed_us = 10.00
time_elapsed_us = 10.00
time_elapsed_us = 10.00

Using mode filters, median filters, or other filters, these spurious results can be removed, at, of course, the cost of reduced frequency response to the thing being measured (all this means really is it takes multiple measurements to know the true value, just as we need to see with our eyes multiple measurements above to deduce 10.0us seems to be the right answer).

Example 2: non-blocking (asynchronous) timing measurement of external event

More-complicated Example: measure how long INPUT pin 9 is HIGH, and print out the HIGH time out each time it goes LOW again.

In general, use this approach for any and all input events you need to measure at the 100us to ~200us resolution or larger level. You can use this on every single pin at once and get good results with resolutions around that level, depending really just on how long your main loop takes to run each iteration.

constexpr uint8_t PIN = 9;

void setup()
{
    Serial.begin(115200);
    pinMode(PIN, INPUT);
}

void loop()
{
    // This will measure how long `SOME_PIN` is HIGH, in microseconds.

    static uint32_t time_start_us = micros();
    bool time_just_acquired = false; // true if a new time value was just measured
    uint32_t time_elapsed_us = 0;

    bool pin_state = digitalRead(PIN);
    static bool pin_state_old = LOW;
    
    if (pin_state == HIGH && pin_state_old == LOW)
    {
        // The pin just barely went HIGH, so "start the timer" by obtaining a timestamp of the 
        // start time
        time_start_us = micros();
        pin_state_old = pin_state; // update
    }
    else if (pin_state == LOW && pin_state_old == HIGH)
    {
        // The pin just barely went LOW, so "stop the timer" by obtaining a timestamp of the 
        // end time
        uint32_t time_end_us = micros();
        pin_state_old = pin_state; // update
        time_elapsed_us = time_end_us - time_start_us;
        time_just_acquired = true;
    }

    // In some other place later down the code where you need this value, 
    // you can use it like this. Here I just print the value. 
    if (time_just_acquired)
    {
        time_just_acquired = false; // reset
        Serial.print("time_elapsed_us = "); 
        Serial.println(time_elapsed_us);
    }
}

IMPORTANT: notice in all my examples above I use ONLY UNSIGNED INTEGER variables for timestamps. This is ESSENTIAL. Using signed integers for timestamps in the same way I've written them here would be a violation of the C standard because it will produce undefined behavior when you do subtraction which results in underflow, or when the integer has overflow. Using unsigned integers, however, is perfectly valid. Ex: (uint8_t)0 - (uint8_t)1 = 255, because it is an unsigned 8-bit integer which safely underflows from its lowest value back around to its highest value. Similarly, (uint8_t)255 + (uint8_t)1 = 0 because it is an unsigned 8-bit integer which safely overflows from its highest value back around to its lowest value. This is how time_elapsed_us = time_end_us - time_start_us works in both of my examples as well. As the 32-bit microsecond counter overflows, which it will every 70 minutes or so, it wraps around back to 0. This means that sometimes time_end_us will be SMALLER than time_start_us, and you could end up with a measurement like this: time_elapsed_us = 124 - 4294967295, which equals 125.

Example 3: use external interrupts to detect changes on pins to measure external events

Use this approach when you need to measure external events at the 4~10us or larger resolution level on 2 pins maximum at a time.

This is a really good approach for measuring external events, but you get only 2 pins per Arduino Uno or Nano or similar that can do it. They are pins 2 or 3. See the table here: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/.

For a demo, see Edgar Bonet's answer here.

Example 4: use pin change interrupts to measure external events

Use this approach when you need to measure external events at the 4~10us or larger resolution level on > 2 pins maximum at a time.

These are like external interrupts except you have to manage up to 8 pins in a single interrupt service routine (ISR), instead of only 1 pin per ISR, so they aren't quite as good as "external interrupts". Every single digital-capable pin on an Arduino Uno or Nano can do this. I use this approach when reading many PWM signals from a radio control receiver, for instance, but it requires some sophistication and a ring buffer to do it right, as the time in the ISR must be minimized or else you get tons of jitter all over the place! That means you just grab the timestamp in the ISR, store it in a ring buffer, and exit. You do NOTHING ELSE! No subtraction, no math, no determining which pin fired, nothing! You then process the ring buffer of timestamps and pin states in your main loop to determine which pin changed and do the math to obtain a new time reading on that pin. I used this for passing signals through a fire-shooting battlebots hexacopter that flew on ABC TV. It worked well. It made me happy to see the ISR do its job.

Example 5: use input capture (on pin 8 only) to measure an external event

This is the "golden", or "best" approach. But, you get 1 pin per Arduino Uno or Nano that can do it. Use this approach when you need to measure external events at the 62.5 nanosecond or larger resolution level jitter-free. There will be NO ISR TIMESTAMP DELAY WHATSOEVER with this approach, which is really cool.

Input Capture is only available on 16-bit timers on the 8-bit AVR microcontrollers such as the ATmega328. Since an Uno or Nano only has 1 16-bit timer, this means they get 1 single input capture pin. It is pin 8. Don't waste this pin for anything else if you need perfect time measurements of external events using input capture. Input capture is the "perfect" way to measure external events since it stores the time count in a hardware register at the moment the event happens, without CPU interaction through an ISR, which, as we know, would cause a delay and 4+us jitter.

I first did this on a commercial product I made which needed to read a single Radio Control receiver PWM pin. It made me happy to see it work right, as it has zero jitter. I'll come back and add a demo (code only, no more mention of the product) if I get a chance. This technique is also perfect for reading PPM (Pulse Position Modulation) signals, which are just a bunch of multiplexed Radio Control PWM signals.

4
  • Your last link points the user to a website that is full of advertisements and solicits either a monetary donation or your email address to continue. This question has been locked.
    – VE7JRO
    Jul 20 '20 at 2:19
  • That isn't supposed to happen. Can you screenshot it? I updated my ads recently to Google auto-ads. I need to adjust it and reduce them. I'll take care of it as soon as I can, but I don't know what email address or donation would possibly be requested to continue. Jul 20 '20 at 2:27
  • Also, I've never heard of locking an entire question over one link. That seems really weird. Jul 20 '20 at 2:31
  • I'm voting to close this answer because the last link points the user to a website that is full of advertisements and solicits either a monetary donation or your email address to continue.
    – VE7JRO
    Jul 20 '20 at 3:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.