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

久々の電子工作ネタです。
どうも電子工作ネタは中途半端になって、自分でもいかんと思いつつ、気を引き締めて新しいネタに挑戦です。
ちなみに、ラジオの制作は途中で止まっています(理由はそのうち明らかに)。牛乳パック解体機の方は実は機械自体は完成しているのですが、少し残念な出来栄えで記事を書くモチベーションが下がっている状態です。そのうちに時間を見て続きを書こうとは思っているのですが。

さて、今回は電子ペーパーを使ってみようというお話。一応、最終目標は時計を作ってやろうと思っています。
実はこの話には、前日譚があって、元々は液晶、それもセグメント液晶で動く時計を作ろうとしたことがあったのですが、これは9割がた完成したところで、基板の設計ミスがあって発注した基板が使えないということが判明して、そのまま放置された状態になっていました。
それを、装いも新たに電子ペーパーで作ってやろうというのが今回の企画です。

基本方針を下記します。

  • ディスプレイは電子ペーパーを使う
  • 低消費電力にして電池で駆動する
  • 時計を1日1回合わせる機能を持たせる

電子ペーパーは、そもそも低消費電力なので(たぶん)ディスプレイの消費電力は気にしなくてよいかと。電子ペーパーは表示を書き換える時に電力を消費するので、頻繁に書き換えるとそこそこ電力を消費する。このへんは作りながら考えることに。
むしろ消費電力で考えるのはコントロール用のマイコンの方。お手軽に試せるArduinoRaspberry Pi なんかを使ったひにゃあっという間に電池がなくなってしまう。ホビー用で低消費電力といえばやっぱりPICということになるので、PICで動かすことを考える。
そうすると、時計合わせの機能をどうするかということになるが、これはTWE-LITEを組み合わせることを考える。このあたりの設計はいろいろ考えた。ESP‐WROOM2とかWIFI機能を持ったマイコンを使ってネット経由で時刻を取得しようかと考えたり、GPSを使って時刻を取得しようかとも考えた。いずれの方法も時刻を取得するまでの時間が数秒以上かかるのでその間の消費電力を考えるとちょっともったいない。おまけに結構電気を食うし。そこで、GPSモジュールをマイコンにつないでそれにTWE-LITEの親機を接続し、タイムサーバーを常に立てておいて、時計側に子機をつないで、1日1回起動してあとは電気を切っておく。そうすると時刻取得までのタイミングは悪くても3秒、うまくいけば1秒以内で収まるし、TWE-LITE自身の消費電力も、ESP-WROOM2やGPSモジュールよりもはるかに小さいので消費電力的にはかなりお得に設計できる。
というところまでの概略は、液晶時計を製作途中に十分考えてあった。そしてタイムサーバー自体は既に制作済みなので、こいつを利用しようと考えている。

まずはArduinoで動かしてみる

とはいえ、電子ペーパーは扱ったことないので、まずは動かしてみないことには始まらない。
ということで、まずは、気楽に試せるArduinoで動かしてみた。
使ったのは、こちら。
www.waveshare.com

WaveShareという会社の2.9inchの白黒タイプ。解像度は296×128ドット。実は、その前にもっと安い電子ペーパーを買って試してみたのだが、スペックシートが難解でよくわからない。適当にいじっているうちに壊してしまった過去があるので、日本語の記事もあるこちらのタイプを選んだ。
参考にしたのはこちらの記事。
qiita.com
ただ、この記事では、おなじWaveShareの電子ペーパーでも3色タイプのものを使っていて、どうも制御用のチップが違うらしい。スペックシートを見てもコマンド体系が全然異なっている。そこで、WaveShareのサイトに載っているでもプログラムをベースにした。
www.waveshare.com
ただ、サンプルプログラムをインストールして動きましたでは、その先に進めないので、サンプルプログラムが何をしているのかを(ある程度)理解して、自分なりにプログラムするところまでをやってみる。そして、そのプログラムを今度はPICに移植するという手順で行うことにした。

Arduino電子ペーパーの接続

ArduinoArduino Unoを使いました。
接続は下記のとおりです。
電子ペーパー    Arduino
5V         5V
GND        GND
DIN         D11
CLK         D13
CS         D10
DC         D9
RST         D8
BUSY        D7

DINとCLK、CSはSPI通信関係です。DINはSPI通信でMOSIと表示されることが多いです。マスター(この場合はArduino)から出力し、スレーブ(この場合は電子ペーパー)へ入力する端子です。逆は、MISOIと呼ばれますが、電子ペーパーを接続する場合は、スレーブからの信号はないので接続していません。CSはこの端子をLowにすることでスレーブ側に通信開始を知らせるものです。通信中はLowにしておき、通信が終わればHighにします。DCは送られてくる信号がデータなのかコマンドなのかを識別するための端子です。Lowの時がコマンド、Highの時がデータです。RSTはリセットするための端子。Lowにするとリセットされます。BUSYは電子ペーパーが動作中でコマンドを受けられないというときにLowになります。新たにコマンドを発行するときは、BUSYピンを見て、Highになっていることを確認する必要があります。 

サンプルプログラムの概要

サンプルプログラムの動作は電子ペーパーを初期化して、文字、図形などを表示し、そのあとWaveShareのロゴを表示して、タイマー表示を行うプログラムになっていた。最後のタイマー表示のところは時計のプログラムに通じるところがあるので、これは好都合。
しかし、中身を見てみると、ライブラリのオンパレードですぐには理解できない。自分は、どうもライブラリが出てくるとあまりよくわからないので困る。
そこで、まずは、SPIなどの標準的なライブラリは使うとして、でもプログラムの独自ライブラリは使わず、プログラムを動かすところまでを行うことにした。
文字、図形などを表示する部分は今回は必要ないので省いた。初期化して、数字を0と1で変化させるという単純なプログラム。
まずはスケッチの全文。途中、Imageという配列は、全画面にWaveShareのロゴを表示するためのデータでとても長いので途中を省略している。
全文を見たい人はオリジナルサイトを見てください。

#include <SPI.h>

#define COLORED     0
#define UNCOLORED   1

#define RST_PIN         8
#define DC_PIN          9
#define CS_PIN          10
#define BUSY_PIN        7

#define EPD_WIDTH       128
#define EPD_HEIGHT      296

#define ROTATE_0            0
#define ROTATE_90           1
#define ROTATE_180          2
#define ROTATE_270          3

#define IF_INVERT_COLOR     1

//unsigned char* image;
int width;
int height;
int rotate;

unsigned long time_start_ms;

unsigned char image[1024];

unsigned char WS_20_30[159] =
{                      
0x80, 0x66, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x40, 0x0,  0x0,  0x0,
0x10, 0x66, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x20, 0x0,  0x0,  0x0,
0x80, 0x66, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x40, 0x0,  0x0,  0x0,
0x10, 0x66, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x20, 0x0,  0x0,  0x0,
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,
0x14, 0x8,  0x0,  0x0,  0x0,  0x0,  0x1,          
0xA,  0xA,  0x0,  0xA,  0xA,  0x0,  0x1,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x14, 0x8,  0x0,  0x1,  0x0,  0x0,  0x1,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x1,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,          
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x0,  0x0,  0x0,      
0x22, 0x17, 0x41, 0x0,  0x32, 0x36
};  

unsigned char _WF_PARTIAL_2IN9[159] =
{
0x0,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x80,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x40,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0A,0x0,0x0,0x0,0x0,0x0,0x2,  
0x1,0x0,0x0,0x0,0x0,0x0,0x0,
0x1,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x22,0x22,0x22,0x22,0x22,0x22,0x0,0x0,0x0,
0x22,0x17,0x41,0xB0,0x32,0x36,
};

const unsigned char IMAGE_DATA[] PROGMEM = {
/* 0X00,0X01,0X80,0X00,0X28,0X01, */
0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,
~ 途中省略 ~
0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,
};

unsigned char zero[12] ={
// @192 '0' (7 pixels wide)
  //0x00, //        
  0xFF,
  //0x38, //   ###  
  0xC7,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x38, //   ###  
  0xC7,
  //0x00, //        
  0xFF,
  //0x00, //        
  0xFF,
  //0x00, //        
  0xFF,
};

unsigned char one[12] = {
// @204 '1' (7 pixels wide)
  //0x00, //        
  0xFF,
  //0x30, //   ##   
  0xCF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x7C, //  ##### 
  0x83,
  //0x00, //        
  0xFF,
  //0x00, //        
  0xFF,
  //0x00, //
  0xFF,        
};

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  pinMode(RST_PIN, OUTPUT);
  pinMode(DC_PIN, OUTPUT);
  pinMode(BUSY_PIN, INPUT); 
  SPI.begin();
  SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
  Init();

  ClearFrameMemory(0xFF);
  DisplayFrame();

  //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();
}

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());
  }
  

}

void Init(){
  Reset();
  WaitUntilIdle();
  SendCommand(0x12);
  WaitUntilIdle();

  SendCommand(0x01);//Driver output control      
  SendData(0x27);
  SendData(0x01);
  SendData(0x00);
  
  SendCommand(0x11); //data entry mode       
  SendData(0x03);

  SetMemoryArea(0, 0, EPD_WIDTH-1, EPD_HEIGHT-1);

  SendCommand(0x21); //  Display update control
  SendData(0x00);
  SendData(0x80); 

  SetMemoryPointer(0, 0);
  WaitUntilIdle();

  SetLut_by_host(WS_20_30);
  
  
}

void Reset(){
  digitalWrite(RST_PIN, HIGH);
  delay(20);
  digitalWrite(RST_PIN, LOW);
  delay(5);
  digitalWrite(RST_PIN, HIGH);
  delay(20);
}

void WaitUntilIdle(){
  while(1){
    if(digitalRead(BUSY_PIN)==LOW)
      break;
    delay(5);
  }
  delay(5);
}

void SendCommand(unsigned char command){
  digitalWrite(DC_PIN, LOW);
  digitalWrite(CS_PIN, LOW);
  SpiTransfer(command);
  digitalWrite(CS_PIN, HIGH);
}

void SendData(unsigned char data){
  digitalWrite(DC_PIN, HIGH);
  digitalWrite(CS_PIN, LOW);
  SpiTransfer(data);
  digitalWrite(CS_PIN, LOW);
}

void SetMemoryArea(int x_start, int y_start, int x_end, int y_end){
  SendCommand(0x44);
    /* x point must be the multiple of 8 or the last 3 bits will be ignored */
  SendData((x_start >> 3) & 0xFF);
  SendData((x_end >> 3) & 0xFF);
  SendCommand(0x45);
  SendData(y_start & 0xFF);
  SendData((y_start >> 8) & 0xFF);
  SendData(y_end & 0xFF);
  SendData((y_end >> 8) & 0xFF);
}

void SetMemoryPointer(int x, int y){
  SendCommand(0x4E);
    /* x point must be the multiple of 8 or the last 3 bits will be ignored */
  SendData((x >> 3) & 0xFF);
  SendCommand(0x4F);
  SendData(y & 0xFF);
  SendData((y >> 8) & 0xFF);
  WaitUntilIdle();
}

void SetLut_by_host(unsigned char *lut){
  SetLut((unsigned char *)lut);
  SendCommand(0x3f);
  SendData(*(lut+153));
  SendCommand(0x03);  // gate voltage
  SendData(*(lut+154));
  SendCommand(0x04);  // source voltage
  SendData(*(lut+155)); // VSH
  SendData(*(lut+156)); // VSH2
  SendData(*(lut+157)); // VSL
  SendCommand(0x2c);    // VCOM
  SendData(*(lut+158));
}

void SetLut(unsigned char *lut){
  unsigned char count;
  SendCommand(0x32);
  for(count=0; count<153; count++) 
    SendData(lut[count]); 
  WaitUntilIdle();
}

void SpiTransfer(unsigned char data){
  digitalWrite(CS_PIN, LOW);
  SPI.transfer(data);
  digitalWrite(CS_PIN, HIGH);
}

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);
  }
}

void DisplayFrame(){
  SendCommand(0x22);
  SendData(0xc7);
  SendCommand(0x20);
  WaitUntilIdle();
}

void Clear(int colored, int width, int height) {
    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            DrawAbsolutePixel(x, y, colored, width, height);
        }
    }
}

void DrawAbsolutePixel(int x, int y, int colored, int width, int height) {
    //Serial.println(width);
    //Serial.println(height);
    if (x < 0 || x >= width || y < 0 || y >= height) {
        return;
    }
    //Serial.println(IF_INVERT_COLOR);
    if (IF_INVERT_COLOR) {
        if (colored) {
            image[(x + y * width) / 8] |= 0x80 >> (x % 8);
        } else {
            image[(x + y * width) / 8] &= ~(0x80 >> (x % 8));
        }
    } else {
        if (colored) {
            image[(x + y * width) / 8] &= ~(0x80 >> (x % 8));
        } else {
            image[(x + y * width) / 8] |= 0x80 >> (x % 8);
        }
    }
}

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]));
    }
}

void SetFrameMemory_Partial(
    const unsigned char* image_buffer,
    int x,
    int y,
    int image_width,
    int image_height
) {
    int x_end;
    int 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;
    //}

    digitalWrite(RST_PIN, LOW);
    delay(2);
    digitalWrite(RST_PIN, HIGH);
    delay(2);
  
  SetLut(_WF_PARTIAL_2IN9);
  SendCommand(0x37); 
  SendData(0x00);  
  SendData(0x00);  
  SendData(0x00);  
  SendData(0x00); 
  SendData(0x00);   
  SendData(0x40);  
  SendData(0x00);  
  SendData(0x00);   
  SendData(0x00);  
  SendData(0x00);

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

  SendCommand(0x22); 
  SendData(0xC0);   
  SendCommand(0x20); 
  WaitUntilIdle();  
  
    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)]);
        }
    }
}

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

スケッチの解説です。
まずは冒頭の部分。

#include <SPI.h>

使うライブラリはSPIのみです。

#define COLORED     0
#define UNCOLORED   1

電子ペーパーでは何故か、メモリに0を書き込むと黒、1を書き込むと白になります。これは白か黒かの定義になります。実は使ってなかったりします。

#define RST_PIN         8
#define DC_PIN          9
#define CS_PIN          10
#define BUSY_PIN        7

電子ペーパーモジュールと接続している、Arduinoのピン番号の定義です。DINとCLKがありませんが、この2つはSPIの使用を宣言すると自動的にそれぞれ13番ピンと11番ピンに紐づけされます。

#define EPD_WIDTH       128
#define EPD_HEIGHT      296

電子ペーパーの表示サイズ、幅と高さの定義です。

#define ROTATE_0            0
#define ROTATE_90           1
#define ROTATE_180          2
#define ROTATE_270          3

電子ペーパーの回転を定義しています。実はこれも使ってなかったりします。

#define IF_INVERT_COLOR     1

これは結構謎なんですが、これも使ってなかったりします。

//unsigned char* image;
int width;
int height;
int rotate

上で幅、高さ、回転を定義していますが、ライブラリ中で変化させているので、ライブラリを使わないプログラムにする必要上からここで変数定義をしています。

unsigned long time_start_ms;

Arduinoのタイマーを格納する変数。

unsigned char image[1024];

これも、少し上でコメントアウトした、unsigned char* image; というのがありますが、その代わりに、配列を定義しています。

unsigned char WS_20_30[159] =
{  
~省略~
};

長いので省略しています。この配列は、全画面を一度に更新するときのLUTになります。LUTとは何ぞやということですが、私にもよくわかっていません。電子ペーパーを使用するときのおまじないみたいなものと思っています。

unsigned char _WF_PARTIAL_2IN9[159] =
{
~省略~
};

こちらは、画面の一部を更新するときに使うLUTです。何故違うのかもわかっていません。

const unsigned char IMAGE_DATA[] PROGMEM = {
/* 0X00,0X01,0X80,0X00,0X28,0X01, */
0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,
~ 途中省略 ~
0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,
};

これは、WaveShareのロゴを表示するためのデータです。

unsigned char zero[12] ={
// @192 '0' (7 pixels wide)
  //0x00, //        
  0xFF,
  //0x38, //   ###  
  0xC7,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x44, //  #   # 
  0xBB,
  //0x38, //   ###  
  0xC7,
  //0x00, //        
  0xFF,
  //0x00, //        
  0xFF,
  //0x00, //        
  0xFF,
};

数字の0を表示するためのデータです。オリジナルはライブラリの中にありましたが、通常の黒が1、白が0というデータになっています。これをさっき謎と書いた、#define IF_INVERT_COLOR 1 を使って白黒を反転しているのだと思いますが、処理をシンプルにするために、データの方を反転させています。従って元のデータはコメントアウトしています。

unsigned char one[12] = {
// @204 '1' (7 pixels wide)
  //0x00, //        
  0xFF,
  //0x30, //   ##   
  0xCF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x10, //    #   
  0xEF,
  //0x7C, //  ##### 
  0x83,
  //0x00, //        
  0xFF,
  //0x00, //        
  0xFF,
  //0x00, //
  0xFF,        
};

同じく数字の1を表示するためのデータです。

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  pinMode(RST_PIN, OUTPUT);
  pinMode(DC_PIN, OUTPUT);
  pinMode(BUSY_PIN, INPUT); 
  SPI.begin();
  SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
  Init();

  ClearFrameMemory(0xFF);
  DisplayFrame();

  //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();
}

初めの方は説明するまでもないと思いますが、シリアルポートのボーレートを115200に設定し、電子ペーパーと接続したデジタルピンのうちBUSYを入力に設定、それ以外を出力に設定。SPIの利用を宣言し、SPIの設定を行っています。SPISettings(2000000, MSBFIRST, SPI_MODE0) の最初の数字はSPIの速度で、SPIの速度を2MHzにしています。次のMSBFIRSTは、シリアル転送は最上位ビットから行うという意味です。そして、SPI_MODE0は、クロックがアイドルの時Low、アイドルから立ち上がるタイミングで通信を行う設定です。SPI通信については、下記のサイトが詳しいです。
tool-lab.com
Init();は電子ペーパーを初期化する関数です。
まず初めの部分。

void Init(){
  Reset();
  WaitUntilIdle();
  SendCommand(0x12);
  WaitUntilIdle();

関数Reset()を呼び出して、電子ペーパーがアイドルになるまで待機し、コマンド0x12を出力して、再びアイドルになるまで待機します。
関数Resetは、

void Reset(){
  digitalWrite(RST_PIN, HIGH);
  delay(20);
  digitalWrite(RST_PIN, LOW);
  delay(5);
  digitalWrite(RST_PIN, HIGH);
  delay(20);
}

となっていて、RSTピンをHighにして20ms後にLow、更に5ms後にHighに戻しています。RSTピンをLowにすることで電子ペーパーをリセットします。
続いて、WaitUntilIdle()ですが、

void WaitUntilIdle(){
  while(1){
    if(digitalRead(BUSY_PIN)==LOW)
      break;
    delay(5);
  }
  delay(5);
}

電子ペーパーが何らかの動作をしているときはBUSYピンがHighになるので、Lowになるまで待ちます。
SendCommand関数ですが、こちらは、

void SendCommand(unsigned char command){
  digitalWrite(DC_PIN, LOW);
  digitalWrite(CS_PIN, LOW);
  SpiTransfer(command);
  digitalWrite(CS_PIN, HIGH);
}

DCピンをLowにして、これから送るのはコマンドであることを電子ペーパーに知らせ、CSピンをLowにして電子ペーパーにシグナルを送ることを知らせます。
続いてSPIポートに引数の数値を送信します。送信が終わればCSピンをHighに戻します。
ここで送られている0x12はデータシートによるとソフトウェアリセットのようです。
Init関数の次の部分です。

  SendCommand(0x01);//Driver output control      
  SendData(0x27);
  SendData(0x01);
  SendData(0x00);

ここでは、コマンドの0x01を送信し、引き続きデータを3つ。0x27、0x01、0x00を送っています。
Driver output controlとなっていますがこの意味はよくわかりません。
ちなみに、SendData関数の中身は

void SendData(unsigned char data){
  digitalWrite(DC_PIN, HIGH);
  digitalWrite(CS_PIN, LOW);
  SpiTransfer(data);
  digitalWrite(CS_PIN, LOW);
}

となっていて、SendCommandとほとんど一緒ですが、DCピンをHighにするところだけが異なっており、これで、これから送信するのがデータであると電子ペーパーに知らせています。
そして、Init関数のその次の部分

  SendCommand(0x11); //data entry mode       
  SendData(0x03);

データは下位3bitのみ有効で、下位2bitが11となっていますので、データシートによるとXincrement、Yincrementとなっています。これはおそらく、メモリにデータを書き込んだ後、アドレスカウンターを自動的にX座標、Y座標を増やす方向に動くのか、減らす方向に動くのかということだと思います。また3bit目はアドレスカウンターをX方向に動かすのか、Y方向に動かすのかということのようです。ここは1をセットしていますのでX方向に動かすというセッティングを行っています。このセッティングでは、ディスプレイの左上から順番にX方向に書き込んでいくセッティングになっていると思われます。この部分を変更すると上下反転、左右反転、回転などがコントロールできると思いますが、あまりよくわかっていません。
引き続きInit関数の残りを見ていきます。

  SetMemoryArea(0, 0, EPD_WIDTH-1, EPD_HEIGHT-1);

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

void SetMemoryArea(int x_start, int y_start, int x_end, int y_end){
  SendCommand(0x44);
    /* x point must be the multiple of 8 or the last 3 bits will be ignored */
  SendData((x_start >> 3) & 0xFF);
  SendData((x_end >> 3) & 0xFF);
  SendCommand(0x45);
  SendData(y_start & 0xFF);
  SendData((y_start >> 8) & 0xFF);
  SendData(y_end & 0xFF);
  SendData((y_end >> 8) & 0xFF);
}

SetMemoryArea関数は4つの引数を持ち、これから書き込む関数のX、Y座標の開始点、終了点を設定しています。
データシートによると、コマンド0x44に引き続き送られる2つのデータでXの開始点、終了点をそれぞれ設定します。X座標に関しては8bit単位という決まりがあるようで、右に3回シフトして(つまり8で割って)その数値を送っています。
Y座標については、コマンドに引き続き送られるデータが4バイトで、9bitのデータが2つ、それぞれ開始点と終了点となっています。従って、初めのデータは0xFFとアンドを取り、2番目のデータは8回シフト(つまり256で割って)送っています。
それから、Init関数の次の部分です。

  SendCommand(0x21); //  Display update control
  SendData(0x00);
  SendData(0x80); 

コマンド0x21はDisplay update controlでデータシートによると、Bypass optionを設定することになっていますが、この部分は正直何のことかわかりません。しかも、コマンド0x21は引き続き送るデータが1つということになっているのですが、ここでは2つのデータを送っています。続けてデータを送ると、次のコマンド0x22にデータを送ったことになるのでしょうか? データシートの読み方がいまいちよくわかりません。
Init関数の続きです。

  SetMemoryPointer(0, 0);
  WaitUntilIdle();

SetMemoryPointer関数を呼び出しています。

void SetMemoryPointer(int x, int y){
  SendCommand(0x4E);
    /* x point must be the multiple of 8 or the last 3 bits will be ignored */
  SendData((x >> 3) & 0xFF);
  SendCommand(0x4F);
  SendData(y & 0xFF);
  SendData((y >> 8) & 0xFF);
  WaitUntilIdle();
}

引数は2つで、メモリーポインターのX,Y座標を設定するようです。
こちらも先ほどのメモリーエリアと同様にX座標は8bit単位、Y座標は9bitの値で設定しています。
そして、Init関数の最後は

  SetLut_by_host(WS_20_30);
  
  
}

となっていて、SetLut_by_host関数を呼び出しています。

void SetLut_by_host(unsigned char *lut){
  SetLut((unsigned char *)lut);
  SendCommand(0x3f);
  SendData(*(lut+153));
  SendCommand(0x03);  // gate voltage
  SendData(*(lut+154));
  SendCommand(0x04);  // source voltage
  SendData(*(lut+155)); // VSH
  SendData(*(lut+156)); // VSH2
  SendData(*(lut+157)); // VSL
  SendCommand(0x2c);    // VCOM
  SendData(*(lut+158));
}

ここでまた、新たな関数SetLutを呼び出しています。

void SetLut(unsigned char *lut){
  unsigned char count;
  SendCommand(0x32);
  for(count=0; count<153; count++) 
    SendData(lut[count]); 
  WaitUntilIdle();
}

コマンド0x32はLUTをレジスターに書き込むコマンドで、引き続き154個のデータを送信しています。送信しているデータはWS_20_30で、これはおそらく電子ペーパーをフルに書き換える時に使うLUTだと思われます。
そして、SetLut_by_host関数では、引き続き、コマンド0x3Fを送って、配列の155個目のデータを書き込んでいますが、この0x3Fというコマンドがデータシートに載っていなくて何かわかりません。次のコマンドは0x03でこれもデータシートに記載がありません、送っているデータは配列の156個目のデータです。プログラムのコメントによるとゲートボルテージの設定のようです。そして、更にコマンド0x04を送っていますが、こちらもデータシートに記載がありません。データは3バイトで配列の157~159個目のデータを送っています。こちらもプログラムのコメントによるとソースボルテージの設定のようです。そして、最後にコマンド0x2Cを送っています。こちらはデータシートに記載があり、VCOMレジスターの値となっていますが、意味はよく分かりません。
これでようやくイニシャライズが終了です。
記事が長くなったのでこの続きは次回にします。