Arduinoで遊ぶページ

Arduinoで遊んだ結果を残すページです。
garretlab
LEDドライバ

概要

TLC5940というLEDドライバの制御を行います。Arduino用のTLC5940ライブラリは既に存在しています。制御の仕組みに興味があったので、ライブラリの解析を行うとともに、リファクタリングしてみました。

目的

TLC5940をArduinoから制御します。

データシート

TLC5940のデータシートからわかることをまとめます。

機能

機能の概要を以下に示します。

チャネル数
TLC5940は、1個のICで16個までのLEDを制御できるドライバです。複数のTLC5940を直列に接続することで、制御可能なLEDの数を増やすことができます。
明るさの制御
個々のLEDの明るさを12ビット(4096段階)のPWM(グレースケールPWM)で制御することができます。
ドット補正
個々のLEDの明るさの最大値を6ビット(64段階)で制御することができます。LEDの明るさにばらつきがあるときや複数の色のLEDを同時に使いたいときに利用できると思います。
最大電流
Vcc < 3.6V のときは60mAまで、Vcc > 3.6Vのときは120mAまで流すことができます。
エラー検出
オープンLED、温度エラーを検出可能です。

ピン

私が持っているのは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に書き込む)もありますが省略します。

  1. ドット補正データ入力モード
  2. グレースケールPWMモード

それぞれの処理の概要は以下の通りです

ドット補正データ入力モード

VPRGをHIGHにするとドット補正データ入力モードに移行します。

ドット補正データは、6ビット(64ステップ)のデータで、チャネルごとに設定することができます。これを設定することにより、出力電流をチャネルごとに補正することができます。

nチャネルのデータ補正値をDC、出力電流 をIOUTn、最大電流をImaxとすると、IOUTn = Imax * DCn / 63 となります。

Imaxは、IREFに接続する抵抗R(IREF)によって設定される値で、Imax = V(IREF) * 31.5 / R(IREF) となります。V(IREF)は1.24Vです。また、Imaxは5mAから120mAの値となるように設定する必要があります。

グレースケールPWMモード

VPRGをLOWにするとグレースケールPWMモードに移行します。

各チャネルのPWMのデューティー比を設定することで、各チャネルの明るさを変更することができます。

グレースケールデータをGSnとすると明るさBrightness(%)は、GSn * 100 / 4095 と表すことができます。

タイミングチャート

タイミングチャートを以下に示します。

ドット補正データを入力した後、グレースケールデータを入力します。データは、一番最後の(番号の大きい)チャネルのMSBから順に送信していきます。XLATにパルスを送信した時点でのSINのデータが保持されます。全データを送信した後、XLATにパルスを送ることでデータがレジスタに保存されます。

2度目以降のグレースケールデータ送信前には、1クロックを送信する必要があります。グレースケールPWMモードでは、BLANKがHIGHのときにXLATパルスを送信する必要があります。

TLC5940ライブラリの解析

ArduinoにはTLC5940を制御するためのライブラリが存在しています。制御方法を理解するために、ソースコードを解析しました。ポイントとなる部分を以下に示します。

XLATとBLANK、GSCLKの生成

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に接続したピンにパルスを送信しています。

TLC5940Lite

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を有効にしてください。

Arduinoで遊ぶページ
Copyright © 2014 garretlab all rights reserved.
inserted by FC2 system