Arduinoのボタンのチャタリング対策!millis()でデバウンスして短押し・長押しを判定する方法
「ボタンを1回押しただけなのに、カウントが2つも3つも増える…」「LEDのON/OFFが押すたびにランダムになるんです」
Arduinoでタクトスイッチを使い始めると、かなりの確率でこの現象に出会いますよね。配線もコードも合っているのに、なぜか1回の押下が何回にも数えられてしまう。原因はチャタリングです。
この記事では、チャタリングの正体をつかんだうえで、delay()を使わずmillis()でデバウンスする方法と、同じしくみで短押し・長押しを判定する方法まで一緒に作っていきます。難易度はArduino入門〜初級。最後に、判定ロジックをPythonで再現して、本当にチャタリングを吸収できるかPCだけで確かめます😊
チャタリングとは?押した瞬間に信号がバタつく現象

タクトスイッチの中では、金属の接点がぶつかって電気が流れます。押した瞬間(離した瞬間も)は接点が細かく跳ね返るので、ほんの数ミリ秒のあいだON→OFF→ON…と信号が何度も切り替わります。これがチャタリング(バウンス)です。
イメージとしては、ボールを床に落としたときの「ポン、ポン、ポン」です。人間には1回押しただけでも、マイコンは1秒間に何万回も状態を読めるので、跳ね返りを全部「押された」と数えてしまうわけです。
まずは対策なしで数えてみる
配線は、タクトスイッチの片側をD2、もう片側をGNDにつなぐだけです。INPUT_PULLUPで内蔵のプルアップ抵抗を使うので、外付けの抵抗はいりません。このとき離しているとHIGH、押すとLOWになる点に注意してください。
// ボタンを押した回数を数える(チャタリング対策なし)
const int BUTTON_PIN = 2; // ボタンのもう片方はGNDへ
int lastState = HIGH; // 前回の状態(INPUT_PULLUPなので離すとHIGH)
int count = 0;
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP); // 内蔵プルアップを使う
}
void loop() {
int state = digitalRead(BUTTON_PIN);
if (lastState == HIGH && state == LOW) { // HIGH→LOWになった=押された
count++;
Serial.print("押された回数: ");
Serial.println(count);
}
lastState = state;
}
ポイントをまとめるとこんな感じです。
- 前回の状態を覚えておき、HIGH→LOWに変わった瞬間だけを「押された」と数える
- 押しっぱなしで何回も数えないための、よくある書き方
考え方は正しいのですが、ゆっくり5回押しても、シリアルモニタには次のように表示されることがあります(表示例。回数はスイッチや押し方で変わります)。
押された回数: 1
押された回数: 2
押された回数: 3
押された回数: 4
押された回数: 5
押された回数: 6
押された回数: 7
跳ね返りのたびにHIGH→LOWが起きるので、5回押しても7回数えてしまう、というわけです。
よく見かける対策は「押されたらdelay(50)で待つ」方法です。手軽ですが、待っているあいだは他の処理がすべて止まります。センサーの読み取りやLEDの点滅と組み合わせると、動きがカクつく原因になるんですよね。
millis()でデバウンスする(短押し・長押し判定つき)
そこで、millis()(起動してからの経過ミリ秒)を使います。考え方はシンプルで、「読んだ値が一定時間(ここでは30ms)変わらなかったら、その値を確定とみなす」だけです。跳ね返りは数ms程度で収まるので、30ms続いた値は「本物の押下」と判断できます。
ここが重要です👇
- 生の値(
reading)と確定した状態(stableState)を別々の変数で持つ - 生の値が変わるたびに、時刻(
lastChange)をリセットする - 確定した時刻を覚えておけば、押していた長さ=長押しの判定にもそのまま使える
// チャタリング対策(millis()デバウンス)+短押し・長押し判定
const int BUTTON_PIN = 2; // ボタンのもう片方はGNDへ
const int LED_PIN = 13; // 基板上のLED
const unsigned long DEBOUNCE_MS = 30; // この時間、値が変わらなければ確定
const unsigned long LONG_PRESS_MS = 800; // これ以上押し続けたら長押し
int lastReading = HIGH; // 前回読んだ生の値
int stableState = HIGH; // 確定した状態
unsigned long lastChange = 0; // 生の値が最後に変わった時刻
unsigned long pressedAt = 0; // 押下が確定した時刻
bool longFired = false; // 長押しを通知済みか
bool ledOn = false;
int pressCount = 0;
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long now = millis();
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading) {
lastChange = now; // 値が変わったら計り直し
}
lastReading = reading;
// 30ms変化がなく、確定状態と違う → 状態を確定させる
if (now - lastChange >= DEBOUNCE_MS && reading != stableState) {
stableState = reading;
if (stableState == LOW) { // 押された(確定)
pressedAt = now;
longFired = false;
} else if (!longFired) { // 離された&長押しではない → 短押し
pressCount++;
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
Serial.print("短押し: ");
Serial.print(pressCount);
Serial.println("回目(LED切り替え)");
}
}
// 押したまま800ms経ったら、離すのを待たずに長押しを1回だけ実行
if (stableState == LOW && !longFired && now - pressedAt >= LONG_PRESS_MS) {
longFired = true;
pressCount = 0;
ledOn = false;
digitalWrite(LED_PIN, LOW);
Serial.println("長押し: カウントをリセットしました");
}
// delay()を使っていないので、ここに他の処理を書いても止まらない
}
短押しでLEDが切り替わり、長押し(800ms)でカウントがリセットされます。シリアルモニタの表示例はこんな感じです。
短押し: 1回目(LED切り替え)
短押し: 2回目(LED切り替え)
短押し: 3回目(LED切り替え)
長押し: カウントをリセットしました
短押し: 1回目(LED切り替え)
ちなみに、短押しを「押したとき」ではなく「離したとき」に判定しているのがポイントです。押した瞬間に短押しとして処理すると、そのあと長押しになったときに両方が実行されてしまいます。長押しのほうは、離すのを待たずに800ms経った時点で1回だけ実行しています。
⚠️ 時刻の変数は必ずunsigned longにしてください。millis()は約49.7日で0に戻りますが、now - lastChangeのように「引き算で経過時間を比べる」書き方なら、0に戻っても正しく計算できます。intにすると約32秒であふれてしまいます。
30msで足りないスイッチ(古いものや安価なもの)は、DEBOUNCE_MSを50ms程度に増やしてみてください。
Pythonで判定ロジックを確かめる
「本当にこれで跳ね返りを吸収できるの?」と気になりますよね。実機だとチャタリングは速すぎて目で見えないので、同じロジックをPythonに移して、チャタリング入りの信号で試してみます。1msごとのピンの状態をリストで作り、for文の番号をmillis()の代わりに使っています。
# ボタン信号のチャタリングを再現して、Arduinoと同じ判定ロジックを試すシミュレーション
import random
import matplotlib.pyplot as plt
random.seed(1) # 毎回同じ結果になるように固定
HIGH, LOW = 1, 0
DEBOUNCE_MS = 30 # この時間、値が変わらなければ「確定」とみなす
LONG_PRESS_MS = 800 # これ以上押し続けたら「長押し」
def make_signal(total_ms, presses):
"""1msごとのピン状態を作る(INPUT_PULLUPなので押すとLOW)"""
sig = [HIGH] * total_ms
for start, end in presses:
for t in range(start, end):
sig[t] = LOW
# 押した直後と離した直後の数msだけ、値がバタつく(チャタリング)
for edge in (start, end):
for t in range(edge, edge + 5):
sig[t] = random.choice([HIGH, LOW])
return sig
# 1回目:200msの短押し 2回目:1000msの長押し
signal = make_signal(2000, [(100, 300), (600, 1600)])
# --- ① そのまま読む:HIGH→LOW の変化を全部「押された」と数える ---
naive_count = 0
prev = HIGH
for t, v in enumerate(signal):
if prev == HIGH and v == LOW:
naive_count += 1
prev = v
print(f"そのまま数えた回数: {naive_count}回")
# --- ② デバウンス+短押し・長押し判定(Arduinoのloop()と同じ考え方) ---
last_reading = HIGH # 前回読んだ生の値
stable_state = HIGH # 確定した状態
last_change = 0 # 生の値が最後に変わった時刻
pressed_at = 0 # 押下が確定した時刻
long_fired = False # 長押しを通知済みか
debounced = [] # グラフ用
for now, reading in enumerate(signal): # now が millis() の代わり
if reading != last_reading:
last_change = now # 値が変わったら計り直し
last_reading = reading
if now - last_change >= DEBOUNCE_MS and reading != stable_state:
stable_state = reading
if stable_state == LOW:
pressed_at = now
long_fired = False
elif not long_fired:
print(f"{now:4d}ms 短押し({now - pressed_at}ms)")
if stable_state == LOW and not long_fired and now - pressed_at >= LONG_PRESS_MS:
long_fired = True
print(f"{now:4d}ms 長押し")
debounced.append(stable_state)
# --- ③ 押した瞬間(100ms付近)をグラフで比べる ---
plt.rcParams["font.family"] = "Meiryo"
fig, axes = plt.subplots(2, 1, figsize=(9, 4.5), sharex=True)
rng = range(80, 140)
axes[0].step(list(rng), [signal[t] for t in rng], where="post", color="tab:red")
axes[0].set_title("押した瞬間の生の信号(チャタリングあり)")
axes[1].step(list(rng), [debounced[t] for t in rng], where="post", color="tab:blue")
axes[1].set_title("デバウンス後(30ms 変化なしで確定)")
for ax in axes:
ax.set_yticks([0, 1], ["LOW(押した)", "HIGH"])
ax.set_ylim(-0.3, 1.3)
axes[1].set_xlabel("時間(ms)")
plt.tight_layout()
plt.show()


シミュレーションしているのは、200msの短押しと1000msの長押し、つまり2回しか押していない信号です。これ、実行せずに「そのまま数えた回数」がいくつになるか予想できますか?
実行すると「そのまま数えた回数: 6回」「333ms 短押し(199ms)」「1435ms 長押し」と表示されました(コードのすぐ下の実行結果とグラフを参照)。
- 対策なしだと、押したときと離したときの跳ね返りを拾って2回の押下が6回に数えられた
- デバウンス後は、短押し1回・長押し1回だけを正しく判定できた
- 短押しが199msと表示されるのは、押下も離しも「30ms変化なし」を待ってから確定させているため。そのぶん反応は少し遅れますが、人の感覚では気にならない程度です
グラフを見ると、上の生の信号は押した直後にバタついていますが、下のデバウンス後の信号はきれいに1回だけ切り替わっているのがわかります。
まとめ
- チャタリングは、スイッチの接点が跳ね返って信号が数msバタつく現象
delay()で待つ方法は簡単だが、待っているあいだ他の処理が止まるmillis()で「一定時間変わらなければ確定」とすれば、止まらずにデバウンスできる- 確定した時刻を使えば、短押し(離したときに判定)と長押し(押している最中に1回だけ)も区別できる
- 時刻の変数は
unsigned long、経過時間は引き算で比べる
ボタン1つで「短押し」「長押し」の2つの操作ができると、作品の幅がぐっと広がります。手元のスイッチでDEBOUNCE_MSを変えながら、ぜひ試してみてください🚀
こちらも読まれています
📡 Arduinoをもっと深く学ぼう!
Arduino・ラズパイ・ロボットプログラミングを体系的に学びたい方へ。おすすめのUdemyコースや電子部品もまとめています。
Arduinoロボット入門のおすすめコース・部品を見る →📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。




