Tuesday, February 18, 2014

Remote Volume Controller: Burr-Brown PGA2311 remote (IR) control

This project remotely controls the volume of an audio device using the low-noise, HiFi, PGA2311 stereo volume IC from Burr-Brown. Volume up/down and mute are implemented via Sony IR codes from a universal remote. An arduino microcontroller interfaces to a color LCD, PGA2311's SPI interface, and the IR detector. I designed this project because I wanted a way of remotely controlling the line-out of my tv, connected to powered (computer) speakers, without using a full-blown audio receiver.



Bill-Of-Materials:


Arduiono Uno:  $20
PGA2311:  $10
MAX1044:  $2
RGB backlit LCD:  $10
Cigar box:  $2
Blue LED:  stock
10k potentiometer:  stock
1k resistor:  stock
5x 10uF capacitor:  stock
2x Stereo 3.5" audio jacks:  stock
Protoboard and wire:  stock
IR detector:  salvaged from a piece of dead equipment
Power supply:  salvaged from a piece of dead equipment






The schematic:

A note about Arduino pins 0 and 1 (TX/RX) as GPIO:

I originally wanted to modulate the LCD's red, green, and blue lines with the PWM lines on the Arduino.  Since I was almost out of I/O, I tried using the Arduino pins 0 and 1 as general purpose I/O, but it appears these are hardware dedicated to TX/RX on the serial port, so when I ran my program the used pins 0 and 1, I could no longer program the Arduino; the running program conflicted with serial programming, and it bricked the Arduino!  I unbricked it by hitting reset continuously while programming the most basic sketch possible.  Unless you absolutely need to, try to avoid using the Arduino's pins 0 and 1 for GPIO.  It will work, but you will lose the ability to program new sketches.

PGA2311 Block Diagram (from datasheet)

Communications with the PGA2311:

The PGA2311 requires a SPI interface (clock, data in, and chip select) to set the programmable resistor network in the OpAmps for attenuation.  The SPI protocol is simple to implement on the Arduino, just use SPI.h; sending gain values to the right and left channels worked the first time, without any debug.  The gain is an 8-bit value (1 to 255) and can be set independently for right and left channels.  The full gain range is -95.5 dB to +31.5 dB, in half dB steps.  Mute is accomplished via software (sends gain = 1).

Generating -5V from +5V:

The PGA2311 utilizes dual-supply OpAmps, and requires +/- 5VDC to operate with the lowest possible Total Harmonic Distortion.  The Arduino supplies +5VDC, but doesn't have negative voltages, so a charge-pump was used to generate a negative supply.  The MAX1044 was chosen because it requires minimal components (2 caps) and was cost-effective.

IR Detector:  

Originally, I was using a detector module that came with an Arduino sensor kit, but I found that the range was limited to only a few feet!  To improve the usable range, I salvaged the IR detector from an old Comcast DVR that we couldn't sell (and no longer can use).  I programmed by TV/cable box's universal remote for Sony IR codes under the AUX function, and learned the Sony codes for volume up, down, and mute:

Mute:  0x281
Volume Up:  0x481
Volume Down:  0xC81

The LED indicates when an IR signal is detected.  The IR detector's signal is pulled to ground when a signal is present, and the LED is connected through a 1k Ohm current limiting resistor.  I had some super-bright blue LEDs that matched the look of this project nicely.  Be careful determining the pinout for the detector chip, some have power and ground swapped.

RGB Backlit LCD Display

Here's a link to the LCD module on Amazon.  It is a HD44780 compliant device, which means you can use the LiquidCrystal.h library to easily interface to the LCD.  The red, green, and blue backlight pins can be used with Pulse-Width Modulation (PWM) on the Arduino to achieve any color desired.  Since I was running out of pins (and I like blue and purple), I grounded the blue pin (fully on), left the green pin floating (fully off), and controlled the voltage of the red pin (changes from blue to purple/pink).  The red pin's PWM value is the inverse of the gain, so the display starts blue, and as the volume is increased, gets more red, until purple/pink is reached at maximum gain.  The volume bar was implemented entirely from scratch and maps the 8-bit gain number to 16 levels of custom characters.  The custom characters are defined and loaded into the LCD's memory (HD44780 compliant device allow up to 8 custom characters).  Here's a snippet of just the volume bar code:
// custom LCD characters for the volume bar
byte v0[8] = {B00000,B00000,B00000,B00000,B00000,B00000,B00000};
byte v1[8] = {B00000,B00000,B00000,B00000,B00000,B00000,B11111};
byte v2[8] = {B00000,B00000,B00000,B00000,B00000,B11111,B11111};
byte v3[8] = {B00000,B00000,B00000,B00000,B11111,B11111,B11111};
byte v4[8] = {B00000,B00000,B00000,B11111,B11111,B11111,B11111};
byte v5[8] = {B00000,B00000,B11111,B11111,B11111,B11111,B11111};
byte v6[8] = {B00000,B11111,B11111,B11111,B11111,B11111,B11111};
byte v7[8] = {B11111,B11111,B11111,B11111,B11111,B11111,B11111};

  lcd.createChar(0, v0);
  lcd.createChar(1, v1);
  lcd.createChar(2, v2);
  lcd.createChar(3, v3);
  lcd.createChar(4, v4);
  lcd.createChar(5, v5);
  lcd.createChar(6, v6);
  lcd.createChar(7, v7);

  // cycle through the 16 volume bar levels, setting each level as appropriate
  for (int i =  0; i < 16; i++) {
    if (nGain/2 > i*8) {
      lcd.setCursor(i, 1);
      lcd.write(round(i/2));  
    }
  }
     

The Arduino sketch:

/*
 Arduino pins:
 - digital pin 13 (SCK) = SPI SCLK (PGA2311 pin 6)
 - digital pin 12 (MISO) = not used
 - digital pin 11* (MOSI) = SPI SDI/MOSI (PGA2311 pin 3)
 - digital pin 10* (SS) = SPI \CS (PGA2311 pin 2)
 - digital pin 9* = LCD Red (LCD pin RED)
 - digital pin 8 = LCD RS (LCD pin RS)
 - digital pin 7 = LCD Enable (LCD pin E)
 - digital pin 6* = IR receiver data
 - digital pin 5* = LCD D4 (LCD pin D4)
 - digital pin 4 = LCD D5 (LCD pin D5)
 - digital pin 3* = LCD D6 (LCD pin D6)
 - digital pin 2 = LCD D7 (LCD pin D7)
 - digital pin 1 = not connected
 - digital pin 0 = not connected
 * is a PWM pin

 LCD schematic
 * LCD R/W pin to ground
 * 10K resistor:
 * ends to +5V and ground
 * wiper to LCD VO pin (pin 3)
 
 * IR detector data = pin 6
 
 * PGA2311 gain:  N = 1 to 255
 * Gain (dB) = 31.5 − [0.5*(255 − N)]
 
 * SPI interface:
 * /CS (pin 2) 
 * SCLK (pin 6)
 * SDI (pin 3)
 * SDO (pin 7)
 
 */

#include <IRremote.h>
#include <LiquidCrystal.h>
#include <stdlib.h>
#include <SPI.h>

int redPin = 9;
int red = 0;
const int slaveSelectPin = 10;
int RECV_PIN = 6;
IRrecv irrecv(RECV_PIN);
decode_results results;

LiquidCrystal lcd(8, 7, 5, 4, 3, 2);  // RS, Enable, D4, D5, D6, D7

char cGain[17];
String gain;
char icGain[6];
float iGain = -95.5;
float nGain = 1;

boolean mute = false;
// custom LCD characters for the volume bar
byte v0[8] = {B00000,B00000,B00000,B00000,B00000,B00000,B00000};
byte v1[8] = {B00000,B00000,B00000,B00000,B00000,B00000,B11111};
byte v2[8] = {B00000,B00000,B00000,B00000,B00000,B11111,B11111};
byte v3[8] = {B00000,B00000,B00000,B00000,B11111,B11111,B11111};
byte v4[8] = {B00000,B00000,B00000,B11111,B11111,B11111,B11111};
byte v5[8] = {B00000,B00000,B11111,B11111,B11111,B11111,B11111};
byte v6[8] = {B00000,B11111,B11111,B11111,B11111,B11111,B11111};
byte v7[8] = {B11111,B11111,B11111,B11111,B11111,B11111,B11111};

void setup() {
  lcd.createChar(0, v0);
  lcd.createChar(1, v1);
  lcd.createChar(2, v2);
  lcd.createChar(3, v3);
  lcd.createChar(4, v4);
  lcd.createChar(5, v5);
  lcd.createChar(6, v6);
  lcd.createChar(7, v7);
  lcd.begin(16, 2);
  
  pinMode(redPin, OUTPUT);
  analogWrite(redPin, red);
  lcd.write(" HiFi IR Volume ");
  
  irrecv.enableIRIn();
  
  pinMode(slaveSelectPin, OUTPUT);
  SPI.begin(); 
}

void decodeIR(int value) {
  switch (value) {
  case 0x281:
    mute = !mute;
    lcd.setCursor(0, 0);
    if (mute == true) {
      lcd.write("Muted                ");
      setGain(1);
    }
    if (mute == false) {  
      setGain(nGain);
      setVolumeBar(nGain);
    }
    delay(1000);
    break;
  case 0x481:
    if (nGain < 255) {  
      nGain = nGain + 1;
      setGain(nGain);
      setVolumeBar(nGain);
    }
    else {
      lcd.setCursor(0, 0);
      lcd.write("Maximum Reached");
      delay(500);
    }  
    break;
  case 0xC81:
    if (nGain > 1) {
      nGain = nGain - 1;
      setGain(nGain);
      setVolumeBar(nGain);
    }
    else {
      lcd.setCursor(0,0);
      lcd.write("Minimum Reached ");
      delay(500);     
    }
    break;
  }
  red = nGain;
  analogWrite(redPin, ~red);
}

void setGain(int nGain) {
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(nGain);  // right channel
  SPI.transfer(nGain);  // left channel
  digitalWrite(slaveSelectPin,HIGH);
}

void setVolumeBar(int nGain) {
  // convert gain to a decibal string, and write to the lcd
  iGain = 31.5 - 0.5*(255 - float(nGain));       
  gain.toCharArray(icGain, 6);
  dtostrf(iGain,4,1,icGain);
  gain = String(icGain);
  lcd.clear();
  ("Volume: " + gain + " dB").toCharArray(cGain,17);
  lcd.setCursor(0, 0);  //first line
  lcd.write(cGain);
  // write the volume bar
  lcd.setCursor(0, 1);  //second line
  for (int i =  0; i < 16; i++) {
    if (nGain/2 > i*8) {
      lcd.setCursor(i, 1);
      lcd.write(round(i/2));  
    }
  }  
}

void loop() {
  // receive an IR signal
  if (irrecv.decode(&results)) {
    decodeIR(results.value);
    irrecv.resume();
  }
}