エクセル で 16 x 16 ドットフォントを自作して、フルカラー 有機EL ( OLED ) 時計を作ってみた ( ESP32 使用 )

ESP32 ( ESP-WROOM-32 )

こんばんは。

前回の記事では、SPI通信の小型 フルカラー OLED ( 有機EL ) モジュール SSD1331 を ESP32 – DevKitC で動かす方法を紹介しました。
今回はそれを応用して、 micro SDHC カード内のフォントデータを読み込んで、OLED ( 有機EL ) に文字を表示してみたいと思います。
しかも、フォントは Microsoft Excel ( エクセル )を使って自作し、NTPサーバーで時刻を定時補正する時計を作ってみます。

Excel ( エクセル )は Windows パソコンをお持ちならば、殆どの方がインストールされていると思われる表計算ソフトです。
アプリを開発しなくても Excel ( エクセル )があれば、フォント図形を CSV 形式で出力できます。
CSV のテキストデータをバイナリ形式データに出力するプログラムを Arduino core for the ESP32 で作れば、micro SDHC カードにバイナリ形式フォントファイルを出力できます。

Excel ( エクセル )の VBA やマクロを使えば、バイナリ直接出力できますが、新たな言語習得が必要なので、今回は それを使いませんでした。

YouTube 動画で、単なる時計表示ではなく、縦方向のカウンタースクロールを作っているものを見かけて、私も作ってみたくなりました。
ESP-WROOM-32 が販売されたことによって、SRAM メモリが大きくなり、CPU 速度も大きくなったので、そういう時計を作っても、他の機能にそれほど影響ないだろうと思いました。

では、早速、以下の動画をご覧ください。

いかがでしょうか。
中高年の私でも、なかなかのカワイイ ( Kawaii )系の文字ができました。
0:02 と 0:39 と 0:59 あたりを見てもらえると、時間と分表示も切り替わってオモシロイです。

縦方向の文字カウントスクロールについては、YouTube 動画ではよく見るのですが、横方向のスライドはあまり見かけません。
文字毎にスクロールする方向を変えるとオモシロイ効果が出て、さらにカワイさが増しますね。
秒表示と分表示でスクロールする速度を変えると、さらにイイ感じです。
自作したフォントがこういう風にスクロールすると、ずっと見ていられるオモシロさがあって、我ながらGood!です。

フルカラー OLED ( 有機EL ) モジュールの SSD1331 には、前回の記事で紹介しましたが、グラフィックアクセラレーションコマンド ( Graphic Acceleration Commands )という他の OLED ( 有機EL )モジュールには無い、と~っても便利なものがあります。
これをうまく使うと、文字のスクロールプログラムがとても組み易くなるんです。
後で述べますが、これ、正直、スンゴイ便利ですよ!!
これは、SSD1306 や SSD1351 には無い機能です。
プログラムをそのまま移植できないのでご了承ください。

では、これの作り方を説明します。

因みに、ここで紹介する製品やプログラム、およびファイル等による、いかなるトラブルも当方では責任を負いかねます。
予めご了承ください。

スポンサーリンク

準備するもの

フルカラー OLED SSD1331 モジュール ( SPI インターフェース用 )

似たような製品で、SSD1306 がありますが、間違えないようにしてください。
これは SPI 通信用のものです。
Amazon.co.jp でも販売店は中国なので、到着までには1週間以上かかります。

なかなか日本の販売店では、OLED ( 有機EL ) のパラレルや SPI 通信のものが売っていないのが残念ですね。
OLED ( 有機EL )の良いところは、斜めから見ても見やすく、液晶よりクッキリで薄型です。
液晶よりもちょっと高いのが難点ですね。

ESP32 – DevKitC ( ESP-WROOM-32 開発ボード )

秋月電子通商さんでは以下のページにあります。

ESP32-DevKitC ESP-WROOM-32開発ボード

Amazon.co.jp ではここにありました。


これは、簡単に言うと Wi-Fi & Bluetooth 機能付きマイコンボードです。
ESPRESSIF 社の ESP32 を日本の電波法をクリアさせて技適認証取得した ESP-WROOM-32 に、更に電圧レギュレーターやUSBシリアル変換を組み込んだ開発ボードです。
これには 2.54mm ピンヘッダが予めハンダ付けしてあります。

SparkFun マイクロSDカードスロット・ピッチ変換基板

SparkFun マイクロSDカードスロット・ピッチ変換基板

micro SD カードスロットだけで、他のチップが一切入っていないシンプルなモジュールです。
これには 2.54mm ピッチピンヘッダが付いていないかも知れませんので、別途購入してハンダ付けする必要があります。

micro SDHC カード

当方で動作確認が取れている micro SDHC カードは以下の物です。
他のものは動作しない場合がありますのでご注意ください。
その件については以下の記事もご参照ください。

ESP32 ( ESP-WROOM-32 ) で micro SDHC メモリカードを使う場合の注意点

パソコンで読み書きする場合には、micro SD カードリーダースロットが必要です。
そして、SDカードスロットがあっても、micro SD スロットが無い場合は、アダプターが必要になります。
以下のカードには恐らくアダプターが付属していると思いますが、念のため付属しているか確認してから購入することをお勧めします。

旧モデル Transcend microSDHCカード 32GB Class10 UHS-I対応 TS32GUSDU1
トランセンドジャパン
¥1,480(2024/04/19 12:13時点)

Twitter の@robo8080さんによれば、これの 16GB は動作しなかったという情報がありました。
私は16GB は持ち合わせていないので、試しに 32GB を購入して確かめてみたら、特に問題無く動作しました。
これにはデータ転送中のエラーを修正する EEC 機能というものがあるらしく、それが影響するのかとも思いましたが、今のところ問題無しです。

Transcend microSDHCカード 8GB Class10

Transcend microSDHCカード 8GB Class10 UHS-I対応 Nintendo Switch 動作確認済 TS8GUSDU1
トランセンドジャパン
¥1,080(2024/04/19 09:14時点)

TDK microSDHCカード 4GB Class4 SDアダプター付き

ブレッドボード SAD-101 ( サンハヤト )

サンハヤト SAD-101 ニューブレッドボード
サンハヤト
¥529(2024/04/19 08:43時点)

このブレッドボードは ESP32 – DevKitC を挿し込んでも、片側1列、もう片側2列の空きがある、私のお勧めブレッドボードです。
他のメーカーの場合、ブレッドボードに空きが無いので注意してください。

1/4W 10kΩ固定抵抗 2つ

これは、SPI通信のMOSI ( Master Output Slave Input ) 端子と MISO ( Master Input Slave Output )端子をプルアップするためのものです。

2.54mmピッチ ピンヘッダ

1×20=20P ピンヘッダ メッキなし 2.54mmピッチ 2個入 <con-051>
札幌貿易(Sapporo Boueki)
¥85(2024/04/18 17:29時点)

SparkFun micro SD カードスロットにはピンヘッダが付属していないかも知れませんので、揃えておきます。
そして、それにハンダ付けしておいてください。

Microsoft Excel

私は 2016 版を使っていますが、特殊な計算式は使っていないので、あまり古くなければ、他のバージョンでも恐らく動くと思います。

Wi-Fi ルーター環境

インターネットに接続してある Wi-Fi 環境が必要です。
NTPサーバーで時刻補正する時に使います。

その他、ジャンパーワイヤー、パソコン等

パソコンには SD または micro SD カードリーダーが付いているか確認してください。

接続する

では、以下のようにブレッドボード上で接続してみてください。

写真では、こんな感じです。

サンハヤトのブレッドボード SAD-101 ならば1枚でマウントできました。

SPI 通信では、SCLK ( クロック )、MOSI ( Master Output Slave Input )、MISO ( Master Input Slave Output ) ラインを共有できます。
CS ( Chip Select )ピンでデバイスを切り替えます。

ここで注意していただきたいのは、MOSI と MISO ピンを10kΩ程度の抵抗でプルアップしておくことです。
そうしないと、micro SDHC カードの読み書きができない場合がありますので要注意です。
その件に関しては以下の記事も参照してみてください。

ESP32 ( ESP-WROOM-32 ) で micro SDHC メモリカードを使う場合の注意点

それと、micro SD カードの GND ピンと OLED ( 有機EL )の GND ピンは、上図のように分けることをお勧めします。
なぜかというと、OLED に大電流が流れた場合、GND電位が変化してしまうので、共通の GNDピンにしてしまうと SDカードの読み書きに影響が出てしまう可能性があるためです。

因みに、今回は ESP32 のSPIインターフェースの VSPI を使っています。
micro SD カードも OLED も両方ともです。

HSPI にするためには少々ノウハウ が必要なので、また別の機会に紹介したいと思います。

Arduino core for the ESP32 を予め設定しておく

Arduino IDE ( 統合開発環境 )は予めインストールしておきます。
現在、1.8.2 で動作確認しています。

その Arduino IDE に Arduino core for the ESP32 をインストールしておきます。
そのインストール方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

Excel ( エクセル )で16×16ドットフォントを作り、CSV ファイル出力

まず、私の自作の Excel ( エクセル )ファイルを GitHub の以下のページからダウンロードします。

縦列16番目が抜けていたので修正しました。
その他、計算式が少々誤っていたので修正しました。
(2017/5/6)

https://github.com/mgo-tec/Excel_Make_Font_CSV

このページを開いて、以下の図のようなところをクリックして ZIP ファイルをダウンロードしてください。

Windows 10 ならば、ダウンロードフォルダにダウンロードされていると思いますので、そのZIP ファイルを解凍しておいてください。

解凍したら、まずウィルスチェックソフトでファイルをチェックしてください。
ご存知だと思いますが、当方でアップロードしたファイルは予めウィルスチェックをした安全なファイルをアップロードしておりますが、インターネット上に公開している以上、ウィルスに汚染される危険性は常にあるということは年頭に置いておいてください。

その後、
MyFont.xlsx
というエクセル ( Excel )ファイルを開いて下さい。

すると、以下のようになるので、編集する場合は「編集を有効にする」をクリックします。

ここでは、数値の0~9までのフォントを編集できます。

MyFont シートにある黒い四角 “■” をコピー&ペーストするなり、削除するなりすると、文字の形が変えられます。
その”■”が有るか無いかで、0と1の数値文字に置き換え、それを縦割りにして、MSB、LSB と1バイトに分けて、HEX欄で 16進数文字列に自動変換されます。

なぜ、このようにビットを分割するかというと、日本語には全角と半角があるので、将来的にそれに対応できるようにするためです。

編集を終えたら、下図の様に
CSV_fileOUTput シート
を開きます。

このシートは、MyFont シートで自動計算された HEX 文字列をリンクして、順番に1列に並べているだけのシートです。
このシートを表示した状態で、ファイルを「名前を付けて保存」する時に、CSV形式で保存すると、この文字列だけテキスト出力されます。

上図のように、CSV(カンマ区切り)形式を選択して、ファイル名を以下のようにします。

MyFont.csv

そして、エンターキーを押すと、以下のようなメッセージが出ますので、OKをクリックしてください。

すると、以下のように表示されるので、元の .xlsx 形式で保存し直しておきます。

では、MyFont.csv ファイルをメモ帳などのテキストエディタで開いてみます。
こんな感じになっていると思います。

このままでもフォントファイルとして使えないこともないです。
しかし、これはテキスト形式なので、例えば最初の文字 “03” をマイコンで読み込むと、

‘0’ = 0x30
‘3’ = 0x33

というように、2バイト読み込まねばなりません。
本来なら、16進数の 0x03 ならば、1バイトで済むはずなのに・・・。
しかも、その次の数値はキャリッジリターンコードと改行コードが入っているので、次の文字を読み込むまで4バイトも費やしてしまいます。
これでは、速度が遅くなってしまうので、このテキスト形式をバイナリ形式に変換して順番に並べ替えます。

では、次にその方法を紹介します。

CSV 形式ファイルをバイナリファイルに変換する。

事前に、micro SDHC カードはフォーマットしてあるか、確認しておいてください。
今、販売してあるものはたいてい FAT32 でフォーマットしてあると思います。
フォーマットされていない場合は以下の記事を参照してください。

micro SD 、micro SDHC カードの初期化(フォーマット)方法

次に、パソコンで、その micro SDHC カードのルートに font フォルダを新規に作成しておいてください。

その font フォルダに、先ほど作成した MyFont.csv ファイルをコピーしておいてください。

次に、ESP32 – DevKitC とカードスロットを接続した状態にします。
そして、パソコンから micro SDHC カードを「安全に取り外し」して、ESP32側のカードスロットに挿入しておきます。
そして、ESP32 – DevKitC とパソコンをUSB 接続して、電源投入しておいてください。

次に、Arduino IDE を起動して、先ほどダウンロードしたファイルの中の

MyFont_CSV_Binary_Convert.ino

を開きます。
これは、CSVフォントファイルをArduino IDE でバイナリ形式に出力するサンプルスケッチです。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include "FS.h"
#include "SD.h"
#include "SPI.h"

const char* MyFont_CSV_Readfile = "/font/MyFont.csv"; //読み込む CSV ファイル名
const char* MyFont_Binary_Writefile = "/font/MyFont.fnt"; //書き込むバイナリファイル名

void setup() {
  Serial.begin(115200);

  SD.begin(5, SPI, 10000000, "/sd"); //SD CS = GPIO #5

  File fr = SD.open(MyFont_CSV_Readfile, FILE_READ);
  File fw = SD.open(MyFont_Binary_Writefile, FILE_WRITE);
  
  int16_t i = 0, j = 0, k = 0;
  uint8_t c[4];
  uint8_t cH, cL;
  uint8_t b[10][2][16];

  for(i=0; i<4; i++){
    c[i] = 0;
  }
  
  if(fr){
    for(i = 0; i < 10; i++){
      Serial.println();
      Serial.println("------------------");
      for(j = 0; j < 2; j++){
        Serial.println();
        Serial.print("###################");
        Serial.println(j);
        for(k = 0; k < 16; k++){
          Serial.print("(");Serial.print(k); Serial.print(")  : ");

          fr.read( c , 4 );

          if( c[0] == '\0' || c[1] == '\0' || c[2] == '\0' || c[3] == '\0') break;

          if((c[0] != '\n') && (c[0] != '\r') && (c[1] != '\n') && (c[1] != '\r')){
            if(c[0] > 0x40){
              cH = (uint8_t)((c[0] - 0x41) + 10);
            }else{
              cH = (uint8_t)(c[0] - 0x30);
            }
            if(c[1] > 0x40){
              cL = (uint8_t)((c[1] - 0x41) + 10);
            }else{
              cL = (uint8_t)(c[1] - 0x30);
            }
            b[i][j][k] = cH*16 + cL;
            
            Serial.print(b[i][j][k], HEX);
          }
          
          if(c[2] == '\n'){
            Serial.println("¥n");
          }
          if(c[2] == '\r'){
            Serial.print("  ¥r");
          }
          if(c[3] == '\n'){
            Serial.println("¥n");
          }
          if(c[3] == '\r'){
            Serial.print("  ¥r");
          }
          yield();
        }
        if( c[0] == '\0' || c[1] == '\0' || c[2] == '\0' || c[3] == '\0') break;
      }
      if( c[0] == '\0' || c[1] == '\0' || c[2] == '\0' || c[3] == '\0') break;
    }
  }else{
    Serial.println(F(" File has not been uploaded to the flash in SD file system"));
    delay(30000);
  }
  fr.close();

  if(fw){
    for(i = 0; i < 10; i++){
      for(j = 0; j < 2; j++){
        for(k = 0; k < 16; k++){
          fw.write(b[i][j][k]);
        }
      }
    }
  }else{
    Serial.println(F(" File has not been uploaded to the flash in SD file system"));
    delay(30000);
  }
  fw.close();
}

void loop() {

}

【解説】

まず、MyFont.csv ファイルを Stirling などのバイナリエディタで見てみるとこうなります。

最初の4バイトに注目すると、
30 は ASCII コードで ‘0’ という文字です。
33 は ASCII コードで ‘3’ という文字です。
0D はエスケープシーケンスの ‘¥r’ で、キャリッジリターン
0A はエスケープシーケンスの ‘¥n’ で、改行です。

それを踏まえて以下の解説を見てください。

●5行目:
micro SDHC 内のCSV ファイルを定義します。

●6行目:
micro SDHC の font フォルダ内に出力するバイナリ形式フォントファイルを定義します。

●11行目:
ここで、SDカードのCSピンを#5 に設定します。

●19行目:
実際にバイナリ出力するバイトを定義しています。

●36行目:
CSVファイルでは、各行にエスケープシーケンスのキャリッジリターン ‘ ¥r’ と、改行’ ¥n ‘ がありますので、4バイト分読み込みます。

●41-51行:
読み込んだ4バイト中の上位2バイトのASCII文字コードの不要部分をカットして、1バイトのバイナリ値に変換します。
1桁目と2桁目をこのようにして、一つの数値にします。
それを b という配列に代入します。

●56-67行:
キャリッジリターンと改行コードを検知したら、シリアルモニターに出力します。

●80-87行:
ここで、実際に MyFont.fnt というファイルにバイナリ値を連続で出力していきます。

●92行:
出力終わったら、必ずファイルをクローズしておきます。

コンパイル実行する

では、このスケッチをコンパイル実行してみてください。
シリアルモニターは 115200bps に設定しておいてください。

シリアルモニターの結果はこんな感じなって、最後までエラーがなければOKです。

次に、ESP32 – DevKitC の電源を落として、micro SDHC カードを取り出して、パソコンで確認してみてください。
下図の様に MyFont.fnt というファイルが出来ていればOKです。

では、このファイルをStirling などのバイナリエディタで見てみます。

こんな感じでテキスト文字から数値にちゃんと変換されて書き込まれていればOKです。

そうしたら、micro SDHC カードをパソコンから「安全に取り外し」をして抜き出して、 ESP32 – DevKitC 側のスロットにセットしておいてください。

OLED のグラフィックアクセラレーションコマンドを使った文字スクロール方法

私が作成した今までの電光掲示板スクロール方法は、1ドット(ピクセル)ずつ移動させていました。
(以下記事参照)
ESP-WROOM-02 で WebSocket リアルタイム制御 日本語電光掲示板作成(単体LEDマトリックス版)

ビットシフトさせた後、16ドットをSPI送信して表示させるような感じです。
フルカラー OLED の場合、ドット毎にカラーを割り当てたバイトを送信するので、256色カラーの場合は、16ドット表示させるために16バイト送信しなければなりません。
ですから、16×16ドットの文字を上方向へ1ドットずらすだけでも
16×16  = 256 バイト
も送信しなければいけません。
これではスクロール速度が遅くなってしまいます。

でも、この SSD1331 では、内蔵グラフィックアクセラレーションコマンドを使って、16×16 ドットの画面を1ドット上にコピー&ペーストするのに、たった7バイトだけで済むんです。
これはとても有難い機能ですね。
ですから、SSD1331 のクロック周波数が遅くても、この機能だけで圧倒的に高速スクロールできるんです。

スクロール方法は下図のような感じです。

例えば、範囲 ( 0, 1) ~ ( 15, 15 ) を左上隅 ( 0, 0 ) にコピペするには、

1.コマンド:0x23 送信
2.コマンド:0 送信
3.コマンド:1 送信
4.コマンド:15 送信
5.コマンド:15 送信
6.コマンド:0送信
7.コマンド:0送信

という感じで、たった7バイトで済んでしまいます。
その後、空いた15番目のドット列に次の文字の0列目のドット列を表示させるだけです。
これは格段にスクロールが簡単で速くなりますね。
このコピー&ペースト範囲が大きければ大きいほど効果を発揮します。
こういう機能を他のディスプレイドライバIC に取り入れて欲しいものですね。

スクロール NTP 時計のスケッチ入力

では、いよいよ NTP サーバー自動補正時計のスケッチを入力してみましょう。

以下のスケッチを入力してみてください。
NTPサーバーからの時刻取得プログラムは Arduino core for the ESP8266 のサンプルスケッチをそのまま流用させていただきました。
そのサンプルスケッチのライセンスは良く分からないのですが、ESP32関係のライブラリと同様にこのスケッチに関してもとりあえず LGPL 2.1 としておきました。
(ネット上でソースコードを公開する場合には、ライセンス表記をしておかないと自由に配布できないそうです。)

以下のコードは古いため、現在は正常に動作しません。
修正したコードは最後に追記しましたので、ご参照ください。

 

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

/*
 * Copyright (c) 2017 Mgo-tec
 * License LGPL 2.1
 * Reference LGPL-2.1 license statement --> https://opensource.org/licenses/LGPL-2.1 
 */
#include <SD.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include "TimeLib.h" //timeライブラリver1.4の場合

const char* ssid = "xxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxx"; //ご自分のルーターのパスワードに書き換えてください

const char* MyFont_file = "/font/MyFont.fnt"; //自作フォントファイル名を定義

const uint8_t cs_SD = 5; //SD card CS ( Chip Select )
const uint8_t cs_OLED = 17; //#15はHPSI用なので使えない。注意
const uint8_t DCpin =  16; //OLED DC(Data/Command)
const uint8_t RSTpin =  4; //OLED Reset

const uint8_t SCLKpin =  18; //SCLK
const uint8_t MOSIpin =  23; //MOSI (Master Output Slave Input)

uint8_t MyFont_buf[10][2][16];

//-------NTPサーバー引数初期化-----------------------------
IPAddress timeServer(52, 168, 138, 145); //time.windows.com
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
const int timeZone = 9;     // Tokyo
WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets
time_t prevDisplay = 0; // when the digital clock was displayed
//-------時刻表示定義-------------------
char sec_c1 = '?', sec_c2 = '?';
uint8_t sec_scl_cnt1 = 0, sec_scl_cnt2 = 0;

char min_c1 = '?', min_c2 = '?';
uint8_t min_scl_cnt1 = 0, min_scl_cnt2 = 0;

char hour_c1 = '?', hour_c2 = '?';
uint8_t h_scl_cnt1 = 0, h_scl_cnt2 = 0;

uint32_t last_Hmin_time = 0, last_sec_time = 0, colon_last_time = 0;

bool colon_in = false;
uint8_t colon_t = 60;

//******************************************
void setup() {
  delay(1000); //ESP32が起動するまで待つ
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    yield();
  }
  Serial.println("");
  Serial.println(F("WiFi connected"));

  SD.begin(5, SPI, 10000000, "/sd");

  File MyF = SD.open(MyFont_file, FILE_READ);
  if (!MyF) {
    Serial.print(MyFont_file);
    Serial.println(" File not found");
    return;
  }else{
    Serial.print(MyFont_file);
    Serial.println(" File read OK!");
  }
  for(int i=0; i<10; i++){
    MyFont_SD_Read(MyF, 2, i, MyFont_buf[i]);
  }
  MyF.close();
 
  SSD1331_Init(cs_OLED, DCpin, RSTpin);

  //NTPサーバーから時刻を取得---------------------------
  Udp.begin(localPort);
  setSyncProvider(getNtpTime);
  delay(1000);

  last_sec_time = millis();
  colon_last_time = millis();
}

void loop() {
  int i, j;
  byte Red, Green, Blue;
  //---------------------------
  if(millis() - last_Hmin_time > 50){ //時間、分表示設定
    Red = 7; //0-7
    Green = 1; //0-7
    Blue = 2; //0-3
    char h_chr[3], m_chr[3];

    sprintf(h_chr, "%2d", hour());//ゼロを空白で埋める場合は%2dとすれば良い
    sprintf(m_chr, "%02d", minute());
    
    if(h_chr[0] != hour_c1){ //時間表示設定
      if(h_chr[0] == ' '){
        uint8_t buf[2][16];
        for(i=0; i<2; i++){ //時間の十の位がゼロならば空白にする
          for(j=0; j<16; j++) buf[i][j] = 0;
        }
        Time_Copy_H_Scroll(0, 2, buf, &h_scl_cnt1, 0, 0, 15, 15, Red, Green, Blue);
      }else{
        uint8_t num = h_chr[0] - 0x30;
        Time_Copy_H_Scroll(0, 2, MyFont_buf[num], &h_scl_cnt1, 0, 0, 15, 15, Red, Green, Blue);
      }
      if(h_scl_cnt1 == 16){
        hour_c1 = h_chr[0];
        h_scl_cnt1 = 0;
      }
    }
    if(h_chr[1] != hour_c2){
      uint8_t num = h_chr[1] - 0x30;
      Time_Copy_V_Scroll(0, 2, MyFont_buf[num], &h_scl_cnt2, 16, 0, 31, 15, Red, Green, Blue);
      if(h_scl_cnt2 == 16){
        hour_c2 = h_chr[1];
        h_scl_cnt2 = 0;
      }
    }
    //----------------------------
    if(m_chr[0] != min_c1){ //分表示設定
      uint8_t num = m_chr[0] - 0x30;
      Time_Copy_H_Scroll(1, 2, MyFont_buf[num], &min_scl_cnt1, 40, 0, 55, 15, Red, Green, Blue);
      if(min_scl_cnt1 == 16){
        min_c1 = m_chr[0];
        min_scl_cnt1 = 0;
      }
    }
    if(m_chr[1] != min_c2){
      uint8_t num = m_chr[1] - 0x30;
      Time_Copy_V_Scroll(1, 2, MyFont_buf[num], &min_scl_cnt2, 56, 0, 71, 15, Red, Green, Blue);
      if(min_scl_cnt2 == 16){
        min_c2 = m_chr[1];
        min_scl_cnt2 = 0;
      }
    }
    last_Hmin_time = millis();
  }
  //-----------------------
  if(colon_t != second()){ //コロン表示
    Drawing_Rectangle_Fill(34, 2, 38, 6, 0, 63, 0, 31, 63, 0); //65000 color
    Drawing_Rectangle_Fill(34, 9, 38, 13, 0, 63, 0, 31, 63, 0); //65000 color

    colon_last_time = millis();
    colon_t = second();
    colon_in = true;
  }
  if(colon_in == true){
    if((millis() - colon_last_time) >= 500){
      Display_Clear(32, 0, 39, 15);
      colon_in = false;
    }
  }
  //----------------------------------
  if(millis() - last_sec_time > 15){ //秒表示設定
    Red = 0; //0-7
    Green = 7; //0-7
    Blue = 3; //0-3
    char s_chr[3];
    sprintf(s_chr, "%02d", second());
    
    if(s_chr[0] != sec_c1){
      uint8_t num = s_chr[0] - 0x30;
      Time_Copy_H_Scroll(0, 2, MyFont_buf[num], &sec_scl_cnt1, 64, 16, 79, 31, Red, Green, Blue);
      if(sec_scl_cnt1 == 16){
        sec_c1 = s_chr[0];
        sec_scl_cnt1 = 0;
      }
    }
    if(s_chr[1] != sec_c2){
      uint8_t num = s_chr[1] - 0x30;
      Time_Copy_V_Scroll(0, 2, MyFont_buf[num], &sec_scl_cnt2, 80, 16, 95, 31, Red, Green, Blue);
      if(sec_scl_cnt2 == 16){
        sec_c2 = s_chr[1];
        sec_scl_cnt2 = 0;
      }
    }
    last_sec_time = millis();
  }
}
//************ SSD1331 初期化 ****************************************
void SSD1331_Init(uint8_t CS, uint8_t DC, uint8_t RST){  
  pinMode(RST, OUTPUT); //RES
  pinMode(DC, OUTPUT); //DC
  pinMode(CS, OUTPUT); //CS

  digitalWrite(RST, HIGH);
  digitalWrite(RST, LOW);
  delay(1);
  digitalWrite(RST, HIGH);

  digitalWrite(CS, HIGH);
  digitalWrite(DC, HIGH);

  SPI.begin(SCLKpin, 12, MOSIpin, cs_OLED);
  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低130ns
  SPI.setDataMode(SPI_MODE2); //オシロで測ると実際はMODE3の信号だった

  CommandWrite(0xAE); //Set Display Off
  CommandWrite(0xA0); //Remap & Color Depth setting 
    CommandWrite(0b00110010); //A[7:6] = 00; 256 color. A[7:6] = 01; 65k color format
  CommandWrite(0xA1); //Set Display Start Line
    CommandWrite(0);
  CommandWrite(0xA2); //Set Display Offset
    CommandWrite(0);
  CommandWrite(0xA4); //Set Display Mode (Normal)
  CommandWrite(0xA8); //Set Multiplex Ratio
    CommandWrite(0b00111111); //15-63
  CommandWrite(0xAD); //Set Master Configration
    CommandWrite(0b10001110); //a[0]=0 Select external Vcc supply, a[0]=1 Reserved(reset)
  CommandWrite(0xB0); //Power Save Mode
    CommandWrite(0b00000000); //0x1A Enable power save mode
  CommandWrite(0xB1); //Phase 1 and 2 period adjustment
    CommandWrite(0x74);
  CommandWrite(0xB3); //Display Clock DIV
    CommandWrite(0xF0);
  CommandWrite(0x8A); //Pre Charge A
    CommandWrite(0x81);
  CommandWrite(0x8B); //Pre Charge B
    CommandWrite(0x82);
  CommandWrite(0x8C); //Pre Charge C
    CommandWrite(0x83);
  CommandWrite(0xBB); //Set Pre-charge level
    CommandWrite(0x3A);
  CommandWrite(0xBE); //Set VcomH
    CommandWrite(0x3E);
  CommandWrite(0x87); //Set Master Current Control
    CommandWrite(0x06);
  CommandWrite(0x15); //Set Column Address
    CommandWrite(0);
    CommandWrite(95);
  CommandWrite(0x75); //Set Row Address
    CommandWrite(0);
    CommandWrite(63);
  CommandWrite(0x81); //Set Contrast for Color A
    CommandWrite(255);
  CommandWrite(0x82); //Set Contrast for Color B
    CommandWrite(255);
  CommandWrite(0x83); //Set Contrast for Color C
    CommandWrite(255);
    
  for(int j=0; j<64; j++){ //Display Black OUT
    for(int i=0; i<96; i++){
      DataWrite(0x00);
      DataWrite(0x00);
      DataWrite(0x00);
    }
  }
  
  CommandWrite(0xAF); //Set Display On
  delay(110); //ディスプレイONコマンドの後は最低100ms 必要
}
//*********** SPI 通信送信 ***********************************
void CommandWrite(uint8_t b){
  digitalWrite(17, LOW);
  digitalWrite(16, LOW);//DC
  SPI.write(b);
  digitalWrite(17, HIGH);
}

void DataWrite(uint8_t b){
  digitalWrite(17, LOW);
  digitalWrite(16, HIGH);//DC
  SPI.write(b);
  digitalWrite(17, HIGH);
}

void CommandWriteBytes(uint8_t *b, uint16_t n){
  digitalWrite(17, LOW);
  digitalWrite(16, LOW);//DC
  SPI.writeBytes(b, n);
  digitalWrite(17, HIGH);
}

void DataWriteBytes(uint8_t *b, uint16_t n){
  digitalWrite(17, LOW);
  digitalWrite(16, HIGH);//DC
  SPI.writeBytes(b, n);
  digitalWrite(17, HIGH);
}
//*********** ディスプレイ消去 ****************************
void Display_Clear(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1){
  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低130ns
  SPI.setDataMode(SPI_MODE2); //オシロで測ると実際はMODE3の信号だった
  delayMicroseconds(500); //クリアーコマンドは400μs 以上の休止期間が必要かも
  CommandWrite(0x25); //Clear Window
    CommandWrite(x0); //Column Address of Start
    CommandWrite(y0); //Row Address of Start
    CommandWrite(x1); //Column Address of End
    CommandWrite(y1); //Row Address of End
  delayMicroseconds(800); //ここの間隔は各自調節してください。
}
//*********** 自作フォントをSDカードから読み込む ****************
void MyFont_SD_Read(File F, uint8_t ZorH, uint8_t num, uint8_t buf[2][16]){
  F.seek(num * (16 * ZorH));
  F.read(buf[0], 16);
  F.read(buf[1], 16);
}
//*********** 時刻垂直スクロール ****************
void Time_Copy_V_Scroll(uint8_t Direction, uint8_t ZorH, uint8_t buf[2][16], uint8_t *SclCnt, uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t col_R, uint8_t col_G, uint8_t col_B){
  uint8_t Dot = (col_R << 5) | (col_G << 2) | col_B;
  uint8_t i, k;
  uint8_t bbb = 0b10000000;
  uint8_t DotDot[8 * ZorH];
  uint8_t com[6];

  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低150ns
  SPI.setDataMode(SPI_MODE2); //オシロで測ると実際はMODE3の信号だった

  switch( Direction ){
    case 0:
      Copy(x0, y0+1, x1, y1, x0, y0);
      
      com[0] = 0x15; //Set Column Address
      com[1] = x0; com[2] = x1;
      com[3] = 0x75; //Set Row Address
      com[4] = y1; com[5] = y1;
      break;
    case 1:
      Copy(x0, y0, x1, y1-1, x0, y0+1);
      
      com[0] = 0x15; //Set Column Address
      com[1] = x0; com[2] = x1;
      com[3] = 0x75; //Set Row Address
      com[4] = y0; com[5] = y0;
      break;
  }  

  CommandWriteBytes(com, sizeof(com));
  
  switch( Direction ){
    case 0:
      for(k=0; k<ZorH; k++){
        bbb = 0b10000000;
        for(i=0; i<8; i++){
          if(i>0) bbb = bbb >> 1;
          if(( buf[k][*SclCnt] & bbb ) > 0){
            DotDot[i + k*8] = Dot;
          }else{
            DotDot[i + k*8] = 0;
          }
        }
      }
      break;
    case 1:
      for(k=0; k<ZorH; k++){
        bbb = 0b10000000;
        for(i=0; i<8; i++){
          if(i>0) bbb = bbb >> 1;
          if(( buf[k][15 - (*SclCnt)] & bbb ) > 0){
            DotDot[i + k*8] = Dot;
          }else{
            DotDot[i + k*8] = 0;
          }
        }
      }
      break;
  }

  DataWriteBytes(DotDot, sizeof(DotDot));
  (*SclCnt)++;
}
//*********** 時刻水平スクロール **********************
void Time_Copy_H_Scroll(uint8_t Direction, uint8_t ZorH, uint8_t buf[2][16], uint8_t *SclCnt, uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t col_R, uint8_t col_G, uint8_t col_B){
  uint8_t com[6];
  uint8_t Dot = (col_R << 5) | (col_G << 2) | col_B;
  uint8_t i;
  uint8_t k = 0;
  uint8_t bbb = 0;

  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低150ns
  SPI.setDataMode(SPI_MODE2); //オシロで測ると実際はMODE3の信号だった
  
  switch( Direction ){
    case 0:
      Copy(x0+1, y0, x1, y1, x0, y0);
  
      com[0] = 0x15; //Set Column Address
      com[1] = x1; com[2] = x1;
      com[3] = 0x75; //Set Row Address
      com[4] = y0; com[5] = y1;

      if((*SclCnt) < 8){
        bbb = 0b10000000 >> (*SclCnt);
      }else if((*SclCnt) >= 8){
        bbb = 0b10000000 >> ((*SclCnt)-8);
      }
    
      if((*SclCnt) < 8){
        k = 0;
      }else{
        k = 1;
      }
      
      break;
    case 1:
      Copy(x0, y0, x1-1, y1, x0+1, y0);
  
      com[0] = 0x15; //Set Column Address
      com[1] = x0; com[2] = x0;
      com[3] = 0x75; //Set Row Address
      com[4] = y0; com[5] = y1;

      if((*SclCnt) < 8){
        bbb = 0b00000001 << (*SclCnt);
      }else if((*SclCnt) >= 8){
        bbb = 0b00000001 << ((*SclCnt) - 8);
      }
    
      if(ZorH == 2){
        if((*SclCnt) < 8){
          k = 1;
        }else{
          k = 0;
        }
      }else if(ZorH ==1){
        k = 0;
      }
      
      break;
  }
  
  CommandWriteBytes(com, 6);  

  uint8_t DotDot[16];

  for(i=0; i<16; i++){
    if((buf[k][i] & bbb) > 0){
      DotDot[i] = Dot;
    }else{
      DotDot[i] = 0;
    }
  }

  DataWriteBytes(DotDot, 16);
  (*SclCnt)++;
}
//********** SSD1331 コピーコマンド使用 範囲コピー *********************************
void Copy(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t X, uint8_t Y){
  uint8_t buf[7];
  buf[0] = 0x23; //Copy Command
  buf[1] = x0; buf[2] = y0; buf[3] = x1; buf[4] = y1; buf[5] = X; buf[6] = Y; 

  delayMicroseconds(500); //これは必要
  CommandWriteBytes(buf, sizeof(buf));
  delayMicroseconds(500); //これは必要
}
//************ SSD1331 65000色 四角 塗りつぶし描画***************************
void Drawing_Rectangle_Fill(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t Line_R, uint8_t Line_G, uint8_t Line_B, uint8_t Fill_R, uint8_t Fill_G, uint8_t Fill_B){
  //Line_R (0-31), Line_G (0-63), Line_B (0-31)
  //Fill_R (0-31), Fill_G (0-63), Fill_B (0-31)
  uint8_t lineR, lineB, fillR, fillB;
  lineR = Line_R <<1;
  lineB = Line_B <<1;
  fillR = Fill_R <<1;
  fillB = Fill_B <<1;
  
  CommandWrite(0x26); //Fill Enable or Disable
    CommandWrite(0b00000001); //A0=0 Fill Enable

  CommandWrite(0x22); //Drawing Rectangle
    CommandWrite(X0); //Column Address of Start
    CommandWrite(Y0); //Row Address of Start
    CommandWrite(X1); //Column Address of End
    CommandWrite(Y1); //Row Address of End
    CommandWrite(lineR);
    CommandWrite(Line_G);
    CommandWrite(lineB);
    CommandWrite(fillR);
    CommandWrite(Fill_G);
    CommandWrite(fillB);
}
//*********************** NTP Time **************************************
time_t getNtpTime(){
  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}
//*********************** NTP Time ************************************
void sendNTPpacket(IPAddress &address){
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;         
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

【解説】

●6-8行:
Arduino core for the ESP32 の標準ライブラリです。

●9行目:
Arduino でよく使う TimeLib ライブラリを使います。
そのインストール方法は以下の記事を参照してください。
https://www.mgo-tec.com/blog-entry-1616shinonome-ws-oled-news.html

●11-12行目:
ご自分のルーターのSSID とパスワードに書き換えてください。

●14行目:
先ほど作ったバイナリ形式フォントファイルを定義します。

●16-22行目:
micro SD カードスロットと、OLED ( 有機EL ) のピン設定をします。

●24行目:
グローバル変数領域で、フォントを一気に取り込むための配列です。

●27-33行:
Arduino core for the ESP8266 用の NTPサーバー取得サンプルスケッチをそのまま ESP32 スケッチに取り込んだだけです。その変数の初期化です。

●35-47行:
時計表示のための変数初期化です。
47行目などは、時計表示で有り得ない数値で初期化しておくことで、初回表示の時にスクロールしてくれるようになります。

●54-60行:
ここで、ルーターと接続します。

●64-78行:
ここで、micro SDHC カードのフォントファイルからフォントデータを読み取り、MyFont_buf に格納しておきます。
フォントデータが少ないのでこういう形は可能です。
ここで注意しておきたいのは、78行目にあるように、かならずファイルをクローズしておくことです。
これを忘れると、沢山のファイルを開くプログラムを組んだ時に他のファイルが開けなくなります。
Arduino core for the ESP32 では、私が調べたところによると、同時に開けるファイル数は4つまでです。

●80行目:
190-260行にある OLED SSD1331 を初期化する関数の実行です。

●83-84行:
ここで、NTPサーバーからUDP通信で時刻を取得します。
setSyncProvider 関数を実行すると、自動的に5分毎に時刻取得して補正してくれます。

●95-146行:
ここでは、時間と分の表示を実行しています。
フォント表示は256色カラーにしていますので、赤と緑は0~7、青は0~3まで指定できて、それを混ぜ合わせた色を作ります。
時刻は101 行のように、TimeLib ライブラリ関数の hour() や minute() で現在時や分を取得し、それを文字列に変換しておきます。
そうすると、1の位や、10の位の数値を分離できて、都合が良いのです。
それぞれの桁の数値が更新されたら、110行目のような Time_Copy_H_Scroll 関数を実行します。
これは、372-445行で関数化しています。
これは、この関数を通る度に垂直方向に1ドット(ピクセル)ずつ移動していく関数です。
移動する間隔は95行目で決めていて、50ms 毎にビット移動しています。
これは後で述べますが、SSD1331 のグラフィックアクセラレーションコマンド( Graphic Acceleration Commands )を使っています。
そして、時間表示の場合、ゼロは空白のため、その場合の条件分岐をしてあります。
122行目にある Time_Copy_V_Scroll 関数は水平にスクロールする関数です。
308-370行で関数化しています。

●148-161行:
ここで、コロンの点滅表示をしています。
点滅は 500ms 毎に点灯と消灯を繰り返します。
149-150行で小さい四角形を描画しています。
これは457-480行で関数化しています。

●163-187行:
ここで秒を表示しています。
15ms 毎にドット(ピクセル)を1ずつずらしてスクロールさせています。
時や分表示よりスクロール速度を速くしています。

●190-260行:
SSD1331 の初期化です。
これに関しては、前回の記事を参照してください。

●262-268行:
SPI 通信で、コマンドやデータを送信する関数です。
SPI.writeBytes 関数を使うとバイトをまとめて送信できます。
これを使う方が通信速度は速くなります。

●290-300行:
前回の記事でも述べたように、画面をクリアするグラフィックアクセラレーションコマンド( Graphic Acceleration Commands )を使っています。
291-292行では、SDカードの読み込みの後にSPI_MODE0になっていた場合の対処で、ここで新たにMODE2 に指定しています。
今回のプログラムで、頻繁にSDカードを読み込むことはないのですが、将来的にあり得るので、こうしています。
前後に適度なdelay関数を置かないと正しく実行してくれないので注意です。

●302-306行:
ここで、micro SDHC カードから指定位置のフォントデータを読み込みます。
フォントデータは半角で16バイト毎にバイナリ値で整列しているので、読み取りは簡単です。

●308-370行:
文字を垂直方向にスクロールしていく関数です。
この関数が呼ばれる度に1ドット(ピクセル)ずつ移動していきます。
Direction でスクロールする方向を変えられます。
ゼロならば下→上方向
1ならば上→下方向
となります。
320行や328行で、447-455行のCopy 関数を使って必要な画面を1ドット上書きコピーさせます。
これの概念は上記で説明した通りです。
339-368行でフォントデータを1bit ずつ読み取って、カラー設定した1バイトを配列に順次代入して、368行でまとめて送信しています。
369行でスクロールカウントを1増やしています。

●372-445行:
文字を水平スクロールする関数です。
垂直スクロールとほぼ同じ構成です。

●447-455行:
SSD1331 へグラフィックアクセラレーションコマンド( Graphic Acceleration Commands )のコピーを送信する関数です。
452行や454行で delayMicroseconds 関数を置いているのは、コピーコマンド実行にはある程度休止期間が無いと実行が間に合いません。
この間隔は各自調整してみてください。
私の場合は0.5ms くらいでした。

●457-480行:
SSD1331 のグラフィックアクセラレーションコマンドの四角形を表示する関数です。
461-464行を注意していただきたいです。
これは、データシートに記載されている通り、1bito 左へシフトしなければなりません。

●482-518行:
これは、Arduino core for the ESP8266 の NTPサーバー時刻取得のサンプルスケッチをそのまま ESP32 へ流用したものです。

コンパイル書き込み、実行

では、インターネットに接続してあるご自分のルーターを起動させて、ESP32 – DevKitC が通信できる状態にしておいてください。
Macアドレスフィルタリングやファイアウォールの設定は予め済ませておいてください。

Wi-Fi環境がすべて整ったら、スケッチをコンパイル書き込み実行させてみてください。
最初に紹介した動画のように表示されればOKです。

シリアルモニター( 115200bpsで起動 )には以下のように表示されていればOKです。
5分毎に自動で時刻補正していることが分かると思います。

以上です。
いかがでしょうか?
うまく動作しましたでしょうか?

もし、不具合等あったらコメント等でご連絡いただけると助かります。
なにしろ私はアマチュア電子工作家で、プログラム等にいろいろと穴があるかも知れません。
いろいろとご容赦願います。

では、今回はここまでです。
これを応用して次回は Yahoo! ニュースや天気予報を表示させてみようと思います。

ではまた・・・。

追記(2022/08/22時点)

2022年8月時点で、上記のコードが動かないというコメント投稿がありましたので、修正しました。
WindowsのNTPタイムserverのIPアドレスが変更になってしまったため、時刻が取得できなかったのです。
そこで、NTPサーバーのIPアドレスで取得するのではなく、サーバーネームからIPアドレスを拾う方式に変更しました。
そして、先の記事で紹介しているように、SPI_MODEは3に変更されていますので、そこも修正しました。
Arduino core for the ESP32 2.0.4 で動作確認しました。

//Use Arduino core for the ESP32 stable 2.0.4

#include <SD.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include "TimeLib.h" //timeライブラリver1.4の場合

static const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
static const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください

const char* MyFont_file = "/font/MyFont.fnt"; //自作フォントファイル名を定義

const uint8_t cs_SD = 5; //SD card CS ( Chip Select )
const uint8_t cs_OLED = 17; //#15はHPSI用なので使えない。注意
const uint8_t DCpin =  16; //OLED DC(Data/Command)
const uint8_t RSTpin =  4; //OLED Reset

const uint8_t SCLKpin =  18; //SCLK
const uint8_t MOSIpin =  23; //MOSI (Master Output Slave Input)
 
uint8_t MyFont_buf[10][2][16];

//-------NTPサーバー引数初期化-----------------------------
const char* ntp_server_name = "time.windows.com";
IPAddress ntp_server_ip;
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
const int timeZone = 9;     // Tokyo
WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets
time_t prevDisplay = 0; // when the digital clock was displayed
//-------時刻表示定義-------------------
char sec_c1 = '?', sec_c2 = '?';
uint8_t sec_scl_cnt1 = 0, sec_scl_cnt2 = 0;

char min_c1 = '?', min_c2 = '?';
uint8_t min_scl_cnt1 = 0, min_scl_cnt2 = 0;

char hour_c1 = '?', hour_c2 = '?';
uint8_t h_scl_cnt1 = 0, h_scl_cnt2 = 0;

uint32_t last_Hmin_time = 0, last_sec_time = 0, colon_last_time = 0;
 
bool colon_in = false;
uint8_t colon_t = 60;

//******************************************
void setup() {
  delay(1000); //ESP32が起動するまで待つ
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    yield();
  }
  Serial.println("");
  Serial.println(F("WiFi connected"));

  SD.begin(5, SPI, 10000000, "/sd");

  File MyF = SD.open(MyFont_file, FILE_READ);
  if (!MyF) {
    Serial.print(MyFont_file);
    Serial.println(" File not found");
    return;
  }else{
    Serial.print(MyFont_file);
    Serial.println(" File read OK!");
  }
  for(int i=0; i<10; i++){
    MyFont_SD_Read(MyF, 2, i, MyFont_buf[i]);
  }
  MyF.close();

  SSD1331_Init(cs_OLED, DCpin, RSTpin);

  //NTPサーバーから時刻を取得---------------------------
  Udp.begin(localPort);
  WiFi.hostByName(ntp_server_name, ntp_server_ip);
  setSyncProvider(getNtpTime);
  delay(1000);

  last_sec_time = millis();
  colon_last_time = millis();
}

void loop() {
  int i, j;
  byte Red, Green, Blue;
  //---------------------------
  if(millis() - last_Hmin_time > 50){ //時間、分表示設定
    Red = 7; //0-7
    Green = 1; //0-7
    Blue = 2; //0-3
    char h_chr[3], m_chr[3];

    sprintf(h_chr, "%2d", hour());//ゼロを空白で埋める場合は%2dとすれば良い
    sprintf(m_chr, "%02d", minute());

    if(h_chr[0] != hour_c1){ //時間表示設定
      if(h_chr[0] == ' '){
        uint8_t buf[2][16];
        for(i=0; i<2; i++){ //時間の十の位がゼロならば空白にする
          for(j=0; j<16; j++) buf[i][j] = 0;
        }
        Time_Copy_H_Scroll(0, 2, buf, &h_scl_cnt1, 0, 0, 15, 15, Red, Green, Blue);
      }else{
        uint8_t num = h_chr[0] - 0x30;
        Time_Copy_H_Scroll(0, 2, MyFont_buf[num], &h_scl_cnt1, 0, 0, 15, 15, Red, Green, Blue);
      }
      if(h_scl_cnt1 == 16){
        hour_c1 = h_chr[0];
        h_scl_cnt1 = 0;
      }
    }
    if(h_chr[1] != hour_c2){
      uint8_t num = h_chr[1] - 0x30;
      Time_Copy_V_Scroll(0, 2, MyFont_buf[num], &h_scl_cnt2, 16, 0, 31, 15, Red, Green, Blue);
      if(h_scl_cnt2 == 16){
        hour_c2 = h_chr[1];
        h_scl_cnt2 = 0;
      }
    }
    //----------------------------
    if(m_chr[0] != min_c1){ //分表示設定
      uint8_t num = m_chr[0] - 0x30;
      Time_Copy_H_Scroll(1, 2, MyFont_buf[num], &min_scl_cnt1, 40, 0, 55, 15, Red, Green, Blue);
      if(min_scl_cnt1 == 16){
        min_c1 = m_chr[0];
        min_scl_cnt1 = 0;
      }
    }
    if(m_chr[1] != min_c2){
      uint8_t num = m_chr[1] - 0x30;
      Time_Copy_V_Scroll(1, 2, MyFont_buf[num], &min_scl_cnt2, 56, 0, 71, 15, Red, Green, Blue);
      if(min_scl_cnt2 == 16){
        min_c2 = m_chr[1];
        min_scl_cnt2 = 0;
      }
    }
    last_Hmin_time = millis();
  }
  //-----------------------
  if(colon_t != second()){ //コロン表示
    Drawing_Rectangle_Fill(34, 2, 38, 6, 0, 63, 0, 31, 63, 0); //65000 color
    Drawing_Rectangle_Fill(34, 9, 38, 13, 0, 63, 0, 31, 63, 0); //65000 color
 
    colon_last_time = millis();
    colon_t = second();
    colon_in = true;
  }
  if(colon_in == true){
    if((millis() - colon_last_time) >= 500){
      Display_Clear(32, 0, 39, 15);
      colon_in = false;
    }
  }
  //----------------------------------
  if(millis() - last_sec_time > 15){ //秒表示設定
    Red = 0; //0-7
    Green = 7; //0-7
    Blue = 3; //0-3
    char s_chr[3];
    sprintf(s_chr, "%02d", second());

    if(s_chr[0] != sec_c1){
      uint8_t num = s_chr[0] - 0x30;
      Time_Copy_H_Scroll(0, 2, MyFont_buf[num], &sec_scl_cnt1, 64, 16, 79, 31, Red, Green, Blue);
      if(sec_scl_cnt1 == 16){
        sec_c1 = s_chr[0];
        sec_scl_cnt1 = 0;
      }
    }
    if(s_chr[1] != sec_c2){
      uint8_t num = s_chr[1] - 0x30;
      Time_Copy_V_Scroll(0, 2, MyFont_buf[num], &sec_scl_cnt2, 80, 16, 95, 31, Red, Green, Blue);
      if(sec_scl_cnt2 == 16){
        sec_c2 = s_chr[1];
        sec_scl_cnt2 = 0;
      }
    }
    last_sec_time = millis();
  }
}
//************ SSD1331 初期化 ****************************************
void SSD1331_Init(uint8_t CS, uint8_t DC, uint8_t RST){  
  pinMode(RST, OUTPUT); //RES
  pinMode(DC, OUTPUT); //DC
  pinMode(CS, OUTPUT); //CS

  digitalWrite(RST, HIGH);
  digitalWrite(RST, LOW);
  delay(1);
  digitalWrite(RST, HIGH);

  digitalWrite(CS, HIGH);
  digitalWrite(DC, HIGH);

  SPI.begin(SCLKpin, 12, MOSIpin, cs_OLED);
  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低130ns
  SPI.setDataMode(SPI_MODE3); //オシロで測ると実際はMODE3の信号だった

  CommandWrite(0xAE); //Set Display Off
  CommandWrite(0xA0); //Remap & Color Depth setting 
    CommandWrite(0b00110010); //A[7:6] = 00; 256 color. A[7:6] = 01; 65k color format
  CommandWrite(0xA1); //Set Display Start Line
    CommandWrite(0);
  CommandWrite(0xA2); //Set Display Offset
    CommandWrite(0);
  CommandWrite(0xA4); //Set Display Mode (Normal)
  CommandWrite(0xA8); //Set Multiplex Ratio
    CommandWrite(0b00111111); //15-63
  CommandWrite(0xAD); //Set Master Configration
    CommandWrite(0b10001110); //a[0]=0 Select external Vcc supply, a[0]=1 Reserved(reset)
  CommandWrite(0xB0); //Power Save Mode
    CommandWrite(0b00000000); //0x1A Enable power save mode
  CommandWrite(0xB1); //Phase 1 and 2 period adjustment
    CommandWrite(0x74);
  CommandWrite(0xB3); //Display Clock DIV
    CommandWrite(0xF0);
  CommandWrite(0x8A); //Pre Charge A
    CommandWrite(0x81);
  CommandWrite(0x8B); //Pre Charge B
    CommandWrite(0x82);
  CommandWrite(0x8C); //Pre Charge C
    CommandWrite(0x83);
  CommandWrite(0xBB); //Set Pre-charge level
    CommandWrite(0x3A);
  CommandWrite(0xBE); //Set VcomH
    CommandWrite(0x3E);
  CommandWrite(0x87); //Set Master Current Control
    CommandWrite(0x06);
  CommandWrite(0x15); //Set Column Address
    CommandWrite(0);
    CommandWrite(95);
  CommandWrite(0x75); //Set Row Address
    CommandWrite(0);
    CommandWrite(63);
  CommandWrite(0x81); //Set Contrast for Color A
    CommandWrite(255);
  CommandWrite(0x82); //Set Contrast for Color B
    CommandWrite(255);
  CommandWrite(0x83); //Set Contrast for Color C
    CommandWrite(255);

  for(int j=0; j<64; j++){ //Display Black OUT
    for(int i=0; i<96; i++){
      DataWrite(0x00);
      DataWrite(0x00);
      DataWrite(0x00);
    }
  }

  CommandWrite(0xAF); //Set Display On
  delay(110); //ディスプレイONコマンドの後は最低100ms 必要
}
//*********** SPI 通信送信 ***********************************
void CommandWrite(uint8_t b){
  digitalWrite(17, LOW);
  digitalWrite(16, LOW);//DC
  SPI.write(b);
  digitalWrite(17, HIGH);
}

void DataWrite(uint8_t b){
  digitalWrite(17, LOW);
  digitalWrite(16, HIGH);//DC
  SPI.write(b);
  digitalWrite(17, HIGH);
}

void CommandWriteBytes(uint8_t *b, uint16_t n){
  digitalWrite(17, LOW);
  digitalWrite(16, LOW);//DC
  SPI.writeBytes(b, n);
  digitalWrite(17, HIGH);
}

void DataWriteBytes(uint8_t *b, uint16_t n){
  digitalWrite(17, LOW);
  digitalWrite(16, HIGH);//DC
  SPI.writeBytes(b, n);
  digitalWrite(17, HIGH);
}
//*********** ディスプレイ消去 ****************************
void Display_Clear(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1){
  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低130ns
  SPI.setDataMode(SPI_MODE3); //オシロで測ると実際はMODE3の信号だった
  delayMicroseconds(500); //クリアーコマンドは400μs 以上の休止期間が必要かも
  CommandWrite(0x25); //Clear Window
    CommandWrite(x0); //Column Address of Start
    CommandWrite(y0); //Row Address of Start
    CommandWrite(x1); //Column Address of End
    CommandWrite(y1); //Row Address of End
  delayMicroseconds(800); //ここの間隔は各自調節してください。
}
//*********** 自作フォントをSDカードから読み込む ****************
void MyFont_SD_Read(File F, uint8_t ZorH, uint8_t num, uint8_t buf[2][16]){
  F.seek(num * (16 * ZorH));
  F.read(buf[0], 16);
  F.read(buf[1], 16);
}
//*********** 時刻垂直スクロール ****************
void Time_Copy_V_Scroll(uint8_t Direction, uint8_t ZorH, uint8_t buf[2][16], uint8_t *SclCnt, uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t col_R, uint8_t col_G, uint8_t col_B){
  uint8_t Dot = (col_R << 5) | (col_G << 2) | col_B;
  uint8_t i, k;
  uint8_t bbb = 0b10000000;
  uint8_t DotDot[8 * ZorH];
  uint8_t com[6];

  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低150ns
  SPI.setDataMode(SPI_MODE3); //オシロで測ると実際はMODE3の信号だった

  switch( Direction ){
    case 0:
      Copy(x0, y0+1, x1, y1, x0, y0);

      com[0] = 0x15; //Set Column Address
      com[1] = x0; com[2] = x1;
      com[3] = 0x75; //Set Row Address
      com[4] = y1; com[5] = y1;
      break;
    case 1:
      Copy(x0, y0, x1, y1-1, x0, y0+1);

      com[0] = 0x15; //Set Column Address
      com[1] = x0; com[2] = x1;
      com[3] = 0x75; //Set Row Address
      com[4] = y0; com[5] = y0;
      break;
  }  

  CommandWriteBytes(com, sizeof(com));

  switch( Direction ){
    case 0:
      for(k=0; k<ZorH; k++){
        bbb = 0b10000000;
        for(i=0; i<8; i++){
          if(i>0) bbb = bbb >> 1;
          if(( buf[k][*SclCnt] & bbb ) > 0){
            DotDot[i + k*8] = Dot;
          }else{
            DotDot[i + k*8] = 0;
          }
        }
      }
      break;
    case 1:
      for(k=0; k<ZorH; k++){
        bbb = 0b10000000;
        for(i=0; i<8; i++){
          if(i>0) bbb = bbb >> 1;
          if(( buf[k][15 - (*SclCnt)] & bbb ) > 0){
            DotDot[i + k*8] = Dot;
          }else{
            DotDot[i + k*8] = 0;
          }
        }
      }
      break;
  }

  DataWriteBytes(DotDot, sizeof(DotDot));
  (*SclCnt)++;
}
//*********** 時刻水平スクロール **********************
void Time_Copy_H_Scroll(uint8_t Direction, uint8_t ZorH, uint8_t buf[2][16], uint8_t *SclCnt, uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t col_R, uint8_t col_G, uint8_t col_B){
  uint8_t com[6];
  uint8_t Dot = (col_R << 5) | (col_G << 2) | col_B;
  uint8_t i;
  uint8_t k = 0;
  uint8_t bbb = 0;

  SPI.setFrequency(5000000); //SSD1331 は Clock Cycle Time 最低150ns
  SPI.setDataMode(SPI_MODE3); //オシロで測ると実際はMODE3の信号だった

  switch( Direction ){
    case 0:
      Copy(x0+1, y0, x1, y1, x0, y0);

      com[0] = 0x15; //Set Column Address
      com[1] = x1; com[2] = x1;
      com[3] = 0x75; //Set Row Address
      com[4] = y0; com[5] = y1;

      if((*SclCnt) < 8){
        bbb = 0b10000000 >> (*SclCnt);
      }else if((*SclCnt) >= 8){
        bbb = 0b10000000 >> ((*SclCnt)-8);
      }

      if((*SclCnt) < 8){
        k = 0;
      }else{
        k = 1;
      }

      break;
    case 1:
      Copy(x0, y0, x1-1, y1, x0+1, y0);

      com[0] = 0x15; //Set Column Address
      com[1] = x0; com[2] = x0;
      com[3] = 0x75; //Set Row Address
      com[4] = y0; com[5] = y1;

      if((*SclCnt) < 8){
        bbb = 0b00000001 << (*SclCnt);
      }else if((*SclCnt) >= 8){
        bbb = 0b00000001 << ((*SclCnt) - 8);
      }

      if(ZorH == 2){
        if((*SclCnt) < 8){
          k = 1;
        }else{
          k = 0;
        }
      }else if(ZorH ==1){
        k = 0;
      }

      break;
  }

  CommandWriteBytes(com, 6);  

  uint8_t DotDot[16];

  for(i=0; i<16; i++){
    if((buf[k][i] & bbb) > 0){
      DotDot[i] = Dot;
    }else{
      DotDot[i] = 0;
    }
  }

  DataWriteBytes(DotDot, 16);
  (*SclCnt)++;
}
//********** SSD1331 コピーコマンド使用 範囲コピー *********************************
void Copy(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t X, uint8_t Y){
  uint8_t buf[7];
  buf[0] = 0x23; //Copy Command
  buf[1] = x0; buf[2] = y0; buf[3] = x1; buf[4] = y1; buf[5] = X; buf[6] = Y; 

  delayMicroseconds(500); //これは必要
  CommandWriteBytes(buf, sizeof(buf));
  delayMicroseconds(500); //これは必要
}
//************ SSD1331 65000色 四角 塗りつぶし描画***************************
void Drawing_Rectangle_Fill(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t Line_R, uint8_t Line_G, uint8_t Line_B, uint8_t Fill_R, uint8_t Fill_G, uint8_t Fill_B){
  //Line_R (0-31), Line_G (0-63), Line_B (0-31)
  //Fill_R (0-31), Fill_G (0-63), Fill_B (0-31)
  uint8_t lineR, lineB, fillR, fillB;
  lineR = Line_R <<1;
  lineB = Line_B <<1;
  fillR = Fill_R <<1;
  fillB = Fill_B <<1;

  CommandWrite(0x26); //Fill Enable or Disable
    CommandWrite(0b00000001); //A0=0 Fill Enable

  CommandWrite(0x22); //Drawing Rectangle
    CommandWrite(X0); //Column Address of Start
    CommandWrite(Y0); //Row Address of Start
    CommandWrite(X1); //Column Address of End
    CommandWrite(Y1); //Row Address of End
    CommandWrite(lineR);
    CommandWrite(Line_G);
    CommandWrite(lineB);
    CommandWrite(fillR);
    CommandWrite(Fill_G);
    CommandWrite(fillB);
}
//*********************** NTP Time **************************************
time_t getNtpTime(){
  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  sendNTPpacket(ntp_server_ip);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}
//*********************** NTP Time ************************************
void sendNTPpacket(IPAddress &address){
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;         
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/04/18 23:57時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥22,503(2024/04/19 09:57時点)
Excelでわかるディープラーニング超入門
技術評論社
¥2,068(2024/04/18 19:22時点)

コメント

  1. シュウ より:

    まるきり内容を理解しないまま書いていただいている内容そのままに進めたところ、
    シリアルモニターを見ると
    WiFi connected
    /font/MyFont.fnt File read OK!
    の後
    Transmit NTP Request
    No NTP Response 🙁
    となってしまうのですが、どんな原因が考えられますか?
    その場合有機ELにも数字が表示されませんか?(現在チラチラと動く何かが表示されるものの時計表示されません)

    • mgo-tec mgo-tec より:

      シュウさん

      記事をご覧いただき、ありがとうございます。

      2017年5月時点の記事ですから、今の環境では動作しません。
      WindowsのNTPサーバーのIPアドレスが変更になってしまったことが原因です。
      NTPサーバーのIPアドレスはいつか変更されてしまう可能性が大きいのです。

      よって、即席ですが、修正したコードを記事の最後に追記しました。
      そちらを参照してみてください。

タイトルとURLをコピーしました