I am using an ATMEGA328p to measure the frequency of a 50% duty cycle square wave in the range 2khz to 15khz. The square wave is being generated by a good quality Function Generator and I can see on the scope that the output is stable.
For the purpose of raising this question I have simplified my code to
#define SERIAL_OUTPUT
#define led 12
volatile boolean FREQint = false;
volatile unsigned int FREQcount = 0;
volatile unsigned int FREQovfcount = 0;
volatile unsigned int temp_TCNT2 = 0;
volatile unsigned int temp_FREQovfcount = 0;
int FREQprescaler = 1;
int FREQdivider[6] = {
0, 1, 8, 64, 256, 1024
};
#define FREQ_input_pin 8 // PCINT0
//////////////////////////////////////////////////
ISR(TIMER2_OVF_vect) {
FREQovfcount++;
//digitalWrite(led, !digitalRead(led));
}
//////////////////////////////////////
ISR(PCINT0_vect)
{
if ((PINB & 0x01) == 0) { // falling edge
//digitalWrite(led, !digitalRead(led));
// take quick snapshot
temp_TCNT2 = TCNT2;
TCNT2 = 0; // reset timer
temp_FREQovfcount = FREQovfcount;
FREQovfcount = 0; // clear overflow counter
// if just missed an overflow
if (TIFR2 & bit (TOV2)) {
TIFR2 = bit (TOV2); //clear pending interrupts
if (temp_TCNT2 <= 40) {
temp_FREQovfcount++;
//digitalWrite(led, !digitalRead(led));
}
}
FREQcount = (temp_TCNT2 + temp_FREQovfcount * 256);
//digitalWrite(led, !digitalRead(led));
FREQint = true;
}
}
//////////////////////////////////////////////////
void setup() {
#ifdef SERIAL_OUTPUT
Serial.begin(115200);
while (!Serial) {
;
}
#endif
pinMode(led, OUTPUT);
cli();//stop interrupts while setting up the timers
TIMSK2 = 0; // turn off Timer
TCCR2A = 0x00; // normal mode
TCNT2 = 0; // clear timer
TCCR2B = FREQprescaler;
pinMode(FREQ_input_pin, INPUT);
// set interrupt on change
PCICR |= 0b00000001; // turn on port B
PCMSK0 |= 0b00000001; // PCINT8 pin D8
TIMSK2 = bit(TOIE2);// enabled global and timer overflow interrupt;
sei();//allow interrupts
}
//////////////////////////////////////////////////////////////////////////////
void loop()
{
#ifdef SERIAL_OUTPUT
if (FREQint) {
byte oldSREG = SREG; // remember if interrupts are on or off
cli(); // turn interrupts off
unsigned int tempcount = FREQcount;
SREG = oldSREG; // turn interrupts back on
Serial.print(" Freq ");
Serial.print(16000000.0 / FREQdivider[FREQprescaler] / tempcount);
Serial.print(" Hz");
Serial.print(" Count ");
Serial.print(tempcount);
//Serial.print(" TCNT2 ");
//Serial.print(temp_TCNT2);
//Serial.print(" OVF ");
//Serial.print(temp_FREQovfcount);
Serial.println();
FREQint = false;
}
#endif
}
This code is similar to a number of examples on the net eg www.gammon.com.au/timers
My issue is that the FREQcount value varies much more than I expected. The extent of the variation is also sensitive to the frequency of the square wave being measured.
For example
Freq 9750.15 Hz Count 1641
Freq 9621.17 Hz Count 1663
Freq 9632.75 Hz Count 1661
Freq 9632.75 Hz Count 1661
Freq 9227.22 Hz Count 1734
Freq 10302.64 Hz Count 1553
Freq 9638.55 Hz Count 1660
Freq 9732.36 Hz Count 1644
Freq 9638.55 Hz Count 1660
Freq 9632.75 Hz Count 1661
Freq 9638.55 Hz Count 1660
Freq 9638.55 Hz Count 1660
Freq 11228.07 Hz Count 1425
Freq 9744.21 Hz Count 1642
Freq 9632.75 Hz Count 1661
Freq 9638.55 Hz Count 1660
Freq 9632.75 Hz Count 1661
Freq 9227.22 Hz Count 1734
Freq 9950.25 Hz Count 1608
Freq 9638.55 Hz Count 1660
Freq 9632.75 Hz Count 1661
Freq 9638.55 Hz Count 1660
The actual frequency should be 9.543khz.
I can certainly understand why there will be some variation
BUT why are there count values of 1425 and 1734 which are well outside the average?
I could understand the count may be bigger if there were other interrupts but why the lower value?
Can anyone explain why this might be happening please?
-
1\$\begingroup\$ Over what period do you accumulate edge-events? \$\endgroup\$analogsystemsrf– analogsystemsrf2019年06月20日 08:42:01 +00:00Commented Jun 20, 2019 at 8:42
-
\$\begingroup\$ You're using a 16 MHz clock aren't you? \$\endgroup\$Andy aka– Andy aka2019年06月20日 08:55:54 +00:00Commented Jun 20, 2019 at 8:55
-
\$\begingroup\$ I am using a 16mhz clock - hardware is actually an Arduino nano \$\endgroup\$IanF– IanF2019年06月20日 09:12:14 +00:00Commented Jun 20, 2019 at 9:12
-
\$\begingroup\$ analogsystemsrf - the accumulation period is from falling edge to falling edge ie one cycle.. I am using the pin change interrupt but I have seen the same variation issue if INT0/INT1 with rising or falling interrupts are used. \$\endgroup\$IanF– IanF2019年06月20日 09:15:12 +00:00Commented Jun 20, 2019 at 9:15
-
\$\begingroup\$ As you can see in my code I have used debug signals to see on the oscilloscope what is happening. Unfortunately the debug signals also mess with timings. I suspect the issue is related to timer2 overflow timing but how? \$\endgroup\$IanF– IanF2019年06月20日 09:24:12 +00:00Commented Jun 20, 2019 at 9:24
3 Answers 3
You've clearly given this some thought, but some of the low-level details are tripping you up. I'm going to focus on one aspect of your problem, and maybe this will lead you into thinking in the right way to solve the rest of it.
After doing a little research, I learned that
- Interrupts are not nested on the ATMEGA328 (at least, not by default)
- Timer 2 is an 8-bit timer
- Pin change interrupts have a higher priority than timer interrupts.
In your pin change ISR, you capture the state of the hardware and software counters:
// take quick snapshot
temp_TCNT2 = TCNT2;
TCNT2 = 0; // reset timer
temp_FREQovfcount = FREQovfcount;
FREQovfcount = 0; // clear overflow counter
One problem is that you're getting readings of around 1660, when calculation shows that a frequency of 9.543 kHz should give you readings of about 1676. You're consistently losing 16 clocks somewhere.
The first statement above reads the TCNT2 hardware register and stores it in an int
variable. One problem is that TCNT2 is an 8-bit unsigned variable, while in the Arduino environment an unsigned int
is a 16-bit variable. Furthermore, you've declared the variable as volatile
, which means that the compiler must generate code to extend the value to 16 bits and then write it to memory as 2 bytes. Then you clear the register.
This code between the reading and the clearing of the register could easily account for the missing 16 clocks.
It would be better to write this ISR in assembly so that you know exactly what code is running in such a time-critical area. Or at least, examine the compiler output and tweak the source code to minimize the number of instructions it generates here. For example, just changing
volatile unsigned int temp_TCNT2 = 0;
to
register unsigned char temp_TCNT2;
and making it local to the ISR (it isn't used anywhere else) will probably get you close.
Better still, don't clear the timer at all — let it free-run and take the difference between the current and previous values. This way, you eliminate any possibility of "dead time", but this does make the job of resolving overflow events somewhat tricky.
-
\$\begingroup\$ Dave, Thanks for looking at this issue. I have tried changing temp_TCNT2 to a "byte" variable but it made no difference to the count variation itself. I think the missing 16 (or so) counts could be accounted for by resetting TCNT2 = 16; This would avoid the complexity of letting the timer free run. I will write the ISR in assembler, but I need to understand why the variation of -235 to +74 counts (or so) is actually happening. It looks like a timing condition with the handling of overflow - but why? \$\endgroup\$IanF– IanF2019年06月20日 20:19:23 +00:00Commented Jun 20, 2019 at 20:19
There are a few logic flaws in the elapsed time (period) measurement process. The system could instead be implemented as a state machine with (at least) these three states: ARMED, MEASURE, and DONE.
FREQINT_STATE_ARMED. This is the starting state. The
setup()
function or theloop()
function places the system into the ARMED state at time t=t0. Sometime later at time t=t1 the input signal's falling edge is detected, which triggers the start of a period measurement. Specifically, the falling edge at time t=t1 invokesISR(TIMER2_OVF_vect)
, and because the system's current state is ARMED the ISR resets the counter subsystem (COUNT=0), and it transitions the system state from ARMED to MEASURE.FREQINT_STATE_MEASURE. During the MEASURE state timer/counter2's count register and the timer/counter2 overflow interrupt accumulate their respective count values. When the input signal's next falling edge is detected at time t=t2,
ISR(TIMER2_OVF_vect)
is called a second time. The ISR detects that the current state is MEASURE, and therefore the ISR saves intoFREQcount
the count value in timer/counter2's count register, it saves intoFREQovfcount
the accumulated overflow count value, it disables the timer/counter2 overflow interrupt, and it transitions the system state from MEASURE to DONE.FREQINT_STATE_DONE. During the DONE state
ISR(TIMER2_OVF_vect)
is disabled. As timer/counter2 continues counting and overflowing,ISR(TIMER2_OVF_vect)
is not invoked when the count overflows; therefore, the overflow count value stored inFREQovfcount
does not change.ISR(PCINT0_vect)
is invoked by the input signal's subsequent state transitions, but in the DONE stateISR(TIMER2_OVF_vect)
ignores those state changes; therefore, the count value stored inFREQcount
does not change. Theloop()
function waits for the DONE state. When the DONE state is detected at time t=t2, function loop() calculates the period and frequency, and then it transitions the system from DONE to ARMED (time t=t3) in preparation for next period measurement.
+-----+ +-----+ ---+ +-----+
| | | | | | |
..+ +---+ +--- ......... +---+ +--- ...
t0 t1 t2 t3
|-------|---------|------...|-------|---------|----...--> t
armed measure done armed measure done
|-------->| |-------->|
COUNT=0 COUNT=N COUNT=0 COUNT=N
I whipped up some changes to your code to show how this might be done. (CAVEAT: I have not tested this code, so I don't know if additional adjustments are still needed. Also, I wrote this code using Atmel Studio 7, but it should build within the Arduino Sketch program (or whatever that Arduino IDE program is called).)
/*Begining of Auto generated code by Atmel studio */
#include <Arduino.h>
/*End of auto generated code by Atmel studio */
//Beginning of Auto generated function prototypes by Atmel Studio
ISR(TIMER2_OVF_vect );
ISR(PCINT0_vect );
//End of Auto generated function prototypes by Atmel Studio
#define SERIAL_OUTPUT
#define led 12
#define FREQINT_STATE_ARMED 0
#define FREQINT_STATE_MEASURE 1
#define FREQINT_STATE_DONE 2
#define TCNT2_RESET_COUNT 0
volatile unsigned int FREQint_state = FREQINT_STATE_DONE;
volatile unsigned int FREQcount = 0;
volatile unsigned int FREQovfcount = 0;
int FREQprescaler = 1;
int FREQdivider[6] = {
0, 1, 8, 64, 256, 1024
};
#define FREQ_input_pin 8 // PCINT0
//////////////////////////////////////////////////
ISR(TIMER2_OVF_vect)
{
FREQovfcount++;
//digitalWrite(led, !digitalRead(led));
}
//////////////////////////////////////
ISR(PCINT0_vect)
{
// Immediately record the value of timer/counter2's count register.
static unsigned int temp_TCNT2 = TCNT2;
if ((PINB & 0x01) == 0) { // falling edge
switch ( FREQint_state ) {
//////////////////////////////////////////////////////////////////////
// If the system is in the ARMED state, this falling edge triggers
// the start of a new period measurement.
//////////////////////////////////////////////////////////////////////
case FREQINT_STATE_ARMED:
// Reset timer/counter2's count value
TCNT2 = TCNT2_RESET_COUNT;
// Clear timer/counter2's count overflow flag. This is done by
// writing a 1 bit into bit TOV2 within TIFR2.
TIFR2 |= bit(TOV2);
// Reset the overflow counter
FREQovfcount = 0;
// Enable timer/counter2's overflow interrupt
TIMSK2 |= bit(TOIE2);
// Transition the system from the ARMED state to the MEASURE state.
FREQint_state = FREQINT_STATE_MEASURE;
break;
//////////////////////////////////////////////////////////////////////
// If the system is in the MEASURE state, this falling edge signals
// the end of the period measurement.
//////////////////////////////////////////////////////////////////////
case FREQINT_STATE_MEASURE:
// Record timer/counter2's count value
FREQcount = temp_TCNT2;
// Disable timer/counter2's overflow interrupt
TIMSK2 &= ~bit(TOIE2);
// Clear any pending overflow interrupt for timer/counter2
if (TIFR2 & bit (TOV2)) {
TIFR2 |= bit (TOV2);
// Some fine tuning in the event timer/counter2 overflowed during
// this execution of the ISR.
if (temp_TCNT2 <= 40) { // magic number 40 (explain 40's meaning)
FREQovfcount++;
}
}
// Transition the system from the MEASURE state to the DONE state.
FREQint_state = FREQINT_STATE_DONE;
break;
//////////////////////////////////////////////////////////////////////
// Code outside this ISR--e.g., function setup() or loop() must
// transition the system state into the ARMED state in preparation
// for a new measurement. Until then this ISR ignores the input
// signal's state transitions.
//////////////////////////////////////////////////////////////////////
case FREQINT_STATE_DONE:
// do nothing
break;
}
}
}
//////////////////////////////////////////////////
void setup()
{
#ifdef SERIAL_OUTPUT
Serial.begin(115200);
while (!Serial) {
;
}
#endif
pinMode(led, OUTPUT);
cli();//stop interrupts while setting up the timers
TIMSK2 = 0; // turn off Timer
TCCR2A = 0x00; // normal mode
TCNT2 = 0; // clear timer
TCCR2B = FREQprescaler;
pinMode(FREQ_input_pin, INPUT);
// set interrupt on change
PCICR |= 0b00000001; // turn on port B
PCMSK0 |= 0b00000001; // PCINT8 pin D8
FREQint_state = FREQINT_STATE_ARMED;
sei();//allow interrupts
}
//////////////////////////////////////////////////////////////////////////////
void loop()
{
#ifdef SERIAL_OUTPUT
if (FREQint_state == FREQINT_STATE_DONE) {
/* CRITICAL_SECTION */
byte oldSREG = SREG; // remember if interrupts are on or off
cli(); // turn interrupts off
unsigned int count = FREQcount + FREQovfcount * 256;
FREQint_state = FREQINT_STATE_ARMED;
SREG = oldSREG; // turn interrupts back on
/* /CRITICAL_SECTION */
Serial.print(" Freq ");
Serial.print(16000000.0 / FREQdivider[FREQprescaler] / count);
Serial.print(" Hz");
Serial.print(" Count ");
Serial.print(count);
//Serial.print(" TCNT2 ");
//Serial.print(temp_TCNT2);
//Serial.print(" OVF ");
//Serial.print(temp_FREQovfcount);
Serial.println();
}
#endif
}
-
\$\begingroup\$ Jim, Thanks. Really appreciate your whipping up some code. The greater overhead means the measured frequency is lower than the actual (for example measured 10126hz v 11994hz. But the more interesting result is that the count variation is always 256. Freq 10126.58 Hz Count 1580 versus Freq 12084.59 Hz Count 1324. This points to an overflow timing issue even though the check of TOV2 during the measure phase is meant to avoid that. \$\endgroup\$IanF– IanF2019年06月20日 22:03:52 +00:00Commented Jun 20, 2019 at 22:03
-
\$\begingroup\$ @IanF, In ISR(PCINT0_vect), in the FREQINT_STATE_ARMED case, I added an instruction that clear's timer/counter2's count overflow flag at the start of the period measurement: TIFR2 |= bit(TOV2). I overlooked that step in my original post. See if adding that instruction solves the off-by-one error for FREQovfcount's value. \$\endgroup\$Jim Fischer– Jim Fischer2019年06月21日 06:53:47 +00:00Commented Jun 21, 2019 at 6:53
-
\$\begingroup\$ Jim, Yes that added instruction avoided the 256 count variation. I think I now understand the root cause(s) of the count variation I saw in my code. For example (if prescaler = 1) any other interrupt that runs for >= (2 * 256 * 1/16mhz) = 32us will delay the TIMER2_OVF_vect ISR and result in an overflow interrupt being missed. Also a TIMER2_OVF_vect ISR that runs just before the PCINT0 interrupt will delay the pin change processing. The first problem can be avoided (eg no serial output during a measurement), The second problem can only be minimised. \$\endgroup\$IanF– IanF2019年06月21日 11:21:22 +00:00Commented Jun 21, 2019 at 11:21
Thanks all for your help. The following code now works without significant frequency measurement variation. The variations in my original code were caused by:
1) At prescaler = 1, any other interrupt that runs for more than 2*256*1/16mhz (32us) will delay the TIMER2_OVF ISR processing and result in an overflow interrupt being missed. For example, serial output interrupts during the measurement cycle can mean an overflow interrupt is missed.
2) Any interrupt (including an overflow interrupt) that triggers just before the pin change interrupt will cause a delay to the start of the pin change ISR. Because of the timing of an overflow, this issue is sensitive to the frequency being measured and can be minimised but not totally avoided.
3) The timer needs to be stopped when taking a snapshot of the Timer2 counter value and the TOV2 flag. Otherwise the results may not always be valid.
4) Ideally all other sources of interrupts should be disabled. For example the built in Arduino Timer0 delay() etc.
#define SERIAL_OUTPUT
volatile boolean FREQint = false;
volatile unsigned int FREQcount = 0;
byte FREQovfcount = 0;
byte temp_TCNT2;
byte temp_TIFR2;
byte temp_FREQovfcount = 0;
int FREQprescaler = 1;
int FREQdivider[6] = {
0, 1, 8, 64, 256, 1024
};
#define FREQ_input_pin 8 // PCINT0
//////////////////////////////////////////////////
ISR(TIMER2_OVF_vect) {
FREQovfcount++;
}
//////////////////////////////////////////////////
// NB any higher priority interrupts (including late overflow interrupts)
// during the frequency cycle can cause timing variation errors +/-
// eg (if prescaler = 1) any interrupt that runs for >= (2 * 256 * 1/16mhz) = 32us
// will result in an overflow interrupt missed
// timer trim to allow for lost time to take snapshot
// at 20khz timer value should be 800. Trim of 15 is ok (prescaler = 1).
const byte TNCT2_TRIM = 15;//2 if prescaler = 2 // 15 if prescaler = 1
ISR(PCINT0_vect)
{
if ((PINB & 0x01) == 0) { // falling edge
// take time critical snapshot
TCCR2B = 0; // stop timer2 so overflow not possible
temp_TIFR2 = TIFR2; // copy TOV2 flag
temp_TCNT2 = TCNT2; // copy timer2 value
TCNT2 = TNCT2_TRIM; // reset timer to allow for lost time taking snapshot
TIFR2 |= bit (TOV2); //clear pending interrupts early
TCCR2B = FREQprescaler; // restart timer2
//non time critical tasks follow since ISR can not be interrupted
temp_FREQovfcount = FREQovfcount;
FREQovfcount = 0; // clear overflow counter
// if just missed an overflow
if (temp_TIFR2 & bit (TOV2)) {
// if an unserviced overflow has happend, TCNT2 value should be
// at least ISR(PCINT0_vect) response time
// plus time before the temp_TCNT2 snapshot is taken 50 is ok (prescaler = 1).
if (temp_TCNT2 <= 50) {
temp_FREQovfcount++;
}
}
FREQcount = (temp_TCNT2 + temp_FREQovfcount * 256);
FREQint = true; // have a valid measurement
}
}
//////////////////////////////////////////////////
void setup() {
#ifdef SERIAL_OUTPUT
Serial.begin(250000);
while (!Serial) {
;
}
#endif
cli();//stop interrupts while setting up the timers
TIMSK2 = 0; // turn off Timer
TCCR2A = 0x00; // normal mode
TCNT2 = 0; // clear timer
TCCR2B = FREQprescaler;
pinMode(FREQ_input_pin, INPUT);
// set interrupt on change
PCICR |= 0b00000001; // turn on port B
PCMSK0 |= 0b00000001; // PCINT8 pin D8
TIMSK2 = bit(TOIE2);// enabled global and timer overflow interrupt;
TCCR0A = 0; // stop timer 0 interrupts eg delay() etc
TCCR0B = 0;
sei();//allow interrupts
}
//////////////////////////////////////////////////////////////////////////////
int loopcount = 0;
void loop()
{
#ifdef SERIAL_OUTPUT
if (FREQint) {
loopcount++;
if (loopcount > 50) { // simple delay to avoid using delay()
loopcount = 0;
//critical timing
byte oldSREG = SREG; // remember if interrupts are on or off
cli(); // turn interrupts off
unsigned int tempcount = FREQcount;
SREG = oldSREG; // turn interrupts back on
float freq = 16000000.0 / FREQdivider[FREQprescaler] / tempcount;
Serial.print(freq);
Serial.print(" ");
Serial.println(tempcount);
}
FREQint = false;
}
#endif
}