先日日本橋の電気街をぶらぶらしていたところ、組み込み用の電子ペーパーを見かけました。
電子ペーパーと言えば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秒かかるのはちょっと残念ですね…。せめて数秒程度にしてほしかった…。