電子ペーパーを使ってみる(その2)

電子ペーパーを使って最終的には時計を作ってみたいと思っています。
WaveShareの電子ペーパーを購入して、サンプルプログラムを参考にArduinoで動かしてみました。
前回は、スケッチの途中まで説明しました。
alasixosaka.hatenablog.com

今回はその続きです。
前回は、Setupの途中、Init関数を呼び出すところまででしたので、その続きからです。
なお、スケッチの全文は前回の記事を参照してください。

  ClearFrameMemory(0xFF);
  DisplayFrame();

また、関数を呼び出しています。
ClearFrameMemory関数は、

void ClearFrameMemory(unsigned char color) {
  SetMemoryArea(0, 0, EPD_WIDTH - 1, EPD_HEIGHT - 1);
  SetMemoryPointer(0, 0);
  SendCommand(0x24);
    /* send the color data */
  for (int i = 0; i < EPD_WIDTH / 8 * EPD_HEIGHT; i++) {
      SendData(color);
  }
}

となっており、引数を一つ持ちます。メモリーエリアをディスプレイの全体とし、メモリーポインターを原点(x=0, y=0)に設定し、コマンド0x24を送信して、電子ペーパーのメモリーに引数で受け取った数値を書いています。引数は0xFFでしたから、全部を白くするという動作になります。
setupの残りの部分ですが、

  //SetRotate(ROTATE_0);
  int rotate = 0;
  //SetWidth(128);
  int width = 128;
  //SetHeight(24);
  int height = 24;

  //Clear(COLORED, width, height);
  //Clear(UNCOLORED, width, height);
  Serial.println("Clear");
  delay(2000);
  Serial.println("Image");
  SetFrameMemory_Base(IMAGE_DATA, width, 296);
  DisplayFrame();

  time_start_ms = millis();
}

上の方で、rotate=0, width=128, height=24としていますが、実は、width以外は使っていません。この間に、図形を描いたり、線を引いたりという処理が挟まっていたのですが、それらを全部省略してしまったので、意味のない変数が残っています。
2秒待ってから、SetFrameMemory_Base関数を呼び出して、WaveShareのロゴを描いています。

void SetFrameMemory_Base(const unsigned char* image_buffer, int width, int height) {
    Serial.println(width);
    Serial.println(height);
    SetMemoryArea(0, 0, width - 1, height - 1);
    SetMemoryPointer(0, 0);
    SendCommand(0x24);
    /* send the image data */
    for (int i = 0; i < width / 8 * height; i++) {
        SendData(pgm_read_byte(&image_buffer[i]));
    }
    SendCommand(0x26);
    /* send the image data */
    for (int i = 0; i < width / 8 * height; i++) {
        SendData(pgm_read_byte(&image_buffer[i]));
    }
}

SetFrameMemory_Base関数の引数は3つで、初めが描画用の配列、2番目が表示する幅、3番目が高さとなっています。
処理は、メモリーエリアをwidth, heightで設定したエリアとし(この場合は全画面)、メモリーポインターを原点に設定し、コマンド0x24を送ってから、メモリーにイメージデータを書き込んでいます。ここまではこれまでの処理と同じで理解できるのですが、この先に、更にコマンド0x26を送って、また、同じデータを書き込んでいます。実はこの部分の処理が謎で、コマンド0x26はデータシートにも載っていないし、なぜ2度同じデータを書き込むのか理解できていません。
しかし、このあとループ関数に進んで部分リフレッシュ機能を使って、数字をほぼ1秒間隔で書き換えているのですが、この部分の処理、WaveShareのロゴをを描く処理を省略してしまうとうまく部分リフレッシュが機能しません。なので、コマンド0x26と部分リフレッシュには何らかの関係があるのではないかと思っています。
setupの最後は、time_start_ms = millis(); として、内部タイマーの値を変数に書き込んでいます。これもオリジナルのデモプログラムで使っていたものですが、今回のプログラムでは使用していません。

引き続きループ関数の説明に移ります。ループ関数は次のようになっています。

void loop() {
  // put your main code here, to run repeatedly:
  //time_now_s = (millis() - time_start_ms) / 1000;
  
  width = 8;
  height = 12;
  rotate = 1;
  //Clear(UNCOLORED, width, height);
  //char  j[] = {'0','\0'};
  boolean i = false;
  while(1){
    //j[0] = i + '0';
    //DrawStringAt(0, 4, j, &Font12, COLORED);
    //SetFrameMemory_Partial(paint.GetImage(), 60, 72, width, height);
    if (i) {
      SetFrameMemory_Partial(zero, 60, 72, width, height);
    }else {
      SetFrameMemory_Partial(one, 60, 72, width, height);
    }
    DisplayFrame_Partial();
    //Clear(UNCOLORED, width, height);
    i = !i;
    Serial.println(millis());
  }  
}

コメントアウトが多くて恐縮ですが、まず、width=8, height=12, rotate=0と変数に数値を与えています。これは、これから描く数字の幅と高さ、回転を設定しているのですが、回転は結局使っていません。
そして、変数iをFalseにして、while(1)の無限ループに入っています。ループ内では、数字の0と1を交互に書き込んでいます。書き換えるとiを反転させてiの値によってTrueなら0をFalseなら1を書き込んでいます。
処理の中身は、SetFrameMemory_Partial関数を呼び出して、表示させたいイメージをメモリに書き込み、Display_Frame_Partial関数で表示させているだけです。
SetFrameMemory_Partial関数は長いので少しずつ説明します。

void SetFrameMemory_Partial(
    const unsigned char* image_buffer,
    int x,
    int y,
    int image_width,
    int image_height
) {
    int x_end;
    int y_end;

関数名の下5行は、引数を受け取るところで、引数は全部で5つ。まず表示イメージ用の配列、表示位置のx座標、表示位置のy座標、イメージの幅、イメージの高さです。それから、表示位置の最後を示す、x_endとy_endを定義しています。
次に

    if (
        image_buffer == NULL ||
        x < 0 || image_width < 0 ||
        y < 0 || image_height < 0
    ) {
        return;
    }

として、イメージ用の配列の中身が空だったり、幅や高さが負の数値だったときは処理を終了して戻るようになっています。
次は、

    /* x point must be the multiple of 8 or the last 3 bits will be ignored */
    x &= 0xF8;
    image_width &= 0xF8;
    //if (x + image_width >= this->width) {
    //    x_end = this->width - 1;
    //} else {
        x_end = x + image_width - 1;
    //}
    //if (y + image_height >= this->height) {
    //    y_end = this->height - 1;
    //} else {
        y_end = y + image_height - 1;
    //}

ここもコメントアウトだらけですが、表示のx座標と、イメージの幅に関しては、やはりバイト単位でないとだめなので、0xF8とアンドを取って8の倍数にそろえています。
それから、表示のx座標の終わりを計算してx_endに代入しています。
y座標も基本的には同じ処理ですが、こちらは、8の倍数のそろえる必要がないので、単純にy座標の終わりだけをy_endに代入しているだけです。
そして、

    digitalWrite(RST_PIN, LOW);
    delay(2);
    digitalWrite(RST_PIN, HIGH);
    delay(2);

  
  SetLut(_WF_PARTIAL_2IN9);

リセットピンをLowにしてリセットをかけて、部分リフレッシュ用のLUTを書き込んでいます。
次は、

  SendCommand(0x37); 
  SendData(0x00);  
  SendData(0x00);  
  SendData(0x00);  
  SendData(0x00); 
  SendData(0x00);   
  SendData(0x40);  
  SendData(0x00);  
  SendData(0x00);   
  SendData(0x00);  
  SendData(0x00);

となっていて、まず、コマンド0x37を送っています。
このコマンドもデータシートに記載がなく謎です。引き続きデータを10個送っていますが、当然何のことかわかりません。
そして、

  SendCommand(0x3C); //BorderWavefrom
  SendData(0x80); 

コマンド0x3Cを送っています。これはデータシートによると、BorderWavefromとなっています。データは0x80を送っていますが、データシートの記載を読んでもよくわかりませんでした。
次は、

  SendCommand(0x22); 
  SendData(0xC0);   

で、コマンド0x22 を送っています。これはデータシートによると、Display update controlで、データが0xC0になっていて、この意味は、To Enable Clock Signal, then Enable CPと書いてあって、よくわかりませんが、部分リフレッシュのタイミング関係なのかと思います。
そして、

  SendCommand(0x20); 
  WaitUntilIdle();  

コマンド0x20をおくっています。これは、データシートでは、Activate Display Update Sequence となっていますので、部分リフレッシュを始めるという合図のようなものかと思います。
その次の部分は、データを書き込むところで

    SetMemoryArea(x, y, x_end, y_end);
    SetMemoryPointer(x, y);
    SendCommand(0x24);
    /* send the image data */
    for (int j = 0; j < y_end - y + 1; j++) {
        for (int i = 0; i < (x_end - x + 1) / 8; i++) {
            SendData(image_buffer[i + j * (image_width / 8)]);
        }
    }
}

モリーエリアを指定し、メモリーポインターを設定、コマンド0x24を書き込んで、それからイメージデータをメモリに書き込んでいます。
SetFrameMemory_Partial関数は以上になります。
引き続き、Display_Frame_Partial関数を説明します。
こちらは、短くて、

void DisplayFrame_Partial(void) {
    SendCommand(0x22);
    SendData(0x0F);
    SendCommand(0x20);
    WaitUntilIdle();
}

コマンド0x22と引き続きデータ0x0Fを送っています。コマンド0x22は先ほどの、Display update controlで、こっちの方はデータが0xC0ではなく、0x0Fとなっています。意味についてはよくわかりません。そして、コマンド0x20、先ほどのActivate Display Update Sequenceを送っています。
これで、部分的に表示を変更することができます。

全体的にあまり意味の分からないところもあるのですが、何となくこれで動きます。実際に動かしたところの写真が次になります。
数字が横向きに倒れていますが、このディスプレイは、縦長に見るのが本来の向きのようです。数字がちゃんと見える向きにして、左上がディスプレイの原点(x=0, y=0)で右方向がx座標方向、縦方向がy座標の方向になっています。とはいえ、時計の表示では縦長では見づらいので、横向きに使用することになるので、イメージの方を回転させてやる必要があります。

WaveShareのロゴの左の方に小さく数字が表示されている

次回は、その辺ですね。イメージを回転させて、もっと大きな数字を表示させてみます。