ESP32

ESP32とLCDモジュールで時計を作ってみます

需要は全然ないと思いますが、ESP32の内部時計を液晶に表示して、ボタンをポチポチと操作することで、日時変更できるようにしてみます。

また、ボタンを6個使用して、かなり無意味ですが「コナミコマンド」の入力にもチャレンジしていきたいと思います。

ESP32、LCDモジュール、ボタンを接続

準備するものは、以下になります。

  • ESP32-DevKitC
  • ブレッドボード
  • タクトスイッチ6個
  • LCDモジュール
  • ジャンパーワイヤー(ブレッドボードに配線できるもの)
  • USBケーブル

以下のように接続を行います。

内部時計設定 プログラム作成

Arduino IDEを使用して、内部時計を設定するプログラムを作成していきます。

処理概要

ちょっとだけプログラムが長くなってしまったので、今回行う処理を箇条書きで以下に記載いたします。

  • ESP32の内部時計をLCDモジュールに表示する。
  • LCDモジュールの上の段に日付、下の段に時間を表示する。
  • 6個のボタン制御を行う(上,下,左,右,B,A)。
  • Aボタンを長押しすることで、時間変更モードになる。
  • 時間変更モードでは時計表示の更新は行わず、変更する箇所に点滅カーソルを表示させる。
  • 左右ボタン押下で、変更項目の年、月、日、時、分、秒を選択する。
  • 上下ボタン押下で、値を変更する(上は+、下は-)。
  • 時間変更モードで、Aボタンを押すと変更した日時を内部時計に反映する。
  • 時間変更モードで、Bボタンを押すと変更した日時をキャンセルする。
  • コナミコマンド(上,上,下,下,左,右,左,右,B,A)を押すとLCDモジュールのバックライトが点滅する。

※時計設定の入力チェックは行っていません。

プログラム作成

処理概要に従い、プログラムを作成しました。

なんとか機能を満たすことができなしたが、もうちょっとまとめられたかもしれません。

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <sys/time.h>

const unsigned int ADDRESS = 0x27;
const int CHARS_NUM = 16;
const int LINES_NUM = 2;

const int BTN1 = 14;  // ボタン1入力ピン
const int BTN2 = 15;  // ボタン2入力ピン
const int BTN3 = 16;  // ボタン3入力ピン
const int BTN4 = 17;  // ボタン4入力ピン
const int BTN5 = 18;  // ボタン5入力ピン
const int BTN6 = 19;  // ボタン6入力ピン

const char BTN_UP = 0x01;     // ボタン上
const char BTN_DOWN = 0x02;   // ボタン下
const char BTN_LEFT = 0x04;   // ボタン左
const char BTN_RIGHT = 0x08;  // ボタン右
const char BTN_B = 0x10;      // ボタンB
const char BTN_A = 0x20;      // ボタンA

const int NUM_X = 3;
const int NUM_Y = 2;
const int NUM_CMD = 10;

unsigned char lastBtnSt = 0; // 前回ボタン状態
unsigned char fixedBtnSt = 0; // 確定ボタン状態
int longflg = 0;  // ボタン長押し
unsigned long longtmr = 0;  // ボタン長押しタイマ

unsigned long smpltmr = 0;  // サンプル時間
unsigned long disptmr = 0;  // 表示時間

int md = 0; // モード(0:ノーマル 1:時計設定)
int posx = 0; // LCDカーソル位置(X方向)
int posy = 0; // LCDカーソル位置(y方向)
struct tm *timeSetting;     // 設定日時

static int lcdBlinkCnt = 0; // LCD点滅用カウント
static unsigned long blinktmr = 0; // LCD点滅用タイマ

LiquidCrystal_I2C lcd(ADDRESS, CHARS_NUM, LINES_NUM);

void setup() {
  lcd.init();
  lcd.backlight();

  pinMode(BTN1, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN2, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN3, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN4, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN5, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN6, INPUT_PULLUP);  // プルアップ設定

  SetClock(2021, 3, 4, 20, 53, 05);

  disptmr = millis();
}

void loop() {
  btnEvent();  // ボタンイベント

  if(millis() - disptmr > 100){
    if(md == 0){
      struct tm *timeInfo;
      time_t tm = time(NULL);
      timeInfo = localtime(&tm);
      DispTime(timeInfo);
    }    
    disptmr = millis();
  }
}

// LCDモジュールカーソル位置
static unsigned char Tbl_setTime[NUM_Y][NUM_X]={
  { 3, 6, 9 }, // 日付カーソル位置
  { 1, 4, 7 }  // 時間カーソル位置
};

// コマンドテーブル(上上下下左右左右BA)
static const unsigned char Tbl_cmd[NUM_CMD] = {
  BTN_UP, BTN_UP, BTN_DOWN, BTN_DOWN, BTN_LEFT, 
  BTN_RIGHT, BTN_LEFT, BTN_RIGHT, BTN_B, BTN_A
};

// ボタン処理イベント
void btnEvent() {
  if(millis() - smpltmr < 40) return;
  smpltmr = millis();
  
  int btnSt = btnSample();
  int cmp = (btnSt == lastBtnSt);
  lastBtnSt = btnSt;

  if(!cmp) return;

  if(btnSt && (btnSt != fixedBtnSt)){
    fixedBtnSt = btnSt;

    if(md){
      btnSettingTime(btnSt);
    }else{
      if((btnSt & BTN_A)){
        longflg = 1;
        longtmr = millis();
      }

      CheckCmd(btnSt);
    }
  }else{
    if(longflg && millis() - longtmr > 2000){
      longflg = 0;
      md = 1;  // 時計設定モード
      time_t tm = time(NULL);
      timeSetting = localtime(&tm);
      DispTime(timeSetting);
      posx = 0;
      posy = 0;
      lcd.setCursor(Tbl_setTime[posy][posx], posy);
      lcd.blink();
    }
  }

  if(!btnSt){
    fixedBtnSt = btnSt;
    longflg = 0;
  }

  BlinkLcd();
}

// ボタンサンプリング
unsigned char btnSample() {
  unsigned char st = 0x00;

  st = digitalRead(BTN1) & 0x01;            // 上
  st |= ((digitalRead(BTN2) & 0x01) << 1);  // 下
  st |= ((digitalRead(BTN3) & 0x01) << 2);  // 左
  st |= ((digitalRead(BTN4) & 0x01) << 3);  // 右
  st |= ((digitalRead(BTN5) & 0x01) << 4);  // B
  st |= ((digitalRead(BTN6) & 0x01) << 5);  // A

  return (~st & 0x3F);
}

// 時計設定ボタン処理
void btnSettingTime(unsigned char btnSt) {
  if(btnSt & BTN_B){
    lcd.noBlink();
    md = 0;
  }

  if(btnSt & BTN_A){
    lcd.noBlink();
    SetTime(*timeSetting);
    md = 0;
  }
  
  if(btnSt & (BTN_LEFT | BTN_RIGHT)){
    if(btnSt & BTN_LEFT){
      posx--;
    }else if(btnSt & BTN_RIGHT){
      posx++;
    }
    if(posx < 0 && posy == 0){
      posx = 0;
    }else if(posx >= NUM_X && posy == 0){
      posx = 0;
      posy = 1;
    }else if(posx < 0 && posy == 1){
      posx = NUM_X - 1;
      posy = 0;
    }else if(posx >= NUM_X && posy == 1){
      posx = NUM_X - 1;
    }
    lcd.setCursor(Tbl_setTime[posy][posx], posy);
  }

  if(btnSt & (BTN_UP | BTN_DOWN)){
    if(posy == 0){
      switch(posx){
      case 0: // 年
        SetValue(btnSt, &timeSetting->tm_year, 1);
        break;
      case 1: // 月
        SetValue(btnSt, &timeSetting->tm_mon, 0);
        break;
      case 2: // 日
        SetValue(btnSt, &timeSetting->tm_mday, 1);
        break;
      default:
        break;
      }
    }else{
      switch(posx){
      case 0: // 時
        SetValue(btnSt, &timeSetting->tm_hour, 0);
        break;
      case 1: // 分
        SetValue(btnSt, &timeSetting->tm_min, 0);
        break;
      case 2: // 秒
        SetValue(btnSt, &timeSetting->tm_sec, 0);
        break;
      default:
        break;
      }
    }
    DispTime(timeSetting);
    lcd.setCursor(Tbl_setTime[posy][posx], posy);
  }
}

// 値設定
void SetValue(int st, int *num, int min) {
  if(st & BTN_UP){
    (*num)++;
  }else{
    DecrimentNum(num, min);
  }
}

// 数値デクリメント処理
void DecrimentNum(int *num, int min) {
  (*num)--;
  if(*num < min){
    *num = min;
  }
}

// LCD時計表示
void DispTime(struct tm *timeInfo) {
  // LCD 日付表示
  lcd.setCursor(0, 0);
  char dateBuf[16];
  sprintf(dateBuf,
          "%04d/%02d/%02d", 
          timeInfo->tm_year + 1900,
          timeInfo->tm_mon + 1,
          timeInfo->tm_mday);
  lcd.print(dateBuf);
  
  // LCD 時間表示
  lcd.setCursor(0, 1);
  char timeBuf[16];
  sprintf(dateBuf,
          "%02d:%02d:%02d", 
          timeInfo->tm_hour,
          timeInfo->tm_min,
          timeInfo->tm_sec);
  lcd.print(dateBuf);
}

// 内部タイマ設定
void SetTime(struct tm timeInfo) {
  int now_sec = mktime(&timeInfo);
  timeval tmv = {now_sec, 0};
  const timeval *tv = &tmv;
  timezone utc = {0,0};
  const timezone *tz = &utc;
  settimeofday(tv, tz);
}

// 時計設定
void SetClock(int year, int mon, int day, int hour, int minute, int sec) {
  struct tm timeInfo;
  timeInfo.tm_year = year - 1900;   // 年
  timeInfo.tm_mon = mon - 1;        // 月
  timeInfo.tm_mday = day;           // 日
  timeInfo.tm_hour = hour;          // 時
  timeInfo.tm_min = minute;         // 分
  timeInfo.tm_sec = sec;            // 秒
  timeInfo.tm_wday = 0;             // 曜日
  timeInfo.tm_yday = 0;             // 通算日
  timeInfo.tm_isdst = 0;            // 夏時間

  SetTime(timeInfo);
}

// コマンド処理
void CheckCmd(unsigned char btnSt)
{
  static int pos = 0;

  if(!(btnSt & Tbl_cmd[pos++])){
    pos = 0;
  }

  if(pos == NUM_CMD){
    lcdBlinkCnt = 1;
    pos = 0;
  }
}

// LCD点滅処理
void BlinkLcd() {
  if(lcdBlinkCnt > 0 && (millis() - blinktmr > 300)){
    if(lcdBlinkCnt % 2) {
      lcd.noBacklight();
    }else{
      lcd.backlight();
    }
    if(++lcdBlinkCnt > 6){
      lcdBlinkCnt = 0;
    }
    blinktmr = millis();
  }
}

プログラムについて、抜き出しながら簡単に説明していきたいと思います。

最初に「setup()」関数で、ボタン設定とESP32の内部時計の時間設定を行っています。

void setup() {
  lcd.init();
  lcd.backlight();

  pinMode(BTN1, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN2, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN3, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN4, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN5, INPUT_PULLUP);  // プルアップ設定
  pinMode(BTN6, INPUT_PULLUP);  // プルアップ設定

  SetClock(2021, 3, 4, 20, 53, 05);

  disptmr = millis();
}

45~54行目で、今回使用する6個のボタン(上、下、左、右、B、A)を設定しています。

56行目の「SetClock()」関数で、2021年3月4日20時52分05秒に時間を設定しています。日時は適当です。

「loop()」関数では、ボタンイベントの監視とLCDモジュールの日時表示を行っています。

void loop() {
  btnEvent();  // ボタンイベント

  if(millis() - disptmr > 100){
    if(md == 0){
      struct tm *timeInfo;
      time_t tm = time(NULL);
      timeInfo = localtime(&tm);
      DispTime(timeInfo);
    }    
    disptmr = millis();
  }
}

66~68行目の処理で内部時計を取得して、69行目の「DispTime()」関数でLCDモジュールに表示しています。

「DispTime()」関数では、引数に与えられた「struct tm」構造体の設定値を「sprintf()」で整形(YYYY/MM/dd HH:mm:ss)して表示しています。

「strunct tm」構造体は、「time.h」で以下のように定義されています。

struct tm {
  int tm_sec;	// 秒
  int tm_min;   // 分
  int tm_hour;  // 時
  int tm_mday;  // 日
  int tm_mon;   // 月
  int tm_year;  // 年
  int tm_wday;  // 曜日
  int tm_yday;  // 通算日
  int tm_isdst; // 夏時間
};

「btnEvent()」関数では、6つのボタンの入力を監視して、ボタンによるイベントの処理を行っています。

6つのボタン入力は、処理を行いやすいように「btnSample()」関数内で1バイトにまとめています。

// ボタンサンプリング
unsigned char btnSample() {
  unsigned char st = 0x00;

  st = digitalRead(BTN1) & 0x01;            // 上
  st |= ((digitalRead(BTN2) & 0x01) << 1);  // 下
  st |= ((digitalRead(BTN3) & 0x01) << 2);  // 左
  st |= ((digitalRead(BTN4) & 0x01) << 3);  // 右
  st |= ((digitalRead(BTN5) & 0x01) << 4);  // B
  st |= ((digitalRead(BTN6) & 0x01) << 5);  // A

  return (~st & 0x3F);
}

137~142行目で「digitalRead()」を使用して、各ボタンの値を取得します。取得した値は、1ビットづつ左にシフトして、135行目の変数「st」に設定しています。

取得した値は、setup()関数でプルアップ設定しているため、ONの時に「0」、OFFの時に「1」となっているため、戻り値を反転しています。「0x3F」でANDをとっているのは、1~6ビット目以外の値をマスクするためです(0x3F → 0011 1111)。

「btnEvent()」関数の処理について、簡単に説明いたします。

この処理は、「ESP32でボタン制御」で作成したプログラムを改造した関数になります。

// ボタン処理イベント
void btnEvent() {
  if(millis() - smpltmr < 40) return;
  smpltmr = millis();
  
  int btnSt = btnSample();
  int cmp = (btnSt == lastBtnSt);
  lastBtnSt = btnSt;

  if(!cmp) return;

  if(btnSt && (btnSt != fixedBtnSt)){
    fixedBtnSt = btnSt;

    if(md){
      btnSettingTime(btnSt);
    }else{
      if((btnSt & BTN_A)){
        longflg = 1;
        longtmr = millis();
      }

      CheckCmd(btnSt);
    }
  }else{
    if(longflg && millis() - longtmr > 2000){
      longflg = 0;
      md = 1;  // 時計設定モード
      time_t tm = time(NULL);
      timeSetting = localtime(&tm);
      DispTime(timeSetting);
      posx = 0;
      posy = 0;
      lcd.setCursor(Tbl_setTime[posy][posx], posy);
      lcd.blink();
    }
  }

  if(!btnSt){
    fixedBtnSt = btnSt;
    longflg = 0;
  }

  BlinkLcd();
}

92行目で、6つのボタンの押下状態を取得しています。基本的には、取得した「btnSt」の値を使って処理を行っています。

今回のプログラムでは、時計表示モード(通常)と時計設定モードの2つの処理を行っています。

このモードは、「Aボタン」の操作で切り替えることになります。

  • 時計表示モードの場合に、Aボタンを2秒以上長押しすることで、時計設定モードに切り替え。
  • 時計設定モードの場合に、Aボタン、または、Bボタンを押すことで、時計表示モードに切り替え。

104行目で「Aボタン」が押されていることを確認して、105行目で、長押しフラグ「longflg」を1、106行目で長押し開始時間を設定しています。

112行目で、長押し開始から2秒以上経過していることを確認して、114行目で「md = 1」を設定して、時計設定モードに切り替えています。「md」が0で時計表示、1で時計設定になります。

115~117行目で、時計設定モードになった日時を画面に表示して、120~121行目で、LCDモジュールに変更項目を指定するためのカーソルを表示しています。

LCDモジュール上のカーソル移動や、設定値変更の処理は、「btnSettingTime()」関数で行っています。

カーソル移動は、160~178行目で行っています。

  if(btnSt & (BTN_LEFT | BTN_RIGHT)){
    if(btnSt & BTN_LEFT){
      posx--;
    }else if(btnSt & BTN_RIGHT){
      posx++;
    }
    if(posx < 0 && posy == 0){
      posx = 0;
    }else if(posx >= NUM_X && posy == 0){
      posx = 0;
      posy = 1;
    }else if(posx < 0 && posy == 1){
      posx = NUM_X - 1;
      posy = 0;
    }else if(posx >= NUM_X && posy == 1){
      posx = NUM_X - 1;
    }
    lcd.setCursor(Tbl_setTime[posy][posx], posy);
  }

「posx」がLCDの横軸、「posy」がLCDの縦軸の位置を表しています。左ボタンを押すとマイナス方向、右ボタンを押すとプラス方向に移動します。1行目の右端で右ボタンを押すと2行目の左端にカーソルが移動して、2行目の左端で左ボタンを押すと1行目の右端にカーソルが移動します。

カーソルが移動する位置は、LCDに表示されている「年、月、日、時、分、秒」の下一桁の表示位置になります。

カーソルの移動位置は、75行目の「Tbl_setTime」に設定されています。

// LCDモジュールカーソル位置
static unsigned char Tbl_setTime[NUM_Y][NUM_X]={
  { 3, 6, 9 }, // 日付カーソル位置
  { 1, 4, 7 }  // 時間カーソル位置
};

年の場合は1行目の「3」、月は「6」、日は「9」といった感じになります。

カーソル位置の値変更は、180~212行目で行っています。

  if(btnSt & (BTN_UP | BTN_DOWN)){
    if(posy == 0){
      switch(posx){
      case 0: // 年
        SetValue(btnSt, &timeSetting->tm_year, 1);
        break;
      case 1: // 月
        SetValue(btnSt, &timeSetting->tm_mon, 0);
        break;
      case 2: // 日
        SetValue(btnSt, &timeSetting->tm_mday, 1);
        break;
      default:
        break;
      }
    }else{
      switch(posx){
      case 0: // 時
        SetValue(btnSt, &timeSetting->tm_hour, 0);
        break;
      case 1: // 分
        SetValue(btnSt, &timeSetting->tm_min, 0);
        break;
      case 2: // 秒
        SetValue(btnSt, &timeSetting->tm_sec, 0);
        break;
      default:
        break;
      }
    }
    DispTime(timeSetting);
    lcd.setCursor(Tbl_setTime[posy][posx], posy);
  }

上ボタンを押すと表示されている値に「+1」、下ボタンを押すと「-1」します。

時計設定モードを終了するための処理は、149~158行目で行っています。

149~151行目の「Bボタン」押下時の処理で、カーソル表示を終了して、時計表示モードに切り替えています。

  if(btnSt & BTN_B){
    lcd.noBlink();
    md = 0;
  }

154~158行目の「Aボタン」押下時の処理で、カーソル表示を終了、現在LCDモジュールに表示されている日時を内部時計に設定して、時計表示モードに切り替えています。

  if(btnSt & BTN_A){
    lcd.noBlink();
    SetTime(*timeSetting);
    md = 0;
  }

時計設定処理は、以上になります。最後にコナミコマンドの処理を説明していきます。といっても 単純に順番にボタンが押されているかの確認になります。

コマンド処理は、281~294行目の「CheckCmd()」関数で行っています。

// コマンド処理
void CheckCmd(unsigned char btnSt)
{
  static int pos = 0;

  if(!(btnSt & Tbl_cmd[pos++])){
    pos = 0;
  }

  if(pos == NUM_CMD){
    lcdBlinkCnt = 1;
    pos = 0;
  }
}

81~85行目の「Tbl_cmd」テーブルで定義されている値と押されたボタンを比較しています。

// コマンドテーブル(上上下下左右左右BA)
static const unsigned char Tbl_cmd[NUM_CMD] = {
  BTN_UP, BTN_UP, BTN_DOWN, BTN_DOWN, BTN_LEFT, 
  BTN_RIGHT, BTN_LEFT, BTN_RIGHT, BTN_B, BTN_A
};

押したボタンとテーブル設定値を比較していき、テーブルの値10個 全て一致した場合は、LCDモジュールを点滅させています。1回でも不一致になった場合は、また最初から確認になります。

まとめ

ESP32の内部時計をLCDモジュールに表示して、ボタンで日時を設定してみました。

今回行った内容は、電池で動作する時計を作成するときぐらいには使えそうな気がします。

プログラムとしては、ちょっと長くなってしまい、もう少しなんとかなりそうな気はしています。

とりあえず今回はコナミコマンドの制御ができたので、よしとしておきます。

【参考図書】

ESP32&Arduino 電子工作 プログラミング入門 [ 藤本壱 ] C言語ポインタ完全制覇 (新・標準プログラマーズライブラリ) [ 前橋和弥 ]