Resolving timing conflicts with Neopixels and IR signal processing

4 minute read

I’ve been teaching my kids about microcontroller programming this year as part of our homeschool curriculum. For a fun holiday project we’ve added a strip of 60 Neopixels to our project. Learning to animate them has been a fun project, and casts fun lights all over our Airstream home.

irremote.jpg

Our microcontroller for our learning is a Plumduino, which has an IR sensor and IR remote attached to it. Our remote came with our Plumduino package, but is similar to this one on Amazon. We’ve written some code to change animations from the remote. A simplified version of our main loop program looks like this:

void loop(){
  //handle IR
  if (irrecv.decode(&results)) {
    switch (results.value) {
      case IR_Up:
        offset_delta = 10;
        break;
      case IR_Down:
        offset_delta = 1;
        break;
    }
    irrecv.resume(); // Receive the next value
  }

  // play animation
  offset += offset_delta;
  fill_solid(leds, NUM_LEDS, CHSV(offset, 255, 100));

  // send instructions to Neopixels
  FastLED.show();

  // standard delay for 30 FPS
  delay(30);
}

The problem that we experience is very unreliable IR signal processing. The signal is received, but is nothing like the expected message after decoding. The remote has kind-of worked, but it has been frustruating. I have finally figured how the actual cause of the problem and a solution. First, we need a little background.

IR Sensing with Arduino-IRRemote

My library of choice for sensing and processing Infrared Remote signals is Arduino-IRremote. The library is easy to work with. It uses interrupts to measure the timing of of the rising and falling edges of the incoming IR signal. After a complete message has been received, it is decoded and made available for use. IR remotes take about 65 milliseconds to send a complete signal.

Neopixel Control with FastLED

Neopixels are the common name of the versatile WS2812 LEDs. It’s use of only one signal wire requires a very specific signal timing. Neopixel libraries accomplish this specific timing by disabling interrupts on the processor during the time the signal is sent down the control wire. Luckily, this is quite fast. It takes about 3 milliseconds to update 100 neopixels.

Timing is Everything

The problem occurs when FastLED.show() is called in the middle of an IR transmission. FastLED shuts off interrupts during the 3ms it uses to send the signal to the LEDs. When the interrupts are re-enabled afterwards, the missing few milliseconds of interrupts causes the IR library to misinterpret the IR signal, resulting in a corrupted message. As you can see from our loop() function above, we repeat the loop approximately every 33ms. This causes the IR message to be interrupted twice during the 65ms IR transmission. Under these timings, the IR message is never received properly. If we lengthen our delay(30) to delay(100) or greater, we can increase the chance that a 65ms IR signal can be received without corruption. This delay does slow down our animation and a significant number of IR messages are still corrupted. Repeatedly pressing a remote button and not seeing the desired change can be quite frustruating. This is especially true for a kid who is not sure their code is going to work anyway.

The Solution

The way to make these two timing specific processes get along is to avoid having them run at the same time. Newer versions of the library have an isIdle() method that is perfect for the job. I’m still using the slowly dying codebender, and they have an older version. I wrote the following function that detects the absense of an ongoing IR message.

bool IR_idle() {
  return irrecv.decode(&results) || results.rawlen == 0;
}

This method returns true if no IR message is in process. irrecv.decode(&results) returns true if a received message is completed, and also populates a few members in the results struct. If a message has not completed, we check the length of the IR timing buffer results.rawlen. If an IR message has started that buffer will be non-zero. A true result of this function indicates that a message has been fully received, or no message has been started.

Rather than mindlessly calling FastLED.show() and potentially disrupting an IR message, we can replace that call with a guarded call like this:

  if (IR_idle()) {
    FastLED.show();
  } else {
    Serial.println("Skipped FastLED.show()");
  }

This avoids interrupting an IR message with an interrupt disabling library call. The else clause and Serial.println() is optional of course, but is sure useful in testing to detect when the guarded call prevents a timing accident.

Note: if you are using the newer library version with the built in isIdle(), it would look like this:

  if (irrecv.isIdle()) {
    FastLED.show();
  } else {
    Serial.println("Skipped FastLED.show()");
  }

Using this method, my IR remote has become extremely reliable without unnecessary slowing of my animation.

Other Uses

This same technique can be used with other interrupt-using libraries. The method of detecting an in-process message will vary, but the results should be the same. If you happen to be building a library that uses interrupts, please help everyone by adding a similar idle method.

irremoteairstream.jpg

Full Code Sample

For completeness, here is a full example program, with my fix in place.

#include <FastLED.h>
#include <IRremote.h>

#define IR_Recv 3
#define DATA_PIN 4    //pixel pin, used for FastLED Library
#define NUM_LEDS    68
#define SERIAL_SPEED 115200

#define IR_Up 0xFF02FD
#define IR_Down 0xFF9867

CRGB leds[NUM_LEDS];        //array to hold FastLED pixel data

IRrecv irrecv(IR_Recv);
decode_results results;

//variables used
byte offset = 0;
byte offset_delta = 1;

void setup()
{
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);  //start FastLED

  irrecv.enableIRIn(); // Start the receiver

  Serial.begin(SERIAL_SPEED);
}

bool IR_idle() {
  return irrecv.decode(&results) || results.rawlen == 0;
}

void loop(){
  //handle IR
  if (irrecv.decode(&results)) {
    switch (results.value) {
      case IR_Up:
        offset_delta = 10;
        break;
      case IR_Down:
        offset_delta = 1;
        break;
    }
    irrecv.resume(); // Receive the next value
  }

  // play animation
  offset += offset_delta;
  fill_solid(leds, NUM_LEDS, CHSV(offset, 255, 100));

  //FastLED.show();

  if (IR_idle()) {
    FastLED.show();
  } else {
    Serial.println("skipped FastLED show()");
  }

  //standard delay for 30 FPS
  delay(30);
}

Updated: