🚨

Arduinoで早押しボタンを作る 【その10 LEDの点滅】

2022/12/26に公開

はじめに

目標

  1. LEDを点滅させる
  2. delay(ms)関数を使わない

1.LEDを点滅させる

これまでの早押しボタンプログラムでは解答権を表示するLEDは単に点灯させていました
このままでも特に問題はないのですが、早稲田式早押し器などは1着は点滅、2着は点灯というような点灯方法の使い分けをしています
現時点で2着判定機能等は実装していませんが、今後追加する機能としてLEDの点滅はほしいところです

また、今回の回路ではその9で示した配線図の様にD2ピンに電源表示用のLEDを接続しています
電源表示用のLEDは電源プラグやスイッチの直後などに接続して、ボードに電源が投入されたことを示すのがよくある実装方法ですが、今回の実装ではD2ピンに電源表示用のLEDを接続し、初期化のプログラムが終わったらLEDが点灯するようなプログラムになっています
せっかくLEDが制御できるのだからこのLEDはもっと有効に活用したいです
任意のパターンで点滅させることができれば、エラーコードを通知したり、解答権のあるボタンの番号を通知したりといったことはできそうな気がします

以上の理由から、LEDの点滅機能を追加することにします

2.delay(ms)関数を使わない

LEDを点滅させるプログラムは極めて簡単でArduinoの入門や動作確認としてよく用いられます
わたしも入門編として記事にしています[1]
LEDの点滅としてよく実装されているのは以下のようなコードです

int LED_PIN = 2;

void setup() {
  // put your setup code here, to run once:
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  delay(500);
}

LED_PINの点↔滅を反転させて500ms休む → LED_PINの点↔滅を反転させて500ms休む → ....
というとても簡単な繰り返しになっています

ここで問題となるのがdelay(500)の部分です
500ms休むという機能の通り500msの間すべてのプログラムがお休み状態になります
「ボタンが押された時はこういう処理をして~~」というコードがこのあとにあっても実行されるのは500ms待った後になってしまうということです[2]

これは困ります
今回のプログラムでいえば、解答権表示のために上のサンプルコードのような点滅のコードを入れてしまうと、正誤判定のボタンを押しても最大で0.5s反応しない(、最悪の場合はボタンが押されたことを検知できずスルーしてしまう)というようなことが起こってしまう可能性があるわけです

この問題を避けるためdelay(ms)関数を使わないLEDの点滅方法を実装することにします

方法

この課題を解決するのは意外に簡単です
公式のリファレンスでも実装方法が示されています
https://docs.arduino.cc/built-in-examples/digital/BlinkWithoutDelay
とても分かりやすく説明されているので読めばわかると思いますが要約すると

  • loopのたびにプログラム開始から何ms経過したか取得するcurrentMillis = millis()
  • currentMillisが前回実行した時間previousMillisからinterval以上経過していたら"処理"を実行する
  • 前回実行した時間previousMilliscurrentMillisに更新する

となります
この処理を繰り返すことでintervalごとに"処理"を行うことができます

回路図

D2ピンに抵抗とLEDを接続するだけなので、配線図は省略します

プログラム

ソースコード

const int LED_PIN = 2;
int ledState = LOW;
unsigned long previousMillis = 0;
const long INTERVAL = 1000;
void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  // プログラム開始から何ms経過したか取得する
  unsigned long currentMillis = millis();

  // 前回実行からINTERVAL以上経過しているかチェックする
  if (currentMillis - previousMillis >= INTERVAL) {
    // 現在の経過時間を前回処理を実行した時間として覚えておく
    previousMillis = currentMillis;

    // なにかやりたい処理(今回はLEDの点滅の反転)
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
    digitalWrite(LED_PIN, ledState);
  }
}

結果

delay(ms)関数を使わずにLEDを点滅させることができました

ただしこの方法もどのようなケースでも使えるわけではないので注意が必要です
たとえば

抜粋
const long INTERVAL = 1000;
void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= INTERVAL) {
    previousMillis = currentMillis;
    /* なにか処理 */
  }  
  /* 5sかかる処理 */
}

のようなケースでは"なにか処理"の部分は1sごとではなく5sごとに実行されてしまいます

どの機能を優先したいのかに応じて実装方法を検討する必要があります

補足

早押しプログラムに組み込んでみる

今回実現したいのは「解答権のあるボタンの番号回」だけ点滅してちょっと休み、「番号回」だけ点滅してちょっと休みを繰り返す機能です
長いので別ファイルでクラスを用いて実装しました。C++のクラスはなんかきらいです

StatusLedController.h
#define LED_BLINK_TIME_S 200
#define LED_BLINK_TIME_L 800

/// @brief m_ledBlinkCntMaxだけ点滅して休みを繰り返す
class StatusLedController
{
public:
  StatusLedController(int ledPin);
  void init();
  void handleEvent();
  void onLed();
  void offLed();
  void startBlink();
  void stopBlink();
  void setLedBlinkCount(int cnt);

private:
  void clear();
  int m_ledPin = -1;
  int m_ledBlinkCnt = 0;
  int m_ledBlinkCntMax = -1;
  unsigned long previousMillis = 0;
  int currentLedState = 0;
  typedef enum state
  {
    stBLINKING_BLINK = 0,
    stBLINKING_STAY,
    stON,
    stOFF,

    stNONE = -1
  } State;
  State st = stNONE;
};

StatusLedController.cpp
#include "StatusLedController.h"
#include "Arduino.h"

/// @brief コンストラクタ
/// @param ledPin 状態表示LEDのピン番号
StatusLedController::StatusLedController(int ledPin)
{
    m_ledPin = ledPin;
    pinMode(m_ledPin, OUTPUT);
    m_ledBlinkCnt = 0;
    init();
}
/// @brief 初期化
void StatusLedController::init(void)
{
    if (m_ledPin == -1)
    {
        return;
    }
    clear();
    st = stBLINKING_STAY;
}
/// @brief 点滅のカウントをクリアする
void StatusLedController::clear(void)
{
    m_ledBlinkCnt = 0;
}
/// @brief LEDを点灯する
void StatusLedController::onLed(void)
{
    // LEDを点灯する
    st = stON;
    // カウントはクリアしておく
    clear();
}
/// @brief LEDを消灯する
void StatusLedController::offLed(void)
{
    // LEDを消灯する
    st = stOFF;
    // カウントはクリアしておく
    clear();
}
/// @brief handleEvent
void StatusLedController::handleEvent(void)
{
    unsigned long currentMillis = millis();
    switch (st)
    {
    case stBLINKING_BLINK:
        if (currentMillis - previousMillis >= LED_BLINK_TIME_S)
        {
            // save the last time you blinked the LED
            previousMillis = currentMillis;

            // if the LED is off turn it on and vice-versa:
            if (currentLedState == LOW)
            {
                m_ledBlinkCnt++;
                currentLedState = HIGH;
            }
            else
            {
                currentLedState = LOW;
            }
            if (m_ledBlinkCntMax < m_ledBlinkCnt)
            {
                m_ledBlinkCnt = 0;
                st = stBLINKING_STAY;
            }
            digitalWrite(m_ledPin, currentLedState);
        }
        break;
    case stBLINKING_STAY:
        digitalWrite(m_ledPin, LOW);
        if (currentMillis - previousMillis >= LED_BLINK_TIME_L)
        {
            previousMillis = currentMillis;
            st = stBLINKING_BLINK;
        }
        break;
    case stON:
        digitalWrite(m_ledPin, HIGH);
        break;
    case stOFF:
        digitalWrite(m_ledPin, LOW);
        break;
    default:
        break;
    }
}
/// @brief 点滅させる回数を設定する
/// @param cnt 点滅させる回数
void StatusLedController::setLedBlinkCount(int cnt)
{
    m_ledBlinkCntMax = cnt + 1;
}
/// @brief 点滅をスタートする
void StatusLedController::startBlink()
{
    st = stBLINKING_BLINK;
    currentLedState = HIGH;
}
/// @brief 点滅をストップする(デフォルトのON状態にする)
void StatusLedController::stopBlink()
{
    st = stON;
}
HayaoshiButton.ino
#include "main.h"
#include "my_sound.h"
+ #include "StatusLedController.h"

int PushedButtonIdx = -1;
Event ev = evNONE;
State st = stNONE;
+ StatusLedController* ledController;

void setup()
{
  // put your setup code here, to run once:
  // Stateの初期化
  st = stTURN_ON;

  // デバッグ用出力の有効化
  Serial.begin(115200);
  while (!Serial)
  {
  }

  Serial.println("setup start");

+   // ステータス表示用LEDの初期化
+   ledController = new StatusLedController(STATUS_LED_PIN);

  // 必要なピンの割り込みを有効にする
  PCICR |= B00000111;  // [X X X X X PCMSK2 PCMSK1 PCMSK0]
  PCMSK0 |= B00000011; // PCINT0_vect[X X SCK MISO MOSI D10 D9 D8] // SCKはまともに動作しなさそう
  PCMSK1 |= B00001111; // PCINT1_vect[X RESET A5 A4 A3 A2 A1 A0]
  PCMSK2 |= B11110000; // PCINT2_vect[D7 D6 D5 D4 D3 D2 D1 D0]

  // Set all the pins of 74HC595 as OUTPUT
  pinMode(SER_74HC595, OUTPUT);
  pinMode(RCLK_74HC595, OUTPUT);
  pinMode(SRCLK_74HC595, OUTPUT);
  clearLeds();

  // 音の再生の初期設定
  initSoundOutput();  
  
  ev = evINIT_END;
}

void loop()
{
  switch (st)
  {
  case stTURN_ON:
    switch (ev)
    {
    case evINIT_END:
      ev = evNONE;
      Serial.println("setup end");
+       ledController->onLed();
      st = stSTANDBY;
      break;
    default:
      break;
    }
  case stSTANDBY:
    switch (ev)
    {
    case evBUTTON_PUSHED:
      ev = evNONE;
      // 何番が押されたかチェックする
      for (size_t i = 0; i < IOpinCnt; i++)
      {
        if (digitalRead(ButtonPins[i]) == LOW)
        {
          PushedButtonIdx = i;
          Serial.print(PushedButtonIdx);
          Serial.println(" was pushed");
          break;
        }
      }

      if (PushedButtonIdx != -1)
      {
        // 対応するLEDを点灯する
        LedON(PushedButtonIdx);
        // 音声を再生する
        playSound(sound_buzzer, sound_buzzer_len);
        // 状態表示用LEDを点滅状態にする
+         ledController->setLedBlinkCount(PushedButtonIdx);
+         ledController->startBlink();
        // 解答中状態に遷移する
        st = stANSWER;
      }
      break;
    default:
      break;
    }
    break;
  case stANSWER:
    switch (ev)
    {
    case evBUTTON_PUSHED:
      ev = evNONE;

      // 押されたのがリセットボタンかチェックする
      if (digitalRead(CORRECT_PIN) == LOW)
      {
        PushedButtonIdx = -1;
        ev = evCORRECT_PUSHED;
        break;
      }
      if (digitalRead(WRONG_PIN) == LOW)
      {
        PushedButtonIdx = -1;
        ev = evWRONG_PUSHED;
        break;
      }
      break;
    case evCORRECT_PUSHED:
      Serial.print("stANSWER");
      Serial.println(" evCORRECT_PUSHED");
      ev = evNONE;
      // LEDを消灯する
      clearLeds();
      // 音を再生する
      playSound(sound_correct, sound_correct_len);
+       // 点滅を止める
+       ledController->stopBlink();
      // ボタン待機状態に戻る
      st = stSTANDBY;
      break;
    case evWRONG_PUSHED:
      Serial.print("stANSWER");
      Serial.println(" evWRONG_PUSHED");
      ev = evNONE;
      // LEDを消灯する
      clearLeds();
      // 音を再生する
      playSound(sound_wrong, sound_wrong_len);
+       // 点滅を止める
+       ledController->stopBlink();
      // ボタン待機状態に戻る
      st = stSTANDBY;
      break;

    default:
      break;
    }
    break;
  default:
    break;
  }
+   ledController->handleEvent();
}

/* 後略 */

まとめ

本体側で解答権があるボタンを確認できるようになりました

今回のmillis()による処理はいろいろなところで活用できそうです
たとえばその7で実装した音声の再生機能でもdelay(ms)関数が使用されています
実際に音声の再生中にはLEDの変更や次のボタンの入力には反応しません
早押しボタンの機能としては「誤答音声の再生が終わったら次の解答者が押せるようになる」が正しい機能だと考えられるので問題ありませんが、この間に何か別の処理を行いたい場合はやはり今回のようなmillis()を利用した実装が必要となります

次回は2着判定機能でも追加してみましょう

脚注
  1. AE-ATMEGA-328 MINIでLチカ ↩︎

  2. delay中でも割り込みは発生するため、割り込みの関数内での処理は文字通り割り込んで可能なようです ↩︎

Discussion