ArduinoでaitendoのTFT液晶(M-Z18SPI-2PB)を制御するライブラリを作成するための調査(5)

投稿者: | ↻ : 2019年1月28日

はじめに

やっと液晶モジュールを制御しているコードを解析するところまできました。

Arduino-ST7735-Libraryは色々なArduinoのボートと、複数の制御チップに対応するため、条件コンパイルや過去との互換性のために残された関数などで少々ゴチャゴチャしています。

aitendoの液晶モジュール[Z180SN009]を制御することを目的に、直接関連しないコードは後回しにして解析を進めます。

Adafruit_ST7735クラス

このライブラリは複数のArduinoのボードに対応し、ハードウェアSPIが使えないボードにも対応するために自前のソフトウェアSPIを実装しています。

制御チップとしてStronix社のST7735、ST7735B、ST7735R、ST7735Sに対応していますが、Adafruitの製品向けに作成されたライブラリなので、同じ制御チップを使用している他社の液晶モジュールでは動作に差異が出ることがあります。

コンストラクタ

2つのコンストラクタの呼び分けにより、ハードウェアSPIとソフトウェアSPIの選択を行います。目的がArduino UNO R3でST7735Sを制御なので、ハードウェアSPIのみを調べます。

コンストラクタに渡すパラメータ

Adafruit_ST7735.hからの抜粋

// ハードウェアSPIを使う時のコンストラクタ
Adafruit_ST7735(int8_t CS, int8_t RS, int8_t RST = -1);

Adafruit_ST7735.cppからの抜粋 (ヘッダの定義と実装の引数名が異なるが、実装側のコードを優先します)

// Constructor when using hardware SPI.  Faster, but must use SPI pins
// specific to each board type (e.g. 11,13 for Uno, 51,52 for Mega, etc.)
Adafruit_ST7735::Adafruit_ST7735(int8_t cs, int8_t dc, int8_t rst) 
  : Adafruit_GFX(ST7735_TFTWIDTH_128, ST7735_TFTHEIGHT_160) {
  _cs   = cs;
  _dc   = dc;
  _rst  = rst;
  hwSPI = true;
  _sid  = _sclk = -1;
}
  • dc:チップ選択信号を送信するArduinoのピン番号 (制御チップのD/CXへ接続する)
  • cs:データ/コマンド切替信号を送信するArduinoのピン番号 (制御チップのCSへ接続する)
  • rst:ハードウェアリセット信号を送信するArduinoのピン番号 (制御チップのRESXへ接続する)

D/CX、CS、RESXのピンに関しては、ST7735Sのデータシートの「6.2 Interface Logic Pin」を参照。

コンストラクタでは渡されたパラメータの退避と、ハードウェアSPIか否かのフラグ設定だけです。別途、初期化処理を呼び出して制御チップの初期化を行います。

制御チップの初期化

Adafruitで販売している液晶モジュール(制御チップと、液晶パネルを組み合わせたもの)の種類別に複数の初期化処理が複数あります。液晶パネルの保護シートについているタブの色を表した定数により処理を分岐しています。initBinitRを統合しないのは古いコードの互換性を維持するためと思います。

Adafruit_ST7735.cppからの抜粋

// Initialization for ST7735B screens
void Adafruit_ST7735::initB(void) {
  commonInit(Bcmd);
  setRotation(0);
}

// Initialization for ST7735R screens (green or red tabs)
void Adafruit_ST7735::initR(uint8_t options) {
  commonInit(Rcmd1);
  ...
  setRotation(0);
}

ST7735シリーズに共通の初期化

initBinitRのどちらの初期化処理でも必ずcommonInitsetRotationが呼び出されています。

commonInit()

commonInitはコメントから共通の初期化処理だと分かります。Arduino UNO以外のボードに関する部分を省略したコードが以下になります。処理の単位で4箇所にコメントを追加しています。

// Initialization code common to both 'B' and 'R' type displays
void Adafruit_ST7735::commonInit(const uint8_t *cmdList) {
  ystart = xstart = colstart  = rowstart = 0; // May be overridden in init func

  /*************************************************************
    (1) SPI通信に使用するピン(D/CX、CS)を出力に設定する
  *************************************************************/
  pinMode(_dc, OUTPUT);
  pinMode(_cs, OUTPUT);

#if defined(USE_FAST_IO)
  csport    = portOutputRegister(digitalPinToPort(_cs));
  dcport    = portOutputRegister(digitalPinToPort(_dc));
  cspinmask = digitalPinToBitMask(_cs);
  dcpinmask = digitalPinToBitMask(_dc);
#endif

  /*************************************************************
    (2) ハードウェアSPIのための初期化
        液晶の制御チップ向けの設定を保持するオブジェクトを生成する
  *************************************************************/
  if(hwSPI) { // Using hardware SPI
#if defined (SPI_HAS_TRANSACTION)
    SPI.begin();
    mySPISettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);
#elif defined (__AVR__) || defined(CORE_TEENSY)
    // 省略
#elif defined (__SAM3X8E__)
    // 省略
#endif
  } else {
	// 省略
  }

  /*************************************************************
    (3) 液晶の制御チップをハードウェアリセットする
  *************************************************************/
  // toggle RST low to reset; CS low so it'll listen to us
  CS_LOW();
  if (_rst != -1) {
    pinMode(_rst, OUTPUT);
    digitalWrite(_rst, HIGH);
    delay(500);
    digitalWrite(_rst, LOW);
    delay(500);
    digitalWrite(_rst, HIGH);
    delay(500);
  }
  
  /*************************************************************
    (4) 液晶の制御チップへのコマンドが指定されていたら、コマンドを送信する
  *************************************************************/
  if(cmdList) commandList(cmdList);
}

処理の内容は次の通りです。

  1. SPI通信に使用するピン(D/CX、CS)を出力に設定する
  2. ハードウェアSPIのための設定を保持するオブジェクトを生成する
  3. 制御チップをハードウェアリセットする
  4. 制御チップへのコマンドが指定されていたら、コマンドを送信する

ハードウェアリセット前にCSをHIGHにして制御チップを選択しているが、ST7735Sのデータシートの「9.13 Power ON/OFF Sequence」には、RESXはCSよりも優先されると書かれています。

実機で確認した結果、CSをHIGHにした状態でもハードウェアリセットがかかりました。RESX信号線はデバイス間で共有できないということです。AdafruitのライブラリではCSの切り替えを行なってからRESXの制御をしているので、共有できそうに見えますがダメでした。

setRotation()

関数名から回転であることが予測できます。

ST7735Sはフレームデータの読み書き方向を変更することができます。左右反転、上下反転、XYの入れ替えの3つを組み合わせて、MADCTL(Memory Data Access Control)コマンドのパラメータとして送信します。

詳細はST7735Sのデータシートを参照

  • コマンドの詳細は「10.1.29 MADCTL (36h): Memory Data Access Control」
  • 読み書き方向の組み合わせは「9.11.4 Frame Data Write Direction According to the MADCTL Parameters (MV, MX and MY)」

setRotationの実装を見るとwritecommand関数とwritedata関数が見つかります。この関数がSPI通信で1byteを送信しています。2つの違いはD/CX信号をコマンド(LOW)とするかデータ(HIGH)とするかの違いです。

Adafruit_ST7735.cppからの抜粋

void Adafruit_ST7735::writecommand(uint8_t c) {
#if defined (SPI_HAS_TRANSACTION)
  if (hwSPI)    SPI.beginTransaction(mySPISettings);
#endif
  DC_LOW();		//// コマンドを選択
  CS_LOW();

  spiwrite(c);

  CS_HIGH();
#if defined (SPI_HAS_TRANSACTION)
  if (hwSPI)    SPI.endTransaction();
#endif
}

void Adafruit_ST7735::writedata(uint8_t c) {
#if defined (SPI_HAS_TRANSACTION)
  if (hwSPI)    SPI.beginTransaction(mySPISettings);
#endif
  DC_HIGH();	//// データを選択
  CS_LOW();
    
  spiwrite(c);

  CS_HIGH();
#if defined (SPI_HAS_TRANSACTION)
  if (hwSPI)    SPI.endTransaction();
#endif
}

inline void Adafruit_ST7735::spiwrite(uint8_t c) {

  if (hwSPI) {
#if defined (SPI_HAS_TRANSACTION)
      SPI.transfer(c);	// 実質この1行が、spiwriteと等価
#elif defined (__AVR__) || defined(CORE_TEENSY)
      // 省略
#elif defined (__arm__)
      // 省略
#endif
  } else {
    // 省略 - ソフトウェアSPI
  }
}

以上を踏まえてsetRotationの実装を追って行きます。

Adafruit_ST7735.cppからの抜粋

#define MADCTL_MY  0x80
#define MADCTL_MX  0x40
#define MADCTL_MV  0x20
#define MADCTL_ML  0x10
#define MADCTL_RGB 0x00
#define MADCTL_BGR 0x08
#define MADCTL_MH  0x04

void Adafruit_ST7735::setRotation(uint8_t m) {

  writecommand(ST7735_MADCTL); // MADCTLコマンド送信
  rotation = m % 4; // can't be higher than 3
  switch (rotation) {
   case 0:
     if ((tabcolor == INITR_BLACKTAB) || (tabcolor == INITR_MINI160x80)) {
       writedata(MADCTL_MX | MADCTL_MY | MADCTL_RGB); // パラメータの送信
                                                      // 方向を180°回転、色の並びを赤緑青
     } else {
       writedata(MADCTL_MX | MADCTL_MY | MADCTL_BGR);
     }

     if (tabcolor == INITR_144GREENTAB) {
       _height = ST7735_TFTHEIGHT_128;
       _width  = ST7735_TFTWIDTH_128;
     } else if (tabcolor == INITR_MINI160x80)  {
       _height = ST7735_TFTHEIGHT_160;
       _width = ST7735_TFTWIDTH_80;
     } else {
       _height = ST7735_TFTHEIGHT_160;
       _width  = ST7735_TFTWIDTH_128;
     }
     xstart = colstart;
     ystart = rowstart;
     break;
    case 1:
      ...
MADCTLコマンド

まず、ST7735_MADCTLを送信しています。

Adafruit-ST77350Libraryは接頭辞にST7735_を付けてコマンドを定義しているので、定数名からコマンドを判断できます。

データシートを見るとMADCTLコマンドには1つのパラメータがあります。

表示の向きの設定

MY、MX、MVの3bitを組み合わせて回転して見えるようにしています。いくつかある組み合わせから、90度単位で回転する設定を以下に抜き出しました。上から0°、90°、180°、270°になります。

色の並びの設定

液晶パネルに描画を行うには1Pixel分のbitを送信します。1Pixelのbit数は選択している色数により決まり、起動後(ハードウェアリセット後)は、18bit(262K色、RGBの各色に6bit)になります。設定により送信する色の順序が異なり、aitendoの液晶モジュール[Z180SN009]はRGBになります。

setRotationではデフォルト値の利用はせず再設定しています。制御チップごとにデフォルト値が異なる可能性を考慮していると思われます。

液晶モジュール固有の設定と情報

内部処理で使用する値を決めています。aitendoの液晶モジュール[Z180SN009]に合わせると、それぞれ以下の値になります。

  • フレームメモリーの書き込み開始位置 (開始X、開始Y)
    開始X(colstart)が2、開始Y(rowstart)が1になります。
  • 液晶パネルのサイズ (幅と高さ)
    aitendoの液晶モジュール[Z180SN009]だと、幅(width)x高さ(height)が、128×160になります。

initBから呼び出されるsetRotationではtabcolorが未初期化だけど、クラスのメンバー変数なので基本型のデフォルト初期化が行われた結果0となり、これがINITR_GREENTAB定数と同じになるので動作している。

定数の値が変わったら処理が崩れてしまう。(修正の途中みたいなコードがあるので変わるかもしれません)

コマンドとパラメータの持ち方

初期化のための一連のコマンドとパラメータがAdafruit_ST7753.cppにuint8_t(符合なしbyte)配列として定義されています。先頭の1byteでコマンド数を表し、2byte目以降からコマンドとパラメータのセットがコマンド数分続きます。コマンドとパラメータのセットは可変長です。以下の図のような構造になっています。

この構造はAdafruitのライブラリで採用されているだけで規約や規格ではありません。

コマンドとパラメータの定義(抜粋)

#define DELAY 0x80
static const uint8_t PROGMEM
  Bcmd[] = {                  // Initialization commands for 7735B screens
    18,                       // 18 commands in list:
    ST7735_SWRESET,   DELAY,  //  1: Software reset, no args, w/delay
      50,                     //     50 ms delay
    ST7735_SLPOUT ,   DELAY,  //  2: Out of sleep mode, no args, w/delay
      255,                    //     255 = 500 ms delay
    ST7735_COLMOD , 1+DELAY,  //  3: Set color mode, 1 arg + delay:
      0x05,                   //     16-bit color
      10,                     //     10 ms delay
    ST7735_FRMCTR1, 3+DELAY,  //  4: Frame rate control, 3 args + delay:
      0x00,                   //     fastest refresh
      0x06,                   //     6 lines front porch
      0x03,                   //     3 lines back porch
      10,                     //     10 ms delay
 ...

PROGMEMとは

コマンドとパラメータの定義の配列に、見慣れないPROGMEMというキーワードが付いています。これは定数をSRAMではなくプログラムコード(コンパイル後バイナリ)と同じフラッシュメモリに保持されるようにするキーワードです。

SRAMは非常に少ない貴重なリソースなので、変化しない定数で使わないようにします。コンパイルでSRAMが足りないことが分かるとビルド失敗となります。

Arduino – PROGMEMArduino – http://playground.arduino.cc/Learning/Memoryを参照。

コマンドとパラメータの送信方法

単発のコマンドはwritecommandwritedataで行なっているようですが、一連のまとまったsコマンドはcommandListにより処理されています。

Adafruit_ST7735.cppからの抜粋

// Companion code to the above tables.  Reads and issues
// a series of LCD commands stored in PROGMEM byte array.
void Adafruit_ST7735::commandList(const uint8_t *addr) {

  uint8_t  numCommands, numArgs;
  uint16_t ms;

  numCommands = pgm_read_byte(addr++);   // Number of commands to follow
  while(numCommands--) {                 // For each command...
    writecommand(pgm_read_byte(addr++)); //   Read, issue command
    numArgs  = pgm_read_byte(addr++);    //   Number of args to follow
    ms       = numArgs & DELAY;          //   If hibit set, delay follows args
    numArgs &= ~DELAY;                   //   Mask out delay bit
    while(numArgs--) {                   //   For each argument...
      writedata(pgm_read_byte(addr++));  //     Read, issue argument
    }

    if(ms) {
      ms = pgm_read_byte(addr++); // Read post-command delay time (ms)
      if(ms == 255) ms = 500;     // If 255, delay for 500 ms
      delay(ms);
    }
  }
}

ちょっとポインタ使ってゴニョゴニョしてますが、1byteづつ読みながらコマンド/パラメータのデータ構造に合わせて順に処理しているだけです。コマンド/パラメータのデータ構造の図を意識しながら、処理の概要を追うと以下のようになります。

処理開始時のコマンド配列の読み取り位置は先頭にあり、読み取り位置は1byteずつ進めます

  1. コマンド配列からコマンド数を取得する。(読み取り位置を進める)

  2. コマンド数分、以下の処理を繰り返す。

    1. コマンド配列からコマンドを送信する。(読み取り位置を進める)

    2. コマンド配列からパラメータ数を取得する。(読み取り位置を進める)

    3. パラメータ数には、待ちの有無の情報が含まれているので取り出します。

    4. パラメータ数分、以下の処理を繰り返す。

      1. コマンド配列からパラメータを送信します。(読み取り位置を進める)
    5. 待ち有りならば、コマンド配列から待ち時間を取得する。(読み取り位置を進める)

    6. 取得した待ち時間分(ms)、プログラムを止める。

msがフラグと待ち時間で、使い回されているのが気になりますが無視します。

まとめ

制御チップへの一連のコマンドの送信と、コマンドとパラメータの持ち方まで分かりました。自作ライブラリを作成する際にも、ほぼ同じ実装になりそうです。

次は、初期化のために送信しているコマンドの内容を調べて行きます。