Reading the Remote Commands

I would suspect that most readers know that TV remotes send commands to the TV via infrared. The TV manufacturer has to choose how to encode each digital command (i.e. power, channel up/down) into the analog signal output by the infrared diode.

Sony SIRC Protocol

I have a Sony TV which uses the SIRC protocol for transmitting remote commands. Which TV you have greatly affects how you decode and read the signals from the remote.

The SIRC protocol uses pulse-width modulation or PWM to encode 0’s and 1’s. A 0 is represented by a 600 microsecond pulse followed by 600 microsecond space. A 1 is a 1.2 millisecond pulse (twice that of a 0) followed by the same wait time.

Each command has an address appended to it which signified what type of device the command is going to (TV, VCR, CD player, etc.).

There are three versions of this protocol: the 12-bit, 15-bit, and 2-bit versions. In each version the length of the actual command is 7 bits, the only thing that changes between versions is the length of the address.

My TV uses the 12-bit versions so that’s the one I will talk about. In the 12-bit version, the address is 5 bits long and for a TV, it is 00001. Each command takes the following form:

Sony SIRC 12-bit protocol

As the picture describes, you start with a 2.4 millisecond pulse (followed by the standard 600 microsecond wait time). Then you transmit the command followed by the address. Both the command and address are little-endian.

For a more in-depth explanation of the protocol, here’s a great article about it: https://www.sbprojects.net/knowledge/ir/sirc.php

Using a TSOP

To read the infrared signal coming from the remote, we need an infrared receiver. You could probably use just a regular IR diode, but it’s much easier to buy a TSOP, an IR receiver than has some extra circuitry to deal with gain and noise-reduction. For this project I bought a TSOP4838.

Diagram of the TSOP

As you can see above, the TSOP has three pins: one for power, one to ground, and one to read the output on. Just what we need.

Let’s hook up the TSOP to our Arduino. You need the +5V pin to be connected to Vs, and connect the GND pin to ground (obviously). Then you need to connect OUT to a pin for reading. I initially thought this would have to be one of the analog pins but it turns out that you can only read from the module when it’s connect to one of the PWM-enabled digital pins. Therefore, we’ll use pin 11.

Schematic for reading from a TSOP

Decoding the Signal

Now that we can read from the IR receiver, we need to decode it to see what the commands are. Now we could use Ken Schiriff’s IRremote library, which is really handy. But, it’s more fun to write it ourselves.

Setting up the pins:

int irPin = 11; void setup() {

Serial.begin(9600);

pinMode(irPin, INPUT);

}

We will need access to the serial monitor later to read the commands, so we start transmission at 9600 bps.

Now, let’s write the code to decode the SIRC commands:

int commandMask = 127; void loop() {

duration = pulseIn(irPin, LOW); if (duration >= 2400) { // start burst

int bitnum = 0;

int bitstring = 0;

int duration = pulseIn(irPin, LOW);



while (duration >= 600 && duration < 2000) {

int currbit = 0; if (abs(duration-1200)<abs(duration-600))

currbit = 1;



bitstring = (currbit << bitnum) | bitstring;

++bitnum; duration = pulseIn(irPin, LOW);

} int address = bitstring >> 7;

int command = bitstring & commandMask; Serial.print(“Address: “);

Serial.print(address);

Serial.print(“, Command: “);

Serial.println(command);

}

}

The pulseIn function returns the duration it takes for the pin in the first argument to go the second argument and back again. Therefore, pinMode(irPin, LOW) returns the time (in microseconds) it takes for the value on pin 11 to go from HIGH to LOW to HIGH.

If we detect a pulse at least 2400 microseconds, we know we have received the start burst of the SIRC protocol.

We next initialize a few integers, bitnum and bitstring . bitnum is the number of bits currently read and bitstring is the command currently read. Then we keep reading until we pulses until we have a duration outside of the allowed range of values (I use 2000 instead of 2400 for the upper bound because I must’ve had some interference).

For each pulse, if the length is closer to 1200 than 600, we decide that we received a logical 1. Otherwise it is a 0. We then set the bitnum-th bit of the bitstring to the logical bit read. Then increment the number of bits read and re-read a pulse.

bitstring should now be 12 bits with the 5 most significant bits being the address and 7 lower bits being the command.

NOTE: Apparently, each command is transmitted three times, approximately 45 milliseconds apart. Therefore, we will should print out the commands thrice.

Uploading the code to the Arduino, pointing the remote at the TSOP, and pressing the corresponding buttons, we can record the commands we need. For my purposes, all I need are power (21), channel up/down (16/17), and volume up/down (18/19).

Transmitting the Remote Commands

Now, again we could use the IRremote library, but it’s more fun to implement this ourselves. The transmitting is surprisingly much more involved than the receiving.

Transmission is complicated since we need to operate at a more refined frequency (40kHz) and duty-cycle (1/4) than can be achieved using the built-in Arduino delayMicroseconds and digitalWrite functions.

Duty Cycle

The discussion before about the pulse for a logical 0 being 600 microseconds long and 1200 for a 1 was incomplete. These pulses are not HIGH for 100% of the burst time. They are parameterized by their duty cycle:

Diagram representing different duty cycles

The duty cycle of a PWM signal is the ratio of amount of time in HIGH to the time that one cycle takes. The SIRC protocol uses a duty cycle of 25% at 40kHz. If we were using delayMicroseconds , we would need to toggle the value on the output pin every 6.25 microseconds. delayMicroseconds doesn’t offer sub-millisecond resolution.

Arduino Timers

Thankfully, there is another way.

The ATmega328p comes with three timer/counter registers for PWM. The basic mechanism behind these timers is that they increment a counter and at a regular interval, increment this counter until a maximum value is hit. Once this value occurs, the timer restarts. This value determines the frequency of the PWM wave.

There is also a register to modify the duty cycle of the PWM wave. When this value is hit by the counter, a value of HIGH is put on an output register and returned to LOW at a time thereafter.

There are different number of operating modes for the timers. These modes modify attributes such as how the timer resets after the limit is hit or the how the top limit is specified.

For a detailed description of all operating modes, take a look at: http://www.righto.com/2009/07/secrets-of-arduino-pwm.html. The operating mode, or waveform generation mode (WGM), that we will use is called Phase-Correct PWM.

Phase-Correct PWM

Timing diagram for Phase-Correct PWM

In Phase-Correct PWM, the timer continually counts up until the max value is hit and then decrements until the counter is at zero. Normally, the max value is the width of the counter register (255 for timers 0 and 2, 65535 for timer 1). However, we can put our own max value into register OCRnA. With the timer registers, the lowercase n is a number 0, 1, or 2 corresponding to one of the timers.

By setting OCRnB, we modify the duty cycle. In Phase-Correct PWM, when the counter goes below the value in OCRnB, a HIGH voltage is put onto the output pin OCnB, and it returns to low when the counter goes above OCRnB. For Timer 2 (the timer we will be using), OC2B is pin 3. From now on, the n in all register names will be replaced with 2.

I initially tried to use Timer 0 however nothing was working. After much digging, I found that Timer 0 is actually used by delay and delayMicroseconds ! Using this timer for our custom PWM breaks those functions.

While the frequency of our PWM is inversely proportional to the value in OCR2A, it does not complete specify the frequency. The actual formula for the frequency is 16MHz/(2 * prescaler * OCR2A). 16MHz is the frequency of the CPU for the ATmega328p while the prescaler is either 1, 8, 32, 64, 128, 256, or 1028 and is specified by the three least significant bits of register TCCR2B.

For our purposes, we will use a prescaler of 1, so to achieve a frequency of 40kHz, OCR2A must be set to 200; 16MHz/(2*200) = 40kHz. Then to get our desired duty cycle of 25%, we must set of 50 or 200/4.

Transmitting the Signal

Now it’s time to code. Let’s first define some constants:

const int startBurstLength = 2400;

const int oneBurstLength = 1200;

const int zeroBurstLength = 600;

const int waitLength = 600; const byte topVal = 200;

const byte dutyCycle = topVal/4;

The burst lengths are what we will use to transmit 0’s or 1’s according to SIRC and topVal and dutyCycle are the values discussed earlier for generating the correct PWM pulse train.

The setup is similar to that of receiving but now pin 3 is used for output:

void setup() {

Serial.begin(9600);

pinMode(3, OUTPUT);

}

We will now write or loop function to transmit the codes we send to the Arduino via the serial monitor:

void loop() {

if (Serial.available()) {

command = Serial.readString().toInt();

if (command == 0) return;



for (int i=0; i<3; i++) {

transmit(command);

delay(40);

}

}

}

We transmit each command three times with a 40ms delay as the SIRC protocol specifies.

Now, we have to write the function to transmit the command:

void mark(int delayUs) {

TCCR2A |= (1<<COM2B1);

delayMicroseconds(delayUs);

space(waitLength);

} void space(int delayUs) {

TCCR2A &= ~(1<<COM2B1); // disable output on b

delayMicroseconds(delayUs);

} void sendTVAddress() {

mark(oneBurstLength);

for (char i=0; i<4; i++) {

mark(zeroBurstLength);

}

} void sendCommand(byte command) {

TIMSK2 &= ~(1<<TOIE2);



digitalWrite(3, LOW);



OCR2A = topVal;

OCR2B = dutyCycle;

TCCR2A = (1<<WGM20);

TCCR2B = (1<<WGM22) | (1<<CS20); mark(startBurstLength);



for (char i=0; i<7; i++) {

if (command % 2 == 0) mark(zeroBurstLength);

else mark(oneBurstLength);

command >>= 1;

}

} void transmit(byte command) {

sendCommand(command);

sendTVAddress();

}

We use two procedures to transmit the command, sendCommand followed by sendTVAddress . sendCommand first clear the TOIE2 bit of TIMSK2 which disables interrupts on Timer 2. If we did not do this, our PWM could get interrupted by the system, invalidating the command. As an aside, we use the 1<<(OFFSET) syntax to set the OFFSET-th bit of a register.

We then put a LOW value on just to be safe.

As discussed earlier, topVal (200) goes into register OCR2A and dutyCycle (50) into OCR2B.

We set WGM20 in TCCR2A and WGM22 in TTCR2B to signify that we want to use Phase-Correct PWM. Below is the chart for which WGM bits to set for a certain WGM:

WGM Bit Description

We have to split the WGM bits between the two registers because this is how TCCR2A/B are structured:

Structure of the TCCR2A register

Structure of the TCCR2B register

These registers are packed with flags and parameters, so Atmel had to split up the three WGM bits.

The last thing to mention about the register initialization in sendCommand is the setting of CS20 in TCCR2B. According, to the datasheet, setting CS20 corresponds to a prescaler of 1:

How to set the prescaler for Timer 2

Now let’s look at the mark function:

void mark(int delayUs) {

TCCR2A |= (1<<COM2B1);

delayMicroseconds(delayUs);

space(waitLength);

}

If you look in the datasheet, you’ll see that the COM2B1 bit enables the B output for Timer 2 (the B output is pin 3). We OR TCCR2A with this bit to set it equal to one then let it run for either 600 or 1200 microseconds depending on whether we want to transmit a 1 or a 0. Finally, we wait 600 microseconds every every pulse:

void space(int delayUs) {

TCCR2A &= ~(1<<COM2B1); // disable output on b

delayMicroseconds(delayUs);

}

This code is very similar to mark but we AND with the negation of COM2B1 to set that bit to 0.

I highly suggest looking at the ATmega328p datasheet if you want to gain a deeper understanding of the different WGM modes as well as how all of the different registers affect timing.

Putting it All Together

Now that we have written the code, it’s time to wire up the IR diode. Once, completed the setup should look like this:

Fritzing sketch for transmitting IR commands

I couldn’t find an IR component in the Fritzing application so make sure you’re not using a red LED for this circuit. I also suggest putting the diode towards the back of the breadboard so you can tilt it up since it is crucial the diode points at the TV.

It’s even better if you have some DuPont male-female wires so you can point the diode without having to tilt the whole breadboard.

Now that everything’s setup, you can upload the code onto the Arduino. Then open up the serial monitor and enter some commands such as 21 for power up or 16 for channel up.

If the TV is responding, it could be due to any number of things: 1) the diode is connected backwards, 2) it is not pointed at the TV’s receiver, or 3) the transmit code is not exact. The timer code is very sensitive to small changes. If for example just one bit is changed in TCCR2A/B, that could do something as drastic as changing the WGM.