work in progress

Dependencies:   FastAnalogIn FastIO USBDevice mbed FastPWM SimpleDMA

Fork of Pinscape_Controller by Mike R

TLC5940/TLC5940.h

Committer:
mjr
Date:
2015-09-23
Revision:
28:cb71c4af2912
Child:
30:2097c6f8f2db

File content as of revision 28:cb71c4af2912:

// Pinscape Controller TLC5940 interface
//
// Based on Spencer Davis's mbed TLC5940 library.  Adapted for the
// KL25Z, and simplified to just the functions needed for this
// application.  In particular, this version doesn't include support 
// for dot correction programming or status input.  This version also
// uses a different approach for sending the grayscale data updates,
// sending updates during the blanking interval rather than overlapping
// them with the PWM cycle.  This results in very slightly longer 
// blanking intervals when updates are pending, effectively reducing 
// the PWM "on" duty cycle (and thus the output brightness) by about 
// 0.3%.  This shouldn't be perceptible to users, so it's a small
// trade-off for the advantage gained, which is much better signal 
// stability when using multiple TLC5940s daisy-chained together.
// I saw a lot of instability when using the overlapped approach,
// which seems to be eliminated entirely when sending updates during
// the blanking interval.

 
#ifndef TLC5940_H
#define TLC5940_H

#include "mbed.h"
#include "FastPWM.h"

/**
  * SPI speed used by the mbed to communicate with the TLC5940
  * The TLC5940 supports up to 30Mhz.  It's best to keep this as
  * high as the microcontroller will allow, since a higher SPI 
  * speed yields a faster grayscale data update.  However, if
  * you have problems with unreliable signal transmission to the
  * TLC5940s, reducing this speed might help.
  *
  * The SPI clock must be fast enough that the data transmission
  * time for a full update is comfortably less than the blanking 
  * cycle time.  The grayscale refresh requires 192 bits per TLC5940 
  * in the daisy chain, and each bit takes one SPI clock to send.  
  * Our reference setup in the Pinscape controller allows for up to 
  * 4 TLC5940s, so a full refresh cycle on a fully populated system 
  * would be 768 SPI clocks.  The blanking cycle is 4096 GSCLK cycles.  
  *
  *   t(blank) = 4096 * 1/GSCLK_SPEED
  *   t(refresh) = 768 * 1/SPI_SPEED
  *   Therefore:  SPI_SPEED must be > 768/4096 * GSCLK_SPEED
  *
  * Since the SPI speed can be so high, and since we want to keep
  * the GSCLK speed relatively low, the constraint above simply
  * isn't a factor.  E.g., at SPI=30MHz and GSCLK=500kHz, 
  * t(blank) is 8192us and t(refresh) is 25us.
  */
#define SPI_SPEED 3000000

/**
  * The rate at which the GSCLK pin is pulsed.   This also controls 
  * how often the reset function is called.   The reset function call
  * rate is (1/GSCLK_SPEED) * 4096.  The maximum reliable rate is
  * around 32Mhz.  It's best to keep this rate as low as possible:
  * the higher the rate, the higher the refresh() call frequency,
  * so the higher the CPU load.
  *
  * The lower bound is probably dependent on the application.  For 
  * driving LEDs, the limiting factor is that lower rates will increase
  * visible flicker.  200 kHz seems to be a good lower bound for LEDs.  
  * That provides about 48 cycles per second - that's about the same as
  * the 50 Hz A/C cycle rate in many countries, which was itself chosen
  * so that incandescent lights don't flicker.  (This rate is a function 
  * of human eye physiology, which has its own refresh cycle of sorts
  * that runs at about 50 Hz.  If you're designing an LED system for
  * viewing by cats or drosophila, you might want to look into your
  * target species' eye physiology, since the persistence of vision
  * rate varies quite a bit from species to species.)  Flicker tends to 
  * be more noticeable in LEDs than in incandescents, since LEDs don't
  * have the thermal inertia of incandescents, so we use a slightly
  * higher default here.  500 kHz = 122 full grayscale cycles per
  * second = 122 reset calls per second (call every 8ms).
  */
#define GSCLK_SPEED    500000

/**
  *  This class controls a TLC5940 PWM driver IC.
  *
  *  Using the TLC5940 class to control an LED:
  *  @code
  *  #include "mbed.h"
  *  #include "TLC5940.h"
  *  
  *  // Create the TLC5940 instance
  *  TLC5940 tlc(p7, p5, p21, p9, p10, p11, p12, 1);
  *  
  *  int main()
  *  {   
  *      // Enable the first LED
  *      tlc.set(0, 0xfff);
  *      
  *      while(1)
  *      {
  *      }
  *  }
  *  @endcode
  */
class TLC5940
{
public:
    /**
      *  Set up the TLC5940
      *  @param SCLK - The SCK pin of the SPI bus
      *  @param MOSI - The MOSI pin of the SPI bus
      *  @param GSCLK - The GSCLK pin of the TLC5940(s)
      *  @param BLANK - The BLANK pin of the TLC5940(s)
      *  @param XLAT - The XLAT pin of the TLC5940(s)
      *  @param nchips - The number of TLC5940s (if you are daisy chaining)
      */
    TLC5940(PinName SCLK, PinName MOSI, PinName GSCLK, PinName BLANK, PinName XLAT, int nchips)
        : spi(MOSI, NC, SCLK),
          gsclk(GSCLK),
          blank(BLANK),
          xlat(XLAT),
          nchips(nchips),
          newGSData(false)
    {
        // allocate the grayscale buffer
        gs = new unsigned short[nchips*16];
        
        // Configure SPI format and speed.  Note that KL25Z ONLY supports 8-bit
        // mode.  The TLC5940 nominally requires 12-bit data blocks for the
        // grayscale levels, but SPI is ultimately just a bit-level serial format,
        // so we can reformat the 12-bit blocks into 8-bit bytes to fit the 
        // KL25Z's limits.  This should work equally well on other microcontrollers 
        // that are more flexible.  The TLC5940 appears to require polarity/phase
        // format 0.
        spi.format(8, 0);
        spi.frequency(SPI_SPEED);
        
        // Set output pin states
        xlat = 0;
        blank = 1;
        
        // Configure PWM output for GSCLK frequency at 50% duty cycle
        gsclk.period(1.0/GSCLK_SPEED);
        gsclk.write(.5);
        blank = 0;

        // Set up the first call to the reset function, which asserts BLANK to
        // end the PWM cycle and handles new grayscale data output and latching.
        // The original version of this library uses a timer to call reset
        // periodically, but that approach is somewhat problematic because the
        // reset function itself takes a small amount of time to run, so the
        // *actual* cycle is slightly longer than what we get from counting
        // GS clocks.  Running reset on a timer therefore causes the calls to
        // slip out of phase with the actual full cycles, which causes 
        // premature blanking that shows up as visible flicker.  To get the
        // reset cycle to line up exactly with a full PWM cycle, it works
        // better to set up a new timer on each cycle, *after* we've finished
        // with the somewhat unpredictable overhead of the interrupt handler.
        // This ensures that we'll get much closer to exact alignment of the
        // cycle phase, and in any case the worst that happens is that some
        // cycles are very slightly too long or short (due to imperfections
        // in the timer clock vs the PWM clock that determines the GSCLCK
        // output to the TLC5940), which is far less noticeable than a 
        // constantly rotating phase misalignment.
        reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0);
    }
    
    ~TLC5940()
    {
        delete [] gs;
    }

    /**
      *  Set the next chunk of grayscale data to be sent
      *  @param data - Array of 16 bit shorts containing 16 12 bit grayscale data chunks per TLC5940
      *  @note These must be in intervals of at least (1/GSCLK_SPEED) * 4096 to be sent
      */
    void set(int idx, unsigned short data) 
    {
        // store the data, and flag the pending update for the interrupt handler to carry out
        gs[idx] = data; 
        newGSData = true;
    }

private:
    // current level for each output
    unsigned short *gs;
    
    // SPI port - only MOSI and SCK are used
    SPI spi;

    // use a PWM out for the grayscale clock - this provides a stable
    // square wave signal without consuming CPU
    FastPWM gsclk;

    // Digital out pins used for the TLC5940
    DigitalOut blank;
    DigitalOut xlat;
    
    // number of daisy-chained TLC5940s we're controlling
    int nchips;

    // Timeout to end each PWM cycle.  This is a one-shot timer that we reset
    // on each cycle.
    Timeout reset_timer;
    
    // Has new GS/DC data been loaded?
    volatile bool newGSData;

    // Function to reset the display and send the next chunks of data
    void reset()
    {
        // turn off the grayscale clock, and assert BLANK to end the grayscale cycle
        gsclk.write(0);
        blank = 1;        

        // If we have new GS data, send it now
        if (newGSData)
        {
            // Send the new grayscale data.
            //
            // Note that ideally, we'd do this during the new PWM cycle
            // rather than during the blanking interval.  The TLC5940 is
            // specifically designed to allow this.  However, in my testing,
            // I found that sending new data during the PWM cycle was
            // unreliable - it seemed to cause a fair amount of glitching,
            // which as far as I can tell is signal noise coming from
            // crosstalk between the grayscale clock signal and the 
            // SPI signal.  This seems to be a common problem with
            // daisy-chained TLC5940s.  It can in principle be solved with
            // careful high-speed circuit design (good ground planes, 
            // short leads, decoupling capacitors), and indeed I was able
            // to improve stability to some extent with circuit tweaks,
            // but I wasn't able to eliminate it entirely.  Moving the
            // data refresh into the blanking interval, on the other 
            // hand, seems to entirely eliminate any instability.
            //
            // Note that there's no CPU performance penalty to this 
            // approach.  The KL25Z SPI implementation isn't capable of
            // asynchronous DMA, so the CPU has to wait for the 
            // transmission no matter when it happens.  The only downside
            // I see to this approach is that it decreases the duty cycle
            // of the PWM during updates - but very slightly.  With the
            // SPI clock at 30 MHz and the PWM clock at 500 kHz, the full
            // PWM cycle is 8192us, and the data refresh time is 25us.
            // So by doing the data refersh in the blanking interval, 
            // we're effectively extending the PWM cycle to 8217us, 
            // which is 0.3% longer.  Since the outputs are all off 
            // during the blanking cycle, this is equivalent to 
            // decreasing all of the output brightnesses by 0.3%.  That
            // should be imperceptible to users.
            update();

            // the chips are now in sync with our data, so we have no more
            // pending update
            newGSData = false;
            
            // latch the new data while we're still blanked
            xlat = 1;
            xlat = 0;
        }

        // end the blanking interval and restart the grayscale clock
        blank = 0;
        gsclk.write(.5);
        
        // set up the next blanking interrupt
        reset_timer.attach(this, &TLC5940::reset, (1.0/GSCLK_SPEED)*4096.0);
    }
    
    void update()
    {
        // Send GS data.  The serial format orders the outputs from last to first
        // (output #15 on the last chip in the daisy-chain to output #0 on the
        // first chip).  For each output, we send 12 bits containing the grayscale
        // level (0 = fully off, 0xFFF = fully on).  Bit order is most significant 
        // bit first.  
        // 
        // The KL25Z SPI can only send in 8-bit increments, so we need to divvy up 
        // the 12-bit outputs into 8-bit bytes.  Each pair of 12-bit outputs adds up 
        // to 24 bits, which divides evenly into 3 bytes, so send each pairs of 
        // outputs as three bytes:
        //
        //   [    element i+1 bits   ]  [ element i bits        ]
        //   11 10 9 8 7 6 5 4 3 2 1 0  11 10 9 8 7 6 5 4 3 2 1 0
        //   [  first byte   ] [   second byte  ] [  third byte ]
        for (int i = (16 * nchips) - 2 ; i >= 0 ; i -= 2)
        {
            // first byte - element i+1 bits 4-11
            spi.write(((gs[i+1] & 0xFF0) >> 4) & 0xff);
            
            // second byte - element i+1 bits 0-3, then element i bits 8-11
            spi.write((((gs[i+1] & 0x00F) << 4) | ((gs[i] & 0xF00) >> 8)) & 0xFF);
            
            // third byte - element i bits 0-7
            spi.write(gs[i] & 0x0FF);
        }
    }
};

 
#endif