5月末からの久しぶりの投稿です。Raspberry Piを弄るのも久しぶりで、どこまでやっていたかを思い出すために自分のブログを読んでしまうぐらい忘れていました。www
いきなりですがコールバックが動かない
このプログラムでは、RPi.GPIOライブラリを使ってロータリーエンコーダーの信号をコールバック関数で取得するようにしています。しかし、朱書きのところでコールバック関数を登録していますが動きませんでした。
コールバックが使えない場合は、ポーリング方法で常に信号の値変化を監視するようにコードを書く必要があります。コールバックは、あらかじめ信号の登録と信号の値が変化したときに実行する処理(コールバック関数)を登録しておくことで、信号の値が変化するとコールバック関数を実行してくれる仕組みです。
以下のブログのプログラムでは、ポーリング方法でロータリーエンコーダーの信号を検知しています。ロータリーエンコーダーを触っていなくても、必ず信号の確認をしているので無駄が多い高コストなコードです。
コールバックが使えることでコードをシンプルに書くことができますし、監視を低コストで行うことができます。
import RPi.GPIO as GPIO
import time
import curses
import sys
# Set up PWM pins and param
ENA = 26
IN1 = 19
IN2 = 13
IN3 = 22
IN4 = 27
ENB = 17
frequency = 1000
duty = 100
CW = True
CCW = False
# Set up Encoder pins
clkPin = 25 # CLK Pin
dtPin = 24 # DT Pin
swPin = 23 # Button Pin
globalCounter = 0
# GPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(clkPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(dtPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(swPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(IN1, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(IN2, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(ENA, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(IN3, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(IN4, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(ENB, GPIO.OUT, initial=GPIO.LOW)
# PWM setup
enA = GPIO.PWM(ENA, frequency)
enB = GPIO.PWM(ENB, frequency)
enA.start(0)
enB.start(0)
# Encoder callback
def encoder_callback(channel):
global globalCounter
clk_state = GPIO.input(clkPin)
dt_state = GPIO.input(dtPin)
if clk_state != dt_state:
globalCounter += 1
else:
globalCounter -= 1
# sw callback
def sw_callback(channel):
global globalCounter
globalCounter = 0
# Motor control
def AxisA(duty, direction):
enA.ChangeDutyCycle(abs(duty))
if direction == CW:
GPIO.output(IN1, GPIO.HIGH)
GPIO.output(IN2, GPIO.LOW)
else:
GPIO.output(IN1, GPIO.LOW)
GPIO.output(IN2, GPIO.HIGH)
def AxisB(duty, direction):
enB.ChangeDutyCycle(abs(duty))
if direction == CW:
GPIO.output(IN3, GPIO.HIGH)
GPIO.output(IN4, GPIO.LOW)
else:
GPIO.output(IN3, GPIO.LOW)
GPIO.output(IN4, GPIO.HIGH)
# Display using curses
def display(stdscr):
global globalCounter
curses.curs_set(0)
stdscr.nodelay(1)
stdscr.clear()
while True:
# Update motor control based on encoder count
if 0 < globalCounter <= 20:
duty = globalCounter * 5
AxisA(duty, CW)
AxisB(duty, CW)
elif -20 <= globalCounter < 0:
duty = -globalCounter * 5
AxisA(duty, CCW)
AxisB(duty, CCW)
else:
duty = 0
AxisA(duty, CW)
AxisB(duty, CW)
# Display values
stdscr.addstr(0, 0, "Encoder Count:")
stdscr.addstr(1, 0, f"{globalCounter:>6}")
stdscr.addstr(2, 0, "Dutycycle:")
stdscr.addstr(3, 0, f"{duty:>6}")
stdscr.refresh()
time.sleep(0.005)
# Main function
def main():
try:
GPIO.add_event_detect(clkPin, GPIO.BOTH, callback=encoder_callback, bouncetime=5)
GPIO.add_event_detect(swPin, GPIO.FALLING, callback=sw_callback, bouncetime=5)
except RuntimeError as e:
print(f"Failed to add edge detection: {e}")
sys.exit(1)
try:
curses.wrapper(display)
except KeyboardInterrupt:
pass
finally:
enA.stop()
enB.stop()
GPIO.cleanup()
if __name__ == "__main__":
main()
GPIO.add_event_detect(clkPin, GPIO.BOTH, callback=encoder_callback, bouncetime=5)
GPIO.add_event_detect(swPin, GPIO.FALLING, callback=sw_callback, bouncetime=5)
プログラムをデバッグ実行すると、以下のエラーが発生します。
RuntimeError: Failed to add edge detection
調べていくと、Raspberry Pi 4Model B向けの最新OS環境(Raspbian 12 BookWorm)では、RPi.GPIOライブラリのadd_event_detect()関数はサポートされていないことがわかりました。_| ̄|○
pigpioライブラリに変更
さらに、解決方法を調べていくとpigpioライブラリがコールバックできそうということがわかりました。ただ、調べてもpigpioライブラリの使い方の情報が少なかったので、pigpioライブラリのサイトで公開されている仕様を確認しながら移植を行いました。
関数名は異なりますが同じことができました。さらにpigpioライブラリでは青字のようにコールバックが使えることを確認できました。
# 予めターミナルでsudo pigpiodデーモンを実行すること
# 確認 ps aux | grep pigpiod
# 停止 sudo killall pigpiod
import pigpio
import time
import curses
import sys
# Set up PWM pins and param
ENA = 26
IN1 = 19
IN2 = 13
IN3 = 22
IN4 = 27
ENB = 17
frequency = 1000
duty = 100
range = 100
CW = True
CCW = False
# Set up Encoder pins
clkPin = 25 # CLK Pin
dtPin = 24 # DT Pin
swPin = 23 # Button Pin
globalCounter = 0
# GPIO setup
pi = pigpio.pi()
pi.set_mode(clkPin, pigpio.INPUT)
pi.set_mode(dtPin, pigpio.INPUT)
pi.set_mode(swPin, pigpio.INPUT)
pi.set_pull_up_down(clkPin,pigpio.PUD_UP)
pi.set_pull_up_down(dtPin,pigpio.PUD_UP)
pi.set_pull_up_down(swPin,pigpio.PUD_UP)
pi.set_mode(IN1, pigpio.OUTPUT)
pi.write(IN1, pigpio.HIGH)
pi.set_mode(IN2, pigpio.OUTPUT)
pi.write(IN2, pigpio.LOW)
pi.set_mode(ENA,pigpio.OUTPUT)
pi.write(ENA, pigpio.LOW)
pi.set_mode(IN3,pigpio.OUTPUT)
pi.write(IN3, pigpio.HIGH)
pi.set_mode(IN4, pigpio.OUTPUT)
pi.write(IN4, pigpio.LOW)
pi.set_mode(ENB, pigpio.OUTPUT)
pi.write(ENB,pigpio.LOW)
# PWM setup
pi.set_PWM_frequency(ENA, frequency)
pi.set_PWM_frequency(ENB, frequency)
pi.set_PWM_range(ENA,range)
pi.set_PWM_range(ENB,range)
# Encoder callback
def encoder_callback(channel,level,tick):
global globalCounter
clk_state = pi.read(clkPin)
dt_state = pi.read(dtPin)
if clk_state != dt_state:
globalCounter += 1
else:
globalCounter -= 1
# SW callback
def sw_callback(channel,level,tick):
global globalCounter
globalCounter = 0
# Motor control
def AxisA(duty, direction):
pi.set_PWM_dutycycle(ENA,abs(duty))
if direction == CW:
pi.write(IN1,pigpio.HIGH)
pi.write(IN2,pigpio.LOW)
else:
pi.write(IN1,pigpio.LOW)
pi.write(IN2,pigpio.HIGH)
def AxisB(duty, direction):
pi.set_PWM_dutycycle(ENB,abs(duty))
if direction == CW:
pi.write(IN3,pigpio.HIGH)
pi.write(IN4,pigpio.LOW)
else:
pi.write(IN3,pigpio.LOW)
pi.write(IN4,pigpio.HIGH)
# Display using curses
def display(stdscr):
global globalCounter
curses.curs_set(0)
stdscr.nodelay(1)
stdscr.clear()
while True:
# Update motor control based on encoder count
if 0 < globalCounter <= 20:
duty = globalCounter * 5
AxisA(duty, CW)
AxisB(duty, CW)
elif -20 <= globalCounter < 0:
duty = -globalCounter * 5
AxisA(duty, CCW)
AxisB(duty, CCW)
else:
duty = 0
AxisA(duty, CW)
AxisB(duty, CW)
# Display values
stdscr.addstr(0, 0, "Encoder Count:")
stdscr.addstr(1, 0, f"{globalCounter:>6}")
stdscr.addstr(2, 0, "Dutycycle:")
stdscr.addstr(3, 0, f"{duty:>6}")
stdscr.refresh()
time.sleep(0.005)
# Main function
def main():
try:
pi.callback(clkPin,pigpio.EITHER_EDGE,encoder_callback)
pi.callback(swPin, pigpio.FALLING_EDGE,sw_callback)
except RuntimeError as e:
print(f"Failed to add edge detection: {e}")
sys.exit(1)
try:
curses.wrapper(display)
except KeyboardInterrupt:
pass
finally:
pi.stop()
if __name__ == "__main__":
main()
このプログラムでは2つのコールバックを登録しています。1つはロータリーエンコーダーの回転信号、もう1つはロータリーエンコーダーのスイッチ信号です。

このロータリーエンコーダーはボリュームの軸がスイッチにもなっており、押すとカチッ!と音がします。スイッチを押すことでロータリーエンコーダーのカウンターをゼロリセットさせる機能を追加しました。リセット機能もスイッチ信号とゼロリセット処理のsw_callback()を登録することで実現できました。
RPi.GPIOとpigpioの対応
移植を行った際にRPi.GPIOライブラリで使用したコードとpigpioライブラリで使用したコードの対応表を作成しました。
RPi.GPIOライブラリした場合 | pigpioライブラリした場合 |
import RPi.GPIO as GPIO | import pigpio |
GPIO.setmode(GPIO.BCM) | – |
– | pi = pigpio.pi() |
GPIO.setup(clkPin, GPIO.IN, pull_up_down=GPIO.PUD_UP) | pi.set_mode(clkPin, pigpio.INPUT) pi.set_pull_up_down(clkPin,pigpio.PUD_UP) |
GPIO.setup(dtPin, GPIO.IN) | pi.set_mode(dtPin, pigpio.INPUT) |
GPIO.setup(IN1, GPIO.OUT, initial=GPIO.HIGH) | pi.set_mode(IN1, pigpio.OUTPUT) pi.write(IN1, pigpio.HIGH) |
GPIO.setup(IN2, GPIO.OUT, initial=GPIO.LOW) | pi.set_mode(IN2, pigpio.OUTPUT) pi.write(IN2, pigpio.LOW) |
enA = GPIO.PWM(ENA, frequency) | pi.set_PWM_frequency(ENA, frequency) |
– | pi.set_PWM_range(ENA,range) |
enA.start(0) | – |
clk_state = GPIO.input(clkPin) | clk_state = pi.read(clkPin) |
enA.ChangeDutyCycle(abs(duty)) | pi.set_PWM_dutycycle(ENA,abs(duty)) |
GPIO.output(IN1, GPIO.HIGH) | pi.write(IN1,pigpio.HIGH) |
GPIO.add_event_detect(clkPin, GPIO.BOTH, callback=encoder_callback, bouncetime=5) | pi.callback(clkPin,pigpio.EITHER_EDGE,encoder_callback) |
enA.stop() | pi.stop() |
GPIO.cleanup() | – |
pigpioライブラリの使用上の注意点
pigpioライブラリを使用する際の注意点が1つあります。あらかじめターミナル画面で以下のコマンドを実行しておきます。pigpiodというのはGPIO制御用のサーバデーモンです。デーモン(daemon)とは、バックグラウンドで常駐し続けるプログラムのことです。pigpiodデーモンのおかげで他のプロセスや Python コードが、非同期で GPIO を制御できるようになります。
sudo pigpiod
ターミナル画面で実行したところです。

pigpiodの実行状態は、ターミナル画面で以下のコマンドを実行することで確認できます。
ps aux | grep pigpiod
ターミナル画面で実行した結果です。1行目のrootで始まる行でpigpiodが起動中であることがわかります。

pigpiodを停止する方法は以下の通りです。「4209」は上記の画面のpigpiodのプロセスIDです。
sudo kill 4209
# または
sudo killall pigpiod
まとめ
Raspberry Piの復習からはじまりました。今回は、Raspberry Piでロータリーエンコーダーの信号をコールバックで検知してPWM制御でモーターを回せるようにしました。Raspberry Piのバージョンとライブラリとの組み合わせにハマってしまいましたが、pigpioライブラリを使うことで解決できました。
コメント