NerdKits - electronics education for a digital generation

You are not logged in. [log in]

NEW: Learning electronics? Ask your questions on the new Electronics Questions & Answers site hosted by CircuitLab.

Project Help and Ideas » IR Remote Control - LG Air Conditioner

May 23, 2011
by jabelch
jabelch's Avatar

Hello all, This is my first post, and my first real project outside of the NerdKit project set. I modified the code by Keyster, Infrared Remote control with NerdKit to allow me to control our LG Air Conditioners in the office. Right now the code supports turning them on and off from the computer using the serial port.

I was able to get the codes by attaching a photo diode (at least that's what I think it is) to my sound card and recording the input using Audacity. I'm not really ready order an Oscilloscope at this point, so this was the best alternative. The resolution was sufficient to see the individual bit transmissions, but not the carrier frequency of 38KHz. I will post the code below.

May 23, 2011
by jabelch
jabelch's Avatar
//  Infrared NEC Protocol Transmitter by Bryan Key
//  Modified by: Joshua Belcher
//  The main change is the addition of a listener 
//  in the main loop to allow for different codes
//  to be sent. I kept most of his interrupt logic
//  but modified for a different protocol. (LG)
//  
//  Future TODO list:
//  Remove need to use '-' as a pause between bits.
//  Add more codes (only helpful to others if you use the same AC)
//  Hexidecimal support - arrays can be 5 bytes instead of 100
//  Learn remote codes - IR receiver as a 'universal' remote
//
// 38kHz carrier wave
// IR LED is connected at PB1
// visible LED is connected at PB2 (for visual aid during testing)

#define F_CPU 14745600

// Set how many pulses are in each burst.  I am using the LG protocol for their AC remote.
// See http://braiden.org/?p=22  for more information about why I use these numbers
#define _BURSTWIDTH_DATA    22  // Total with data and pause = 43 periods of wave (pulses).
#define _BURSTWIDTH_PAUSE   21  // Off time between bits.
#define _HEADER_ON          344 // 43*8

#include <stdio.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
//#include <string.h>
#include <inttypes.h>

#include "../libnerdkits/delay.h"
#include "../libnerdkits/lcd.h"
#include "../libnerdkits/uart.h"

#include "ac_remote.h"

// prescale 8 & 102 counter = 18070.588 Hz
// prescale 8 & 51 counter = 36141.176 Hz   (~36kHz)
// prescale 8 & 48 counter = 38400 Hz       (~38kHz)
// prescale 8 & 25 counter = 73728 Hz  (.01356 ms)
// prescale 8 & 24 counter = 76800 Hz (~38hz * 2) (.01302 ms)
//   i uses 76800 because there is one cycle with LED on and one cycle with the LED off
//   hince making one complete LED cycle at ~38kHz

volatile uint8_t indx = 0;
volatile int8_t toggle = 0, HighLow = 0;
volatile uint16_t BurstWidth;
volatile uint8_t done;

volatile uint8_t* code;     //pointer to to allow us to switch arrays easily
//  Remote Code Arrays
volatile uint8_t on[100];
volatile uint8_t off[100];
volatile uint8_t up[100];

// each interrupt fire is 0.01302ms
//
ISR(TIMER0_COMPA_vect){

    // Check if we're done. If so, return to main loop
    if(done){
        return;
    }

    int8_t data;

    if (toggle)
    {   PORTB |= (HighLow<<PB1);        // if HighLow is 1 then turn on PB1
        PORTB |= (HighLow<<PB2);        // PB2 is a "test" LED (visible light)
    }
    else
    {   PORTB &= ~(1<<PB1);         // turn off PB1
        PORTB &= ~(1<<PB2);         // turn off PB2 (test LED)
        BurstWidth--;                   //each burst change is 0.02604ms
    }
    toggle ^= 1;                        //toggle the toggle

    if (!BurstWidth)                    //if BurstWidth == 0
    {   BurstWidth = _BURSTWIDTH_DATA;  // reset BurstWidth counter

        data = code[indx++];    // grab the next byte in string

        if (data == 'E')        // look for 'E'. "end-of-string"
        {   indx = 0;           // reset index back to zero
            HighLow = 0;        // turn pulse off
            done = 1;           // signals loop in main to break
        }
        if (data == '-')        // '-' = Pause between bits
        {   HighLow = 0;        // pause always keeps pulse off.
            BurstWidth = _BURSTWIDTH_PAUSE;
        }
        if (data == 'H')        // 'H' = Header ON
        {   HighLow = 1;        // Header pulse is always on
            BurstWidth = _HEADER_ON;
        }
        if (data == '1')
            HighLow = 1;        // a '1' turns the pulse on
        if (data == '0')
            HighLow = 0;        // a '0' turns the pulse off
    }

}

 void init(){

    TCCR0A = (1<<WGM01);            //CTC (Clear Timer on Compare Match)
    TCCR0B = (1<<CS01);         // set prescaler to 8
    OCR0A = 23;                     //24 is 76800 interrupts per second (38kHz * 2)

    DDRB |= (1<<DDB1) | (1<<DDB2);  // set pins for Output
    TIMSK0 = 2;                     // set the bit mask to enable interrupt - Timer 0 Compare Match A

    //_________________________*HEADER*|___B___|___D___|___E___|___B___|___F___|___F___|___D___|___B___|___D___|___0___|*IGNORE*
    memcpy_P((void*)off,PSTR("H0-0-0-0-1-0-1-1-1-1-0-1-1-1-1-0-1-0-1-1-1-1-1-1-1-1-1-1-1-1-0-1-1-0-1-1-1-1-0-1-0-0-0-0----E----"),95);  
    //_________________________*HEADER*|___B___|___D___|___F___|___F___|___F___|___A___|___D___|___A___|___F___|___0___|*IGNORE*
    memcpy_P((void*)on, PSTR("H0-0-0-0-1-0-1-1-1-1-0-1-1-1-1-1-1-1-1-1-1-1-1-1-1-0-1-0-1-1-0-1-1-0-1-0-1-1-1-1-0-0-0-0----E----"),95);  
    //_________________________*HEADER*|___B___|___D___|___F___|___E___|___F___|___D___|___6___|___D___|___F___|___0___|*IGNORE*
    memcpy_P((void*)up, PSTR("H0-0-0-0-1-0-1-1-1-1-0-1-1-1-1-1-1-1-1-0-1-1-1-1-1-1-0-1-0-1-1-0-1-1-0-1-1-1-1-1-0-0-0-0----E----"),95);

    // Default Values
    toggle = 1;
    HighLow = 1;
    BurstWidth = _BURSTWIDTH_DATA;  //set the number of pulses in each burst
    code = &off[0];     // Default Code to prevent errors when connecting serial cable to PC (Powering NK using cable)

 }

int main()
{
    cli();              //make sure interrups are disabled during setup
    init();             //Init timer and code arrays

    // init LCD
    lcd_init();
    FILE lcd_stream = FDEV_SETUP_STREAM(lcd_putchar, 0, _FDEV_SETUP_WRITE);
    lcd_write_string(PSTR("Belcher AC CONTROL"));

    // init serial port
    uart_init();
    FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
    stdin = stdout = &uart_stream;

    char tc;
    while (1)
    {   
        cli();              // Stop the interrupts

        // This line of code will wait until a char is sent by serial cable.  If you want to 
        // run your remote independant of the computer, you could replace this section with code 
        // that waits for input from buttons on different pins (on/off , up/down, etc...)
        tc = uart_read();   // Read and process data from Serial Port

        if(tc=='0') {code = &off[0];}       //OFF
        else if(tc=='1') {code =  &on[0];}  //ON
        else if(tc=='2') {code =  &up[0];}  //UP - Not useful at the moment.
        else {continue;}    // Not one of our codes, ignore - start at beginning of loop

        // Print the current selection to the serial port and LCD
        printf_P(PSTR("CMD: %c\r\n"), tc);              // Serial Port
        lcd_line_two();
        fprintf_P(&lcd_stream, PSTR("CMD: %c"), tc);    //LCD

        done = 0;           // reset varible for new command
        sei();              //start the interrupts

        // Let the interrupt do the work, breaks when done = 1
        while(!done)
        {
        }
        //delay_ms(1000);
    }

}
April 05, 2013
by xodin
xodin's Avatar

I know this is a relatively old post, but I referenced both Keyster and jabelch's code posts in my current endeavor to do a similar project. I'm still waiting for my infrared emitters and receivers to get here, but I've got the microprocessor controlled modulation ready to go. While my primary goal is to do one-way and then two-way infrared data communication between two microcontrollers, I believe I'll also try using one as a remote control for the TV as you two have done. Anyway, I thought I'd post my code, which is similar but a bit different than the two examples already posted. I opted to let the interrupt do all the work instead of manually managing it inside the main() function. The IF block where I translate the commands A, B, C, D to numbers of half-cycles and bit values are just examples, and they can be easily customized to any application. Enjoy, and thanks guys for the original posts!

P.S. I'm using the ATMega328p, so if you want to use this code for the ATMega168 you'll need to remove the 328p header file.

// infraredTX.c

#include <stdio.h>
#include <stdlib.h>

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <inttypes.h>

#include "../libnerdkits/delay.h"
#include "../libnerdkits/lcd.h"
#include "../libnerdkits/uart.h"
#include "../libnerdkits/io_328p.h"

volatile char cmd;
volatile uint16_t half_cycles = 0;
volatile uint8_t toggle = 1;
volatile uint8_t cur_bit = 0;

void infraredTX_setup() {
  // Set Port B pins 1 and 2 to output
  DDRB |= (1<<DDB1) | (1<<DDB2);

  // Enable interrupt on Timer0 compare match A
  TIMSK0 |= (1<<OCIE0A);

  // Set Timer0 to CTC mode - Clear Timer on Compare Match
  TCCR0A |= (1<<WGM01);

  // Timer clock rate prescaled to 1/8th CPU rate
  TCCR0B |= (1<<CS01);

  // Set TOP to 22 - Clear timer and fire interrupt every time the timer reaches 22
  OCR0A = 22;

  // Interrupt fire frequency is: CPU_CLK/(8*(1+22))=14745600/(8*23)=80139 Hz
  // and since each interrupt fire can only set output pin HIGH or LOW, the resulting
  // waveform will have a maximum frequency of 40070 Hz, which is close enough to the
  // 40 KHz carrier frequency required by the infrared receiver that it will work fine
}

ISR(TIMER0_COMPA_vect) {    
  // Time for a new command if half_cycles == 0
  if ( half_cycles == 0 ) {
    // Default action if no command given or invalid command given
    half_cycles = 2;
    cur_bit = 0;

    // If there is a pending command, read it...
    if (uart_char_is_waiting()) {
      cmd = uart_read();

      // ...and set the number of half_cycles and cur_bit accordingly
      if ( cmd == 'A' ) {
        half_cycles = 12;
        cur_bit = 1;
      } else if ( cmd == 'B' ) {
        half_cycles = 12;
        cur_bit = 0;
      } else if ( cmd == 'C' ) {
        half_cycles = 18;
        cur_bit = 1;
      } else if ( cmd == 'D' ) {
        half_cycles = 18;
        cur_bit = 0;
      }
    }
  }

  // Toggle depending on current bit and toggle turn
  if ( toggle == 1 ) {
    // Set the output pin HIGH only if the bit is supposed to be 1
    if ( cur_bit == 1 )
      PORTB |= (1<<PB1) | (1<<PB2);

    toggle = 0;
  } else {
    // Keep the output pin LOW during all other occasions
    PORTB &= ~((1<<PB1) | (1<<PB2));
    toggle = 1;
  }

  // Decrement the number of remaining half_cycles
  if (half_cycles > 0 )
    half_cycles--;
}

int main() {
  // Disable interrupts
  cli();

  // Initialize microcontroller
  infraredTX_setup();

  // Initialize serial port
  uart_init();
  FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
  stdin = stdout = &uart_stream;

  // Enable interrupts
  sei();

  // Execution completely managed by interrupt
  while (1) { 
  }

  return 0;
}
April 05, 2013
by pcbolt
pcbolt's Avatar

Hi xodin -

One thing you may need to consider in the future is having the "uart_read()" inside the interrupt routine. Even at 115,200 bits per sec your looking at 11,520 bytes per sec (8 bits data, one start bit, one stop bit) so each read take a minimum of about 90 uSec. The time period of your carrier (40 Khz) is 25 uSec so it is slowing down during UART reads. Not a problem the way your code is set up, but when you switch to something more complicated, it could come into play. You may want to move at least that portion of the code back into the main loop and communicate with the interrupt using a global variable. Or have it in a separate interrupt.

April 05, 2013
by xodin
xodin's Avatar

Hi pcbolt,

Thanks for the comment. I see where you get the 90 uSec value, being approximately 1 sec/11,520 bytes, but I don't follow how you conclude that the uart_read() command takes a minimum of 90 uSec? The uart_read() command only executes if there is a char waiting in the buffer, thanks to the uart_char_is_waiting() function--so the time it takes to execute the command is effectively only the time required to copy one byte of data from one place in memory (the buffer) to another (the global variable), correct? Therefore, I don't think there is any chance of the interrupt procedure taking longer than the 25 uSec it is allowed from the 40 KHz carrier.

Please correct me if I've overlooked something.

Thanks,

xodin

April 05, 2013
by xodin
xodin's Avatar

Hmm, now you've got me wondering if I did overlook something. Everything appeared perfect on the oscilloscope, even when I sent dozens of sequential commands from the computer. Since the bits are processed at 40 KHz (or 40,000 bps) by the microprocessor, and the bits are sent over UART at 115,200 bps, it would seem that the bitrate exceeds the processing rate, and so everything should be good right? I can see how such a system could screw things up though if the UART bitrate was less than the processing bitrate.

Given that the full commands are going to be long sequences of A, B, C, D, etc, I take it the full-proof approach would be to buffer all of the A, B, C, D's of a command sequence in the main loop, store them in volatile memory, and then execute them sequentially by the interrupt after a full command has been received?

Thanks again,

xodin

April 05, 2013
by xodin
xodin's Avatar

Every time I hit submit I wish I would have thought about things a little more. Sorry for the multiple posts.

I see your point now, and my last two posts were not entirely correct. The "bits" (as in IR bits) are processed at 40 KHz by the microprocessor, but they are not the same "bits" (as in serial data bits) that are transferred by the UART's 115,200 bps. Instead, one byte long characters are being sent over UART to instruct the microprocessor, which is where your 11,520 Bps comes from. It likely works fine as it is because I have each IR bit lasting for 12 or 18 cycles (and 12 or 18 * 11520 > 40,000), but if I changed that to 1 or 2 cycles it wouldn't work correctly anymore (1 or 2 *11520 < 40000). Hopefully that makes sense.

It seems that my second paragraph above was still correct though, that buffering a full command in the main loop prior to executing it sequentially by the interrupt should work for all cases, all the time, correct?

xodin

April 05, 2013
by pcbolt
pcbolt's Avatar

xodin -

I think your code should be safe. To copy from the UART Rx buffer takes only a cycle or two of processing time (70 to 140nSec). The time it takes from one command byte to the next would be 90 uSec (minimum) and since you aren't waiting between reads, you should be fine. I was just wondering what would happen when your code needs to expand to deal with two MCU's talking to each other. Buffering the commands would be my choice, but it looks like your code will work as is (and your O-scope proves it out). Writing to the UART will be another question since that will take some time to Tx between writes.

April 06, 2013
by xodin
xodin's Avatar

Right, it does successfully clear the buffer on time, but it's not going to work as intended for the reasons you mentioned. The timing will be off since the delays between commands are going to be slowed down. I will buffer the commands and repost my code, though it will likely look a lot more like the original posters now :P

April 06, 2013
by xodin
xodin's Avatar

Ok, here is the code for buffered commands. Pretty similar to the original posters except the instruction sets are sent over UART from the computer rather than being pre-stored in variables on the microprocessor. Hopefully this resolves any potential timing issues.

// infraredTX.c

#define BUFFER_SIZE 255

#include <stdio.h>
#include <stdlib.h>

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <inttypes.h>

#include "../libnerdkits/delay.h"
#include "../libnerdkits/lcd.h"
#include "../libnerdkits/uart.h"
#include "../libnerdkits/io_328p.h"

volatile char *cmd;
volatile char instruction_set[BUFFER_SIZE];
volatile uint16_t half_cycles = 0;
volatile uint8_t toggle = 1;
volatile uint8_t cur_bit = 0;
volatile uint8_t done = 0;

void infraredTX_setup() {
  // Set Port B pins 1 and 2 to output
  DDRB |= (1<<DDB1) | (1<<DDB2);

  // Enable interrupt on Timer0 compare match A
  TIMSK0 |= (1<<OCIE0A);

  // Set Timer0 to CTC mode - Clear Timer on Compare Match
  TCCR0A |= (1<<WGM01);

  // Timer clock rate prescaled to 1/8th CPU rate
  TCCR0B |= (1<<CS01);

  // Set TOP to 22 - Clear timer and fire interrupt every time the timer reaches 22
  OCR0A = 22;

  // Interrupt fire frequency is: CPU_CLK/(8*(1+22))=14745600/(8*23)=80139 Hz
  // and since each interrupt fire can only set output pin HIGH or LOW, the resulting
  // waveform will have a maximum frequency of 40070 Hz, which is close enough to the
  // 40 KHz carrier frequency required by the infrared receiver that it will work fine
}

ISR(TIMER0_COMPA_vect) { 
  // Time for the next command if half_cycles == 0
  if ( half_cycles == 0 ) {
    // Set the number of half_cycles and cur_bit according to command code
    if ( *cmd == 'A' ) {
      half_cycles = 12;
      cur_bit = 1;
    } else if ( *cmd == 'B' ) {
      half_cycles = 12;
      cur_bit = 0;
    } else if ( *cmd == 'C' ) {
      half_cycles = 18;
      cur_bit = 1;
    } else if ( *cmd == 'D' ) {
      half_cycles = 18;
      cur_bit = 0;
    } else if ( *cmd == '\0' ) {
      toggle = 1;
      done = 1;
      return;
    } else {
      // Default action if invalid command finds its way here, shouldn't happen
      half_cycles = 2;
      cur_bit = 0;
    }

    // Increment cmd to the next character in the instruction set
    cmd++;
  }

  // Toggle depending on current bit and toggle turn
  if ( toggle == 1 && cur_bit == 1 ) {
    // Set the output pin HIGH only if the bit is supposed to be 1
    PORTB |= (1<<PB1) | (1<<PB2);
  } else {
    // Keep the output pin LOW during all other occasions
    PORTB &= ~((1<<PB1) | (1<<PB2));
  }

  // Toggle the toggle
  toggle ^= 1;

  // Decrement the number of remaining half_cycles
  if (half_cycles > 0 )
    half_cycles--;
}

int main() {
  char c;
  int count;

  // Disable interrupts
  cli();

  // Initialize microcontroller
  infraredTX_setup();

  // Initialize serial port
  uart_init();
  FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
  stdin = stdout = &uart_stream;

  while(1) {    
    // Reset the done flag and count var for new instruction set
    done = 0;
    count = 0;

    // Set pointer to the beginning of the instruction_set array
    cmd = instruction_set;

    // Read in characters until the 'E' (end of instruction) character is received
    while((c = uart_read()) != 'E') { 
      // Only copy read character into instruction set if it is a valid instruction
      // i.e. no \n, \r, etc... as they would interfere with proper timing of the instruction
      if ( c == 'A' || c == 'B' || c == 'C' || c == 'D' ) {
        *cmd = c;      
        cmd++; 
        count++;
      }

      if ( count == BUFFER_SIZE ) {
        printf_P(PSTR("ERROR: Instruction set longer than buffer, splitting into pieces...\r\n"));
        break;
      }
    }

    // Terminate the string at that location
    *cmd = '\0';

    // Respond back over UART with the received instruction for verification
    printf_P(PSTR("Instruction received: %s (Length: %d)\r\n"), instruction_set, count);

    // Reset the pointer to the beginning of the instruction_set array
    cmd = instruction_set;

    // Enable interrupts
    sei();

    // Execution now managed by interrupt until full instruction set is sent
    while (!done) { 
    }

    // Disable interrupts
    cli();
  }

  return 0;
}

Post a Reply

Please log in to post a reply.

Did you know that sound travels via pressure waves in the air, and you can make these with a piezoelectric buzzer? Learn more...