ILI9341ベースのQVGA/SPI TFT液晶モジュールを試してみる

# モジュールについて
Amazonで680円で売ってたので購入した2.2インチのQVGA液晶モジュール。環境整備の一環として動作確認してみました。モノ的にはaitendoさんの[この商品](http://www.aitendo.com/product/7277)と同等かと思います。

SDカードのアダプタも付いてるので、マイコン1つでSDカードからデータ呼んで液晶に表示、みたいな使い方は気軽にできそう。Raspberry Piの外部ディスプレイとしては、サイズは小さいですが速度は60fps出せるようなので、わりと使いみちがありそうです。

追記：HiLetgoから出てたモジュールなんですが、現在は見当たらないですね。[このあたり](https://www.amazon.co.jp//dp/B01DXBI3I8/)の物とほぼ同タイプなんですが、コントローラ名とか適当に表記して売られている場合が多いようなので、なんとも。そもそも、最近になってレジスタを読み出して調べてみたら、このモジュールもILI9341ベースではありませんでした。ほぼほぼ互換で動いてますが。

コントローラの確認に良く使われているらしいLCD_ID_readreg.inoというArduino向けのスケッチがあるのですが、それを[Orange Pi向けに移植した物](https://github.com/toyoshim/opi_LCD_ID_readreg)を使ってレジスタをダンプした結果が以下の通りです。

```
Read LCD Registers
reg(0x0000) 00 00	 ID: ILI9320, ILI9325, ILI9335, ...
reg(0x0004) 00 00 00 00	 Manufacturer ID
reg(0x0009) 00 00 30 80 00	 Status Register
reg(0x000a) 00 08	 Get Power Mode
reg(0x000c) 00 06	 Get Pixel Format
reg(0x0061) 00 00	 RDID1 HX8347-G
reg(0x0062) 00 00	 RDID2 HX8347-G
reg(0x0063) 00 00	 RDID3 HX8347-G
reg(0x0064) 00 00	 RDID1 HX8347-A
reg(0x0065) 00 00	 RDID2 HX8347-A
reg(0x0066) 00 00	 RDID3 HX8347-A
reg(0x0067) 00 00	 RDID Himax HX8347-A
reg(0x0070) 00 00	 Panel Himax HX8347-A
reg(0x00a1) 00 00 00 00 00	 RD_DDB SSD1963
reg(0x00b0) 00 00	 RGB Interface Signal Control
reg(0x00b4) 00 00	 Inversion Control
reg(0x00b6) 00 00 00 00 00	 Display Control
reg(0x00b7) 00 00	 Entry Mode Set
reg(0x00bf) 00 00 00 00 00 00	 ILI9481, HX8357-B
reg(0x00c0) 00 00 00 00 00 00 00 00 00	 Panel Control
reg(0x00c8) 00 00 00 00 00 00 00 00 00 00 00 00 00	 GAMMA
reg(0x00cc) 00 00	 Panel Control
reg(0x00d0) 00 00 00	 Power Control
reg(0x00d2) 00 00 00 00 00	 NVM Read
reg(0x00d3) 00 00 00 00	 ILI9341, ILI9488
reg(0x00d4) 00 00 00 00	 Novatek ID
reg(0x00da) 00 00	 RDID1
reg(0x00db) 00 00	 RDID2
reg(0x00dc) 00 00	 RDID3
reg(0x00e0) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00	 GAMMA-P
reg(0x00e1) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00	 GAMMA-N
reg(0x00ef) 00 00 00 00 00 00	 ILI9327
reg(0x00f2) 00 00 00 00 00 00 00 00 00 00 00 00	 Adjust Control 2
reg(0x00f6) 00 00 00 00	 Interface Control
```

各コントローラのIDが入っているべきレジスタは全て空になっていたため、あまり有名な互換チップではないのかも。Status Register、Power Mode、Pixel formatあたりは、ILI9341と同じような値が返ってきてきます。

# Arduinoで使ってみる
## サンプルコード
先ほどのaitendoさんのページにサンプルコードがあるんですが、初期化コードが冗長なのと、GPIO使ってそのままArduinoに移植したら遅すぎたので、最低限の初期化とSPIモジュールを使った形で書きなおしてみました。
コマンドについてはaitendoさんからリンクされてる資料には説明がなかったので、ILI9341関連の資料を漁って調べました。

```cpp
#include <SPI.h>

// 後ろから前からどうぞ、ではなく縦画面と横画面の切り替え
#define PORTRAIT

#define CMD_SLEEP_OUT 0x11
#define CMD_DISPLAY_ON 0x29
#define CMD_COLUMN_ADDRESS_SET 0x2a
#define CMD_PAGE_ADDRESS_SET 0x2b
#define CMD_MEMORY_WRITE 0x2c
#define CMD_MEMORY_ACCESS_CONTROL 0x36
#define CMD_COLMOD 0x3a

#define MAC_PORTRAIT 0xe8
#define MAC_LANDSCAPE 0x48
#define COLMOD_16BIT 0x55
#define COLMOD_18BIT 0x66

#if defined(PORTRAIT)
# define MAC_CONFIG MAC_PORTRAIT
# define WIDTH 320
# define HEIGHT 240
#else
# define MAC_CONFIG MAC_LANDSCAPE
# define WIDTH 240
# define HEIGHT 320
#endif

// この3本はGPIO使ってます
// SPIのMOSIとSCKはArduino Pro Miniだと15、17
enum {
  TFT_CS = 2,
  TFT_RESET = 3,
  TFT_RS = 4
};

void write_command(uint8_t c) {
  digitalWrite(TFT_RS, LOW);
  SPI.transfer(c);
}

void write_data(uint8_t d) {
  digitalWrite(TFT_RS, HIGH);
  SPI.transfer(d);
}

void write_data16(uint16_t d) {
  digitalWrite(TFT_RS, HIGH);
  SPI.transfer16(d);
}

void lcd_init() {
  // リセット後の20ms待ちはデータシートが要求してます
  digitalWrite(TFT_RESET, LOW);
  delay(20);
  digitalWrite(TFT_RESET, HIGH);
  delay(20);

  // 液晶しか繋いでないのでチップセレクトは常時選択
  digitalWrite(TFT_CS, LOW);

  // メモリの読み出し方向の設定で縦画面、横画面が作れる
  write_command(CMD_MEMORY_ACCESS_CONTROL);
  write_data(MAC_CONFIG);

  // デフォルトの18bitモードより16bitモードの方が速くて楽
  write_command(CMD_COLMOD);
  write_data(COLMOD_16BIT);

  // スリープ解除後の60ms待ちはデータシートが要求してます
  write_command(CMD_SLEEP_OUT);
  delay(60);
  write_command(CMD_DISPLAY_ON);
}

void set_update_rect(uint16_t sx, uint16_t ex, uint16_t sy, uint16_t ey) {
  // VRAM内の書き込み矩形領域を設定するとCMD_MEMORY_WRITEに続くデータがその領域内に書かれる
  write_command(CMD_COLUMN_ADDRESS_SET);
  write_data16(sx);
  write_data16(ex);
  write_command(CMD_PAGE_ADDRESS_SET);
  write_data16(sy);
  write_data16(ey);
  write_command(CMD_MEMORY_WRITE);
}

void setup() {
  pinMode(TFT_CS, OUTPUT);
  pinMode(TFT_RESET, OUTPUT);
  pinMode(TFT_RS, OUTPUT);

  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2);
  lcd_init();
}

void loop() {
  static uint16_t c = 0;
  set_update_rect(0, WIDTH, 0, HEIGHT);

  // やってる事は以下と同義
  // for (uint16_t y = 0; y < HEIGHT; ++y)
  //   for (uint16_t x = 0; x < WIDTH; ++x)
  //     write_data16(RRRRRGGGGGGBBBBB);
  for (uint16_t i = 0; i < 9600; ++i) {
      write_data16(c);
      write_data16(c);
      write_data16(c);
      write_data16(c);
      write_data16(c);
      write_data16(c);
      write_data16(c);
      write_data16(c++);
  }
}
```

## 性能
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f31323834332f34643565366232372d303361362d363265342d653561372d3035373336323736653131342e6a706567.jpeg](https://qiita-image-store.s3.amazonaws.com/0/12843/b3f45240-e141-a3e8-668b-07405fc68179.jpeg)
ざっくり見て1.5FPSくらいなので必要な部分だけ更新にしないと実用には厳しそう。縦方向のスクロールがコマンドでサポートされてるようなので、これを使えばコンソールのスクロールなんかはスムーズに実現できるかも。USARTやI2Cで受けてコンソール表示とかできるようにマイコン繋いどくとデバッグ用には良いかも。
液晶モジュール側のSPIは100nsのクロック周期までは耐えるようなので理論値では最大10Mbpsくらいは受けられるはず。320×240×16がざっくり1.2Mbitのデータ量なので、AVRを20MHzで動かしてSPIを2分周できっちり回せば8fpsくらいまでは出るのかな。

# Linux（Raspberry Pi / Orange Pi）から使ってみる
## FBTFTドライバからの利用
```shell-session
$ sudo modprobe fbtft_device custom name=fb_ili9341 rotate=90 bgr=1 \
gpios=reset:0,cs:3,dc:1 speed=80000000 txbuflen=32768 fps=60 busnum=1 \
init=-1,0x3a,0x55,0x1f,-1,0x11,-2,120,-1,0x29,-3
```

これで使えてます。初期化コマンドはマニュアルで入れないと駄目でした。モジュールにサンプルコードが付いてる（かつArduino等で動作してる）場合には、そのコードの初期化シーケンスを変換してあげると確実です。

追記：自分の使っていた液晶モジュールは、長時間電源を入れているとこのシーケンスだけでは動かなるようです。その場合、initの頭に-1,0x7f,0x20を追加すると安定して動きました。ILI9341のデータシートによればPump ratio Controlに対してDDVDH = 2 x VCIを設定しています。

配線に関してはgpios引数で指定している通り、RESETがGPIOのA0、CSがGPIOのA3、DC/RSがGPIOのA1に対応、その他MISO/MOSI/SCKはSPI1に接続しています。busnum=1でSPI1の指定です。

その他の引数は[ドライバのwiki](https://github.com/notro/fbtft/wiki/fbtft_device)にも説明がある通りなのですが、特に気をつけるのは2点、txbuflenの値によってはDMAが有効にならない事、speedを指定するとSPI、指定しないとパラレルインタフェース（i8080バス）が選択される事、でしょうか。他は間違ってたら調べて直す、くらいで大丈夫。

また初期化コマンドをinitで指定しています。デフォルトの初期化シーケンスは色々とやっていてモドキなチップでは動かない事が多いようです。ここでは必要最低限の初期化コマンドを送っていて、手元にある複数のモドキで動いています。まずはこれで試して、気に入らなければガンマ補正の値などを頑張って追加してみると良いかもしれません。

ちなみにinitコマンドの書き方は

* -1の直後はコマンド書き込み
* -2の直後はwait時間（msec）
* -3で終了
* それ以外はデータ書き込み

となっているようです。今回のデータをデコードすると

* コマンド: 0x3a（Pixel Format Set）, データ: 0x55, 0x1f（16 bits / pixel）
* コマンド: 0x11（Sleep Out）
* 120msec待ち
* コマンド: 0x29（Display ON）

となっています。

## 動作確認
私の環境（Orange Pi Zero / Armbian）では/dev/fb0-7が既に存在するため、ドライバを入れると/dev/fb8が作成されました。このfb8を使って簡単な動作確認を行います。

```shell-session
% con2fbmap 1 8
```

これで1番のコンソールがfb8に出力されるようになります。USBキーボードなどを使ってログイン、そのままシェルで作業できます。

![IMG_20171217_044603.jpg](https://qiita-image-store.s3.amazonaws.com/0/12843/103772a2-809e-82ac-77ee-bd2212eaa284.jpeg)


先程マップしたコンソール上からなら、単純に以下のコマンドを打つだけで画像が表示できます。

```shell-session
% fbi <画像ファイル名>
```

![IMG_20171217_044835.jpg](https://qiita-image-store.s3.amazonaws.com/0/12843/34e8919e-014f-c5e9-5367-64a63a9eede5.jpeg)


もし、ssh等で作業してるなら

```shell-session
% fbi -T 1 -d /dev/fb8 <画像ファイル名>
```

などと引数を追加してみて下さい。-Tを使うと別のttyを使ってバックグラウンドで動作するため、消す際にはpsで探してkillしてあげましょう。

他、SDL向けのアプリケーションを

```shell-session
% SDL_FBDEV=/dev/fb8 sdlgnuboy sml.gb
```

みたいな形で実行して上げることもできます。実際ゲームでも動かしてみないと速度はわからないですよね。

## 性能
手元のモジュールはSPIが80MHzで動作可能でした。ILI9341のスペックシート的には10MHzが限度のはずだったので、ほぼ1桁オーバークロックして動作しています。結果、60fpsが普通に出せてしまう……。実は60fpsが欲しくて別途パラレルインタフェースを持つ液晶を手に入れたのですが、そちらのほうが全然遅く、ドライバのCPU負荷もかなり高くなっていました。少し記憶があいまいですが、1GHz超の4コアれぞれに20%くらい負荷がかかってた感じだったかな？SPIでDMAが有効になれば、ドライバのCPU負荷は全く見えなくなります。

# 結論
* こんな楽にカラー液晶に表示できるなんて良い時代。
* 速度が欲しければパラレルよりSPIの方が良かった。
* 今なら3.5インチ以上ならHDMI対応の物もあり、そっちの方が少し高いけどずっと楽。