.. include:: /index.rst
:start-after: start_hello_message
:end-before: end_hello_message
.. _py_rotary_encoder:
2.12 ロータリーエンコーダー
=============================
**はじめに**
ロータリーエンコーダーは、回転運動をデジタル信号に変換する入力デバイスです。メニューの操作、値の調整、項目のスクロールなどによく使用されます。このプロジェクトでは、プッシュボタン内蔵のロータリーエンコーダーをFusion HAT+経由でRaspberry Piに接続し、回転ステップを読み取り、エンコーダーのスイッチでカウンターをリセットする方法を学びます。
この実験では、イベントコールバックを使用してロータリーエンコーダーを扱う方法を示し、低遅延で滑らかな入力処理を実現します。
----------------------------------------------
**必要なもの**
このプロジェクトに必要なコンポーネントは以下のとおりです。
.. list-table::
:widths: 30 20
:header-rows: 1
* - COMPONENT INTRODUCTION
- PURCHASE LINK
* - :ref:`cpn_rotary_encoder`
- |link_rotary_encoder_buy|
* - :ref:`cpn_wires`
- |link_wires_buy|
* - :ref:`cpn_fusion_hat`
- \-
* - Raspberry Pi
- \-
----------------------------------------------
**回路図**
以下はこのプロジェクトの回路図です。
.. image:: img/fzz/2.1.6_rotary_sch.png
:width: 80%
:align: center
----------------------------------------------
**配線図**
以下の配線図を参考にして接続してください。
.. image:: img/fzz/2.1.6_rotary_bb.png
:width: 80%
:align: center
**このサンプルで使用するピン割り当て**
- **CLK** → **GPIO 17**
- **DT** → **GPIO 4**
- **SW(ボタン)** → **GPIO 27** (内部プルアップ使用)
- **+** → **3.3V**
- **GND** → **GND**
すべての接続が確実であることを確認してください。エンコーダーのスイッチ端子が **C(COM)** と **NO/NC** に分かれている場合は、 **C** をGNDに、 **NO** をSWピンに接続してください(ソフトウェア側で内部プルアップを有効にしています)。
----------------------------------------------
**サンプルの実行**
このチュートリアルで使用するすべてのサンプルコードは ``ai-lab-kit`` ディレクトリに含まれています。
以下の手順に従ってサンプルを実行してください。
.. raw:: html
.. code-block:: shell
cd ~/ai-lab-kit/python/
sudo python3 2.12_RotaryEncoder.py
スクリプトを実行すると、ロータリーエンコーダーを回すたびにカウンターが増減し、現在のカウンター値がリアルタイムでコンソールに表示されます。エンコーダーのボタンを押すとカウンターは0にリセットされ、リセットメッセージが表示されます。プログラムは ``Ctrl + C`` を押して終了するまで動作し続け、ユーザー入力に応答します。
----------------------------------------------
**コード**
以下は、このプロジェクトで使用するPythonコードです。
.. raw:: html
.. code-block:: python
#!/usr/bin/env python3
from fusion_hat.pin import Pin, Mode, Pull
import time
# GPIO pins (BCM numbering)
CLK_PIN = 17
DT_PIN = 4
SW_PIN = 27
# Initialize pins with internal pull-ups
clk = Pin(CLK_PIN, mode=Mode.IN, pull=Pull.UP)
dt = Pin(DT_PIN, mode=Mode.IN, pull=Pull.UP)
sw = Pin(SW_PIN, mode=Mode.IN, pull=Pull.UP) # Button is active LOW
raw = 0 # Raw quadrature transitions
last_clk = clk.value() # Previous CLK state
last_detent = None # Last displayed detent value
print("Rotate the knob. Press the button to reset. CTRL + C to exit.")
try:
while True:
c = clk.value()
if c != last_clk:
# Direction: DT != CLK means one direction, else the other
raw += 1 if dt.value() != c else -1
# Most encoders generate 2 transitions per detent (click)
detent = raw // 2
# Only update output when the detent value changes
if detent != last_detent:
print(f"\rCounter: {detent} ", end="", flush=True)
last_detent = detent
last_clk = c
# Reset when button is pressed
if sw.value() == 0:
raw = 0
detent = 0
print("\rCounter: 0 ", end="", flush=True)
last_detent = 0
time.sleep(0.25) # Button debounce
time.sleep(0.001) # Polling interval (1 ms)
except KeyboardInterrupt:
print("\nExit")
----------------------------------------------
**コードの解説**
1. **インポート**
.. code-block:: python
from fusion_hat.pin import Pin, Mode, Pull
import time
- ``Pin`` は、ロータリーエンコーダー用のGPIOピンを設定・読み取りするために使用します。
- ``Mode`` と ``Pull`` は、ピンの入出力設定および内部プルアップ抵抗の設定に使用します。
- ``time`` は、ポーリング間隔やボタンのデバウンス時間を制御するために使用します。
2. **GPIOピンの設定**
.. code-block:: python
CLK_PIN = 17
DT_PIN = 4
SW_PIN = 27
clk = Pin(CLK_PIN, mode=Mode.IN, pull=Pull.UP)
dt = Pin(DT_PIN, mode=Mode.IN, pull=Pull.UP)
sw = Pin(SW_PIN, mode=Mode.IN, pull=Pull.UP)
- ``CLK`` と ``DT`` は、ロータリーエンコーダーの2つの位相信号ピンです。
- ``SW`` は、エンコーダー内蔵のプッシュボタンです。
- 内部プルアップ抵抗により、エンコーダーが操作されていないときでも信号を安定させます。
- ボタンはアクティブLOWで、押されると ``0`` を読み取ります。
3. **状態変数**
.. code-block:: python
raw = 0
last_clk = clk.value()
last_detent = None
- ``raw`` は、エンコーダーの低レベルな位相信号の変化回数を記録します。
- ``last_clk`` は、信号変化を検出するために前回のCLK状態を保持します。
- ``last_detent`` は、同じ値の再表示を防ぐために最後に表示したカウンター値を記録します。
4. **回転の検出**
.. code-block:: python
c = clk.value()
if c != last_clk:
raw += 1 if dt.value() != c else -1
- スクリプトはCLKピンを継続的にポーリングします。
- CLKの状態変化は、エンコーダーが回転したことを意味します。
- 回転方向は、DTとCLKの状態を比較して判定します。
5. **デテント(クリック)の計算**
.. code-block:: python
detent = raw // 2
- 多くのロータリーエンコーダーは、物理的な1クリックごとに2回の信号変化を出力します。
- そのため、2で割ることで生の変化回数を実際のクリック数に変換しています。
6. **見やすいコンソール出力**
.. code-block:: python
if detent != last_detent:
print(f"\\rCounter: {detent} ", end="", flush=True)
- カウンター値が変化したときだけ表示を更新します。
- ``\\r`` はカーソルを行頭に戻し、その場で表示を更新するために使用します。
- ``flush=True`` により、端末へ即座に表示を反映します。
7. **ボタンによるリセット処理**
.. code-block:: python
if sw.value() == 0:
raw = 0
detent = 0
print("\\rCounter: 0 ", end="", flush=True)
time.sleep(0.25)
- ボタンを押すとカウンターが0にリセットされます。
- チャタリングによる多重入力を防ぐため、短い待機時間を入れています。
8. **ポーリング間隔とプログラム終了**
.. code-block:: python
time.sleep(0.001)
- 1msのポーリング間隔により、応答性とCPU負荷のバランスを取っています。
- ``Ctrl + C`` を押すことで、プログラムを安全に終了できます。
----------------------------------------------
**トラブルシューティング**
1. **回しても出力されない**
- **原因**: CLK/DTの配線ミス、またはGND接続不良。
- **対処方法**: **CLK→GPIO17**、**DT→GPIO4**、**GND共通** を確認してください。電源は3.3Vを使用し、5V出力のエンコーダーをGPIOに直接接続しないでください。
2. **カウンター値が不安定に飛ぶ(チャタリング/ノイズ)**
- **原因**: 機械的なチャタリング、または長い配線によるノイズ。
- **対処方法**: 配線はできるだけ短くし、可能であればツイストペアにしてください。必要に応じてCLK/DTとGNDの間に小容量コンデンサ(例:0.01〜0.1 µF)を追加してください。また、使用しているライブラリにデバウンス機能があれば有効にしてください。
3. **ボタンが検出されない**
- **原因**: スイッチ接点がGNDではなく3.3V側に接続されている、またはプルアップ設定が誤っている。
- **対処方法**: ボタンを押したときに **SW** が **GND** に接続される構成になっていることを確認し、``pull=Pin.PULL_UP`` が設定されていることを確認してください。
4. **回転方向が逆になる**
- **原因**: CLKとDTの接続が意図した方向と逆になっている。
- **対処方法**: **CLK** と **DT** の配線を入れ替えるか、アプリケーション側で増減方向を反転させてください。
----------------------------------------------
**拡張アイデア**
1. **変数を滑らかに調整する(例:音量/明るさ)**
.. code-block:: python
value = 50 # 0..100
def rotary_change():
global value
value = max(0, min(100, 50 + encoder.steps()))
print(f'Value = {value}')
2. **ボタンでモードを切り替える**
.. code-block:: python
mode = ['Fine', 'Coarse']
idx = 0
def reset_counter():
global idx
idx = 1 - idx
encoder.reset()
print(f'Mode switched to: {mode[idx]}')
3. **LED/ブザーによるフィードバック**
.. code-block:: python
from fusion_hat import Pin
led = Pin(26, Pin.OUT)
def rotary_change():
print('Counter =', encoder.steps())
led.on()
# brief flash without blocking callbacks is recommended
led.off()
4. **長押しで特別な動作を実行する**
- ``sw.when_activated`` や(利用可能であれば) ``sw.when_deactivated`` でタイムスタンプを記録し、短押しと長押しで異なる動作を実装できます。
----------------------------------------------
**まとめ**
この実験では、Fusion HAT+上でロータリーエンコーダーとその内蔵ボタンを使用し、イベント駆動型のコールバックで入力を読み取る方法を学びました。この方法を使えば、Raspberry Piプロジェクトにおいて、メニュー操作、ダイヤル式の設定変更、リアルタイム制御用のジョグホイールなど、応答性の高いインターフェースを構築できます。