(arduino) simple frequency divider, unwanted unsigned long int rollover - Page 1
Forum section: electronics / general

author: JeanF
author_rank: Regular Contributor
timestamp: November 13, 2020, 11:45:14 pm
content: Hello,TL;DR I don't understand why the " if ( (currentTime - lastInterruptTime) >= TIMEOUT)" test doesn't behave as expected. All variables are unsigned long and I should be calling micros() in the right order, but apparently, I'm not. currentTime should be always more recent (larger) than lastInterruptTime, but in reality, lastInterruptTime is sometimes (seemingly randomly) larger than currentTime, so the difference rolls over to a very large number around 2^32, and the "if" condition is then true instead of false as it should be.Details: I'm trying to make a quick and dirty frequency divider (while preserving input duty cycle) for a low-frequency signal (~300 Hz) with an Arduino. I happen to have an Arduino Mega lying around but that shouldn't matter.Here is my code :Code:// defines and constantsconst byte INPUT_PIN = 2;const byte OUTPUT_PIN = 7;const int FREQ_DIV_FACTOR = 10;const unsigned long TIMEOUT = 1000000UL;// global variablesint edgeCounter = 0;unsigned long currentTime = 0, lastInterruptTime = 0, lastOutputRisingEdge = 0;unsigned long inputPulseWidth = 0;//functionsvoid pinChangeInterrupt();void setup(){pinMode(INPUT_PIN, INPUT);pinMode(OUTPUT_PIN, OUTPUT);attachInterrupt(digitalPinToInterrupt(INPUT_PIN), pinChangeInterrupt, CHANGE);}void loop(){currentTime = micros();if ( (currentTime - lastOutputRisingEdge) >= (FREQ_DIV_FACTOR * inputPulseWidth) ){digitalWrite(OUTPUT_PIN, LOW);}if (edgeCounter == (2 * FREQ_DIV_FACTOR)){digitalWrite(OUTPUT_PIN, HIGH);edgeCounter = 0;lastOutputRisingEdge = currentTime;}}void pinChangeInterrupt(){currentTime = micros();byte currentInputState = digitalRead(INPUT_PIN);if (currentInputState == LOW){inputPulseWidth = currentTime - lastInterruptTime;}lastInterruptTime = currentTime;edgeCounter++;return;}I used an Analog Discovery 2 to generate a PWM signal with variable frequency and duty cycle, and both scope channels to see input and output waveforms.The code works well as it is for the low frequency I'm dealing with (tested from 20 Hz to 2 kHz and duty cycle 1% to 99%) The code above can surely be improved, some would say that I should be manipulating registers directly instead of using "crazy" functions like digitalWrite(), and maybe avoiding Arduino altogether, but let's not get into that too much if we can!I would like to modify the code in order to correctly deal with 0% and 100% input duty cycle, so I was adding the following at the beginning of the main loop :Code:if ( (currentTime - lastInterruptTime) >= TIMEOUT){digitalWrite(OUTPUT_PIN, digitalRead(INPUT_PIN));return;}and that doesn't work. There is a lot of unwanted output pin toggling, even when the input duty cycle is nowhere near 1% or 99%. The output waveform is completely unusable now, even when PWM is present at the input ; not in the DC case I want to address.It seems that sometimes, lastInterruptTime is greater than currentTime, and the difference rolls over to a very large number as it can't be negative. But I've been puzzled by this for hours and I can't figure out why!I've tried to buffer the value of lastInterruptTime, as well as calling micros() inside the if instead of some time before, and toggle another pin to see it with a scope, as follows :Code:void loop(){unsigned long test = lastInterruptTime;currentTime = micros();if ( (micros() - test) >= TIMEOUT){PINE = _BV(PE5);}if ( (currentTime - lastOutputRisingEdge) >= (FREQ_DIV_FACTOR * inputPulseWidth) ){digitalWrite(OUTPUT_PIN, LOW);}if (edgeCounter == (2 * FREQ_DIV_FACTOR)){digitalWrite(OUTPUT_PIN, HIGH);edgeCounter = 0;lastOutputRisingEdge = currentTime;}}but the rollover still happens frequently.That must be a simple thing hiding in plain sight!Thank you for any help,JF

author: DrG
author_rank: Super Contributor
timestamp: November 13, 2020, 11:54:47 pm
content: You know that micos() will rollover ever ~71 minutes - right? millis() ~49 dayssee herehttps://roboticsbackend.com/arduino-millis-vs-micros/#millis()_vs_micros()_overflowor a kazillion other places.Does that change your perception of the problem? I am asking sincerely because I did not carefully go over your code/text.

author: JeanF
author_rank: Regular Contributor
timestamp: November 14, 2020, 12:06:22 am
content: Thank you, yes I'm aware of it, maybe "rollover" is not the best word to describe what I'm experiencing, it's just what I had in mind.I read the usual tips in various places as you say to always compare (micros() - oldtimestamp) to something and not put micros() directly next to the operator. But I don't think this is the problem here, as the "unwanted trigger" of the problematic If condition happens many times per second, not just after 70 min.The problem is probably also related to the input waveform and the ISR being called, and there must be a case of lastInterruptTime being updated with a newer value of micros() when I'm not expecting it, but I'm having a hard time understanding how it could happen... too much (or not enough) caffeine maybe

author: Ian.M
author_rank: Super Contributor
timestamp: November 14, 2020, 12:26:47 am
content: currentTimeandlastInterruptTime are modified by the ISR and accessed in the main loop so *must* be declared volatile.What I think's happening is:Code:if ( (currentTime - lastOutputRisingEdge) >= (FREQ_DIV_FACTOR * inputPulseWidth) )occasionally gets a borked value for the variables because the ISR changes them in the middle of the calculation!The easy fix is disabling interrupts while the main program accesses them:Code:nointerrupts();timeDiff=currentTime - lastOutputRisingEdge;interrupts();if ( timeDiff >= (FREQ_DIV_FACTOR * inputPulseWidth) )however that adds to the latency of the edge detection interrupt so you may have to do something cleverer . . .

author: DrG
author_rank: Super Contributor
timestamp: November 14, 2020, 12:46:56 am
content: what about not doing all that processing inside the ISR...something like....Code:// defines and constantsconst byte INPUT_PIN = 2;const byte OUTPUT_PIN = 7;const int FREQ_DIV_FACTOR = 10;const unsigned long TIMEOUT = 1000000UL;// global variablesint edgeCounter = 0;unsigned long currentTime = 0, lastInterruptTime = 0, lastOutputRisingEdge = 0;unsigned long inputPulseWidth = 0;volatile unsigned int ISR_counter=0;//functionsvoid pinChangeInterrupt();void setup(){pinMode(INPUT_PIN, INPUT);pinMode(OUTPUT_PIN, OUTPUT);attachInterrupt(digitalPinToInterrupt(INPUT_PIN), isr_routine, CHANGE);}void loop(){//currentTime = micros();if(isr_counter){isr_counter--;pinChangeInterrupt()}if ( (currentTime - lastOutputRisingEdge) >= (FREQ_DIV_FACTOR * inputPulseWidth) ){digitalWrite(OUTPUT_PIN, LOW);}if (edgeCounter == (2 * FREQ_DIV_FACTOR)){digitalWrite(OUTPUT_PIN, HIGH);edgeCounter = 0;lastOutputRisingEdge = currentTime;}}void pinChangeInterrupt(){currentTime = micros();byte currentInputState = digitalRead(INPUT_PIN);if (currentInputState == LOW){inputPulseWidth = currentTime - lastInterruptTime;}lastInterruptTime = currentTime;edgeCounter++;//return;}void isr_routine{ISR_counter++;}

author: brucehoult
author_rank: Super Contributor
timestamp: November 14, 2020, 12:51:56 am
content: I don't think there's any practical choice but to disable interrupts while loading a four-byte value that is updated by an interrupt routine. It's pretty quick as CLI/SEI are very simple 1-cycle instructions. You can minimize it by loading it into a local variable first and doing the subtraction after the SEI.With a two byte value you can avoid disabling interrupts using code such as:Code:char hi, hi_again, lo, volatile *lastOutputRisingEdgeBytes = (volatile char*)&lastOutputRisingEdge;do {hi = lastOutputRisingEdgeBytes;lo = lastOutputRisingEdgeBytes;hi_again = lastOutputRisingEdgeBytes;} while (hi != hi_again);int myLastOutputRisingEdge = (hi<<8) | lo;But this get impractical for a four byte value.In fact it's not even that great for a two byte value as the lower byte will roll over often. It's good for reading a 64 bit value on a 32 bit MPU.

author: JeanF
author_rank: Regular Contributor
timestamp: November 14, 2020, 12:30:27 pm
content: Thank you all for your comments.Temporarily disabling interrupts looks like a very good thing to do indeed! In fact, I was using noInterrupts() and interrupts() a few days ago when I was trying to use pulseIn(). Then I gave up with pulseIn() and looked for a non-blocking alternative, and managed to forget that it was possible to disable interrupts!Duly noted about volatile declaration, I wasn't aware of that.I think the latency of noInterrupts() and interrupts() is negligible. The input signal is slow, so the inputPulseWidth values are substantial. And I'm using slow digitalWrite() and digitalRead() functions everywhere anyways. But I'll keep that in mind for future projects.The code works as expected now.List of changes :- reduced number of global variables :- I think currentTime doesn't need to be shared between ISR and loop(), I used local variables for time instead,- lastOutputRisingEdge is only used by loop() so I converted it to a static local variable,- disabling interrupts while making a copy of lastInterruptTime, and calling micros() after that and not before, so hopefully currentTime is always more recent,- added the following snippet to loop() to deal with DC at the input :Code:if ( (currentTime - lastInterruptTime_Copy) >= TIMEOUT){digitalWrite(OUTPUT_PIN, digitalRead(INPUT_PIN));}- removed the "return" in the above snippet, with it the output would never be able to oscillate again when PWM comes back at the input,- changed the "if ( (currentTime - lastOutputRisingEdge) >= (FREQ_DIV_FACTOR * inputPulseWidth) )" which is used to pull the output low at the correct time in the output cycle to an "else if" to avoid interference with the previous if() which detects DC at input,- changed "==" to ">=" in "if (edgeCounter == (2 * FREQ_DIV_FACTOR))" because with == the condition can be missed and the output can fail to start when going from DC to PWM at the inputfinal code :Code:// defines and constantsconst byte INPUT_PIN = 2;const byte OUTPUT_PIN = 7;const int FREQ_DIV_FACTOR = 10;const unsigned long TIMEOUT = 1000000UL;// global variablesvolatile int edgeCounter = 0;volatile unsigned long lastInterruptTime = 0;volatile unsigned long inputPulseWidth = 0;//functionsvoid pinChangeInterrupt();void setup(){pinMode(INPUT_PIN, INPUT);pinMode(OUTPUT_PIN, OUTPUT);attachInterrupt(digitalPinToInterrupt(INPUT_PIN), pinChangeInterrupt, CHANGE);}void loop(){unsigned long currentTime;unsigned long lastInterruptTime_Copy;static unsigned long lastOutputRisingEdge;noInterrupts();lastInterruptTime_Copy = lastInterruptTime;interrupts();currentTime = micros();if ( (currentTime - lastInterruptTime_Copy) >= TIMEOUT){digitalWrite(OUTPUT_PIN, digitalRead(INPUT_PIN));}else if ( (currentTime - lastOutputRisingEdge) >= (FREQ_DIV_FACTOR * inputPulseWidth) ){digitalWrite(OUTPUT_PIN, LOW);}if (edgeCounter >= (2 * FREQ_DIV_FACTOR)){digitalWrite(OUTPUT_PIN, HIGH);edgeCounter = 0;lastOutputRisingEdge = currentTime;}}void pinChangeInterrupt(){unsigned long t = micros();if (digitalRead(INPUT_PIN) == LOW){inputPulseWidth = t - lastInterruptTime;}lastInterruptTime = t;edgeCounter++;return;}I have a few more questions if you don't mind:- I wonder whether the "if... else if" is OK there or is a bit dodgy, because I'm not testing the same thing in the two; first (currentTime - lastInterruptTime_Copy) and then (currentTime - lastOutputRisingEdge). Should I keep two separate "if()" instead and use a variable such as "byte pwmIsActive;" to avoid interference between the two?- DrG, yes I know that in general ISRs should be as short as possible but in this case, the goal is to measure input pulse width so I assumed that timestamping the interrupt was the absolute priority, so I thought that calling micros() in the ISR could be acceptable. Is it really that bad? Of course I can do what you suggest and raise a flag and deal with it later in loop(). Is there a way (as a beginner) I could set up my oscilloscope to see which of the two solutions gives better performance?- I see you commented out "return" in pinChangeInterrupt(), why? Does this mean that in general, whatever can be omitted should be omitted?Bruce, thanks for the tip about splitting values in half, I'll keep that in mind.Thanks again

author: DrG
author_rank: Super Contributor
timestamp: November 14, 2020, 02:04:09 pm
content: Quote from: JeanF on November 14, 2020, 12:30:27 pmYour "return" comes at the end of a void function (one not returning a value) and it does not serve any purpose since the function will return at that point with or without the "return" statement. Take a look herehttps://www.arduino.cc/reference/en/language/structure/control-structure/return/for examples of using "return"."whatever can be omitted should be omitted?"Well, that gets a bit philosophical. Yes, in the sense that you don't want statements in your code that get executed but that you don't need, but in some cases, one could argue that something is there that could be eliminated but serves some kind of explanatory value...I guess that is usually a pretty weak argument, at least when I have made itFor what I think that you want to do, you may want to look atpulseInorpulseInLong....https://www.arduino.cc/en/Reference.PulseInhttps://www.best-microcontroller-projects.com/arduino-pulsein.htmlhttps://stackoverflow.com/questions/20727327/arduino-pulsein-returns-weird-valuesThe second citation above gives a pretty good explanation. Edit: The third citation, the solution entry, shows a cleaner way (?) of using micros() inside an interrupt.Hope it helps.

author: JeanF
author_rank: Regular Contributor
timestamp: November 14, 2020, 06:23:21 pm
content: Well, fair enough. I was under the impression that "every function must return something, even if that something is "void", and the return statement is necessary" but obviously I must have invented that somehow, and should have double-checked before askingThis is one of the things that I did as a C noob "just because I've read it somewhere" without doing my homework... Now when I think about it, I agree there is little to no justification to use "return" at the end. As someone said on stackexchange, "when the code ends, it ends"I'll be more attentive next time. Sorry for the philosophical minute, that wasn't intended, I replied a bit too quickly!About pulseIn(), I used it at first but I changed my mind for 2 reasons, 1st is I wasn't able to produce reasonably clean output pulses. Not saying it's impossible though; most probably my code was flawed. Also, I was disappointed that it was not possible to measure a pair of consecutive HIGH and LOW pulses, there would always be a 1-cycle offset. In the end, it doesn't matter, because as the division factor is an integer, I can just count edges and there is no need to measure the off period. I may give pulsein() another chance, just for the sake of learning.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 14, 2020, 06:44:57 pm
content: Note that once you qualify a variable asvolatile, the optimization (of reusing old memory load/store, or rearranging operations, for example) for that variable is prevented each and every time you use it.Also note, as shown earlier, volatile itself doesn't make the access atomic, which is what you want; atomicity is preventing any ISR that touches the variable from running, by disabling and enabling interrupts.All in all, the pattern you may want to typically use, to minimize the duration of disabled interrupts, and maximize the amount of optimizations the compiler is allowed to do:Code:volatile int32_t thing;// atomic access here:cli(); // 1 instructionint32_t tmp_thing = thing; // four memory loads on a 8-bit architecturesei(); // 1 instruction// do whatever with tmp_thing; compiler can now optimize this properly,// for example combine these operations and avoid loading from / storing to memorytmp_thing -= 42;tmp_thing *= 2;if(tmp_thing > 123)...// write back to the volatile variable here, and only here:// atomic access here:cli(); // 1 instructionthing = tmp_thing; // four memory stores on a 8-bit architecturesei(); // 1 instructionThis is hugely more efficient than the functionally equivalent:Code:volatile int32_t thing;cli();thing -= 42; // forces memory read-modify-write, because thing is volatilething *= 2; // dittoif(thing > 123) // it's read from memory again......sei();This example shows the volatile variable being modified here, which isn't needed in your case, though.

author: JeanF
author_rank: Regular Contributor
timestamp: November 15, 2020, 01:33:42 pm
content: Thanks for your input. With the help of the previous posts, that's what I used (cli(), make a copy of the value, sei() right after). I appreciate that you took the time to confirm it was the right thing to do, with even more details. I didn't know about "atomicity" until now