PIC16F19155を使ってみる(その2)

PIC16F19155を使ってみるの2回目です。
前回は、Lチカをやってみるところまででした。
alasixosaka.hatenablog.com

PICはたかがLチカですが、設定がいろいろあって結構労力を使います。前回は何故かRA5端子ではうまく動かずRA0に繋ぎ変えると無事にLチカが動作しました。
今回はRTCCを使ってみます。さっきも書いたように、PICは設定が面倒なので、最近のMPLAB XIDEでは、この労力を減らしてくれるようになっています。
まず、最もめんどくさいコンフィグレーションビットの設定ですが、production->Set Configration Bitsというコマンドを選んで、右下に出てくる各コンフィグレーションビットの設定を選択し、画面下のGenerate Source Code to Outputというボタンを押してやるとコンフィグレーションビットの設定コマンドを出力してくれるので、そいつをコピペしてやると簡単にできるようになします。

MCCを使ってみる

もっと色々な設定ができるものとして、MCCというのがあります。使い方はこちらに詳しいので、参考にしました。
zattouka.net
MCCを使うと、ピンの設定(デジタルかアナログか、入力か出力か)、各種ペリフェラルの設定、オシレータの設定、タイマーの設定などを簡単にできます。
その分、設定コマンドがライブラリのようになってしまうのでブラックボックス化してプログラムがわかりにくくなるという欠点があります。一長一短ですが、簡単にテストをしてみたいというときは重宝します。
今回は、RTCCを設定して動かしてみました。
PICの16F1シリーズでRTCCを動かしている記事を見つけられなかったので、こちらの記事を参考にしました。
zattouka.net
machoto2.g2.xrea.com

配線です。

RTCCテスト用の配線

前回から、RTCC用にクリスタル(32.768kHz)とコンデンサ(22pF)×2個を追加。そして、RA2端子に抵抗(330Ω)とLEDを繋いでいます。こちらは、RTCCの出力確認用です。
プログラムは基本は前回のLチカと変えていません。RA0に繋いだLEDが1秒間隔で点滅します。もう一方のLEDはRA2に繋いでいますが、こちらは、PPSを使ってRTCCからの出力をRA2から出るようにしました。
RTCCのアラーム設定は10秒間隔でアラームが出るようにしてみました。消費電力の関係から電子ペーパーでは1秒間隔で更新するのが難しそうと思っていますので10秒間隔で動かすことを想定しています。参考サイトによると、アラームを10秒間隔にした場合、RTCC出力は10秒間1、次の10秒間0を繰り返すことになっていますが、やってみると、0.5秒間隔で0と1を繰り返すようになっていました。

データシートは良く読まないとだめ

ザーッとデータシートを読んだ限りでは、PIC16F19155と参考サイトのPIC18F26J50のRTCCの所に大きな差がなかったので、同じことが起こるだろうと思っていたのですが、データシートをよく読むと、PIC16F19155では、SECONDSというのが直接RTCCピンにつながっていました。

PIC16F19155のデータシートRTCCの部分

なので、1秒間隔でLEDが点滅するのは動作として正しいということです。
しかし、アラームが正しく10秒間隔で実行されているかはこのままでは確認できません。
次回は、その辺を割り込みを使って確認してみたいと思います。

PIC16F19155を使ってみる(その1)

今回はPICの記事です。
実は、電子ペーパーを動かす記事の続きなんですが、しばらくはPICを動かしてみることをやっていくのでタイトルを変えています。
前回の記事はこちら。
alasixosaka.hatenablog.com

PICは久しぶりなので、まずは基本のLチカです。
選んだPICはPIC16F19155という品番になります。こちらは、PIC16F1シリーズの一つで、液晶ドライバ搭載のタイプですが、今回は液晶は駆動しません。
選んだ理由はリアルタイムクロックモジュール(RTCC)が搭載されているからです。もっと高級な18Fや24Fには割と搭載されていますが、16F1シリーズだと、液晶ドライバの積んである16F191xxという品番のものしかありません。察するに、液晶ドライバ搭載ですので、時計用のチップなのではと思われます。液晶は使いませんが電子ペーパーを使って時計を作る予定なので、一応用途的にはピッタリというところでしょか?

基本のLチカではまる。

さて、PICですが、Arduinoのようにお手軽には使えません。開発環境はMicrochip社提供のMPLAB XIDEというのを使います。Cで開発するためにはXC8コンパイラというのをさらにインストールする必要があります。ちなみにXC8の8は8ビットのことで、Microchip社の8ビットマイコン用のコンパイラになります。16ビットのマイコンを使う場合にはXC16をインストールする必要があります。32ビット用のXC32というのもあります。まあ、ここまでくるとホビーの領域を超えているような気もしますが。とりあえず、8ビットで十分なのでそいつを使います。
MPLAB XIDEのバージョンは6.0、XC8のバージョンは2.4です。
PICは最初のコンフィグレーションビットの設定をしてやったり、ピンの設定を1からやったりと、初期設定が面倒なのでArduinoに比べるとハードルが高いのですが、前の記事にも書いたように消費電力で選べばPICということになります。今回の16F19155というチップには更に低消費電力の16LF19155というのもあります。F品番は電源電圧が2.8-5.5Vなのに対し、LF品番は電源電圧が1.8-3.6Vと低電圧駆動で電池で駆動する用途に向いていますし、より低消費電力なので本当はそちらを使いたかったのですが、秋月で入手できるのがこちらだったので今回はF品番を使います。
さて、マイコンで手始めにやる定番と言えばLチカです。早速やってみました。
配線はこちら。

Lチカ用の配線

左上のピンヘッダはPICKit接続用です。一番左が1番ピンです。MCLRを1kΩでプルアップ。RA0に330Ωの抵抗とLEDを接続しています。VCCとVSSの間に0.1μFのコンデンサを入れていますが、一応念のためくらいです。
テストなので、給電はPICKitから行っています。
プログラムです。LEDが1秒間隔で一瞬チカっと光って消えるを繰り返す単純なプログラムです。

// CONFIG1
#pragma config FEXTOSC = OFF    // 外部オシレータを無効にする
#pragma config RSTOSC = HFINT1  // COSCを HFINTOSC (1MHz)に設定する。COSCを0x110に設定したの同じ。クロック周波数は1MHzになる
#pragma config CLKOUTEN = OFF   // クロック信号を外部端子に出力しない
#pragma config VBATEN = OFF     // VBATピンを無効にする
#pragma config LCDPEN = OFF     // LCD チャージポンプを無効にする
#pragma config CSWEN = ON       // クロックモード変換を有効にする
#pragma config FCMEN = ON       // フェイルセーフクロックモニターを有効にする

// CONFIG2
#pragma config MCLRE = ON       // MCLR ピンを有効にする
#pragma config PWRTE = OFF      // パワーアップタイマーを無効にする
#pragma config LPBOREN = OFF    // Low-Power BORは無効
#pragma config BOREN = ON       // ブラウンアウトリセットを有効にする
#pragma config BORV = LO        // ブラウンアウトリセットの電圧設定を低にする 1.9V 
#pragma config ZCD = OFF        // ゼロクロスディテクトを無効にする
#pragma config PPS1WAY = ON     // PPSの設定変更を許可しない(一度設定したら変更不可)
#pragma config STVREN = OFF     // スタックオーバーフローリセットは無効

// CONFIG3
#pragma config WDTCPS = WDTCPS_31// ウォッチドックタイマーの周期設定 1:65536; 
#pragma config WDTE = OFF       // ウォッチドックタイマーを無効にする
#pragma config WDTCWS = WDTCWS_7// ウォッチドックタイマーのウィンドウは常にオープン
#pragma config WDTCCS = SC      // ウォッチドックタイマーのインプットはソフトで選択

// CONFIG4
#pragma config BBSIZE = 512     // ブートブロックのサイズは512ワード
#pragma config BBEN = OFF       // ブートブロックは無効
#pragma config SAFEN = OFF      // SAFイネーブルビットは無効
#pragma config WRTAPP = OFF     // アプリケーションのライトプロテクトは無効
#pragma config WRTB = OFF       // ブートブロックのライトプロテクトは無効
#pragma config WRTC = OFF       // コンフィグレーションレジスタのライトプロテクトは無効
#pragma config WRTD = OFF       // EEPROMのライトプロテクトは無効
#pragma config WRTSAF = OFF     // ストレージエリアのライトプロテクトは無効
#pragma config LVP = OFF        // ローボルテージプログラミングは無効

// CONFIG5
#pragma config CP = OFF         // UserNVMプログラムメモリのプロテクトは無効


// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

#include <xc.h>


#define _XTAL_FREQ 1000000

void main(void) {

    ANSELA = 0b00000000;  // ポートAをすべてデジタル
    TRISA  = 0b00001000;  // RA3以外は入力

    RA0 = 0;
    
    while(1){
        RA0 = 0;
        __delay_ms(950);            
        RA0 = 1;
        __delay_ms(50);
    }
    return;
}

このプログラムでバッチリ動くのですが、実は最初は出力ポートをRA0でなく、RA5で試していました。別に大した理由はなく、参考にしたサイトの記事がRA5を使っていたのでそうしただけなのですが、LEDが光らず結構悩みました。とりあえず、テスターで電圧値を計ってみると、ゆっくりと電圧が上昇していき、0Vに戻るを繰り返しています。とても1秒間隔には見えず明らかに変な挙動です。データシートを見ると、RA5はVBATという表示があります。VBATは何かというと、リアルタイムクロックをバックアップするためのバックアップ電源をつなぐ端子です。しかし、VBATはコンフィグレーションビットで無効にしているはずなので通常のデジタル端子として動作するはずなのですが、どうもそうなっていない様子。そこで、プログラムを書き換え、LEDをRA0に繋ぎ変えると普通に動きました。データシートを見ても、RA5端子に関する特別な設定が見当たらず、よくわかりませんでした。なので、RA5は使わずに別の端子を使う方が良いのではという結論になりました。ちょっと引っかかるものがありますが。
次回はリアルタイムクロック(RTCC)を使ってみたいと思います。

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

前回は、結局電子ペーパーを使わず、大きなフォントを作ったところで終わりました。
alasixosaka.hatenablog.com

今回は、そのフォントを表示させてみます。
といってもあまり大きな変更点はありません。
フォントのデータが大きいのでちょっと長いですが、スケッチの全文です。実際の時計を動かす段になったらもっとデータは大きくなってしまうのですが。

#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,
};

unsigned char zero[96] ={
// @192 '0' (7 pixels wide)
 255, 255, 255, 255, 255, 255, 255, 255, 
 255, 255, 255, 255, 255, 255, 255, 255, 
 255, 192, 7, 255, 254, 0, 0, 255, 
 252, 0, 0, 63, 240, 127, 248, 31, 
 241, 255, 255, 31, 227, 255, 255, 143, 
 231, 255, 255, 207, 231, 255, 255, 207, 
 231, 255, 255, 207, 227, 255, 255, 143, 
 241, 255, 255, 31, 240, 63, 252, 31, 
 248, 0, 0, 63, 254, 0, 0, 255, 
 255, 192, 7, 255, 255, 255, 255, 255, 
 255, 255, 255, 255, 255, 255, 255, 255, 
 255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char one[96] = {
// @204 '1' (7 pixels wide)
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 127, 
  243, 255, 254, 127, 243, 255, 255, 63, 
  243, 255, 255, 191, 243, 255, 255, 159, 
  241, 255, 255, 159, 240, 0, 0, 15, 
  240, 0, 0, 7, 240, 0, 0, 7, 
  241, 255, 255, 255, 243, 255, 255, 255, 
  243, 255, 255, 255, 243, 255, 255, 255, 
  243, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char two[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  243, 255, 255, 255, 240, 255, 254, 31, 
  240, 127, 254, 15, 240, 31, 255, 15, 
  241, 143, 255, 207, 241, 199, 255, 231, 
  241, 227, 255, 231, 241, 241, 255, 231, 
  241, 252, 127, 231, 241, 254, 63, 199, 
  241, 255, 15, 135, 240, 255, 128, 15, 
  240, 127, 192, 31, 240, 127, 240, 63, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char tre[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 248, 127, 254, 31, 
  248, 127, 254, 31, 248, 255, 255, 15, 
  241, 255, 63, 207, 243, 255, 63, 231, 
  243, 255, 63, 231, 243, 255, 31, 231, 
  243, 255, 31, 231, 243, 254, 31, 231, 
  249, 254, 79, 199, 248, 248, 103, 135, 
  252, 0, 96, 15, 254, 0, 240, 31, 
  255, 3, 252, 63, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char fou[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 243, 255, 255, 
  255, 240, 255, 255, 255, 240, 127, 255, 
  255, 242, 31, 255, 255, 243, 143, 255, 
  255, 243, 195, 255, 255, 243, 241, 255, 
  255, 243, 248, 127, 247, 243, 254, 63, 
  243, 243, 255, 15, 240, 0, 0, 7, 
  240, 0, 0, 7, 240, 0, 0, 7, 
  243, 243, 255, 255, 247, 243, 255, 255, 
  255, 243, 255, 255, 255, 241, 255, 255, 
  255, 240, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char fiv[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 248, 127, 255, 255, 
  248, 127, 128, 7, 240, 255, 0, 7, 
  241, 255, 191, 199, 243, 255, 159, 199, 
  243, 255, 159, 199, 243, 255, 159, 199, 
  243, 255, 159, 199, 243, 255, 159, 199, 
  249, 255, 31, 199, 248, 124, 63, 199, 
  252, 0, 63, 199, 254, 0, 127, 195, 
  255, 129, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char six[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 128, 63, 255, 254, 0, 7, 255, 
  252, 0, 1, 255, 248, 126, 64, 255, 
  249, 255, 56, 127, 241, 255, 62, 63, 
  243, 255, 159, 31, 243, 255, 159, 159, 
  243, 255, 159, 207, 243, 255, 159, 207, 
  241, 255, 159, 199, 249, 255, 31, 231, 
  248, 126, 63, 231, 252, 0, 63, 231, 
  254, 0, 127, 255, 255, 129, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char sev[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 254, 7, 
  255, 255, 254, 7, 255, 255, 255, 135, 
  243, 255, 255, 199, 240, 127, 255, 199, 
  240, 31, 255, 199, 248, 7, 255, 199, 
  255, 0, 255, 199, 255, 224, 63, 199, 
  255, 248, 7, 199, 255, 255, 1, 199, 
  255, 255, 224, 71, 255, 255, 248, 7, 
  255, 255, 255, 7, 255, 255, 255, 231, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char eig[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 3, 240, 127, 252, 1, 224, 31, 
  252, 0, 192, 31, 248, 124, 143, 143, 
  241, 254, 31, 207, 241, 255, 63, 231, 
  243, 255, 63, 231, 243, 255, 63, 231, 
  243, 255, 63, 231, 243, 255, 63, 231, 
  243, 254, 63, 231, 249, 254, 31, 199, 
  248, 248, 79, 143, 252, 0, 192, 15, 
  254, 1, 224, 31, 255, 3, 240, 63, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

unsigned char nin[96] = {
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 192, 255, 255, 255, 0, 63, 
  255, 254, 0, 31, 255, 252, 63, 15, 
  243, 252, 127, 207, 243, 252, 255, 231, 
  243, 252, 255, 231, 249, 252, 255, 231, 
  248, 252, 255, 231, 252, 126, 255, 231, 
  252, 62, 127, 199, 254, 3, 63, 15, 
  255, 128, 0, 31, 255, 224, 0, 63, 
  255, 252, 1, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255, 
  255, 255, 255, 255, 255, 255, 255, 255,
};

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(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 = 32;
  height = 24;
  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);
    for (int i=0; i<60; i++){
      int sec1 = i / 10;
      int sec2 = i % 10;
      SetFrameMemory_Partial_pre();
      //Serial.println(sec1);
      switch (sec1){
        case 1:
        SetFrameMemory_Partial(one, 16, 200, width, height);
        break;
        case 2:
        SetFrameMemory_Partial(two, 16, 200, width, height);
        break;
        case 3:
        SetFrameMemory_Partial(tre, 16, 200, width, height);
        break;
        case 4:
        SetFrameMemory_Partial(fou, 16, 200, width, height);
        break;
        case 5:
        SetFrameMemory_Partial(fiv, 16, 200, width, height);
        break;
        case 6:
        SetFrameMemory_Partial(six, 16, 200, width, height);
        break;
        case 7:
        SetFrameMemory_Partial(sev, 16, 200, width, height);
        break;
        case 8:
        SetFrameMemory_Partial(eig, 16, 200, width, height);
        break;
        case 9:
        SetFrameMemory_Partial(nin, 16, 200, width, height);
        break;
        default:
        SetFrameMemory_Partial(zero, 16, 200, width, height);
      }
      switch (sec2){
        case 1:
        SetFrameMemory_Partial(one, 16, 224, width, height);
        break;
        case 2:
        SetFrameMemory_Partial(two, 16, 224, width, height);
        break;
        case 3:
        SetFrameMemory_Partial(tre, 16, 224, width, height);
        break;
        case 4:
        SetFrameMemory_Partial(fou, 16, 224, width, height);
        break;
        case 5:
        SetFrameMemory_Partial(fiv, 16, 224, width, height);
        break;
        case 6:
        SetFrameMemory_Partial(six, 16, 224, width, height);
        break;
        case 7:
        SetFrameMemory_Partial(sev, 16, 224, width, height);
        break;
        case 8:
        SetFrameMemory_Partial(eig, 16, 224, width, height);
        break;
        case 9:
        SetFrameMemory_Partial(nin, 16, 224, width, height);
        break;
        default:
        SetFrameMemory_Partial(zero, 16, 224, 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 DisplayFrame(){
  SendCommand(0x22);
  SendData(0xc7);
  SendCommand(0x20);
  WaitUntilIdle();
}

void SetFrameMemory_Base(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]));
        SendData(0xFF);
    }
    SendCommand(0x26);
    /* send the image data */
    for (int i = 0; i < width / 8 * height; i++) {
        //SendData(pgm_read_byte(&image_buffer[i]));
        SendData(0xFF);
    }
}

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

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

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

主な変更点だけ記載します。
まず、メモリを大量に消費していたWaveShareのロゴのデータを削除しました。その代わり、全部白くなるように(要するにクリアと同じ)0xFFを書き込むようにしました。これでクリアができるので、Setupから、ClearFrameMemory関数を呼び出すところをコメントアウト、また、画面クリアのコマンドClear(COLORED, width, height); と Clear(UNCOLORED, width, height); もコメントアウトして動作しないようにしてあります。これらのコマンドで呼び出している関数も不要なので削除しています。また、前回のスケッチで使っていないDrawAbsolutePixelという関数が記載されていました。これも削除しています。
loop関数の中のWhile(1)から始まる無限ループの中身ですが、カウンタ変数をiとして0から59まで変化させ、その数値を電子ペーパーに表示させています。

for (int i=0; i<60; i++){
      int sec1 = i / 10;
      int sec2 = i % 10;
      SetFrameMemory_Partial_pre();
      //Serial.println(sec1);
      switch (sec1){
        case 1:
        SetFrameMemory_Partial(one, 16, 200, width, height);
        break;
       ~省略~
        default:
        SetFrameMemory_Partial(zero, 16, 200, width, height);
      }
      switch (sec2){
        case 1:
        SetFrameMemory_Partial(one, 16, 224, width, height);
        break;
        ~省略~
        default:
        SetFrameMemory_Partial(zero, 16, 224, width, height);
      }
      DisplayFrame_Partial();

変数sec1がiの10の位、sec2が1の位です。
そして、SetFrameMemory_Partial_pre(); という関数を新しく作って呼び出しています。これは、前回のスケッチのSetFrameMemory_Partial関数の前半部分、メモリーに書き込む前までを切り出したものです。数字を2桁分書き込みますので、書き込みが2回に分けて行われるのでメモリに書き込む前までの処理を1回行い、10の位を書き込み、1の位を書き込み、表示させるという処理の流れになっています。書き込むところが、それぞれswitch(sec1)とswitch(sec2)の部分で、数値に応じてそれぞれのデータを書き込んでいます。ポインタを使ったり、配列を使ったりして、SetFrameMemory_Partial関数を呼び出す部分をもう少しスマートにかけると思いますが、とりあえず動けばOKという感じでswitch文を使いました。一応数字はきれいに表示できました。

自分で作った(というかWindowsのフォントからとってきた)大きな数字を表示

しかし、問題はいろいろ山積です。
まず、ウェイトを何も入れていないのですが、書き換えにほぼ1秒かかっています。スペックシートでは部分書き換えの処理時間は0.5秒となっていましたがそんなに短くできそうにありません。処理時間のうち大半は書き換えを待っている時間(つまりマイコンは何もせず待機している)と思われますので、果報は寝て待てということでWaitUntilBusyを実行する前にマイコンはスリープしてやってもいいとは思います。これでマイコンの消費電力は下げることができますが、電子ペーパーがずっと動きっぱなしということになるので、果たしでどのくらい電流を消費するのか? 電子ペーパーの方もうスリープをかけてやらないと電池駆動は難しいのではないかと考えています。
まあ、とりあえず、Arduinoでできそうなことはここまでのような感じですので、次回からPICを使って動かそうと思います。

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

電子―ペーパーシリーズの3回目です。
前回までで、Arduinoを使って、数字を表示するところまでできました。
alasixosaka.hatenablog.com

ただ、これでは数字があまりに小さいのでフォントを大きくしてみたいと思います。

巨大フォントを作る

この手のグラフィックスディスプレイを使うときに一番困るのがフォントをどうするかです。
Arduinoなら、有名なU8glibを使えば、結構色々なフォントが揃っているので、それほど困らないのかもしれませんが、最終的にはPICを使おうと思っているので、フォントを自前で用意してやる必要があります。
実は、ラジオを作っていた時も、有機ELディスプレイを使って、近畿の放送局くらいは漢字で表示しようと考えていたのですが、適当なフォントが見つからず結局そこで挫折して今日に至っているという状況です。
グラフィックスディスプレイで表示しようと思うとビットマップフォントが必要になってくるのですが、なんせPCの世界では(スマホでもそうですが)絶滅危惧種ですから、おいそれと自分に都合のいいフォントが転がっているわけはありません。無料で使おうと虫のいいことも考えていますので特にです。
ならば、自分で作ればということになるのですが、でっかいフォントをかっこよく作るのは結構難しい。今回の電子ペーパーであれば、128×296ドットのディスプレイですから、横長に使うとして、縦方向に90ドットくらいのフォントは欲しいところ。そうなると本当にネットに転がっているフォントなんて皆無に近いです。

U8gのフォントを利用する

ならば、種類もサイズも豊富なU8gのフォントを使ってみようかということになります。
フォントは一応ここに公開されています。
github.com
フォントの見本はこちら
github.com

ところが、ソースを見てもどの部分がビットマップなのかよくわからない。フォーマットを書いたヘルプがあるかと思って探しても見つからず。
ようやく見つけたのがこのサイト。
hackworlds.com
このサイトでは、ビットマップフォントをFonyというWindowsアプリで作成して、BDFというフォーマットで保存し、BDFからU8g用のフォーマットに変換する方法が書いてあります。
BDFフォーマットは、例えばこちら
software.fujitsu.com
を読めばだいたいのことは判ります。
そこで、BDFフォーマットとU8gのフォーマットを見比べて、U8gのファイルを解読してやれば巨大なビットマップフォントが手に入るはず。
そこで、Fonyでとりあえず、ちっちゃい0と1を作ってみました。
BDFファイルはこちら

STARTFONT 2.1
COMMENT Exported by Fony v1.4.7
FONT test
SIZE 12 96 96
FONTBOUNDINGBOX 9 11 0 -3
STARTPROPERTIES 6
COPYRIGHT "Created with Fony 1.4.7"
RESOLUTION_X 96
RESOLUTION_Y 96
FONT_ASCENT 9
FONT_DESCENT 3
DEFAULT_CHAR 0
ENDPROPERTIES
CHARS 2
STARTCHAR 048
ENCODING 48
SWIDTH 576 0
DWIDTH 8 0
BBX 6 8 1 0
BITMAP
FC
84
84
84
84
84
84
FC
ENDCHAR
STARTCHAR 049
ENCODING 49
SWIDTH 576 0
DWIDTH 8 0
BBX 2 8 2 0
BITMAP
40
C0
40
40
40
40
40
40
ENDCHAR
ENDFONT

文字のサイズが書いてあるところは上から5行目、FONTBOUNDINGBOXのところで、9×11となっています。後の2つの意味はよくわかりません。
そして、BITMAPの1つ上の行にもBBXというのがあって、上記の富士通のサイトではこれは、FONTBOUNDINGBOXと同じ数値を入れることになっていますが、このファイルでは異なっています。上のキャラクターは数字の0ですが、BBXは6×8となっています。また後ろに謎の数字が2つありますがとりあえず気にしないことにして、下のキャラクターは数字の1ですが、BBXは2×8となっています。察するに上から下までオールゼロの列は無視して何らかのデータがある列をカウントしているのではないかと思われます。なので、表示の細い1の方が2×8と横が小さい数字になっているのかと。
さてこれをU8g用に変換したファイルがこちらです。

/*
  Fontname: test
  Copyright: Created with Fony 1.4.7
  Capital A Height: 0, '1' Height: 8
  Calculated Max Values w= 6 h= 8 x= 2 y= 0 dx= 8 dy= 0 ascent= 8 len= 8
  Font Bounding box     w= 9 h=11 x= 0 y=-3
  Calculated Min Values           x= 0 y= 0 dx= 0 dy= 0
  Pure Font   ascent = 8 descent= 0
  X Font      ascent = 8 descent= 0
  Max Font    ascent = 8 descent= 0
*/
#include "u8g.h"
const u8g_fntpgm_uint8_t test[45] U8G_SECTION(".progmem.test") = {
  0,9,11,0,253,8,0,0,0,0,48,49,0,8,0,8,
  0,6,8,8,8,1,0,252,132,132,132,132,132,132,252,2,
  8,8,8,2,0,64,192,64,64,64,64,64,64};

include "u8g.h"以降が肝心なところですが、数字の羅列のところを見ると、初めの2つ目と3つ目がそれぞれ9と11になっています。これは、FONTBOUNDINGBOXの数値と同じです。そして、11番目と12番目が48,49となっています。これは0と1のアスキーコードです。そのあと6つ挟んで8,8となっています。これがBBXに相当するものかと思います。オールゼロを無視せずカウントすると8x8になりますから。そして、また3つ挟んで、252,132,.....252と続きます。ここが数字の0のビットマップの部分のように思います。そしてまた、2を挟んで8,8ときて、3つ挟んで64,192,64....64と続くのが数字の1のビットマップのようです。
なんとなくわかってきたんで、Fonyをつかっていろいろなフォントを作っているうちにもっといい方法を発見しました。

Fonyを使えばもっと楽ちん

じつは、FonyというソフトはWindowsにインストールされているフォントを取り込むことができることがわかりました。しかも任意のサイズでです。したがって、U8gの中から適当なサイズのフォントを探して、そしてファイルの内容を解析してといった面倒なことをせずに、FonyでBDFファイルを作ってやれば、そこに書かれているビットマップがそのまま使えるということです。
やり方ですが、Fonyを立ち上げて、File->Import->Installed TrueType Font を選択します。

そこから好きなフォント選んで、スタイルとサイズを選びます。サイズはデフォルトでは72ポイントまでしか選べませんが、数字を手で打ち込めばもっと大きなサイズも入力可能です。
色々なフォントがあって迷うところですが、Cambriaというフォントを選んでみました。取り込むときにFirst charとLast charの欄があってここにアスキーコードを打ち込んでほしいキャラクタを選択します。今回は数字と:だけあればいいので48と58を入力しました。(下のスクショは数字を入力する前なので、0と255になっています)

電子ペーパー用に加工する

今回試しているWaveShareの電子ペーパーでは、前回も書いたように、基本は縦長が正しい向きのようで、縦長にして、左上がディスプレイの原点(x=0、y=0)になっています。縦長に置いたときに横方向がx座標、縦方向がy座標です。
したがって、横長にして通常のビットマップフォントを表示させると横に90°傾いた表示になります。また、通常ビットマップフォントは1が表示で0が非表示ということになっていますが、電子ペーパーのメモリに書き込むデータは1が白、0が黒と通常とは逆になっています。したがって、横に90°回転させて、しかも0と1を反転したデータを用意する必要があります。
これらを全部Fonyでできれば言うことはなかったのですが、残念ながら回転する機能だけがありません。
仕方ないので、回転だけは自分でプログラムを書いて対応することにします。
また、データの幅と高さですが、BDFフォーマットでは、自動的に横幅を8bit(バイト)単位に調整してしまい、余った部分には勝手に0を埋めるというルールがあります。つまり、幅6bitのデータを作っても残りの2bitを0で埋めたデータを作ります。電子ペーパーでは、0は黒く表示されてしまいますので、幅が8の倍数でないフォントを作ってしまうと、縦に黒い筋が入ってしまうことになります。
幅は、Fony上で調整できるので、8の倍数になるように調整してやります。例えば、36ポイントのフォントサイズで読み込んだ場合は、横幅が22bitになるので、右上の黒三角をクリックして24に幅を広げます。するとフォント全体が中心からずれてしまうので今度は青の矢印を使って真ん中に調整します。
フォント全体を反転させるのはEdit->Invertを選択します。
そして、最後に上下を反転させます。Edit->Mirror->Mirror Vert. を選択します。

上下を反転させたのは、あとでプログラム上で、ビットマップを2次元配列に展開して、xとyを入れ替えれば丁度90°回転したフォントが出来上がるからです。
これで準備が整いました。File->Export->BDF fontを選べばBDFフォーマットのファイルが出来上がります。

Pythonで加工する

FonyでBDFファイルを出力したら今度はPythonを使ってフォントを回転させます。実は、フォントのビットマップを回転させるプログラムを書くのは結構厄介なのですが、上にも書いたようにあらかじめ上下反転しておけば、xとyを入れ替えるだけで90°回転してくれます。
まずは、BDFのファイルを開いて、お目当てのフォントのビットマップの部分を見つけます。
始めのうちは延々とビットマップ以外の部分が続きますが、その下にビットマップがずらっと並んでいます。今回作ったフォントで数字の0のところはこのようになっています。

BBX 24 43 0 -8
BITMAP
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FF83FF
FE00FF
FE387F
FC7C7F
F8FE3F
F8FE3F
F9FE3F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F1FF1F
F8FF3F
F8FE3F
F8FE3F
FC7C7F
FC387F
FE00FF
FF83FF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
FFFFFF
ENDCHAR

BBXが24と43ですから24×43のサイズということになります。ここで気を付けないといけないのは、フォントの高さも8の倍数にする必要があるということです。なぜなら、このフォントは90°回転して表示するので、フォントの縦方向が電子ペーパーのx座標になってしまうため、電子ペーパーの制約上x軸方向は8bit単位でしか指定できないため8の倍数にしておく必要があります。この場合、8の倍数で一番近いのは40ですが、見てわかるように上下にFの羅列がたくさんあって、実は高さは32ドットでも数字の部分は十分に収まってしまいます。ですので、縦を32行分にして、適当に切り取って、メモ帳に貼り付け、適当な名前を付けて保存します。それをPythonを使って加工して回転してやります。
Pythonスクリプトです。

import numpy as np
import tkinter as tk

byte = 3
bit = byte * 8

data = np.loadtxt("d:/font/cambria_36_9.txt",  dtype = "unicode")
print(data)

font = []

for i in range(len(data)):
    font.append(data[i][0:2])
    font.append(data[i][2:4])
    font.append(data[i][4:6])

print(font)
n = np.array(font)
n1y = int(len(font)/byte)
n1 = np.zeros((n1y,byte),dtype='int')
n2 = np.zeros((n1y,bit),dtype='int')
n1 = n.reshape(n1y,byte)

for i in range(n1y):
    for j in range(byte):
        for k in range(8):
            bi = int(n1[i,j],16)
            bii = ((bi >> (7-k))&1)
            n2[i,j*8+k] = bii

print(n2)


fon=[]

for j in range(bit):
    for i in range((n1y//8)):
        by = 0
        for k in range(8):
            x = i*8+k
            by = by | (n2[x,j]<<(7-k))
        fon.append(by)

print (fon)


root=tk.Tk()
root.geometry('400x300')
canvas = tk.Canvas(root, width=400, height=300)
canvas.place(x=0, y=0)

for i in range(24):
    for j in range(32):
        if (n2[j,i]==1):
            canvas.create_rectangle(i*8, j*8, i*8+8, j*8+8, outline="black", fill="blue")

root.mainloop()

やっていることは極めてシンプルなので説明しなくてもわかるかと思いますが、初めの変数byteはバイト単位であらわしたフォントの横幅を示しています。今回は24ドットの横幅にしたのでbyteは3です。bitはその8倍でビット幅を示しています。ビットマップデータは、numpyのnp.loadtxtで変数dataに読み込んでいます。凝ったことをせずにただ順番に読み込むだけならこの方法がシンプルでお勧めです。
dataに読み込まれたデータは、1行に3バイト分が書かれているので、スライスを使って、これを1バイトずつに分離します。そして、fontという配列に順番に入れていきます。fontはリスト型の配列ですので、扱いやすいndarray に変換して、nという配列に入れます(あまり意味ないかもしれませんが、メモリーを無駄に使っている)。そして、n1、n2という二次元配列を2つ作ります。一つは、バイト単位のデータを格納するため、もう一つはビット単位のデータを格納するためです。したがってn2の配列には0か1しか入りません。マイコンでこんなプログラムを組んだら、なんてメモリの無駄遣いと怒られそうですが、潤沢にメモリーのあるPCですから、気にせず使っていきます。
その次のループがビット単位にばらすところです。配列n1を順番に読んでいき、ビットシフト機能で1ビットずつ読んでいき、n2に格納しています。
そして、その次のループが今度はxとyを入れ替えて、ビットデータをバイト単位のデータに直しています。配列はまたリスト型ですが、fonという配列を作ってそこに順番に入れていきます。
ここでは、結構原始的なんですが、画面に表示されたfonの中身をコピペしてビットマップデータとしています。また、読み込みも数字1つずつという原始的な方法を使っています。プログラムでBDFファイルを読み込んで内容を解析して自動的にxy座標を入れ替えたビットマップデータのファイルを出力するようにしてもよかったのですが、処理するのが数字だけでそんなに数が多くないので、テストを兼ねて作ったプログラムで済ませてしまっています。もっといろんなフォントを使うようになったらまた考えたいと思います。
最後のTkinterを使った描画のところはおまけみたいなもので、一応ビットマップデータを確認のため表示しています。xyを入れ替える前なので、Fonyの画面で見えていたものと同じく、白黒反転で上下にひっくり返った表示になります。
今回は結局電子ペーパーを使っていませんが、次回、この作ったフォントを電子ペーパーに表示してみます。

ALTRAのMont Blanc BOAを衝動買い

トレランを始めてから、家内からそんなに靴がいるのと言われている還暦間近のランナーですが、ALTRAの公式ツイートでMont BlancのBOAの発売を知りついつい衝動買いしてしまいました。

現在持っているトレランシューズは全部で5足。そのうち一番古いOnのCloud Venture(古い方)はオリエンテーリング用に降ろしてしまっているので実質4足です。内訳は、
On Cloud Venture(新しい方)。Cloud Ventureのモデルチェンジ前にと思って買ったのですが。よく見比べてみるとモデルチェンジ後のものでした。幸い足にはフィットするので問題なかったですが、他の靴の出番が多くなってきたので現在はあまり使っていません。おもに近場のトレーニング用。
Newton BOCO AT4 ニュートンのシューズはロードロップでしかも軽量ということで基本的に好きなんですが、このシューズはトレラン用になっていますが結構滑るので、お天気のいい日に近場のトレーニング用になってしまっています。もうちょっとグリップが良ければもっと使えるのに。
Hoka SPEED GOAT Hokaのシューズは履き心地が抜群で好きなんですが、どうも足に合わないようで、こいつで大峯奥駈道に3度行きましたが、その度に足の爪が内出血起こしてしまうということになって、最近ではほとんど履いていません。シューズが悪いと一概には言えないのですが。
ALTRA LONE PEAK アルトラのトレランシューズの中でもベストセラーと言われるシューズで、山に行っても履いている人が多いです。私もこいつが最近のお気に入りで、20km以上のロングトレイルをやるときはこれにすることが多いです。ALTRAのコンセプトであるゼロドロップと足先にゆとりを持たせた設計がお気に入りです。少し気になっているのは、ややクッションがソフトすぎると感じるところと、少し滑りやすいところ。

で、Mont Blancなんですが、ちょっと気になっていて、確か今年の春位にノーマルのモデルを見に行ったのですが、残念ながらその時はお店の方で在庫がなく試し履きをすることができませんでした。そして、今回BOAモデルが出たということで、ついつい衝動買いしてしまいました。BOAのシューズが欲しかったんです。BOAとは、ダイヤルを回すだけで靴ひもを締めるのと同じことができるとても便利なシステムで、自転車用のシューズではスペシャライズドのシューズを1足持っています。履くときにいちいち靴ひもを締める必要がなく(自転車用はベロクロになっているのも多いですが)、脱ぐときもダイヤルを引っ張るだけで緩んでくれるので、これを履きだすとやめられなくなります。トレラン用のシューズでも同じく、履いたり、脱いだりが楽になるのはもちろんのこと、走っているうちに靴ひもが緩んできて締めなおすのにも、ダイヤルを回すだけですぐにできてしまいます。特に、下り坂など少し締めておきたいときにすぐにできるのはとってもありがたい機能です。特に、私は足のつま先がシューズに当たるとすぐに爪から内出血して黒くなってしまうので前からBOAのトレランシューズが欲しかったんですがお気に入りのアルトラから発売されたということでついつい衝動的にポチってしまいました。
買ったお店はいつもアルトラのシューズを購入している、Stride Lab 那須店。5500円以上購入で送料無料。返品、交換にも応じてくれて、ホームページには店員さんのレビューとかがいっぱい載っています。購入するといつも手書きでメッセージを入れてくれます。また、高額商品だけだと思いますが、おまけをつけてくれます。今回はライスピュレがおまけについていました。こういったさりげないサービスが結構好きでまたここで買おうと思ってしまいます。
さて、実物の写真はこちら。

AltraのMontBlanc BOAのダイヤルが特徴。

BOAのダイヤルが2か所あって、前と後ろでそれぞれに調整できるようになっています。
早速履いて走ってみました。といっても、私は9月10月はスズメバチが怖いので極力山に入らないようにしているので、いつものトレーニングコースで少し未舗装路があるコースで、基本的に舗装路でアップダウンのあるコースを走ってみました。
まず最初に感じたのは、クッション性はローンピークほどではなく、自分的にはちょうどいいくらいかと感じました。また、グリップはとても強く、ローンピークよりもグリップがよいように感じました。ぬれた路面(アスファルトですが)でも程よくグリップしてくれるので、かなり良い印象を受けました。ただ、下りではグリップが良すぎて、ブレーキをかけた走りをしようとすると足に負担がかかるような感じがしました。なので、自然と足を前に出す感じが良いように思いました。下りに入る前にダイヤルを回して靴の後ろの部分だけ少し締めたのですが簡単にできるし、締めたおかげで靴の中で足が動かずいい感じでした。
未舗装区間もほんの少し走って小石交じりの路面でしたが、ここでもグリップの良さを感じました。
Altraのホームページによると、モンブランは足形がスタンダードということで、ローンピークなどのオリジナルよりも少し細身になっています。確かにローンピークほどゆったりという感じはしませんでしたが、自分的にはこれでも他のシューズよりは十分ゆとりがあると感じました。
ということで、まだほんの少ししかも大半が舗装路という状況でしたが、これは使えるというのが印象でした。まあ、お値段が¥27500とローンピークの倍とまではいかなくてもかなりの額ですから、それに見合っているのかという点は、その人それぞれで考え方がると思いますが、自分的にはありと感じています。

電子ペーパーを使ってみる(その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のロゴの左の方に小さく数字が表示されている

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

電子ペーパーを使ってみる(その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レジスターの値となっていますが、意味はよく分かりません。
これでようやくイニシャライズが終了です。
記事が長くなったのでこの続きは次回にします。