G1OJS Ham Radio and Electronics

Adventures in electronics, ham radio and antennas

Minimal TV IR Decoder and Motor Driver using ATTiny mpc without library

// TV Remote Vol up / Vol down decoder and motor driver
// for motorised volume control using ATTiny88
// and LG Magic Remote
//
// (C) 2025 Alan Robinson G1OJS 

// Code readability constants
#define LEFT 2
#define RIGHT 1
#define OFF 0
#define IR_PIN 24  // A5 on my board with DrAzzy core
#define LED_PIN 0  // Built in LED
#define MOTORPOS_PIN 10
#define MOTORNEG_PIN 11
// Magic Remote Vol+ / Vol -
#define MRtx_VOL_UP 0xFA    
#define MRtx_VOL_DOWN 0xF8
// 'Device Connector' codes (LG Sound Sync off)
#define TVtx_VOL_UP 0xE8     
#define TVtx_VOL_DOWN 0xE9
#define TVtx_MUTE 0xE0

unsigned long lastPulseDetected_ms=0;  // to decide if button is held down

void motorDrive(unsigned char state){
  digitalWrite(MOTORPOS_PIN,(state==RIGHT));
  digitalWrite(MOTORNEG_PIN,(state==LEFT));
  digitalWrite(LED_PIN, (state!=OFF));
  // if motor drive is on, pause here until most recent IR activity is > ~ 0.2 seconds
  // (i.e. wait for button release)
  if (state!=OFF) {
    do {
      if (digitalRead(IR_PIN)==LOW) lastPulseDetected_ms=millis();
      bool inactive = (millis()-lastPulseDetected_ms) > 200; 
      if (inactive) break;
    }   while (1);
  }  
}

// This is for debugging and working out codes only.
// Blinks out 8 bits MSB to LSB, long flash = 1, short flash = 0.
void blinkLED_bits(unsigned long bits) {
  for (int i = 0; i < 8; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay((bits & 128)? 400:100);
    digitalWrite(LED_PIN, LOW);
    delay((bits & 128)? 100:400);
    bits *=2; // used * rather than bit shift to be absolutely sure I'm shifting the right way!
  }
  digitalWrite(LED_PIN, LOW);
  delay(200);
}

// This is the core of the code, waiting for IR activity and then timing pulses to decide
// whether a 0 or 1 was transmitted and packing these bits into the 32 bit control word 
// It uses the NEC Protocol. Note that IR module ouitput is HIGH when IR remote is OFF, 
// and LOW when IR remote is transmitting IR. Code could be made more readable by adding 
// something to make this explicit.
// https://wiki.keyestudio.com/052035_Basic_Starter_V2.0_Kit_for_Arduino#Project_14:_IR_Remote_Control
unsigned long getAddrAndCmdWord(){
  unsigned long cmdWord;
  unsigned char i=0;
  while (digitalRead(IR_PIN)==HIGH) {};
  cmdWord=0;
  do {
    cmdWord >>= 1;
    if(pulseIn(IR_PIN, HIGH, 5000) < 1250) 
      cmdWord |=0x80000000;
  } while (i++ < 32);
  return cmdWord;
}

// helper function to check that the received word is valid
// by checking that the inverted repeat of the command matches the first transmitted command
unsigned char validCmd(unsigned long addrAndCmdWord){
  byte cmd    = (addrAndCmdWord >> 16) & 0xFF;
  byte cmdInv = (addrAndCmdWord >> 24) & 0xFF;
  if ((cmd ^ cmdInv) == 0xFF) {return cmd;} else {return 0;}
}

// the usual stuff to set up pins ...
void setup() {
  pinMode(IR_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  pinMode(MOTORPOS_PIN, OUTPUT);
  pinMode(MOTORNEG_PIN, OUTPUT);  
}

// get the command word, pull out commands if valid, drive the motor.
// Readability again - need to make the "wait until IR is quiet again"
// more explicit here (it's hidden above in motorDrive())
void loop() {
  unsigned long addrAndCmdWord = getAddrAndCmdWord();
  unsigned char cmd = validCmd(addrAndCmdWord);
  if ((cmd == MRtx_VOL_UP) || (cmd == TVtx_VOL_UP) )  { motorDrive(RIGHT); }
  else if ((cmd == MRtx_VOL_DOWN) || (cmd == TVtx_VOL_DOWN) ) { motorDrive(LEFT); }
  else {blinkLED_bits(cmd);}
  motorDrive(OFF);
}