Anleitung: Bibliothek für TLC5947 mit SPI

Motivation: Im Thema MIDI Controller PART 3 sollen 48 LEDs gedimmt verwendet werden. Als Hardware wurde der 24-Channel 12-bit PWM LED Driver von Adafruit ausgewählt. Adafruit bewirbt diesen mit „SPI Interface“, die angebotene Bibliothek nutzt aber leider nur einfache Ein-Ausgabe. Die Suche nach einer geeigneteren Bibliothek führte zu RobTillaarts TLC5947, der dank Portmanipulationen bei AVRs die Daten überträgt. Das geht schneller, nutzt aber leider auch nicht die schnelleren Möglichkeiten der SPI-Hardware, mit der die Datenübertragung nochmal fast zehnmal schneller erfolgt:

Bibliothek µs
Adafruit_TLC5947 8896
TLC5947 1624
TLC5947_SPI 172

Zeiten für zwei Platinen mit zusammen 48 LEDs, jeweils das Setzen eines PWM-Wertes für eine LED und die Übertragung der Daten für alle LEDs. Getestet mit einem Mega2560, bei SPI mit einem Takt von 8 MHz.

Konzept: SPI kann nur acht Bit übertragen, weshalb zweimal 12 Bit in drei Byte gespeichert werden. Zur Übertragung wird ein temporäres Feld verwendet, da dieses bei der Übertragung verändert wird. Das Setzen von PWM-Werten erfolgt zeitsparend getrennt von der Übertragung zum TLC5947. Anders als bei SPI üblich erfolgt am Ende der Übertragung an CS ein LOW-HIGH-Impuls, da dies dem LATCH-Signal entspricht.

Anschluß:

µC TLC5947
COPI DIN Data
CIPO nicht angeschlossen
SCA CLK Clock
CS LAT Latch

Der TLC5947-Pin OE wird von der Bibliothek nicht verwendet. Bei meinem Board werden die Ausgänge mit einem PullDown-Widerstand ständig aktiviert, was nach Reset zu einem Flackern führen kann. Mit einem externen PullUp-Widerstand und einem HIGH-LOW-Wechsel erst am Ende von setup() kann dies verhindert werden, siehe Beispielprogramm.

Bibliothek:

TLC5947_SPI(const uint8_t latch, const uint8_t deviceCount = 1, const uint32_t SPIclock = 8000000UL, SPIClass &spi = SPI)

  • latch: Pin für die Datenübernahme
  • deviceCount: Anzahl der angeschlossenen Platinen mit je 24 LEDs, Standard ist 1, begrenzt auf 10.
  • SPIclock: SPI-Takt, Maximum und Standard ist 8 MHz. Beim Mega2560 sind auch 4 MHz und 1 MHz einstellbar.
  • spi: SPI-Port, wenn der µC mehrere anbietet. Für den Mega2560 nicht relevant, nicht getestet.

void begin();

  • Initialisierung.

int getChannels();

  • Rückgabewert ist die Anzahl der LEDs.

void setAll(uint16_t pwm);

  • pwm: Helligkeitswert, 0 ist aus, 4095 ist maximale Helligkeitswert.

int setPWM(const uint8_t channel, uint16_t pwm);

  • channel: Nummer der LED, Zählung beginnend bei 0.
  • pwm: Helligkeitswert, 0 ist aus, 4095 ist der maximale Helligkeitswert.

Rückgabewert ist im Fehlerfall -1, sonst 0.

uint16_t getPWM(const uint8_t channel);

  • channel: Nummer der LED, Zählung beginnend bei 0.

Zurückgegeben wird der PWM-Wert aus dem Puffer oder im Fehlerfall -1.

void update();

  • Die Daten aller LEDs werden an die angeschlossenen TLC5947 übertragen. Nach setPWM muß also immer ein write erfolgen, damit die Änderung sichtbar wird.

Noiasca Took Kit for LEDs - Arduino Effect Library for your LEDs nutzt diese Bibliothek zur Ansteuerung von TLC5947.


Aktuelle Version: TLC5947_SPI.ZIP (3,5 KB)

Programmbeispiel
/* Beispiel für TLC5947_SPI: Lauflicht

 Belegung Mega2560
DATA  = COPI = 51   Pin von Hardware-SPI, teilweise noch als MOSI bezeichnet.
CLOCK = SCA  = 52   Pin von Hardware-SPI
LATCH = CS   = 53   Freier Chip Select Pin. Bei der Parallelschaltung mehrerer SPI-Busteilnehmer braucht
                    es je Teilnehmer einen CS-Pin, also CS1=pinLATCH1, CS2=pinLATCH2 ... CSn=pinLATCHn.
OE           = 49   Frei wählbarer Pin.
*/
#include "TLC5947_SPI.hpp"

constexpr uint8_t DEVICES  {2};     // Anzahl der angeschlossenen Platinen mit je 24 LEDs.
constexpr uint8_t pinLATCH {53};    // Pin für die Datenübernahme.
constexpr uint8_t pinOE    {49};    // Pin für die Aktivierung der LED-Treiber (O̲utput E̲nable).

TLC5947_SPI tlc(pinLATCH, DEVICES);

uint16_t pwm = 400;

void setup() {
  SPI.begin();
  tlc.begin();
  pinMode(pinOE, OUTPUT);

  tlc.setAll(pwm);  // Lampentest
  tlc.update();
  digitalWrite(pinOE, LOW);
  delay(1000);
  tlc.setAll(0);
}

void loop() {
  for (byte led = 0; led < tlc.getChannels(); led++) {
    tlc.setPWM(led, pwm);
    tlc.update();
    delay(100);
    tlc.setPWM(led, 0);
  }
}
TLC5947_SPI.hpp
/* Arduino library for the TLC5947 24 channel PWM device
 angelehnt an: https://github.com/RobTillaart/TLC5947
 
 Diese Bibliothek überträgt die PWM-Werte für die LEDs per SPI, um die schnelle
 SPI-Hardware der µCs nutzen zu können. Dazu werden die 24 PWM-Werte von je 12 Bit
 in 36 Byte für das Schieberegister untergebracht. 
 
 ChipSelect (CS) bleibt im Unterschied zum SPI-Protokoll während der Datenübertragung
 auf HIGH, erst am Ende gibt es, typisch Latch, einen kurzen Impuls zur Datenübernahme.
 
 Nicht Teil der Bibliothek ist OutputEnable (/OE). Empfehlenswert ist ein 
 PullUp-Widerstand anzuschließen und am Ende von setup() /OE auf LOW zu bringen, 
 um ein Flackern der LEDs nach Reset des µCs zu verhindern.
 
 Getestet mit UNO und Mega2560.
 
 Verwende SPI-Pins:
 CLOCK = SCA
 DATA  = COPI
 LATCH = CS
*/
#pragma once

#include <SPI.h>
#include "Arduino.h"

class TLC5947_SPI {
  static constexpr int TLC5947_OK{ 0 };
  static constexpr int TLC5947_CHANNEL_ERROR{ -1 };
  static constexpr uint16_t MAXPWM{ 4095 };
  static constexpr uint32_t MAXSPI{ 8000000UL };
  uint16_t _channels;  // Anzahl der angeschlossenen Platinen mit je 24 LEDs, begrenzt auf 10.
  size_t _numBuf;      // Größe des Speichers in Byte.
  uint8_t *_buffer;    // Speicher für die PWM-Werte.
  uint8_t _latch;      // Pin für die Datenübernahme.
  uint32_t _SPIclock;  // SPI-Takt, Standard ist 8 MHz.
  SPIClass _spi;       // SPI-Port, wenn der µC mehrere anbietet.

public:
  TLC5947_SPI(const uint8_t latch, const uint8_t deviceCount = 1, const uint32_t SPIclock = 8000000UL, SPIClass &spi = SPI) {
    _latch = latch;
    if (deviceCount > 10) {
      _channels = 240;  // Begrenzung auf 10 TLC5947
    } else {
      _channels = deviceCount * 24;  // 24 LEDs pro TLC5947
    }
    if (SPIclock > MAXSPI) {
      _SPIclock = MAXSPI;
    } else {
      _SPIclock = SPIclock;
    }
    _spi = spi;
    _numBuf = _channels * 3 / 2;  // 3 Byte für die Daten von 2 LEDs
    _buffer = (uint8_t *)calloc(_numBuf, sizeof(uint8_t));
  }

  void begin() {
    pinMode(_latch, OUTPUT);
    digitalWrite(_latch, LOW);
  }

  int getChannels() {
    return _channels;
  }

  void setAll(uint16_t pwm)  // vierfache Beschleunigung gegenüber 24 * setPWM
  {
    if (pwm > MAXPWM) pwm = MAXPWM;
    setPWM(0, pwm);
    setPWM(1, pwm);
    for (int j = _numBuf - 4; j >= 0; j--) _buffer[j] = _buffer[j + 3];
  }

  //////////////////////////////////////////////////
  //
  //  PWM
  //
  uint16_t getPWM(const uint8_t channel) {
    if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
    uint16_t _channel = channel;
    uint16_t i = _channel / 2 * 3;  // Integerarethmetik!
    uint16_t pwm = 0;
    if (bitRead(_channel, 0)) {  // ungerade
      pwm = (_buffer[_numBuf - 3 - i] << 4) | (_buffer[_numBuf - 2 - i] >> 4);
    } else {  // gerade
      pwm = ((_buffer[_numBuf - 2 - i] & 0x0F) << 8) | _buffer[_numBuf - 1 - i];
    }
    return pwm;
  }

  int setPWM(const uint8_t channel, uint16_t pwm) {
    if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
    uint8_t _channel = channel;
    if (pwm > MAXPWM) pwm = MAXPWM;
    uint16_t i = _channel / 2 * 3;  // Integerarethmetik!
    if (bitRead(_channel, 0)) {     // ungerade
      if (pwm > 0) {
        _buffer[_numBuf - 2 - i] = (_buffer[_numBuf - 2 - i] & 0x0F) | (pwm << 4);
        _buffer[_numBuf - 3 - i] = pwm >> 4;
      } else {
        _buffer[_numBuf - 2 - i] &= 0x0F;
        _buffer[_numBuf - 3 - i] = 0;
      }
    } else {  // gerade
      if (pwm > 0) {
        _buffer[_numBuf - 1 - i] = pwm;
        _buffer[_numBuf - 2 - i] = (_buffer[_numBuf - 2 - i] & 0xF0) | (pwm >> 8);
      } else {
        _buffer[_numBuf - 1 - i] = 0;
        _buffer[_numBuf - 2 - i] &= 0xF0;
      }
    }
    return TLC5947_OK;
  }

  //////////////////////////////////////////////////
  //
  //  WRITE
  //
  void update() {
    uint8_t muster[_numBuf];           // die zu übertragenen Daten
    memcpy(muster, _buffer, _numBuf);  // das Feld wird von SPI gelöscht, daher ist eine temporäre Kopie notwendig
    _spi.beginTransaction(SPISettings(_SPIclock, MSBFIRST, SPI_MODE0));
    _spi.transfer(muster, _numBuf);
    _spi.endTransaction();
    digitalWrite(_latch, LOW);
    digitalWrite(_latch, HIGH);
  }
};
//  -- END OF FILE --

Version bis 11.11.2025
Programmbeispiel
/* Beispiel für TLC5947_SPI: Lauflicht

 Belegung Mega2560
DATA  = COPI = 51
CLOCK = SCA  = 52
LATCH = CS   = 53
OE           = 49
*/
#include "TLC5947_SPI.h"

constexpr uint8_t DEVICES  {2};
constexpr uint8_t pinLATCH {53};
constexpr uint8_t pinOE    {49};

TLC5947_SPI tlc(pinLATCH, DEVICES);

uint16_t pwm = 400;

void setup() {
  SPI.begin();
  tlc.begin();
  pinMode(pinOE, OUTPUT);
  digitalWrite(pinOE, LOW);

  tlc.setAll(pwm);  // Lampentest
  tlc.write();
  delay(1000);
  tlc.setAll(0);
}

void loop() {
  for (byte led = 0; led < tlc.getChannels(); led++) {
    tlc.setPWM(led, pwm);
    tlc.write();
    delay(100);
    tlc.setPWM(led, 0);
  }
}
TLC5947_SPI.h
/* Arduino library for the TLC5947 24 channel PWM device
 angelehnt an: https://github.com/RobTillaart/TLC5947
 
 Diese Bibliothek überträgt die PWM-Werte für die LEDs per SPI, um die schnelle
 SPI-Hardware der µCs nutzen zu können. Dazu werden die 24 PWM-Werte von je 12 Bit
 in 36 Byte für das Schieberegister untergebracht. 
 
 ChipSelect (CS) bleibt im Unterschied zum SPI-Protokoll während der Datenübertragung
 auf HIGH, erst am Ende gibt es, typisch Latch, einen kurzen Impuls zur Datenübernahme.
 
 Nicht Teil der Bibliothek ist OutputEnable (/OE). Empfehlenswert ist ein 
 PullUp-Widerstand anzuschließen und am Ende von setup() /OE auf LOW zu bringen, 
 um ein Flackern der LEDs nach Reset des µCs zu verhindern.
 
 Getestet mit UNO und Mega2560.
 
 Verwende SPI-Pins:
 CLOCK = SCA
 DATA  = COPI
 LATCH = CS
*/
#pragma once

#include <SPI.h>
#include "Arduino.h"

constexpr int TLC5947_OK {0};
constexpr int TLC5947_CHANNEL_ERROR {-1};


class TLC5947_SPI
{
public:
  TLC5947_SPI(const uint8_t latch, const uint8_t deviceCount = 1, uint32_t SPIclock = 8000000UL, SPIClass &spi = SPI);

  void     begin();
  int      getChannels();
  void     setAll(uint16_t pwm);

  //  erst alle PWM-Werte in den Puffer schreiben, dann mittels write() die Daten an die Hardware übertragen
  int      setPWM(const uint8_t channel, uint16_t pwm);
  uint16_t getPWM(const uint8_t channel);  // Wert aus dem Puffer, nicht vom TLC5947!
  void     write();

private:
  uint16_t   _channels;
  size_t     _numBuf;
  uint8_t  * _buffer;
  uint8_t    _latch;
  uint32_t   _SPIclock;
  SPIClass   _spi;
  const uint16_t MAXPWM = 4095;
};

//  -- END OF FILE --
TLC5947_SPI.cpp
#include "TLC5947_SPI.h"

TLC5947_SPI::TLC5947_SPI(const uint8_t latch, const uint8_t deviceCount, uint32_t SPIclock, SPIClass &spi)
{
  if (deviceCount > 10) {
    _channels = 240;                // Begrenzung auf 10 TLC5947
  } else {
    _channels = deviceCount * 24;   // 24 LEDs pro TLC5947
  }
  _latch    = latch;
  _SPIclock = SPIclock;
  _spi      = spi;
  _numBuf   = _channels * 3 / 2;  // 3 Byte für die Daten von 2 LEDs
  _buffer   = (uint8_t *) calloc(_numBuf, sizeof(uint8_t));

}

void TLC5947_SPI::begin()
{
  pinMode(_latch, OUTPUT);
  digitalWrite(_latch, LOW);
}

int TLC5947_SPI::getChannels()
{
  return _channels;
}

void TLC5947_SPI::setAll(uint16_t pwm)  // vierfache Beschleunigung gegenüber 24 * setPWM
{
  if (pwm > MAXPWM) pwm = MAXPWM;
  setPWM(0, pwm);
  setPWM(1, pwm);
  for (int j = _numBuf-4; j >= 0; j--) _buffer[j] = _buffer[j+3];
}

//////////////////////////////////////////////////
//
//  PWM
//
uint16_t TLC5947_SPI::getPWM(const uint8_t channel)
{
  if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
  uint16_t _channel = channel;
  uint16_t i = _channel / 2 * 3;  // Integerarethmetik
  uint16_t pwm = 0;
  if (bitRead(_channel, 0)) {     // ungerade
    pwm = (_buffer[_numBuf-3 - i] << 4) | (_buffer[_numBuf-2 - i] >> 4);
  } else {                        // gerade
    pwm = ((_buffer[_numBuf-2 - i] & 0x0F) << 8) | _buffer[_numBuf-1 - i];
  }
  return pwm;
}

int TLC5947_SPI::setPWM(const uint8_t channel, uint16_t pwm)
{
  if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
  uint8_t _channel = channel;
  if (pwm > MAXPWM) pwm = MAXPWM;
  uint16_t i = _channel / 2 * 3;  // Integerarethmetik
  if (bitRead(_channel, 0)) {     // ungerade
    if (pwm > 0) {
      _buffer[_numBuf-2 - i] = (_buffer[_numBuf-2 - i] & 0x0F) | (pwm << 4);
      _buffer[_numBuf-3 - i] = pwm >> 4;
    } else {
      _buffer[_numBuf-2 - i] &= 0x0F;
      _buffer[_numBuf-3 - i] = 0;
    }
  } else {                        // gerade
    if (pwm > 0) {
      _buffer[_numBuf-1 - i] = pwm;
      _buffer[_numBuf-2 - i] = (_buffer[_numBuf-2 - i] & 0xF0) | (pwm >> 8);
    } else {
      _buffer[_numBuf-1 - i] = 0;
      _buffer[_numBuf-2 - i] &= 0xF0;
    }
  }
  return TLC5947_OK;
}

//////////////////////////////////////////////////
//
//  WRITE
//
void TLC5947_SPI::write()
{
  uint8_t muster[_numBuf];           // die zu übertragenen Daten
  memcpy(muster, _buffer, _numBuf);  // das Feld wird von SPI gelöscht, daher ist eine temporäre Kopie notwendig
  _spi.beginTransaction(SPISettings(_SPIclock, MSBFIRST, SPI_MODE0));
  _spi.transfer(muster, _numBuf);
  _spi.endTransaction();
  digitalWrite(_latch, LOW);
  digitalWrite(_latch, HIGH);
}

//  -- END OF FILE --

Version bis 07.11.2025
Programmbeispiel
/* Beispiel für TLC5947_SPI: Lauflicht

 Belegung Mega2560
DATA  = COPI = 51
CLOCK = SCA  = 52
LATCH = CS   = 53
OE           = 49
*/
#include "TLC5947_SPI.h"

const int DEVICES =  2;
const int LATCH   = 53;
const int OE      = 49;

TLC5947_SPI tlc(DEVICES, LATCH);

uint16_t pwm = 400;

void setup() {
  SPI.begin();
  tlc.begin();
  pinMode(OE, OUTPUT);
  digitalWrite(OE, LOW);

  tlc.setAll(pwm);  // Lampentest
  tlc.write();
  delay(1000);
  tlc.setAll(0);
}

void loop() {
  for (byte led = 0; led < tlc.getChannels(); led++) {
    tlc.setPWM(led, pwm);
    tlc.write();
    delay(100);
    tlc.setPWM(led, LOW);
  }
}
TLC5947_SPI.h
/* Arduino library for the TLC5947 24 channel PWM device
 angelehnt an: https://github.com/RobTillaart/TLC5947
 
 Diese Bibliothek überträgt die PWM-Werte für die LEDs per SPI, um die schnelle
 SPI-Hardware der µCs nutzen zu können. Dazu werden die 24 PWM-Werte von je 12 Bit
 in 36 Byte für das Schieberegister untergebracht. 
 
 ChipSelect (CS) bleibt im Unterschied zum SPI-Protokoll während der Datenübertragung
 auf HIGH, erst am Ende gibt es, typisch Latch, einen kurzen Impuls zur Datenübernahme.
 
 Nicht Teil der Bibliothek ist OutputEnable (/OE). Empfehlenswert ist ein 
 PullUp-Widerstand anzuschließen und am Ende von setup() /OE auf LOW zu bringen, 
 um ein Flackern der LEDs nach Reset des µCs zu verhindern.
 
 Getestet mit UNO und Mega2560.
 
 Verwende SPI-Pins:
 CLOCK = SCA
 DATA  = COPI
 LATCH = CS
*/
#pragma once

#include <SPI.h>
#include "Arduino.h"

#define  TLC5947_OK                     0
#define  TLC5947_CHANNEL_ERROR         -1


class TLC5947_SPI
{
public:
  TLC5947_SPI(const uint8_t deviceCount, const uint8_t latch, uint32_t SPIclock = 8000000UL);
  ~TLC5947_SPI();

  void     begin();
  uint16_t getChannels();
  void     setAll(uint16_t pwm);

  //  fill the internal buffer with PWM values
  //  call write() after all channels are updated (or per channel).
  int16_t  setPWM(const uint16_t channel, uint16_t pwm);
  uint16_t getPWM(const uint16_t channel);
  void     write();

private:
  uint16_t   _channels;
  size_t     _numBuf;
  uint8_t  * _buffer;
  uint8_t    _latch;
  uint32_t   _SPIclock;
  const uint16_t MAXPWM = 4095;
};


//  -- END OF FILE --
TLC5947_SPI.cpp
#include "TLC5947_SPI.h"

TLC5947_SPI::TLC5947_SPI(const uint8_t deviceCount, const uint8_t latch, uint32_t SPIclock)
{
  _channels = deviceCount * 24;   // 24 LEDs pro Device
  _latch    = latch;
  _SPIclock = SPIclock;
  _numBuf   = _channels * 3 / 2;  // 3 Byte für die Daten von 2 LEDs
  _buffer   = (uint8_t *) calloc(_numBuf, 1);
}

TLC5947_SPI::~TLC5947_SPI()
{
  if(_buffer) free(_buffer);
}

void TLC5947_SPI::begin()
{
  pinMode(_latch, OUTPUT);
  digitalWrite(_latch, LOW);
}

uint16_t TLC5947_SPI::getChannels()
{
  return _channels;
}

void TLC5947_SPI::setAll(uint16_t pwm)  // vierfache Beschleunigung
{
  if (pwm > MAXPWM) pwm = MAXPWM;
  setPWM(0, pwm);
  setPWM(1, pwm);
  for (int j = _numBuf-4; j >= 0; j--) _buffer[j] = _buffer[j+3];
}

//////////////////////////////////////////////////
//
//  PWM
//
uint16_t TLC5947_SPI::getPWM(const uint16_t channel)
{
  if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
  uint16_t _channel = channel;
  uint16_t pwm = 0;
  uint16_t i = _channel / 2 * 3;  // Integerarethmetik
  if (bitRead(_channel, 0)) {    // ungerade
    //Serial.println("ungerade");
    pwm = (_buffer[_numBuf-3 - i] << 4) | (_buffer[_numBuf-2 - i] >> 4);
  } else {  // gerade
    //Serial.println("gerade");
    pwm = ((_buffer[_numBuf-2 - i] & 0x0F) << 8) | _buffer[_numBuf-1 - i];
  }
  return pwm;
}

int16_t TLC5947_SPI::setPWM(const uint16_t channel, uint16_t pwm)
{
  if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
  uint16_t _channel = channel;
  if (pwm > MAXPWM) pwm = MAXPWM;
  uint16_t i = _channel / 2 * 3;  // Integerarethmetik
  if (bitRead(_channel, 0)) {    // ungerade
    //Serial.println("ungerade");
    if (pwm > 0) {
      _buffer[_numBuf-2 - i] = (_buffer[_numBuf-2 - i] & 0x0F) | (pwm << 4);
      _buffer[_numBuf-3 - i] = pwm >> 4;
    } else {
      _buffer[_numBuf-2 - i] &= 0x0F;
      _buffer[_numBuf-3 - i] = 0;
    }
  } else {  // gerade
    //Serial.println("gerade");
    if (pwm > 0) {
      _buffer[_numBuf-1 - i] = pwm;
      _buffer[_numBuf-2 - i] = (_buffer[_numBuf-2 - i] & 0xF0) | (pwm >> 8);
    } else {
      _buffer[_numBuf-1 - i] = 0;
      _buffer[_numBuf-2 - i] &= 0xF0;
    }
  }
  return TLC5947_OK;
}

//////////////////////////////////////////////////
//
//  WRITE
//
void TLC5947_SPI::write()
{
  //  also write when blank == LOW to "preload the registers"
  uint8_t muster[_numBuf];           // die zu übertragenen Daten
  memcpy(muster, _buffer, _numBuf);  // das Feld wird von SPI gelöscht, daher ist eine temporäre Kopie notwendig
  SPI.beginTransaction(SPISettings(_SPIclock, MSBFIRST, SPI_MODE0));
  SPI.transfer(muster, _numBuf);
  SPI.endTransaction();
  //  pulse latch to hold the signals
  digitalWrite(_latch, LOW);
  digitalWrite(_latch, HIGH);
}

//  -- END OF FILE --

Viel Spaß und Erfolg beim Ausprobieren!

Hilfreiche Kommentare und konstruktive Kritik ist immer willkommen :smiley:


EDIT:
20251029 22:15: 68 durch _numBuf-4 ersetzt.
20251030 9:32: j > 0 durch j >= 0 ersetzt,
20251031 10:40: Beschreibung siehe #7.
20251031 12:20: SPI.begin(); in das Anwendungsprogramm verschoben.
20251031 16:00: Link nach Adafruit ergänzt.
20251107 17:50: aktuelle Version
20251112 08:40: aktuelle Version nur noch in einer hpp-Datei.
20251114 09:35: write() in update() umbenannt und Standardformatierung (<Strg>+T) der IDE 2.3.6 verwendet.
20251115 20:08: Zum Download zip-Datei ergänzt.
20251116 09:36: Link zu Noiasca Took Kit for LEDs - Arduino Effect Library for your LEDs ergänzt.

7 Likes

(Genial: Die Tabellen lassen sich nicht direkt zitieren)

Aber sag mal.... Wie hast Du die Zeiten gemessen?

1 Like

Ich vermute mal vor und nach dem write() ein digitalWrite() und den Pin mit einem Logicanalyzer gemessen.

Wo kommen da eigentlich die 68 her?

2 Likes
  start = micros();
  tlc.setLED(0, pwm, 0, pwm);
  tlc.write();
  stop = micros();
  Serial.print("WRITE\t");
  Serial.print(stop - start);
  Serial.println(" µs");

Analog die anderen Bibliotheken.

Mit einem Logicanalyzer ergeben sich etwas andere Werte, weil da digitalWrite noch mit reinspielt. Mir geht es aber eh nur um die Größenordnung, um einschätzen zu können, ob sich eine spezielle Bibliothek überhaut lohnt.

Ich hoffte, alle magischen Zahlen rausgeworfen zu haben, hat nicht geklappt. Werde ich gleich ändern.

Muß _numBuf-4 sein, weil drei Byte für zwei LEDs benötigt werden. Die ersten drei Byte kopiere ich dann nur noch ins restliche Feld.

1 Like

Das sind echt mal krasse unterschiede

Nein, wieder so ein blöder Fehler, es muß j >= 0 heißen.

Danke!

Ich hoffe, Dich damit für die Nutzung der SPI-Hardware begeistern zu können.
:wink:

1 Like

Dank an alle, die sich bis hierhin an einer Verbesserung der Bibliothek beteiligt haben :smiley:

Zeit für kosmetische Korrekturen:

  • Im Konstruktor const ergänzt.
  • In setPWM und getPWM channel als Konstante.
  • Wert von TLC5947_CHANNEL_ERROR auf -1 geändert.
  • Rückgabewert von setPWM auf den Typ int16_t geändert.
  • Hilfsmethode drucke8 entfernt.

Die Änderungen habe ich in #1 eingearbeitet.

Hast du dich schon mal mit Git auseinander gesetzt?

Nein.

Die Bibliothek würde m. E. gut in die Sammlung von @noiasca passen, aber auch andere Möglichkeiten wären denkbar. Eine gute Übersetzung ins Englische traue ich mir aber eher nicht zu.

Du meinst in den Toolkit für LEDs? Da bräuchte man (ich) eigentlich nur einen wrapper ähnlich wie für den PCA9685 schreiben. Wenn du aus deinem Code eine ganze Library machst, würde ich sowas ergänzen.

Code:
Was aus meiner Sicht verändert werden soll, die Klasse soll kein SPI.begin() aufrufen, schon gar nicht im Konstruktor. Gib das besser in den User-Sketch.

Library:
da kann ich dir gerne Helfen. Das wäre neben den zwei Arduino Styleguides mein Lesetipp:

Wenn du dazu sonst noch Fragen hast - einfach laut im Thread reinrufen.

Hardware:
kannst du evtl. in deinem Eingangspost noch auf ein Bastelboard linken das es sich zu kaufen lohnt? Dann besorg ich mir sowas zum Rumspielen...

Danke, habe ich gleich mal umgesetzt.

Ich lese dann mal :face_with_monocle:

1 Like

Hallo agmue,

einiges wurde schon gesagt.
Die #defines stören mich und die Integerrechnung stört mich, wie diese berechnet wird. Korrekt runden kann man das auch noch.
Dann wäre noch die Frage was ist pwm? Frequenz oder Duty Cycle? Pin Nummern sollte man auch klarer benennen. struct?
Rückgabewert von setPWM() sollte bei 'int' bleiben. -1 passt da immer rein. Nur wenn es rein auf 8Bit AVR laufen soll, könnten man über int8_t nachdenken.

Die Handhabung mit den Rückgabewerten gefällt mir auch nicht. Mein Hauptproblem ist getPWM(). Normal kommt ein pwm Wert zurück. Im Fehlerfall soll -1 zurückkommen, kommt aber nicht, wegen unit16_t. Selbst mit in16_t wäre das irreführend wenn plötzlich ein negativer Wert zurückkommt. Wäre bei setPWM ähnlich aber ohne Folgen. Ich würde alles verwerfen, also die Methode nicht weiter ausführen, wenn channel Parameter außerhalb des Limits liegt oder ich würde auf den Maximalwert begrenzen und die Methode abarbeiten. Je nach Anforderung. Oder es gibt noch eine 'getError' Methode womit man bei Bedarf den aktuellen Fehler abfragen kann, wenn man Probleme erkennt aber nicht sieht das channel falsch übergeben wurde oder was auch immer. Ich würde aber nicht einen normalen Rückgabewert mit einem Fehlercode so einfach vermischen. Was man noch machen könnte, ist mehr Aufwand, einen Fehlercode in die freien höchstwertigen Bits einbauen, was man dann wieder filtern muss. Dafür kannste dir die uart Lib von Peter Fleury anschauen. Auf jeden Fall würde ich es so wie es ist nicht belassen.

Der Dekonstruktor ist auch nett gedacht. Aber was gibt denn _buffer zurück? Ist das sinnvoll? Den Dekonstruktor erstellt der Compiler selbst. Eine Speicherfreigabe wird es jedoch auf einen Controller ohne OS nie geben, weil die Instanz immer vorhanden ist und der Buffer ja ständig in Verwendung bleiben sollte. Eine ständige "Speicheranfrage" wäre sowieso nicht gut. Mit Referenz statt Zeiger hat man zwar mehr Tipparbeit, halte ich jedoch für besser. Wenigstens könnte man den Zeiger konstant machen.

Einfach einmal in Ruhe darüber nachdenken. Ob und wie liegt in deiner Hand. Ich finde nur der Anspruch für eine Veröffentlichung sollte hoch liegen. :wink:

Habe ich gemacht, hoffentlich mit vernünftigen Ergebnis.

Rausgeschmissen und durch Konstanten ersetzt.

Das hat auch Adafruit nicht erklärt, ich lese PulsWeitenModulation und verstehe es als Wert für den Strom durch die LEDs, was wiederum die LEDs nichtlinear heller oder dunkler leuchten läßt.

Da weiß ich nicht, was Du meinst.

Die Bibliothek ist für den Mega2560 gedacht, daher bleibe ich bei int8_t.

pwm übergebe ich jetzt als Referenz und nutze den Rückgabewert alleine für den Fehler. Besser?

Hatte ich so übernommen, fliegt raus.

Programmbeispiel
/* Beispiel für TLC5947_SPI: Lauflicht

 Belegung Mega2560
DATA  = COPI = 51
CLOCK = SCA  = 52
LATCH = CS   = 53
OE           = 49
*/
#include "TLC5947_SPI.h"

const int DEVICES =  2;
const int LATCH   = 53;
const int OE      = 49;

TLC5947_SPI tlc(DEVICES, LATCH);

uint16_t pwm = 400;

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("\nStart");
  SPI.begin();
  tlc.begin();
  pinMode(OE, OUTPUT);
  digitalWrite(OE, LOW);

  tlc.setAll(pwm);  // Lampentest
  tlc.write();
  uint16_t aktpwm = 0xFFFF;
  tlc.getPWM(0, aktpwm);
  Serial.print("aktpwm: "); Serial.println(aktpwm);
  delay(1000);
  tlc.setAll(0);
}

void loop() {
  for (byte led = 0; led < tlc.getChannels(); led++) {
    tlc.setPWM(led, pwm);
    tlc.write();
    delay(100);
    tlc.setPWM(led, LOW);
  }
}
TLC5947_SPI.h
/* Arduino library for the TLC5947 24 channel PWM device
 angelehnt an: https://github.com/RobTillaart/TLC5947
 
 Diese Bibliothek überträgt die PWM-Werte für die LEDs per SPI, um die schnelle
 SPI-Hardware der µCs nutzen zu können. Dazu werden die 24 PWM-Werte von je 12 Bit
 in 36 Byte für das Schieberegister untergebracht. 
 
 ChipSelect (CS) bleibt im Unterschied zum SPI-Protokoll während der Datenübertragung
 auf HIGH, erst am Ende gibt es, typisch Latch, einen kurzen Impuls zur Datenübernahme.
 
 Nicht Teil der Bibliothek ist OutputEnable (/OE). Empfehlenswert ist ein 
 PullUp-Widerstand anzuschließen und am Ende von setup() /OE auf LOW zu bringen, 
 um ein Flackern der LEDs nach Reset des µCs zu verhindern.
 
 Getestet mit UNO und Mega2560.
 
 Verwende SPI-Pins:
 CLOCK = SCA
 DATA  = COPI
 LATCH = CS
*/
#pragma once

#include <SPI.h>
#include "Arduino.h"

const int8_t TLC5947_OK = 0;
const int8_t TLC5947_CHANNEL_ERROR = -1;


class TLC5947_SPI
{
public:
  TLC5947_SPI(const uint8_t deviceCount, const uint8_t latch, uint32_t SPIclock = 8000000UL);

  void     begin();
  uint16_t getChannels();
  void     setAll(uint16_t pwm);

  //  fill the internal buffer with PWM values
  //  call write() after all channels are updated (or per channel).
  int8_t   setPWM(const uint16_t channel, uint16_t pwm);
  int8_t   getPWM(const uint16_t channel, uint16_t &pwm);
  void     write();

private:
  uint16_t   _channels;
  size_t     _numBuf;
  uint8_t  * _buffer;
  uint8_t    _latch;
  uint32_t   _SPIclock;
  const uint16_t MAXPWM = 4095;
};
//  -- END OF FILE --
TLC5947_SPI.cpp
#include "TLC5947_SPI.h"

TLC5947_SPI::TLC5947_SPI(const uint8_t deviceCount, const uint8_t latch, uint32_t SPIclock)
{
  _channels = deviceCount * 24;   // 24 LEDs pro Device
  _latch    = latch;
  _SPIclock = SPIclock;
  _numBuf   = _channels * 3 / 2;  // 3 Byte für die Daten von 2 LEDs
  _buffer   = (uint8_t *) calloc(_numBuf, 1);
}

void TLC5947_SPI::begin()
{
  pinMode(_latch, OUTPUT);
  digitalWrite(_latch, LOW);
}

uint16_t TLC5947_SPI::getChannels()
{
  return _channels;
}

void TLC5947_SPI::setAll(uint16_t pwm)  // vierfache Beschleunigung
{
  if (pwm > MAXPWM) pwm = MAXPWM;
  setPWM(0, pwm);
  setPWM(1, pwm);
  for (int j = _numBuf-4; j >= 0; j--) _buffer[j] = _buffer[j+3];
}

//////////////////////////////////////////////////
//
//  PWM
//
int8_t TLC5947_SPI::getPWM(const uint16_t channel, uint16_t &pwm)
{
  if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
  uint16_t _channel = channel;
  uint16_t i = _channel / 2 * 3;  // Integerarethmetik
  if (bitRead(_channel, 0)) {    // ungerade
    //Serial.println("ungerade");
    pwm = (_buffer[_numBuf-3 - i] << 4) | (_buffer[_numBuf-2 - i] >> 4);
  } else {  // gerade
    //Serial.println("gerade");
    pwm = ((_buffer[_numBuf-2 - i] & 0x0F) << 8) | _buffer[_numBuf-1 - i];
  }
  return TLC5947_OK;
}

int8_t TLC5947_SPI::setPWM(const uint16_t channel, uint16_t pwm)
{
  if (channel >= _channels) return TLC5947_CHANNEL_ERROR;
  uint16_t _channel = channel;
  if (pwm > MAXPWM) pwm = MAXPWM;
  uint16_t i = _channel / 2 * 3;  // Integerarethmetik
  if (bitRead(_channel, 0)) {    // ungerade
    //Serial.println("ungerade");
    if (pwm > 0) {
      _buffer[_numBuf-2 - i] = (_buffer[_numBuf-2 - i] & 0x0F) | (pwm << 4);
      _buffer[_numBuf-3 - i] = pwm >> 4;
    } else {
      _buffer[_numBuf-2 - i] &= 0x0F;
      _buffer[_numBuf-3 - i] = 0;
    }
  } else {  // gerade
    //Serial.println("gerade");
    if (pwm > 0) {
      _buffer[_numBuf-1 - i] = pwm;
      _buffer[_numBuf-2 - i] = (_buffer[_numBuf-2 - i] & 0xF0) | (pwm >> 8);
    } else {
      _buffer[_numBuf-1 - i] = 0;
      _buffer[_numBuf-2 - i] &= 0xF0;
    }
  }
  return TLC5947_OK;
}

//////////////////////////////////////////////////
//
//  WRITE
//
void TLC5947_SPI::write()
{
  //  also write when blank == LOW to "preload the registers"
  uint8_t muster[_numBuf];           // die zu übertragenen Daten
  memcpy(muster, _buffer, _numBuf);  // das Feld wird von SPI gelöscht, daher ist eine temporäre Kopie notwendig
  SPI.beginTransaction(SPISettings(_SPIclock, MSBFIRST, SPI_MODE0));
  SPI.transfer(muster, _numBuf);
  SPI.endTransaction();
  //  pulse latch to hold the signals
  digitalWrite(_latch, LOW);
  digitalWrite(_latch, HIGH);
}
//  -- END OF FILE --

Habe ich bis auf die Integerrechnung an alle Punkte gedacht?

Besser so?

Hallo,

gefällt mir persönlich besser, ist auch immer Geschmackssache.
pwm ist demzufolge der Duty Cycle, also die Pulsweite. Okay.

Auch wenn es pingelig erscheint, ich sage es dennoch. :wink:

Die Konstanten machste bitte noch constexpr.

Wegen Integerrechnung in setPWM und getPWM.
uint16_t i = _channel / 2 * 3; // Integerarethmetik
Sollte das nicht lieber lauten
uint16_t i = _channel * 3 / 2; // Integerarethmetik
wie im Konstruktor?
Runden nicht notwendig?
Fehler abfangen nicht notwendig?
Limitprüfung?
Sind nur Fragen, eine Art Checkpunkte.

Ansonsten sehe ich erstmal nichts weiter.

Eine Kleinigkeit noch im Beispiel. Ich weiß pingelig. :wink:
Das LOW sieht hier komisch aus. Ich kann mir da nicht helfen.
tlc.setPWM(led, LOW);
Besser
tlc.setPWM(led, 0);
Noch besser wäre
tlc.setOff(led);
oder so.

Die Bibliothek ist für den Mega2560 gedacht, daher bleibe ich bei int8_t.

Dann solltest du eine library.properties einpflegen und es für architectures=avr einschränken.

Den Rest zeigt die Praxis.

Als i Punkt kannst du noch eine Led Korrekturtabelle einpflegen, mittels der man die Helligkeit in Prozent als Parameter angibt und man damit einen fürs Auge linearen Helligkeitsverlauf hat. :rofl:

Damit klinke ich mich erstmal aus. Man hört sich

Darum frage ich, also danke für Deine Pingeligkeit!

Erledigt.

Ein Beispiel:

  • _channel=10: _channel / 2 * 3 liefert 15 und adressiert dann buffer.
  • _channel=11: _channel / 2 * 3 liefert mit dem Taschenrechner 16,5, was keine ganze Zahl ist. Das bewußte Rechnen mit ganzen Zahlen liefert wieder 15, was ich verwenden kann.

Wie sollte ich das besser machen?

Hmm... _buffer ist ein Pointer, der wird weggeräumt. Aber was ist mit dem Speicher auf den der Pointer zeigt?

Dann würde ich noch das SPI Objekt als Parameter reingeben. Damit werden Systeme mit meheren SPI Controllern unterstützt und zudem wird dadurch für mich klarer, das SPI benutzt wird und ich SPI.begin() aufrufen sollte.

Hallo,

wegen Integerrechnung und Channel. Wenn ich das richtig sehe, kenne jetzt nur das Datenblatt, möchtest mit der Channel Nummer die DAP-No berechnen von der DAP Gehäuseform? Wenn ja, würde ich nichts groß rechnen sondern einen Offset von 5 addieren. Sollte man irgendwo kommentieren, dass es zur DAP Gehäsueform gehört. Wäre man beim nächsten Fall. Gehäuseform als Parameter im Konstruktor. DAP oder RHB. Offset 1 oder Offset 5 zur Channelnummer. Und so kommt zum anderen und die Lib wird immer besser.

Hallo,

helf mir einmal auf die Sprünge. Warum wird _buffer abgeräumt? Von wem? Von was?

Meinst Du so?

Teil von TLC5947_SPI.cpp
TLC5947_SPI::TLC5947_SPI(const uint8_t deviceCount, const uint8_t latch, uint32_t SPIclock, SPIClass &spi)
{
  _channels = deviceCount * 24;   // 24 LEDs pro Device
  _latch    = latch;
  _SPIclock = SPIclock;
  _spi      = spi;
  _numBuf   = _channels * 3 / 2;  // 3 Byte für die Daten von 2 LEDs
  _buffer   = (uint8_t *) calloc(_numBuf, 1);
}

Und im Programm TLC5947_SPI tlc(DEVICES, LATCH, 4000000UL, SPI);.

Nein. SPI kann nach meinem derzeitigen Wissensstand nur Bytes übertragen, also 8 Bit. Es geht also darum, zwölf Bit in Bytes unterzubringen. Da 2 * 12 = 3 * 8 ist, packe ich zweimal 12 Bit in drei Bytes, in der richtigen Reihenfolge für das Schieberegister. Das ist sozusagen der Kerngedanke dieser Bibliothek.

Ja, ich freue mich :smiley:

In Constructor reservierst du dir mit calloc() Speicher und hälst den Pointer darauf in _buffer. Der generierte Destructor gibt _buffer wieder frei. Aber was ist mit dem reservierten Speicher?

Ja