Remote-controlled lamps on an Arduino Uno
A 21-button handset, a VS1838B and three lamps. NEC decoded from pulse widths, and a receiver that inverts everything it hears. The whole build, an Arduino Uno and 8 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $32. On Mokxi they're free.
Step by step, with the wiring and the common mistakes: Arduino IR Remote: Decode NEC, Switch Lamps
What you'll learn
- How an infrared receiver reports a signal
- The NEC remote protocol and its timing
- Picking one output with a
switchon a command - Dimming LEDs with
analogWrite - Why a held button sends repeats
- Difficulty: Intermediate
- Time: About 20 minutes
- Board: Arduino Uno R3
- Parts: 10
How it works
The VS1838B receiver listens for the infrared remote and sends its output to pin 2. The sketch decodes each button press into an address and a command. Buttons 1, 2 and 3 pick the red, green or blue lamp on pins 5, 6 and 9. Button 0 turns all lamps off.
Only one lamp is lit at a time. The CH- and CH+ buttons lower and raise its brightness in steps of 25, between 0 and 255. Holding either one keeps changing it.
Each decoded press is printed to the Serial Monitor, along with the older style of number that many tutorials use for the same button.
Parts list
10 parts, plus the jumper wires. They retail for about $32; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Infrared remote, 21 buttons
- 1 × Infrared receiver, VS1838B
- 3 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, redIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, blueIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
Kits that can build it
These 8 starter kits have every part in the circuit. Each kit's page lists what else it can build.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Resistor, 220 Ω (3) pin 1 | Arduino Uno R3 pin 9 |
| Resistor, 220 Ω (2) pin 1 | Arduino Uno R3 pin 6 |
| Resistor, 220 Ω (1) pin 1 | Arduino Uno R3 pin 5 |
| Infrared receiver, VS1838B pin OUT | Arduino Uno R3 pin 2 |
| Infrared receiver, VS1838B pin VCC | Arduino Uno R3 pin 5V |
| Infrared receiver, VS1838B pin GND | Arduino Uno R3 pin GND |
| LED, red pin C | Arduino Uno R3 pin GND |
| LED, green pin C | Arduino Uno R3 pin GND |
| LED, blue pin C | Arduino Uno R3 pin GND |
| Infrared receiver, VS1838B pin IR | Infrared remote, 21 buttons pin IR |
| LED, red pin A | Resistor, 220 Ω (1) pin 2 |
| LED, green pin A | Resistor, 220 Ω (2) pin 2 |
| LED, blue pin A | Resistor, 220 Ω (3) pin 2 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Remote-controlled lamps: an infrared receiver on pin 2, three lamps.
//
// pin 2 the VS1838B's OUT
// pin 5 red lamp press 1 on the handset
// pin 6 green lamp press 2
// pin 9 blue lamp press 3
// 0 turns all three off; CH- and CH+ step the brightness of whichever is on
//
// The handset's IR pin is wired to the receiver's: in Mokxi two parts only meet
// on a net, so the beam is a wire. Everything else here is what happens on a
// real desk.
//
// # The receiver inverts
//
// OUT idles HIGH and goes LOW while a burst of light is arriving. So the 9 ms
// leader burst is 9 ms of LOW, and every timing in the decoder is written from
// that side. It is the first surprise and it catches everybody once.
//
// # NEC is widths, not levels
//
// One unit is 562.5 us, 21 cycles of the 38 kHz carrier, which is why it is
// not a round number. A leader is 16 units of burst and 8 of space; then
// thirty-two bits, each one unit of burst and either one unit of space (a
// nought) or three (a one); then a stop burst. The decoder measures the space
// after each burst and splits them at 1.1 ms.
//
// The thirty-two bits are address, address inverted, command, command inverted,
// **least significant bit first**. The two inversions are the whole of the
// error checking, and firmware/lib/mokxi_ir.h checks both, so a frame that
// arrived with somebody's headlights through it is thrown away rather than
// acted on.
//
// # The number every table prints
//
// Tutorials call the CH- button 0xFFA25D; this sketch calls it command 0x45.
// They are the same thirty-two bits. The bits arrive least significant first
// and the old Arduino decoder assembled them the other way round, so each byte
// came out bit-reversed: 0x45 reversed is 0xA2. The sketch prints both, so
// the two readings can be seen to be one thing.
//
// # Repeats
//
// Hold a button and the handset sends a whole frame once and then a short
// repeat every 110 ms. A sketch that treats a repeat as a fresh press is why
// volume buttons run away, so this one only acts on repeats for the two
// brightness keys, where running is the point.
#include "mokxi_ir.h"
const int IR_PIN = 2;
const int LAMPS[3] = {5, 6, 9};
IrReceiver ir(IR_PIN);
int lit = -1; // which lamp is on, or -1
int brightness = 200; // 0..255, on whichever lamp is lit
unsigned char lastCommand = 0;
void pick(int which);
void applyBrightness(int by);
void setup() {
for (int i = 0; i < 3; i++) {
pinMode(LAMPS[i], OUTPUT);
analogWrite(LAMPS[i], 0);
}
Serial.begin(115200);
Serial.println("Mokxi Uno: NEC remote, 1/2/3 pick a lamp, 0 is off");
ir.begin();
}
void loop() {
IrFrame frame = ir.receive(300);
if (!frame.valid) {
return;
}
if (frame.repeated) {
// Only the two that should run while held.
if (lastCommand == IR_CH_DOWN || lastCommand == IR_CH_UP) {
applyBrightness(lastCommand == IR_CH_UP ? 25 : -25);
}
return;
}
lastCommand = frame.command;
Serial.print("address 0x");
Serial.print((long)frame.address, HEX);
Serial.print(" command 0x");
Serial.print((long)frame.command, HEX);
Serial.print(" (tables call it 0x");
Serial.print((long)frame.legacy(), HEX);
Serial.println(")");
switch (frame.command) {
case IR_1:
pick(0);
break;
case IR_2:
pick(1);
break;
case IR_3:
pick(2);
break;
case IR_0:
pick(-1);
break;
case IR_CH_UP:
applyBrightness(25);
break;
case IR_CH_DOWN:
applyBrightness(-25);
break;
default:
Serial.println(" no lamp on that button");
break;
}
}
// Light one lamp and put the other two out. -1 is all off.
void pick(int which) {
lit = which;
for (int i = 0; i < 3; i++) {
analogWrite(LAMPS[i], i == which ? brightness : 0);
}
Serial.print(" lamp ");
Serial.println(which < 0 ? -1 : which + 1);
}
void applyBrightness(int by) {
brightness += by;
if (brightness < 0) {
brightness = 0;
}
if (brightness > 255) {
brightness = 255;
}
if (lit >= 0) {
analogWrite(LAMPS[lit], brightness);
}
Serial.print(" brightness ");
Serial.println(brightness);
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Sets each lamp pin as an output and turns it off, starts the Serial Monitor at 115200, and starts the infrared receiver on pin 2.
void setup() {
for (int i = 0; i < 3; i++) {
pinMode(LAMPS[i], OUTPUT);
analogWrite(LAMPS[i], 0);
}
Serial.begin(115200);
Serial.println("Mokxi Uno: NEC remote, 1/2/3 pick a lamp, 0 is off");
ir.begin();
}loop sketch.ino
Waits for a valid frame. A repeat frame only changes brightness, and only for CH- or CH+. A fresh press is printed and then handled in a switch: 1, 2 and 3 call pick, 0 turns everything off, and the channel keys call applyBrightness.
void loop() {
IrFrame frame = ir.receive(300);
if (!frame.valid) {
return;
}
if (frame.repeated) {
// Only the two that should run while held.
if (lastCommand == IR_CH_DOWN || lastCommand == IR_CH_UP) {
applyBrightness(lastCommand == IR_CH_UP ? 25 : -25);
}
return;
}
lastCommand = frame.command;
Serial.print("address 0x");
Serial.print((long)frame.address, HEX);
Serial.print(" command 0x");
Serial.print((long)frame.command, HEX);
Serial.print(" (tables call it 0x");
Serial.print((long)frame.legacy(), HEX);
Serial.println(")");
switch (frame.command) {
case IR_1:
pick(0);
break;
case IR_2:
// ... 19 more lines in the editorpick sketch.ino
Remembers which lamp is lit and writes the current brightness to it and 0 to the other two. Passing -1 turns all three off.
// Light one lamp and put the other two out. -1 is all off.
void pick(int which) {
lit = which;
for (int i = 0; i < 3; i++) {
analogWrite(LAMPS[i], i == which ? brightness : 0);
}
Serial.print(" lamp ");
Serial.println(which < 0 ? -1 : which + 1);
}applyBrightness sketch.ino
Adds or subtracts from the brightness and keeps it between 0 and 255. If a lamp is lit, it updates that lamp right away, then prints the new value.
void applyBrightness(int by) {
brightness += by;
if (brightness < 0) {
brightness = 0;
}
if (brightness > 255) {
brightness = 255;
}
if (lit >= 0) {
analogWrite(LAMPS[lit], brightness);
}
Serial.print(" brightness ");
Serial.println(brightness);
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- No lamp responds to the remote This project
Check that the receiver's OUT goes to pin 2 and that it has 5V and GND. - CH+ changes nothing This project
Brightness only affects a lamp that is already on. Press 1, 2 or 3 first. - A lamp stays dark This project
Check the LED direction: the anode goes to the resistor and the cathode to GND. - Button codes come out wrong or do not match another sketch. Infrared remote
Remotes differ. Print the raw codes for your buttons first, then compare against those values. A repeat while you hold a button is a special repeat code, not a new press. - Nothing is received at all. Infrared remote
Check the receiver pin order of OUT, GND and VCC, which varies by module. Use the pin your IR library expects. Keep the receiver away from bright light and aim the remote at it. - No signal is seen, or the module gets hot, because the three legs are in the wrong order. Infrared receiver
On the VS1838B, with the dome facing you, the legs are usually OUT, GND, VCC from left to right. Check your module's marking before powering it. Supply is 2.7 to 5.5 V. - A plain LED blinking at the receiver is never detected. Infrared receiver
The receiver only responds to light modulated at about 38 kHz. Send with a library such as IRremote, which makes the carrier. OUT idles high and goes low during a burst, so decode that inverted signal. - The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.