Measure of Duty Cicle of a PWM signal

Hi,

I want to read de the duty cicle of a PWM signal with Arduino.
I would like to use interrupts to detects the change of high to low and low to high.
I worry if it's possible to use the timers of arduino as counters to read the time which the signal is in high and low.

Thanks!.

Maybe something like the attached will help as a starting point.

Frequency_Counter.ino (2.73 KB)

Riva:
Maybe something like the attached will help as a starting point.

Yes, thanks, with that i can detect the change of flank, the problem is that micros() function can be too slow to read the duty cicle with high accuracy.

What is the frequency range of your PWM signal ?

Rolaro:
Yes, thanks, with that i can detect the change of flank, the problem is that micros() function can be too slow to read the duty cicle with high accuracy.

If micros() is to slow for the resolution your looking for then you need to move to a faster MCU or some external hardware to do the timing. IIRC the AVR micro() function updates in steps of 4 so your getting errors to begin with without the code overhead.

Rolaro:
Yes, thanks, with that i can detect the change of flank, the problem is that micros() function can be too slow to read the duty cicle with high accuracy.

Hi Rolaro,

You are correct = the function micros() always comes back with a value that is a multiple of 4! This imposes a limit of 250KHz on even a hypothetical no-time sketch. But of course the sketch time adds significantly to the overhead. The maximum frequency and duty cycle precision are adversely affected.

However, this code is not ideal. It spends way too much time in the interrupt service routine! Plus it accesses three volatile variables. Volatile variables are read/written slower than non-volatile variables. ISR code can access byte sized arguments (aka “atomic” variables) in a single cycle, so they need not be declared volatile, as they can’t change mid-cycle.

The ISR should be super simple. Like set a single atomic variable and exit. Do everything else in the loop() function. The following code does this and reads a frequency on pin 2 (and ground!) up to 13KHz.

// UNO, pwm signal to pin 2 + common ground
// 050518 clh 2630/252 show frequency and duty cycle

const byte inputPin = 2;  // INT.0

boolean inputState = false;
boolean lastInputState = false;

unsigned long pulseLength = 0;
unsigned long lastPulseLength = 0;
unsigned long startMicros;
unsigned long timeBetween = 0;
unsigned long lastTimeBetween = 0;
unsigned long stopMicros;

unsigned long previousDisplayMillis = millis();
long displayMillis = 1000L;

void setup() {
  pinMode(inputPin, INPUT_PULLUP);  // pin <-- 100R <-- vibePin

  Serial.begin(115200);

  attachInterrupt(digitalPinToInterrupt(inputPin), setInputState, CHANGE);
}

void setInputState() {
//  inputState = digitalRead(inputPin);
  inputState = PIND & 0b0100;
}

void loop() {

  if (inputState != lastInputState) {
    if (pulseDone()) {
      lastPulseLength = pulseLength;
    } else {
      lastTimeBetween = timeBetween;
    }
    lastInputState = inputState;
  }

  // --------- Run this code every second ------------------
  if (millis() - previousDisplayMillis >= displayMillis) {
    previousDisplayMillis += displayMillis;

    unsigned long period = (lastPulseLength + lastTimeBetween);
    long frequency = 1000000L / period;
    byte dutyCycle = ((100L * lastPulseLength) / period) + 1;

    Serial.print("Frequency: ");
    Serial.print(frequency);
    Serial.print(" Hz | Duty Cycle: ");
    Serial.print(dutyCycle);
    Serial.println('%');
  }
}

// If pulseDone() is true, the variable pulseLength is valid.
// If false, then timeBetween is valid, but pulseLength is 0.
boolean pulseDone () {
  unsigned long nowMicros = micros();  // resolution of micros() is 4µs, so *
  if (inputState) {             // rising edge of the pulse.
    startMicros = nowMicros;    // start the microseconds clock
    pulseLength = 0UL;

    timeBetween = (nowMicros - stopMicros);
    return false;
  }
  else {                        // falling edge - stop clock
    stopMicros = nowMicros;    // start the microseconds clock
    timeBetween = 0UL;

    pulseLength = (nowMicros - startMicros);
    return true;
  }
}

Also, if you just check frequency, skipping duty cycle, and completely forgo using interrupts, you can measure up to 60-ish KHz. With a regularly timed event, the interrupt code that runs behind the scenes interferes, and actually slows things down near the physical limits of the machine.

const byte inputPin = 2;  // INT.0

boolean inputState = false;
boolean lastInputState = false;
long count = 0UL;

unsigned long previousDisplayMillis = millis();
const long displayMillis = 500L;

void setup() {
  pinMode(inputPin, INPUT_PULLUP);

  Serial.begin(115200);
}

void loop() {
  inputState = PIND & 4;  //  faster than: inputState = digitalRead(inputPin);
  if (inputState != lastInputState) {
    count++;
    lastInputState = inputState;
  }

  // --------- Run this code every *half* second ------------------
  if (millis() - previousDisplayMillis >= displayMillis) {
    previousDisplayMillis += displayMillis;

    Serial.print(count);   //   *** SERIAL PRINT ***
    Serial.print(" Hz");   //   *** SERIAL PRINT ***
    Serial.print('\n');    //   *** SERIAL PRINT ***
    
    count = 0UL;

  }
}

Although not specifically using Arduino, my project may contain a few ideas about measuring duty cycle, espcially at high frequencies: