2022年3月14日月曜日

e-Paper(電子ペーパー)を制御する

先日日本橋の電気街をぶらぶらしていたところ、組み込み用の電子ペーパーを見かけました。

電子ペーパーと言えばKindleなどに採用されているディスプレイで、電源を切っても表示内容を維持することができるタイプの画面です。そのため、原理的に画面更新時にしか電力を消費せず、電子書籍のように1分に1回とかの更新で良いような用途では有用なディスプレイですね。

しかし、そのお店で売っていた電子ペーパーは1万円台前半と非常に高く、「ちょっと買ってみるか」くらいでは手が出るものではありませんでした。ですが、ちょっと調べてみると、秋月電子で2,500円の電子ペーパーが売っているじゃありませんか。普通の液晶に比べればちょっと高めですが、まあ、買ってやれないものでもないでしょう。

ということで、実際に買って制御してみました。まずは完成形から見ていきましょう。

見ての通り、画面の更新に約30秒かかります。これは仕様のようです。

SPIでコントローラーのSRAMのバッファーを更新するのはほとんど一瞬で終わりますが、その後の画面リフレッシュに30秒くらいかかるようです。 時計などの用途を考えるとあんまりイケてなさそうですね。

回路設計

回路自体はそんなに難しくないのですが、ドキュメントがイケてないのでめちゃめちゃわかりにくいです。

File:2.13inch e-Paper HAT Schematic.pdf

こちらが回路図になります。ただとても読みにくい。何がどこにつながっているのかかなり難解ですし、挙句の果てには「P1」「J2」「U1」などの部品番号も基板上にプリントされていないという有様です。

解説すると、液晶コントローラー自体は3.3Vで動作するようで、 5V系のマイコンでも操作できるようにレギュレーターとレベルコンバーターが付いているようです。そのレベルコンバーターがU1、レギュレーターがU2です。P3(基板に横向きに出ている白色コネクタ)からコントロールする場合は5Vで制御できます。

一方で、P1はピンソケットで、Raspberry Pi Zeroに直接接続することを意図しているようです(というか液晶サイズもRaspberry Pi Zeroピッタリです)。ですので、ピン配列はRaspberry Pi Zeroのものを参照するとわかりやすいでしょう。ここのコネクタから3.3V系の回路に直結できますので、3.3Vで動作するマイコンに接続する場合はP3を使用する必要はなく、P1から直接接続することができます。

今回はPIC16F18326を使うことにしました。理由は、170円と安価な割にプログラムメモリが16kワードと大きいからです。今回の液晶は104×212ドットの2色表示のため、画像1枚で5.5kBほど使います。

回路図はこんな感じになります。一応e-paper側でDeep Sleepに入ると消費電力が数μAまで下がるようですが、完全にシャットアウトできるようにPICでe-paperの電源を掌握できるようにしています。

ソフト作成

ドライバソフトはメーカーのGithubに載っています。

e-Paper

ただし、PIC用は無いため、Arduino用のを移植する必要があります。今回使うのは3色タイプのLCDなので、e-Paper/Arduino/epd2in13bc/ 以下のファイルを移植していきます。

#include "epd2in13bc.h"


int EpdIf_Init(void) {
    /*
    SPI.begin();
    SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
    */
    
    return 0;
}

void EpdIf_SpiTransfer(unsigned char data) {
    DO_SCL = 0;
    DO_CS = 0;

    for(int i = 0; i < 8; i ++) {
        DO_SDA = (data & 0x80) ? 1 : 0;
        data <<= 1;
        DO_SCL = 1;
        NOP();
        NOP();
        DO_SCL = 0;
    }

    DO_CS = 1;
}



int Epd_Init(void) {
    /* this calls the peripheral hardware interface, see epdif */
    if(EpdIf_Init() != 0) {
        return -1;
    }
    /* EPD hardware init start */
    Epd_Reset();
    Epd_SendCommand(BOOSTER_SOFT_START);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendCommand(POWER_ON);
    Epd_WaitUntilIdle();
    Epd_SendCommand(PANEL_SETTING);
    Epd_SendData(0x8F);
    Epd_SendCommand(VCOM_AND_DATA_INTERVAL_SETTING);
    Epd_SendData(0x37);
    Epd_SendCommand(RESOLUTION_SETTING);
    Epd_SendData(EPD_WIDTH);     // width: 104
    Epd_SendData(0x00);
    Epd_SendData(EPD_HEIGHT);     // height: 212
    /* EPD hardware init end */
    return 0;

}

/**
 *  @brief: basic function for sending commands
 */
void Epd_SendCommand(unsigned char command) {
    DO_DC = 0;
    EpdIf_SpiTransfer(command);
}

/**
 *  @brief: basic function for sending data
 */
void Epd_SendData(unsigned char data) {
    DO_DC = 1;
    EpdIf_SpiTransfer(data);
}

/**
 *  @brief: Wait until the busy_pin goes HIGH
 */
void Epd_WaitUntilIdle(void) {
    while(DI_BUSY == 0) {      //0: busy, 1: idle
        __delay_ms(100);
    }      
}

/**
 *  @brief: module reset. 
 *          often used to awaken the module in deep sleep, 
 *          see Epd::Sleep();
 */
void Epd_Reset(void) {
    DO_RST = 0;
    __delay_ms(200);
    DO_RST = 1;
    __delay_ms(200);   
}

/**
 *  @brief: transmit partial data to the SRAM
 */
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the black part of SRAM
 */
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the red part of SRAM
 */
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 * @brief: refresh and displays the frame
 */
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red) {
    if (frame_buffer_black != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_1);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_black[i]);
            //Epd_SendData(~(i & 0xFF));
        }
        __delay_ms(2);
    }
    if (frame_buffer_red != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_2);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_red[i]);
            //Epd_SendData(0xFF);
        }
        __delay_ms(2);
    }
    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: clear the frame data
 */
void Epd_ClearFrame(void) {
    Epd_SendCommand(DATA_START_TRANSMISSION_1);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);

    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: This displays the frame data from SRAM
 */
void Epd_Refresh(void) {
    Epd_SendCommand(DISPLAY_REFRESH); 
    Epd_WaitUntilIdle();
}


/**
 * @brief: After this command is transmitted, the chip would enter the deep-sleep mode to save power. 
 *         The deep sleep mode would return to standby by hardware reset. The only one parameter is a 
 *         check code, the command would be executed if check code = 0xA5. 
 *         You can use Epd::Reset() to awaken and use Epd::Init() to initialize.
 */
void Epd_Sleep() {
    Epd_SendCommand(POWER_OFF);
    Epd_WaitUntilIdle();
    Epd_SendCommand(DEEP_SLEEP);
    Epd_SendData(0xA5);     // check code
}
#ifndef EPD2IN13B_H
#define EPD2IN13B_H

#include <xc.h>
#define _XTAL_FREQ 32000000    // Fosc = 8MHz


#define DO_SDA          LATCbits.LATC2
#define DO_SCL          LATCbits.LATC0
#define DO_CS           LATCbits.LATC4
#define DO_DC           LATCbits.LATC5
#define DO_RST          LATCbits.LATC3
#define DI_BUSY         PORTAbits.RA4


// Display resolution
#define EPD_WIDTH       104
#define EPD_HEIGHT      212

// EPD2IN13B commands
#define PANEL_SETTING                               0x00
#define POWER_SETTING                               0x01
#define POWER_OFF                                   0x02
#define POWER_OFF_SEQUENCE_SETTING                  0x03
#define POWER_ON                                    0x04
#define POWER_ON_MEASURE                            0x05
#define BOOSTER_SOFT_START                          0x06
#define DEEP_SLEEP                                  0x07
#define DATA_START_TRANSMISSION_1                   0x10
#define DATA_STOP                                   0x11
#define DISPLAY_REFRESH                             0x12
#define DATA_START_TRANSMISSION_2                   0x13
#define VCOM_LUT                                    0x20
#define W2W_LUT                                     0x21
#define B2W_LUT                                     0x22
#define W2B_LUT                                     0x23
#define B2B_LUT                                     0x24
#define PLL_CONTROL                                 0x30
#define TEMPERATURE_SENSOR_CALIBRATION              0x40
#define TEMPERATURE_SENSOR_SELECTION                0x41
#define TEMPERATURE_SENSOR_WRITE                    0x42
#define TEMPERATURE_SENSOR_READ                     0x43
#define VCOM_AND_DATA_INTERVAL_SETTING              0x50
#define LOW_POWER_DETECTION                         0x51
#define TCON_SETTING                                0x60
#define RESOLUTION_SETTING                          0x61
#define GET_STATUS                                  0x71
#define AUTO_MEASURE_VCOM                           0x80
#define READ_VCOM_VALUE                             0x81
#define VCM_DC_SETTING                              0x82
#define PARTIAL_WINDOW                              0x90
#define PARTIAL_IN                                  0x91
#define PARTIAL_OUT                                 0x92
#define PROGRAM_MODE                                0xA0
#define ACTIVE_PROGRAM                              0xA1
#define READ_OTP_DATA                               0xA2
#define POWER_SAVING                                0xE3

int  Epd_Init(void);
void Epd_SendCommand(unsigned char command);
void Epd_SendData(unsigned char data);
void Epd_WaitUntilIdle(void);
void Epd_Reset(void);
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l);
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red);
void Epd_Refresh(void);
void Epd_ClearFrame(void);
void Epd_Sleep(void);

#endif

オリジナルのライブラリでは、ハードウェアのインターフェース部と論理部できれいに分けて実装されていたのですが、移植するにあたってごちゃ混ぜにしてしまいました。XC8がC++をサポートしていないうえ、PICのピン操作は直接レジスタ触ることになるというのが主な理由です。もう少しやりようはあったかもしれませんが、まあ処理速度優先ということで。

あと、一応回路上ではPICのMSSP(ハードウェアSPI)に対応したピンにアサインしているのですが、SPI送信中に何か別のことをしたいわけでもないので、ソフトウェアで簡単にSPIを実装しています。SPI程度ならMSSPの使い方をデータシート見ながら実装するよりGPIOで作っちゃったほうが簡単ですね。

void UpdateLcd()
{
    DO_LCD_PWR = 1;
    TRIS_LCD_PWR = 0;
    
    __delay_ms(100);
    
    Epd_Init();

    switch(content++) {
        case 0:
            Epd_DisplayFrame(img1_black, img1_yellow);
            break;
        case 1:
            Epd_DisplayFrame(img2_black, img2_yellow);
            break;
        default:
            Epd_ClearFrame();
            content = 0;
            break;
    }

    Epd_Sleep();    
    TRIS_LCD_PWR = 1;
    LATA = 0;
    LATC = 0;
}

void init()
{
    LATA = 0;
    WPUA = 0x20;
    ODCONA = 0;
    ANSELA = 0;
    TRISA = 0xFF;

    LATC = 0;
    WPUC = 0;
    ODCONC = 0;
    ANSELC = 0;
    TRISC = 0xC2;

    IOCAN = 0x20;
    PIE0 = 0x10;
    INTCON = 0x80;
}

void loop()
{
    SLEEP();
    NOP();
    
    if(DI_SW == 0)
        UpdateLcd();    
}

void __interrupt() ISR(void)
{
    if(PIR0bits.IOCIF) {
        if(IOCAFbits.IOCAF5) {
            IOCAFbits.IOCAF5 = 0;
        }
    }
}

void main(void) {
    init();
    
    while(1)
        loop();
}

main.cでは処理をしていない間はPICをスリープにするようにしています。スイッチを押されるとInterrupt On Change (IOC)にてスリープからウェイクアップすると続きの処理を実行するようになります。この際発生する割り込みの割り込みフラグはちゃんとリセットしないとスリープに入れない(常にスリープ復帰条件ができてしまう)ので、割り込みルーチンではフラグのリセットのみを行っています。

また、e-Paperをdeep sleepにした後はLCDの電源を切っていますが、その際に制御用ピンがHighになっていると、そちらから電源側へ電流が流れてしまう(おそらく液晶コントローラー内に保護用にダイオードが入っている)ので、同時にすべてのピンの出力をゼロにしています。

これで、待機中はナノアンペアオーダーの電流しか流れない、ボタン電池で駆動しても何の支障もないe-Paperができました。

 

それにしても、更新に30秒かかるのはちょっと残念ですね…。せめて数秒程度にしてほしかった…。

0 件のコメント:

コメントを投稿