Expanding PWM

Hi Everyone

I've seen forum posts about multiplexers/demultiplexers that can be used to expand the outputs of an Arduino board so as to, for example, control a greater number of LEDs. The examples I've seen for LEDs, however, seem to be only for turning LEDs on and off. I'm wondering if there's a way to control a large number LEDs such that they are dimmable (expanding the Arduino's PWM output somehow).

Thanks!
Jesse

I have TLC5940NT chips on order, about $4 each from digikey. These are 16 channel LED drivers with 4096 shades of PWM gray on each. The arduino can drive them with just a couple digital lines and a clock. You can daisy chain them if you need more than one, so you won't take more arduino lines. There is nifty logic in there for calibrating your LEDs, but I won't have the voltage to program that in the circuit.

There are a number of similar products, I chose by what I could get in a DIP package.

Have you thought about how to program the serial data transfer for that chip? The GS packet format makes enough sense, but how fast can the clock change and how tolerant is the TLC5940 of clock variation? I was interested in using that chip (very interested) but haven't used it because I have no idea how to program the ATMega168 to interface with it.

How I interpret having to code the ATMega168 (for simplicity, just turning all the bits on for a "full on" to all 16 outputs):

main loop {
  digitalWrite( toTLCClock, 0 );
  digitalWrite( toTLCIn, 0 );

  for ( x = 0; x <= 191; x++) {
    digitalWrite( toTLCIn, 1 );
    digitalWrite( toTLCClock, 1);
    delayMicroseconds( 3 );
    digitalWrite( toTLCClock, 0);
  }

  digitalWrite ( toTLCLatch, 1 );
  delayMicroseconds( 3 );
  digitalWrite ( toTLCLatch, 0 );
  digitalWrite ( toTLCClock, 1 );
  delayMicroseconds( 3 );
  digitalWrite ( toTLCClock, 0 );
}

Does that vague pseudocode look right? I have no idea if the ATMega chip is fast enough to blow out the ~10 nanosecond delays required by the TLC chip, so I put in some short microsecond delays. Obviously I would base the inner for loop on some array with the output level data in it but this is just an idea for the serial format.

Thanks, I'd be keenly interested in seeing documentation if someone is able to get something like this working. My personal goal in this would be to have computer controlled dimmable LEDs (by max-msp or similar) for a light/sound installation.

That code by sdbrown looks like what I am planning. I am not anticipating a need for delays, the arduino should be well under the maximum clock rates of the LED driver. I also don't anticipate any kind of jitter issue since the bit clocking is synchronous. I could be surprised, but that is what I expect now.

I'm trying to do the same thing, but was slightly off track for a while. And now, since the led driver listed above isn't available on digikey anymore, I'm curious why I couldn't daisy chain a couple of ATMEGA168 chips together, utilizin their PWM outputs. Is this crazy? Is there any other way to do this without the led driver?

TI has a free sample service... hunt around their website. you usually have to be logged in to find it.

There are some really nice output expanders from Maxim-IC, like the MAX7213, but you have to deal with their teensy-tiny QFN or THIN packages.

D

And now, since the led driver listed above isn't available on digikey anymore, I'm curious why I couldn't daisy chain a couple of ATMEGA168 chips together, utilizin their PWM outputs. Is this crazy?

No, it's not crazy at all. In fact, I just got done doing this very thing: I have one ATMega168 as the server and 8 ATMega168s as clients. The server sends data out the serial TX pin which is received by the client chips and the client chips then make adjustments to their PWM outputs, which are connected to LEDs. The difference is primarily one of price: the cost of additional outputs using the TLC chip is less than half the cost of using the ATMega168 chip. On the other hand, by using the ATMega168 chips as clients you can add all sorts of extra functionality or just offload some processing from the server chip (which is what I've done).

sdbrown, I'm just curious what other components you needed along with the additional ATMEGA168s. Did you have to build a whole other Arduino board or were the chips standalone?

Thanks!

Branton

Sorry for the double post, but I've just found that the TLC5940NT is available again, so I'm going to give it a go. Can anyone give a more complete coding example on how to control PWM on multiple LEDs with this. I'm not real familiar with serial communication, etc. so even a generic example of how to send the GS info would be extremely helpful.

Thanks!

Branton

I should have parts in a couple days, as soon as digikey notices my backorder and fills it from the 153 they have in stock, I'll code up a library and post it here as soon as they come in.

I have placed an example of driving this part in the Playground, Arduino Playground - TLC5940. It doesn't rise to the level of library code, but it does get all the bits in and out.

Thanks to insights gained from jimS and Amp and others in the forum, I've developed this code for the TLC5490, just for fading across a row of LEDs smoothly.

To fully grok what's going on here, get the data sheet:

The biggest challenge was avoiding flicker, since the usual Arduino digitalWrite commands are a little too slow. Amp's "4MHz output from ports" post helped reveal to me the PORTB call, which is easy to use and much faster. Note I only needed to do this when pulsing the 5940's greyscale clock.

Have fun...

/*Ti 5940 16-port LED driver
 = overlapped fade across 16 LEDs at a low background level
 * Peter Mackey  June 2007  Pratt Digital Arts  pmackey@pratt.edu
 * smooth flickerless fading (thanks to Amp on the Arduino forum)
 * additional logic from David Cuartielles's & Marcus Hannerstig's LEDdriver demo
 */

//using the pin codes on the TLC5940NT to name the Arduino ports 
#define VPRG 2 //"chip pin 27 to Arduino pin 2"
#define SIN 3 
#define SCLK 7 
#define XLAT 4
#define BLANK 5
#define DCPRG 6
#define GSCLK 8 //note: but using PORTB method

#define FADEMIN 100 //lowest fade level LEDs will reach (min would be 0, max 4095)
#define FADEINCR 64   //determines how many steps it takes to run the desired range (lower=longer)

int fadeLevel[] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //stores a level for each of 16 ports
int faderNdx = 0;  //counter used in this fading sequence

int fadeState[] = {
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //stores the direction of fading for each port 1,0,-1
//start with first port

int word[] = {
  0,0,0,0,0,0,0,0,0,0,0,0}; //temp storage for reversing bits in a word (for greyscale setting)

void setup() {
  pinMode(VPRG, OUTPUT);    
  pinMode(SIN, OUTPUT);    
  pinMode(SCLK, OUTPUT); 
  pinMode(XLAT, OUTPUT);    
  pinMode(BLANK, OUTPUT);    
  pinMode(DCPRG, OUTPUT);    
  pinMode(GSCLK, OUTPUT);       //could also set DDRB directly

 preset();    //input “Dot Correction” data
}

void loop () {
  setGreys(); 
  feedPorts();    
  
  incrementFades();
  }
} 

void incrementFades() {
  //uses two 16 item arrays 
  //fadeState stores -1,0,or1 to specify direction of fade
  //fadeLevel stores current value of fade
  //too long to post, sorry...
}

//=======5940 control
void [b]setGreys[/b]()  {
  //data for each port (12 bit word * 16 ports =192 bits in this loop)...
  //read the fadeLevel array
  for (int i=15; i>=0; i--) { // ports, count DOWN
    int datb = fadeLevel[i];    

    //load fade level bits into the temp array BACKWARDS
    for (int j=11; j>=0; j--) { 
      word[j]=(datb & 1); //& bitwise AND      
      datb >>= 1;        //shift right and assign
      // (maybe there's a slicker way to do this!? but this works...)
    }
    //send the data to the 5940
    for (int j=0; j<12; j++) { 
      digitalWrite(SIN,word[j]);  
      pulseSCLK();
    }  
  }  
  digitalWrite(XLAT, HIGH);
  digitalWrite(XLAT, LOW);
}

void [b]feedPorts[/b]() { 
  //The actual sequencing of the PWM data into the LEDs, must do constantly...
  digitalWrite(BLANK, HIGH);    
  digitalWrite(BLANK, LOW);      //=all outputs ON, start PWM cycle

  for (int i=0; i<4096; i++) {
    pulseGSCLK();
  }
}

////DOT CORRECTION...do once
void [b]preset[/b]() {
  //Input “DotCorrex” Data
  //16 outputs, 64 posssible levels of adjustment, 6 bits/chan = 96 bits total
  //[use if any LEDs in array are physically too bright]

  digitalWrite(DCPRG, HIGH);      //leaving it H is my arbitrary choice (="write to register not EEPROM")  
  digitalWrite(VPRG, HIGH);     //=inputting data into dot correx register

  digitalWrite(BLANK, HIGH);      //=all outputs off, when this goes high it resets the greyscale counter
  digitalWrite(SIN, LOW);       //to start dot correction
  digitalWrite(XLAT, LOW);      

  //begin loading in the dot correx data, most significant bit first...
  //but here we are not correcting anything, so LSB is going first!
  for (int i=0; i<16; i++) { //16 ports
    for (int j=0; j<6; j++) { //6 bits of data for each port
      digitalWrite(SIN,      HIGH);      //for now, 111111 for everybody
      pulseSCLK();
      digitalWrite(SIN,      LOW);
    }
  }

  //FIRST GREYSCALE SETTING

  digitalWrite(XLAT, HIGH);   //latch the dot data into the dot correx register
  digitalWrite(XLAT, LOW);
  digitalWrite(VPRG, LOW);    //entering greyscale mode

  for (int i=0; i<16; i++) { //16 ports
    int datb = 4095;        //using same fade level for all ports this first time

    for (int j=0; j<12; j++) { //data for each port, all the same value to start
      digitalWrite(SIN,      datb & 01);      
      pulseSCLK();
      datb>>=1;
    }  
  }  
  digitalWrite(XLAT, HIGH);  //latch the greyscale data 
  digitalWrite(XLAT, LOW);
  pulseSCLK();               //193rd clock pulse only need to do the FIRST time after dot correx

  digitalWrite(BLANK, LOW);  //=all outputs ON, start PWM cycle... moved here
}

//SCLK used in dot correx and greyscale setting
void [b]pulseSCLK[/b]() {
  digitalWrite(SCLK, HIGH);
  digitalWrite(SCLK, LOW);
}

void [b]pulseGSCLK[/b]() {
  //ultra fast pulse trick, using digitalWrite caused flickering
  PORTB=0x01; //bring PORTB0 high (pin 8), other ports go low [0x01 does only pin 8, 0x21 also lifts pin 13]
  //16nanosecs is the min pulse width for the 5940, but no pause seems needed here
  PORTB=0x20;  //keep pin13 high for oard LED [0x00 would be all low]
}

I've used the 5940 for LED signs and it works great but the datasheet and features you have to deal with are bit daunting.

I wanted to point out the Arduino is fast enough to PWM all of it's pins smoothly, so depending on how many LED's you need dimming for this might be an alternative. I didn't throw in the Analog pins but there's nothing that would prevent it. You need to do some bitshifting and bitmath though to maintain the speed.

See this approach.

http://www.arduino.cc/playground/Main/PWMallPins

Hi,

How possible would it be to use 2 (or 3?) 5940s multiplexed to drive an 8x8 RGB LED matrix (like the one in sparkfun LED Matrix - Tri Color - Large - COM-00683 - SparkFun Electronics ) ?

would it be possible to get a good refresh rate for a non-flicker image?

Thanks,
Gonzalo

You can daisychain the 5940, I think something like 18 of them before load becomes an issue. Refresh rate can be an issue though. You won't get tearing, all of the 5940s will latch their data at the same time, but you do need to shift 192 bits times the number of 5940s, so if you had 4 of them for an 8x8 array that would be 768 bits and lets say it takes you 2 microseconds per bit that would still be a 650Hz refresh rate. If you configure for a 100Hz display refresh rate like I do in the playground article, and wanted to provide new data every frame, you would use about 20% of your CPU capacity shifting bits

Notice that part about "2 microseconds per bit". The playground article is going to take about 15 microseconds per bit, but since the time that was written arduino-0008 has come out and it is easy to bypass the digitalWrite() function and go directly to the registers. I think you might get it under 1 microsecond per bit with those functions.

Hi,

Let me see if I get this right. These are RGB led panels, so they have 8*24 leds...
I was actually thinking on the possibility of using 8x24 multiplexing of the PWM signals, so you would need only two 5940s to drive the each column and scan each row (or the opposite). But you would need to change the PWM values 8 times for each scan period... I think..

Would this work? I've seen the examples multiplexing 8x8 on/off led matrices, can the same approach be used with PWM signals for a full color matrix?

Thanks,
Gonzalo

Oh, RGB panels... the easy way is to use 12 5940s but that starts to get big and expensive and is a lot more power than you need and won't work with that panel's wiring, so... bad idea.

I think it will work. The 5940s only sink current so they will have to go on the 24 cathodes. You will need to switch the 8 anode lines from something that can source 480mA of current. (Maybe a 3:8 mux that drives up close to the power rail and 8 NPN transistors? You might need 8 current limiting resistors between the mux and the transistors.) Your anode source doesn't have to be terribly well regulated, just so at 480mA it still has enough voltage to drive 20mA through the LED. If you set your 24 series resistors so they burn off most of the excess power you shouldn't have a heat problem in the 5940.

For switching the anodes, I think you could rework the timing of the BLANK signal so there is a larger gap between refresh cycles and also an interrupt fired when the BLANK starts. Then your interrupt handler can be responsible for strobing in the next frame and changing the anodes.

Hi,

OK, thanks. I think I get it.
One question though... if the 5940 will go on the cathodes... shouldn't the transistors be PNP as they will go on the anodes?

Thanks again,

Gonzalo

Hi,

I am having problems compiling the 5940 sample code in the Playgroung (Arduino Playground - TLC5940).

For an atmega168 it gives me this:
error: 'TCCR2' was not declared in this scope

However, if I set my MCU to be an atmega16 the code compiles fine...
I am using arduino-0007 under linux (ubuntu FF)

any hint?
Thanks.