It is possible to use the USI hardware in SPI master mode to create the timing. This is the (corrected) code fragment to send one byte taken from the datasheet:
15.3.2 SPI Master Operation Example
The following code demonstrates how to use the USI as an SPI Master:
SPITransfer:
out USIDR, r16
ldi r16, (1<<USIOIF)
out USISR, r16
ldi r16, (1<<USIWM0)|(1<<USICS1)|(1<<USICLK)|(1<<USITC)
SPITransfer_loop:
out USICR, r16
in r17, USISR // bugfix: can not use r16 here
sbrs r17, USIOIF
rjmp SPITransfer_loop
in r16,USIDR
ret
A C code implementation might look like this:
static void SPITransfer(uint8_t value)
{
USIDR = value;
for (uint8_t i = 0; i < 8; i++) { // do 8 bits
USICR = (1<<USIWM0)|(0<<USICS0)|(1<<USITC);
USICR = (1<<USIWM0)|(0<<USICS0)|(1<<USITC)|(1<<USICLK);
// some microseconds delay maybe needed here to
// compensate function call timing jitter
}
}
An Arduino code implementation without the USI hardware:
static void SPITransfer(uint8_t value)
{
for (uint8_t i = 0; i < 8; i++) { // do 8 bits
digitalWrite (SDO_OUTPUT, (value & 0x80) != 0);
value <<= 1;
// some microseconds delay maybe needed here to
// compensate function call timing jitter
}
}
I did not compile or test this code. It is just my interpretation of the datasheet content and some experience with the atTiny family and should give you a starting point.
As I understand chapter 8.5 of the TLC5973 datasheet, you have to send 32 bits per chip in a continous stream, then 3-4 bit frames containing low output to mark the end of the data for one chip. This will be repeated for all chips in the chain. Finally 8 or more bit frames containing low only marks the end of the transfer.
To send a low bit you transfer 0x80 via SPI, this will create the start bit and the bit frame timing.
To send a high bit you transfer e.g. 0x90 (or 0xA0) via SPI, which creates the start bit and the second bit in the first half of the bit frame.
To send the gap frames containing low, you transfer 0x00 via SPI.
To transfer the color values to one chip you can implement something like this:
static void send_rgb (uint32_t rgb_data) {
rgb_data &= 0x00FFFFFF; // strip high byte (just paranoia)
rgb_data |= 0x3A000000; // add write command signature
for (uint8_t i = 0; i< 32; i++) { // do 32 bits
if (rgb_data & 0x80000000) { // highest bit 1?
SPITransfer (0x90); // send 1 bit pattern
} else {
SPITransfer (0x80); // send 0 bit pattern
}
rgb_data <<= 1; // next bit to test
}
for (uint8_t i = 0; i< 3; i++) { // do 3 gap bit frames containing low
SPITransfer (0x00);
}
}
For high update rates it may be useful to write special code to create the single pulse (zero), double pulse (one) and no pulse (gap) patterns using direct port I/O functions and some microsecond delays.
** UPDATE **
I have tested this on an Arduino Leonardo using the Arduino IDE. It is indeed possible to implement this without assemby code.
There are two options in this code, either a solution using a timer with compare interrupts or the use of the SPI hardware.
The SPI solution can reach twice the frame rate compared to the timer solution but needs an inverter at the output.
The CPU load is around 15%.
I must admit, that I don't have a TLC59731, but the oscilloscope shows the required timing. So there may be some real world tweaking needed.
/*
* Program to send PWM data to a chain of TLC59731
* Tested on Arduino Leonardo (atMega32U4@16Mhz)
* Average CPU load is around 15%
* Longest interrupt execution is around 8 us
* PortB.2 used as data output
* PortB.0 used as interrupt active diagnostic output
* If SPI mode is used the output must be inverted,
* because MOSI is idle high
* SPI mode creates 40 kHz bit frequency
* TimerMode creates 20 kHz bit frequency
*/
#include <Arduino.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#define LED_CHIP_COUNT 2 // number of TLC59731 in chain
#define USE_SPI
#define TLC_WRITE_CMD 0x3A
// I/O
#define BIT_ON_MASK 0x04
#define BIT_OFF_MASK 0xFB
#define DIAG_ON_MASK 0x01 // this is SPI /SS and must be an output
#define DIAG_OFF_MASK 0xFE
// timing
#define CHIP_GAP_BITS 3
#define STREAM_GAP_BITS 8
// timer mode parameters
#define BIT_PERIOD_TICKS 770
#define PULSE_HIGH_TICKS 170
#define PULSE1_OFF_TICK PULSE_HIGH_TICKS
#define PULSE2_ON_TICK 350
#define PULSE2_OFF_TICK (PULSE2_ON_TICK + PULSE_HIGH_TICKS)
// data of one chip in chain
struct TLedChipData {
byte hdr; // fixed write command 3A
byte value0; // chip OUT0 PWM
byte value1; // chip OUT1 PWM
byte value2; // chip OUT2 PWM
};
// PWM data buffer
static volatile TLedChipData LedChipData[LED_CHIP_COUNT];
// transfer management data
static volatile byte rem_chips; // remaining chip data to send
static volatile byte byte_idx; // current byte index of chip
static volatile byte cur_byte; // current byte in transfer
static volatile byte rem_bits; // remaining bits of byte
static volatile byte rem_gap_bits; // remaining bits of gap
static volatile byte* pStartByte; // chain data reload ptr
static volatile byte* pCurByte; // ptr to current byte
static volatile bool SendOne; // true = current bit is one
static volatile bool OutIsHigh; // current output bit status
static byte loop_counter;
static void CreateDemoData (void) {
for (byte i = 0; i < LED_CHIP_COUNT; i++) {
LedChipData[i].hdr = TLC_WRITE_CMD;
// some PWM test pattern
LedChipData[i].value0 = 0x5A;
LedChipData[i].value1 = 0x3C;
LedChipData[i].value2 = 0xA5;
}
}
void setup() {
while (millis() < 5000) {}; // dummy wait
loop_counter = 0;
CreateDemoData();
// prepare interrupt for chip chain processing
rem_chips = LED_CHIP_COUNT;
byte_idx = 0;
rem_bits = 8;
rem_gap_bits = STREAM_GAP_BITS;
pStartByte = &LedChipData[0].hdr;
pCurByte = pStartByte;
cur_byte = *pCurByte;
#ifdef USE_SPI
DDRB = BIT_ON_MASK | DIAG_ON_MASK | 0x02; // set SCK as output
PORTB |= BIT_ON_MASK;
PORTB &= DIAG_OFF_MASK;
SPSR = 0x01; // set double speed
SPCR = 0xD2; // spi master, 500 kHz
SPDR = 0xFF; // send first dummy to invoke the int handler
#else
DDRB = BIT_ON_MASK | DIAG_ON_MASK;
PORTB &= BIT_OFF_MASK & DIAG_OFF_MASK;
// setup timer 3 mode 4, CTC mode
TCCR3B = 0; // stop
TCCR3A = 0;
TCNT3 = 0; // reset
OCR3A = BIT_PERIOD_TICKS;
OCR3B = 0xFFFF;
TCCR3A = 0x00; // lower mode bits 0
TCCR3B = 0x09; // ctc mode, divisor 1 = 16 MHz counter clock
TIFR3 = 0x06; // quit pending comp ints
TIMSK3 = 0x02; // enable comp A int only
#endif
}
#ifdef USE_SPI
//SPI end of transfer interrupt function
ISR (SPI_STC_vect)
{
PORTB |= DIAG_ON_MASK;
if (rem_gap_bits) {
// handle gap
rem_gap_bits--;
SPDR = 0xFF; // send gap, produce next int
} else {
// bit out
if ((cur_byte & 0x80) != 0) {
SPDR = 0x6F; // b7 and b4 = zero
} else {
SPDR = 0x7F; // b7 = zero
}
// prep next bit
cur_byte <<= 1;
if (--rem_bits == 0) {
// prep next byte
rem_bits = 8;
pCurByte++;
if (++byte_idx == 4) {
// prep next chip
byte_idx = 0;
if (--rem_chips == 0) {
// reload chain
rem_chips = LED_CHIP_COUNT;
pCurByte = pStartByte;
rem_gap_bits = STREAM_GAP_BITS;
} else {
rem_gap_bits = CHIP_GAP_BITS;
}
}
cur_byte = *pCurByte; // get byte to shift out
}
}
PORTB &= DIAG_OFF_MASK;
}
#else // USE_SPI
// compare B interrupt,
// manages pulse off and second pulse on timing
ISR (TIMER3_COMPB_vect)
{
PORTB |= DIAG_ON_MASK;
if (OutIsHigh) {
// turn off bit
PORTB &= BIT_OFF_MASK;
OutIsHigh = false;
if (SendOne) {
// need second bit
OCR3B = PULSE2_ON_TICK;
} else {
// bit frame done
TIMSK3 = 0x02; // enable comp A int only
}
} else {
// turn second pulse on
PORTB |= BIT_ON_MASK;
OutIsHigh = true;
SendOne = false; // just done
OCR3B = PULSE2_OFF_TICK;
}
PORTB &= DIAG_OFF_MASK;
}
// compare A interrupt,
// start of bit frame, 20 kHz call rate
// max. measured execution time is around 8 us
ISR (TIMER3_COMPA_vect) {
PORTB |= DIAG_ON_MASK;
if (rem_gap_bits) {
// handle gap
rem_gap_bits--;
} else {
// bit out
SendOne = (cur_byte & 0x80) != 0;
// first rising edge
PORTB |= BIT_ON_MASK;
OutIsHigh = true;
OCR3B = PULSE1_OFF_TICK;
TIFR3 = 0x04; // quit any pending comp B int
TIMSK3 = 0x06; // enable comp A and comp B int
// prep next bit
cur_byte <<= 1;
if (--rem_bits == 0) {
// prep next byte
rem_bits = 8;
pCurByte++;
if (++byte_idx == 4) {
// prep next chip
byte_idx = 0;
if (--rem_chips == 0) {
// reload chain
rem_chips = LED_CHIP_COUNT;
pCurByte = pStartByte;
rem_gap_bits = STREAM_GAP_BITS;
} else {
rem_gap_bits = CHIP_GAP_BITS;
}
}
cur_byte = *pCurByte; // get byte to shift out
}
}
PORTB &= DIAG_OFF_MASK;
}
#endif // USE_SPI
void loop() {
loop_counter++;
if (!loop_counter) {
// play with the PWM data
LedChipData[0].value0++;
LedChipData[1].value2++;
}
}