ESP32と7セグメントLEDで簡単なNTP時計を作る

# はじめに
今回はインターネットと接続し、正確な時刻を同期できる時計を簡単に自作します。NTP時計として市販もされていますが、そこそこ値が張るようなので試しに作ってみました。ESP32を使い、Arduino開発環境でプログラミングします。
![picture-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/5b166483-c5d4-e223-8b78-060a86e9aed1.jpeg)
# 開発環境
* Windows 11 Home 22H2
* Arduino IDE 2.2.1
# ダイナミック点灯方式
例えば、4桁の7セグメントLEDなら小数点も含めて合計32個のLEDから構成されている訳ですが、個別に制御しようとすると明らかにGPIOが不足します。また、配線も煩雑になってしまいます。そこで、ダイナミック点灯方式を採用します。まず、共通ピンにより1桁だけ選択し、各セグメントを任意のパターンで点灯させます。同様に1桁だけ点灯させることを全ての桁に高速で繰り返していくとあたかも個別に制御できているように見えるのです。各セグメントの8本と各桁の4本、合計12本のGPIOで済み、消費電力も抑えることができます。
# 部品の選定
7セグメントLEDを制御するため、M54564Pというトランジスタアレイを選びました。3461BSというアノードコモンの7セグメントLEDを使うため、ソースタイプです。8回路も入っていますが、桁数に合わせて4回路だけ使います。そして、出力電流は最大500mAです。データシートよりLEDの順方向電圧は約1.8Vなので電源電圧を3.6V、電流制限抵抗を1kΩとすると、オームの法則より電流は1.8mAと求まります。1桁当たり最大8個のLEDを点灯させても14.4mAなので余裕はありそうです。また、回路はブレッドボードに組むことを考え、秋月電子通商からピッチ変換基板が入手できるESP32-WROVER-Eを選びました。開発ボードを購入するよりも安く済みます。
# 回路図
ESP32-WROVER-Eには入力専用ピンもあるので注意が必要です。最初、誤って入力専用ピンを出力ピンに設定していましたが、しっかりとエラーメッセージが表示されて教えてくれました。また、ENは常にプルアップしておく必要があります。
![schematic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/99403279-09d7-834d-2c2a-7d48a59700da.png)
ちなみに、この回路図は水魚堂のBSch3Vを使用して作成しました。KiCadのように複雑なことはできませんが、必要最低限のことなら十分できます。また、日本語に対応していて軽量、UIも洗練されている感じなのでお勧めです。[**こちら**](https://www.suigyodo.com/online/schsoft.htm)から無料でダウンロードできます。
# 書き込み
汎用的なUSBシリアル変換モジュールを使用します。IO0をGNDに落としてから電源を入れると書き込みできる状態になります。シリアルモニタにも書き込みが始まるまで待機しているというメッセージが表示されるはずです。その後は普通に書き込めます。ちなみに、転送速度の初期値は115200bpsですが、921600bpsくらいにまで引き上げると書き込みが早く終わるので快適です。IO0をGNDと切り離してから電源を入れ直すと書き込んだプログラムが動き始めます。なお、ボードの種類は`ESP32 Wrover Module`ではなく`ESP32 Dev Module`を選択しないと書き込みに失敗するみたいでした。また、理由は後述しますが、外部電源で駆動しています。
![picture-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/1007b9de-42aa-7e88-6795-d106b494aca2.jpeg)
# プログラム
Wi-Fiへ接続できたら10秒毎に時刻を取得します。15秒経過しても接続できなかった場合は再起動するようになっています。ただし、関数`getLocalTime`は初回だけNTPサーバから時刻を取得し、同期できたら2回目以降は内蔵RTCから時刻を取得する仕様になっています。また、7セグメントLEDは前述のダイナミック点灯方式により1桁当たり2msだけ点灯させることを高速で繰り返し、あたかも全ての桁が同時に点灯しているかのように見せています。各セグメントのパターンは0なら消灯、1なら点灯として8ビットで表現し、配列に格納しています。
```cpp:ntp_clock.ino
#include <WiFi.h>
#include <time.h>

const char *ssid = "{ssid}"; //put your Wi-Fi SSID
const char *password = "{password}"; //put your Wi-Fi password

const char *timeZone = "JST-9";
const char *server = "pool.ntp.org"; //"ntp.nc.u-tokyo.ac.jp"

const int digitPins[] = {26, 32, 33, 25}; //d4, d3, d2, d1
const int segmentPins[] = {12, 27, 2, 19, 13, 14, 4, 15}; //dp, g, f, e, d, c, b, a

const byte digitCodes[] = { //a, b, c, d, e, f, g, dp
  0b11111100, //0
  0b01100000, //1
  0b11011010, //2
  0b11110010, //3
  0b01100110, //4
  0b10110110, //5
  0b10111110, //6
  0b11100000, //7
  0b11111110, //8
  0b11110110 //9
};

int hour = 0;
int minute = 0;
unsigned long previousTime = 0;


void update() {
  int timeStamp = hour * 100 + minute;
  for (int i = 0; i < 4; i++) {
    int number = timeStamp % 10;
    for (int j = 0; j < 8; j++) {
      if (digitCodes[number] & (1 << j)) {
        digitalWrite(segmentPins[j], LOW);
      } else {
        digitalWrite(segmentPins[j], HIGH);
      }
    }
    timeStamp /= 10;
  
    digitalWrite(digitPins[i], HIGH);
    delay(2);
    digitalWrite(digitPins[i], LOW);
  }
}

void initialize() {
  WiFi.begin(ssid, password);
  Serial.print("Connecting");
  
  int retryCount = 0;
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    if (retryCount > 30) { //attempt to connect for 15 seconds
      Serial.println("");
      Serial.println("Failed to connect to Wi-Fi");
      Serial.println("Restarting...");
      ESP.restart();
    }

    delay(500);
    retryCount++;
  }
  Serial.println("");
  Serial.println("Connected to Wi-Fi successfully");

  configTzTime(timeZone, server);
}  

void setup() {
  Serial.begin(115200);
  Serial.println("Started");

  for (int i = 0; i < 4; i++) {
    pinMode(digitPins[i], OUTPUT);
    digitalWrite(digitPins[i], LOW);
  }

  for (int i = 0; i < 8; i++) {
    pinMode(segmentPins[i], OUTPUT);
    digitalWrite(segmentPins[i], HIGH);
  }

  initialize();
}

void loop() {
  if (millis() - previousTime > 10000) { //get the time every 10 seconds
    struct tm localTime;
    if (getLocalTime(&localTime)) {
      hour = localTime.tm_hour;
      minute = localTime.tm_min;
      Serial.printf("%02d:%02d\n", hour, minute);
    } else {
      Serial.println("Failed to get the time");
    }
    previousTime = millis();
  }

  update();
}
```
# 動作確認
前述のUSBシリアル変換モジュールはCP2012というICを搭載し、内蔵レギュレータにより3.3Vも出力できるため、最初は電源として使うことを考えていました。しかし、書き込みには成功したものの、電流が不足していたようでESP32は再起動を繰り返し、まともに動きませんでした。別電源を用意したら正常に動かすことができました。
![picture-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/bad9ab61-d9e3-b673-af63-8246684e4d3e.jpeg)
# おわりに
必要な部品数は少ないですが、ブレッドボードだと配線に少し苦労してしまいました。見た目も悪いので気が向いたら専用のプリント基板を設計していきたいと思います。またはI2C接続できる7セグメントLEDモジュールも便利かなと思います。
# 参考文献
* https://www.qoosky.io/products/3fcce30e7e
* https://www.elec-hobbyist.com/MicomMemo/Pgm_Method/Pgm_method_1.html
* https://lang-ship.com/blog/work/esp32-gpio/