2017年11月11日土曜日

RaspberryPiにサーボモータ(SG90)をつないでみる - その1

今回は、手元にあるRaspberry Pi 3と、とにかく安いサーボモータということでラジコン用の(多分ステアリングの角度を決める)サーボモータであるSG90というモータを接続してみます。Amazonで1個450円位ですが、例によって偽物が多く出回っている状況ですので信頼できるところから購入した方がいいと思います。偽物の判定ができるページまで用意されています・・・。
スペックは、秋月電子さんが提供してくれているものにリンクを張っておきます。 http://akizukidenshi.com/download/ds/towerpro/SG90_a.pdf
このサーボモータは50Hz(20ms)の周期の中で電圧がかかっている時間に応じて角度を決めて最大180°回転します。データシートによって2種類の記載があるようだけど、色々と試した結果と他サイトの情報を確認するとパターン2が正しそう。ですので、以降の文書はパターン2を前提に動作を確認していきます。

パターン1

1 ms -90°
1.5 ms
2 ms 90°

パターン2

0.5 ms -90°
1.45 ms
2.4 ms 90°

上の表については下記のような図をイメージすればよいのかと思います。0.5msで-90°、2.4 msで+90°となります。これを50Hzのサイクルで送ってあげれば良いみたいです。

この短い時間の電力を制御するために、PWMという方法を使って電力のスイッチングを行います。ソフトウェア的に実現してもよいのですが、タイミングがかなりシビアですので、通常はハードウェアの出力したPWMを利用するのだと思います。

Raspberry Pi 3には、2系統のPWMを出力する機構が備わっていて、1系統はステレオジャックから出される音源と共用しています。ですので、ステレオジャックを利用する場合は、1系統しかPWMを出力することができません。(**PIGPIOを利用する場合とモータドライバの基盤を別途用意する場合は別です。)

基本的には最大でも2系統しかPWMを出力できませんので、2台のサーボモータしか制御することができません。

先に書いておくと、ライブラリとしてPIGPIOを使用するか、サーボモータドライバを増設するかのどちらか正解で、ある程度まともな制御とサーボの台数が必要となると、どちらかを選択することとなります。でも、色々なパッケージを試して理解したい事もあったので、ここでは下記のような順番で試していくことにします。

  • RPi.GPIOのPWMを使用する。
  • WiringPiのPWMを使用する。
  • PIGPIOのOWMを使用する。
  • サーボモータドライバ(PCA9685)を利用する。

RPi.GPIOで1台動作させる

まずは、RPiで1台のサーボを動作させてみます。 RPiのPWMでは、最初に周波数(Hz)を決めてあげて、パルスを送る比率(デューティー比)を渡してあげることで、サーボモータを制御します。

接続方法

ソースコード

#!/usr/bin/python
# coding: utf-8

import RPi.GPIO as GPIO
import time
import signal
import sys


def rotate_servo(servo, angle):
    #   0度の位置 0.5 ms / 20 ms * 100 = 2.5 %
    # 180度の位置 2.4 ms / 20 ms * 100 = 12 %
    #      変動幅 12% - 2.5% (9.5%)
    # angle * 9.5 / 180
    if -90 <= angle <= 90:
        d = ((angle + 90) * 9.5 / 180) + 2.5
        servo.ChangeDutyCycle(d)
    else:
        raise ValueError("angle")


def init_servo(gpios):
    """
    初期化します。gpiosは利用するGPIOをLISTで指定してください。
    :param gpios: GPIO番号(LIST)
    :return: GPIO.PWM (List)
    """
    pwms = []
    GPIO.setmode(GPIO.BCM)
    if isinstance(gpios, list):
        for gpio in gpios:
            GPIO.setup(gpio, GPIO.OUT)

            s = GPIO.PWM(gpio, 50)
            s.start(0.0)
            pwms.append(s)
    else:
        raise Exception("gpios isn't list object.")

    return pwms


if __name__ == "__main__":
    # GPIO 12番を使用
    GPIO_12 = 12

    # 初期化
    pwms = init_servo([GPIO_12])

    try:
        # -90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], -90)
        time.sleep(3)

        # 0°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 0)
        time.sleep(3)

        # 90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 90)
        time.sleep(3)

        for mm in range(4):
            # -90°の位置まで動かします。
            rotate_servo(pwms[0], -90)
            time.sleep(2)
            for i in range(-90, 91):
                # -90~90°まで20ミリ秒毎に動かします。
                rotate_servo(pwms[0], i)
                time.sleep(0.02)

    except KeyboardInterrupt as ki:
        # サーボの動作を停止します。
        pwms[0].stop()
        GPIO.cleanup()

動作状況

動画をYoutubeにあげてみましたが、明らかに怪しい挙動を示す時があります。特に負荷がかかっておらず、停止している時に突然プルプル震えだしていることがわかると思います。まぁ、これでかまわないという人はこれでいいのかなと思いますが、普通はちょっとなんとかしたくなりますよね・・・。

RPi.GPIOで2台動作させる

問題はあるものの、そのままサーボの数を2台に増やしてみます。

接続方法

ソースコード

#!/usr/bin/python
# coding: utf-8

import RPi.GPIO as GPIO
import time
import signal
import sys


# def rotate_servo(servo, angle):
#     # 2.5 - 12 (9.5)
#     # 0 - 180
#     d = (angle / 18.94736842105263) + 2.0
#     servo.ChangeDutyCycle(d)
def rotate_servo(servo, angle):
    #   0度の位置 0.5 ms / 20 ms * 100 = 2.5 %
    # 180度の位置 2.4 ms / 20 ms * 100 = 12 %
    #      変動幅 12% - 2.5% (9.5%)
    # angle * 9.5 / 180
    if -90 <= angle <= 90:
        d = ((angle + 90) * 9.5 / 180) + 2.5
        servo.ChangeDutyCycle(d)
    else:
        raise ValueError("angle")

def init_servo(gpios):
    """
    初期化します。gpiosは利用するGPIOをLISTで指定してください。
    :param gpios: GPIO番号(LIST)
    :return: GPIO.PWM (List)
    """
    pwms = []
    GPIO.setmode(GPIO.BCM)
    if isinstance(gpios, list):
        for gpio in gpios:
            GPIO.setup(gpio, GPIO.OUT)
            # GPIO.PWM(GPIO, 周波数(Hz))
            s = GPIO.PWM(gpio, 50)
            s.start(0.0)
            pwms.append(s)
    else:
        raise Exception("gpios isn't list object.")

    return pwms


if __name__ == "__main__":
    # GPIO 12番を使用
    GPIO_12 = 12
    GPIO_13 = 13

    # 初期化
    pwms = init_servo([GPIO_12, GPIO_13])

    try:
        # -90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], -90)
        rotate_servo(pwms[1], -90)
        time.sleep(3)

        # 0°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 0)
        rotate_servo(pwms[1], 0)
        time.sleep(3)

        # 90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 90)
        rotate_servo(pwms[1], 90)
        time.sleep(3)

        for mm in range(4):
            # -90°の位置まで動かします。
            rotate_servo(pwms[0], -90)
            rotate_servo(pwms[1], -90)
            time.sleep(2)
            for i in range(-90, 91):
                # -90~90°まで20ミリ秒毎に動かします。
                rotate_servo(pwms[0], i)
                rotate_servo(pwms[1], i)
                time.sleep(0.02)

    except KeyboardInterrupt as ki:
        # サーボの動作を停止します。
        pwms[0].stop()
        pwms[1].stop()
        GPIO.cleanup()

動作状況

接続する台数を2台に増やしてみましたが、やはり不定期に震えていることがわかります。また、Raspberry Piに接続された画面に頻繁に雷マークが出るようになりました。これは電圧降下(ドロップ)が発生しているためでサーボが要求する電圧をRaspberry Piが供給できていないために発生していると思われます。(なお、アイコス用の2.4AのUSB充電器を使用していますので、元々0.1A不足しています)


問題点

さて、ちょっと長くなってしまいましたので今回はここまでということで、現在抱えている問題点は下記の3点ということになります。
  • 電圧降下
  • 不定期に震える
  • 2台までしか設置できない
これらは、次回以降やっつけていくことにします。