次回以降の開催の予定(タップ or クリックすると開くよ)

次回以降の開催予定のCoderDojo岐阜は以下の日程で参加申込と開催を行う予定です。

参加申し込みの前に必ず参加の際のルールなどをご確認ください。

参加申し込み開始2024年11月9日(土)9時〜 (開催日より1週間前日)
開催日時・場所2024年11月16日(土)
時間:9時30分〜11時30分
場所:みんなの森 ぎふメディアコスモス おどるスタジオ

今回やること(じゃんけんゲームを作る)

今回はESP32に出力手段のLEDに加えて、入力手段としてスイッチを接続します。最終的には3色のLEDと3個のスイッチを使って「じゃんけんゲーム」を作ってみます。

ESP32にスイッチを付ける

ESP32に出力手段としてLEDと、音も出すためにブザーを接続します。入力手段としてタクトスイッチを接続します。

  • IO12:LED(赤)
  • IO25:ブザー
  • IO21:タクトスイッチ

タクトスイッチを押すとIO21端子が0V(GND)に接続される回路になります。

プログラムのソースコードです。スイッチを押したらLEDが点灯してブザーが鳴ります。

#define RED_LED 12  // LED(赤)を接続するIO番号
#define BUZZER 25   // ブザーを接続するIO番号
#define IN_SW 21    // スイッチを接続するIO番号

// 初期化処理部分
void setup() {
  pinMode(RED_LED, OUTPUT); // 出力に指定
  pinMode(BUZZER, OUTPUT);  // 出力に指定
  pinMode(IN_SW, INPUT);    // 入力に指定
}

// プログラム本体(無限ループ)
void loop() {
  if (digitalRead(IN_SW)==LOW){
    // スイッチが押されていたら点灯
     digitalWrite(RED_LED,HIGH);
     digitalWrite(BUZZER,HIGH);
  }else{
    // スイッチが押されていないなら消灯
     digitalWrite(RED_LED,LOW);
     digitalWrite(BUZZER,LOW);
  }
  delay(50);  // 0.05秒待ち
}

さっそく実行してみましょう。

あれれれ・・・何か変ですね? なぜかスイッチを押していなくてもLEDが点灯してブザーが鳴ってしまいました。

プログラムバグでしょうか?それともオバケがボタンを押している(笑)

入力端子にはプルアップまたはプルダウンが必要です

実はマイコンの入力端子は内部の抵抗が高いために、何も接続されていない状態では電位が不定となっています。

(A)図のようにスイッチONのときには、Input端子は0V(GND)に接続されますので、Input端子の電位は0Vになります。

一方(B)図のスイッチOFFのときには、Input端子は何も接続されていない、いわゆる「浮いている」状態となってしまいます。

この状態ではInput端子は[HIGH][LOW]が決まらず、ノイズなどのいろいろな条件により[HIGH]になったり[LOW]になったりしてしまいます。

なので、動画のようにスイッチを押していなくともLEDが点灯してブザーが鳴ってしまったのです。

プルアップする

マイコンのInput端子に抵抗器を介してプラス電源(+3.3V)に接続することをプルアップと呼びます。

(A)図のスイッチONのときには、Input端子はスイッチで0V(GND)に接続されますので、Input端子の電位は0V=[LOW]になります。このとき抵抗器には+3.3Vの電圧が加わることになります。

(B)図のスイッチOFFのときには、Input端子は抵抗器を介してプラス電源(+3.3V)だけに接続されていますので、Input端子の電位はほぼ+3.3V=[HIGH]になります。

マイコンの内部抵抗は大きいので抵抗器に電流はほとんど流れず、抵抗器での電圧降下はごく小さいものになります。

この抵抗器にはスイッチONの状態では両端に電源電圧(+3.3V)が加わるので電流が流れます。ですので抵抗器の値が小さいと大きな電流が流れてしまいます。(抵抗器を使わずに直接接続するとショートになってしまいます。)

このプルアップ(と、この後のプルダウン)に使用する抵抗器の抵抗値は比較的大きな値のものを使用します。一般的に10kΩ程度を使うことが多いです。

計算する方法もありますが、厳密に値を決める必要は無いので気にしないでプルアップ抵抗は10kΩとしておけば構いません。

参照:プルアップとプルダウン抵抗の値ってどうやって求めるの?(株式会社マクニカHPより)

プルダウンする

マイコンのInput端子に抵抗器を介してGND(0V)に接続することをプルダウンと呼びます。

(A)図のスイッチONのときには、Input端子はスイッチでプラス電源(+3.3V)に接続されますので、Input端子の電位は+3.3V=[HIGH]になります。このとき抵抗器には+3.3Vの電圧が加わることになります。

(B)図のスイッチOFFのときには、Input端子は抵抗器を介してGND(0V)だけに接続されていますので、Input端子の電位はほぼ0V=[LOW]になります。

プルアップ・プルダウンもどちらを使用しても良いですが、一般的にはプルアップを使用することが多いです。

最初のテスト回路にプルアップ抵抗器を追加する

最初のテスト回路にプルアップ抵抗器を追加します。IO21:端子とプラス電源(+3.3V)間を10kΩの抵抗器で接続します。
それでは実行してみましょう。

ちゃんとスイッチを押している間だけ、LEDが点灯してブザーが鳴るようになりました。

プラスの知識

ESP32では、IO0、IO2、IO5、IO12、IO15端子は、起動モードの設定に使用されます。これらの端子を外部抵抗でプルアップ・プルダウンするとプログラムの書き込みがうまくできなくなることがあります。ESP32を使用していて、書き込みができなくなったときには、このことも確認してみてください。

マイコン内蔵のプルアップ・プルダウン機能を使用する

ESP32のようなマイコンでは、外付けのプルアップ・プルダウン抵抗器を付加しなくとも、マイコン内でプルアップ・プルダウンを行える機能を内蔵しています。この機能の有無はマイコンの機種によって異なります。

// 初期化処理部分
void setup() {
  pinMode(RED_LED, OUTPUT);
  pinMode(BUZZER, OUTPUT);
  pinMode(IN_SW, INPUT_PULLUP); // ←この部分をINPUTからINPUT_PULLUPに変更 
}

ESP32には内蔵プルアップ・プルダウン両方の機能のハードウェアを備えています。

内蔵プルアップを使用するには入力端子を定義しているpinMode設定で”INPUT“と設定していたところを”INPUT_PULLUP“に変更するだけです。これでタクトスイッチを接続したIO21:端子は内蔵プルアップされた入力端子となります。

このようにIO21:端子を内蔵プルアップされた入力端子とすることで、一番初めのプルアップ抵抗が使用されていないテスト回路でもスイッチを押している間だけ、LEDが点灯してブザーが鳴る正しい動作が行われるようになります。

ESP32は内蔵プルダウンの機能のハードを備えていますが、ArduinoIDEのpinMode設定ではプルダウンを指定する機能がありません。

どうしても内蔵プルダウンを使用したいときには、直接ESP32の設定を行う関数を使用します。(プルアップできればプルダウンの必要はあまり無いので、ここではその方法の説明は行いません。)

プラスの知識

ESP32のIO34からIO39端子は内部プルアップ・プルダウンは使用できません。

じゃんけんゲームを作る

LEDとスイッチ、ブザーを使用してじゃんけんゲームを作ってみましょう。スイッチ入力の端子は内蔵プルアップを使用します。ブザーのある側のLEDがマイコン側、スイッチのある側のLEDが自分側のグー(赤)・チョキ(緑)・パー(青)となります。

ブレッドボード上で回路を組んだ様子です。部品数が増えて配線もたくさんになってきました。
スイッチ入力に内部プルアップを使用しないで、外付けの抵抗器を使用して外部プルアップとするとこのようになります。
黄緑で囲った部分にプルアップ抵抗器が追加されてスイッチ周りがゴチャゴチャしています。内部プルアップ機能のありがたみがわかりますね。

じゃんけんゲームのソースコード(プログラム)

じゃんけんゲームのソースコードです。まずは、関数を使わずズラズラと書いてみました。

//===== 入出力割り付け
#define PC_RED 12  // マイコン側グーLED(赤)のIO番号
#define PC_GRN 13  // マイコン側チョキLED(緑)のIO番号
#define PC_BLU 14  // マイコン側パーLED(青)のIO番号
#define MY_RED 15  // 自分側グーLED(赤)のIO番号
#define MY_GRN 16  // 自分側チョキLED(緑)のIO番号
#define MY_BLU 17  // 自分側パーLED(青)のIO番号

#define BUZZER 25  // ブザーのIO番号

#define IN_RED 21  // グー(赤)スイッチIO番号
#define IN_GRN 22  // チョキ(緑)スイッチIO番号
#define IN_BLU 23  // パー(青)スイッチIO番号

//===== スイッチを押している状態の番号
#define NASI 0     // 何も押していない
#define GU 1       // グー(赤)スイッチを押している
#define CYOKI 2    // チョキ(緑)スイッチを押している
#define PA 3       // パー(青)スイッチを押している

//===== 自分から見ての勝敗の番号
#define AIKO 0     // あいこ
#define WIN 1      // 勝ち
#define LOSE 2     // 負け

//===== 初期化処理部分 =====
void setup() {
  //===== 出力端子の割り付け
  pinMode(PC_RED, OUTPUT);
  pinMode(PC_GRN, OUTPUT);
  pinMode(PC_BLU, OUTPUT);
  pinMode(MY_RED, OUTPUT);
  pinMode(MY_GRN, OUTPUT);
  pinMode(MY_BLU, OUTPUT);
  pinMode(BUZZER, OUTPUT);
  //===== 入力端子の割り付け(内部プルアップ)
  pinMode(IN_RED, INPUT_PULLUP);
  pinMode(IN_GRN, INPUT_PULLUP);
  pinMode(IN_BLU, INPUT_PULLUP);
}

//===== プログラム本体(無限ループ) =====
void loop() {
  int myGcp; // 自分側のグー/チョキ/パー/押していない
  int pcGcp; // マイコン側のグー/チョキ/パー
  int count; // 押されているスイッチ個数カウント用

  // スイッチを押していない間繰り返すループ
  while (true)
  {
    // スイッチの押している状態を取得
    count = 0;
    myGcp = NASI;
    if (digitalRead(IN_RED) == LOW) {
      myGcp = GU;
      count++;
    } else if ( digitalRead(IN_GRN) == LOW) {
      myGcp = CYOKI;
      count++;
    } else if ( digitalRead(IN_BLU) == LOW) {
      count++;
      myGcp = PA;
    }
    if (count > 1) myGcp = NASI;  // スイッチが2個以上同時押しは無しとする

    // マイコン側のグー・チョキ・パーをランダムで取得(1~3の数字)
    pcGcp = random(1, 4);

    // マイコン側のLEDをグー・チョキ・パーに合わせて点灯/消灯する
    if (pcGcp == GU) {
      digitalWrite(PC_RED, HIGH);
      digitalWrite(PC_GRN, LOW);
      digitalWrite(PC_BLU, LOW);

    } else if (pcGcp == CYOKI) {
      digitalWrite(PC_RED, LOW);
      digitalWrite(PC_GRN, HIGH);
      digitalWrite(PC_BLU, LOW);

    } else { // PA
      digitalWrite(PC_RED, LOW);
      digitalWrite(PC_GRN, LOW);
      digitalWrite(PC_BLU, HIGH);
    }

    if (myGcp != NASI) break; // スイッチが押さ手ていたらループを抜ける
    delay(50);
  }

  //=== 自分側のグー・チョキ・パーLEDを点灯/消灯して、勝ち負け判定をする
  int kekka = AIKO;
  if (myGcp == GU) {
    digitalWrite(MY_RED, HIGH);
    digitalWrite(MY_GRN, LOW);
    digitalWrite(MY_BLU, LOW);
    if (pcGcp == CYOKI) {
      kekka = WIN;
    } else if (pcGcp == PA) {
      kekka = LOSE;
    }

  } else if (myGcp == CYOKI) {
    digitalWrite(MY_RED, LOW);
    digitalWrite(MY_GRN, HIGH);
    digitalWrite(MY_BLU, LOW);
    if (pcGcp == PA) {
      kekka = WIN;
    } else if (pcGcp == GU) {
      kekka = LOSE;
    }

  } else { // PA
    digitalWrite(MY_RED, LOW);
    digitalWrite(MY_GRN, LOW);
    digitalWrite(MY_BLU, HIGH);
    if (pcGcp == GU) {
      kekka = WIN;
    } else if (pcGcp == CYOKI) {
      kekka = LOSE;
    }
  }

  //=== 勝負結果対してブザーを鳴らす
  if (kekka == WIN) {
    // 勝ち→ピピピピピ 
    for (int ii = 0 ; ii < 5; ii++) {
      digitalWrite(BUZZER, HIGH);
      delay(50);
      digitalWrite(BUZZER, LOW);
      delay(50);
    }
    delay(1500);
  } else if (kekka == LOSE) {
    // 負け→ピー(1秒)
    digitalWrite(BUZZER, HIGH);
    delay(1000);
    digitalWrite(BUZZER, LOW);
    delay(1000);
  } else {
    // あいこ→ピッ
    digitalWrite(BUZZER, HIGH);
    delay(50);
    digitalWrite(BUZZER, LOW);
    delay(1950);
  }

  //=== 自分側LEDを全部消灯
  digitalWrite(MY_RED, LOW);
  digitalWrite(MY_GRN, LOW);
  digitalWrite(MY_BLU, LOW);
}

マイコンに書き込んで動かしてみましょう。

グー・チョキ・パーの絵が下手なのは勘弁してくださいね。マイコンに対して勝つと「ピピピピピ」と5回ブザーが鳴ります。負けると「ピー」と1秒間連続でブザーが鳴ります。あいこだと「ピッ」と短く1回ブザーが鳴ります。

この「じゃんけんゲーム」のソースコードはArduino(=ほぼC言語)の関数という、機能をまとめてプログラムの部品とする関数というものを使用しないで書いています。プログラム本体(無限ループ)が42行から最終行までとなっていて、見通しが悪いソースコードになっています。また、同じようなコードのぐり返しとなってムダが多くなっています。

そこで、関数を使ってプログラミングするとこのようになります。

/===== 入出力割り付け
#define PC_RED 12  // マイコン側グーLED(赤)のIO番号
#define PC_GRN 13  // マイコン側チョキLED(緑)のIO番号
#define PC_BLU 14  // マイコン側パーLED(青)のIO番号
#define MY_RED 15  // 自分側グーLED(赤)のIO番号
#define MY_GRN 16  // 自分側チョキLED(緑)のIO番号
#define MY_BLU 17  // 自分側パーLED(青)のIO番号

#define BUZZER 25  // ブザーのIO番号

#define IN_RED 21  // グー(赤)スイッチIO番号
#define IN_GRN 22  // チョキ(緑)スイッチIO番号
#define IN_BLU 23  // パー(青)スイッチIO番号

//===== スイッチを押している状態の番号
#define NASI 0     // 何も押していない
#define GU 1       // グー(赤)スイッチを押している
#define CYOKI 2    // チョキ(緑)スイッチを押している
#define PA 3       // パー(青)スイッチを押している

//===== 自分から見ての勝敗の番号
#define AIKO 0     // あいこ
#define WIN 1      // 勝ち
#define LOSE 2     // 負け

//===== 自分側かマイコン側か?
#define IS_MY 0    // 自分側
#define IS_PC 1    // マイコン側

//===== 初期化処理部分 =====
void setup() {
  //===== 出力端子の割り付け
  pinMode(PC_RED, OUTPUT);
  pinMode(PC_GRN, OUTPUT);
  pinMode(PC_BLU, OUTPUT);
  pinMode(MY_RED, OUTPUT);
  pinMode(MY_GRN, OUTPUT);
  pinMode(MY_BLU, OUTPUT);
  pinMode(BUZZER, OUTPUT);
  //===== 入力端子の割り付け(内部プルアップ)
  pinMode(IN_RED, INPUT_PULLUP);
  pinMode(IN_GRN, INPUT_PULLUP);
  pinMode(IN_BLU, INPUT_PULLUP);
}

//===== プログラム本体(無限ループ) =====
void loop() {
  int myGcp; // 自分側のグー/チョキ/パー/押していない
  int pcGcp; // マイコン側のグー/チョキ/パー

  // スイッチを押していない間繰り返すループ
  while (true)
  {
    // スイッチの押している状態を取得
    myGcp = GetNowSwtichStatus();

    // マイコン側のグー・チョキ・パーをランダムで取得(1~3の数字)
    pcGcp = random(1, 4);

    // マイコン側のLEDをグー・チョキ・パーに合わせて点灯/消灯する
    SetLedGuCyokiPa(IS_PC, pcGcp);

    if (myGcp != NASI) break; // スイッチが押さ手ていたらループを抜ける
    delay(50);
  }

  // 自分側のグー・チョキ・パーLEDを点灯/消灯
  SetLedGuCyokiPa(IS_MY, myGcp);

  //じゃんけん結果判定
  int kekka = JyankenJudge(myGcp, pcGcp);

  // 勝負結果対してブザーを鳴らす
  SoundTheBbuzzer(kekka);

  // 自分側LEDを全部消灯
  SetLedGuCyokiPa(IS_MY, NASI);
}

//===== 以下は関数 =====

// 機能;現在のスイッチの押している状態を取得する
// 引数:なし
// 戻値:スイッチの状態
int GetNowSwtichStatus()
{
  int count = 0; // 押されているスイッチ個数カウント用
  int input = NASI;
  if (digitalRead(IN_RED) == LOW) {
    input = GU;
    count++;
  } else if ( digitalRead(IN_GRN) == LOW) {
    input = CYOKI;
    count++;
  } else if ( digitalRead(IN_BLU) == LOW) {
    input = PA;
    count++;
  }
  // スイッチが2個以上同時押しは無しとする
  if (count > 1) input = NASI;
  return (input);
}

// 機能;グー(赤)、チョキ(緑)、パー(青)のLED点灯パターンする
// 引数:myPc 自分側/マイコン側指定
//      guChokiPa グー/チョキ/パー/全消灯
// 戻値:なし
void SetLedGuCyokiPa(int myPc, int guCyokiPa)
{
  // 自分側なのかマイコン側なのかで各色のLEDのIO番号を変数にセット
  // 右辺:(条件) ? [条件が真のときの値] : [条件が偽のときの値]
  int ioRed = (myPc == IS_MY) ? MY_RED : PC_RED;
  int ioGrn = (myPc == IS_MY) ? MY_GRN : PC_GRN;
  int ioBlu = (myPc == IS_MY) ? MY_BLU : PC_BLU;

  // グー・チョキ・パーのLED点灯・消灯パターンセット
  if (guCyokiPa == GU) {
    digitalWrite(ioRed, HIGH);
    digitalWrite(ioGrn, LOW);
    digitalWrite(ioBlu, LOW);

  } else if (guCyokiPa == CYOKI) {
    digitalWrite(ioRed, LOW);
    digitalWrite(ioGrn, HIGH);
    digitalWrite(ioBlu, LOW);

  } else if (guCyokiPa == PA) {
    digitalWrite(ioRed, LOW);
    digitalWrite(ioGrn, LOW);
    digitalWrite(ioBlu, HIGH);

  } else {  // NASI(全消灯)
    digitalWrite(ioRed, LOW);
    digitalWrite(ioGrn, LOW);
    digitalWrite(ioBlu, LOW);
  }
}

// 機能;じゃんけんの勝ち負け判定をする
// 引数:myGcp 自分側のグー/チョキ/パー
//      pcGcp マイコン側のグー/チョキ/パー
// 戻値:自分側から見た勝ち負け
int JyankenJudge(int myGcp, int pcGcp)
{
  int kekka = AIKO;
  if (myGcp == GU) {
    // 自分側グー
    if (pcGcp == CYOKI) {
      kekka = WIN;
    } else if (pcGcp == PA) {
      kekka = LOSE;
    }

  } else if (myGcp == CYOKI) {
    // 自分側チョキ
    if (pcGcp == PA) {
      kekka = WIN;
    } else if (pcGcp == GU) {
      kekka = LOSE;
    }

  } else {
    // 自分側パー
    if (pcGcp == GU) {
      kekka = WIN;
    } else if (pcGcp == CYOKI) {
      kekka = LOSE;
    }
  }
  return (kekka);
}

// 機能;じゃんけん結果に応じたパターンでブザーを鳴らす
// 引数:kekka じゃんけんの結果
// 戻値:なし
void SoundTheBbuzzer(int kekka)
{
  if (kekka == WIN) {
    // 勝ち→ピピピピピ 
    for (int ii = 0 ; ii < 5; ii++) {
      digitalWrite(BUZZER, HIGH);
      delay(50);
      digitalWrite(BUZZER, LOW);
      delay(50);
    }
    delay(1500);

  } else if (kekka == LOSE) {
    // 負け→ピー(1秒)
    digitalWrite(BUZZER, HIGH);
    delay(1000);
    digitalWrite(BUZZER, LOW);
    delay(1000);

  } else {
    // あいこ→ピッ
    digitalWrite(BUZZER, HIGH);
    delay(50);
    digitalWrite(BUZZER, LOW);
    delay(1950);
  }
}

関数を使ったこのソースコードでは、プログラム本体(無限ループ)が46~78行とコンパクトになっています。機能ごとに関数にまとめてあり、順に呼び出しているため見通しが良くなっています。

最後に

今回はハードウェア(回路)の説明がメインとなっており、プログラミングについての説明がありませんでした。
次回に、このじゃんけんプログラミング内容の説明を予定しています。

今回はソースコードをこのままコピー&ペーストして試してみてください。

LINEでCoderDojo岐阜について質問・相談ができます!