LEDキューブを作った

LEDキューブとは発光LEDを立方体に並べて作られた文字通りの箱。下の写真が完成品。

Make: Tokyo Meeting(MTM)に行くとよく見かけるんだけど、任意の点をあの少ない足の数でどうやって光らせてるのかさっぱり分からなった。ソフト屋が仕組み聞いても理解できないんだろうとスルーしてたんだけど、前回のMTM07で展示してる人に何気なく聞いてみると「LEDキューブでググるとわかるよ」とドラクエみたいな回答をもらったので検索してみると、確かに分かる。ので、コレなら俺でも作れそうと作ってみた次第。

とはいえ実際はググるだけじゃわからない事も多かったので、オライリーMake vol08に載ってた4 * 4 * 4の作り方を参考に、5 * 5 * 5にして作ってみた。クリスマス近いので電球色のLEDを選ぶ。

↓は雨粒が落ちるようなイメージでアニメーションさせたところ。


作ってみて分かったけど、LEDは小さめのものを選んだ方が綺麗かもしれない。LEDを綺麗に並べる為の治具も、真面目に木とドリルで作らないとウネウネになってしまったりして、作りが荒くなってしまった。参考にした本では1辺4つのキューブの作り方だったけど、小さくてなんだかつまんないので1辺を5つにしたら電流は設計以上に流れるわUSBの給電が不安になるわで、LEDだと省エネで節電とか言われてもこういう小さい電子工作の世界だと電気量もろ影響するんだなーとか妙に感心した。で、無計画にサイズ変更したので部品が足りなくなり秋葉原に何度も通うという失態。これだからソフトウェア発想の工作は駄目なんだ。

仕組みの話

Arduino Mega 2560というマイコンとIOポートとパソコンからのプログラムの焼き付けが一体となった超簡単基盤で点灯の制御をさせた。普及版のArduino DuemilanoveってやつはIOピンが20数本くらいだけど、Arduino Mega 2560はAnalogピン含めると70くらいあるので、今回みたいにピンが沢山要るのを作りたくなっても余裕なのだ。

少ない足で3Dの任意の場所に点灯させる仕組みは、上述のオライリー(Make vol08)を読むか「LEDのダイナミック点灯」で調べると大体分かる。
大まかに説明すると、1階部分だけ通電状態にして25個のLEDの点灯と消灯を設定。次は2階部分だけのLEDの点灯と消灯を設定...と5階分する。ゆっくりやると下から上に光るだけだけど、コレを目に見えないくらい高速に実施すると同時にキューブ全体が光ってるように見えるというカラクリ。
なので、1辺が5つのキューブの場合全部の球は125個だけど、制御に使うIOポートは30個で済む。1面25個で、各階の通電制御で5個。あわせて30個。

制御のためのソフトウェアはArduino IDEと呼ばれる開発環境で開発できる。言語仕様は大体C。違うところはmain()から始まらずに、初回にsetup()が呼ばれて、loop()が呼ばれ続けてループし続けるというところが違う感じ。

アート方面などの異分野の人にでも簡単に開発できるように配慮されているらしくて、とても簡単(いややっぱ異分野にはムズイか)。コード書いてアップロードボタンを押すと、コンパイルからメモリへの焼き付けまで自動で実施してくれる優れもの。

そういえばRubyで書けるgemも発見したけどまだ試していない。今度試したいなー。

動画の制御のコードは以下の通り。

#define LED_SIZE   25
#define FLOOR_SIZE  5
#define SIDE_SIZE   5

byte dots[] = {
  22, 23, 25, 27, 29,
  31, 33, 35, 37, 39,
  41, 43, 45, 47, 49,
  51, 53, 69, 68, 67,
  57, 58, 59, 60, 61
};

byte floors[] = {
  2, 3, 4, 5, 6
};

byte memmap[FLOOR_SIZE *  LED_SIZE];


void pset(byte* pattern, int floor_num, int dot_num, byte value) {
  pattern[floor_num * LED_SIZE + dot_num] = value;
}

byte pget(byte* pattern, int floor_num, int dot_num) {
  return pattern[floor_num * LED_SIZE + dot_num];
}

void setup() {
  int i, j;
  
  randomSeed(analogRead(0));
  
  for (i = 0; i < LED_SIZE; i++)
    pinMode(dots[i], OUTPUT);
    
  for (i = 0; i < FLOOR_SIZE; i++)
    pinMode(floors[i], OUTPUT);
    
  setFloor(0);
  
  for (i = 0; i < FLOOR_SIZE; i++)
    for (j = 0; j < LED_SIZE; j++)
      pset(memmap, i, j, LOW);
}

// num: 0 origin
void writeDotValue(int num, int value) {
  digitalWrite(dots[num], value); 
}

// num: 0 origin
void setFloor(int num) {
  int i;

  for (i = 0; i < FLOOR_SIZE; i++)
    digitalWrite(floors[i], i == num ? HIGH: LOW);
}

// 一回分の描画
#define DELAY_US 600
int setDots(byte* pattern) {
  long current;
  long wait_us = 0;
  int f, i;

  for (f = 0; f < FLOOR_SIZE; f++) {
    current = micros();
    
    setFloor(f);
    for (i = 0; i < LED_SIZE; i++)
      writeDotValue(i, pget(pattern, f, i));

    delayMicroseconds(DELAY_US);
    wait_us += micros() - current;
  }
  return (int)(wait_us / 1000);
}

void draw(byte* pattern, int ms) {
  int total_ms = 0;
  
  while (true) {
     total_ms += setDots(pattern);
     if (total_ms >= ms)
       break;
  }
}

void loop() {
  int i, j;
  
  for (i = 0; i < FLOOR_SIZE - 1; i++)
    for (j = 0; j < LED_SIZE; j++) 
       pset(memmap, i, j,
            pget(memmap, i + 1, j));
  
  for (j = 0; j < LED_SIZE; j++) 
    pset(memmap, 4, j, random(10000) % 9 == 0 ? HIGH: LOW);
    
  draw(memmap, 150);
}