317.PICでサーボを動かす(PIC16F873)


2003/01/10 【ソフトウエア編TOPに戻る】

今回はPIC16F873を使ってラジコンサーボを動かしてみます。ラジコンサーボの動かし方は112.ラジコンサーボモータを動かすのページを参照して頂くとして、単純に言えば20mSの周期で約1mS〜2mSのパルス幅を出力させれば良いので、全然難しいことはありません。このようなパルス波形はPWM波形と呼ばれ、PIC16F873CCP機能を使うことによって出力させることができます。CCP機能を使うと、タイマー2のTMR2(8bitカウンタレジスタ)のカウント値がPR2(周期レジスタ)に設定した値と一致するまでの時間がPWMの周期になり、同じくCCPRL(CCP下位8bitレジスタ)とCCPCON(CCPコントロールレジスタ)内の2bit、あわせて10bit分に設定した値が、プリスケーラを含むTMR2の10bit分と比較されてパルス幅となります。(ちょっとややこしいので、PICのデータシートを参照して下さい)

でもちょっと待った。TMR2は8bitのカウンタであり、設定できるプリスケーラは最大で1:16までです。例えばPICのマスタークロックの周波数が10MHzの場合、PWM周期は最長でも1.6mS程度までしか稼げません。今回、サーボモータを動かすためには20mS程度の周期が必要になるため、これでは全然足りないことになります。クロックの周波数をぐっと下げれば対応できるのでしょうが、本位ではありません。

そこで、ここではタイマー1の16bitカウンタTMR1を使って、コンペアマッチ割込みによるPWM波形の生成をしてみようと思います。また、I2C通信を使って複数のサーボモータを制御する事例もご紹介します。CCP機能によるPWM波形の出力については、また別の機会にご紹介することにしましょう。


317−1.タイマー1によるPWM波形の生成

タイマー1のコンペアマッチ割込みについては、315.タイマー1でコンペアマッチ割込みのページで既にご紹介していますので、動作の詳細についてはそちらを参照して下さい。ただし、そちらではアセンブラによるプログラム例のご紹介でしたが、今回はCCSのCコンパイラを使ったC言語でプログラムを組んでみました。

今回のプログラム動作を簡単に表すと、上図のような動作ということになります。

TMR1タイマーカウンタを0x0000からカウントアップさせ、CCP1CCP2にセットした値とのコンペアマッチで発生するそれぞれの割り込みを利用して、RCポート2の端子からPWM波形を出力させます。

 

■CCP2のコンペアマッチ割込み処理

CCP2のコンペアマッチで発生する割り込み処理では、RCポート2の端子からを出力させて、TMR10x0000にクリアします。

 

■CCP1のコンペアマッチ割込み処理

CCP1のコンペアマッチで発生する割り込み処理では、単純にRCポート2の端子の出力をにクリアしているだけです。

 

PICのマスタークロックを10MHz、プリスケーラを1:1にした場合、CCP250000 (0xC350)という値をセットすることによって、PWM周期は20mSになります。 (4μS x 50000 = 20mS) 同様に、CCP12500(0x09C4)〜5000(0x1388)の値をセットすることによって、パルス幅は1.0mS〜2.0mSとなります。通常、周期は一定とするので、プログラム中でCCP1の値を書き換えてあげるだけで、サーボモータの首振りをさせることができます。

ちなみに、CCP1CCP2のレジスタは上位8bit(CCPxH)下位8bit(CCPxL)に分かれており、アセンブラでは個別に値のセットをしてあげなければなりませんが、C言語上ではlong型CCP1CCP2というひとつの変数として、0xFFFFまでの値を直接書き込むことができます。

【先頭に戻る】


317−2.サンプルプログラムの紹介 picpwm1.c

 

では、早速ご紹介した動作でサーボを動かすサンプルプログラムをご紹介します。RCポート2の端子から、1秒ごとに1.5mS/1.0mS/2.0mSの幅のパルス出力を繰り返すものです。RCポート2の端子にサーボモータのコントロール端子を接続すると、勝手に首振りを開始します。

実験を行ったハードウエアは、9.PIC学習用ボードのRCコネクタを、8.簡易型実験用I/Oボードのサーボ用コネクタに接続しているものです。

サンプルプログラムpicpwm1.cのソースは、こちらをクリックするとダウンロードできます。おまけに直接PICに流し込めるヘキサファイルpicpwm1.hexも、こちらからダウンロードできるようにしておきます。ヘキサファイルはPIC16F873専用ですので、ご注意ください。

//////////////////////////////////////////////////
// PWMテストプログラム1                                         //
// PIC16F873                                RIKIYA 2002.12.15  //
//                                                          picpwm1.C  //
// TMR1をつかってサーボーモータ用のPWM波形を出力  //
// CCP2コンペアマッチ割込で周期20mSを生成             //
// CCP1コンペアマッチ割込でパルス幅を生成               //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                       // clock 10MHz
#use fast_io(C)                                          // 固定入力モード

void ccp1_int(void);                                     // プロトタイプ
void ccp2_int(void);                                     // プロトタイプ


//メイン関数////////////////////////////////////
main(){
      setup_timer_1(T1_INTERNAL | T1_DIV_BY_1);
      setup_ccp1(CCP_COMPARE_INT);        // CCP1コンペアマッチ割込み設定
      setup_ccp2(CCP_COMPARE_INT);        // CCP2コンペアマッチ割込み設定
      set_timer1(0x0000);                           // TMR1クリア
      CCP_1 = 0;                                      // パルス幅 0
      CCP_2 = 50000;                               // 周期 0.4u * 1 * 50000 = 20mS
      set_tris_c(0x00);                               //RC 7-0:OUT

      enable_interrupts(INT_CCP1);             //CCP1コンペアマッチ割込み許可
      enable_interrupts(INT_CCP2);             //CCP2コンペアマッチ割込み許可
      enable_interrupts(GLOBAL);              //全設定割込み許可

      while(1){
            CCP_1 = 3752;                         //パルス幅 0.4u * 3752 = 1.5mS
            delay_ms(1000);
            CCP_1 = 2500;                         //パルス幅 0.4u * 2500 = 1.0mS
            delay_ms(1000);
            CCP_1 = 5000;                         //パルス幅 0.4u * 5000 = 2.0mS
            delay_ms(1000);
      }
}

//パルスクリア///////////////////////////////////
#INT_CCP1
void ccp1_int(){
      output_bit(PIN_C2,0);                      // RC3BITを0にする。
}

//パルス出力/////////////////////////////////////
#INT_CCP2
void ccp2_int(){
      if(CCP_1 != 0x0000){                     // CCP_1が0でなければ、
            output_bit(PIN_C2,1);              // RC3BITを1にする。
      }
      set_timer1(0x0000);                      // TMR1クリア
}

■プログラムの説明

ここでは細かな説明は省略させて頂きますが、各部の概要についてご説明します。

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                       // clock 10MHz
#use fast_io(C)                                          // 固定入力モード

お決まりのようなプリプロセッサ部分です。

 

void ccp1_int(void);                                     // プロトタイプ
void ccp2_int(void);                                     // プロトタイプ

プロトタイプは、main関数以外で作成している関数の型について明示しています。CCSのCコンパイラは、このプロトタイプをちゃんと書いておかないとエラーになります。

 

main関数の始めの部分は、タイマー1を動かすための設定が記述されています。

      setup_timer_1(T1_INTERNAL | T1_DIV_BY_1);

では、タイマー1を内部クロックで動作させ、プリスケーラ1:1だと設定しています。

      setup_ccp1(CCP_COMPARE_INT);        // CCP1コンペアマッチ割込み設定

では、CCP1をコンペアマッチで割り込みを発生させるように設定しています。

      setup_ccp2(CCP_COMPARE_INT);        // CCP2コンペアマッチ割込み設定

では、同様にCCP2をコンペアマッチで割り込みを発生させるように設定しています。

      set_timer1(0x0000);                           // TMR1クリア

では、TMR1タイマーカウンタに0x0000という16bitの数値を書き込んでいます。

      CCP_1 = 0;                                      // パルス幅 0
      CCP_2 = 50000;                               // 周期 0.4u * 1 * 50000 = 20mS

では、CCP1とCCP2の16bitレジスタに、それぞれ0と50000という数値を書き込んでいます。CCP_1CCP_2という変数はlong型で、CCSのCコンパイラで定義されています。

      set_tris_c(0x00);                               //RC 7-0:OUT

は、RCポートの入出力を設定するためのtriscレジスタに値を書き込むための組み込み関数です。今回は全て0を書き込んで、全て出力に設定します。

 

      enable_interrupts(INT_CCP1);             //CCP1コンペアマッチ割込み許可
      enable_interrupts(INT_CCP2);             //CCP2コンペアマッチ割込み許可
      enable_interrupts(GLOBAL);              //全設定割込み許可

では、CCPによる割込みを許可し、あわせて全体の割込みも許可しています。これをしないと今回のプログラムは割り込みが発生せず動作しません。

 

      while(1){
            CCP_1 = 3752;                         //パルス幅 0.4u * 3752 = 1.5mS
            delay_ms(1000);
            CCP_1 = 2500;                         //パルス幅 0.4u * 2500 = 1.0mS
            delay_ms(1000);
            CCP_1 = 5000;                         //パルス幅 0.4u * 5000 = 2.0mS
            delay_ms(1000);
      }

この部分で、パルス幅を変化させ、サーボの首の振り方を決めています。CCP_1 = の部分で値に変化を持たせているだけです。delay_ms( )は、引数にミリ秒の数値を入れてあげると、ループ処理で待機状態になります。つまりここでは1秒待機ということになります。プリプロセッサブ部分で#use delay(clock = 10000000)を記述してあげることによって使える関数です。

 

//パルスクリア///////////////////////////////////
#INT_CCP1
void ccp1_int(){
      output_bit(PIN_C2,0);                      // RC3BITを0にする。
}

この部分がCCP1のコンペアマッチによって発生する割り込み処理です。#INT_CCP1の直後の関数が、CCP1による割込み時に実行されることになっています。内容はRCポートのbit2をクリアしているだけです。

 

//パルス出力/////////////////////////////////////
#INT_CCP2
void ccp2_int(){
      if(CCP_1 != 0x0000){                     // CCP_1が0でなければ、
            output_bit(PIN_C2,1);              // RC3BITを1にする。
      }
      set_timer1(0x0000);                      // TMR1クリア
}
この部分がCCP2のコンペアマッチによって発生する割り込み処理です。上と同様に、#INT_CCP2の直後の関数が、CCP2による割込み時に実行されることになっています。CCP2はPWM周期を決定するところなので、RCポートのbit2を1にして、タイマーカウンタTMR1をクリアしています。また、CCP_1の値が0だった場合は、パルスを出力させないという処理も入れています。

【先頭に戻る】


317−3.サンプルプログラムpicpwm1.cの実験風景

左の写真が実験風景です。右がPICの実験基板、中央がI/O基板で左がサーボです。PICは16F873を実装しています。中央のI/O基板は色々実装されていますが、使っているのは最上部のコネクタ変換部分だけです。ここの3チャンネル目のコネクタにサーボを接続して動かしています。

サーボは元気よく左右中央への首振りを繰り返します。

 

【先頭に戻る】


317−4.I2Cを使った複数サーボの制御例 picpwm2m.c/picpwm2s.c

さて、上でご紹介したプログラムだけでは芸がないので、I2C通信と組み合わせてもうすこしだけ高度な動作をさせてみましょう。

ひとつのマスターとなるPICから4個のスレーブとなるPICに対してデータを送り、スレーブのPICが受信したデータに基づいてサーボの首振りを行うということをしてみます。つまり、PICを使った分散制御への第一歩といったところです。I2C通信プログラミングの詳細については、316.PICでI2C通信のページを参照して下さい。

サンプルプログラムはマスター用とスレーブ用の2種類あります。マスター用こちらからダウンロードできます。picwm2m.c スレーブ側こちらからダウンロードできます。picpwm2s.c おまけに、直接PIC16F873に流し込めるヘキサファイルも、それぞれダウンロードできます。マスター用picpwm2m.hexスレーブ用picpwm2s.hex

以下にマスター用とスレーブ用それぞれのソースファイルを簡単にご説明します。

 

■マスター用プログラム picpwm2m.c

//////////////////////////////////////////////////
// PWMテストプログラム2                                        //
// PIC16F873                                 RIKIYA 2002.12.19 //
//                                                         picpwm2m.C //
// I2Cマスター役となりスレーブ4個のPWM制御をする      //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000) // clock 10MHz
#use fast_io(B) // 固定入力モード
#use i2c(MASTER,SDA=PIN_C4,SCL=PIN_C3,FAST,FORCE_HW)

int p_width[3] = {0,125,250};                   //パルス幅1.0, 1.5, 2.0mS
int add[4] = {0,2,4,6};                            //スレーブアドレス

//メイン関数////////////////////////////////////
main(){
int i,j;
      set_tris_b(0xf0);                             //RB 7-4:IN 3-0:OUT
      set_tris_c(0x00);                            //RC 7-0:OUT
      output_float(PIN_C3);                     //I2C pin float
      output_float(PIN_C4);                     //I2C pin float

// 4個のサーボの首振りを繰り返す
      while(1){
            for(j=0;j<3;j++){
                  for(i=0;i<4;i++){
                        i2c_start();                 //スタートコンディション
                        i2c_write(add[i]);         //アドレスaddに送信
                        i2c_write(p_width[j]);    //データを送信
                        i2c_stop();                 //ストップコンディション
                  }
                  delay_ms(1000);               //1000mS待機
            }
      }
}

マスターは、スレーブに対してサーボの首振りデータを送信しますが、

int p_width[3] = {0,125,250}; 

の配列変数で、そのデータテーブルを準備しています。今回のI2C通信では1バイトのデータしか送らないため、0〜255の値ということになります。後でご紹介しますが、スレーブ側では受け取った0〜255のデータをもとにうまくサーボの首振りができるように出力するパルス幅の調整を行っています。

int add[4] = {0,2,4,6}; 

この配列変数は、接続されるスレーブのアドレスを格納しています。

実際のデータ送信は、2重のfor( )文の中で行っています。慣れないと動作を追うのが大変かもしれませんが、この手の複数の(ネストが深い)ループ処理は良く用いられます。スレーブが沢山ぶら下がると、その数だけ送信処理を行わなければなりませんが、このように配列変数のデータテーブルとループ処理を組合せると、すっきりと記述させることができます。

この2重のfor( )文では、アドレス0,2,4,6全てのスレーブに対して順に0というデータを送信し、1秒待機したあと全てのスレーブに125というデータを送信します。で、1秒待機してから今度は250というデータを全てのスレーブに送信するという動作を永遠に繰り返します。

【先頭に戻る】

 

■スレーブ用プログラム picpwm2s.c

//////////////////////////////////////////////////
// PWMテストプログラム2                                        //
// PIC16F873                                 RIKIYA 2002.12.20 //
//                                                          picpwm2s.c //
// I2Cスレーブ役としてマスターからのデータに               //
// 基づいてサーボモータを動かす。                             //
// スレーブアドレスは、PB4〜7ビット                            //
// から読み込むデータで自動設定する                        //
// I2C受信割り込み処理あり。                                    //
//////////////////////////////////////////////////


#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000) // clock 10MHz
#use fast_io(B) // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

void add_set(int add);                                // プロトタイプ宣言
void ccp1_int(void);                                   // プロトタイプ
void ccp2_int(void);                                   // プロトタイプ

//メイン関数/////////////////////////////////////////
void main (){
long data;
      setup_timer_1(T1_INTERNAL | T1_DIV_BY_1);
      setup_ccp1(CCP_COMPARE_INT);     // CCP1コンペアマッチ割込み設定
      setup_ccp2(CCP_COMPARE_INT);     // CCP2コンペアマッチ割込み設定
      set_timer1(0x0000);                        // TMR1クリア
      CCP_1 = 3752;                              // パルス幅 0.4u * 1 * 3752 = 1.5mS
      CCP_2 = 50000;                            // 周期 0.4u * 1 * 50000 = 20mS

      set_tris_b(0xf0);                            //RB 7-4:IN 3-0:OUT
      set_tris_c(0x18);                           //RC 4,3:IN 7-5,2-0:OUT
      output_float(PIN_C3);                    //I2C pin float
      output_float(PIN_C4);                    //I2C pin float
      output_b(0x01);                            //正常起動確認

// スレーブアドレス設定
      add_set((input_b() >> 3) & 0x0e);

      enable_interrupts(INT_CCP1);         //CCP1コンペアマッチ割込み許可
      enable_interrupts(INT_CCP2);         //CCP2コンペアマッチ割込み許可
      enable_interrupts(GLOBAL);           //全ての割り込みを許可

      while (1) {
            data = i2c_read();                   //データ受信
            CCP_1 = 2500+(data*10);        //パルス幅の設定
            output_b(data);                      //受信データのモニタ
      }
}

//パルスクリア///////////////////////////////////
#INT_CCP1
void ccp1_int(){
      output_bit(PIN_C2,0);                    // RC3BITを0にする。
}

//パルス出力/////////////////////////////////////
#INT_CCP2
void ccp2_int(){
      if(CCP_1 != 0x0000){                    // CCP_1が0でなければ、
            output_bit(PIN_C2,1);             // RC3BITを1にする。
      } 
      set_timer1(0x0000);                     // TMR1クリア
}

//アドレス自動設定////////////////////////////////////
void add_set(int add){
int *sspadd = (int *)147;       // 変数*sspaddにSSPADDのアドレス147をセット
      *sspadd = add;             // SSPADDレジスタにアドレスをセット
}

スレーブ側については、このページの始めにご紹介したサンプルプログラムpicpwm1.cの内容と、316.PICでI2C通信のページでご紹介している割り込みなしのスレーブ側プログラムを組み合わせているだけなので、説明は省略させて頂きますが、以下に特記事項だけをお話します。

■受信データの処理

今回受信するのは1バイトのデータとしますので、0〜255ということになります。出力するPWM波形のパルス幅を1.0mS〜2.0mSの間で可変させようとしたとき、パルス幅を決定するCCP1の値は、2500〜5000となるので、以下の計算式で受信データからCCP1に設定する値を求めています。

            CCP_1 = 2500+(data*10);        //パルス幅の設定

本当は、サーボの首振りをもっと一杯まで行えるように、追い込まなければならないところですが、今回は簡易的に上記の計算式としています。従って、0〜250の間でデータを受ければ、安定した首振り動作が保証されます。

なお、変数dataの型は、int型では正常に動作せず、long型でなければ正しい計算結果になりません。ご注意ください。

 

■I2Cの受信処理は割り込みなし

PICでは、基本的に割り込み処理実行中に別の割込み処理を実行させることは出来ません。今回、パルスを発生させるためにコンペアマッチ割込みを使用しているため、I2Cの受信処理に割り込みを利用することができません。コンペアマッチ割込みが発生するタイミングと、I2C受信で割り込みが発生するタイミングを完全に管理でき、割り込みが重ならないような処理にできれば両者とも割り込みを使ってかまいませんが、I2Cの受信タイミングはランダムに発生するため、そこまでは管理できません。

I2C受信を割り込み処理で行おうとすると、正常に動作しませんのでご注意ください。

【先頭に戻る】


317−5.I2C制御の実験風景 picpwm2m.c/picpwm2s.c

左の写真が実験風景です。右がPICの実験基板、中央がI2Cの実験基板、左がI/O基板です。PICはすべて16F873を実装しており、右の基板のPICがマスター用で、中央の基板のPIC4個がスレーブ用です。スレーブ4個分のパルス出力が左の基板でコネクタ変換され、各サーボモータに接続されています。マスターとスレーブ間のケーブルは、1mのものでも安定して動作しました。

プログラムを実行すると、4個のサーボが同時に元気良く首振りを開始します。

【先頭に戻る】


実はI2C通信で2バイトのデータを送りたかったのですが、I2Cの動作が不安定(というかクセを掴みきれずに)で、やむなく1バイトで妥協しました。I2Cの複数バイトの一括送受信は、もう少し勉強が必要のようです。

まあ、それは別として、I2C通信と組み合わせてサーボの制御をすることによって、マスターとなるCPUのPWM出力のチャンネル数に制限されることなく、多くのサーボをコントロールできるようになります。あとは動作の安定性がうまく確保できれば、それなりに使えるシステムになりそうですよ。

 

【ソフトウエア編TOPに戻る】


【表紙に戻る】