Ambitious, but rubbish

Stupid simple interrupt-driver IR remote decoder for ATtiny/ATmega

written by Gia Ferrari on 2012-09-22

Quick one. For a project, I needed a really simple IR decoder program for an ATtiny85 that (a) Used interrupts for pulse processing, but (b) didn't use a timer interrupt, and (c) worked with an apple remote I had laying around. None of the libraries I found did this (mostly b, plus none really had the 85 in mind), so I whipped up my own. It's stupid. It's simple. It works well enough for me. Just whack your IR decoder's output to the 85's physical pin 3, hook a 9600,8,n,1 uart input to physical pin 2, press buttons on your remote, and marvel at the numbers and stuff appearing on your screen. Enough blab, here's the goods. This should work fine with pretty much any AVR, as long as you fix up the interrupt configuration in setup() for your particular chip.

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.

/* SuperSimpleIR -
 * Bare-minimum no-frills code to interpret input from an IR decoder, using pin change interrupts.
 * Does no fancy decoding - it's up to you to figure out which button corresponds with the
 * output from this snippet. The provided message codes work with an Apple remote, but this is by
 * no means limited to such remotes (for instance, my Yamaha receiver's remote works fine with this).
 * Possible improvements:
 * - Use a timer interrupt instead of timeToCommitMessage. I didn't bother
 *   implementing this as the project I developed this for will need the timer for other tasks.
 * - The message commit code is (barely) vulnerable to concurrency issues if a pulse comes in while
 *   we try to commit a message. In practice I don't think this matters.
 * - It gets confused easily if the signal gets weak and corrupted.

// From the Tiny core.
#include <TinyDebugSerial.h>

TinyDebugSerial mySerial;

// Pin defs.
#define IN_IR 4 // If this is changed, change the code that enables interrupts.

#define IR_MSG_PLUS               2011287570
#define IR_MSG_MINUS              2011279378
#define IR_MSG_REW                2011238418
#define IR_MSG_FFWD               2011291666
#define IR_MSG_PLAYPAUSE          2011242514
#define IR_MSG_MENU               2011250706

// After this much time has elapsed after the last transition, consider the
// message complete.

enum IRProtocolState { Idle, Building };

unsigned long timeOfLastIRPinChange;  // Time when the last pin change happened on the IR decoder.
unsigned long timeToCommitMessage;    // Time after which we should consider the message complete.
unsigned long message;                // Workspace where the message is built up.
IRProtocolState irProtocolState;      // What our protocol decoder is doing.

void processIRMessage(long message);

ISR(PCINT0_vect) {
  boolean isPinHigh = digitalRead(IN_IR);
  long currentTime = micros();
  long microsSinceLastChange = (currentTime - timeOfLastIRPinChange);
  timeOfLastIRPinChange = currentTime;

  // Bump up the commit time, since we got a level change.
  timeToCommitMessage = MSG_COMMIT_TIME_MICROSEC + timeOfLastIRPinChange;

  if (!isPinHigh)
     // Going low.
    if (microsSinceLastChange < 700)
      // Space
      message = message << 1;
    else if (microsSinceLastChange < 2000)
      // Mark.
      message = message << 1;
      message |= 1;
    else if (microsSinceLastChange < 20000)
      // First LOW.
      message = 0;
      irProtocolState = Building;


void setup()
  mySerial.println("IR test up.");

  timeOfLastIRPinChange = micros();
  timeToCommitMessage = -1;
  irProtocolState = Idle;

  GIMSK = _BV(PCIE);    // Enable pin change interrupt
  PCMSK = _BV(PCINT4);  // Enable the interrupt for only pin 4.

void loop() {
  // Check to see if we can commit a message. If so, process it.
  if (micros() >= timeToCommitMessage && irProtocolState == Building)
    long localMessage = message;
    message = 0;
    irProtocolState = Idle;

void processIRMessage(long message)
  switch (message)
      case IR_MSG_PLUS: mySerial.println("Plus"); break;
      case IR_MSG_MINUS: mySerial.println("Minus"); break;
      case IR_MSG_REW: mySerial.println("REW"); break;
      case IR_MSG_FFWD: mySerial.println("FFWD"); break;
      case IR_MSG_PLAYPAUSE: mySerial.println("Play/Pause"); break;
      case IR_MSG_MENU: mySerial.println("Menu"); break;
      case IR_MSG_REPEAT_LAST_BUTTON: mySerial.println("(repeat last)"); break;
      default: mySerial.print("Unknown code: "); mySerial.println(message); break;