TLC5940 and ATtiny84 - working!

I just got a TLC5940 hooked up to a ATtiny84!

I might make the code into a proper library if anybody's interested, leave votes below...

Did you use existing USI code or roll your own?

I rolled my own...

If the offer still stands I am definitely interested in your code. You do not need to take the time to turn it into a library.

The reason I never made a library is that it uses Timer0*, so millis(), delay(), etc. would stop working. That would cause a lot of support issues (driving the TLC5940 with hardware needs two timers).

The interrupt in the library is called at about 1ms intervals (with 8MHz CPU clock). It would be easy to increment a counter in there for program timing.

Here's the code FWIW: :slight_smile:

#ifndef TINY940_H
#define TINY940_H
#include <avr/io.h>
#include <avr/interrupt.h>

/*------------------------------------------------------------------------
  User configurable values
------------------------------------------------------------------------*/

#ifndef NUM_TLC5940s
#define NUM_TLC5940s 1
#endif


/*------------------------------------------------------------------------
  Standard data types
------------------------------------------------------------------------*/
#include <stdint.h>
typedef uint8_t byte;
typedef int16_t int16;
typedef uint16_t uint16;

/*------------------------------------------------------------------------
  Speed of timers
------------------------------------------------------------------------*/
// Timer 0 flips OC0A every (GSCLK_TIM+1) clock cycles
// With a setting of "1" we get a complete clock pulse every 4 CPU cycles
// ie. A 2mHz GSCLK at 8mHz CPU speed
#define GSCLK_TIM 1

// How many CPU cycles in a complete GSCLK pulse
#define GSCLK_CYCLES (2*(GSCLK_TIM+1))

/*------------------------------------------------------------------------
  Chip-specific things

  Most of the pin definitions can't be changed
------------------------------------------------------------------------*/

#if defined(__AVR_ATtiny84__)

// Pin definitions for ATtiny84
#define BLANK_PIN PB0
#define XLAT_PIN PB1
#define GSCLK_PIN PB2      /* Must be OC0A */
#define SERIAL_CLK PA4
#define SERIAL_OUT PA5
#define SERIAL_DDR DDRA    /* Where the serial direction bits are */
#define SERIAL_PORT PORTA  /* Where the serial output pins are */

void initHardwareTimers()
{
  // Timer 0 -> 2MHz GSCLK output on OC0A
  TCCR0A=0; TCCR0B=0;   // Stop the timer
  TIMSK0 = 0;           // No interrupts from this timer
  TCNT0 = 0;            // Counter starts at 0
  OCR0A = GSCLK_TIM;    // Timer restart value
  TCCR0A = _BV(WGM01)   // CTC mode
          |_BV(COM0A0); // Toggle OC0A output on every timer restart
  TCCR0B = _BV(CS00);   // Start timer, no prescale

  // Timer 1 -> Generate an interrupt every 4096 GSCLK pulses
  TCCR1A=0; TCCR1B = 0; // Stop the timer
  TCNT1 = 0;            // Counter starts at 0
  OCR1A = 0;            // Where the interrupt happens during the count
  ICR1 = GSCLK_CYCLES*4096; // There's four clock cycles for every CSCLK and 4096 per PWM cycle
  TIMSK1 = _BV(OCIE1A); // Interrupt every time the timer passes zero
  TCCR1A = 0;           // No outputs on chip pins
  TCCR1C = 0;           // Just in case...
  TCCR1B = _BV(WGM12)   // Start timer, CTC mode
          |_BV(WGM13)   // Start timer, CTC mode
          |_BV(CS10);   // no prescale
}

#elif defined (__AVR_ATtiny85__)

// Pin definitions for ATtiny85
#define BLANK_PIN PB3
#define XLAT_PIN PB4
#define GSCLK_PIN PB0      /* Must be OC0A */
#define SERIAL_CLK PB2
#define SERIAL_OUT PB1
#define SERIAL_DDR DDRB    /* Where the serial direction bits are */
#define SERIAL_PORT PORTB  /* Where the serial output pins are */

void initHardwareTimers()
{
  // Timer 0 -> 2MHz GSCLK output on OC0A
  TCCR0A=0; TCCR0B=0;   // Stop the timer
  TIMSK = 0;            // No interrupts from this timer
  TCNT0 = 0;            // Counter starts at 0
  OCR0A = GSCLK_TIM;    // Timer restart value
  TCCR0A = _BV(WGM01)   // CTC mode
          |_BV(COM0A0); // Toggle OC0A output on every timer restart
  TCCR0B = _BV(CS00);   // Start timer, no prescale

  // Timer 1 -> Generate an interrupt every 4096 GSCLK pulses
  TCCR1=0;  GTCCR=0;    // Stop the timer
  TCNT1 = 0;            // Counter starts at 0
  OCR1A = 0;            // Where the interrupt happens during the count
  OCR1C = GSCLK_CYCLES; // Prescaler is 4096...total clocks is 4096*GSCLK_CYCLES
  TIMSK = _BV(OCIE1A);  // Interrupt every time the timer passes zero
  TCCR1 = _BV(CTC1)     // Start timer, CTC mode
          |_BV(CS10)    // Prescale=4096
          |_BV(CS12)
          |_BV(CS13);
}

#else
  unsupported_chip;
#endif

/*------------------------------------------------------------------------
  Interrupt handler - BLANK and XLAT are sent here
------------------------------------------------------------------------*/
// Set this flag whenever you want an XLAT bit to be sent
static bool doXLAT = false;

// Interrupt called at the end of each PWM cycle  - do BLANK/XLAT here
ISR(TIM1_COMPA_vect) // nb. *NOT*   "TIMER1_COMPA_vect"
{
  if (doXLAT) {
    PORTB |= _BV(BLANK_PIN)|_BV(XLAT_PIN);
    doXLAT = false;
    PINB = _BV(XLAT_PIN);      // XLAT low
  }
  else {
    PORTB |= _BV(BLANK_PIN);
  }
  PINB = _BV(BLANK_PIN);       // BLANK low
}

/*------------------------------------------------------------------------
  The TLC5940 controller
------------------------------------------------------------------------*/
class Tiny5940 {
  typedef byte pwm_index_type;
  byte pwmData[24*NUM_TLC5940s];
  void sendByte(byte b) {
    // Put the byte in the USI data register
    USIDR = b;
    // Send it
#define tick USICR = _BV(USIWM0)|_BV(USITC)
#define tock USICR = _BV(USIWM0)|_BV(USITC)|_BV(USICLK);
    tick;  tock;  tick;  tock;
    tick;  tock;  tick;  tock;
    tick;  tock;  tick;  tock;
    tick;  tock;  tick;  tock;
  }
public:
  void init() {
    cli();            // The system may already have interrupts running on the timers, disable them
    DDRB |= _BV(BLANK_PIN)|_BV(XLAT_PIN)|_BV(GSCLK_PIN);  // All port B pins as outputs
    PORTB |= _BV(BLANK_PIN);                              // BLANK pin high (stop the TLC5940, disable all output)
    initHardwareTimers();
    setAll(0);
    PINB = _BV(XLAT_PIN);
    PINB = _BV(XLAT_PIN);
    update();
    sei();
  }
  void setAll(uint16 value) {
    for (pwm_index_type i=0; i<16*NUM_TLC5940s; ++i) {
      set(i,value);
    }
  }
  void set(pwm_index_type channel, uint16 value) {
    if (channel < (16*NUM_TLC5940s)) {
      // Pointer to a pair of packed PWM values
      byte *pwmPair = pwmData+((channel>>1)*3);
      if (channel&1) {
        // Replace high part of pair
        pwmPair[1] = (pwmPair[1]&0x0f) | ((value<<4)&0xf0);
        pwmPair[2] = value >> 4;
      }
      else {
        // Replace low part of pair
        pwmPair[0] = value&0xff;
        pwmPair[1] = (pwmPair[1]&0xf0) | ((value>>8)&0x0f);
      }
    }
  }
  bool update() {
    // Still busy sending the previous values?
    if (doXLAT) {
      // Yes, return immediately
      return false;
    }

    // Set up USI to send the PWMdata
    byte sOut = _BV(SERIAL_CLK)|_BV(SERIAL_OUT);
    SERIAL_DDR |= sOut;     // Pins are outputs
    SERIAL_PORT &= ~sOut;   // Outputs start low
    USICR = _BV(USIWM0);    // Three wire mode

    // Send it...
    pwm_index_type numBytes = sizeof(pwmData);
    byte *pwm = pwmData+numBytes;   // High byte first...
    while (numBytes > 0) {
      sendByte(*--pwm);
      --numBytes;
    }

    // Tell the interrupt routine there's data to be latched
    doXLAT = true;
 
    return true;
  }
};


// Create a Tiny5940 for people to use
Tiny5940 tlc;

#endif

I think it's compatible with the regular TLC5940 library.

fungus:
(driving the TLC5940 with hardware needs two timers)

I believe the "standard" TLC5940 library also uses two timers. I'm baffled. Why is that necessary?

Yep, but the Mega328 has three timers so it can leave Timer0 alone.

One has to generate GSCLK. The other has to count to 4096. There's no way to do it with a single timer.

Hi,

Two questions.

Would this code work on the Uno (with little or lots of modifications), and would it drive some servos off the TLC5940 ?

Thanks.

@fungus...

The other library use CLKO for GSCLK and a timer to count to 4096. CLKO requires a fuse change which seems reasonable when working with an ATtiny processor.

Would that work for your project?

Riccarr:
Would this code work on the Uno (with little or lots of modifications)...

This works on the Uno...
http://code.google.com/p/tlc5940arduino/

...and would it drive some servos off the TLC5940 ?

From that same page...

Now supports servos! See the example (Sketchbook->Examples->Library-Tlc5940->Servos) and documentation.

I don't see any reason why not...the TLC5940 will accept up to 30MHz on GSCLK.

You'll have to change "GSCLK_CYCLES" to 1 and take out the setup for Timer0 in "initHardwareTimers()" in the source code.

My code was based on that... :slight_smile:

I did a quick sanity check. I have an ATtiny84 with CKOUT (aka CLKO) connected to the TLC5940 GSCLK pin. Everything else is connected an Uno. No problems.

8 MHz, 4096 steps, that comes out to 512 microseconds. I wonder how well blink-without-delay would work? Is it really that important for BLANK to be fired accurately?

@fungus, what did you build with an ATtiny84 and a TLC5940? How accurate does BLANK need to be?

All LEDs turn off after 4096 pulses on GSCLK (when they go past their PWM value). The BLANK signal turns them on again and resets the internal counter for a new PWM cycle. It doesn't have to be perfect.

Wow! Nice job with the programming!

@fungus, what clock speed are you using? 8 MHz? Are you using the internal oscillator?

Yes.

Great work Fungus !

I really appreciate it. I'm a total noob but I like to tinker with those Tinies so much. Last week by accident, I recieved 13x TLC5940 for free. I was desperatly looking for a tiny snippet/code to play with those TLC5940s

I'm able to compile the code for a attiny45 in the Arduino IDE. hooray ! thank you again :grin: :smiley: