TLC5940というLEDドライバの制御を行います。Arduino用のTLC5940ライブラリは既に存在しています。制御の仕組みに興味があったので、ライブラリの解析を行うとともに、リファクタリングしてみました。
TLC5940をArduinoから制御します。
TLC5940のデータシートからわかることをまとめます。
機能の概要を以下に示します。
私が持っているのは28ピンのDIPタイプのTLC5940です。以下に各ピンの役割を示します。
ピン番号 | 名前 | 意味 |
---|---|---|
1 | OUT1 | 定電流出力(LEDを接続する)。 |
2 | OUT2 | 定電流出力(LEDを接続する)。 |
3 | OUT3 | 定電流出力(LEDを接続する)。 |
4 | OUT4 | 定電流出力(LEDを接続する)。 |
5 | OUT5 | 定電流出力(LEDを接続する)。 |
6 | OUT6 | 定電流出力(LEDを接続する)。 |
7 | OUT7 | 定電流出力(LEDを接続する)。 |
8 | OUT8 | 定電流出力(LEDを接続する)。 |
9 | OUT9 | 定電流出力(LEDを接続する)。 |
10 | OUT10 | 定電流出力(LEDを接続する)。 |
11 | OUT11 | 定電流出力(LEDを接続する)。 |
12 | OUT12 | 定電流出力(LEDを接続する)。 |
13 | OUT13 | 定電流出力(LEDを接続する)。 |
14 | OUT14 | 定電流出力(LEDを接続する)。 |
15 | OUT15 | 定電流出力(LEDを接続する)。 |
16 | XERR | エラー出力。エラーを検出するとLOWになる。 |
17 | SOUT | シリアルデータ出力。複数のTLC5940を接続する際、次のTLC5940のシリアルデータ入力に接続する。 |
18 | GSCLK | グレースケールPWM用のリファレンスクロック。 |
19 | DCPRG | ドット補正データの入力先の選択。LOWのときはEEPROMから、HIGHのときはレジスタから入力する。 |
20 | IREF | 出力電流設定用。 |
21 | VCC | 電源入力。 |
22 | GND | アース。 |
23 | BLANK | HIGHのときは全出力をLOWにする。LOWのときはグレースケールPWMにより制御する。 |
24 | XLAT | シリアルデータ入力完了を示すラッチ信号の入力。 |
25 | SCLK | シリアルデータ用クロック。 |
26 | SIN | シリアルデータ入力。 |
27 | VPRG | LOWのときはグレースケールデータ入力モード、HIGHのときはデータ補正データ入力モード。 |
28 | OUT0 | 定電流出力(LEDを接続する)。 |
TLC5940の動作は大きく以下の2つに分けることができます。他にEEPROMプログラミングモード(ドット補正データをEEPROMに書き込む)もありますが省略します。
それぞれの処理の概要は以下の通りです
VPRGをHIGHにするとドット補正データ入力モードに移行します。
ドット補正データは、6ビット(64ステップ)のデータで、チャネルごとに設定することができます。これを設定することにより、出力電流をチャネルごとに補正することができます。
nチャネルのデータ補正値をDCn、出力電流 をIOUTn、最大電流をImaxとすると、IOUTn = Imax * DCn / 63 となります。
Imaxは、IREFに接続する抵抗R(IREF)によって設定される値で、Imax = V(IREF) * 31.5 / R(IREF) となります。V(IREF)は1.24Vです。また、Imaxは5mAから120mAの値となるように設定する必要があります。
VPRGをLOWにするとグレースケールPWMモードに移行します。
各チャネルのPWMのデューティー比を設定することで、各チャネルの明るさを変更することができます。
グレースケールデータをGSnとすると明るさBrightness(%)は、GSn * 100 / 4095 と表すことができます。
タイミングチャートを以下に示します。
ドット補正データを入力した後、グレースケールデータを入力します。データは、一番最後の(番号の大きい)チャネルのMSBから順に送信していきます。XLATにパルスを送信した時点でのSINのデータが保持されます。全データを送信した後、XLATにパルスを送ることでデータがレジスタに保存されます。
2度目以降のグレースケールデータ送信前には、1クロックを送信する必要があります。グレースケールPWMモードでは、BLANKがHIGHのときにXLATパルスを送信する必要があります。
ArduinoにはTLC5940を制御するためのライブラリが存在しています。制御方法を理解するために、ソースコードを解析しました。ポイントとなる部分を以下に示します。
XLATとBLANK、GSCLKは、ハードウェアのタイマを直接利用して生成しています。
XLATとBLANKはTIMER1AとTIMER1Bを、Phase and Frequency Correct PWMモードで動作させています。このモードは、タイマカウンタ(TCNT1)がBOTTOM(0)からTOP(ICR1に設定した値。TLC5940ライブラリのデフォルトでは8192に設定されています)まで増加し、その後、TOPからBOTTOMまで減少します。この際、減少時にTCNT1とOCR1A/OCR1Bとが一致した際に、出力がHIGHを出力し、増加時にTCNT1とOCR1A/OCR1Bとが一致した際に、出力がLOWになります。
TLC5940ライブラリでは、OCR1Aを1にOCR1Bを2に設定することで、BLANKがHIGHになっているときに、XLATがHIGHになるようにしています。
XLATとBLANKを生成するためのタイマの設定は以下の通りです。TLC_PWM_PERIODはデフォルトで8192です。
/* Timer 1 - BLANK / XLAT */
TCCR1A = _BV(COM1B1); // non inverting, output on OC1B, BLANK
TCCR1B = _BV(WGM13); // Phase/freq correct PWM, ICR1 top
OCR1A = 1; // duty factor on OC1A, XLAT is inside BLANK
OCR1B = 2; // duty factor on BLANK (larger than OCR1A (XLAT))
ICR1 = TLC_PWM_PERIOD; // see tlc_config.h
TCCR1B |= _BV(CS10); // no prescale, (start pwm output)
GSCLKは、TIMER2AをFAST PWMモードで動作させています。タイマカウンタ(TCNT2)はBOTTOM(0)からTOP(OCR2Aに設定した値)までカウントしたらBOTTOMに戻ります。タイマカウンタが0からOCR2Bに設定した値の間の間、HIGHを出力します。BLANKとBLANKの間に4096のパルスを生成する必要があります。
GSCLKを生成するためのタイマの設定は以下の通りです。TLC_GSCLK_PERIODはデフォルトで3です。BLANKとBLANKの間は(8192*2 - 4)クロックです。この間に4096個のパルスが必要です。(8192*2 - 4) / 4096 = 3.999なので、これより小さい、3が設定されています。
TCCR2A = _BV(COM2B1) // set on BOTTOM, clear on OCR2A (non-inverting),
// output on OC2B
| _BV(WGM21) // Fast pwm with OCR2A top
| _BV(WGM20); // Fast pwm with OCR2A top
TCCR2B = _BV(WGM22); // Fast pwm with OCR2A top
OCR2B = 0; // duty factor (as short a pulse as possible)
OCR2A = TLC_GSCLK_PERIOD; // see tlc_config.h
TCCR2B |= _BV(CS20); // no prescale, (start pwm output)
XLATとBLANKの動作の概念図を以下に示します。
ドット補正データやグレースケールデータは、SPIを使って送信します。TLC5940ライブラリでは、SPIライブラリは利用せず、直接レジスタを設定しています。
データ転送とタイマカウンタによるPWM出力とは同期していないので、データ転送前にXLATが出力されないようにレジスタを設定し、データをすべて送信した後、再度XLATが出力されるようにレジスタを設定しています。ドット補正データ送信時は、GSCLKが止まっているので、単にXLATに接続したピンにパルスを送信しています。
TLC5940ライブラリを私の好みに合わせてリファクタリングました。その際機能追加と機能削減しています。機能が減っていることもありTLC5940Liteというライブラリ(クラス)名としました。主な変更点は以下の通りです。
項目 | TLC5940 | TLC5940Lite | 備考 |
---|---|---|---|
クラス化 | 一部の関数はクラス内のメソッドではなく、通常の関数として定義されている。 | 全ての関数をクラス内のメソッドにした。 | 割り込みベクタから呼び出される関数については、静的メンバ関数としてクラス外からも呼び出しできるようにしてある。 |
ドット補正データの扱い | チャネル個別にドット補正データを取り扱うことができない。 | チャネル個別にドット補正データを取り扱うことができる。 | |
複数TLC5940の扱い | 設定ファイルに個数を定義。 | クラスの初期化時にメソッドのパラメータとして個数を渡す。 | 変数を動的に確保するコードが追加されているため、プログラムサイズが増加している。 |
初期化手順 | init()ですべての初期化を行う。 | ドット補正データを個別に設定するメソッドを作成したため、初期化処理を2段階に分割。 | 初期化1と初期化2の間に、ドット補正データを設定する。 |
関数名 | スネークケース。 | ローワーキャメルケースにして、名称変更。 | |
入力系関数 | XERRを読み出すルーチンあり。 | XERRを読み出すルーチンなし。 |
TLC5940Liteを使ってLEDを点灯しているところです。デバッグのためにTLC5940を2個接続しています。右側のほうが明るくなるようにしています。
今回の作成したしたスケッチを以下に示します。TLC5940ライブラリはGPLv3に従うので、このライブラリもGPLv3です。GitHubのリポジトリはこちら。
TLC5940Lite.h
/*
* TLC5940Lite.h
*
* Copyright (c) 2014, garretlab at gmail.com All rights reserved.
* This library is based on TLC5940 Library which is based on GNU General Public License Version 3.
*/
#ifndef TLC5940LITE_H
#define TLC5940LITE_H
#include <Arduino.h>
class TLC5940Lite {
public:
void init(uint8_t numTLC5940, int vprgPin, uint8_t initialDC = 63, uint16_t initialGS = 0);
void begin();
void setDCData(uint8_t channel, uint8_t value);
void sendDCData();
uint8_t getDCData(uint8_t channel);
void setGSData(uint8_t channel, uint16_t value);
uint8_t sendGSData();
uint16_t getGSData(uint8_t channel);
// Need to be public to use in interrupt vector
static void disableXLAT();
static void clearXLATInterrupt();
static volatile uint8_t needXLAT;
private:
int numTLC5940;
int vprgPin;
int xlatPin;
int blankPin;
int sclkPin;
int gsclkPin;
int firstGSInputCycle;
uint8_t *dcData;
uint8_t *gsData;
void enableXLAT();
void pulsePin(int pin);
void setXLATInterrupt();
void dcModeStart();
void dcModeStop();
void beginPWM();
};
extern TLC5940Lite Tlc;
#endif
TLC5940Lite.cpp
/*
* TLC5940Lite.cpp
*
* Copyright (c) 2014, garretlab at gmail.com All rights reserved.
* This library is based on TLC5940 Library which is based on GNU General Public License Version 3.
*/
#include "TLC5940Lite.h"
#include "Arduino.h"
#include <SPI.h>
void TLC5940Lite::init(uint8_t numTLC5940, int vprgPin, uint8_t initialDC, uint16_t initialGS) {
needXLAT = 0;
// The number of TLC5940s
this->numTLC5940 = numTLC5940;
dcData = new uint8_t[numTLC5940 * 12];
gsData = new uint8_t[numTLC5940 * 24];
for (int i = 0; i < numTLC5940 * 16; i++) {
setDCData(i, initialDC);
setGSData(i, initialGS);
}
// VPRG: User defined
this-> vprgPin = vprgPin;
pinMode(vprgPin, OUTPUT);
// XLAT: TIMER1A
xlatPin = 9;
pinMode(xlatPin, OUTPUT);
// BLANK: TIMER1B
blankPin = 10;
pinMode(blankPin, OUTPUT);
// GSCKL: TIMER2B
gsclkPin = 3;
pinMode(gsclkPin, OUTPUT);
// SCLK: SPI SCK
sclkPin = SCK;
pinMode(sclkPin, OUTPUT);
firstGSInputCycle = 1;
// SPI setup
SPI.begin();
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
SPI.setClockDivider(SPI_CLOCK_DIV2);
}
void TLC5940Lite::begin() {
sendDCData();
sendGSData();
pulsePin(xlatPin);
beginPWM();
}
void TLC5940Lite::setDCData(uint8_t channel, uint8_t value) {
uint8_t *dc = dcData + (16 * numTLC5940 - channel - 1) * 3 / 4;
value &= 0x3f;
switch (channel % 4) {
case 0:
*dc = (*dc & 0xc0) | value;
break;
case 1:
*dc = (*dc & 0xf0) | (value >> 2);
dc++;
*dc = (*dc & 0x3f) | (value << 6);
break;
case 2:
*dc = (*dc & 0xfc) | (value >> 4);
dc++;
*dc = (*dc & 0x0f) | (value << 4);
break;
case 3:
*dc = (*dc & 0x03) | (value << 2);
break;
}
}
void TLC5940Lite::sendDCData() {
disableXLAT();
clearXLATInterrupt();
needXLAT = 0;
digitalWrite(vprgPin, HIGH);
uint8_t *p = dcData;
for (int i = 0; i < (numTLC5940 * 12); i++) {
SPI.transfer(*p++);
}
pulsePin(xlatPin);
digitalWrite(vprgPin, LOW);
firstGSInputCycle = 1;
}
uint8_t TLC5940Lite::getDCData(uint8_t channel) {
uint8_t *dc = dcData + (16 * numTLC5940 - channel - 1) * 3 / 4;
uint8_t value;
switch (channel % 4) {
case 0:
value = *dc & 0x3f;
break;
case 1:
value = ((*dc & 0x0f) << 2) | (*(dc + 1) & 0xc0) >> 6;
break;
case 2:
value = ((*dc & 0x03) << 4) | (*(dc + 1) & 0xf0) >> 4;
break;
case 3:
value = (*dc & 0xfc) >> 2;
break;
}
return value;
}
void TLC5940Lite::setGSData(uint8_t channel, uint16_t value) {
value &= 0x0fff;
uint8_t *gs = gsData + (16 * numTLC5940 - channel -1) * 3 / 2;
switch (channel % 2) {
case 0:
*gs = (*gs & 0xf0) | (value >> 8);
gs++;
*gs = value & 0xff;
break;
case 1:
*gs = value >> 4;
gs++;
*gs = (*gs & 0x0f) | (value << 4);
break;
}
}
uint8_t TLC5940Lite::sendGSData() {
if (needXLAT) {
return 1;
}
disableXLAT();
if (firstGSInputCycle) {
firstGSInputCycle = 0;
} else {
pulsePin(sclkPin);
}
uint8_t *p = gsData;
for (int i = 0; i < (numTLC5940 * 24); i++) {
SPI.transfer(*p++);
}
needXLAT = 1;
enableXLAT();
setXLATInterrupt();
return 0;
}
uint16_t TLC5940Lite::getGSData(uint8_t channel) {
uint8_t *gs = gsData + (16 * numTLC5940 - channel -1) * 3 / 2;
uint16_t value;
switch (channel % 2) {
case 0:
value = (uint16_t)(*gs & 0x0f) << 8 | *(gs + 1);
break;
case 1:
value = (uint16_t)(*gs << 4) | (*(gs + 1) & 0xf0)>> 4;
break;
}
return value;
}
void TLC5940Lite::enableXLAT() {
TCCR1A = _BV(COM1A1) | _BV(COM1B1);
}
void TLC5940Lite::disableXLAT() {
TCCR1A = _BV(COM1B1);
}
void TLC5940Lite::setXLATInterrupt() {
TIFR1 |= _BV(TOV1);
TIMSK1 = _BV(TOIE1);
}
void TLC5940Lite::clearXLATInterrupt() {
TIMSK1 = 0;
}
void TLC5940Lite::pulsePin(int pin) {
digitalWrite(pin, HIGH);
digitalWrite(pin, LOW);
}
void TLC5940Lite::dcModeStart() {
disableXLAT();
clearXLATInterrupt();
needXLAT = 0;
digitalWrite(vprgPin, HIGH);
}
void TLC5940Lite::dcModeStop(void) {
digitalWrite(vprgPin, LOW);
firstGSInputCycle = 1;
}
void TLC5940Lite::beginPWM() {
TCCR1A = _BV(COM1B1);
TCCR1B = _BV(WGM13);
OCR1A = 1;
OCR1B = 2;
ICR1 = 8192;
TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
TCCR2B = _BV(WGM22);
OCR2A = 3;
OCR2B = 0;
TCCR1B |= _BV(CS10);
TCCR2B |= _BV(CS20);
}
ISR(TIMER1_OVF_vect) {
TLC5940Lite::disableXLAT();
TLC5940Lite::clearXLATInterrupt();
TLC5940Lite::needXLAT = 0;
}
volatile uint8_t TLC5940Lite::needXLAT;
TLC5940Lite Tlc;
TLC5940test.ino
include "TLC5940Lite.h"
#include <SPI.h>
int leds[] = {1, 2, 3, 28, 29, 30}; /* Where LEDs are connected */
int numLeds = sizeof (leds) / sizeof (leds[0]);
void setup() {
Tlc.init(2, 8, 63, 4095);
Tlc.begin();
}
void loop() {
for (int channel = 0; channel < numLeds; channel++) {
for (int brightness = 0; brightness < 4095; brightness++) {
Tlc.setGSData(leds[channel], brightness);
Tlc.sendGSData();
delayMicroseconds(100);
}
for (int brightness = 4095; brightness > -1; brightness--) {
Tlc.setGSData(leds[channel], brightness);
Tlc.sendGSData();
delayMicroseconds(100);
}
}
}
Arduino 1.5.5/Arduino Uno
メニューを表示するためにJavaScriptを有効にしてください。