ホーム < 電子工作やってみた


Arduino編

その9 Arduinoでサーボモータをキュイキュイ動かす!


 これまでLEDを光らせたり(その2)、光センサーの結果を音にしてみたり(その7)、距離センサーを触ってみたり(その8)と、Arduinoで簡単な実験をいくつかやってきました。ゲームプログラムもそうですが、沢山の小さな実験と経験が大切なのは電子工作も同じだなと感じます。

 で、今回はより実用的な「モーターを回す」という事に挑戦してみます。私は「モーターと言えばミニ四駆(初代)でタミヤ!」という世代なんですが、残念ながらラジコンカー等には触れてきませんで、モーターは電気を通すとすげー回転する…くらいの知識しかありません。ただ、だからと言ってArduinoの5V電源にモーターつないで「ギュイーン」と回して「やった〜」はさすがにどうかと思うので、今回は「サーボモータ」を使ってみる事にしました。ちなみに、この冒頭を書いている時点で「さーぼもーたぁ?」とひらがな読みする位しか知識はありません。さて…この章どうなるのか本人もドキドキ(^-^;



@ サーボモータ「SG90」

 

 世に大小沢山のモータがあり、そのどれもが「軸が回る」という共通項を持っています。その回り方にも色々種類があり、その中の一つが「サーボモータ」という部類に入るモータです(Wikipedia:サーボモータ)。サーボ(サーボ機構)というのは「物体の位置、方位、姿勢などを制御量として、目標値に追従するように自動で作動する機構(Wikipedia:サーボ機構)」とあります。つまり、ただ回転するのではなくて、その回転の回数や回転角度などを指定し制御できる特殊なモータがサーボモータという訳です。

 サーボモータも非常に沢山の種類があり値段もスペックも様々。そんな中でAmazonが扱っていて相対的にう〜んとリーズナブルなサーボモータが「SG90」でした(Amazon:SG90)。2個入りで送料込みで900円位。お試しとしては十分かなと思いました(^-^)

 上の写真がその現物です。青い箱がサーボモータ。TowerPro製です。モータから出ているケーブルの先端には3端子のコネクタが付いていました。Arduinoには雄ピンが無いので中間ケーブルで接続する事になるかな。右に並んでいるのは一般に「サーボホーン」と呼ばれているモータのギアにペコっとはめて使う部品です。ネジは大きい方がモータを固定する為のネジ、小さいのは…んー、どう使うか良く分かりません(^-^;


○ スペック

 TowerProのホームページにサーボモータのスペック一覧がありました(スペック(PDF))。SG90のスペックは以下の通り:

項目
寸法 23 x 12.2 x 29 mm
重さ 9 g
トルク 1.8 kg/cm (4.8V)
駆動速度 0.1 sec/60 deg. (4.8V)
定格電圧 4.8 V
適正温度 0 〜 55 ℃
無応答幅(Dead band
width)
10 μsec
ギア材質 ナイロン
用途 ヘリコプター、3Dフライヤー、F3A

 大きさは500円玉くらいで全体がプラスチック製なのでとっても軽いです。トルクは回転軸から半径1cmの所で出せる力で、4.8Vで駆動させると1.8kgの物を動かせるという事です。駆動速度は0.1秒で60度軸が回転するというスペックですが、これは多分純粋な軸の速度で、サーボホーンに付ける物の負荷によって当然変わります。定格電圧は4.8Vなのに対しArduinoは5Vなので、まぁ直接電源を取って問題無いかなと踏んでます。無応答幅は「動け!」と命令してから反応するまでの時間です。用途を見ると、このサーボモータは主にラジコン飛行機に使われる物のようです。「F3A」というのはラジコン飛行機の型の一つだそうです(その辺り疎くてゴメンナサイ(T-T))

 コネクタですが、どうやら世の中には「フタバタイプ」と「JRタイプ」というのがあるようです。業界標準的位置にあるのは逆挿し防止が付いたフタバタイプのようなのですが、SG90のコネクタはJRタイプ。これは逆挿し防止が特に無いようなのでちょっと注意。

 で、このサーボモータSG90の使い方ですが…TowerProのホームページには特にマニュアルが公開されていませんでした。んー、センサー系は何だかどれも「ごちゃごちゃ言わずに使えってんでい!」っという感じの男気に溢れてます(^-^;



A 使い方

 使い方について、先駆者の皆様のページを参考にする事にしました。JRタイプのコネクタはオレンジと赤と茶色のケーブルのセットになっています:

各ケーブルは以下のような対応になっています(※参照:Yahoo!知恵袋「tower pro sg90サーボモーターの使い方」):

茶色 GND
Vcc(+電源)
オレンジ 信号線

つまり、赤色のケーブルに電源を繋ぎ、茶色の方にマイナス側(GND)を繋げれば電気は通ると。で、オレンジの信号線ですが、ここにはパルス信号(矩形波:PWM(Pulse Width Modulation))を通すようです(※参照:PIC AVR 工作室 サーボモーター制御ライブラリ)。参照サイトがとても詳しく説明してくれているように、Arduinoにはそのものずばり「Servo」というライブラリがあります。最終的にはそれを使えば欲しいPWMを作ってくれるわけで、実際それで制御しているサイトは世界中に沢山ありました。でも、私はまずサーボモータ制御その物の性質を知りたいと思いまして、敢えてゴリッとコードを書いてみる事にしました。Servoライブラリの使い方は、他の多くの紹介サイトをご参照ください。

 PWMというとArduinoのanalogWrite関数がまず思い付きます。この関数はPWMを作って指定のピンに出力してくれます。Arduinoのリファレンス(analogWrite関数)によると、Arduino Uno3等のアナログ出力ピン(3,5,6,9,10,11ピン)の内3,9,10,11ピンは490HzなPWMを出力します(5,6ピンは980Hz)。490Hzは周期が約2msec。一方多くのサーボモータの信号線には20msecなPWMを送るのだそうです。という事は、analogWrite関数が作るPWMはサーボモータに与えるにも周期が短過ぎます。ま、実際これでどうなるかは実験してみます。analogWrite関数が使えないとなると、泥臭いのですが20msec周期の矩形波をデジタルピンに出力するコードを自作する事になりそうです。

 で、サーボモータはPWMのDuty比で回転角度を制御するそうです。具体的にはHIGHを0〜2msecの間で与えます。この辺りも実験ですね。



B 実験してみます

 では、早速動かしてみましょう。


○ analogWrite関数で実験(×)

 まずはanalogWrite関数を実験。回路自体は至極シンプルです:

アナログ出力なので信号線を3番ピンに接続しています。Arduinoのコードは次のようになります:

void setup() {
    pinMode( 3, OUTPUT );
}

void loop() {
    analogWrite( 3, 20 );
}

いったんGNDに繋いだ線をはずし、このコードをArduino基板に送った後にGNDに再度線を繋ぐと…「ギューイン、ギュイン」、うぉ、何か動いた(笑)。ただ、結論としては駄目でした。予想通りパルス幅が短過ぎて全く制御できませんでした。


○ PWMを自作で実験

 次にPWMを自作してみます。信号線はデジタル出力ピン(以下は2番ピン)に変更します:

const int deg0msec = 600; // msec.
const int deg180msec = 2350; // msec.
int microSec = deg0msec;

void setup() {
    pinMode( 2, OUTPUT );
    Serial.begin( 9600 );
}

void loop() {
    if ( Serial.available() > 0 ) {
        delay(10);
        int deg = serialReadAsInt();
        if ( deg >= 0 && deg <= 180 )
        microSec = deg0msec + deg / 180.0 * ( deg180msec - deg0msec );
    }

    // create PWM
    if ( microSec >= deg0msec ) {
        digitalWrite( 2, HIGH );
        delayMicroseconds( microSec ); // ON
        digitalWrite( 2, LOW );
        delayMicroseconds( 10000 ); // OFF
        delayMicroseconds( 10000 - microSec ); // OFF
    }
}

int serialReadAsInt() {
    char c[ 9 ] = "0";
    for ( int i = 0; i < 8; i++ ) {
        c[ i ] = Serial.read();
        if ( c[ i ] == '\0' )
            break;
    }
    return atoi( c );
}

 明らかに先程とコードの気合が違います(笑)。要は上手く行ったという事です(^-^)b

 一番最初の「deg0msec」「deg180msec」は、それぞれサーボモータの軸角度が0度と180度になるHIGH時間のマイクロ秒です。600μsとか2350μsは実はあれこれテストして探し出した値です。購入したSG90の特性値とも言えます。setup関数内では2番ピンを出力用に設定し、シリアルを有効にしています。

 loop関数では最初にシリアルポートからの入力値のチェックをしています。もしSerial.available関数が0より大きな値を返したら、何かシリアルポートから入力があった事を表しますので入力値取得に移ります。「delay(10)」と10ms程待っていますが、これはシリアルポートからの入力を確実にバッファに溜めるために待っています。これを入れないと、入力が行われている最中に文字列を取り出そうとしてしまいます。これについては結構重要なので後述します。

 serialReadAsInt関数は自前で作った入力文字列を整数値に変換する関数です。関数の中では入力バッファから1バイトずつ取得し(Serial.read関数)、それを一時的に蓄え、終端文字('\0')に達したらループを抜け、atoi関数で受け取った文字列を整数に変換しています。

 シリアルからはサーボモータの軸角度(0〜180度)が制す値で入力される事を期待しています。まぁ1度単位で今は十分です。入力された角度は次にマイクロサーボに与えるPWMのHIGH時間に変換されます。0度の時がdeg0msec、180度の時がdeg180msecになるよう線形補間で時間を求めています。最終的なHIGH時間をmicroSec変数に格納します。

 次に得たmicroSecから実際にPWMを作成しています。矩形波は単なるスイッチのオンオフの繰り返しなので、プログラムで作るのは簡単です。2番ピンをまずHIGHにし、microSec時間だけ待ってLOWに落とします。上のコードではLOWにしてからdelayMicroseconds関数が2回呼ばれています。これ、一見「無駄じゃね?」と見えるのですが、実は落とし穴がありまして、delayMicroseconds関数は執筆段階のバージョンで最大16383μ秒しか待てない仕様になっています(2014.5)。これは将来的にもっと伸ばす予定だとリファレンスにはありました。パルス周期は20ms = 20000μ秒にする必要があるため、上のような分解した待ち方をしています。

 さて、これでArduinoのシリアルモニタから軸角度を与えてみるとこんな感じに動いてくれました:

この動画、やけに苦労してセッティングして撮ったのにモアレてるし暗いし…orz。ま、まぁとりあえず自前でゴリゴリと書いたコードでちゃんと角度制御はできましたという事で(^-^)。



C 気が付いた注意点

 実験をしてあれこれ失敗した事をまとめておきます。

 まず、PWM信号ですが、HIGHの長さは短くなるほど0度に近付くと思っていましたが、これは違いました。HIGHを0μsecにするとそもそも反応しませんし、10μsなど極めて小さなパルスにすると、0度以下の方に無理に行こうとして「ガガガガガッ」とストッパーと格闘してしまいます。逆にHIGHを3000μsecなど大きくすると、今度は180度以上の角度にまで振れてしまいます。SG90だけでなく他の角度制限のあるサーボモータはそうだと思うのですが、0〜180度ではなく実際は-15〜190度くらいまで幅に余力があるようです。なので、ストッパーに触れない範囲のHIGH時間幅で0〜180度を制御した方が動作が安定する事がわかりました。

 シリアルポートからの文字入力についての注意です。Arduinoのシリアルポートから数値を入力する時、当初次のようなコードを使っていました:

void loop() {
    if ( Serial.available() > 0 ) {
        int deg = serialReadAsInt();
    ....

 先のコードの一部を抜粋したのですが、Serial.available()の下にdelay(10)というのが抜けています。つまり、「シリアルポートに何か入力があったら、即それを取得して整数として返しなさい」というプログラムになっています。所が、こうすると、サーボモータが意図しない動きになってしまいました。コード上にはミスは無くて、しばらく「???」でした。

 試しに出力値のdegをSerial.print関数で吐き戻してみた所、例えば「180」と送信したのに「18」としか返ってきていない事が判明。原因を調査した所、これは「シリアルポートからの入力タイミングとloop関数の速さ」に原因がありました。シリアルポートからの入力速度は今回は9600bps、つまり1秒間に1200文字送受信できる速さに設定していました。これは1文字当たり1/1200秒で送信される事を意味します。「180」と4文字(終端文字が含まる)打ち込むと、「1」「8」「0」「\0」とこの速度で順次送信され、バッファに書き込まれます。一方、loop関数はありったけの速さでグルグルと回っています。今回の場合、1周期が20μsのパルス波を作っているので、単純に言えば1秒間に最大50000回くらい回る事になります。実際は条件文などで時間を取られますので数千〜数万回程度だとは思いますが、少なくとも「シリアルポートからの入力速度である1200文字/秒よりは速い」事はわかります。

 そうすると、「文字送信!」っとArduinoのシリアルモニタで数値を打ち込んでEnterを押して、それがArduinoのバッファに全部溜まる前にSerial.available関数が呼ばれてしまう事があり得ます。4文字送信したはずなのに、2文字目辺りで「送信あり!」っと反応してしまうという事です。すると、serialReadAsInt関数内ですぐさまそのバッファから文字列を取り出そうとしますが、まだ2文字しか送信されていないので、中途半端な数字が出来あがってしまう…そういうカラクリが起こっていました。

 そこで、「送信あり!」と反応した後にdelay(10)で10ms程十分の待ってあげる事で、確実にバッファに数値を収める事ができ、挙動も安定する結果となりました:

void loop() {
    if ( Serial.available() > 0 ) {
        delay(10);
        int deg = serialReadAsInt();
    ....

ちょっとした事ですが、安定動作の為に大切です。


 実際結構右往左往しましたが、サーボモータをキュイキュイと動かす事ができました。楽しい遊びアイテムがまた一つ増えてホクホクです(^-^)