Arduino Microでスイッチ状態取得

## 導入

タクトスイッチ(押しボタンスイッチ)のON/OFFで回路を制御できるように、マイコンでタクトスイッチの状態を読み取れるようにする。

## 基礎知識

マイコンのデジタルI/Oピンを入力に設定すればピンにかかる電圧がHIGHかLOWかを判断することができる。これを利用して、タクトスイッチがONの時とOFFの時とでピンにかかる電圧のHIGHとLOWが変わるように回路を組めばタクトスイッチのONとOFFを判定できる。

### タクトスイッチ

タクトスイッチは、接点がスイッチの上側と下側にくるような向き(下図左)になるように置いた場合、左側の接点同士、右側の接点同士は常に接続されていて、スイッチを押している間は左側と右側とが接続される(下図右)。

![switch.png](https://qiita-image-store.s3.amazonaws.com/0/28644/affa5e5e-fbca-1c87-8a8d-57c64043795e.png) ![switch_circuit.png](https://qiita-image-store.s3.amazonaws.com/0/28644/5e95eea8-8dac-20d9-2d2f-7d3184385afb.png)

### チャタリング

タクトスイッチの場合、スイッチを押した直後やスイッチを離した直後の短い時間にONとOFFが繰り返される[チャタリング](https://ja.wikipedia.org/wiki/%E3%83%81%E3%83%A3%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0)が発生する。マイコンの処理速度は十分速いため、単純にデジタル入力ピンで電圧を取得し続けるとチャタリングが起きている間のON/OFFの変化も取得されてしまい、実際にスイッチを押した回数以上にスイッチを押したことになってしまう。
このチャタリングの対策としては、多少遅延は発生するもののソフトウェアで対策する方法のほか、遅延が許されない場合は回路に手を加えてハードウェアで対策する方法があるようだ。ここでは、回路がシンプルになるようにソフトウェアで対策することにした。

### フローティング

下図のように単純に5Vピン→スイッチ→入力ピンと接続すれば、スイッチを押している間は5Vピンと接続されるのでHIGHとなってスイッチの状態を読み取れそうだが、これだとスイッチを押していない間は入力ピンに電圧がかかっておらず不安定な状態になる。この状態を浮いている(フローティング)と呼ぶ。
スイッチを押している間5Vピンから抵抗を介さず直接入力ピンに接続されるのでショートしそうだが、汎用I/Oピン(GPIO)を入力に設定するとハイ・インピーダンス(高抵抗)となりわずかな電流(リーク電流)しか流れないので問題ない。なお、高信頼性システムだと内部劣化によるショートを考慮して抵抗を挟むこともあるようだ。

![floating.png](https://qiita-image-store.s3.amazonaws.com/0/28644/229c815a-b755-f1c4-243e-ef7c93e509db.png)

試しに次のスケッチをArduino Microに書き込んでスイッチには全く触らず放置しておくと、シリアルモニタに0(LOW)だけでなく1(HIGH)も出力される。スイッチを押している間は5Vピンと接続されるのでHIGHとなり1が出力され続けるので問題ないが、スイッチを押していない間にも1が出力されるのでは正しく判定できず使い物にならない。

```C++
int buttonPin = 11;

void setup() {
  pinMode(buttonPin, INPUT);
  Serial.begin(9600);
}

void loop() {
  int readState = digitalRead(buttonPin);
  Serial.println(readState);
}
```

## 回路とスケッチ

### プルアップ

スイッチを押していない間にフローティング状態にならないように、入力ピンにHIGHと判定される電圧をかける方法をプルアップと呼ぶ。スイッチを押していない間はHIGHになるので、スイッチを押している間にLOWになるようにすればスイッチの状態を判定できる。
下図のようにスイッチを押していない間は5Vピン→スイッチ→入力ピンと接続されるように、スイッチを押している間は入力ピンがGNDに接続されるようにする。抵抗を挟んでいるのは、スイッチを押している間5Vピンがスイッチを介してGNDと接続されてショートするからである。このプルアップに使用する抵抗のことをプルアップ抵抗と呼ぶ。プルアップ抵抗の値は小さすぎると無駄に電流が流れてしまうし、大きすぎるとノイズの影響を受けやすくなる。また、タクトスイッチやArduino Micro、抵抗などの定格も考慮する必要がある。このあたりを考慮した上で適当な抵抗値にするようだ。10kΩがよく用いられるようだが1kΩにしても問題なく動作した。

![pullup.png](https://qiita-image-store.s3.amazonaws.com/0/28644/755ae224-e3a9-363f-be4c-fb05eab62c74.png)

書き込んだスケッチは以下の通り。スイッチを押している間LEDが点灯するようにしてある。LOWとHIGHの変化を検知したらチャタリングが収まるまでの間(デバウンス時間)待機し、それでも同じ値のままであれば確実に変化したと判断している。
previousStateの初期値がHIGHなのは、プルアップによりスイッチを押していない状態でHIGHとなっているからである。デバウンス時間は、参考資料ではあるもののタクトスイッチのデータシートにBounce 5msと書かれていたため余裕を持って10msにした。10msでも誤判定するようであれば長くすれば良い。デバウンス時間は短すぎると誤判定するが、長すぎるとスイッチを押した後の反応(ここではLEDの点灯・消灯)が鈍くなる。

```C++
const int buttonPin = 11;
const int ledPin = 13;

int previousState = HIGH;
int currentState;
int ledState = LOW;

void setup()
{
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
}

void loop()
{
  int readState = digitalRead(buttonPin);

  // スイッチ操作またはノイズによる状態変化検知
  if (previousState != readState)
  {
    currentState = readState;

    // デバウンス時間待機
    delay(10);

    // デバウンス時間待機しても同じ状態ならスイッチ操作とみなす
    if (currentState == digitalRead(buttonPin))
    {
      // LED点灯時は消灯、消灯時は点灯
      ledState = !ledState;
      digitalWrite(ledPin, ledState);

      previousState = currentState;
    }
  }
}
```

### プルダウン

プルアップとは逆に、スイッチを押していない間は入力ピンをLOWにする方法をプルダウンと呼ぶ。スイッチを押していない間はLOWになるので、スイッチを押している間にHIGHになるようにすればスイッチの状態を判定できる。
下図のようにスイッチを押していない間は入力ピンがGNDと接続されるように、スイッチを押している間は5Vピン→スイッチ→入力ピンと接続されるようにする。抵抗を挟んでいるのは、スイッチを押している間に5Vピンがスイッチを介してGNDと接続されてショートするからである。このプルダウンに使用する抵抗のことをプルダウン抵抗と呼ぶ。プルダウン抵抗の値もプルアップと同様に考慮した上で適当な抵抗値にするようだ。

![pulldown.png](https://qiita-image-store.s3.amazonaws.com/0/28644/1cd8abd1-bd84-a66f-b7e7-907bcb567139.png)

書き込んだスケッチは以下の通り。スイッチを押している間LEDが点灯するようにしてある。プルアップの時とほとんど同じだが、スイッチを押していない状態でLOWになるのでpreviousStateの初期値をLOWにしてあるのが唯一の違い。

```C++
const int buttonPin = 11;
const int ledPin = 13;

int previousState = LOW;
int currentState;
int ledState = LOW;

void setup()
{
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
}

void loop()
{
  int readState = digitalRead(buttonPin);

  // スイッチ操作またはノイズによる状態変化検知
  if (previousState != readState)
  {
    currentState = readState;

    // デバウンス時間待機
    delay(10);

    // デバウンス時間待機しても同じ状態ならスイッチ操作とみなす
    if (currentState == digitalRead(buttonPin))
    {
      // LED点灯時は消灯、消灯時は点灯
      ledState = !ledState;
      digitalWrite(ledPin, ledState);

      previousState = currentState;
    }
  }
}
```

### プルアップ(内部プルアップ抵抗)

Arduino Microでは汎用I/Oピンを入力に設定した時、マイコン内部のプルアップ抵抗を利用することができる。これを利用すると、わざわざ別のプルアップ抵抗を用意せずにプルアップにできる。
内部プルアップ抵抗を有効にすると入力ピンはHIGHになるので、スイッチが押された時にLOWになるように下図のように接続すればよい。

![inner_pullup.png](https://qiita-image-store.s3.amazonaws.com/0/28644/ec18e37f-45a5-9453-27b5-e1dac44311a9.png)

スケッチは以下の通り。プルアップの時とほとんど同じだが、入力ピンの設定(pinMode)でINPUTの代わりにINPUT_PULLUPを指定しているのが変更点。INPUTを指定すれば内部プルアップ抵抗は無効となるが、INPUT_PULLUPを指定すると内部プルアップ抵抗が有効になる。

```C++
const int buttonPin = 11;
const int ledPin = 13;

int previousState = HIGH;
int currentState;
int ledState = LOW;

void setup()
{
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
}

void loop()
{
  int readState = digitalRead(buttonPin);

  // スイッチ操作またはノイズによる状態変化検知
  if (previousState != readState)
  {
    currentState = readState;

    // デバウンス時間待機
    delay(10);

    // デバウンス時間待機しても同じ状態ならスイッチ操作とみなす
    if (currentState == digitalRead(buttonPin))
    {
      // LED点灯時は消灯、消灯時は点灯
      ledState = !ledState;
      digitalWrite(ledPin, ledState);

      previousState = currentState;
    }
  }
}
```