0

Has anyone had any experience communicating with a TLC59731 from an Attiny85? I'm not sure how to configure the USI to support the EasySet "protocol" described in the datasheet. I imagine I would need to configure as UART with start/stop bits disabled (is that even possible?).

The datasheet also seems to suggest that there is a rising edge before every bit (Fig. 17); What's this all about? It's not something I've seen before...

Also, how would I transmit EOS and GSLAT which require the line to be held low for a determined amount of time? The acceptable range seems to be somewhat forgiving so perhaps I could get away with sending a few 0 bytes, although I would prefer something a little more robust!

Must admit I am struggling to make sense of the datasheet: https://www.ti.com/lit/ds/symlink/tlc59731.pdf

19172281
  • 685
  • 1
  • 9
  • 24
  • Why do you specifically want to use USI for it? Why not any other method? And as you seem to be confused how to send bits to the chip, did you see Figure 14 which explains how bits are sent to the chip? – Justme May 18 '23 at 13:39
  • @Justme what other method do you propose? Surely the only alternative is to implement in software. – 19172281 May 18 '23 at 14:24
  • It depends how exactly you want to use the USI and if it is suitable for that. Just as an example, AVR SPI hardware keeps one bit time delay between two bytes, so depending on how intend to use USI, you may also find out that the byte transfer has delays after last bit before your code finds out that next byte can be transferred. – Justme May 18 '23 at 15:15
  • @Justme, Am I correct in thinking that 0 is transmitted as 1 rising edge, and 1 is transmitted as 2 rising edges? Is this common in serial protocols and is there a name for it? – 19172281 May 18 '23 at 19:30
  • Not very common I'd say. And it migh be better to not think just the rising edges, but pulses. Two pulses with short time between is 1, one pulse with long time after it is 0. Floppy disks used to call this "FM" and called them clock pulses and data pulses. If you only have clock pulse and no data pulse is different from having clock pulse and data pulse. – Justme May 18 '23 at 19:45
  • Would you just implement the whole thing in software using timer interrupts or would it be preferable to exploit the USI hardware in whatever way I can? I plan on having 2 TLC59731s in series which are refreshed at 60hz. I worry a software implementation will hog the CPU. – 19172281 May 18 '23 at 19:52
  • Any implementation might hog the CPU. I'll try making an answer. – Justme May 18 '23 at 20:04

2 Answers2

1

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++;
  }
}

Jens
  • 5,598
  • 2
  • 7
  • 28
  • It might be quite difficult maintaining consistent timing between each byte and using SPI looks like a bit of a hack. I'm wondering whether it would be preferable to implement the whole thing in software using timer interrupts. What do you think? Otherwise, would another MCU have hardware serial that could better support such a protocol? – 19172281 May 18 '23 at 19:41
  • @19172281 Yes, using a hardware serial would improve the solution a lot, since the output timing is independent of the software. You would need an inverter at the TX output since UART is idle high. Using 8N1, sending 0x00 creates the short single pulse (start bit only) to indicate the zero bit value, 0x08 would indicate the double pulse then. However, the gaps cannot be created this way, because the start bit cannot be suppressed. A kind of delay function after TX-Empty is required. Also a regular SPI hardware with clock would be a great advantage over this ugly USI thing. – Jens May 18 '23 at 20:00
  • As an alternative you can implement this with a periodic hardware timer interrupt of e.g. 10 kHz to create the start pulses and a single shot 20 µs timer for the second pulse for the "one" started on demand within the periodic interrupt. The payload on an 8 MHz MCU without using assemby code also is critical. Playing with different timer compare interrupts on one timer can help here as well. – Jens May 18 '23 at 20:15
  • When you say a delay function after TX-Empty is needed, do you mean for the gap between the first and second pulses used by the TLC59731 to establish timing? – 19172281 May 18 '23 at 20:59
  • Yes, this is the problem – Jens May 19 '23 at 00:26
  • Perhaps the the first 2 pulses could be achieved with one byte where the MSB and LSB are set to 1. I could then play with the clock frequency to create the required delay between the pulses. What do you think? – 19172281 May 21 '23 at 04:09
  • @19172281 The more I think about this question, the more I tend to use a timer overflow interrupt, where the decision about creating a start bit (data or gap) is made and a compare B interrupt, where the decision about a second pulse in the bit frame is needed. This code could scan a data pattern in RAM and produce the bits and gaps as a cyclic autonomous background task. – Jens May 21 '23 at 17:32
  • I'm wondering whether this could all be implemented in 1 ISR. I could establish a clock frequency and decide that 1 pulse is let's say 1 clock long, Tcycle is 7 clocks cycles, etc – 19172281 May 22 '23 at 19:07
  • I guess a concern with using only 1 timer would be the need for many if statements to maintain state which might considerably increase the number of instructions – 19172281 May 22 '23 at 20:51
  • Thanks for the updated answer. Do you think it would be possible to efficiently achieve the timing using a single timer and compare match? Obviously the ISR frequency would have to be considerably higher. – 19172281 May 25 '23 at 08:46
1

The protocol is pretty forgiving.

It can run between 20 and 600 kbps, where 1 bit means either 1 or 2 pulses.

The pulse high time just need to be at least 14ns in length and there must be at least 14ns low time between pulses, and at least 275ns between two rising edges for the dual pulses.

It might be worth to use timer to trigger a bit transmission every 20 kHz, and use the USI to transmit out a byte at suitable speed which contains either 1 or 2 pulses that are compliant with the timing.

Having a timer to run at 20 kHz or faster interrupt rate is a lot, so it might be useful to update the LEDs at rate of 60 Hz and have the MCU do nothing else for the time during the LEDs are updated, regardless of how you update them.

A purely software solution can also work, but it may not tolerate interrupts running in the background, but it would allow to time the pulses so that the data rate is closer to 600 kHz and you spend less time banging out the bits to bus.

Each LED takes 32 bits, so even at rate of 96 kHz you spend less than 1ms to transfer out enough bits for 2 LEDs plus the headers and trailers of the protocol.

Justme
  • 127,425
  • 3
  • 97
  • 261
  • Does timing between each byte matter? I would think provided the line isn't low longer than the duration for EOS or GSLAT it shouldn't be a problem. How would you configure the USI? SPI, UART, etc? – 19172281 May 18 '23 at 20:47