TL;DR
I can't manage to properly measure duty cycle of a PWM signal above several kHz, and I need to get to about 30kHz.
Hello,
I'm interested in using Arduino for frequency and duty cycle measuring of a PWM signal in a project I'm making. I've read up on the matter and tried about four or five different methods so far but none have met my requirements yet. This is what I need:
- frequency measurement range: 1Hz to at least 30kHz (I need to get above audible frequencies, each additional kilohertz is a bonus)
- duty cycle measurement range: at least 20% to 80%, ideally 10% to 90%
I don't think they're unreasonable but I have problems with the second part. I'll describe the issue but first I'll mention that I tried measurements with pulseIn() which was pretty bad and with attachInterrupt which was much better but bad at frequencies above 10kHz and had the duty cycle issue as well. Currently I am using a custom mishmash of two codes Nick Gammon wrote on the matter (pasting the relevant parts of it at the end of this post), which I believe uses Input Capture ability of the internal timers or something like that. It allows me to go up to 60kHz (Nick Gammon says he managed to push it to 200kHz but I couldn't) which is nice, but has the darned duty cycle measurement issue.
So, what's the problem? Basically, the higher the frequency, the narrower the measurable range of the duty cycle is. If I increase the duty cycle too much, I believe the ISR takes too much time and the Arduino doesn't register the RISING edge so very quickly after the FALLING edge. This results in frequency readout showing roughly half of the true frequency and the duty cycle being not 90% or something like that, but usually around 45%. The same situation happens on the opposite end of the duty cycle range: when I go too low (usually when the pulse is shorter than a few micro seconds, i.e. 10kHz and above), the FALLING edge comes too quickly after RISING edge, the shown frequency goes down and duty cycle read spikes from let's say 15% to 60%. This issue starts appearing at 1600 Hz when I push the DC above 98% (no problem in the lower range of DC yet). The lower range measurement starts to get wonky at 15kHz below about 11% DC while at that frequency the upper range issue appears at only 90%. Finally at 35kHz I can't go above 77% DC or below 22% DC without the issue.
Now, I know that there are libraries that allow Arduino to measure extreme frequencies such as FreqCounter, but those can't measure the duty cycle, which is integral to the project I'm making. The code I have so far has been the best but is just not enough (and I think the ISR is as quick as possible as it is). So I don't believe I can fix this problem (though I'd love to do so). What I'd like to do instead is to find out how to recognise when the measurement is inaccurate. I'd like to read the DC on an LCD, and when it starts to be wonky, show fixed "out of range" instead of "83.56%" or something like that. I have absolutely no idea how to program it that way though: in the case of the issue, I just get half the true frequency, which is not something I can easily detect as an error.
So as it is apparent, I'm stumped and I need some advice on how to proceed. Thanks for any help.
volatile boolean first;
volatile boolean sec;
volatile boolean triggered;
volatile unsigned long overflowCount;
volatile unsigned long startTime;
volatile unsigned long fallTime;
volatile unsigned long finishTime;
long int refresh_freq=0, refresh_time=0, refreshLCD=0;
// timer overflows (every 65536 counts)
ISR (TIMER1_OVF_vect)
{
overflowCount++;
} // end of TIMER1_OVF_vect
ISR (TIMER1_CAPT_vect)
{
// grab counter value before it changes any more
unsigned int timer1CounterValue;
timer1CounterValue = ICR1; // see datasheet, page 117 (accessing 16-bit registers)
unsigned long overflowCopy = overflowCount;
// if just missed an overflow
if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 0x7FFF)
overflowCopy++;
// wait until we noticed last one
if (triggered)
return;
if (first)
{
startTime = (overflowCopy << 16) + timer1CounterValue;
TIFR1 |= bit (ICF1); // clear Timer/Counter1, Input Capture Flag
TCCR1B = bit (CS10); // No prescaling, Input Capture Edge Select (falling on D8)
first = false;
sec=true;
return;
}
if(sec)
{
fallTime = (overflowCopy << 16) + timer1CounterValue;
TIFR1 |= bit (ICF1);
TCCR1B = bit (CS10) | bit (ICES1);
sec = false;
return;
}
finishTime = (overflowCopy << 16) + timer1CounterValue;
triggered = true;
TIMSK1 = 0; // no more interrupts for now
} // end of TIMER1_CAPT_vect
void prepareForInterrupts ()
{
noInterrupts (); // protected code
first = true;
sec=false;
triggered = false; // re-arm for next time
// reset Timer 1
TCCR1A = 0;
TCCR1B = 0;
TIFR1 = bit (ICF1) | bit (TOV1); // clear flags so we don't get a bogus interrupt
TCNT1 = 0; // Counter to zero
overflowCount = 0; // Therefore no overflows yet
// Timer 1 - counts clock pulses
TIMSK1 = bit (TOIE1) | bit (ICIE1); // interrupt on Timer 1 overflow and input capture
// start Timer 1, no prescaler
TCCR1B = bit (CS10) | bit (ICES1); // plus Input Capture Edge Select (rising on D8)
interrupts ();
} // end of prepareForInterrupts
void setup ()
{
Serial.begin(115200);
prepareForInterrupts ();
} // end of setup
void loop ()
{
if (triggered)
{
unsigned long totalTime = finishTime - startTime;
unsigned long onTime = fallTime - startTime;
float freq = F_CPU / float (totalTime);
float duty = (onTime*100.0)/totalTime;
Serial.print(freq);
Serial.print("Hz / ");
Serial.print(duty);
Serial.print(" %");
Serial.print("\n");
prepareForInterrupts ();
}
} // end of loop