Thursday 5 May 2016

Surround Sound Switch #6: Arduino (remote control)

SparkFun Electronics Arduino Uno R3
Previously:
Mother Of all Relay Boxes
Rolling your own Commutator
Bulgaria (rotary switches)
Group Theory (toggle switches)
Relayer (electromagnetic relays)
Adding remote control to the relay-based prototype can be fairly trivial, since quite often the work has mostly been done for us already by others. One such easy route is via Arduino and infra-red, for which many IR receiver modules are cheaply available. Also available incidentally are WiFi and Bluetooth modules for Arduino, not to mention the fully Wi-Fi integrated MKR1000 and Uno WiFi, so there's no shortage of options. But today, I'll just be looking at IR.

Arduino pins driving transistors driving relays.
The Arduino Interface

Regardless of the connectivity solution adopted, the first requirement is for the Arduino device to take over operation of the four signals controlling the seven relays. At the moment these terminate at the thumbwheel switch, which can selectively operate one or more coils by connecting their lower ends to 0V. In the case of a relay pair, this results in a current of 150mA sinking through the switch contact - much more than an Arduino digital output can either sink or source (20mA continuous recommended, 40mA absolute max).

The solution is to use a transistor, as shown in this circuit diagram, to amplify the current capacity between each Arduino output and its associated relay coil(s). Here I've used my old favourite, the silicon bipolar npn device; MOSFETs are another option. The relay drivers are on pins 2 (45°), 4 (90°), 7 (180°), and 8 (Mode 5). The four so-called freewheeling diodes (e.g. 1N4007), slung across the relay coils in reverse, protect the transistors from the back EMF generated when the highly inductive load is switched. Now when one of these four Arduino outputs goes high (+5V), current flows through a resistor into the transistor base, switching it on. This allows a larger current to flow from the +12Vdc rail through the relay coil(s) and the transistor to 0V. Any general purpose npn transistor with the following specifications will do:
min DC current gain hFE ≥ 40
max DC collector current IC ≥ 200mA
max collector-emitter voltage VCEO ≥ 20V
max total power dissipation Ptot ≥ 100mW
The popular 2N2222 is one example of a suitable component, but note that its frequently cited European "functional equivalent" BC548 is actually ruled out by having too low a maximum collector current (100mA). Now let's choose an appropriate resistor value:
Rmax = Vcc / (Imax / hFE) = 5V / (150mA / 40) = 5V / 3.75mA = 1.3kΩ.
I'd probably recommend using 1kΩ or so for that extra 30% safety margin. If the base resistor value is too high, the base current will be too low to ensure the transistor saturates and remains outside its high dissipation, potentially destructive "linear" mode. By contrast, when in digital mode, the transistor is either fully off (collector current is zero) or fully on (collector-emitter voltage is essentially zero), so in each case, the power P = I * V ≈ 0.

The IR Library

The Arduino microcontroller development system has access to an excellent free IR control library, Arduino IRremote, by Ken Shirriff. Thanks Ken! This code resource both sends and receives infra-red signals. Many people have made use of this, including Jason Poel Smith, who very reasonably asks,
Most of the buttons on a remote control are never used.
So why not use them to control appliances and other electronics around your house?
then goes on to do just that - repurposing any unused command on any of your IR remotes, to control an electrical outlet switch. He even includes a simple and easy-to-use learning mode, whereby a single additional button press is all you need to teach your electronics which new signal it has to respond to.

The Command Set

Our requirements are a little more complicated than controlling the state of a single relay, but not by that much. We have to drive three transistors to control the orientation, and a fourth for Mode 5. So, four digital outputs, rather than one? No big deal.

Now for our UI commands. We'd like buttons to take us directly to a particular orientation, numbered maybe 0-7, maybe 1-8, or maybe mapped to the physical layout of a numeric pad - whatever you prefer. Two more buttons, to rotate from the current orientation by 45° increments, either left or right. A toggle, and/or two separate commands, to engage/disengage Mode 5. A reset button to set the orientation back to 0 and disengage Mode 5.

Sketch

Can't remember the last time the blog known as My Code Here contained any actual computer code, but anyway, here is the full Arduino sketch source for the project:

/*
  RoomSpin
  Audio soundstage rotation switch for 7.x surround sound system with 8 satellites
  http://mycodehere.blogspot.co.uk/2016/05/surround-sound-switch-6-arduino-remote.html
  This code is in the public domain - created 6 March 2016 by John Michael Kerr
*/

#include <irremote.h>
#include <irremoteint.h>

//#define TEST

void setup()
{
  setupRelays();
  #ifdef TEST
    setupTest();
  #else
    setupMain();
  #endif
}

void loop()
{
  #ifdef TEST
    loopTest();
  #else
    loopMain();
  #endif
}

// Main program setup & loop

void setupMain()
{
  setupReceiver();
}

void loopMain()
{
  long code = readReceiver();
  if (code)
    performCode(code);
}

// IR receiver handling

const int pinIR = A5;

IRrecv* receiver;
decode_results code;

void setupReceiver()
{
  Serial.begin(9600);
  receiver = new IRrecv(pinIR);
  receiver->enableIRIn();
}

long readReceiver()
{
  long result = 0;
  if (receiver->decode(&code))
  {
    result = code.value;
    Serial.println(result, HEX);
    receiver->resume();
  }
  return result;
}

// Command codes

const long
  codeDigits[8] =
  {
    0xbeef0000,
    0xbeef0001,
    0xbeef0002,
    0xbeef0003,
    0xbeef0004,
    0xbeef0005,
    0xbeef0006,
    0xbeef0007
  },
  codeLeft = 0xbeef0008,
  codeRight = 0xbeef0009,
  codeMode5_ON = 0xbeef000A,
  codeMode5_OFF = 0xbeef000B,
  codeMode5_TOGGLE = 0xbeef000C,
  codeReset = 0xbeef000D;

// Command codes for Sony BD (RMT-B119P)
//
//const long
//  codeDigits[8] =
//  {
//    0x00090B47, // 0
//    0x00000B47, // 1
//    0x00080B47, // 2
//    0x00040B47, // 3
//    0x000C0B47, // 4
//    0x00020B47, // 5
//    0x000A0B47, // 6
//    0x00060B47  // 7
//  },
//  codeLeft = 0x000DCB47, // Left arrow
//  codeRight = 0x0003CB47, // Right arrow
//  codeMode5_ON = 0x000E0B47, // 8
//  codeMode5_OFF = 0x00010B47, // 9
//  codeMode5_TOGGLE = 0x00066B47, // Blue
//  codeReset = 0x000E6B47; // Red

int compass = 0;
bool mode5 = false;

bool codeToMode(long code)
{
  switch (code)
  {
    case codeMode5_ON:
      return true;
    case codeMode5_OFF:
      return false;
  }
  return !mode5;
}

int performCode(long code)
{
  for (int c = 0; c < 8; c++)
    if (code == codeDigits[c])
      return rotateTo(c);
  switch (code)
  {
    case codeLeft:
      return rotateBy(-1);
    case codeRight:
      return rotateBy(+1);
    case codeMode5_ON:
    case codeMode5_OFF:
    case codeMode5_TOGGLE:
      return setMode5(codeToMode(code));
    case codeReset:
      mode5 = false;
      return rotateTo(0);
  }
  return 0;
}

int rotateBy(int eighths)
{
  return rotateTo(compass + eighths);
}

int rotateTo(int eighths)
{
  compass = eighths & 7;
  setRelays();
  return compass;
}

int setMode5(bool value)
{
  mode5 = value;
  setRelays();
  return 0;
}

// Drive the relays

const int pinCTRL[4] = {7, 8, 12, 13};

void setRelayMask(int pin, int mask)
{
  setPinMask(pin, compass, mask);
}

void setRelays()
{
  for (int p = 0; p < 3; p++)
    setRelayMask(pinCTRL[p], 1 << p);
  setPinIf(pinCTRL[3], mode5);
  delay(100); // Let the relays settle.
}

void setupRelays()
{
  for (int p = 0; p < 4; p++)
    pinMode(pinCTRL[p], OUTPUT);
}

// Low level I/O support

void setPinIf(int pin, bool condition)
{
  digitalWrite(pin, condition ? HIGH : LOW);
}

void setPinMask(int pin, int value, int mask)
{
  setPinIf(pin, (value & mask) != 0);
}

// End of tab

This listing shows placeholders for the actual IR remote codes generated by your remote. Run the program with the Serial Monitor enabled, then blast it with your own remote, making note of the hex code generated by each of your chosen command buttons. Then search my source for the string 0xbeef, and replace these hex constants with your own. The numeric keys (here numbered 0 to 7) are stored in order in the codeDigits array, and the command names following these should be self-explanatory.

I'm currently using this prototype with codes for a Sony BDPS590 Blu-Ray player (remote control model number RMT-B119P), these are the codes in the commented-out section below the placeholders. Known affectionately to my wife and me as as "stubby buttons", this is a well-behaved remote - most buttons generate a single code followed by a stream of 0xFFFFFFFF, as long as they're held down. The only exceptions are volume up/down, mute, and the other TV buttons, whose output depends entirely upon which make & model of TV you've programmed it for. With other remote brands, be prepared to do a little C++ protocol tweaking to handle alternate and/or repeating code complications.

Wire Test

Last time I promised you a fully automated wiring test using just the Arduino Uno with no additional hardware. How are we going to achieve that with only 14 digital I/O pins available on the development board, when there are 19 or 20 terminations on our relay loom? Count them: 4 control inputs, and on the audio side, 7 or 8 inputs plus 8 outputs. Answer: by pressing the Arduino's six analog inputs A0-A5 into service. These work just as well as digital inputs, and bring the available total to exactly plenty. In fact I've already used A5 to interface the IR receiver module (pinIR in the code), rather than the default pin 11.

Say we keep the existing pins 2/4/7/8 attached to the four coil controls, as in the diagram above. Now associate pins 3/5/6/9/10/11/12/13 respectively with the eight audio amplifier outputs. For test purposes these will take the place of the physical amplifier outputs in real life.

Next, for the loudspeaker inputs, associate analog inputs A0-A5, operating in digital mode, with the first six, and pins 0/1 with the remaining two.  The IR receiver module must be disconnected from pin A0 during this test. Now all our test program needs to do is drive the coil controls with every binary pattern from 0 to 15, and for each pattern, walk a single bit (actually a logic zero) from the first audio amplifier output through to the last, checking that it appears only on the expected loudspeaker input pin, if any.

Note the change in I/O terminology here. While designing the relay network, we called the amplifier signals inputs and the loudspeaker destinations outputs. That made sense from the viewpoint of the switch. Now in the Arduino software, from the perspective of the system testing the switch, our ins & outs are swapped around.

Here is the source code for the wire test, which should be added as a new tab to the main code above. Then in the main sketch, remove the double slashes from the line //define TEST. Remember to undo this edit (and reconnect the IR receiver module) once the wire test is complete.

/*
  WireTest
  A wiring test utility for the RoomSpin project
  http://mycodehere.blogspot.co.uk/2016/05/surround-sound-switch-6-arduino-remote.html
  This code is in the public domain - created 6 March 2016 by John Michael Kerr
*/

const int
  pinIN[8] = {A0, A1, A2, A3, A4, A5, 0, 1},
  pinOUT[8] = {3, 5, 6, 9, 10, 11, 12, 13};
  
int
  output,
  expected,
  actual;

void setupTest()
{
  for (int p = 0; p < 8; p++)
  {
    pinMode(pinIN[p], INPUT_PULLUP);
    pinMode(pinOUT[p], OUTPUT);
  }
  writeOutput(0xFF);
}

void loopTest()
{
  for (compass = 0; compass < 8; compass++)
  {
    setRelays();
    for (int mask = 1; mask < 0x100; mask <<= 1)
    {
      writeOutput(mask ^ 0xFF);
      delay(50); // Let the outputs settle.
      readExpected();
      readActual();
      while (actual != expected); // Crash!
    }
    writeOutput(0xFF);
  }
  mode5 = !mode5;
}

void readActual()
{
  actual = 0;
  for (int p = 0, mask = 1; p < 8; p++, mask <<= 1)
    if (digitalRead(pinIN[p]))
      actual |= mask;
    else
      actual &= ~mask;
}

void readExpected()
{
  int mask = output ^ 0xFF;
  if (mode5)
    mask = useMode5(mask);
  mask <<= compass;
  if (mask > 0xFF)
    mask >>= 8;
  expected = mask ^ 0xFF;
}

int useMode5(int mask)
{
  switch (mask)
  {
    case 0x01:
    case 0x40:
      return 0;
    case 0x02:
      return 0x01;
    case 0x20:
      return 0x40;
  }
  return mask;
}

void writeOutput(int value)
{
  output = value;
  for (int p = 0, mask = 1; p < 8; p++, mask <<= 1)
    setPinMask(pinOUT[p], output, mask);
}

// End of tab

There's one headache with using up all 20 I/O pins in this way. Serial communications normally proceed via Arduino pins 0 and 1. With these tied up, how are we to glean any diagnostic information form the wire test?

My simple solution is first to add LEDs with series current limiting resistors to all twelve output pins (four relay drivers and eight audio channels). Now run the test, and jump into an infinite loop as soon as any unexpected result occurs. That's the function of this rather suspect looking line of code, with its barely noticeable empty loop statement:
while (actual != expected); // Crash!
All being well, these LEDs will flash binary patterns and masks, repeating one full test cycle every eight seconds. When the unthinkable happens, the LEDs become frozen, displaying in an unambiguous snapshot the state of all output signals, at the instant of fault detection. Yay diagnostics!

Here's a short video of the wire test in action. It's a bit less dramatic than its title suggests. But if you've read this far, you know that already.



In a past life, I worked with embedded systems and microcontroller projects, based on hardware such as the Motorola MC68HC705 series [pdf], for over 15 years (1980-1995). This is the first time I've used a high level language which I didn't have to design and implement entirely on my own. Okay, it's only C++ with a little preprocessor supplied syntactic sugar, but I'm still impressed. I like your brave new world!

Future Expansion

Hmm, so back into normal operation, and the Arduino Uno still has a bunch of those analog input pins free, eh. It's tempting to drive them with signals derived from the actual audio waveforms, suitably rectified and limited, then perhaps using some custom automatic gain control (AGC) code, translate those input levels to PWM brightnesses feeding a retro ring of eight front panel LEDs. When the audio is quiet, these LEDs could pull double duty by indicating the currently selected orientation.

In fact that was the thinking behind the quirky output pin selections for the relay drivers and the IR receiver module. Since outputs PD3/5/6 and PB1/2/3 are capable of PWM operation, they're reserved for future LED driving duty. I'd be happy enough driving these LEDs in pairs just like the relays, but if you demand one LED per audio channel, you might want to reassign some I/O and use the Arduino Leonardo. That board offers an additional PWM output on pin 13, as well as extending analog input capability to several of the digital I/O pins.

Any other additional features? Maybe we'd also like the switch to revert automatically to the default, powered-down state, after a few hours of inactivity - just so we don't accidentally leave the relay coils needlessly burning up the watts for weeks on end when not in use.

Two Distinct Defaults

Typically a switch like this will spends most of its life in just one particular orientation, with an occasional foray into a second, still less frequently a third, and so on. Obviously it's worth wiring the most frequently used orientation as the default one, which has been called "North" in my descriptions to date, and in which all seven relays are de-energised. Then for most of the time you can simply have the device unplugged or switched off, saving power and component life.

Less obviously, the second most popular switch state might benefit from being stored in non-volatile memory, and selected automatically on power up. That way, whenever movie night, holiday projector time, or whatever other occasion rocks up, you need only power up, and the sound stage rotates instantly to the secondary setting, ready for the evening's entertainment.

Such a fixed "secondary default" could easily be programmed with a few seconds' work. A better solution however might be to introduce a new command, allowing the current switch state to be saved in the Arduino microcontroller's non-volatile EEPROM memory with the press of a button, and subsequently, to be retrieved from there upon power up. Or to automate the process completely, write the state to EEPROM every time it's changed, so the switch effectively remembers its setting through a power cycle. Just be aware of the EEPROM erase/write limit of nominally 100,000 operations.

The EEPROM storage requirements of this design are reasonably low, at one half of a byte.

Next time: wrapping up.

No comments:

Post a Comment