Raspberry Pi Picoでボタンの短押し・長押し・ダブルクリック判定【MicroPython入門】

MicroPython

押しボタンの押し方に違いをつけられたら、1つのボタンでもさまざまな指示が出せるのになぁ〜と感じたことはありませんか?でも、押す時間回数によって動きを変えようとすると、一気に難しく見えてしまいます。

この記事では、その「なんとなく難しそう」を実際に手を動かして理解できるレベルまで一緒にほぐしていきます。

実際、私も最初は「短押し」と「長押し」ってどう判定するの?「ダブルクリック」ってパソコン以外でもできるの?と疑問だらけでした。そこで今回は、PicoとMicroPythonを使ってボタンの押し方を判定し、LEDで結果を知らせるプログラムを作りながらステップアップしていきます。専門用語はできるだけ丁寧に解説するので安心してください。

完成イメージ

ボタンを

  • 短く押すと 右のLED
  • 長押しすると 中央のLED
  • 素早く二回押すと 左のLED

が光るようになります。

手元のボタンでパソコンのマウスのような操作が再現できるのはとても楽しく、ゲームやツールの操作に応用できます。まずは動かしてみることを目標に、コードを書き換えながら理解を深めていきましょう。

この記事でできること

このチュートリアルでは、次の内容を学べます。

  • ボタンの短押し・長押し・ダブルクリックの違いと判定方法を理解する
  • まずは1ファイルですべて動く“完成版コード”を試し、手軽に動作を体験する
  • コードをモジュールに分割して読みやすくし、再利用しやすい形に整理する
  • クラスを使ってオブジェクト指向で整理し、他のプロジェクトへ応用できる構造を作る

難易度は初級〜中級です。MicroPythonの基本的な文法(ifやwhileなど)がわかれば、十分ついてこられます。
所要時間はステップ1で10分、ステップ2と3を合わせて1〜2時間ほどです。

準備:必要な道具・環境

パーツ一覧

品名役割代替案
Raspberry Pi Pico Wマイクロコントローラ本体Pico無印、Pico2でもOK
Micro USBケーブル(データ通信対応)PicoとPCの接続用USB‑C変換アダプタがあれば他のケーブルでも可
ブレッドボードはんだ付け不要で回路を組めるユニバーサル基板でも代用可
ジャンパワイヤ(オス‑オス)Picoと部品をつなぐリード線でも可
LED(3個)短押し・長押し・ダブルクリックの表示用3個あればOK
抵抗220〜330Ω ×3LEDを保護するための電流制限270Ωや330Ωでも可
ボタン(タクトスイッチ)押下を検出するセンサー押しボタンなら何でもOK

動作環境(筆者の検証環境)

  • PC:MacBook Air (M1, macOS 15.5 arm64)
  • エディタ:Thonny 4.1.6(Python 3.10.11, Tk 8.6.13)
  • デバイス:Raspberry Pi Pico W(MicroPython)
  • 補足:WindowsやLinuxでも同様に動作しますが、インタプリタやドライバの設定が異なる場合があります。

配線図

配線はシンプルです。PicoのGPIOピンを直接LEDやボタンにつなぐだけですが、安全のため以下のポイントを守りましょう。

  • 各LEDの長い足(+側)をGPIOに、短い足(−側)を抵抗経由でGNDにつなぎます。
    抵抗を通さないとLEDが焼けてしまいます。
  • タクトスイッチの片側をGPIOに、反対側をGNDに接続します。今回はプログラムで内部プルアップ設定しているので、電源にプルアップする必要はありません。
  • 静電気や短絡防止のため、作業前に手を乾いた布で拭くなどして静電気を逃がしておきましょう。

ステップ1:ベタ書きで短押し・長押し・ダブルクリックを体験

まずはMicroPythonのコード1ファイルだけで、ボタンの押し方に応じて3つのLEDが光る仕組みを体験します。全ての機能が詰まっているので長く感じますが、ロジックの流れを掴むことが目的です。

from machine import Pin
from time import ticks_ms, ticks_diff, sleep_ms

# ===== 設定値(あとで変える可能性があるものはここに集約) =====
BUTTON_PIN = 11
LED_PINS = [16, 17, 18]

DEBOUNCE_MS = 30         # チャタリング対策用
LONG_PRESS_MS = 200      # これ以上押し続けたら「長押し」
DOUBLE_CLICK_MS = 120    # 1回目と2回目の間の最大間隔

PRESS_NONE = 0
PRESS_SHORT = 1
PRESS_LONG = 2
PRESS_DOUBLE = 3

BUTTON_PRESSED  = 0   # 物理的に押されている状態
BUTTON_RELEASED = 1   # 離されている状態
# ============================================================

button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)
leds = [Pin(pin_num, Pin.OUT) for pin_num in LED_PINS]


def set_led_by_press_type(press_type: int) -> None:
    """押下種別に応じてLEDを点灯させる"""
    # 全消灯
    for led in leds:
        led.value(0)

    if press_type == PRESS_SHORT:
        leds[0].value(1)   # LED_PINS[0] → 16番ピン(短押し)
    elif press_type == PRESS_LONG:
        leds[1].value(1)   # LED_PINS[1] → 17番ピン(長押し)
    elif press_type == PRESS_DOUBLE:
        leds[2].value(1)   # LED_PINS[2] → 18番ピン(ダブルクリック)
    # PRESS_NONE のときは何もしない(全消灯のまま)
    sleep_ms(1000)
    for led in leds:
        led.value(0)


def read_button() -> int:
    """ボタン状態を1回読む(0=押された, 1=離されている)"""
    raw = button.value()
    if raw == 0:
        return BUTTON_PRESSED
    else:
        return BUTTON_RELEASED


def wait_for_press_event() -> int:

    # まず「ボタンが離された状態」になるまで待つ(安全のため)
    while read_button() == BUTTON_PRESSED:
        sleep_ms(DEBOUNCE_MS)

    # 押されるまで待つ
    while True:
        if read_button() == BUTTON_PRESSED:
            # デバウンス
            sleep_ms(DEBOUNCE_MS)
            if read_button() == BUTTON_PRESSED:
                break
        sleep_ms(1)

    t_pressed = ticks_ms()

    # 押している間を監視 → 長押しかどうか判定
    while True:
        now = ticks_ms()
        if read_button() == BUTTON_RELEASED:
            # 離された
            press_duration = ticks_diff(now, t_pressed)
            break

        # 押しっぱなしで一定時間を超えたら長押し確定
        if ticks_diff(now, t_pressed) >= LONG_PRESS_MS:
            # 離されるまで待ってから返す
            while read_button() == BUTTON_PRESSED:
                sleep_ms(1)
    
            return PRESS_LONG

        sleep_ms(1)

    # ここに来るのは「LONG_PRESS_MS 未満で離された」=短押し候補
    # → ダブルクリック待ち
    first_release_time = ticks_ms()

    while ticks_diff(ticks_ms(), first_release_time) < DOUBLE_CLICK_MS:
        if read_button() == BUTTON_RELEASED:
            # 2回目の押下を検出
            sleep_ms(DEBOUNCE_MS)
            if read_button() == BUTTON_PRESSED:
                # 2回目の押しが確定したので、離されるまで待つ
                while read_button() == BUTTON_PRESSED:
                    sleep_ms(1)
                return PRESS_DOUBLE

        sleep_ms(1)

    # 一定時間内に2回目の押しがなかった → 短押し
    return PRESS_SHORT


# ===== メインループ =====
while True:
    press_type = wait_for_press_event()
    set_led_by_press_type(press_type)
    # 状態がわかりやすいように、少し余韻を持たせる
    sleep_ms(50)

コードの流れはこうです。まずボタンが離されるまで待ち、押されると押し始めの時刻を記録します。押している時間がLONG_PRESS_MS(200ms)を超えたら長押し、離された場合は一旦短押し候補として、続けてDOUBLE_CLICK_MS(120ms)以内にもう一度押されたらダブルクリックと判定します。押下種別に応じて3個のLEDを点灯し、1秒後に消灯します。余計な処理を行わない単純なループなので、まずは動作確認に最適です。

ピコさんのひとこと

ピコさん
ピコさん
全部入りのコードは長く見えるけど、やっていることは押している時間を測っているだけだよ。怖がらずに試してみてね!

ステップ2:モジュール分割で再利用しやすく

ステップ1で「とにかく全部入りのコード」を動かしてみたので、ここからは少しずつ整理していきます。

そこでステップ2ではボタンの処理LEDの表示を分けて、mainスクリプトから呼び出せるモジュール構成にします。クラスは使わず関数だけで整理しているので、初心者でも構造を理解しやすくなります。

ボタン入力モジュールの作成 

まずはボタン入力部分をbutton_input_module_step2.pyというモジュールにまとめます。押し方の種類を数値で返すだけにして、LEDとは切り離しています。

button_input_module_step2.py

from machine import Pin
from time import ticks_ms, ticks_diff, sleep_ms

# ===== 設定値(ボタン入力まわり) =====
BUTTON_STATE_PRESSED  = 0
BUTTON_STATE_RELEASED = 1

PRESS_NONE   = 0
PRESS_SHORT  = 1
PRESS_LONG   = 2
PRESS_DOUBLE = 3

DEBOUNCE_MS        = 30    # チャタリング対策
LONG_PRESS_MS      = 200   # これ以上押し続けたら「長押し」
DOUBLE_CLICK_MS    = 120   # ダブルクリックの2回目を待つ時間
BUTTON_PULL_MODE   = Pin.PULL_UP
SLEEP_TICK_MS      = 1     # ポーリングループのスリープ
# ====================================


def init_button(pin_num: int) -> Pin:
    """ボタン用のPinオブジェクトを作成して返す"""
    button = Pin(pin_num, Pin.IN, BUTTON_PULL_MODE)
    return button


def read_button(button: Pin) -> int:
    """ボタン状態を1回読む(BUTTON_STATE_PRESSED / RELEASED を返す)"""
    raw = button.value()
    if raw == 0:
        return BUTTON_STATE_PRESSED
    else:
        return BUTTON_STATE_RELEASED


def wait_for_press_event(button: Pin) -> int:
    """
    1回分の「ボタン操作」が完了するまで待ち、
    押し方の種類を返す:
      - PRESS_SHORT
      - PRESS_LONG
      - PRESS_DOUBLE
    """

    # 1) まず「ボタンが離された状態」になるまで待つ(安全のため)
    while read_button(button) == BUTTON_STATE_PRESSED:
        sleep_ms(DEBOUNCE_MS)

    # 2) 押されるまで待つ(デバウンス付き)
    while True:
        if read_button(button) == BUTTON_STATE_PRESSED:
            sleep_ms(DEBOUNCE_MS)
            if read_button(button) == BUTTON_STATE_PRESSED:
                break
        sleep_ms(SLEEP_TICK_MS)

    t_pressed = ticks_ms()
    is_long_press = False

    # 3) 押している間を監視 → 長押しかどうか判定
    while True:
        now = ticks_ms()

        if read_button(button) == BUTTON_STATE_RELEASED:
            # 離された → 短押し候補
            break

        if ticks_diff(now, t_pressed) >= LONG_PRESS_MS:
            # 長押し確定 → 離されるまで待つ
            is_long_press = True
            while read_button(button) == BUTTON_STATE_PRESSED:
                sleep_ms(SLEEP_TICK_MS)
            break

        sleep_ms(SLEEP_TICK_MS)

    if is_long_press:
        return PRESS_LONG

    # 4) 短押し候補 → ダブルクリックを待つ
    first_release_time = ticks_ms()

    while ticks_diff(ticks_ms(), first_release_time) < DOUBLE_CLICK_MS:
        if read_button(button) == BUTTON_STATE_PRESSED:
            sleep_ms(DEBOUNCE_MS)
            if read_button(button) == BUTTON_STATE_PRESSED:
                # 2回目の押下を確定 → 離されるまで待つ
                while read_button(button) == BUTTON_STATE_PRESSED:
                    sleep_ms(SLEEP_TICK_MS)
                return PRESS_DOUBLE
        sleep_ms(SLEEP_TICK_MS)

    # 5) ダブルクリックが来なかった → 短押し
    return PRESS_SHORT

このモジュールのポイントは、GPIOの読み取りと押下時間の計測を関数化していることです。物理的なボタン判定はここに集約されるので、後でLEDの処理を変更しても影響を受けません。

LED表示モジュールの作成

次に、LEDの点灯制御をled_output_module_step2.pyに分けます。押下種別に応じて指定したLEDを光らせるだけのシンプルな関数です。

led_output_module_step2.py

from machine import Pin
from time import sleep_ms
from button_input_module_step2 import PRESS_SHORT, PRESS_LONG, PRESS_DOUBLE

# ===== 設定値(LEDまわり) =====
LED_PINS = [16, 17, 18]   # 0:短押し, 1:長押し, 2:ダブルクリック
LED_ON_TIME_MS = 1000
# =============================


def init_leds() -> list:
    """LED用のPinオブジェクトのリストを作成して返す"""
    leds = [Pin(pin, Pin.OUT) for pin in LED_PINS]
    return leds


def set_led_by_press_type(leds: list, press_type: int) -> None:
    """押下種別に応じてLEDを点灯させる"""

    # 全消灯
    for led in leds:
        led.value(0)

    if press_type == PRESS_SHORT:
        leds[0].value(1)   # 短押し
    elif press_type == PRESS_LONG:
        leds[1].value(1)   # 長押し
    elif press_type == PRESS_DOUBLE:
        leds[2].value(1)   # ダブルクリック

    sleep_ms(LED_ON_TIME_MS)

    for led in leds:
        led.value(0)

ステップ1の点灯処理を丸ごと関数にしたので、押下判定とは独立しました。LED_PINSLED_ON_TIME_MSを冒頭で定義しておけば後から変更しやすいのも利点です。

メインプログラムで組み合わせ

最後に、ボタンモジュールとLEDモジュールを組み合わせるメインコードmain_step2.pyを書きます。押下種別を取得し、LED表示関数に渡すだけのシンプルな流れです。

main_step2.py

from time import sleep_ms
import button_input_module_step2 as btn
import led_output_module_step2 as ledmod

# ===== 設定値 =====
BUTTON_PIN = 11
MAIN_LOOP_WAIT_MS = 50
# ==================

button = btn.init_button(BUTTON_PIN)
leds = ledmod.init_leds()

while True:
    press_type = btn.wait_for_press_event(button)
    ledmod.set_led_by_press_type(leds, press_type)
    sleep_ms(MAIN_LOOP_WAIT_MS)

この段階でコードは3つのファイルに分かれ、役割が明確になりました。ボタンの判定ロジックを他のプロジェクトに再利用したいときはbutton_input_module_step2.pyだけをコピーすれば済みます。

Raspberry Pi Pico本体にあらかじめ
button_input_module_step2.py
led_output_module_step2.py
を保存しておかないと、main_step2.pyから呼び出せません(importできません)。
Pico本体だけで動かすには、main_step2.pymain.pyにリネームしておきましょう。

ピコさんのひとこと

ピコさん
ピコさん
ファイルを分けるだけで、どこで何をしているかがスッキリしましたね!他の作品でボタンを使いたいときにもコピペできます。

ステップ3:クラス化でさらに整理

ステップ2で関数ベースのモジュール構成にできたので、次はもう一歩だけ踏み込んでクラス化に挑戦してみます。複数のボタンやLEDを扱うときは、処理をクラスにまとめておくとさらに便利です。ステップ3ではボタン処理LED表示をクラスにして、オブジェクト指向的に組み合わせます。

クラス化ちょっと難しいので、無理に理解せずともステップ2でも十分です。

ボタン入力クラスの作成

button_input_module_step3.pyでは、ボタン1個につきクラスのインスタンスを用意します。長押しやダブルクリックの判定ロジックはほぼステップ2と同じですが、複数ボタンでも扱えるようにメンバー変数としてピンや設定を持たせます。

button_input_module_step3.py

from machine import Pin
from time import ticks_ms, ticks_diff, sleep_ms

# ===== 公開用の定数 =====
PRESS_NONE   = 0
PRESS_SHORT  = 1
PRESS_LONG   = 2
PRESS_DOUBLE = 3

BUTTON_STATE_PRESSED  = 0
BUTTON_STATE_RELEASED = 1

DEFAULT_DEBOUNCE_MS     = 30
DEFAULT_LONG_PRESS_MS   = 200
DEFAULT_DOUBLE_CLICK_MS = 120
DEFAULT_PULL_MODE       = Pin.PULL_UP
SLEEP_TICK_MS           = 1
# ======================


class ButtonInput:
    """
    1個のボタンから
      - 短押し
      - 長押し
      - ダブルクリック
    を読み取るクラス。
    """

    def __init__(
        self,
        pin_num: int,
        debounce_ms: int = DEFAULT_DEBOUNCE_MS,
        long_press_ms: int = DEFAULT_LONG_PRESS_MS,
        double_click_ms: int = DEFAULT_DOUBLE_CLICK_MS,
        pull_mode: int = DEFAULT_PULL_MODE,
    ) -> None:
        self._button = Pin(pin_num, Pin.IN, pull_mode)
        self._debounce_ms = debounce_ms
        self._long_press_ms = long_press_ms
        self._double_click_ms = double_click_ms

    def _read_button(self) -> int:
        """内部用: ボタン状態を enum 風に返す"""
        raw = self._button.value()
        if raw == 0:
            return BUTTON_STATE_PRESSED
        else:
            return BUTTON_STATE_RELEASED

    def wait_for_event(self) -> int:
        """
        1回分の「ボタン操作」が完了するまで待ち、
        押し方の種類を返す:
          - PRESS_SHORT
          - PRESS_LONG
          - PRESS_DOUBLE
        """

        # 1) まず「ボタンが離された状態」になるまで待つ
        while self._read_button() == BUTTON_STATE_PRESSED:
            sleep_ms(self._debounce_ms)

        # 2) 押されるのを待つ(デバウンス付き)
        while True:
            if self._read_button() == BUTTON_STATE_PRESSED:
                sleep_ms(self._debounce_ms)
                if self._read_button() == BUTTON_STATE_PRESSED:
                    break
            sleep_ms(SLEEP_TICK_MS)

        t_pressed = ticks_ms()
        is_long_press = False

        # 3) 押している間を監視 → 長押しかどうか判定
        while True:
            now = ticks_ms()

            if self._read_button() == BUTTON_STATE_RELEASED:
                # 離された → 短押し候補
                break

            if ticks_diff(now, t_pressed) >= self._long_press_ms:
                # 長押し確定 → 離されるまで待つ
                is_long_press = True
                while self._read_button() == BUTTON_STATE_PRESSED:
                    sleep_ms(SLEEP_TICK_MS)
                break

            sleep_ms(SLEEP_TICK_MS)

        if is_long_press:
            return PRESS_LONG

        # 4) 短押し候補 → ダブルクリックを待つ
        first_release_time = ticks_ms()

        while ticks_diff(ticks_ms(), first_release_time) < self._double_click_ms:
            if self._read_button() == BUTTON_STATE_PRESSED:
                sleep_ms(self._debounce_ms)
                if self._read_button() == BUTTON_STATE_PRESSED:
                    # 2回目の押下を確定 → 離されるまで待つ
                    while self._read_button() == BUTTON_STATE_PRESSED:
                        sleep_ms(SLEEP_TICK_MS)
                    return PRESS_DOUBLE
            sleep_ms(SLEEP_TICK_MS)

        # 5) ダブルクリックが来なかった → 短押し
        return PRESS_SHORT

このクラスでは、ボタンの読み取りや判定をメソッドとして実装しています。引数でデバウンス時間や長押し閾値を変えられるので、ボタンごとに微調整することもできます。

LED表示クラスの作成

led_output_module_step3.pyではLEDの表示をクラスにまとめます。初期化時にピン番号や点灯時間を指定できるので、LEDの本数や色を簡単に変更できます。

led_output_module_step3.py

from machine import Pin
from time import sleep_ms
from button_input_module_step3 import PRESS_SHORT, PRESS_LONG, PRESS_DOUBLE

# ===== 設定値(LEDまわり) =====
LED_PINS = [16, 17, 18]   # 0:短押し, 1:長押し, 2:ダブルクリック
DEFAULT_LED_ON_TIME_MS = 1000
# =============================


class LedIndicator:
    """押し方の種類に応じてLEDを点灯させるクラス"""

    def __init__(self, led_pins=None, on_time_ms: int = DEFAULT_LED_ON_TIME_MS) -> None:
        if led_pins is None:
            led_pins = LED_PINS
        self._leds = [Pin(pin, Pin.OUT) for pin in led_pins]
        self._on_time_ms = on_time_ms

    def _all_off(self) -> None:
        for led in self._leds:
            led.value(0)

    def show(self, press_type: int) -> None:
        """押下種別に応じてLEDを点灯させる"""
        self._all_off()

        if press_type == PRESS_SHORT:
            self._leds[0].value(1)
        elif press_type == PRESS_LONG:
            self._leds[1].value(1)
        elif press_type == PRESS_DOUBLE:
            self._leds[2].value(1)

        sleep_ms(self._on_time_ms)
        self._all_off()

LEDクラスは内部にLEDのリストを保持し、showメソッドで指定された種類のLEDを点灯・消灯します。表示時間もインスタンス生成時に設定でき、プロジェクトに応じて柔軟に調整できます。

メインプログラムの構築

最後はmain_step3.pyです。ボタンとLEDのクラスをインスタンス化し、イベント待ちと表示を繰り返すだけのスマートなコードになっています。

main_step3.py

from time import sleep_ms
from button_input_module_step3 import ButtonInput
from led_output_module_step3 import LedIndicator

# ===== 設定値 =====
BUTTON_PIN = 11
MAIN_LOOP_WAIT_MS = 50
# ==================

button = ButtonInput(pin_num=BUTTON_PIN)
indicator = LedIndicator()

while True:
    press_type = button.wait_for_event()
    indicator.show(press_type)
    sleep_ms(MAIN_LOOP_WAIT_MS)

クラス化によって読みやすさと拡張性が格段に上がりました。これならボタンを増やしてもクラスを追加するだけですし、LEDの本数や色を変えるのも簡単です。ゲームコントローラーやメディアコントローラーなど、複数ボタンを扱う応用にも役立ちます。

ピコさんのひとこと

ピコさん
ピコさん
クラスにするとボタンやLEDが増えてもすっきり書けるよ!ひとつの部品を自分で作ったみたいで楽しいね。

トラブルシューティング

うまく動かないときは、以下の症状と原因を照らし合わせてチェックしてみてください。配線写真がなくても解決できるようにまとめました。

症状原因対処
LEDがまったく光らないLEDの極性が逆、抵抗を忘れている、LEDピン番号の設定ミス長い足をGPIO側に接続し、抵抗を必ず通す。LED_PINSの配列を実際の接続に合わせて修正
ボタンを押しても何も起きないボタン配線が誤ってVCCに接続されている、プルアップ設定と配線が合っていないタクトスイッチの片側をGPIOに、もう一方をGNDに接続。PULL_UP設定に合わせて正しい配線にする
短押しがダブルクリックと誤判定されるダブルクリック待ち時間が長すぎる、チャタリングがひどいDOUBLE_CLICK_MSを短め(100ms程度)にする。DEBOUNCE_MSを増やす
長押しが判定されない長押し時間の設定が短すぎる、押している間に手が震えて離れてしまうLONG_PRESS_MSを長めに設定する。ボタンをしっかり押し続ける
LEDが消えないループから抜けていない、sleep時間が長すぎて見逃しているMAIN_LOOP_WAIT_MSLED_ON_TIME_MSを適度に調整し、ループが回っているかprintで確認する

まとめ

今回はRaspberry Pi PicoとMicroPythonを使って、ボタンの短押し・長押し・ダブルクリックを判定し、LEDで結果を表示するまでを3つのステップで解説しました。ステップ1では1ファイルでの基本実装を体験し、ステップ2ではモジュールに分割して再利用しやすくしました。ステップ3ではクラス化して複数ボタンやLEDにも対応できる構造を作りました。

これでボタン入力に自信がついたはずです!
次の課題としては、
LEDの数を増やして順番に点灯させるプログラム
押した回数によって動作が変わるようなゲーム性のあるプロジェクト
に挑戦してみてください。きっとPicoでの電子工作がもっと楽しくなることでしょう。


コメント