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
setPWMmuß also immer einwriteerfolgen, 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 ![]()
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.