Interfacing with 0-10v input AC dimmers

Hi there!

Being a relatively electronics noob I've been playing with an Arduino. What I had in mind creating is the brain for a set of 0-10v AC dimmers. The software (sketch) is no problem at all and I'm well on my of having a 6ch smoothly fading program, but what I'm getting stuck on is the circuit to build for 'converting' the 0-5v PWM output to 'proper' 0-10v for controlling standard AC dimmers.

I've seen several comments about getting this and that (op-amps / transistors) but unfortunately never a complete instruction / circuit. I was hoping / wondering if anyone can point me to a good instruction for a complete circuit for this purpose.

Regards,

Arne

If anyone is interested, here is the first version of the sketch. It is a 6ch dimmer with each channel having it's own button + a master button. Pressing and holding a single channel button fades the channel up until max level after which the direction is reverted. Releasing + holding again will change the direction of the fade as well. A single pulse when in off state restores the previous light level, but pulsing when there is a light level switches the channel to off. The master button does the same, but then for all channels at the same time.

Next version I'm currently playing with has a separation of setting the target of the light level and the actual changing the level. This results in a smooth fading in / out when doing the 'pulse' controls.

/*
 *  Single button 6 channel dimmer with master button
 *
 *  Author:  Arne de Bree (arne@bukkie.nl)
 *
 *  +-------+------------+-----------+
 *  | Group | Button PIN | Light PIN |
 *  +-------+------------+-----------+
 *  |     1 |             2 |         3 |
 *  |     2 |             4 |         5 |
 *  |     3 |             7 |         6 |
 *  |     4 |             8 |         9 |
 *  |     5 |            12 |        10 |
 *  |     6 |            13 |        11 |
 *  +-------+------------+-----------+
 *
 *  Master button PIN:   analog 0
 *
 */

// ----------------------------------------------------------------- //

#define STEP_TIME             5       // minimal ms per step, lower is faster level change
#define PULSE_TIME            250     // ms to consider button state change to be a pulse

// ----------------------------------------------------------------- //

#define NR_GROUPS             6       // nr of groups (channels)

#define DIR_UP                1       // Direction of a fade
#define DIR_DOWN              0            

#define MAX_LIGHT_VALUE       255     // the maximum value a PWM output can have
#define MAX_ANALOG_IN_VALUE   1023

#define IDX_PIN_BTN           0       // the pin number the button is connected on
#define IDX_PIN_LIGHT         1       // The pin number the light is connected on

#define IDX_LAST_BTN_STATE    0       // Per group previous button state
#define IDX_LIGHT_VALUE       1       // The level of the light 
#define IDX_LAST_LIGHT_VALUE  2       // Previous level of the light 
#define IDX_DIR               3       // Fade direction

#define IDX_LAST_CHANGE       0       // Time of the last level change of the group
#define IDX_START_TIME        1       // Time of the last start of a level change of the group
#define IDX_STOP_TIME         2       // Time of the last stop of a level change of the group

// ----------------------------------------------------------------- //

/*
 * The data structure used in this application consists of three 2D arrays:
 *   - pins    => the pins used for connecting the buttons and the lights
 *   - groups  => several states of the channel like light level and fade direction
 *   - times   => milli seconds based time registration of the start and stop of an action
 */
 
/*
  0:  PIN_BTN
  1:  PIN_LIGHT
*/
int pins[NR_GROUPS][2] = {
       2,    3
  ,    4,    5
  ,    7,    6
  ,    8,    9
  ,   12,   10
  ,   13,   11
};

/*
  0:  lastBtnState
  1:  lightValue
  2:  lastLightValue
  3:  dir
*/
int groups[NR_GROUPS][4];

/*
  0:  lastChange
  1:  startTime
  2:  stopTime
*/
unsigned long times[NR_GROUPS][3];

// -------------------------------------------------------- //

void setup() 
{
  // Initialize the per-channel datastructures
  //
  for ( int i = 0; i < NR_GROUPS; i++ )
  {
    groups[i][IDX_LAST_BTN_STATE]   = LOW;
    groups[i][IDX_LIGHT_VALUE]      = 0;
    groups[i][IDX_LAST_LIGHT_VALUE] = 0;
    groups[i][IDX_DIR]              = DIR_UP;
    
    times[i][IDX_LAST_CHANGE]       = 0;
    times[i][IDX_START_TIME]        = 0;
    times[i][IDX_STOP_TIME]         = 0;    
    
    pinMode( pins[i][IDX_PIN_BTN],   INPUT );
    pinMode( pins[i][IDX_PIN_LIGHT], OUTPUT );
  }
}

// -------------------------------------------------------- //

boolean avgState = false;
int lastBtnStateMaster = LOW;

unsigned long startTimeMaster = 0;
unsigned long stopTimeMaster  = 0;

boolean startPulseMaster = false;
boolean stopPulseMaster  = false;

int actualMasterLevel = 0;

// -------------------------------------------------------- //

void loop() 
{  
  int btnStateMaster = ( analogRead( 0 ) > 1000 ) ? HIGH : LOW;
  
  startPulseMaster = false;
  stopPulseMaster  = false;
  
  // Was it a pulse?
  // 
  if ( btnStateMaster != lastBtnStateMaster )
  {    
    if ( HIGH == btnStateMaster )
    {
      // Make sure all directions are UP
      //
      for ( int i = 0; i < NR_GROUPS; i++ )
      {
        groups[i][IDX_DIR] = DIR_UP;
      }
    
      startTimeMaster = millis();
      
      unsigned long interval = startTimeMaster - stopTimeMaster;
      
      if ( PULSE_TIME > interval )
      {
        startPulseMaster = true;
      }
    }
    else
    {
      // Make sure all directions are DOWN
      //
      for ( int i = 0; i < NR_GROUPS; i++ )
      {
        groups[i][IDX_DIR] = DIR_DOWN;
      }
  
      stopTimeMaster = millis();
      
      unsigned long interval = stopTimeMaster - startTimeMaster;
      
      if ( PULSE_TIME > interval )
      {
        stopPulseMaster = true;
      }      
    }
    
    lastBtnStateMaster = btnStateMaster;
  }
 
  int aggrState = 0;
  int totalLevel = 0;
  for ( int i = 0; i < NR_GROUPS; i++ )
  {
    totalLevel += groups[i][IDX_LIGHT_VALUE];
    
    if ( 0 < groups[i][IDX_LIGHT_VALUE] )
    {
      aggrState++;
    }
  }  
  
  // Determine the average (binary) state the groups are in
  //
  avgState = ( NR_GROUPS / 2 <= aggrState );
  
  // Globally switch direction when all group reached max or min
  //
  if (  0 >= totalLevel )
  {
    for ( int i = 0; i < NR_GROUPS; i++ )
    {
      groups[i][IDX_DIR] = DIR_UP;
    }      
  }
  else if ( NR_GROUPS * MAX_LIGHT_VALUE <= totalLevel )
  {
    for ( int i = 0; i < NR_GROUPS; i++ )
    {
      groups[i][IDX_DIR] = DIR_DOWN;
    }     
  }
  
  // Process the possible changes per group
  //
  for ( int i = 0; i < NR_GROUPS; i++ )
  {
    processGroup( i, btnStateMaster );
  }
}

// -------------------------------------------------------- //

void processGroup( int id, int btnStateMaster ) {
  
  // Read button state but OR it with the masterbutton, so if either is HIGH it is HIGH
  //
  int btnState = digitalRead( pins[id][IDX_PIN_BTN] ) | btnStateMaster;

  // Was there a change with the previous state?
  //
  if ( groups[id][IDX_LAST_BTN_STATE] != btnState ) 
  {    
    groups[id][IDX_LAST_BTN_STATE] = btnState;

    // On button down we 
  
    if ( HIGH == btnState )
    {
      times[id][IDX_START_TIME] = millis();
    
      unsigned long stop_interval  = millis() - times[id][IDX_STOP_TIME];

      int stop_pulse  = PULSE_TIME > stop_interval;

      if ( stop_pulse )
      {
        groups[id][IDX_DIR] = !groups[id][IDX_DIR];
      }
      else
      { 
        groups[id][IDX_DIR] = DIR_UP;
      }
    }
    else
    {
      times[id][IDX_STOP_TIME] = millis();
      unsigned long interval = millis() - times[id][IDX_START_TIME];

      // If there was a 'pulse' restore last light value in case current value is 0
      // or set it to 0 when currently on
      //
      if ( PULSE_TIME > interval )
      {  
        if ( stopPulseMaster )
        {
          if ( false == avgState )
          {
            groups[id][IDX_LIGHT_VALUE] = groups[id][IDX_LAST_LIGHT_VALUE];
          }
          else 
          {
            groups[id][IDX_LAST_LIGHT_VALUE] = groups[id][IDX_LIGHT_VALUE];
            groups[id][IDX_LIGHT_VALUE] = 0;
          }                    
        }
        else          
        {
          if ( 0 == groups[id][IDX_LIGHT_VALUE] )
          {
            groups[id][IDX_LIGHT_VALUE] = groups[id][IDX_LAST_LIGHT_VALUE];
          }
          else if ( 0 < groups[id][IDX_LIGHT_VALUE] ) 
          {
            groups[id][IDX_LAST_LIGHT_VALUE] = groups[id][IDX_LIGHT_VALUE];
            groups[id][IDX_LIGHT_VALUE] = 0;
          }
        }
        
        analogWrite( pins[id][IDX_PIN_LIGHT], groups[id][IDX_LIGHT_VALUE] );        
      }
    }    
  }

  unsigned long start_interval = millis() - times[id][IDX_START_TIME];

  if ( HIGH == btnState && PULSE_TIME < start_interval )
  {
    unsigned long interval = millis() - times[id][IDX_LAST_CHANGE];
  
    // Only do a step every STEP_TIME
    //
    if ( STEP_TIME < interval )
    { 
      // In- or decrease lightvalue based upon direction currently set
      //
      groups[id][IDX_LIGHT_VALUE] = ( DIR_UP == groups[id][IDX_DIR] ) 
        ? groups[id][IDX_LIGHT_VALUE] + 1
        : groups[id][IDX_LIGHT_VALUE] - 1
      ;
  
      // Toggle direction, but only when fading the specific channel
      // when fading up or down with the master button direction changes 
      // are toggled on global level
      //
      if ( !btnStateMaster ) 
      { 
        if (        0 >= groups[id][IDX_LIGHT_VALUE] )
        {
          groups[id][IDX_DIR] = DIR_UP;
        }
        else if ( 255 <= groups[id][IDX_LIGHT_VALUE] )
        {       
          groups[id][IDX_DIR] = DIR_DOWN;
        }
      }
      
      // Make sure the light value doesn't go below 0 or above MAX_LIGHT_VALUE
      //
      groups[id][IDX_LIGHT_VALUE] = constrain( groups[id][IDX_LIGHT_VALUE], 0, MAX_LIGHT_VALUE );      
  
      // Store lightValue in case it needs to be restored when 'toggling'
      //
      groups[id][IDX_LAST_LIGHT_VALUE] = groups[id][IDX_LIGHT_VALUE];
  
      times[id][IDX_LAST_CHANGE] = millis();       
    }
  
    analogWrite( pins[id][IDX_PIN_LIGHT], groups[id][IDX_LIGHT_VALUE] );
  }
}
1 Like

Take a look at Operational amplifier applications - Wikipedia

You want a non-inverting amplifier with a gain of 2, so basically R1 = R2. Remember that Vcc for the op-amp has to be high enough to supply your output voltage.

Ok, thanx for your info, trying to get the part list together now... I found the following post:

http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1150063488/2#2

Where Massimo mentions the LM358:

http://www.national.com/mpf/LM/LM358.html#General%20Description

Which is actually two op-amps in a single IC.

So I assume that is a proper option for an op-amp, but how about the resistor values? I understand they need to be equal to get the x2 amplification, but the actual values I don't know how to decide. In several tutorials I encountered the use of 10k for short circuiting an input to gnd so the input would 'float'. Is this a sane value for the use in this opamp setup? Could someone help me on this?

And for the Vcc note you make, when I power the board and connect the op-amps V+ with 12V DC that should be high enough to run safely I expect. Could you confirm that?

One obstacle I might have is that the 0-10v dimmers I have don't like the fast PWM output. I can imagine this can be fixed by 'smoothing' the wave using a capacitor. A suggestion I found is "4.7uF through a 100R resistor". Should I place these components before or after the opamp? Or are these values incorrect to start with?

Should I place these components before or after the opamp?

After.

However an op-amp is not the best solution. Most cheap op amps won't go closer than 1.5V of the rails supplying it.

Just use a transistor with the collector resistor connected to 10 to 12V. Then you have a 10V PWM signal which you can smooth with your R&C.

Your response above and in PM (quoted below) combined I drew up the below circuit. Could you have a look at it and confirm this is what it should be?

Certainly that schematic you posted is most odd, but it is part way there. Connect the collector resistor to +9 to 12 V and make it a 10K value. Remove that capacitor. Connect the collector to the input of the dimmer through a 200R resistor. Then put a capacitor say 47uF from the input of the dimmer to ground.

The smoothing values are not that important. Too low and you might get flickering, too high and it will be sluggish to respond.

I had some trouble with the above scheme to actually have it output 0-10v... so I tried the OPAMP route, resulting in the following:

Any comments are welcome!

This topic is of great interest to me. Can I ask what AC dimmers you are using ? Velleman has the K8064 kit that is marketed as 0-10V but is capable of 0-5V as well. I was going to try tonight to see how well it handles the PWM input directly.

As for your schematic, how stable is the voltage at the 10V side ? Is that what C1 is for, to flatten the signal a bit ?

I will try this out tonight, thanks again.

The first schematic you posted is wrong but the right idea.

Connect the ground of the arduino direct to the - of the module.
Connect the capacitor from the ground to the + of the module.