Arduino で 8x8 ドットマトリクス LED を階調表示する

#はじめに
最初 Tang Nano (FPGA) で8x8のドットマトリクス LED を駆動する回路を書こうと思ったのですが、Tang Nano の I/O の VIN は Max 3.6V で LED を駆動するにはレベルシフタが無いと無理でした。仕方ないのとやっぱり回路(RTL)を書くのが面倒くさかったので、とりあえずやりたいことを Arduino UNO にソフトウェアで実装してみました。えーと、これでいいじゃん（笑）

#概要
タイマー割り込みで駆動タイミングを作り、8x8 のフレームバッファのデータをドットマトリクス LED に表示する。各ドットは 8階調の階調表示ができる。
フレームバッファはダブルバッファとし、表示していない側のフレームバッファにデータを書き込んでから表示を書き換えるとチラつきが。。低減できるはず。

#回路
ドットマトリクス LED のピン配はわかりにくく、また自分で調べるのも面倒だったので、[https://www.toyship.org/archives/1209](https://www.toyship.org/archives/1209)を参考にしました。

とりあえずアノード側(COL)、カソード側(ROW)と呼びます。

配線がすごくゴチャゴチャしていますが、まずは COL 側の 8つの端子全てに電流制限抵抗 150ohm を入れます。この時ROW側に抵抗を入れてはいけません。ROW一つで8つのダイオードを同時にONすることができるのですが、ROWに入れた抵抗は共通インピーダンスとなり、ONするCOLの数で流れる電流が変わるので抵抗の両端の電圧も変わり、LEDの電流がバラバラになるからです。

次に COL, ROW を Arduino に配線していくのですが、タイミングをなるべく揃えたいので、特に COL は同じポート PORTD に 集めています。並びもフレームバッファに書き込む際や読み出した後に余計な操作をして時間を食いたくないので、配線がぐちゃぐちゃになりますが、これも COL1,2,3..8 を PORTD0,1,2..7 と順序よく揃えています。

ROW1-8 側も同じポートにまとめたかったのですが、8つ同時に使えるポートがなく、仕方なく PORTC0,1..5 と PORTB0,1 に分割しています。

![8x8LEDdotmatrix_ブレッドボード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206813/cf472134-1fcd-665d-033f-c1c78782fe00.png)

#タイミングチャート
よくあるディスプレイの駆動の仕方と同じです。Vsync, Hsync などは特に必要ないので作ってません。

ROW 側を順次スキャンしていき、ROW に対応する COL のデータをフレームバッファから読み出して COL に出力します。COL のデータはそのまま出力せずに、ON する時間をコントロールして階調を出すようにします。各 ROW は 32分割されていて、8階調の値にしたがって ON 時間がコントロールされます。8階調なのに 32分割している理由は、ON duty と実際に感じる明るさがリニアでは無いので、二次関数に近い値にマッピングした ON duty を使用しています。

またタイマー割り込みはこの 32分割のタイミングで生じます。大体、1/60sec で一画面を書き換えれば見た目のチラつきは抑えられます。32 x 8 = 256 で、1/60/256 = 65us で割り込みをかけています。

ROW は常にどこか一つが L レベルを出力します。
![timing-chart-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206813/9ad47256-283f-357a-55f7-77cd5bfa673b.png)

ROW が L になっている期間を拡大すると、COL は画素の値に応じて、32階調中の 8 箇所で ON duty を変更します。
![timing-chart-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206813/0f3c9008-7bf8-9640-6ce8-95bd60976070.png)



#プログラム
表示を一定周期で書き換えます。表示データはフレームバッファ frame_buf にストアされているデータを使います。タイマー割り込みを使い timerFire() を定期的に呼び出します。このタイミングは COL の 32階調のタイミングに相当します。Timer1 を割り込みソースに使っています。

timerFire() では COL をクリアし、次の ROW をドライブして、既に用意してある COL データを GPIO に出力します。その後に、カウンターをインクリメントしたり、次の表示データを準備したりします。またこの中で操作する GPIO のアクセスも速くしたいので、PORTx に直接代入しています。_BV() マクロも使ってません。

フレームバッファの値は loop() の中などで表示とは非同期のタイミングで書き換えることができます。
フレームバッファは 3次元の配列 frame_buf[page][row][col] で表され、page は 0, 1 を選択でき、row(0-7), col(0-7) 番号に相当するところに輝度の値(0-7)を入れます。
表示したいページはグローバル変数 disp_page で表示したい page を指定します。


```cpp:8x8dotmatrix_arduino_uno.ino
#include <TimerOne.h>

#define DUTY_MAX  32
#define ROW_NUM   8
#define COL_NUM   8

int row[ROW_NUM] = {A0, A1, A2, A3, A4, A5, 8, 9};  // PORTC0-5, PORTB0,1
int col[COL_NUM] = {0, 1, 2, 3, 4, 5, 6, 7};        // PORTD

int color_table[] = {0, 1, 2, 5, 8, 13, 20, 31};
int frame_buf[2][ROW_NUM][COL_NUM];

volatile int disp_page = 0;

int duty_cnt = 0;
int row_cnt = 0;
uint8_t col_data = 0;

void setRow(int row) {
  // Disable all ROW drives
  PORTC |= 0x3F;
  PORTB |= 0x03;

  // Set ROW drive
  int row_b = row - 6;
  if (row > 5) {
    PORTB &= ~(1 << row_b);
  } else {
    PORTC &= ~(1 << row);
  }
}

void timerFire() {
  // Clear COL
  PORTD = 0;
  
  // Set new ROW
  setRow(row_cnt);
  
  // Set new COL
  PORTD = col_data;

  // Update conter
  duty_cnt++;
  if (duty_cnt == DUTY_MAX) {
    duty_cnt = 0;
    row_cnt++;
    row_cnt %= 8;
  }

  // Prepare next COL data
  col_data = 0;
  for (int i = 0; i < COL_NUM; i++) {
    if (color_table[frame_buf[disp_page][row_cnt][i]] > duty_cnt) {
      col_data |= (1 << i);
    }
  }
}

void setup() {
  // Timer setting
  Timer1.initialize(65);
  Timer1.attachInterrupt(timerFire);
  
  // ROW, COL all off
  for (int i = 0; i < COL_NUM; i++) {
    pinMode(col[i], OUTPUT);
    digitalWrite(col[i], LOW);
  }

  for (int i = 0; i < ROW_NUM; i++) {
    pinMode(row[i], OUTPUT);
    digitalWrite(row[i], HIGH);
  }
  
  // Set page1 image
  for (int i = 0; i < ROW_NUM; i++) {
    for (int j = 0; j < COL_NUM; j++) {
      if (i > 3) {
        if (j > 3) {
          frame_buf[1][i][j] = 6;
        } else {
          frame_buf[1][i][j] = 2;
        }
      } else {
        if (j > 3) {
          frame_buf[1][i][j] = 2;
        } else {
          frame_buf[1][i][j] = 6;
        }
      }
    }
  }

}

void loop() {
  // Draw demo images
  // Display page0
  disp_page = 0;
  // Random dot
  for (int i = 0; i < 5; i++) {
    int x = random(0, 8);
    int y = random(0, 8);
    for (int k = 0; k < 8; k++) {
      frame_buf[disp_page][x][y] = k;
      delay(30);
    }
    for (int k = 0; k < 8; k++) {
      frame_buf[disp_page][x][y] = 7 - k;
      delay(30);
    }
  }
  delay(400);

  // Gradation
  for (int h = 0; h < 8; h++) {
    for (int i = 0; i < 8; i++) {
      for (int j = 0; j < 8; j++) {
        frame_buf[disp_page][i][j] = ((j + i) / 2) >> (7 - h);
      }
    }
    delay(50);
  }
  delay(300);

  // Increase brightness
  for (int h = 0; h < 8; h++) {
    for (int i = 0; i < 8; i++) {
      for (int j = 0; j < 8; j++) {
        frame_buf[disp_page][i][j] = h;
      }
    }
    delay(80);
  }

  // Decrease brightness
  for (int h = 0; h < 8; h++) {
    for (int i = 0; i < 8; i++) {
      for (int j = 0; j < 8; j++) {
        frame_buf[disp_page][i][j] = 7 - h;
      }
    }
    delay(80);
  }

  // Display page1
  disp_page = 1;
  delay(600);

}

```

#動画
実物はもう少し綺麗です。
[![You tube movie](http://img.youtube.com/vi/mDksmNHmEk8/0.jpg)](http://www.youtube.com/watch?v=mDksmNHmEk8)