Mastering the SiLabs C8051 Microcontroller
|
External Interrupt (EXTI) and Timer (TMR) – Decoding IR Remote
Infrared (IR) remote controllers are widely used and cheap methods of remotely controlling appliances such as televisions, stereos, home theatres, etc. An IR remote transmitter sends data to an IR receiver by modulating/mixing data signals with a carrier wave of frequency between 30kHz to 40kHz. This modulation technique ensures the safety of data during long-distance transmissions. At the receiving end, an IR receiver picks up the modulated data and demodulates it, resulting in a stream of pulses. These pulses possess variable properties such as pulse widths, timing, position, and phase, and they carry data. The pulse properties are governed by a set of rules known as the protocol. By decoding the pulses according to the protocol, the sent information can be retrieved.
In most microcontrollers, there is no dedicated hardware for decoding IR remote protocols. Additionally, the microcontroller receiving IR remote data does not have prior knowledge of when it will receive an IR burst. Therefore, a combination of an external interrupt and a timer coupled with some software tricks is required for decoding IR data.
In this segment, we will explore how an external interrupt and a timer can be utilized to decode a NEC IR remote effortlessly. It is worth mentioning that this approach can be extended to decode any IR remote controller. To begin, I suggest studying the NEC IR protocol, which can be found here. For comprehensive information regarding IR remotes, SB-Project’s website is highly recommended as an excellent resource.
Code
SW_I2C.h
#define bit_shift_right(val, shift_size) (val >> shift_size)
#define bit_shift_left(val, shift_size) (val << shift_size)
#define bit_mask(bit_pos) bit_shift_left(1, bit_pos)
#define bit_set(val, bit_val) (val |= bit_mask(bit_val))
#define bit_clr(val, bit_val) (val &= (~bit_mask(bit_val)))
#define bit_tgl(val, bit_val) (val ^= bit_mask(bit_val))
#define get_bit(val, bit_val) (val & bit_mask(bit_val))
#define get_reg(val, msk) (val & msk)
#define test_if_bit_set(val, bit_val) (get_bit(val, bit_val) != FALSE)
#define test_if_bit_cleared(val, bit_val) (get_bit(val, bit_val) == FALSE)
#define test_if_all_bits_set(val, msk) (get_reg(val, msk) == msk)
#define test_if_any_bit_set(val, msk) (get_reg(val, msk) != FALSE)
#define SDA_DIR_OUT() do{bit_set(P1MDOUT, 6); bit_set(P0SKIP, 6);}while(0)
#define SDA_DIR_IN() do{bit_clr(P1MDOUT, 6); bit_set(P0SKIP, 6);}while(0)
#define SCL_DIR_OUT() do{bit_set(P1MDOUT, 7); bit_set(P0SKIP, 7);}while(0)
#define SCL_DIR_IN() do{bit_clr(P1MDOUT, 7); bit_set(P0SKIP, 7);}while(0)
#define SDA_HIGH() bit_set(P1, 6)
#define SDA_LOW() bit_clr(P1, 6)
#define SCL_HIGH() bit_set(P1, 7)
#define SCL_LOW() bit_clr(P1, 7)
#define SDA_IN() get_bit(P1, 6)
#define I2C_ACK 0xFF
#define I2C_NACK 0x00
#define I2C_timeout 1000
void SW_I2C_init(void);
void SW_I2C_start(void);
void SW_I2C_stop(void);
unsigned char SW_I2C_read(unsigned char ack);
void SW_I2C_write(unsigned char value);
void SW_I2C_ACK_NACK(unsigned char mode);
unsigned char SW_I2C_wait_ACK(void);
SW_I2C.c
#include "SW_I2C.h"
void SW_I2C_init(void)
{
XBR1 = 0x40;
SDA_DIR_OUT();
SCL_DIR_OUT();
delay_us(100);
SDA_HIGH();
SCL_HIGH();
}
void SW_I2C_start(void)
{
SDA_DIR_OUT();
SDA_HIGH();
SCL_HIGH();
delay_us(40);
SDA_LOW();
delay_us(40);
SCL_LOW();
}
void SW_I2C_stop(void)
{
SDA_DIR_OUT();
SDA_LOW();
SCL_LOW();
delay_us(40);
SDA_HIGH();
SCL_HIGH();
delay_us(40);
}
unsigned char SW_I2C_read(unsigned char ack)
{
unsigned char i = 8;
unsigned char j = 0;
SDA_DIR_IN();
while(i > 0)
{
SCL_LOW();
delay_us(20);
SCL_HIGH();
delay_us(20);
j <<= 1;
if(SDA_IN() != 0x00)
{
j++;
}
delay_us(10);
i--;
};
switch(ack)
{
case I2C_ACK:
{
SW_I2C_ACK_NACK(I2C_ACK);;
break;
}
default:
{
SW_I2C_ACK_NACK(I2C_NACK);;
break;
}
}
return j;
}
void SW_I2C_write(unsigned char value)
{
unsigned char i = 8;
SDA_DIR_OUT();
SCL_LOW();
while(i > 0)
{
if(((value & 0x80) >> 7) != 0x00)
{
SDA_HIGH();
}
else
{
SDA_LOW();
}
value <<= 1;
delay_us(20);
SCL_HIGH();
delay_us(20);
SCL_LOW();
delay_us(20);
i--;
};
}
void SW_I2C_ACK_NACK(unsigned char mode)
{
SCL_LOW();
SDA_DIR_OUT();
switch(mode)
{
case I2C_ACK:
{
SDA_LOW();
break;
}
default:
{
SDA_HIGH();
break;
}
}
delay_us(20);
SCL_HIGH();
delay_us(20);
SCL_LOW();
}
unsigned char SW_I2C_wait_ACK(void)
{
signed int timeout = 0;
SDA_DIR_IN();
SDA_HIGH();
delay_us(10);
SCL_HIGH();
delay_us(10);
while(SDA_IN() != 0x00)
{
timeout++;
if(timeout > I2C_timeout)
{
SW_I2C_stop();
return 1;
}
};
SCL_LOW();
return 0;
}
PCF8574.h
#include "SW_I2C.c"
#define PCF8574_address 0x4E
#define PCF8574_write_cmd PCF8574_address
#define PCF8574_read_cmd (PCF8574_address | 1)
void PCF8574_init(void);
unsigned char PCF8574_read(void);
void PCF8574_write(unsigned char data_byte);
PCF8574.c
#include "PCF8574.h"
void PCF8574_init(void)
{
SW_I2C_init();
delay_ms(20);
}
unsigned char PCF8574_read(void)
{
unsigned char port_byte = 0;
SW_I2C_start();
SW_I2C_write(PCF8574_read_cmd);
port_byte = SW_I2C_read(I2C_NACK);
SW_I2C_stop();
return port_byte;
}
void PCF8574_write(unsigned char data_byte)
{
SW_I2C_start();
SW_I2C_write(PCF8574_write_cmd);
SW_I2C_ACK_NACK(I2C_ACK);
SW_I2C_write(data_byte);
SW_I2C_ACK_NACK(I2C_ACK);
SW_I2C_stop();
}
LCD_2_Wire.h
#include "PCF8574.c"
#define clear_display 0x01
#define goto_home 0x02
#define cursor_direction_inc 0x06
#define cursor_direction_dec 0x04
#define display_shift 0x05
#define display_no_shift 0x04
#define display_on 0x0C
#define display_off 0x0A
#define cursor_on 0x0A
#define cursor_off 0x08
#define blink_on 0x09
#define blink_off 0x08
#define _8_pin_interface 0x30
#define _4_pin_interface 0x20
#define _2_row_display 0x28
#define _1_row_display 0x20
#define _5x10_dots 0x60
#define _5x7_dots 0x20
#define BL_ON 1
#define BL_OFF 0
#define dly 2
#define DAT 1
#define CMD 0
void LCD_init(void);
void LCD_toggle_EN(void);
void LCD_send(unsigned char value, unsigned char mode);
void LCD_4bit_send(unsigned char lcd_data);
void LCD_putstr(char *lcd_string);
void LCD_putchar(char char_data);
void LCD_clear_home(void);
void LCD_goto(unsigned char x_pos, unsigned char y_pos);
LCD_2_Wire.c
#include "LCD_2_Wire.h"
static unsigned char bl_state;
static unsigned char data_value;
void LCD_init(void)
{
PCF8574_init();
delay_ms(10);
bl_state = BL_ON;
data_value = 0x04;
PCF8574_write(data_value);
delay_ms(10);
LCD_send(0x33, CMD);
LCD_send(0x32, CMD);
LCD_send((_4_pin_interface | _2_row_display | _5x7_dots), CMD);
LCD_send((display_on | cursor_off | blink_off), CMD);
LCD_send((clear_display), CMD);
LCD_send((cursor_direction_inc | display_no_shift), CMD);
}
void LCD_toggle_EN(void)
{
data_value |= 0x04;
PCF8574_write(data_value);
delay_ms(1);
data_value &= 0xF9;
PCF8574_write(data_value);
delay_ms(1);
}
void LCD_send(unsigned char value, unsigned char mode)
{
switch(mode)
{
case CMD:
{
data_value &= 0xF4;
break;
}
case DAT:
{
data_value |= 0x01;
break;
}
}
switch(bl_state)
{
case BL_ON:
{
data_value |= 0x08;
break;
}
case BL_OFF:
{
data_value &= 0xF7;
break;
}
}
PCF8574_write(data_value);
LCD_4bit_send(value);
delay_ms(1);
}
void LCD_4bit_send(unsigned char lcd_data)
{
unsigned char temp = 0x00;
temp = (lcd_data & 0xF0);
data_value &= 0x0F;
data_value |= temp;
PCF8574_write(data_value);
LCD_toggle_EN();
temp = (lcd_data & 0x0F);
temp <<= 0x04;
data_value &= 0x0F;
data_value |= temp;
PCF8574_write(data_value);
LCD_toggle_EN();
}
void LCD_putstr(char *lcd_string)
{
do
{
LCD_putchar(*lcd_string++);
}while(*lcd_string != '\0') ;
}
void LCD_putchar(char char_data)
{
if((char_data >= 0x20) && (char_data <= 0x7F))
{
LCD_send(char_data, DAT);
}
}
void LCD_clear_home(void)
{
LCD_send(clear_display, CMD);
LCD_send(goto_home, CMD);
}
void LCD_goto(unsigned char x_pos,unsigned char y_pos)
{
if(y_pos == 0)
{
LCD_send((0x80 | x_pos), CMD);
}
else
{
LCD_send((0x80 | 0x40 | x_pos), CMD);
}
}
lcd_print.h
#define no_of_custom_symbol 1
#define array_size_per_symbol 8
#define array_size (array_size_per_symbol * no_of_custom_symbol)
void load_custom_symbol(void);
void print_symbol(unsigned char x_pos, unsigned char y_pos, unsigned char symbol_index);
void print_C(unsigned char x_pos, unsigned char y_pos, signed int value);
void print_I(unsigned char x_pos, unsigned char y_pos, signed long value);
void print_D(unsigned char x_pos, unsigned char y_pos, signed int value, unsigned char points);
void print_F(unsigned char x_pos, unsigned char y_pos, float value, unsigned char points);
lcd_print.c
#include "lcd_print.h"
void load_custom_symbol(void)
{
unsigned char s = 0;
const unsigned char custom_symbol[array_size] =
{
0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00
};
LCD_send(0x40, CMD);
for(s = 0; s < array_size; s++)
{
LCD_send(custom_symbol[s], DAT);
}
LCD_send(0x80, CMD);
}
void print_symbol(unsigned char x_pos, unsigned char y_pos, unsigned char symbol_index)
{
LCD_goto(x_pos, y_pos);
LCD_send(symbol_index, DAT);
}
void print_C(unsigned char x_pos, unsigned char y_pos, signed int value)
{
char ch[5] = {0x20, 0x20, 0x20, 0x20, '\0'};
if(value < 0x00)
{
ch[0] = 0x2D;
value = -value;
}
else
{
ch[0] = 0x20;
}
if((value > 99) && (value <= 999))
{
ch[1] = ((value / 100) + 0x30);
ch[2] = (((value % 100) / 10) + 0x30);
ch[3] = ((value % 10) + 0x30);
}
else if((value > 9) && (value <= 99))
{
ch[1] = (((value % 100) / 10) + 0x30);
ch[2] = ((value % 10) + 0x30);
ch[3] = 0x20;
}
else if((value >= 0) && (value <= 9))
{
ch[1] = ((value % 10) + 0x30);
ch[2] = 0x20;
ch[3] = 0x20;
}
LCD_goto(x_pos, y_pos);
LCD_putstr(ch);
}
void print_I(unsigned char x_pos, unsigned char y_pos, signed long value)
{
char ch[7] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, '\0'};
if(value < 0)
{
ch[0] = 0x2D;
value = -value;
}
else
{
ch[0] = 0x20;
}
if(value > 9999)
{
ch[1] = ((value / 10000) + 0x30);
ch[2] = (((value % 10000)/ 1000) + 0x30);
ch[3] = (((value % 1000) / 100) + 0x30);
ch[4] = (((value % 100) / 10) + 0x30);
ch[5] = ((value % 10) + 0x30);
}
else if((value > 999) && (value <= 9999))
{
ch[1] = (((value % 10000)/ 1000) + 0x30);
ch[2] = (((value % 1000) / 100) + 0x30);
ch[3] = (((value % 100) / 10) + 0x30);
ch[4] = ((value % 10) + 0x30);
ch[5] = 0x20;
}
else if((value > 99) && (value <= 999))
{
ch[1] = (((value % 1000) / 100) + 0x30);
ch[2] = (((value % 100) / 10) + 0x30);
ch[3] = ((value % 10) + 0x30);
ch[4] = 0x20;
ch[5] = 0x20;
}
else if((value > 9) && (value <= 99))
{
ch[1] = (((value % 100) / 10) + 0x30);
ch[2] = ((value % 10) + 0x30);
ch[3] = 0x20;
ch[4] = 0x20;
ch[5] = 0x20;
}
else
{
ch[1] = ((value % 10) + 0x30);
ch[2] = 0x20;
ch[3] = 0x20;
ch[4] = 0x20;
ch[5] = 0x20;
}
LCD_goto(x_pos, y_pos);
LCD_putstr(ch);
}
void print_D(unsigned char x_pos, unsigned char y_pos, signed int value, unsigned char points)
{
char ch[5] = {0x2E, 0x20, 0x20, 0x20, 0x20};
ch[1] = ((value / 100) + 0x30);
if(points > 1)
{
ch[2] = (((value / 10) % 10) + 0x30);
if(points > 1)
{
ch[3] = ((value % 10) + 0x30);
}
}
LCD_goto(x_pos, y_pos);
LCD_putstr(ch);
}
void print_F(unsigned char x_pos, unsigned char y_pos, float value, unsigned char points)
{
signed long tmp = 0x00000000;
tmp = value;
print_I(x_pos, y_pos, tmp);
tmp = ((value - tmp) * 1000);
if(tmp < 0)
{
tmp = -tmp;
}
if(value < 0)
{
value = -value;
LCD_goto(x_pos, y_pos);
LCD_putchar(0x2D);
}
else
{
LCD_goto(x_pos, y_pos);
LCD_putchar(0x20);
}
if((value >= 10000) && (value < 100000))
{
print_D((x_pos + 6), y_pos, tmp, points);
}
else if((value >= 1000) && (value < 10000))
{
print_D((x_pos + 5), y_pos, tmp, points);
}
else if((value >= 100) && (value < 1000))
{
print_D((x_pos + 4), y_pos, tmp, points);
}
else if((value >= 10) && (value < 100))
{
print_D((x_pos + 3), y_pos, tmp, points);
}
else if(value < 10)
{
print_D((x_pos + 2), y_pos, tmp, points);
}
}
main.c
#include "LCD_2_Wire.c"
#include "lcd_print.c"
#define sync_high 16000
#define sync_low 10800
#define one_high 2700
#define one_low 1800
#define zero_high 1400
#define zero_low 900
bit received;
unsigned char bits = 0;
unsigned int frames[33];
void PCA_Init(void);
void Timer_Init(void);
void Port_IO_Init(void);
void Oscillator_Init(void);
void Interrupts_Init(void);
void Init_Device(void);
void erase_frames(void);
unsigned int get_timer(void);
void set_timer(void);
unsigned char decode(unsigned char start_pos, unsigned char end_pos);
void decode_NEC(unsigned char *addr, unsigned char *cmd);
void IR_receive(void)
iv IVT_ADDR_EX0
ilevel 0
ics ICS_AUTO
{
frames[bits] = get_timer();
bits++;
TR0_bit = 1;
if(bits >= 33)
{
received = 1;
TR0_bit = 0;
}
set_timer();
}
void main(void)
{
unsigned char i = 0;
unsigned char address = 0;
unsigned char command = 0;
Init_Device();
LCD_init();
LCD_clear_home();
LCD_goto(0, 0);
LCD_putstr("ADR:");
LCD_goto(0, 1);
LCD_putstr("CMD:");
while(1)
{
if(received)
{
decode_NEC(&address, &command);
print_I(12, 0, address);
print_I(12, 1, command);
erase_frames();
}
};
}
void PCA_Init(void)
{
PCA0MD &= ~0x40;
PCA0MD = 0x00;
}
void Timer_Init(void)
{
TCON = 0x01;
TMOD = 0x01;
}
void Port_IO_Init(void)
{
// P0.0 - Skipped, Open-Drain, Digital
// P0.1 - Unassigned, Open-Drain, Digital
// P0.2 - Unassigned, Open-Drain, Digital
// P0.3 - Unassigned, Open-Drain, Digital
// P0.4 - Unassigned, Open-Drain, Digital
// P0.5 - Unassigned, Open-Drain, Digital
// P0.6 - Unassigned, Open-Drain, Digital
// P0.7 - Unassigned, Open-Drain, Digital
// P1.0 - Unassigned, Open-Drain, Digital
// P1.1 - Unassigned, Open-Drain, Digital
// P1.2 - Unassigned, Open-Drain, Digital
// P1.3 - Unassigned, Open-Drain, Digital
// P1.4 - Unassigned, Open-Drain, Digital
// P1.5 - Unassigned, Open-Drain, Digital
// P1.6 - Skipped, Push-Pull, Digital
// P1.7 - Skipped, Push-Pull, Digital
P1MDOUT = 0xC0;
P0SKIP = 0x01;
P1SKIP = 0xC0;
XBR1 = 0x40;
}
void Oscillator_Init(void)
{
OSCICN = 0x82;
}
void Interrupts_Init(void)
{
IE = 0x81;
IT01CF = 0x00;
}
void Init_Device(void)
{
PCA_Init();
Timer_Init();
Port_IO_Init();
Oscillator_Init();
Interrupts_Init();
}
void erase_frames(void)
{
delay_ms(90);
for(bits = 0; bits < 33; bits++)
{
frames[bits] = 0x0000;
}
set_timer();
received = 0;
bits = 0;
}
unsigned int get_timer(void)
{
unsigned int time = 0;
time = TH0;
time <<= 8;
time |= TL0;
return time;
}
void set_timer(void)
{
TH0 = 0;
TL0 = 0;
}
unsigned char decode(unsigned char start_pos, unsigned char end_pos)
{
unsigned char value = 0;
for(bits = start_pos; bits <= end_pos; bits++)
{
value <<= 1;
if((frames[bits] >= one_low) && (frames[bits] <= one_high))
{
value |= 1;
}
else if((frames[bits] >= zero_low) && (frames[bits] <= zero_high))
{
value |= 0;
}
else if((frames[bits] >= sync_low) && (frames[bits] <= sync_high))
{
return 0xFF;
}
}
return value;
}
void decode_NEC(unsigned char *addr, unsigned char *cmd)
{
*addr = decode(2, 9);
*cmd = decode(18, 25);
}
Schematic
Explanation
An NEC transmission consists of a total of 33 pulses. The initial pulse serves as the sync pulse, while the subsequent 32 pulses carry the address and command information. These 32 bits can be divided into two groups. The first group comprises 16 bits, which include the address and its inverted counterpart. The second group consists of another 16 bits, containing the command and its inverted form. Comparing the non-inverted signals with the inverted ones allows for data integrity verification.
In the case of the NEC IR protocol, the received IR data is represented as a sequence of pulses. These pulses correspond to sync bits, ones, and zeros. Notably, the pulse widths vary within this protocol. The sync bit is characterized by a high pulse lasting 9ms, followed by a low pulse of 4.5ms, resulting in a total pulse time of 13.5ms. Refer to the timing diagram below, where the blue pulses represent those from the transmitter, and the yellow pulses represent those received by the receiver. It is evident that the pulse streams are inverted, with this inversion occurring at the IR receiver.
In the NEC IR protocol, a logic one is indicated by a pulse with a high time of 560μs and a low time of 2.25ms, resulting in a total pulse time of 2.81ms. Conversely, a logic zero is represented by a pulse with a high time of 560μs and a low time of 1.12ms, giving a total pulse time of 1.68ms.
These timings can vary up to ±30% of their theoretical values due to several factors such as medium, temperature, reflections, etc. Thus, we have to define these time tolerances in our code.
#define sync_high 16000
#define sync_low 10800
#define one_high 2700
#define one_low 1800
#define zero_high 1400
#define zero_low 900
In this example, the system clock is set to 12.25MHz. We have to be as precise as possible with timings because we are dealing with a time-sensitive application.
void Oscillator_Init(void)
{
OSCICN = 0x82;
}
Setting up an external interrupt to detect falling edges and a timer are crucial parts in detecting IR pulse streams. This leaves the MCU free for other non-critical tasks. P0.0 is tied to external interrupt 0 (INT0) as per the code below. Timer 0 is also used.
void Timer_Init(void)
{
TCON = 0x01;
TMOD = 0x01;
}
....
void Interrupts_Init(void)
{
IE = 0x81;
IT01CF = 0x00;
}
Timer 0 is operating at a speed of approximately 1MHz, where 1 timer tick equals 1µs. This timer is clocked with the 12.25MHz system clock, which has been divided by a prescaler of 12. In this example, the code utilizes the “gate” feature of this timer, which automatically starts or stops the timer. There are two possible cases with the gate feature. Either the timer runs like other timers when instructed to run via software, or it runs when it is coded to run and the INT0 pin is in a logic high state. The latter feature is used in this example. Note that the timer overflow interrupt is not needed here.
Upon an external interrupt, the timer immediately stops due to the falling edge that triggered the interrupt and the subsequent low level at the INT0 pin. The count of the timer is captured and then stored in an array. TR0 bit is also set high so that the timer can run again.
void IR_receive(void)
iv IVT_ADDR_EX0
ilevel 0
ics ICS_AUTO
{
frames[bits] = get_timer();
bits++;
TR0_bit = 1;
if(bits >= 33)
{
received = 1;
TR0_bit = 0;
}
set_timer();
}
After capturing 33 pulses, the timer is fully stopped in order to process captured pulses.
The captured pulse times are compared against the timing extremes as mentioned earlier and sorted accordingly. This results in decoding IR data.
unsigned char decode(unsigned char start_pos, unsigned char end_pos)
{
unsigned char value = 0;
for(bits = start_pos; bits <= end_pos; bits++)
{
value <<= 1;
if((frames[bits] >= one_low) && (frames[bits] <= one_high))
{
value |= 1;
}
else if((frames[bits] >= zero_low) && (frames[bits] <= zero_high))
{
value |= 0;
}
else if((frames[bits] >= sync_low) && (frames[bits] <= sync_high))
{
return 0xFF;
}
}
return value;
}
void decode_NEC(unsigned char *addr, unsigned char *cmd)
{
*addr = decode(2, 9);
*cmd = decode(18, 25);
}
In the main loop, after receiving and decoding an NEC frame, the address and command data are displayed on an LCD. Following this, the microcontroller is readied to receive a new NEC frame.
if(received)
{
decode_NEC(&address, &command);
print_I(12, 0, address);
print_I(12, 1, command);
erase_frames();
}
Demo
|
A valid alternative to Silab´s development board: http://www.while1.eu/arduone/arduone.html
Overall, I thoroughly enjoyed your article and found it highly informative, thanks for sharing.
Thanks for the feedback….