duty cycle measurement

For an irrigation project I need to measure the duty cycle of a 2.4kHz signal.

The processor (Atmega 168P or 328P) has to do the following:

  1. wake-up after a call from a DS3231 RTC, once an hour or more
  2. wait a short while for the sensor to settle (5 seconds?)
  3. do the duty cycle measurements at the digital input during X periods (to be decided, between 1 and hundreds..)
  4. convert this value to a string
  5. transmit this string through hardware serial to a HC-12 434MHz transceiver.
  6. go back to extended sleep

So the processor has nothing to do except sleep, wake up and do some measurements, calculate a value and convert that to a string, transmit, and go back to sleep

I want to use direct register addressing, no "digitalRead" (too slow). I found this thread on the subject : Frequency and duty cycle measurement issues - Project Guidance - Arduino Forum but would like to know if any adaptations need to be made to this solution. Or if a better solution exists?

Thanks for any assistance!!

Try the pulseIn() function

Allan

PulseIn() is not accurate enough. Shortest pulse read is 10 microseconds which is 100kHz fastest frequency, hence about 5% resolution at best. I need accuracy of at least 0.25%.

I am sorry, I should have specified my required accuracy. On a pulse of about 400 microseconds length I need 1 microsecond precision, which is about 0.25%. And this has to be repeatable continually.

If it were that simple -with PulseIn()- sure enough highly skilled programmers such as dlloyd in Frequency and duty cycle measurement issues - Project Guidance - Arduino Forum would not have written what they wrote as sample program. So I would prefer to stick to their proposed suggestions.

My question acually elaborates on their work: how to best use their code -or what to alter- for duty cycle measurement on a digital input of a 2.4kHz signal with the resolution and accuracy mentioned above, on a Atmega168P or 328P?

This is the code where I need the part that generates the PWM to be removed. I only need the part that actually reads the incoming signal to measure the duty cycle:

// Test PWM period and width measurement (connect pin 3 to pin 8)
// Measurement timing diagram: http://i.imgur.com/NqV4nXe.png

const byte pwmInputPin = 2;
volatile byte testState = 0;
volatile word timerValue[4];
float pwmPeriod, pwmWidth, pwmDuty, pwmFrequency;

void setup()
{
  pwmBegin(1);                     // 250kHz PWM, range for duty is 1-63
  TIMSK0 = 0;                      // for testing with timer0 disabled
  pwmMeasureBegin();
  Serial.begin(115200);
}

void loop()
{
  if (testState == 5) {            // tests completed
    noInterrupts();
    word periodValue = timerValue[3] - timerValue[2];
    word widthValue = timerValue[2] - timerValue[1];
    word diffValue = widthValue - periodValue;
    pwmPeriod = periodValue * 0.0625;
    pwmWidth =  (widthValue - (2 * periodValue)) * 0.0625;
    if (pwmWidth > pwmPeriod)
      pwmWidth -= pwmPeriod;
    pwmDuty = (pwmWidth / pwmPeriod) * 100;
    pwmFrequency = 1000 / pwmPeriod;

    Serial.print("pwmPeriod     ");
    Serial.print(pwmPeriod, 3);
    Serial.println(" us");

    Serial.print("pwmWidth      ");
    Serial.print(pwmWidth, 3);
    Serial.println(" us");

    Serial.print("pwmDuty       ");
    Serial.print(pwmDuty, 3);
    Serial.println(" %");

    Serial.print("pwmFrequency  ");
    Serial.print(pwmFrequency, 3);
    Serial.println(" kHz");
    Serial.println();

    interrupts();
    pwmMeasureBegin ();
  }
}

ISR (TIMER1_CAPT_vect)
{
  switch (testState) {

    case 0:                            // first rising edge
      timerValue[0] = ICR1;
      testState = 1;
      break;

    case 1:                            // second rising edge
      timerValue[1] = ICR1;
      TCCR1B &=  ~bit (ICES1);         // capture on falling edge (pin D8)
      testState = 2;
      break;

    case 2:                            // first falling edge
      testState = 3;
      break;

    case 3:                            // second falling edge
      timerValue[2] = ICR1;
      testState = 4;
      break;

    case 4:                            // third falling edge
      timerValue[3] = ICR1;
      testState = 5;                   // all tests done
      break;
  }
}

void pwmBegin(unsigned int duty) {
  TCCR2A = 0;                               // TC2 Control Register A
  TCCR2B = 0;                               // TC2 Control Register B
  TIMSK2 = 0;                               // TC2 Interrupt Mask Register
  TIFR2 = 0;                                // TC2 Interrupt Flag Register
  TCCR2A |= (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);  // OC2B cleared/set on match when up/down counting, fast PWM
  TCCR2B |= (1 << WGM22) | (1 << CS20);     // no clock prescaler for maximum PWM frequency
  OCR2A = 63;                               // TOP overflow value is 63 producing 250 kHz PWM
  OCR2B = duty;
  pinMode(3, OUTPUT);
}

void pwmMeasureBegin ()
{
  TCCR1A = 0;                               // normal operation mode
  TCCR1B = 0;                               // stop timer clock (no clock source)
  TCNT1  = 0;                               // clear counter
  TIFR1 = bit (ICF1) | bit (TOV1);          // clear flags
  testState = 0;                            // clear testState
  TIMSK1 = bit (ICIE1);                     // interrupt on input capture
  TCCR1B =  bit (CS10) | bit (ICES1);       // start clock with no prescaler, rising edge on pin D8
}

You can low pass filter the signal and then read it with ADC. This should give you duty too.

Smajdalf:
You can low pass filter the signal and then read it with ADC. This should give you duty too.

That is what I want to avoid. I know the hardware solution but that gives me a lower resolution than when using the software solution.

I am a hardware engineer and would have preferred that solution if I was not convinced the software solution is more accurate if used properly (direct register addressing).

If you want best precision you need to use Input Capture feature of Timer/Counter1. This way you can get resolution of 1 main clock tick.

Smajdalf:
If you want best precision you need to use Input Capture feature of Timer/Counter1. This way you can get resolution of 1 main clock tick.

You know better than me, I am absolute novice here : can you give some more detailed help please?
Thanks!!

This is the code where I need the part that generates the PWM to be removed.

// Test PWM period and width measurement (connect pin 3 to pin 8)
// Measurement timing diagram: http://i.imgur.com/NqV4nXe.png

const byte pwmInputPin = 2;
volatile byte testState = 0;
volatile word timerValue[4];
float pwmPeriod, pwmWidth, pwmDuty, pwmFrequency;

void setup()
{
  //pwmBegin(1);                     // 250kHz PWM, range for duty is 1-63
  TIMSK0 = 0;                      // for testing with timer0 disabled
  pwmMeasureBegin();
  Serial.begin(115200);
}

void loop()
{
  if (testState == 5) {            // tests completed
    noInterrupts();
    word periodValue = timerValue[3] - timerValue[2];
    word widthValue = timerValue[2] - timerValue[1];
    word diffValue = widthValue - periodValue;
    pwmPeriod = periodValue * 0.0625;
    pwmWidth =  (widthValue - (2 * periodValue)) * 0.0625;
    if (pwmWidth > pwmPeriod)
      pwmWidth -= pwmPeriod;
    pwmDuty = (pwmWidth / pwmPeriod) * 100;
    pwmFrequency = 1000 / pwmPeriod;

    Serial.print("pwmPeriod     ");
    Serial.print(pwmPeriod, 3);
    Serial.println(" us");

    Serial.print("pwmWidth      ");
    Serial.print(pwmWidth, 3);
    Serial.println(" us");

    Serial.print("pwmDuty       ");
    Serial.print(pwmDuty, 3);
    Serial.println(" %");

    Serial.print("pwmFrequency  ");
    Serial.print(pwmFrequency, 3);
    Serial.println(" kHz");
    Serial.println();

    interrupts();
    pwmMeasureBegin ();
  }
}

ISR (TIMER1_CAPT_vect)
{
  switch (testState) {

    case 0:                            // first rising edge
      timerValue[0] = ICR1;
      testState = 1;
      break;

    case 1:                            // second rising edge
      timerValue[1] = ICR1;
      TCCR1B &=  ~bit (ICES1);         // capture on falling edge (pin D8)
      testState = 2;
      break;

    case 2:                            // first falling edge
      testState = 3;
      break;

    case 3:                            // second falling edge
      timerValue[2] = ICR1;
      testState = 4;
      break;

    case 4:                            // third falling edge
      timerValue[3] = ICR1;
      testState = 5;                   // all tests done
      break;
  }
}
/*
void pwmBegin(unsigned int duty) {
  TCCR2A = 0;                               // TC2 Control Register A
  TCCR2B = 0;                               // TC2 Control Register B
  TIMSK2 = 0;                               // TC2 Interrupt Mask Register
  TIFR2 = 0;                                // TC2 Interrupt Flag Register
  TCCR2A |= (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);  // OC2B cleared/set on match when up/down counting, fast PWM
  TCCR2B |= (1 << WGM22) | (1 << CS20);     // no clock prescaler for maximum PWM frequency
  OCR2A = 63;                               // TOP overflow value is 63 producing 250 kHz PWM
  OCR2B = duty;
  pinMode(3, OUTPUT);
}
*/
void pwmMeasureBegin ()
{
  TCCR1A = 0;                               // normal operation mode
  TCCR1B = 0;                               // stop timer clock (no clock source)
  TCNT1  = 0;                               // clear counter
  TIFR1 = bit (ICF1) | bit (TOV1);          // clear flags
  testState = 0;                            // clear testState
  TIMSK1 = bit (ICIE1);                     // interrupt on input capture
  TCCR1B =  bit (CS10) | bit (ICES1);       // start clock with no prescaler, rising edge on pin D8
}

If you want best precision you need to use Input Capture feature of Timer/Counter1.

The code you posted uses the input capture feature of Timer1.

cattledog:

// Test PWM period and width measurement (connect pin 3 to pin 8)

// Measurement timing diagram: http://i.imgur.com/NqV4nXe.png

const byte pwmInputPin = 2;
volatile byte testState = 0;
volatile word timerValue[4];
float pwmPeriod, pwmWidth, pwmDuty, pwmFrequency;

void setup()
{
  //pwmBegin(1);                    // 250kHz PWM, range for duty is 1-63
  TIMSK0 = 0;                      // for testing with timer0 disabled
  pwmMeasureBegin();
  Serial.begin(115200);
}

void loop()
{
  if (testState == 5) {            // tests completed
    noInterrupts();
    word periodValue = timerValue[3] - timerValue[2];
    word widthValue = timerValue[2] - timerValue[1];
    word diffValue = widthValue - periodValue;
    pwmPeriod = periodValue * 0.0625;
    pwmWidth =  (widthValue - (2 * periodValue)) * 0.0625;
    if (pwmWidth > pwmPeriod)
      pwmWidth -= pwmPeriod;
    pwmDuty = (pwmWidth / pwmPeriod) * 100;
    pwmFrequency = 1000 / pwmPeriod;

Serial.print(“pwmPeriod    “);
    Serial.print(pwmPeriod, 3);
    Serial.println(” us”);

Serial.print(“pwmWidth      “);
    Serial.print(pwmWidth, 3);
    Serial.println(” us”);

Serial.print(“pwmDuty      “);
    Serial.print(pwmDuty, 3);
    Serial.println(” %”);

Serial.print(“pwmFrequency  “);
    Serial.print(pwmFrequency, 3);
    Serial.println(” kHz”);
    Serial.println();

interrupts();
    pwmMeasureBegin ();
  }
}

ISR (TIMER1_CAPT_vect)
{
  switch (testState) {

case 0:                            // first rising edge
      timerValue[0] = ICR1;
      testState = 1;
      break;

case 1:                            // second rising edge
      timerValue[1] = ICR1;
      TCCR1B &=  ~bit (ICES1);        // capture on falling edge (pin D8)
      testState = 2;
      break;

case 2:                            // first falling edge
      testState = 3;
      break;

case 3:                            // second falling edge
      timerValue[2] = ICR1;
      testState = 4;
      break;

case 4:                            // third falling edge
      timerValue[3] = ICR1;
      testState = 5;                  // all tests done
      break;
  }
}
/*
void pwmBegin(unsigned int duty) {
  TCCR2A = 0;                              // TC2 Control Register A
  TCCR2B = 0;                              // TC2 Control Register B
  TIMSK2 = 0;                              // TC2 Interrupt Mask Register
  TIFR2 = 0;                                // TC2 Interrupt Flag Register
  TCCR2A |= (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);  // OC2B cleared/set on match when up/down counting, fast PWM
  TCCR2B |= (1 << WGM22) | (1 << CS20);    // no clock prescaler for maximum PWM frequency
  OCR2A = 63;                              // TOP overflow value is 63 producing 250 kHz PWM
  OCR2B = duty;
  pinMode(3, OUTPUT);
}
*/
void pwmMeasureBegin ()
{
  TCCR1A = 0;                              // normal operation mode
  TCCR1B = 0;                              // stop timer clock (no clock source)
  TCNT1  = 0;                              // clear counter
  TIFR1 = bit (ICF1) | bit (TOV1);          // clear flags
  testState = 0;                            // clear testState
  TIMSK1 = bit (ICIE1);                    // interrupt on input capture
  TCCR1B =  bit (CS10) | bit (ICES1);      // start clock with no prescaler, rising edge on pin D8
}




The code you posted uses the input capture feature of Timer1.

…which is telling of my lack of knowledge on the matter…

Can someone please help in cleaning up this code so that it just “reads” from a digital input where my signal will be applied to? I do not need the PWM generator part.

I would subsequently place that code in a function so that I can refer to that function whenever I need to read the 2.4kHz signal in order to measure the duty cycle.

Can someone please help in cleaning up this code so that it just "reads" from a digital input where my signal will be applied to? I do not need the PWM generator part.

I commented out the generating part in the code I posted.

cattledog:
I commented out the generating part in the code I posted.

You are a star!!

cattledog:
I commented out the generating part in the code I posted.

I want to try and understand the code. Where would be a good starting place to be delving into the subject?

Try look into the Datasheet at description of the Timer1.

Smajdalf:
Try look into the Datasheet at description of the Timer1.

Ok. I checked the 660 page manual and found the chapter 12 p57 Interrupts. Tough reading, but sheds a bit of light, to start.
In the above code I also find for example TCCR1A, TIMSK0 = 0; ISR (TIMER1_CAPT_vect) , TCCR1B &= ~bit (ICES1); ...

... totally new to me.
Where should I start reading up on that?

I have always found that Nick Gammon's tutorials are worth reading. There is a good example of an input capture code for frequency/period. The Timer setup cheat sheets in reply #18 on page 2 are a very handy reference.

http://www.gammon.com.au/timers

In my datasheet (also 660 pages long) is chapter 16 starting on page 111. It describes behavior of the Timer1, at the end of the chapter there is decription of registers and the bit values.

Smajdalf:
In my datasheet (also 660 pages long) is chapter 16 starting on page 111. It describes behavior of the Timer1, at the end of the chapter there is decription of registers and the bit values.

Thanks! I obviously looked in the wrong place; now I have some evening lecture.

There are some practical examples of timer usage in Embedded C Programming and the Atmel AVR. The linked version should be more current than the 2009 hardcopy I just had on loan from the library.

cattledog:
I commented out the generating part in the code I posted.

Hi cattledog, something is wrong with the current code and I am lost when trying to understand it.

When I apply a 2.4kHz square wave to pin 8 I get results of 700 to 800% duty cycle…

The frequency shown in the serial monitor is correct, not the calculated duty cycle.

What is wrong?

// https://forum.arduino.cc/index.php?topic=410853.15
// http://forum.arduino.cc/index.php?topic=507719.0
// Test PWM period and width measurement (connect pin 3 to pin 8)
// Measurement timing diagram: http://i.imgur.com/NqV4nXe.png
// code without the PWM generating part

const byte pwmInputPin = 2;
volatile byte testState = 0;
volatile word timerValue[4];
float pwmPeriod, pwmWidth, pwmDuty, pwmFrequency;

void setup()
{
  //pwmBegin(1);                     // 250kHz PWM, range for duty is 1-63
  TIMSK0 = 0;                      // for testing with timer0 disabled
  pwmMeasureBegin();
  Serial.begin(115200);
}

void loop()
{
  if (testState == 5) {            // tests completed
    noInterrupts();
    word periodValue = timerValue[3] - timerValue[2];
    word widthValue = timerValue[2] - timerValue[1];
    word diffValue = widthValue - periodValue;
    pwmPeriod = periodValue * 0.0625;
    pwmWidth =  (widthValue - (2 * periodValue)) * 0.0625;
    if (pwmWidth > pwmPeriod)
      pwmWidth -= pwmPeriod;
    pwmDuty = (pwmWidth / pwmPeriod) * 100;
    pwmFrequency = 1000 / pwmPeriod;

    Serial.print("pwmPeriod     ");
    Serial.print(pwmPeriod, 3);
    Serial.println(" us");

    Serial.print("pwmWidth      ");
    Serial.print(pwmWidth, 3);
    Serial.println(" us");

    Serial.print("pwmDuty       ");
    Serial.print(pwmDuty, 3);
    Serial.println(" %");

    Serial.print("pwmFrequency  ");
    Serial.print(pwmFrequency, 3);
    Serial.println(" kHz");
    Serial.println();

    interrupts();
    pwmMeasureBegin ();
  }
}

ISR (TIMER1_CAPT_vect)
{
  switch (testState) {

    case 0:                            // first rising edge
      timerValue[0] = ICR1;
      testState = 1;
      break;

    case 1:                            // second rising edge
      timerValue[1] = ICR1;
      TCCR1B &=  ~bit (ICES1);         // capture on falling edge (pin D8)
      testState = 2;
      break;

    case 2:                            // first falling edge
      testState = 3;
      break;

    case 3:                            // second falling edge
      timerValue[2] = ICR1;
      testState = 4;
      break;

    case 4:                            // third falling edge
      timerValue[3] = ICR1;
      testState = 5;                   // all tests done
      break;
  }
}
/*
void pwmBegin(unsigned int duty) {
  TCCR2A = 0;                               // TC2 Control Register A
  TCCR2B = 0;                               // TC2 Control Register B
  TIMSK2 = 0;                               // TC2 Interrupt Mask Register
  TIFR2 = 0;                                // TC2 Interrupt Flag Register
  TCCR2A |= (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);  // OC2B cleared/set on match when up/down counting, fast PWM
  TCCR2B |= (1 << WGM22) | (1 << CS20);     // no clock prescaler for maximum PWM frequency
  OCR2A = 63;                               // TOP overflow value is 63 producing 250 kHz PWM
  OCR2B = duty;
  pinMode(3, OUTPUT);
}
*/
void pwmMeasureBegin ()
{
  TCCR1A = 0;                               // normal operation mode
  TCCR1B = 0;                               // stop timer clock (no clock source)
  TCNT1  = 0;                               // clear counter
  TIFR1 = bit (ICF1) | bit (TOV1);          // clear flags
  testState = 0;                            // clear testState
  TIMSK1 = bit (ICIE1);                     // interrupt on input capture
  TCCR1B =  bit (CS10) | bit (ICES1);       // start clock with no prescaler, rising edge on pin D8
}