【電気・電子】Raspberry Pi 4 Model BでRPi.GPIOライブラリのコールバックが動かいない!pigpioライブラリに変更して対応しました

電気・電子

 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 GPIOimport 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ライブラリを使うことで解決できました。

コメント

タイトルとURLをコピーしました