【RaspberryPi】ハードPWMではなくソフトPWMでサーボモータを制御 with C++

f:id:sugalog:20191003054017j:plain

ラズパイで同時に複数のサーボを使いたかった!

ラズパイでpwmを出力しようとすると、GPIO18(物理12pin)を使うのがフツーだと思います。
これがググるといっぱい出てくるハードpwmで、ハード的にpwm波を作ってくれます。

しかし「いっぱいサーボを使うんだ!」ということで、他の普通のGPIOピンからソフト的にpwm波を作り、サーボを制御させてみました。

言語はC++です。
Python?だって…C++の方が画像処理が速いんだい…(今は関係ない)

結論から言っておくと、サーボはうまくいきませんでした。
±2度程度をキュイキュイ言ってます(; x ;)


コンテンツ

使ったもの

raspberry pi zero 2でも3でも(多分4でも)変わらないです。
・sg90
  【仕様】  周期 :20 [ms]
       パルス幅:0.5~2.4[ms]
       回転角 :0~180 [度]

また以下の二つのライブラリを使いました。

・wiringPi.h
・softPwm.h

インストール方法はGoogle先生にお任せします。


配線

ラズパイ  16      39
      |      |
 sg90  オレンジ 赤  茶
          |  |
電池(5v)    (+)(-)

ラズパイはどちらも物理ピン番号です。


使った関数

int wiringPiSetup()

wiringPi.hよりGPIOピンの初期化です。
失敗すると-1を返します。

int softPwmCreate(int output_pin, int initial_phase, int palse_cycle)

softPwm.hよりソフトpwmを出力するピンのセットアップです。
成功で0を返します。
各パラメータは以下の通りです。

int output_pin
 出力するピンをwiringPi番号(BCM番号でも物理番号でもない)で指定。
 今回はwiringPi番号4(BCM番号23、物理番号16)を使用します。

int inital_phase
 …初期値を指定するらしい。
 らしい、というのもよく分かってないからです。
 とりあえず0にしておけ、と(^^;

int palse_cycle
 pwm波の周期を100[μs]単位で指定。
 サーボの仕様によるので、今回はsg90の仕様から200(20[ms])を指定します。

void softPwmWrite(int output_pin, int palse_width)

softPwm.hよりソフトpwm波を出力します。
この関数で別にスレッドを立てて、そこで時間を刻んでパルス波を出力する感じみたいです。
各パラメータは以下の通りです。

int output_pin
 出力ピン。
 こっちもwiringPi番号でピンを指定するので、今回は4。

int palse_width
 パルス幅を100[μs]単位で指定。
 サーボはこのパルス幅によってどの位置を向くかが決まる。
 sg90なら0.5~2.4[ms]でそれぞれ0~180度を向く。


テストコード

0から180の入力を待ち、それに応じた角度を向かせるようにしました。
終了時は”end"と打てば終了です。

入力が数字でない時にstoi関数が例外を投げるので、例外処理を入れました。
その入力は無視して、再入力を求めるようになっています。


コードは以下のようになってます。

#include <wiringPi.h>
#include <softPwm.h>
#include <iostream>
#include <string>

using namespace std;

float computePalseWidth(int angle) {
   // Servo can position only 0-180
   angle = angle >   0 ? angle :   0;
   angle = angle < 180 ? angle : 180;
   // Palse-width must be 0.5-2.4[ms]
   return angle / 180.0 * 1.9 + 0.5;
}

int main() {
   const int PWM_OUT = 4; //wPi番号
   const int PALSE_RANGE = 20; //[ms]

   // Init
   wiringPiSetup();
   softPwmCreate(PWM_OUT, 0, 10*PALSE_RANGE);

   cout << "When you finish : end" << endl;
   while (true) {
      float palseWidth;

      try {
         string angle;
         cout << "(0 ~ 180) -> ";
         cin >> angle;
         if (angle == "end") break;

         palseWidth = computePalseWidth( stoi(angle) );

      } catch (...) { /* Skip */ continue; }

      softPwmWrite(PWM_OUT, 10*palseWidth);
      delay(500);
   }

   // Finalize
   // Do nothing
}

そういえば、ハードpwmの時には出力を停止してあげる必要がありましたね?
そこはソフトpwmですから。
プログラムが終了すると自動で出力はなくなり、終了時に忘れずにやらなきゃいけないことはないのです!

見たまんまですが、computePalseWidth()ではパルス幅を0.5~2.4[ms]で求めています。


結果!

f:id:sugalog:20191003071757j:plain
駄目でした (omg!!

というのも、動くには動いたのですが、どうもパルス波が安定しない様子。
±2度くらいをキュイキュイ行ったり来たりです…。

配線が切れかかってるんだ!
と現実逃避気味にハードpwmでやってみると、お察しの通りピタっと止まってくれますね。

単純にソフトpwmの限界だろうと結論づけます ( ; ; )



ちなみに、地味にsoftPwmCreate()の第三引数とsoftPwmWrite()の第二引数が0.1[ms]単位なのに気づかず30分くらいはまりました。
intで表したい、かつ分解能がそんなに高くない、ということでのこの単位なのでしょうが…分かりにくいやい!


LEDとかの明るさを制御するためなら全然使えると思いますが、サーボでは使えませんね。
ラズパイで複数のサーボを別々に制御するために何か代案がないものか…要検討です、
といったところで、今回は終しまいです :)