Custom Search

Wednesday, October 7, 2015

A close look at pwm input

Motivation

PWM is used for things like controlling servos, motors and LEDs. Output from a micro-controller is easy, and the hardware usually handles it. Remote control receivers also output it, as they are used to control these things as well. RC transmitters often output the closely related PPM (aka CPPM). It's not unusual to want to read those values with an Arduino microcontroller, but this is not as easy, as common - meaning ATmega328 and similar - hardware doesn't do it directly. So let's look at some options to do that.

Code can be found in the blog repository, in the Arduino/PWM_input folder.
A word of warning here. All the method I'm discussing are fine for RC usage. They are not suitable for full range PWM signals! RC signals - even at their extremes - are always between 4 and 2.5 milliseconds of a roughly 20 millisecond frame. Full range signals can go from 0 milliseconds - no pulse - to the full frame length - all pulse. None of these methods deals with those two cases.

The options

If you look on the web, you'll find things like this, listing some of the options. I've as yet to see all four I know of in the same place, in part because the fastest of them isn't supported by an Arduino library. So I'll discuss them all here, providing code examples for the harder three.

pulseIn

pulseIn is the easy option. It works fine if you don't mind having your CPU tied up to do the input, and can do everything else between pulses or don't mind missing a pulse. I'm not going to spend much more time on this.

Pin change interrupts

This is the most general solution. Just set up an interrupt on pin changes, and then when it changes record the current time on a rising edge, and subtract that value from the time on the falling edge to get the PWM pulse length. So, here's my code:
#include <EnableInterrupt.h>

#define MY_PIN 5 // we could choose any pin

uint16_t pwm_value = 0;

void change() {
  static unsigned long prev_time = 0;

  if (digitalRead(MY_PIN))
    prev_time = micros();
  else
    pwm_value = micros() - prev_time;
}

void setup() {
  pinMode(MY_PIN, INPUT_PULLUP);
  enableInterrupt(MY_PIN, &change, CHANGE);
  Serial.begin(115200);
}

void loop() {
  uint16_t pwmin;

  noInterrupts();
  pwmin = pwm_value ;
  interrupts();  

  Serial.println(pwmin);
  delay(500);
}
To run this, you'll need the enableInterrupt library installed.
If you compare this to the version I referenced earlier, you'll notice I used the enableInterrupt library instead of the pinChangeInt library. The latter has recently been depreciated in favor of the former.
I also used an if in an ISR that handles both cases instead of one ISR per case and changing the interrupt each time. I presume this is because attachInterrupt is faster than digitalRead, as the two interrupts are generated in the same amount of time. This is an artifact of the Arduino Hardware Abstraction Layer (HAL), which has to translate from an Arduino pin number to an AVR port and bit number to read it's value. If you accessed the hardware directly, things would be different. Given the amount of time lost just by using the Arduino HAL, I decided to go with the more maintainable code.

External interrupts

External interrupts are essentially identical to pin change interrupts. You arrange to get an interrupt when a pin changes, save the time on a rising edge and calculate the interval on a falling edge. The difference is in how the interrupts are handled in the hardware, and that's all hidden by the HAL. Common Arduino boards only have two external interrupts, each of which can occur on a single pin. Pin change interrupts have three interrupts, each shared by 7 or 8 pins, which the HAL sorts outs for you.
In any case, here's the code:
#define MY_PIN 3 // Must be pin 2 or 3

// Work around bug in Arduino 1.0.6
#define NOT_AN_INTERRUPT (-1)

uint16_t pwm_value = 0;

void change() {
  static unsigned long prev_time = 0;

  if (digitalRead(MY_PIN))
    prev_time = micros();
  else
    pwm_value = micros() - prev_time;
}

void setup() {
  pinMode(MY_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(MY_PIN), change, CHANGE);
  Serial.begin(115200);
}

void loop() {
  uint16_t pwmin;

  noInterrupts();
  pwmin = pwm_value;
  interrupts();  

  Serial.println(pwmin);
  delay(500);
}
If you can arrange to use an external interrupt, they will be faster, though the difference is negligible compared to the overhead of using the Arduino HAL. They also don't require that extra library install. I recommend using them if possible, but don't stress over it.

Input capture interrupts

There's another thing consuming in both of these routines: micros()! While it's doesn't seem like much, it has to disable interrupts to safely copy the current value into your variable - which is a waste, since they are disabled by being in the interrupt handler.
Turns out there's a way to get rid of that, though. The input capture interrupt will snapshot the value of a timer when an interrupt happens. Unfortunately, this hardware capability isn't wrapped by the Arduino HAL, so you have to implement things by hand. The upside of that is that it'll be a lot faster than the HAL version, as most of the HAL calls wind up just needing a few instructions.
The more important downside is that doing this uses the 328P's single 16 bit timer. So you have to use a specific pin and lose the two PWM outputs that use that timer.
Here's the code:
#define MY_PIN 8        // Must be pin 8 on 328P's.

static uint16_t pulse_length;   // in ticks

#define ICESB _BV(ICES1)

ISR(TIMER1_CAPT_vect) {
  if (TCCR1B & ICESB)           // On rising edge, start of pulse & frame
    TCNT1 = 0;          // Reset the counter
  else              // Falling edge
    pulse_length = ICR1;    // Save pulse length in ticks

  TCCR1B ^= ICESB;      // Detect other edge next time
  TIFR1 |= _BV(ICF1);
}

void setup() {
  TCCR1A = 0 ;          // Not doing anything here.
  TCCR1B = _BV(CS11) | ICESB;   // Enable with rising edge capture, prescaler 8.
  TIMSK1 = _BV(ICIE1);      // And unmask this interrupts.

  pinMode(MY_PIN, INPUT_PULLUP);
  Serial.begin(115200);
}

void loop() {
  uint16_t pwm_value;

  TIMSK1 &= ~_BV(ICIE1);    // Turn off my interrupt to grab the value
  pwm_value = pulse_length;
  TIMSK1 |= _BV(ICIE1);

  Serial.println(clockCyclesToMicroseconds(pwm_value * 8));
  delay(500);
}
A couple of things to note. The values saved by this routine isn't milliseconds, but clock ticks at 8 per count. So we use the HAL function clockCyclesToMicroSeconds to turn that into the value we want. Second, that tick count is only reset on a rising edge. You could get the pulse length if you wanted to do something like calculate the duty cycle as a percentage or some such by reading it from ICR1 when we reset TCNT1. Doing that when we're reading values from micros is a bit more complicated, as we need the old and new values of prev_time to calculate it. Finally, in the main routine, we don't need to disable all interrupts via the HAL, but just the one we're using

Summary

pulseIn works, but only if you can do everything else that needs doing between pulses. In particular, other input devices that need prompt handling cause problems. And this method doesn't work if you want to handle C/PPM inputs, which use a much larger percentage of the frame, even for RC.
Pin change interrupts are the most flexible solution, but require coordination with other things that might be using them since multiple pins go through the same interrupt vector. I may well use this. On the Uno, all the pins are available, but that isn't true on newer Arduinos. Check the docs.
External interrupts get a dedicated interrupt vector, so will be slightly faster than pin change interrupts. But that also means the number of pins that can use them is limited. This is probably my choice for applications that don't need every ยต-second.
Finally, the input capture interrupt provides a very fast alternative, but the pin choices are even more limited, and you lose a couple of PWM output pins since it uses a timer. And the reason it's fast is that you don't have the convenience of the Arduino HAL. That could be done with the others two choices as well. And pulseIn, for that matter, but that would just mean you're waiting very quickly.