Software PWM using daisy chained 74HC595s / SPI

(1/5) > >>

marklar:
It took quite some doing to get easy to use PWM control working with the 595 shift registers.  I figured I'd pass along a slimmed down version of the application that just implements the PWM control via a timer1 process.

The idea is to add "pwm pins" using daisy chained 595s and then to be able to use them just like normal pwm pins, not having to deal with the low level actions.  To this end I use a srPins array to hold the value and a pwmWrite function to write a 0 to 255 value for that pin.

This code should be easily reused / modified to run a number of shift registers.  

Using SPI allow for many more shift registers to be used with PWM control without flickering than a standard shiftOut mechansim.

Here is the code.
Code:

/**************************************************************
 * Name    : 74HC595 PWM SPI
 * By      : Joseph Francis
 *
 * Open Source
 *
 * Date    : 04 Jul, 2010                                      
 * Version : 1.3                                              
 * Notes   : Software PWM using daisy chained 74HC595 shift registers via SPI
 *
 * I wrote this code in a way that uses a srPins array to hold the byte pwm value for each pin.
 * Similar to doing an analogWrite, you can do a pwmWrite call to a "virtual" SR pin or just set srPins[x].
 * A timer process runs in the background to do PWM control, so the loop can concentrate on the process.
 *
 * Using this technique I have pwm controlled over 40 rgb leds with plenty of room for patterns, etc.
 *
 * While I would suggest using another chip to drive LEDs, the 595s provide an economical way to get started.
 ****************************************************************/

#include <TimerOne.h>

//--- Update TICKER_PWM_LEVELS to control how many levels you want
//     usually 32 levels of brightess is good (32K colors)
#define TICKER_PWM_LEVELS 32
#define TICKER_STEP 256/TICKER_PWM_LEVELS

//--- Update TIMER_DELAY to control how fast this runs.  
//    As long as you don't see flicker ... a higher number (slower) is better to assure
//    the loop has room to do stuff
#define TIMER_DELAY 280
#define SR_COUNT 6

//--- Used in iProcess to control the software PWM cycle
int ticker = 0;

//--- Pin connected to ST_CP of 74HC595
int latchPin = 10;
//--- Pin connected to SH_CP of 74HC595
int clockPin = 13;
//--- Pin connected to DS of 74HC595
int dataPin = 11;

//--- Used for faster latching
int latchPinPORTB = latchPin - 8;


//--- Holds a 0 to 255 PWM value used to set the value of each SR pin
byte srPins[SR_COUNT*8];

//--- Function used to set a shift register pin with a PWM value
//    can also just set srPins .. using this function allows you to map to pins if you need
//    a virtual mapping
void pwmWrite(int port, byte val){
  srPins[port] = val;  
}

//--- This process is run by the timer and does the PWM control
void iProcess(){
  //--- Create a temporary array of bytes to hold shift register values in
  byte srVals[SR_COUNT];
  //--- increment our ticker
  ticker++;
  //--- if our ticker level cycles, restart
  if( ticker > TICKER_PWM_LEVELS )
    ticker = 0;
  //--- get ticker as a 0 to 255 value, so we can always use the same data regardless of actual PWM levels
  int myPos = ticker * TICKER_STEP;

  //--- Loop through all bits in the shift register (8 pin for the 595's)
  for (int i = 0 ; i < 8; i++ ){
    int myLev = 0;
    //--- Loop through all shift registers and set the bit on or off
    for (int iSR = 0 ; iSR < SR_COUNT ; iSR++){
      //--- Start with the bit off
      myLev = 0;
      //--- If the value in the sr pin related to this SR/Byte is over the current pwm value
      //     then turn the bit on
      if (srPins[i+(iSR*8)] > myPos)
        myLev = 1;
      //--- Write the bit into the SR byte array
      bitWrite(srVals[iSR],i,myLev );
    }

  }


  //--- Run through all the temporary shift register values and send them (last one first)
  // latching in the process
  latchOff();
  for (int iSR = SR_COUNT-1 ; iSR >= 0 ; iSR--){
    spi_transfer(srVals[iSR]);
  }
  latchOn();

}


//--- Direct port access latching
void latchOn(){
  bitSet(PORTB,latchPinPORTB);
}
void latchOff(){
  bitClear(PORTB,latchPinPORTB);
}

//--- Used to setup SPI based on current pin setup
//    this is called in the setup routine;
void setupSPI(){
  byte clr;
  SPCR |= ( (1<<SPE) | (1<<MSTR) ); // enable SPI as master
  SPCR &= ~( (1<<SPR1) | (1<<SPR0) ); // clear prescaler bits
  clr=SPSR; // clear SPI status reg
  clr=SPDR; // clear SPI data reg
  SPSR |= (1<<SPI2X); // set prescaler bits
  delay(10);
}


//--- The really fast SPI version of shiftOut
byte spi_transfer(byte data)
{
  SPDR = data;                    // Start the transmission
  loop_until_bit_is_set(SPSR, SPIF);
  return SPDR;                    // return the received byte, we don't need that
}

//---- ignore (used for multiplexing but disabled for example)
#define GROUND_1 6
#define GROUND_2 7
//-- end ig

void setup() {
  Serial.begin(9600);

  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  //---- ignore (used for multiplexing but disabled for example)
  pinMode(GROUND_1, OUTPUT);
  pinMode(GROUND_2, OUTPUT);
  digitalWrite(GROUND_1,LOW);
  digitalWrite(GROUND_2,HIGH);
  //-- end ig

  digitalWrite(latchPin,LOW);
  digitalWrite(dataPin,LOW);
  digitalWrite(clockPin,LOW);

  //--- Setup to run SPI
  setupSPI();

  //--- Activate the PWM timer
  Timer1.initialize(TIMER_DELAY); // Timer for updating pwm pins
  Timer1.attachInterrupt(iProcess);

}

//--- Sets all the pins to the PWM value passed
void allTo(byte theVal){
  for( int i = 0 ; i < (SR_COUNT*8) ; i++ ){
    srPins[i] = theVal;
  }
}

//--- Blinks all pins full on and then off.  Total time for blink passed as the duration
void allBlink(int theDuration){
  for( int i = 0 ; i < (SR_COUNT*8) ; i++ ){
    srPins[i] = 255;
    delay(theDuration/2);
    srPins[i] = 0;
    delay(theDuration/2);
  }
}

//--- Very simple demo loop
void loop(){

  //--- fade all the pins up
  for( int i = 0 ; i < 256 ; i++ ){
    allTo(i);
    delay(10);
  }

  //--- all off then blink them all quickly
  allTo(0);
  delay(1000);
  allBlink(150);


  //Note: The below code assumes a common cathode RGB is on the first 595 with R,G,B pins hookedup respectively
  //--- Manually run the first RGB led through a color wheel
  //- Red to Yellow
  for( int i = 0 ; i < 256 ; i++ ){
    srPins[0] = 255;
    srPins[1] = i;
    srPins[2] = 0;
    delay(10);
  }
  //- Yellow to Green
  for( int i = 255 ; i >= 0 ; i-- ){
    srPins[0] = i;
    srPins[1] = 255;
    srPins[2] = 0;
    delay(10);
  }
  //- Green to Cyan
  for( int i = 0 ; i < 256 ; i++ ){
    srPins[0] = 0;
    srPins[1] = 255;
    srPins[2] = i;
    delay(10);
  }
  //- Cyan to Blue
  for( int i = 255 ; i >= 0 ; i-- ){
    srPins[0] = 0;
    srPins[1] = i;
    srPins[2] = 255;
    delay(10);
  }
  //- Blue to Majenta
  for( int i = 0 ; i < 256 ; i++ ){
    srPins[0] = i;
    srPins[1] = 0;
    srPins[2] = 255;
    delay(10);
  }
  //- Majenta back to Red
  for( int i = 255 ; i >= 0 ; i-- ){
    srPins[0] = 255;
    srPins[1] = 0;
    srPins[2] = i;
    delay(10);
  }

}


Here is a low quality video showing the code running on 16 RGBs.


Hope this help, let me know if you see any errors or room for improvements.



raron:
Nice work!

It might be helpful to link to the library used too? http://www.arduino.cc/playground/Code/Timer1
I haven't tested this, but at least it compiles now :)

marklar:
It will not let me update the original post.

Thanks for pointing to the needed library:
http://www.arduino.cc/playground/Code/Timer1

In the future I'll remember to include details about dependent libraries.

Regards

PostOfficeBuddy:
Excellent, thanks for sharing this. I'm going to be getting some STP04CM05 4-bit shift-register/high current LED drivers in the mail soon, and will try to adapt this code for that application. They also feature cascading so I'm guessing the only trick will be changing any 8-bit references in the code to 4-bits instead.

If this works I will be saving a lot of $ on hardware and a lot of head-banging on circuit design! Before I found the STP04CM05 the only cost-effective solution was a slew of TI PWM multiplexers, hex converters, transistors, and current regulators.

I will definitely buy you a beer in that case.  8-)

marklar:
Actually I think you may get away with using this with no modification to the 8 bit vs 4 bit.  The cheep and easy route may be to just cut the number you use in the SR_COUNT variable in 1/2.  So in your case if you have 8 shift registers cascaded, you would set SR_COUNT to 4 to equal the same number of pins.  

Try that first and see if everything else just works for you (it may).  

The reason is because you will most likely end up wanting to combine 4 bits of data together in pairs to equal a byte, but having a even number of 4 bit SRs makes it work just like an 8 bit SR.

Someone chime in here if I am bass ackward on this one please.

Cheers

Navigation

[0] Message Index

[#] Next page