Amazonで売ってる廉価なRaspberryPi用3色電子ペーパーHATをESP32で動かしてみる

#Amazonで売ってる廉価なRaspberryPi用3色電子ペーパーHATをESP32で動かしてみる

![title_image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/17acb98b-2efc-aaa3-683a-6a90112f2b30.jpeg)

##概要

ESP32で遊ぶ用に、Amazonで一番安価だった3色表示(黒白赤)可能な電子ペーパーHATを買ってみたのですが、RaspberryPi用のためESP32(Arduino)用のライブラリがありませんでした。
なので、データシートを読みつつRaspberryPi用サンプルコードをArduino向けに移植してみました。


##目的
製品情報ページにあるRaspberryPi用のサンプルコード(画面全体を赤->黒->白に書換え)を移植し、ESP32での最低限の動作確認を行います。

##使用機器(買ったもの)

[ESP32モジュール]
Aideepen 2 Pcs ESP32 ESP-32S Wireless WiFi + Bluetooth Dual Mode Development Board
https://www.amazon.co.jp/gp/product/B07MH58JS2/

[電子ペーパー]
GeeekPi 2.13" 250x122 SPI Electronic Paper Module E-Paper E-ink Display HAT for Raspberry Pi 4B/3B+/3B/2B/Zero/Zero W/Zero WH 
https://www.amazon.co.jp/gp/product/B08WZ21N7Q/

##調査

###データシートの解読
まずは電子ペーパモジュール単体のデータシートを読んでみます。
https://wiki.52pi.com/index.php/File:DEPG0213RWS800F13_V1.2_FINAL.pdf


####ピンアサイン(抜粋)

| Pin | I/O | 説明 | 【参考】HAT上のPin番号(※) |
|:-:|:-:|:-:|:-:|
| BUSY | O | 処理実行中にHIGHになるピン      | 18  |
| RES# | I | ハードリセット(LOWでリセット)   | 11  |
| D/C# | I | データ(HIGH)/コマンド(LOW)選択 | 22  |
| CS#  | I | SPI_CS  | 24  |
| SCL  | I | SPI_CLK | 23  |
| SDA  | I | SPI_DATA| 19  |

※RaspberryPiのピンアサインに準じています。HATをコネクタ側から見て右下がpin1です。
![HAT_pin_assign](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/93e7c728-7d68-fcca-e4b2-0f1eb510f053.jpeg)


####制御コマンド(抜粋)

| Cmd | Data | 説明 |
|:-:|:-:|:-:|
| 0x10 | 00:ノーマルモード<br>01:Deep Sleep1<br>02:Deep Sleep2  | DeepSleepモード(コントローラの電源を切り、電力消費を抑える)<br>ハードリセットで復帰。 |
| 0x12 | N/A  | 設定値初期化 |
| 0x20  | N/A  | RAMにセットされたデータで画面を更新  |
| 0x24  | [data(4kbytes)]  | 白/黒表示データをRAMに転送(0=黒/1=白)※ |
| 0x26  | [data(4kbytes)]  | 赤表示データをRAMに転送(0=白or黒/1=赤)※  |

※描画用データは以下のとおり解釈されるようです。(赤のbitが1の場合は赤が優先されて表示？)
![association_bit_and_grid](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/59fe7d31-7900-57c8-de2d-40680e36d933.jpeg)


###サンプルコードの解読
続いてHATの製品ページに載っているRaspberry Pi用のサンプルコードを読んでみます。
https://wiki.52pi.com/index.php/2.13_inch_E-paper_(BRW)_SKU:_EP-0133


####デバイス操作部分

```c:HAT製品ページのRasPi用サンプルコード.c(デバイス操作部分抜粋)

//BUSY状態が終わるの待ち
void DEPG0213RWS800F13_Wait(void)
{
      while (1)
      {
            if (digitalRead(BUSY_PIN) == 0)
                  break;
            usleep(5);
      }
      usleep(100);
}

//コマンド送信
void DEPG0213RWS800F13_Command(uint8_t cmd)
{
      digitalWrite(DC_PIN, 0);
      uint8_t buffer[1] = {cmd};
      wiringPiSPIDataRW(fd, buffer, 1);
}

//データ送信
void DEPG0213RWS800F13_Data(uint8_t data)
{
      digitalWrite(DC_PIN, 1);
      uint8_t buffer[1] = {data};
      wiringPiSPIDataRW(fd, buffer, 1);
}

//画面を書き換えてDeepsleep
void DEPG0213RWS800F13_UpdateAndSleep(void)
{
      DEPG0213RWS800F13_Command(0x20);
      DEPG0213RWS800F13_Wait();

      DEPG0213RWS800F13_Command(0x10);
      DEPG0213RWS800F13_Data(0x01);
      usleep(100 * 1000);
}

//初期化
void DEPG0213RWS800F13_Init(void)
{
      usleep(10 * 1000);
      digitalWrite(RESET_PIN, 0);
      usleep(10 * 1000);
      digitalWrite(RESET_PIN, 1);
      usleep(10 * 1000);
      DEPG0213RWS800F13_Wait();
      DEPG0213RWS800F13_Command(0x12);
      DEPG0213RWS800F13_Wait();
}
```

#####わかったこと

* デバイスが操作を受け付けられる状態(!=Busy)かどうかはBUSYピンの状態を見る
* Commandを送信するときはDCピンをLow、Dataを送信するときはDCピンをHighにし、SPIでデータを流す
* リセットはRESETピンを0->1に上げ下げして行う

####表示用RAM操作部分

```c:HAT製品ページのRasPi用サンプルコード.c(RAM操作部抜粋)
//赤表示するRAMを1で埋める(全画面赤)
void DEPG0213RWS800F13_RedOn(void)
{
      uint16_t i;
      DEPG0213RWS800F13_Wait();
      DEPG0213RWS800F13_Command(0x26);
      for (i = 0; i < 4000; i++)
      {
            DEPG0213RWS800F13_Data(0xFF);
      }
}

//赤表示するRAMを0で埋める(全画面白or黒)
void DEPG0213RWS800F13_RedOff(void)
{
      uint16_t i;
      DEPG0213RWS800F13_Wait();
      DEPG0213RWS800F13_Command(0x26);
      for (i = 0; i < 4000; i++)
      {
            DEPG0213RWS800F13_Data(0x00);
      }
}

//白/黒表示するRAMを0で埋める(全画面黒)
void DEPG0213RWS800F13_BlackOn(void)
{
      uint16_t i;

      DEPG0213RWS800F13_Wait();
      DEPG0213RWS800F13_Command(0x24);
      for (i = 0; i < 4000; i++)
      {
            DEPG0213RWS800F13_Data(0x00);
      }
}

//白/黒表示するRAMを1で埋める(全画面白)
void DEPG0213RWS800F13_BlackOff(void)
{
      uint16_t i;

      DEPG0213RWS800F13_Wait();
      DEPG0213RWS800F13_Command(0x24);
      for (i = 0; i < 4000; i++)
      {
            DEPG0213RWS800F13_Data(0xFF);
      }
}
```

#####わかったこと

* 各メソッドで白/黒用と赤用の表示用RAMを0または1でフィルする操作をしている
* 4000回繰り返しているのはRAM領域のバイト数(250*128Pixel/8bytes=4000bytes)分を全て埋めるため<br>(実際の表示領域は250*122Pixelだが、上でも書いた通りRAM上ではキリよく縦128pixels(=16bytes)分確保していて、各行の末尾6bitは領域外データとして何が入っていても画面に反映されない）

####メインルーチン

```c:HAT製品ページのRasPi用サンプルコード.c(メインルーチン)
int main()
{
      //WiringPiセットアップ
      wiringPiSetup();
      pinMode(DC_PIN, OUTPUT);      // DC
      pinMode(RESET_PIN, OUTPUT);   // RST
      pinMode(BUSY_PIN, INPUT);     // BUSY

      fd = wiringPiSPISetup(0, 500000);

      //pngファイル操作(今回は無視)
      read_png_file("new.png");
      process_file();

      /* display testing code */
      //全画面赤
      printf("Preparing to paint all red\n");
      DEPG0213RWS800F13_Init();
      DEPG0213RWS800F13_RedOn();
      DEPG0213RWS800F13_BlackOff();
      DEPG0213RWS800F13_UpdateAndSleep();
       sleep(3);

      //全画面黒
      printf("Preparing to paint all black\n");
      DEPG0213RWS800F13_Init();
      DEPG0213RWS800F13_RedOff();
      DEPG0213RWS800F13_BlackOn();
      DEPG0213RWS800F13_UpdateAndSleep();
      sleep(3);

      //全画面白
      printf("Preparing to paint all white\n");
      DEPG0213RWS800F13_Init();
      DEPG0213RWS800F13_RedOff();
      DEPG0213RWS800F13_BlackOff();
      DEPG0213RWS800F13_UpdateAndSleep();
      sleep(3);
      */

      return 0;
}
```

#####わかったこと

* 「ピン設定ののち、画面を赤->黒->白に3秒間隔で書き換える」という処理を行っている
* pngファイルから表示データを作るメソッドも読んでいるが、今回は動作確認目的なので一旦無視する


このサンプルコードと同じ動作をESP32で実現できるようにやっていきましょう。

##接続

ということで、実際に動かしてみましょう。
![wiring_image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/265f4d9d-6019-c508-88af-8d2dfe7f9a6f.jpeg)
まずはESP32と電子ペーパーHATを以下の通り接続します。

|  機能 | ESP32(シルク) | 電子ペーパーHAT | 線の色(参考) |
|:-:|:-:|:-:|:-:|
| 3.3V | 3.3V(3v3) | 1  | 赤 |
| GND | GND(GND) | 6<br>(他のGNDピンでも可) | 茶 |
| DIN | GPIO23(D23) | 19 | 橙 |
| CLK  | GPIO18(D18) | 23 | 黄 |
| CS | GPIO05(D5) | 24 | 緑 |
| DC | TXD2/GPIO17(TX2) | 22 | 青 |
| RST | RXD2/GPIO16(RX2) | 11 | 紫 |
| BUSY | GPIO04(D4) | 18 | 白 |

##ESP32用のテストコード

調査したRaspberryPi用のサンプルコードと接続したピンアサインを元に、手元のESP32環境向けにコードを移植してみます。

```c#:esp32_epd_test.ino
#include <SPI.h>
#define RST_PIN 16
#define DC_PIN 17
#define CS_PIN 5
#define BUSY_PIN 4

const int PIXEL = 4000;

//Busy状態解除待ち
void waitBusy()
{
  while (1)
  {
    if (digitalRead(BUSY_PIN) == 0)
    {
      Serial.println("BUSY has turned low");
      break;
    }
    usleep(100);
  }
}

//コマンド送信
void sendCommand(unsigned char cmd)
{
  digitalWrite(DC_PIN, 0);
  SPI.transfer(cmd);
}

//データ送信
void sendData(unsigned char data)
{
  digitalWrite(DC_PIN, 1);
  SPI.transfer(data);
}

//赤表示するRAMを1で埋める(全画面赤)
void redOn(void)
{
  Serial.println("redOn()");
  waitBusy();
  sendCommand(0x26);
  for (int i = 0; i < PIXEL; i++)
  {
    sendData(0xFF);
  }
}

//赤表示するRAMを0で埋める(全画面白or黒)
void redOff(void)
{
  Serial.println("redOff()");
  waitBusy();
  sendCommand(0x26);
  for (int i = 0; i < PIXEL; i++)
  {
    sendData(0x00);
  }
}

//白/黒表示するRAMを0で埋める(全画面黒)
void blackOn(void)
{
  Serial.println("blackOn()");
  waitBusy();
  sendCommand(0x24);
  for (int i = 0; i < PIXEL; i++)
  {
    sendData(0x00);
  }
}

//白/黒表示するRAMを1で埋める(全画面白)
void blackOff(void)
{
  Serial.println("blackOff()");
  waitBusy();
  sendCommand(0x24);
  for (int i = 0; i < PIXEL; i++)
  {
    sendData(0xFF);
  }
}

//画面を書き換えてDeepsleep
void updateAndSleep(void)
{
  Serial.println("updateAndSleep()");
  sendCommand(0x20);
  waitBusy();
  sendCommand(0x10);
  sendData(0x01);
  usleep(100 * 1000);
}

//初期化
void initScreen()
{
  Serial.println("initScreen()");
  usleep(10 * 1000);
  digitalWrite(RST_PIN, 0);
  usleep(10 * 1000);
  digitalWrite(RST_PIN, 1);
  usleep(10 * 1000);
  waitBusy();
  sendCommand(0x12);
  waitBusy();
}

void setup()
{
  Serial.println("setup()");
  Serial.begin(115200);
  pinMode(RST_PIN, OUTPUT);
  pinMode(DC_PIN, OUTPUT);
  pinMode(CS_PIN, OUTPUT);
  pinMode(BUSY_PIN, INPUT);

  SPI.begin();
  Serial.println(digitalRead(BUSY_PIN));
}

void loop()
{
  //赤一色
  initScreen();
  redOn();
  blackOff();
  updateAndSleep();
  Serial.println("Screen has been updated to: RED");

  delay(3000);

  //黒一色
  initScreen();
  redOff();
  blackOn();
  updateAndSleep();
  Serial.println("Screen has been updated to: BLACK");

  delay(3000);

  //白一色
  initScreen();
  redOff();
  blackOff();
  updateAndSleep();
  Serial.println("Screen has been updated to: WHITE");

  delay(3000);
}

```

##結果
![movie_running_sample](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/8ea8321c-5054-da89-0d81-dfeefda49302.gif)

無事こんな感じで想定通り動きました。やったー。(手ブレすみません。)
画面書き換えが概ね15secと長かったのは想定外でしたが、ちゃんと動いて満足です。
2021年10月現在、Amazonでクーポン込み2000円切りで買える3色表示の電子ペーパーはこれだけなので、ESP32(Arduino)で電子ペーパーをお安く遊んでみたい方はよかったら参考にしてみてください。

##今後の課題
動作確認として白黒赤の各色を全画面に表示できたので、次は自分で作成した画像を表示できるようにしたいと思います。
ESP32側で画像処理するのは大変そうなので、PC側でRAMに書き込むデータを事前に生成して書き込む方式になるかな…。

##参考にした情報
RaspberryPiのピンアサイン
https://www.qoosky.io/techs/25c2e558f3

ESP32のSPI接続のピンアサイン
https://ht-deko.com/arduino/esp-wroom-32.html#13_01

基盤回路図(DOIT ESP32 Development Board)
https://ht-deko.com/arduino/esp32_Schematic_Prints.pdf

GxEPDライブラリがあるデバイスをフルスクラッチで動かしている先達の方の記事
https://qiita.com/nanbuwks/items/1a4d58fc7f74ab8c8d85


##次の記事
pngファイルからデータを生成して画面表示できるようにしました。

https://qiita.com/ridervance/items/a46a2459796739a344de