Using IR Remotes in Place of Mechanical Switches
-
Physical switches and pushbuttons are commonly used to control devices and
influence program flow. Each physical switch requires its own GPIO input;
for example, five switches consume five Arduino input pins.
In addition, mechanical switches are prone to contact bounce and are best
handled by detecting "state changes" rather than continuously reading their
current state. -
An IR remote provides a flexible alternative to hard-wired switches.
For example, a 44-button IR remote requires only a single Arduino input pin.
To provide user feedback, a piezo speaker may be added to emit a short beep
whenever a command is received. -
The following discussion explains how we can use an IR remote in place of physical switches in our projects.
- Below is a schematic of the circuit we will use in this project.
-
Before we get into the required software, we will make a keypad overlay.
-
We will use an NEC IR remote for our keypad.
3 versions are referred to herein. -
We use a drawing program to create a 1 to 1 inch keypad overlay.
You can print this PDF at actual size to make the 44 button overlay discussed.
44 Button Overlay.pdf (76.4 KB)
- The following sketch is written as a reference tutorial.
This tutorial demonstrates how to use an inexpensive IR remote control
as a flexible replacement for multiple physical buttons or switches
using an Arduino UNO and the IRremote library.
//
//================================================^================================================
// IR_Remote_44_Tutorial_TinyIRReceiver
//================================================^================================================
//
// https://forum.arduino.cc/t/using-ir-remotes-in-place-of-mechanical-switches/1425851
//
// LarryD
//
// Version YY/MM/DD Comments
// ======= ======== ========================================================================
// 1.00 25/09/14 Running code
// 1.01 25/11/14 Added SHORT and LONG press detection.
// 1.02 26/01/18 Added the suggestion in Post #2 @J-M-L from the above URL.
// 1.03 26/01/22 Fixed some minor bugs.
//
// R E F E R E N C E T U T O R I A L :
// This tutorial demonstrates how to use an inexpensive IR remote control
// as a flexible replacement for multiple physical buttons or switches
// using an Arduino UNO and the IRremote library.
//
// Instead of consuming one GPIO pin per switch/button, an IR remote allows
// dozens of “virtual buttons” to be handled through a single input pin.
// This sketch shows a clean, scalable way to:
// - receive IR commands using interrupts
// - distinguish between short and long button presses
// - map raw IR codes to readable button symbols
// - cleanly separate hardware handling from application logic
//
// This sketch is intended both as a learning example and as a
// drop-in foundation for your own projects.
//
// T A R G E T A U D I E N C E :
// - Arduino users who are comfortable with:
// * digitalRead(), digitalWrite(), and analogWrite(), Arrays
// * using the Serial Monitor
// * how millis() can be used to make a non-blocking TIMER
// - No prior interrupt experience is required.
// (Interrupts are used internally and explained conceptually)
// - Tested on Arduino UNO (ATmega328P).
//
// Q U I C K S T A R T :
// 1. Install "Arduino-IRremote" library (Version 4.5 or later).
// 2. Wire all components as per accompanying schematic.
// IRremoteTutorial_WorkSheet.jpg
// 3. Select your NEC remote by uncommenting ONE define:
// #define Remote17
// 4. Upload sketch and open Serial Monitor at 115200 baud.
//
// E x a m p l e :
// When a remote button is pressed:
// 1. The IR receiver detects the signal.
// 2. An interrupt is triggered immediately.
// 3. The Interrupt Service Routine (ISR) decodes the command
// and stores it in a shared data structure.
// 4. A flag is set to indicate that a new command is available.
// 5. The main loop() checks for this flag.
// 6. The button press is classified as either SHORT or LONG.
// 7. The command is processed and the appropriate action occurs
// (LEDs change, PWM adjusts, variables update, program flow is influenced).
//
// Conceptual diagram:
// IR Remote
// ↓
// TSOP4838
// ↓ (interrupt)
// TinyIRReceiver ISR
// ↓
// Flags + Command
// ↓
// loop()
// ↓
// serviceButtonPress()
// ↓
// processButtonPress()
//
// N E X T S T E P S :
// - Add more remote buttons and map them to new features.
// - Replace LEDs with relays, motors, or displays.
// - Integrate this sketch into an existing project.
// - Add menus, modes, or configuration screens.
//
// P R E A M B L E :
// Physical switches and pushbuttons are commonly used to control devices and
// influence program flow. Each physical switch requires its own GPIO input;
// for example, five switches consume five Arduino input pins.
// In addition, mechanical switches are prone to contact bounce and are best
// handled by detecting "state changes" rather than continuously reading their
// current state.
//
// An IR remote provides a flexible alternative to hard-wired switches.
// For example, a 44-button IR remote requires only a single Arduino input pin.
// To provide user feedback, a piezo speaker may be added to emit a short beep
// whenever a command is received.
//
// Each remote IR button press sends a HEX command to the Arduino.
// We translate that HEX code into a human-readable "button"
// so the rest of the sketch never deals with raw IR values.
//
// All remote button presses are decoded by a single IR handler, greatly
// simplifying the sketch logic. A simple switch/case construct can then be
// used to control program behavior.
//
// Hardware construction is also simplified: only one opening is required in
// the enclosure for the IR receiver. This reduces internal wiring, allows for
// a smaller enclosure, improves execution efficiency, and makes it easy to
// add new “virtual switches.” The IR remote also enables remote operation
// (for example, from 10 feet away).
//
// With minor modifications, this sketch can be used as drop-in code for existing projects.
//
// O P E R A T I O N :
// "Using the Arduino UNO"
// A TSOP4838 IR receiver is connected to GPIO pin 2 (IR_RECEIVE_PIN).
// IR receive commands are demodulated by this module.
// Any controller GPIO pin can be connected to the IR module output.
// The module has a built-in pull-up resistor.
// A Piezo speaker is connected to GPIO pin 7.
// A short 3400Hz burst is generated when a new IR command is received.
// A heartbeat LED, connected to GPIO pin 13, toggles every 500 ms.
// It is also used by the IR library for IR feedback.
//
// Button mappings:
// - Button '1' Increases PWM duty cycle on LED1 (PWM pin 9);
// supports short and long presses.
// - Button '2' Decreases PWM duty cycle on LED1 (PWM pin 9);
// supports short and long presses.
// - Button '3' Toggles LED2 on GPIO pin 8;
// supports short presses.
// - Up Arrow Menu navigation; short and long press supported.
// - Down Arrow Menu navigation; short and long press supported.
// - Left/Right Handled similarly to the Up and Down arrows.
//
// N O T E S :
// The TinyIRReceiver.hpp is used in this example. It is part of the "IRremote library".
// Install the latest version, as of this writing, version 4.5 is available.
// https://github.com/Arduino-IRremote/Arduino-IRremote/tree/master
// To install this library, use "Include Libraries/Manage Libraries" under the "Sketch" tab (IDE 1.8.18).
// TinyIRReceiver.hpp is being used as it has a small footprint.
// This library is designed for NEC remotes. It is based on interrupts not timers, fully compatible with
// tone() and analogWrite()
//
// Selected library functions and flags:
// - initPCIInterruptForTinyReceiver()
// Initializes and enables interrupt generation on IR input changes.
// - disablePCIInterruptForTinyReceiver()
// Disables interrupt generation on IR input changes.
// - enablePCIInterruptForTinyReceiver()
// Re-enables interrupt generation on IR input changes.
// - TinyIRReceiverData.justWritten
// TRUE indicates a newly decoded IR command.
// Set this flag to FALSE immediately after processing.
// - TinyIRReceiverData.Command
// Holds the received IR command.
// - TinyIRReceiverData.Flags
// Contains status flags for the received command.
// IRDATA_FLAGS_IS_REPEAT indicates a repeat frame.
//
// R E F E R E N C E :
// Tiny NEC receiver and sender:
// https://github.com/Arduino-IRremote/Arduino-IRremote?tab=readme-ov-file#tiny-nec-receiver-and-sender
//
// TSOP4838 IR receiver:
// https://www.vishay.com/docs/82459/tsop48.pdf
//
//================================================^================================================
//Leave these "defines" at this location. i.e. before line: #include "TinyIRReceiver.hpp"
//
//#define IR_FEEDBACK_LED_PIN 13 //Defaults to 13.
//#define NO_LED_FEEDBACK_CODE //Disables LED feedback.
#define IR_RECEIVE_PIN 2 //Defaults to 2.
#define USE_CALLBACK_FOR_TINY_RECEIVER //Allow the user ISR callback function to execute.
//==================================
#include <TinyIRReceiver.hpp> //For use with NEC IR remotes.
#include <avr/pgmspace.h>
//================================================^================================================
//Four (4) NEC IR keypad configurations are defined for use.
//Select which IR keypad we are using; these arrays are stored in flash memory.
//
//===================
//
//#define Portrait44
//#define Landscape44
#define Remote17
//#define Remote21
#if !defined(Portrait44) && !defined(Landscape44) && !defined(Remote17) && !defined(Remote21)
#error "No IR remote type selected"
#endif
//Button Mapping Tables
//Each row contains:
// [0] = raw IR command code (hex value sent by the remote)
// [1] = a readable symbol (ASCII character assigned by the user)
//
//Example:
// {0x5C, '1'} → when IR code 0x5C is received, treat it like button '1'
//
//PROGMEM stores these tables in FLASH memory instead of RAM,
//which is important on small boards like the Arduino UNO.
//==================================
//Portrait keypad definition for a 44 button NEC IR remote.
#if defined(Portrait44)
const byte buttonCode[][2] PROGMEM =
{
{0x5C, '1' }, {0x5D, '2' }, {0x41, '3' }, {0x40, '4' },
{0x58, '5' }, {0x59, '6' }, {0x45, '7' }, {0x44, '8' },
{0x54, '9' }, {0x55, '0' }, {0x49, '-' }, {0x48, 0x20},
{0x50, 'A' }, {0x51, 'B' }, {0x4D, 'C' }, {0x4C, 'D' },
{0x1C, 'E' }, {0x1D, 'F' }, {0x1E, 'G' }, {0x1F, 'H' },
{0x18, 'I' }, {0x19, 'J' }, {0x1A, 'K' }, {0x1B, 'L' },
{0x14, 'M' }, {0x15, 'N' }, {0x16, 'O' }, {0x17, 'P' },
{0x10, 'Q' }, {0x11, 'R' }, {0x12, 'S' }, {0x13, 'T' },
{0x0C, 'U' }, {0x0D, 'V' }, {0x0E, 'W' }, {0x0F, 'X' },
{0x08, 'Y' }, {0x09, 'Z' }, {0x0A, '^' }, {0x0B, 'v' },
{0x04, 0xFC}, {0x05, 0x0A}, {0x06, '<' }, {0x07, '>' }
};
//==================================
//Landscape keypad definition for a 44 button NEC IR remote.
#elif defined(Landscape44)
const byte buttonCode[][2] PROGMEM =
{
{0x04, '1' }, {0x08, '2' }, {0x0C, '3' }, {0x10, '4' }, {0x14, '5' }, {0x18, '6' }, {0x1C, '7' }, {0x50, '8' }, {0x54, '9' }, {0x58, '0' }, {0x5C, '-' },
{0x05, 'Q' }, {0x09, 'W' }, {0x0D, 'E' }, {0x11, 'R' }, {0x15, 'T' }, {0x19, 'Y' }, {0x1D, 'U' }, {0x51, 'I' }, {0x55, 'O' }, {0x59, 'P' }, {0x5D, 0x0A},
{0x06, 0xFC}, {0x0A, 'A' }, {0x0E, 'S' }, {0x12, 'D' }, {0x16, 'F' }, {0x1A, 'G' }, {0x1E, 'H' }, {0x4D, 'J' }, {0x49, 'K' }, {0x45, 'L' }, {0x41, '^' },
{0x07, 'Z'} , {0x0B, 'X' }, {0x0F, 'C' }, {0x13, 'V' }, {0x17, 'B' }, {0x1B, 0X20}, {0x1F, 'N' }, {0x4C, 'M' }, {0x48, '<' }, {0x44, '>' }, {0x40, 'v' }
};
//==================================
//Keypad definition for a 17 button NEC IR remote.
#elif defined(Remote17)
const byte buttonCode[][2] PROGMEM =
{
// Index KeyName
{0x46, '^' }, //0 ^
{0x44, '<' }, //1 <
{0x40, 0x0A}, //2 OK
{0x43, '>' }, //3 >
{0x15, 'v' }, //4 v
{0x16, '1' }, //5 1
{0x19, '2' }, //6 2
{0x0D, '3' }, //7 3
{0x0C, '4' }, //8 4
{0x18, '5' }, //9 5
{0x5E, '6' }, //10 6
{0x08, '7' }, //11 7
{0x1C, '8' }, //12 8
{0x5A, '9' }, //13 9
{0x42, '*' }, //14 *
{0x52, '0' }, //15 0
{0x4A, '#' } //16 #
};
//==================================
//Keypad definition for a 21 button NEC IR remote.
#elif defined(Remote21)
const byte buttonCode[][2] PROGMEM =
{
// Index KeyName
{0x45, '-' }, //0 CH-
{0x46, '%' }, //1 CH
{0x47, '+' }, //2 CH+
{0x44, '<' }, //3 PRE
{0x40, '>' }, //4 NEXT
{0x43, '$' }, //5 Play
{0x07, '!' }, //6 Vol+
{0x15, '"' }, //7 Vol-
{0x09, '=' }, //8 EQ
{0x16, '0' }, //9 0
{0x19, '*' }, //10 100+
{0x0D, '#' }, //11 200+
{0x0C, '1' }, //12 1
{0x18, '2' }, //13 2
{0x5E, '3' }, //14 3
{0x08, '4' }, //15 4
{0x1C, '5' }, //16 5
{0x5A, '6' }, //17 6
{0x42, '7' }, //18 7
{0x52, '8' }, //19 8
{0x4A, '9' } //20 9
};
#endif
// G P I O s A n d V a r i a b l e s
//================================================^================================================
//
//Macros
//================================================
//
#define LEDon HIGH
#define LEDoff LOW
#define ENABLED true
#define DISABLED false
//Analogs
//================================================
//
//INPUTS
//================================================
//
//Defined above IR_RECEIVE_PIN 2 //Connected to the IR receiver, you can use any GPIO pin.
//OUTPUTS
//================================================
//
const byte tonePin = 7; //Pin7---[220R]---[PiezoSpeaker]---GND
const byte testLED2 = 8; //Pin8---[220R]---A[LED]K---GND
const byte testLED1 = 9; //PWM_Pin9---[220R]---A[LED]K---GND
const byte heartbeatLED = 13; //Toggles every 500ms. Also flashes when an IR signal is detected.
//ISR VARIABLES
//================================================
//These variables are shared between:
// - the interrupt routine (ISR)
// - the main loop()
//
//'volatile' tells the compiler that these values can change at ANY time,
//so it must always read them directly from memory.
//
volatile bool irCommandReady = false;
volatile byte irCommand = 0;
enum COMMAND_TYPES {NEW_COMMAND, REPEAT};
volatile COMMAND_TYPES commandType = NEW_COMMAND;
//VARIABLES
//================================================
//
const byte arraySize = sizeof(buttonCode) / sizeof(buttonCode[0]);
int pwmLevelLED1 = 0;
byte rateChange = 5; //The PWM duty cycle "step change" for each button press.
byte lastCommand = 0;
//Timing stuff.
//========================
unsigned long heartbeatTime = 0;
const unsigned long heartbeatInterval = 500ul;
//Long-press definitions.
//========================
enum PRESS_TYPE {PRESS_SHORT, PRESS_LONG};
//"false", we are in the short press state. "true", we are in the long press state.
bool longPressActive = false;
const unsigned long longPressInterval = 500ul; //The time when we enter the long press state.
const unsigned long repeatInterval = 150ul; //Auto-repeat time. When we "again" proceed to the application code.
//Button press TIMER variables.
unsigned long pressStartTime = 0;
unsigned long lastRepeatTime = 0;
// s e t u p ( )
//================================================^================================================
//
void setup()
{
Serial.begin(115200);
//Serial.println(F("START " __FILE__ " from " __DATE__ "\r\n Using library version " VERSION_TINYIR));
pinMode(testLED1, OUTPUT);
pinMode(testLED2, OUTPUT);
pinMode(tonePin, OUTPUT);
pinMode(heartbeatLED, OUTPUT);
//This single function call does ALL of the following:
//1) Configures the IR receive pin.
//2) Enables pin-change interrupts.
//3) Starts listening for NEC IR commands.
//
//When a command is detected, the library automatically calls
//handleReceivedTinyIRData() (our ISR callback).
//
initPCIInterruptForTinyReceiver();
} //END of setup()
// l o o p ( )
//================================================^================================================
//
void loop()
{
//======================================================================== T I M E R heartbeatLED
//NOTE: This LED is "dual purpose".
//Is it time to toggle the heartbeat LED ?
//TinyIRReceiver.hpp also uses this LED for IR RX FEEDBACK.
if (millis() - heartbeatTime >= heartbeatInterval)
{
//Restart this TIMER.
heartbeatTime = millis();
//Toggle the heartbeat LED.
digitalWrite(heartbeatLED, digitalRead(heartbeatLED) == HIGH ? LOW : HIGH);
}
//======================================================================== Process new IR received commands
//Did we receive a new IR remote command ?
bool tempFlag;
COMMAND_TYPES _commandType;
byte _irCommand;
//Atomic access
//The ISR can change "irCommandReady" "irCommand" "commandType" at any moment.
//To avoid reading half-updated data, we briefly disable interrupts,
//copy the values, then immediately re-enable interrupts.
//This keeps the program safe and predictable.
//
noInterrupts();
if (irCommandReady)
{
tempFlag = true;
_irCommand = irCommand;
_commandType = commandType;
irCommandReady = false;
}
else
{
tempFlag = false;
}
interrupts();
//Did we receive a new command ?
if (tempFlag == true)
{
//Process the new IR command.
serviceButtonPress(_irCommand, _commandType);
}
//================================================
// Other non blocking code goes here
//================================================
} //END of loop()
// h a n d l e R e c e i v e d T i n y I R D a t a ( )
//================================================^================================================
//We do not call this ISR ourselves — the IR library does this automatically when an IR signal is detected.
//
void handleReceivedTinyIRData()
{
if (irCommandReady == false)
{
//Save the IR code for later processing.
irCommand = TinyIRReceiverData.Command;
//Is this a "Repeat" or "New command" ?
commandType = (TinyIRReceiverData.Flags & IRDATA_FLAGS_IS_REPEAT) ? REPEAT : NEW_COMMAND;
//Flag that we have a new IR code.
irCommandReady = true;
}
//IMPORTANT ISR RULES
//- Keep ISRs VERY short
//- Do NOT use Serial.print()
//- Do NOT use delay()
//- Do NOT do heavy calculations
//
//For this ISR only:
//1) set a flag to tell loop() that new data is ready,
//2) copy the received command, and
//3) determine if this is a "new command" or a "repeat" command.
} //END of handleReceivedTinyIRData()
// s e r v i c e B u t t o n P r e s s ( )
//================================================^================================================
//Short vs Long Button Press
// - NEW_COMMAND happens when a button is first pressed.
// - REPEAT happens automatically while the button is held.
//
//We measure time using millis(), we then decide when a press
//becomes a "long press", and when auto-repeat actions need to happen.
//
void serviceButtonPress(byte _irCommand, COMMAND_TYPES _commandType)
{
unsigned long now = millis();
//Is this a new button command ?
//The "repeat" command "is not" handled here.
if (_commandType == NEW_COMMAND)
{
pressStartTime = now;
lastRepeatTime = now;
lastCommand = _irCommand;
longPressActive = false;
handleMappedButton(_irCommand, PRESS_SHORT);
}
//Is this a "repeat" command situation; i.e. a button is being held ?
//The "repeat" command "is" handled here.
else if (_commandType == REPEAT && _irCommand == lastCommand)
{
//During the "short press" state, have we held the button long enough
//to get into a "long press" state ?
if (longPressActive == false && (now - pressStartTime >= longPressInterval))
{
//We are now in the "long press" state.
longPressActive = true;
lastRepeatTime = now;
handleMappedButton(_irCommand, PRESS_LONG);
}
//During a "long press" state, has the repeat interval expired ?
else if (longPressActive == true && (now - lastRepeatTime >= repeatInterval))
{
//Restart this TIMER.
lastRepeatTime = now;
//It is time to handle a repeat action for the pressed button.
handleMappedButton(_irCommand, PRESS_LONG);
}
} //END of if (_commandType == NEW_COMMAND)
} //END of serviceButtonPress()
// h a n d l e M a p p e d B u t t o n ( )
//================================================^================================================
//
void handleMappedButton(byte irCommand, PRESS_TYPE pressType)
{
//Find the button being pressed.
for (byte i = 0; i < arraySize; i++)
{
//Do we have a match ?
if (pgm_read_byte(&buttonCode[i][0]) == irCommand)
{
//Convert the raw IR command into a readable symbol.
//This allows us to use a simple switch(button)
//instead of remembering hex codes like 0x16 or 0x5A.
char buttonASCII = char(pgm_read_byte(&buttonCode[i][1]));
//When we are in the "short press" state, notify the "user" of the button press.
if (pressType == PRESS_SHORT)
{
Serial.println(buttonASCII);
//tone() inside main loop can glitch PWM
//tone() uses Timer2 on UNO.
//analogWrite() on pin 9 uses Timer1 → OK, Breaks PWM on pins 3 & 11 (Timer2)
tone(tonePin, 3400, 100);
}
//Execute the user's application code.
processButtonPress(buttonASCII, pressType);
//We found the button, no need to continue looking.
break;
}
}
} //END of handleMappedButton()
// h a n d l e B u t t o n P r e s s ( )
//================================================^================================================
//USER APPLICATION CODE; modify this function to control your own hardware.
//
void processButtonPress(char button, PRESS_TYPE pressType)
{
switch (button)
{
//==================== PWM increase LED1
case '1':
{
//Executed during a Short or Long press.
pwmLevelLED1 = pwmLevelLED1 + rateChange;
pwmLevelLED1 = constrain(pwmLevelLED1, 0, 255);
analogWrite(testLED1, pwmLevelLED1);
}
break;
//==================== PWM decrease LED1
case '2':
{
//Executed during a Short or Long press.
pwmLevelLED1 = pwmLevelLED1 - rateChange;
pwmLevelLED1 = constrain(pwmLevelLED1, 0, 255);
analogWrite(testLED1, pwmLevelLED1);
}
break;
//==================== Toggle LED2
case '3':
{
//Executed during a Short press.
if (pressType == PRESS_SHORT)
{
digitalWrite(testLED2, !digitalRead(testLED2));
}
}
break;
//==================== Navigating UP
case '^':
{
//Executed during a Short press.
if (pressType == PRESS_SHORT)
{
Serial.println(" UP");
}
//Executed during a Long press.
else if (pressType == PRESS_LONG)
{
Serial.println(" FAST UP");
}
}
break;
//==================== Navigating DOWN
case 'v':
{
//Executed during a Short press.
if (pressType == PRESS_SHORT)
{
Serial.println(" Down");
}
//Executed during a Long press.
else if (pressType == PRESS_LONG)
{
Serial.println(" FAST Down");
}
}
break;
//==================== Navigating Right
case '>':
{
//Executed during a Short press.
if (pressType == PRESS_SHORT)
{
Serial.println(" RIGHT");
}
//Executed during a Long press.
else if (pressType == PRESS_LONG)
{
Serial.println(" FAST RIGHT");
}
}
break;
//==================== Navigating Left
case '<':
{
//Executed during a Short press.
if (pressType == PRESS_SHORT)
{
Serial.println(" Left");
}
//Executed during a Long press.
else if (pressType == PRESS_LONG)
{
Serial.println(" FAST Left");
}
}
break;
//====================
default:
{
//This button has no code to execute.
if (pressType == PRESS_SHORT)
{
Serial.println("No code for this button.\n");
}
}
break;
}
} //END of handleButtonPress()
// E N D o f S K E T C H
//================================================^================================================
//
EDIT
- Added the changes from @J-M-L Post #2









