マイコンにおけるチャタリング&ノイズ対策

チャタリングとは、例えばマウスのクリックがなぜかダブルクリックになる現象です。
マウスに使われているタクトスイッチの劣化など機械的な要因から発生するものですが、様々な防止方法があり、「ハードウェア」「ソフトウェア」でそれぞれ異なります。

本記事では「ソフトウェア」にあたるマイコンのファームウェアに焦点をあてて解説します。

目次

なぜチャタリングは起こるか

スイッチの動作イメージを掴めば何となく理解できます。スイッチと一言で言っても色々ありますが、今回はマイクロスイッチを使います。

上記の写真のスイッチでは3つの端子が確認できますね。
下の端子から電気を流すと右上の端子にそのまま電気が流れますが、スイッチ上部の赤い突起を押すと右下の端子に電気が流れるようになります。

マイクロスイッチのCOM→NC
マイクロスイッチのCOM→NO

つまり、右下の端子から電気が流れてきたらスイッチが押されたと判断できます。

チャタリングの図
こんな単純な構造ではないはずですが、説明のために色々省いてます

上記の図のうち、チャタリングが起こるタイミングは(2)と(4)です。この2つはONでもOFFでもない状態になっています。(1)と(5)は完全に端子とくっついていて、(3)は端子から完全に離れていることからON/OFFの区別が可能です。

チャタリング中では本当にほんの一瞬だけ電気が通ったり通らなかったりするのです。リアルタイムに電気信号を受け取り続けるプログラムからすれば、超高速でONとOFFが繰り返されているように見えます。
ざっくりとした説明ではありましたが、これがチャタリングの正体です。

チャタリングというと押す時に発生するイメージがありますが、離す時にも発生します。

チャタリングをプログラムでどう防ぐか

まずは「チャタリング時間」について説明しなければいけません。

チャタリング波形
チャタリングノイズは押す時と離す時に発生する

チャタリング時間とは、チャタリングが発生してから落ち着くまでの時間です。スイッチによって異なり、マイクロスイッチであれば数ミリ秒であることが多いです。

これから述べるチャタリング対策では全て「チャタリングが落ち着いてから入力を受け取る」という処理をしますが、言い換えれば「チャタリング時間よりも長い時間待ってから入力を受け取る」ことになります。なので、まずは使用するスイッチのチャタリング時間を知る必要があるのです。

開発元のメーカーのサイトなどに記載されていることもあるようですが、分からない場合はまず仮の時間を想定し、どの程度でチャタリングが起こらなくなるか調査(プログラム上の値を適当な数値に設定していく)する必要があります。

さて、本題ですが、チャタリング対策のアルゴリズムは大きく分けて3つあります。

ディレイ方式

読み込んだスイッチがONなら一定時間待ち、再度読み込んだ際にまたONであれば入力を受け付ける方式です。

メリット

  • 3つの方式の中では最も実装が簡単
  • 予め正確な入力遅延の秒数が分かる

デメリット

  • 「2つ以上のスイッチの同時押しはしない」という前提でなければ使えない
while (1) {
  // 0:OFF 1:ON
  if (SW_A == 1) {
    delay(5); // 5ミリ秒何もせず待つ
    if (SW_A == 1) {
      // 入力受け付け
    }
  }
}

while文の中のif文でスイッチがONであるかどうかを延々と確認しているイメージです。
特に特殊なことはしていないので、組み込み初心者でも簡単に実装できるはずです。

上記のコードはあくまで例ですが、察しがいい方はSW_A、SW_B、SW_Cとあった場合にスイッチの同時押しができないことに気が付くかもしれません。
delay関数の実行時、CPUは他の処理を行えなくなるので、スイッチを押したらnミリ秒経つまで他のスイッチが動作しないのです。
スイッチ押下時に処理を止めてもいい・同時押しは判定しない等の条件下でなければ使えません。

delay(5); // CPUは5ミリ秒間何もできない

しかし、「きっかり5ミリ秒待つ」という処理ができるのは強みです。この場合、スイッチを押してから入力が確定するまで5ミリ秒かかるので、入力遅延は5ミリ秒となります。実は、ディレイ方式以外では正確な遅延秒数を導き出すことができないので、入力遅延が数ミリ秒レベルで厳しいシステムにいいのではないでしょうか。

簡単に実装できる割に扱いは難しい方式です。

ゲージ判定方式

一般的な用語ではないと思われますが、割り込みを使わずに実装できるので一つの方式として紹介します。

メリット

  • 複数のスイッチの同時押しが可能
  • 長押しを判定できる
  • 外来ノイズに強い
  • 割り込みを使わないので実装が簡単

デメリット

  • スイッチの数だけカウンタが必要なので、それを確保するメモリが必要
  • 「きっかりnミリ秒待つ」という動作ができず、入力遅延を非常に求めづらい

まずはスイッチごとにカウンタを用意します。押されたらそのスイッチに対応するカウンタをカウントアップし、ある一定回数以上の値(一致検出回数)に達したら入力を受けつけるというものです。

チャタリング波形ゲージ判定方式
カウントアップ=ゲージを貯めていくイメージ

カウントアップ中は入力として受け取らないので、チャタリング時間を超えるディレイを取って入力を受け取ればチャタリングを防げます。また「カウンタの値がn回以上で長押しと判定」とすることも可能です。

外来ノイズにも強いです。通常であればノイズが発生したら入力を開始してしまうところ、この方式では(閾値に到達しなければ)カウントアップの被害のみで済みます。

チャタリング波形ノイズ発生
外来ノイズにも色々あるが、一瞬だけ入るノイズならばこうなる

なお、この場合の外来ノイズとは、関係のない回路やリード線などから何らかの現象で誤って電気が流入してしまうような現象です。想像しやすい例を挙げるならば、ボタンを強打すると部品が揺れてリード線同士が接触してしまい、両方のリード線に電気が流れてしまう等が考えられます。その場合の物理的な対策としては、リード線が接触しても大丈夫なようにシールドを施すなどしますが、対策が難しいものであればこの方式の採用を考えるべきでしょう。

while (1) {
  // 一致検出回数 = 50
  if (COUNTER[SW_A] == 50 && COUNTER[SW_B] == 50) {
    // AとBの同時入力
    // something...
  }
  else if (COUNTER[SW_A] == 50) {
    // Aの入力
    // something...
  }
  else if (COUNTER[SW_B] == 50) {
    // Bの入力
    // something...
  }
  // 以下カウントアップ
  if (sw_a_pushed == 1 && COUNTER[SW_A] < 50) {
    countUpA();
  }
  if (sw_b_pushed == 1 && COUNTER[SW_B] < 50) {
    countUpB();
  }
}

この方式の欠点は、1回のカウントにどれだけの時間がかかるか分かりにくいところです。「他の処理を実行→ONならカウントする処理を実行→他の処理を実行」を繰り返しているので、他の処理でif文の中に入り、カウントが遅れてしまうということが考えられます。

上記の例に挙げたコードであれば、下記の4ケースで1カウント当たりの時間が異なります。

  • 「sw_a_pushed」と「sw_b_pushed」ともに1である
  • 「sw_a_pushed」が1である
  • 「sw_b_pushed」が1である
  • 「sw_a_pushed」と「sw_b_pushed」ともに1でない

50カウント目でようやく入力になりますから、各ボタンの入力遅延が異なったりするのです。ボタンを押すのは人間ですし、チャタリングでカウントされなかったりしますし、プログラム中の全ての条件分岐を網羅する必要があるので、入力遅延を求めることはとても難しいです。

「このプログラムの入力遅延は最大nミリ秒です!」などと宣言することが難しくなってしまいますね。入力遅延を定められる(入力遅延を知りたい)開発ケースではあまり取りたくない方法です。

ちなみに、コードはあくまで例ですので悪しからず。
実際には次のアルゴリズムで作り、スイッチを押した際と離した際のチャタリングの両方を対策しましょう。

  • 入力していないとき、ONならカウンタをカウントアップし、一致検出回数nに達したら入力を始める
  • 入力しているとき、OFFならカウンタをカウントダウンし、0に達したら入力をやめる

実装の際はカウンタのオーバーフローに気を付けてください。

(簡易)サンプリング方式

割り込みを使う方式です。組み込み初心者には扱いづらいですが、仕組みは単純です。

メリット

  • 複数のスイッチの同時押しが可能
  • 予め大まかな入力遅延の秒数が分かる
  • チャタリングノイズに強い

デメリット

  • 外来ノイズに弱い

タイマ割り込みを使って一定時間(サンプリング周期)ごとにスイッチの状態(ON/OFF)を読み出すというものです。この方式では、サンプリング周期が訪れたタイミングで全てのスイッチの状態をメモリへ書き込み、プログラムはメモリ上のスイッチの状態を取得します。

チャタリング波形4サンプリング(ノイズ発生なしパターン)
自然な波形になる

そうすれば次のスイッチの状態の読み出し時までに処理系は同じ値を読み取るわけですから、図に表せば安定した信号になります。もちろんチャタリング時間よりも長いサンプリング周期を取れば理論上はチャタリングが起こりません。

またディレイ方式ほどの正確さはないものの、入力遅延を求めることができます。
サンプリング周期 = 10msとした場合

  • 最良のケース:遅延「約 0 ms」 スイッチを押した直後にサンプリング周期が訪れる
  • 最悪のケース:遅延「約10 ms」サンプリング周期が訪れた直後にスイッチを押す

つまり、スイッチを押したタイミングによって0~10msのブレが発生します。平均遅延秒数は「サンプリング周期 / 2」で5msなので、これを基準にします。

ディレイ方式やゲージ判定方式に比べて大きなデメリットが少なく、チャタリング対策アルゴリズムしては最も適しているのですが、1つ問題があります。
それは、外来ノイズに弱いことです。

チャタリング波形サンプリング(ノイズ発生パターン)
入力時間もサンプリング周期分になる

ノイズが入って一瞬だけONになったタイミングに運悪くサンプリング周期が差し掛かると、入力を受け付けてしまいます。

これはサンプリング方式にゲージ判定方式を加えることで解決できます。

サンプリング(+ゲージ判定)方式

実は最も一般的な方式です。
サンプリング方式と言うと一般的にはこれを指します。

メリット

  • 複数のスイッチの同時押しが可能
  • 長押しを判定できる
  • 予め大まかな入力遅延の秒数が分かる
  • チャタリングノイズに強い
  • 外来ノイズに強い

デメリット

  • 入力遅延にブレが発生しやすい

まずはスイッチごとにカウンタを用意します。タイマ割り込みを使って一定時間(サンプリング周期)ごとにスイッチの状態(ON/OFF)を読み出し、ONであればそのカウンタがカウントアップされ、ある一定以上の値(一致検出回数)に達したら入力を受け付けるというものです。

ゲージ判定方式に比べるとカウンタの増減がサンプリング周期ごとになるので、必要になる一致検出回数が少なくなり、メモリに厳しい環境なら少ないビットでカウンタを動作させる等のテクニックも活用できます(一致検出回数 = 3回ならスイッチ1つ辺り2ビットで処理可能)。

基本的にはゲージ判定方式とサンプリング方式のいいとこ取りで、一般的なチャタリング対策や外来ノイズ対策としては完成形であると思われます。

ただし、1点だけ注意が必要です。

一致検出処理サンプリングタイミング遅延秒数
全て一致最速約20ms
全て一致最遅約30ms
1回不一致最速約30ms
1回不一致最遅約40ms
2回不一致最速約40ms
2回不一致最遅約50ms
サンプリング周期 = 10ms / 一致検出回数 = 3回

上記の表の通り、不一致になるとサンプリング周期分の遅れが生じます。
実際には一致したり不一致だったりするので、「ブレ」が発生します。

平均遅延秒数は、不一致になった場合を除くのであれば「サンプリング周期 * 一致検出回数 - (サンプリング周期 / 2)」で求められるので、ゲーミングデバイスなどはこれを遅延秒数として公表しているのだと思います。
不一致になった場合を考慮して実際には「サンプリング周期 * 一致検出回数」くらいがいいのかもしれませんが、計算式の根拠が難しいですよね。実際に不一致になるかどうかなんて計測するのも大変です。

サンプリング周期と一致検出回数の適正値について

調べてみると「慣例的にはサンプリング周期 = 10ms : 一致検出回数 = 3回である」というような情報が散見されましたが、これは参考程度に留めておくべきだと思います。

INFINITASコンのメンテ中
beatmania IIDXのコントローラのメンテナンス中

D2MV等のマイクロスイッチ(新品)であれば「サンプリング周期 = 3ms / 一致検出回数 = 2回」でも問題なく動作しました。一般的なマイクロスイッチならこれくらいでも大丈夫なのでしょう。
ただし、スイッチの劣化も鑑みるならもう少し長めに設定するべきです。
とりあえず設定してみる数値程度の認識で、問題なければそのまま使ってもいいと思います。

まとめ

アルゴリズム実装コスト同時押しチャタリングノイズ外来ノイズ遅延
ディレイ方式××
ゲージ判定方式×
サンプリング方式×
サンプリング
(+ゲージ判定)方式

一通り紹介しましたが、ほとんどは「サンプリング(+ゲージ判定)方式」で事足ります。

遅延について

チャタリング対策に入力遅延はつきものです。遅延を増やすほどチャタリングが起こりにくくなるので、「どの頻度までチャタリングを許すか」「どの程度まで遅延を許すか」が争点になります。

一般的なマウスであればチャタリングが起こらないように入力遅延を多めにするでしょうが、ヘビーゲーマー向けのマウスであれば少なめにして応答速度を高めるという策も取れるでしょう。

ちなみに、格闘ゲームの専用コントローラにはあえてチャタリング対策を行っていないものがあるそうです。そうすると遅延がゼロになるので、遅延を許容できないヘビーゲーマーからは好まれるんですね。

記事の内容的に身も蓋もありませんが、ものによってはそういう実装もあるということも頭の片隅に留めておくといいかもしれません。

マイコン以外でのチャタリング&ノイズ対策

本記事は、あくまでマイコン側でチャタリング&ノイズ対策を行う場合の方法を記載しています。
ハードウェアで対策する場合の方法は以下が詳しいです。
スイッチのチャタリングの概要。チャタリングを防止する方法 | マルツオンライン

マイコンが送信するデータの出力先がOSになるなら、OS側で対策する手もあります。
実はWindowsやMacのマウスとキーボードは初期設定の時点である程度チャタリング対策がされており、マウスのダブルクリック間隔(速度)などをデフォルト値から調節できる機能があります。

Windowsのマウスのプロパティ

その他、HIDデバイスからの入力値をハックすることで出力を制御し、チャタリングを解消するアプリケーションなども有志によって配布されていたりします。
ただしOS側で対策した場合、遅延や入力精度ではハードウェアやマイコン側で対策するよりもはるかに劣ります。それでも導入する側としては設定するだけで済むので、用途によってはOS側に任せるのも手です。

REVIVE USBを使ったチャタリング対策ツール

REVIVE USBという自作USB入力デバイス向け基板のファームウェアを製作しました。

オープンソースであり、メーカーからファームウェアのソースコードを取得できるので、自分で好きなように弄れるという代物です。元々チャタリング対策がされていなかったので、その機能を追加したファームウェアを開発し、その際に調べた結果をこの記事に書いたという経緯があります。

REVIVE USB Debounce
サンプリング周期と一致検出回数を設定できる

この記事で言うところの「サンプリング(+ゲージ判定)方式」で作っています。

基板にマイクロスイッチを接続してキーボードに設定するとたまにチャタリングを確認できます(「a」が「aa」になる)が、本ファームウェアを使って設定するとそれを防ぐことができます。
このツールを使えばファームウェア再書き込みやUSBコネクタの着脱等の操作なく、設定ボタンを押すだけでサンプリング周期/一致検出回数を変えられるので、スイッチのチャタリング時間の調査にも便利です。

GitHub
GitHub - ushui/REVIVE_USB_Debounce Contribute to ushui/REVIVE_USB_Debounce development by creating an account on GitHub.

開発元に倣ってオープンソースなので、チャタリングについて調べたい方はどうぞ。

補足追記 (2021年)

この記事は、学生時代に運営していたブログの記事を書き直したものです。

移行前のブログにコメントがあったので、超遅レスで申し訳ないですが返信させていただきます。

初めまして。 ReviveUSBにて音声ファイル再生ソフトのリモートを作る際に チャタリング対策版を使わせて頂いています。 

お世話になっています。

昨年発売された micro にも同様の対策版を作って頂くことは出来ないでしょうか。

もし、既に公開済みでしたら 教えて頂けると とても助かります。

Unknown

Unknownさん

チャタリング対策版、お使いいただきありがとうございます! REVIVE USB Microの対策版ファームウェアは以下にあります。
※マトリクス版は公開していません

https://github.com/ushui/ADRVMIC-REVIVE-USB-Micro-Debounce

※さらに追記。上記コメントのファームウェアは公式にマージされ、新製品として発売されました。

  • URLをコピーしました!
目次