ゆるく楽しむプログラミング

ATMega328P(Arduino UNO)で「学校のチャイム」をタイマー制御してみる

Seeeduino XIAOで鳴らす「学校のチャイム」は役に立っているのですが、私が使っているのは試作段階で作った基板なのでスピーカーが付いてません。
5百円強の汎用パーツ(Seeeduino XIAO)を組み込んでいることも貧乏性なもので気になって仕方ありません。
なのでATTiny85(130円)で組もうと思ったのですが…メモリーが足りませんでした (;´・ω・)
次善の策としてATMega328P-PU(240円)で組んでみました。

ATMega328P-PUはArduino UNOに使われているAVRマイコンのオシレータ内蔵型です。
オシレータを内蔵しているのでクリスタルを外付けする必要がなく扱いが簡単です。

ArduinoUNOで動作検証 スケッチ作成

メモリーに余裕があるのでOLEDに使用するフォントも色々使えますので今回はTiny4kOLEDを使用しました。
Seeeduino XIAOには存在しなかったEEPROMがArudino UNOには存在しますのでチャイムのスケジュールはEEPROMに保存します。

SeeeduinoXIAに比べればピン数も多いのでスイッチも1端子に1個を割り当てられて楽です。
基本的にはSeeeduinoXIA版と同じなのでサクッと行きます。

チェイムは3系統に増設、スピーカーも付けてみました。
電源入れっぱなしでの運用を想定しているので祭日などにチェイムをOFFに出来るようにするのでチェイムのON/OFFモニター用にLEDを付けています。



OLEDもRTCもよく使うパーツなのでもはやユニット化して化していつでも使えるようにしていたり
タクトスイッチも一々抵抗繋ぐの面倒なのでユニット化していたりするので超簡潔になっています。
出力もテストなので1系統だけで済ませてます。
【割と重要】RTCユニット使用時の注意


-- CHIME3.ino --
 
ArduinoUNOとATMega328P-PUで入力ピンの番号を変えているので
 ArduinoUNOで使用する場合は 13行目を #define ARDUINO 1 に
 ATMega328P-PUで使用する場合は #define ARDUINO 0 に変更してください。
 

#include <Wire.h>
#include <Tiny4kOLED.h>
#include <EEPROM.h>

uint8_t width = 128;
uint8_t height = 64;
uint8_t fontWidth = 8 ;
uint8_t fontHight = 2 ; // 16 ÷ 2

// レイアウトの都合で入力ピン番号を変えてるだけなので変更しなくても別に実害はない。
// 私の配線図を参考にするなら ATMega328Pで使用する場合には▼0に変更。
#define ARDUINO 0 // 1:ArduinoUNO 0:ATMega328P

#ifdef ARDUINO
// ---ArduinoUNO
#define LED_PIN 13
#define SPK_PIN 9
#define CHIME1_PIN 10
#define CHIME2_PIN 11
#define CHIME3_PIN 12

#define SWITCH_PIN 2
#define BTN0_PIN 3
#define BTN1_PIN 4
#define BTN2_PIN 5
#define BTN3_PIN 6
#define BTN4_PIN 7
#else
// ---ATMega328
#define LED_PIN 13
#define SPK_PIN 9
#define CHIME1_PIN 10
#define CHIME2_PIN 11
#define CHIME3_PIN 12

#define BTN0_PIN 0
#define BTN1_PIN 1
#define BTN2_PIN 2
#define BTN3_PIN 3
#define BTN4_PIN 4
#define SWITCH_PIN 8
#endif

// --------------
#define MODE_DISP 0
#define MODE_CLOCK 1
#define MODE_ALERM 2
#define MODE_INIT 3

// △▽△▽△▽△▽△▽△▽△▽△▽
// RTC操作系変数
// RTCのレジスタテーブル(8byte)
int RegTbl[8];
// RTC デバイスアドレス(スレーブ)
byte RTC_ADDRESS = 0x68;  // 2進数 1101000
// 状態変位用変数
uint8_t sec ;     // 秒
uint8_t mnt ;     // 分
uint8_t how ;     // 時
uint8_t weekday ; // 曜日(1~7)
uint8_t secPre ;  // 秒 - 前回取得時

// △▽△▽△▽△▽△▽△▽△▽△▽
uint8_t chimeSwitch ; // チャイムスイッチ
uint8_t nextChimeH ; // チャイム(時): 88-OFF
uint8_t nextChimeM ; // チャイム(分): 88-OFF
uint8_t nextChimeT ; // チャイムタイプ : 0xFF - チャイムデータ無効
uint8_t nextChimeD ; // Weekdayチャイムスイッチ 日-0x01 月-0x02 ~ 土-0x040
unsigned char playChimeFlag = false ; // チャイム演奏フラグ  

#define ENABLECHIME 0xFF

unsigned char mode3 ; // 作業用データ:チャイム番号
unsigned char buttonStat ;    // ボタンの状態: BTN1=0x01 BTN2=0x02 BTN3=0x10 BTN4=0x20 
unsigned char buttonStatPre ; // ボタンの状態 - 前回取得時
unsigned char btn ; // ボタンが押された 1-4
unsigned char mode1 ; // 動作モード 0:時計表示 1:時刻設定 2:チャイム設定 3:初期化
unsigned char mode2 ; // 動作状態
                      //   MODE1=1: 0:時間1 1:時間2 3:分1 4:分2
                      //   MODE1=2: 0:チャイムNo 1-7:ON/OFF 8:時間1 9:時間2 10:分1 11:分2

// 表示用Buffer
char dispStr[20] ;
char dispStr2[8] ;

// △▽△▽△▽△▽△▽△▽△▽△▽
static char Weekday3Str[][4] = {
  "Sun","Mon","Tue","Wed","Thu","Fri","Sat"
} ;

static char Weekday2Str[][3] = {
  "Su","Mo","Tu","We","Th","Fr","Sa"
} ;

#define MAXCHIMETYPE 7
#define SPKCHIMETYPE 4
static char ChimeTypeStr[][4] = {
  "OFF","Ex1","Ex2","Ex3","Sp1","Sp2","Sp3"
} ;

typedef struct chimedata_s {
  unsigned short int chimeTime;
  unsigned short int chimeType ;
} chimedata_t ;

// チャイム初期値
static char eepRomHead[] = { "SAKI01A\0" } ; 
#define MAXCHIMENUM 10
static chimedata_t chimeFlash[MAXCHIMENUM] = {
  { 845,0x013e} , {1200,0x013e} , {1300,0x013e} , {1730,0x013e} , {0,0x8000} ,
  { 0,0x8000} , {0,0x8000} , {0,0x8000} , {0,0x8000} , {0,0x8000} 
} ;

chimedata_t todayChime[MAXCHIMENUM] ;
chimedata_t chimeData[MAXCHIMENUM] ;

// △▽△▽△▽△▽△▽△▽△▽△▽
#define MAXSPKTYPE 3
typedef struct melody_s {
  unsigned short int tone;
  unsigned short int length ;
} melody_t ;

// ビックベンの鐘
static melody_t melody1[] = {
    {698,800},{880,800},{784,800},{523,2300},{0,100},
    {698,800},{784,800},{880,800},{698,2300},{0,100},
    {880,800},{698,800},{784,800},{523,2300},{0,100},
    {523,800},{784,800},{880,800},{698,2300},{0,100},
    {0,0} } ;
// ファミマの音
static melody_t melody2[] = {
    {740,300},{587,300},{440,300},{587,300},{659,300},{880,800},
    {659,300},{740,300},{659,300},{440,300},{587,300},{0,0} } ;
// キラキラ星
static melody_t melody3[] = {
    {262,450} ,{0,50}, {262,450},{0,50}, {392,450},{0,50}, {392,450},{0,50}, {440,450},{0,50}, {440,450},{0,50}, 
    {392,1000},{0,50}, {349,450},{0,50}, {349,450},{0,50}, {330,450},{0,50}, {330,450},{0,50}, {294,450},{0,50}, 
    {294,450},{0,50}, {262,500}, {0,0} } ;

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// 操作系
// △▽△▽△▽△▽△▽△▽△▽△▽
// 動作モード切り替え時の初期化処理
void initMode(int mode) {
  int wd = (1<= weekday && weekday <= 7) ? weekday - 1 : 0 ;
  switch(mode) {
    default:
    case 0: // 時刻表示
      initDisp(0);
      getTime( ) ;
      dispTime(6 , 2 , true) ;
      getNextChime() ;
      dispNextChime(6 , 3, true) ;
      break ;

    case 1: // 時刻合わせ
      initDisp(1);
      mode2 = 0 ;
      sprintf(dispStr,"%s",Weekday3Str[wd]) ;
      oled.invertOutput(true) ;
      cursor(9,2);
      oled.print(dispStr);
      oled.invertOutput(false) ;
      dispTime(6 , 3 , false) ;
      break ;

    case 2: // チャイム設定
      mode2 = 0 ;
      mode3 = 1 ;
      nextChimeH = chimeData[0].chimeTime / 100 ;
      nextChimeM = chimeData[0].chimeTime % 100 ;
      nextChimeT = (chimeData[0].chimeType & 0xff00) >> 8 ;
      nextChimeD = chimeData[0].chimeType & 0x00ff ;

      initDisp(2);
      dispChimeDays(mode3) ;
      dispChimeTime(mode3) ;
      break ;
  
    case 3: // CONFIG
      mode2 = 1 ;
      initDisp(3);
      break ;
  }
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// ボタンの状態のチェック
void checkButton( ) {
  buttonStat = 0 ;
  if (digitalRead(BTN0_PIN) == HIGH) buttonStat |= 0x01 ;
  if (digitalRead(BTN1_PIN) == HIGH) buttonStat |= 0x02 ;
  if (digitalRead(BTN2_PIN) == HIGH) buttonStat |= 0x04 ;
  if (digitalRead(BTN3_PIN) == HIGH) buttonStat |= 0x08 ;
  if (digitalRead(BTN4_PIN) == HIGH) buttonStat |= 0x10 ;

  btn = 0 ;
  if ((buttonStat & 0x10) != 0 && (buttonStatPre & 0x10) == 0) {
    btn = 5 ; 
  }
  if ((buttonStat & 0x08) != 0 && (buttonStatPre & 0x08) == 0) {
    btn = 4 ; 
  }
  if ((buttonStat & 0x04) != 0 && (buttonStatPre & 0x04) == 0) {
    btn = 3 ; 
  }
  if ((buttonStat & 0x02) != 0 && (buttonStatPre & 0x02) == 0) {
    btn = 2 ; 
  }
  if ((buttonStat & 0x01) != 0 && (buttonStatPre & 0x01) == 0) {
    btn = 1 ; 
  }
  buttonStatPre = buttonStat ;

  // Chime ON / OFF
  if (digitalRead(SWITCH_PIN) == HIGH) {
    chimeSwitch = (chimeSwitch) ? false : true ;
    if (chimeSwitch) {
      digitalWrite(LED_PIN, HIGH);
    } else {
      digitalWrite(LED_PIN, LOW);
    }
    delay(200);
  }

}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// 表示系
// △▽△▽△▽△▽△▽△▽△▽△▽
// フォント切り替え
void selectFont(int size) {
  switch(size) {
    case 1: // 小さいフォント
      oled.setFont(FONT6X8);
      fontHight = 1 ;
      fontWidth = 6 ;
      break ;
    case 2: // 大きいフォント
      oled.setFont(FONT8X16);
      fontHight = 2 ;
      fontWidth = 8 ;
      break ;
  }
}
// 画面初期表示
void initDisp(int mode) {
  oled.clear();
  switch(mode) {
    case 0: // DispTime
      cursor(1,0);
      oled.print("SCHOOL CHIME");
      cursor(1,1);
      oled.print("=============");
      cursor(1,2);
      oled.print("Time 12:00 Sun");
      cursor(1,3);
      oled.print("Next 00:00 Ex1");
      break ;

     case 1: // Set Time
      cursor(1,0);
      oled.print("SET TIME");
      cursor(1,1);
      oled.print("=============");
      cursor(1,2);
      oled.print("Weekday");
      oled.invertOutput(true) ;
      cursor(9,2);
      oled.print("Sun");
      oled.invertOutput(false) ;
      cursor(1,3);
      oled.print("Time 00:00 SET");
      break ;

     case 2: // Set Chime
      cursor(1,0);
      oled.print("SET CHIME");
      cursor(1,1);
      oled.print("No.01 SET ===");

      oled.invertOutput(true) ;
      cursor(4,1);
      oled.print("01");
      oled.invertOutput(false) ;
      selectFont(1) ;
      cursor(1,4);
      oled.print("Su Mo Tu We Th Fr Sa");
      cursor(1,5);
      oled.print("OF ON ON ON ON ON OF");
      selectFont(2) ;
      cursor(1,3);
      oled.print("TIME 00:00 Ex1");
      break ;

     case 3: // CONFIG
      cursor(1,0);
      oled.print("CONFIG");
      cursor(1,1);
      oled.print("=============");
      cursor(1,2);
      oled.print("TEST Ex1");
      oled.invertOutput(true) ;
      cursor(1,3);
      oled.print("Exit");
      oled.invertOutput(false) ;
  }
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 表示位置設定
void cursor(int x, int y) {
  int px = x * fontWidth ;
  int py = y * fontHight ;
  oled.setCursor(px,py);
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 時刻表示
void dispTime(int x ,int y , bool dispWeekday) {
  // 時刻文字列
  if (dispWeekday) {
    int wd = (1<= weekday && weekday <= 7) ? weekday - 1 : 0 ;
    sprintf(dispStr,"%02d:%02d %s",how , mnt  , Weekday3Str[wd]) ;
  } else {
    sprintf(dispStr,"%02d:%02d",how , mnt) ;
  }
  cursor(x,y);
  oled.print(dispStr);
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 次のチャイム時刻表示
void dispNextChime(int x , int y , bool dispType) {
  if (dispType) {
    if (nextChimeT == ENABLECHIME) {
      sprintf(dispStr,"--:-- OFF") ;
    } else {
      int ct = (0<= nextChimeT && nextChimeT <= MAXCHIMETYPE) ? nextChimeT : 0 ;
      sprintf(dispStr,"%02d:%02d %s",nextChimeH , nextChimeM , ChimeTypeStr[ct]) ;
    }
  } else {
    if (nextChimeT == ENABLECHIME) {
      sprintf(dispStr,"--:--") ;
    } else {
      sprintf(dispStr,"%02d:%02d",nextChimeH , nextChimeM) ;
    }
  }
  cursor(x,y);
  oled.print(dispStr);
}

// チャイム時刻表示
void dispChimeTime(int chNo) {
   chNo = (1 <= chNo && chNo <= MAXCHIMENUM) ? chNo - 1 : 0 ;

  int h = chimeData[chNo].chimeTime / 100 ;
  int m = chimeData[chNo].chimeTime % 100 ;
  int t = (chimeData[chNo].chimeType & 0xff00) >> 8 ;
  int ct = (0<= t && t <= MAXCHIMETYPE) ? t : 0 ;
   sprintf(dispStr,"%02d:%02d %s",h , m , ChimeTypeStr[ct]) ;
   cursor(6,3);
   oled.print(dispStr);
}

// チャイムスイッチ表示
void dispChimeDays(int chNo) {
  chNo = (1 <= chNo && chNo <= MAXCHIMENUM) ? chNo - 1 : 0 ;
  selectFont(1) ;
  cursor(1,4);
  oled.print("Su Mo Tu We Th Fr Sa");
  cursor(1,5);
  sprintf(dispStr,"%s %s %s %s %s %s %s",
    (((chimeData[chNo].chimeType & 0x0001) != 0) ? "ON" : "OF") , 
    (((chimeData[chNo].chimeType & 0x0002) != 0) ? "ON" : "OF") , 
    (((chimeData[chNo].chimeType & 0x0004) != 0) ? "ON" : "OF") , 
    (((chimeData[chNo].chimeType & 0x0008) != 0) ? "ON" : "OF") , 
    (((chimeData[chNo].chimeType & 0x0010) != 0) ? "ON" : "OF") , 
    (((chimeData[chNo].chimeType & 0x0020) != 0) ? "ON" : "OF") , 
    (((chimeData[chNo].chimeType & 0x0040) != 0) ? "ON" : "OF")) ;
  oled.print(dispStr);
  selectFont(2) ;
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// RTC操作系
// △▽△▽△▽△▽△▽△▽△▽△▽
// 2進化10進数(BCD)を10進数に変換
byte BCDtoDec(byte value) {
  return ((value >> 4) * 10) + (value & 0x0F) ;
}
// △▽△▽△▽△▽△▽△▽△▽△▽
// 時刻設定
void setTime(int h,int m,int wd) {
  int setH = (h/10) * 16 + (h % 10) ;
  int setM = (m/10) * 16 + (m % 10) ;
  
  // I2Cスレーブに対して送信処理を開始する
  Wire.beginTransmission(RTC_ADDRESS);
 
  // レジスタのアドレスを指定する(0-63の内、先頭から)
  Wire.write(0x00);
  
  // ---------------------------------
  //  タイマーキーパーレジスタ
  // ---------------------------------
  // 2進化10進数(BCD)で指定します。(03以外)
  
  // [00]Seconds(00秒)
  Wire.write(0x0);
  // [01]Minutes(m分)
  Wire.write(setM);
  // [02]Hours(h時)
  Wire.write(setH);

  // [03]Day(月)
  // 1:日 2:月 3:火 4:水 5:木 6:金 7:土
  Wire.write(wd);

  // [04]Date(31日)
  Wire.write(0x31);
  // [05]Month(12月)
  Wire.write(0x12);
  // [06]Years(2021年)
  Wire.write(0x21);
 
  // I2Cスレーブへの送信完了
  Wire.endTransmission();
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 時刻取得
// 4桁の10進数 hhmm
int getTime( ) {
  // レジスタのアドレスを先頭にする
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.endTransmission();
 
  // I2Cスレーブに8byteのレジスタデータを要求する
  Wire.requestFrom(RTC_ADDRESS, 8);
 
  // 8byteのデータを取得する
  int i;
  for (i = 0; i < 8; i++) {
    while (Wire.available() == 0 ) {}
    RegTbl[i] = Wire.read();
  }
 
  // 現在日時 : グローバル変数にセット
  weekday = BCDtoDec(RegTbl[3] & 0x07) ;
  how = BCDtoDec(RegTbl[2] & 0x3F) ;
  mnt = BCDtoDec(RegTbl[1] & 0x7F) ;
  sec = BCDtoDec(RegTbl[0] & 0x7F) ;

  return ( how * 10000 + mnt * 100 ) ;   // リターン値として返す
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// チャイム系
// △▽△▽△▽△▽△▽△▽△▽△▽
// チャイム時刻のディフォルト値を設定
void initChimeData( ) {
  for (int ii=0;ii= wd && wd > 1) {
     wdBit <<= (wd - 1) ;
  }

  for (int ii=0;ii> 8 ;
    } else {
      todayChime[ii].chimeTime = 0000 ;
      todayChime[ii].chimeType = ENABLECHIME ;
    }
  }
  for (int i1=0;i1 todayChime[i2].chimeTime) {
        int w = todayChime[i1].chimeTime ;
        todayChime[i1].chimeTime = todayChime[i2].chimeTime ;
        todayChime[i2].chimeTime = w ;
        w = todayChime[i1].chimeType ;
        todayChime[i1].chimeType = todayChime[i2].chimeType ;
        todayChime[i2].chimeType = w ;
      }
    }
  }
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// Nextチャイム時刻取得
void getNextChime( ) {
  int n = how * 100 + mnt ; // 現在時刻
  int nc = 0 ;
  for (int i=0;i> 8 ;
  nextChimeD = chimeData[chNo].chimeType & 0x00ff ;
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// チャイム時刻設定
void setChimeData(int chNo) {
  chimeData[chNo].chimeTime = nextChimeH * 100 + nextChimeM ;
  chimeData[chNo].chimeType = (nextChimeT << 8) | nextChimeD ;
}

// △▽△▽△▽△▽△▽△▽△▽△▽
//チャイム演奏
void internalChime(int chNo)
{
    melody_t *melody = melody1 ;
    switch(chNo) {
      case 4: melody = melody1 ; break ;
      case 5: melody = melody2 ; break ;
      case 6: melody = melody3 ; break ;
    }

    int i ;
    for (i=0;melody[i].tone != 0 || melody[i].length != 0;i++) {
      if (melody[i].tone == 0) {
        noTone(SPK_PIN);
      } else {
        tone(SPK_PIN, melody[i].tone);
      }
      delay(melody[i].length);
    }
    noTone(SPK_PIN);
}

// △▽△▽△▽△▽△▽△▽△▽△▽
void externalChime(int chNo) {
  int exPin = CHIME1_PIN ;
  switch(chNo) {
    case 1: exPin = CHIME1_PIN ; break ;
    case 2: exPin = CHIME2_PIN ; break ;
    case 3: exPin = CHIME3_PIN ; break ;
  }
  digitalWrite(exPin, HIGH); // EXTERNAL CHIME ON
  delay(200);
  digitalWrite(exPin, LOW);  // EXTERNAL CHIME OFF
}


// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// ▲▼ SETUP
// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
void setup() {
  // 入出力ピン設定
  pinMode(LED_PIN, OUTPUT);
  pinMode(SPK_PIN, OUTPUT);
  pinMode(CHIME1_PIN, OUTPUT);
  pinMode(CHIME2_PIN, OUTPUT);
  pinMode(CHIME3_PIN, OUTPUT);
  pinMode(SWITCH_PIN, INPUT);
  pinMode(BTN0_PIN, INPUT);
  pinMode(BTN1_PIN, INPUT);
  pinMode(BTN2_PIN, INPUT);
  pinMode(BTN3_PIN, INPUT);
  pinMode(BTN4_PIN, INPUT);

  chimeSwitch = true ;
  digitalWrite(LED_PIN, HIGH);
  digitalWrite(SPK_PIN, LOW);
  digitalWrite(CHIME1_PIN, LOW);
  digitalWrite(CHIME2_PIN, LOW);
  digitalWrite(CHIME3_PIN, LOW);

  // マスタとしてI2Cバスに接続する
  Wire.begin();

  // 変数初期化
  btn = 0 ;
  buttonStat = 0 ;
  buttonStatPre = 0 ;

  sec = 0 ;
  secPre = 0 ;
  mnt = 0 ;
  how = 0 ;
  weekday = 1 ;

  mode1 = 0 ;
  mode2 = 0 ;
  mode3 = 0 ;  

  // ボタン5が押されていたら:起動時にRTCを初期化
  int i ;
  for (i=0;i<10;i++) {
    if (digitalRead(BTN4_PIN) == HIGH) {
      how = 12 ;
      mnt = 0 ;
      weekday = 1 ;
      setTime(how,mnt,weekday) ;
      break ;
    }
    delay(100);
  }

  // ----- CHIME DATA -----
  // EEPROM READ TEST
  char rdata ;
  int flagOK = true ;
  int romAdr = 0 ;
  for (romAdr=0;eepRomHead[romAdr] != 0;romAdr++) {
    rdata = EEPROM.read(romAdr) ;
    if (rdata != eepRomHead[romAdr]) {
      flagOK = false ;
      break ;
    }
  }
  if (flagOK) {
    loadEEPROMChimeData(romAdr) ; 
  } else {
    initChimeData( ) ;
  }
  loadChimeByWeekday(weekday) ;
  
  // Display 初期化
  oled.begin(width, height, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  selectFont(2) ;
  initDisp(mode1);
  oled.on();
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// ▲▼ MAIN LOOP
// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
void loop() {
  // ----- Loop Process -----
  if (chimeSwitch) {
    if (how == nextChimeH && mnt == nextChimeM) {
      if (playChimeFlag == false) {
        playChimeFlag = true ;
        // チャイム実行
        switch(nextChimeT) {
          case 1: // Extern Chime 1
          case 2: // Extern Chime 2
          case 3: // Extern Chime 3
            externalChime(nextChimeT) ;
            break ;
          case 4: // Speaker Melody 1
          case 5: // Speaker Melody 2
          case 6: // Speaker Melody 3
            internalChime(nextChimeT) ;
            break ;
        }
      }
    } else {
      playChimeFlag = false ;
    }
  }

  checkButton( ) ;

  // --- DEBUG ------------------
//  oled.invertOutput(true) ;
//  sprintf(dispStr,"B:%d M:%d %d ", btn , mode2 , mode3) ;
//  cursor(1,0);
//  oled.print(dispStr);
//  oled.invertOutput(false) ;
  // ----------------------------
  
  switch(mode1) {
    // 時刻表示
    default:
    case 0: 
      mode0Loop() ;
      break ;

    // 時刻合わせ
    case 1:
      mode1Loop() ;
      break ;

    // チャイム設定
    case 2:
      mode2Loop() ;
      break ;

    // CONFIG
    case 3:
      mode3Loop() ;
      break ;
  }

  // ----- Loop Term -----
  delay(100);
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// ▲▼ MODE-0 LOOP
// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
void mode0Loop() {
  // MODE SELECT BUTTON
  if (btn == 5) {
    mode1 = 1 ; // 時刻合わせ -> チャイム設定
    loadChimeByWeekday(weekday) ;
    getNextChime() ;
    initMode(mode1) ;
    return ;
  }

  // ----- Heart Beet -----
  if  (sec != secPre) { 
    secPre = sec ;
    cursor(0,0);
    oled.print((sec % 2 == 0)? "*" : "+");
  }
  int weekdayPre = weekday ;
  int mntPre = mnt ;

  getTime() ;
  if (weekdayPre != weekday) {
    loadChimeByWeekday(weekday) ;
  }
  if (mntPre != mnt) {
    dispTime(6 , 2 , true) ;
    getNextChime() ;
    dispNextChime(6 , 3, true) ;
  }
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// ▲▼ MODE-1 LOOP
// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
void mode1Loop() {
  // mode2 : 編集位置
  if (btn != 0) {
    mode2EditDisp(mode2) ;
  }

  switch(btn) {
    case 1: // ←
      mode2 = (mode2 == 0) ? 5 : mode2 - 1;
      break ;

    case 4: // →
      mode2 = (mode2 == 5) ? 0 : mode2 + 1;
      break ;

    case 2: // ↑
      switch(mode2) {
        case 0: // WeekDay
          weekday = (weekday ==7 ) ? 1 : weekday + 1 ;
          break ;
  
        case 1: // H1
          if (how >= 20) {
            how = how % 10 ;
          } else {
            how = (how+10 > 24) ? 24 : how + 10 ;
          }
          mode2EditDisp(2) ;
          break ;

        case 2: // H2
          if (how / 20 == 2) {
            how = (how >= 4) ? 0 : how + 1 ;
          } else {
            how = (how / 10 * 10) + (how +1) % 10 ;
          }
          mode2EditDisp(1) ;
          break ;

        case 3: // M1
          mnt = (mnt / 10 == 5) ? mnt % 10 : mnt + 10 ;
          break ;

        case 4: // M2
          mnt = mnt / 10 * 10 + (mnt +1) % 10 ;
          break ;
      }
      break ;

    case 3: // ↓
      switch(mode2) {
        case 0: // WeekDay
          weekday = (weekday == 1) ? 7 : weekday - 1 ;
          break ;
  
        case 1: // H1
          if (how < 10) {
            how = 20 + how % 10 ;
            if (how > 24) how = 24 ;
          } else {
            how -= 10 ;
          }
          mode2EditDisp(2) ;
          break ;

        case 2: // H2
          if (how / 20 == 2) {
            how = (how / 10 * 10 ) + ((how % 10 > 0) ? how % 10 - 1 : 4 );
          } else {
            how = (how / 10 * 10 ) + ((how % 10 > 0) ? how % 10 - 1 : 9) ;
          }
          mode2EditDisp(1) ;
          break ;

        case 3: // M1
          mnt = (mnt / 10 > 0) ? mnt - 10  : 50 + mnt % 10 ;
          break ;

        case 4: // M2
          mnt = (mnt % 10 > 0) ? mnt - 1 : mnt /10 * 10 + 9 ;
          break ;
      }
      break ;

    case 5: // 〇
      if (mode2 == 5) {
        setTime(how,mnt,weekday) ;
        loadChimeByWeekday(weekday) ;
        mode1 = 0 ; // 時刻合わせ -> 時刻表示
      } else {
        mode1 = 2 ; // 時刻合わせ -> チャイム設定
      }
      initMode(mode1) ;
      return ;
  }

  if (btn != 0) {
    oled.invertOutput(true) ;
    mode2EditDisp(mode2) ;
    oled.invertOutput(false) ;
  }
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 編集中項目表示
void mode2EditDisp(int mode) {
  int wd = (1<= weekday && weekday <= 7) ? weekday - 1 : 0 ; // この行をSwitch内に入れると発狂する。コンパイラのバグ?
  switch(mode) {
    case 0: // WeekDay
      sprintf(dispStr,"%s",Weekday3Str[wd]) ;
      cursor(9,2);
      oled.print(dispStr);
      break ;

    case 1: // H1
      sprintf(dispStr,"%d",how / 10) ;
      cursor(6,3);
      oled.print(dispStr);
      break ;

    case 2: // H2
      sprintf(dispStr,"%d",how % 10) ;
      cursor(7,3);
      oled.print(dispStr);
      break ;

    case 3: // M1
      sprintf(dispStr,"%d",mnt / 10) ;
      cursor(9,3);
      oled.print(dispStr);
      break ;

    case 4: // M2
      sprintf(dispStr,"%d",mnt % 10) ;
      cursor(10,3);
      oled.print(dispStr);
      break ;

    case 5: // SET
      cursor(12,3);
      oled.print("SET");
      break ;
  }
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// ▲▼ MODE-2 LOOP
// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
void mode2Loop() {
  // mode2 0:チャイムNo 1-7:ON/OFF 8:時間1 9:時間2 10:分1 11:分2
  // mode3 チャイム番号

  if (btn == 5) {
    // チャイムデータ保存
    setChimeData(mode3-1) ;
    if (mode2 == 13) {
      saveEEPROMChimeData( );
    }

    // ----------
    mode1 = 3 ; // チャイム設定 -> CONFIG
    loadChimeByWeekday(weekday) ;
    getNextChime() ;
    initMode(mode1) ;
    return ;
  }

  if (btn != 0) {
    mode2EditDispCH(mode2) ;
  }

  switch(btn) {
    case 1: // ←
      mode2 = (mode2 == 0) ? 13 : mode2 - 1;
      break ;

    case 4: // →
      mode2 = (mode2 == 13) ? 0 : mode2 + 1;
      break ;

    case 2: // ↑
      switch(mode2) {
        case 0: // ChimeNo
          setChimeData(mode3-1) ;
          mode3 = (mode3 >= 10) ? 1 : mode3 + 1 ;
          
          getChimeData(mode3-1) ;
          dispChimeDays(mode3) ;
          dispChimeTime(mode3) ;
          break ;
  
        case 1: // Sun
          nextChimeD ^= 0x0001 ;
          break ;
        case 2: // Mon
          nextChimeD ^= 0x0002 ;
          break ;
        case 3: // Tue
          nextChimeD ^= 0x0004 ;
          break ;
        case 4: // Wed
          nextChimeD ^= 0x0008 ;
          break ;
        case 5: // Thr
          nextChimeD ^= 0x0010 ;
          break ;
        case 6: // Fri
          nextChimeD ^= 0x0020 ;
          break ;
        case 7: // Sat
          nextChimeD ^= 0x0040 ;
          break ;

        case 8: // H1
          if (nextChimeH >= 20) {
            nextChimeH = nextChimeH % 10 ;
          } else {
            nextChimeH = (nextChimeH+10 > 24) ? 24 : nextChimeH + 10 ;
          }
          mode2EditDispCH(9) ;
          break ;

        case 9: // H2
          if (nextChimeH / 20 == 2) {
            nextChimeH = (nextChimeH >= 4) ? 0 : nextChimeH + 1 ;
          } else {
            nextChimeH = (nextChimeH / 10 * 10) + (nextChimeH +1) % 10 ;
          }
          mode2EditDispCH(8) ;
          break ;

        case 10: // M1
          nextChimeM = (nextChimeM / 10 == 5) ? nextChimeM % 10 : nextChimeM + 10 ;
          break ;

        case 11: // M2
          nextChimeM = nextChimeM / 10 * 10 + (nextChimeM +1) % 10 ;
          break ;

        case 12: // ChimeType
          nextChimeT = (nextChimeT < MAXCHIMETYPE - 1) ? nextChimeT + 1 : 0 ;
          break ;
      }
      break ;

    case 3: // ↓
      switch(mode2) {
        case 0: // ChimeNo
          setChimeData(mode3-1) ;
          mode3 = (mode3 <= 1) ? 10 : mode3 - 1 ;
          getChimeData(mode3-1) ;
          dispChimeDays(mode3) ;
          dispChimeTime(mode3) ;
          break ;
  
          case 1: // Sun
          nextChimeD ^= 0x0001 ;
          break ;
        case 2: // Mon
          nextChimeD ^= 0x0002 ;
          break ;
        case 3: // Tue
          nextChimeD ^= 0x0004 ;
          break ;
        case 4: // Wed
          nextChimeD ^= 0x0008 ;
          break ;
        case 5: // Thr
          nextChimeD ^= 0x0010 ;
          break ;
        case 6: // Fri
          nextChimeD ^= 0x0020 ;
          break ;
        case 7: // Sat
          nextChimeD ^= 0x0040 ;
          break ;

      case 8: // H1
          if (nextChimeH < 10) {
            nextChimeH = 20 + nextChimeH % 10 ;
            if (nextChimeH > 24) nextChimeH = 24 ;
          } else {
            nextChimeH -= 10 ;
          }
          mode2EditDispCH(9) ;
          break ;

        case 9: // H2
          if (nextChimeH / 20 == 2) {
            nextChimeH = (nextChimeH / 10 * 10 ) + ((nextChimeH % 10 > 0) ? nextChimeH % 10 - 1 : 4 );
          } else {
            nextChimeH = (nextChimeH / 10 * 10 ) + ((nextChimeH % 10 > 0) ? nextChimeH % 10 - 1 : 9) ;
          }
          mode2EditDispCH(8) ;
          break ;

        case 10: // M1
          nextChimeM = (nextChimeM / 10 > 0) ? nextChimeM - 10  : 50 + nextChimeM % 10 ;
          break ;

        case 11: // M2
          nextChimeM = (nextChimeM % 10 > 0) ? nextChimeM - 1 : nextChimeM /10 * 10 + 9 ;
          break ;

        case 12: // ChimeType
          nextChimeT = (nextChimeT > 0) ? nextChimeT - 1 : MAXCHIMETYPE - 1 ;
          break ;
      }
      break ;
  }

  if (btn != 0) {
    oled.invertOutput(true) ;
    mode2EditDispCH(mode2) ;
    oled.invertOutput(false) ;
  }
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 編集中項目表示
void mode2EditDispCH(int mode) {
  // mode 0:チャイムNo 1-7:ON/OFF 8:時間1 9:時間2 10:分1 11:分2 12:ChimeType
  int wd = (1 <= mode && mode <= 7) ? mode : 1 ;
  unsigned short int wdBit = 0x00001 ;
  if (7 >= wd && wd > 1) {
     wdBit <<= (wd - 1) ;
  }
  int x = (wd - 1) * 3 + 1 ;

  switch(mode) {
    case 0: // ChimeNo
      sprintf(dispStr,"%02d",mode3) ;
      cursor(4,1);
      oled.print(dispStr);
      break ;

    case 1: // Sun
    case 2: // Mon
    case 3: // Tue
    case 4: // Wed
    case 5: // Thr
    case 6: // Fri
    case 7: // Sat
      selectFont(1) ;
      cursor(x,4); oled.print(Weekday2Str[mode-1]);
      cursor(x,5); 
      sprintf(dispStr,"%s",(((nextChimeD & wdBit) != 0) ? "ON" : "OF")) ;
      oled.print(dispStr);
      selectFont(2) ;
      break ;

    case 8: // H1
      sprintf(dispStr,"%d",nextChimeH / 10) ;
      cursor(6,3);
      oled.print(dispStr);
      break ;

    case 9: // H2
      sprintf(dispStr,"%d",nextChimeH % 10) ;
      cursor(7,3);
      oled.print(dispStr);
      break ;

    case 10: // M1
      sprintf(dispStr,"%d",nextChimeM / 10) ;
      cursor(9,3);
      oled.print(dispStr);
      break ;

    case 11: // M2
      sprintf(dispStr,"%d",nextChimeM % 10) ;
      cursor(10,3);
      oled.print(dispStr);
      break ;

    case 12: // ChimeType
      cursor(12,3);
      sprintf(dispStr,"%s",ChimeTypeStr[nextChimeT]) ;
      oled.print(dispStr);
      break ;

    case 13: // SET
      cursor(7,1);
      oled.print("SET");
      break ;
  }
}

// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
// ▲▼ MODE-3 LOOP
// ▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼
void mode3Loop() {
  // mode2 0-TEST 1-EXIT
  // mode3 Chime Type No 1-6

  if (btn == 5) {
    if (mode2 == 1) {
      mode1 = 0 ; // CONFIG -> DISP TIME
      initMode(mode1) ;
    } else
    if (mode2 == 0) {
      // MAXCHIMETYPE 7
      // SPKCHIMETYPE 4
      // TYPE : "OFF","Ex1","Ex2","Ex3","Sp1","Sp2","Sp3"
      switch(mode3) {
        case 1: // Extern Chime 1
        case 2: // Extern Chime 2
        case 3: // Extern Chime 3
          externalChime(mode3) ;
          break ;
        case 4: // Speaker Melody 1
        case 5: // Speaker Melody 2
        case 6: // Speaker Melody 3
          internalChime(mode3) ;
          break ;
      }
    }
    return ;
  }

 if (btn != 0) {
    mode3EditDispCH(mode2,mode3) ;
  }

  switch(btn) {
    case 1: // ←
      mode2 = (mode2 == 0) ? 1 : 0 ;
      break ;

    case 4: // →
      mode2 = (mode2 == 0) ? 1 : 0 ;
      break ;

    case 2: // ↑
      if (mode2 == 0) {
        mode3 = (mode3 >= 6 ) ? 1 : mode3 + 1 ;
      }
      break ;

    case 3: // ↓
      if (mode2 == 0) {
        mode3 = (mode3 <= 1) ? 6 : mode3 -1 ;
      }
      break ;
  }

  if (btn != 0) {
    oled.invertOutput(true) ;
    mode3EditDispCH(mode2,mode3) ;
    oled.invertOutput(false) ;
  }
}

// △▽△▽△▽△▽△▽△▽△▽△▽
// 編集中項目表示
void mode3EditDispCH(int mode,int testType) {
  switch(mode) {
    case 0: // TEST
      sprintf(dispStr,"%s",ChimeTypeStr[testType]) ;
      cursor(6,2);
      oled.print(dispStr);
      break ;
    case 1: // EXIT
      cursor(1,3);
      oled.print("EXIT");
      break ;
  }
}

解説動画