9.PWMでモーターを動かしてみよう


ここでは、モーターを動かします。


PWM
CdSPWM


子供のころモーターで遊んだ時に電池を一杯直列に繋ぎ、モーターを高速に動かして遊んだ人もいるかもしれません。多く繋ぎすぎると何やら匂ってきてモーターが壊れたりします。電圧でモーターの速度を変えるとこんな事件も起こります。
コンピュータではモーターを電圧制御するのは不得意です。できますがお金がかかります。
簡単にモーターのスピードを変える方法はないでしょうか?
コンピュータは、0と1の世界。電流を流すか流さないかのスイッチの世界です。もし、一秒電流を流し、一秒電流を流さないとしましょう。するとモーターは動いたり止まったりします。この一秒を0.1秒にしてみます。すると止まる暇もなくまた、完全に高速にもなりません。従って遅く回ります。今度は電流が流れる時間を短くしてみます。するとどんどんと遅くなっています。このような考え方でモーターのスピードを制御する方法をPWM制御(pulse width modulation)と言っています。

PWM

タイマーの値を利用してPWM制御でLEDを高速で点滅させてみます。
だんだん明るくなったり、だんだん暗くなったりすることを繰り返します。

以下のプログラムをコピーして、貼り付けて使用してください。

「PWM」プログラム
// ファイル名 PWM.c
// 16ms周期のPWM波形を作り、赤のLEDの明るさをコントロールする
#include <htc.h>
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

#define _XTAL_FREQ 20000000
#define PERIOD 100

/*
20MHz/4 = 5MHz
TMR0 * prescale*counter/5MHZ
= (256-PERIOD) * 256*2/5MHZ 
= (256-100) * 256*2/5MHZ = 16m秒
*/

unsigned char counter=0;
void interrupt Intr(void)
{
  // 割り込みがtimer0から来る
  counter++;
  TMR0 = PERIOD;
  T0IF = 0;                 // 割り込みフラグクリア
  return;
}
main()
{
  unsigned int i, j;
  PORTA = 0;                // PORTAを0にする
  TRISA = 0;                // PORTAを出力に設定する
  ANSEL = 0;                // アナログ入力をOFF
  
  // Timer0 の設定
  OPTION = 0xd0;            // FOSC/4 プリスケーラ1/2
  T0IE=1;                   // Timer0 の割り込みON
  T0IF=0;                   // 割り込みフラグクリア
  GIE=1;                    // CPUへの割り込みON
  while(1) {
    for(i=0; i<255; i++) {
      for(j=0; j<100; j++) {
        if(counter <= i) RA0 = 1;
        else RA0 = 0;
      }
    }    
    for(i=255; i>0; i--) {
      for(j=0; j<100; j++) {
        if(counter <= i) RA0 = 1;
        else RA0 = 0;
      }
    }    
  }
}

if(counter <= i) RA0 = 1;
else RA0 = 0;
1周期(16ms)を256分割して、0から255の時間に分けます。例えば190よりも小さいときに1、 それ以外の場合には0とすれば下図のようになります。(下図の場合発振器の都合で周期が約17msになっています。1目盛が5msです)

CdSPWM

 次は、CdSの値でモーターを制御してみます。
動作は、言葉で説明するより、次のビデオを見た方が分かりやすいでしょう。
DCモーターを動かすためにTA7291PというICを用いています。これはフルブリッジドライバーと呼ばれ、モーターを正逆回転をさせることができます。
下の図で、OUT1とOUT2の間にDCモーターをつなぎ、IN1とIN2の2ビットをコンピュータでコントロールします。CWはClockwise(時計回り)、CCWはCounterclockwise(反時計回り)の略です。また、ハイインピーダンスとは、出力が切断され、電流が流れない状態のことを言います。もし切断される前にモーターが回っていたら惰性で暫く回ります。これに対して、ブレーキは積極的に電流を流しません。
右のモーター用にポートCの4と5を用い、左のモーター用には同様にポートCの6と7を用います。
ポートCは下の4bitをADコンバータの入力、上位4bitをモーターコントロール用で出力に設定します。
プログラム中に以下の定義がありますが、これはモーターのスピードです。0~255まで設定可能ですが、小さすぎる値とか大きすぎる値では正常に動作しません。この辺は各自の好みでいじってみてください。これからのサンプルでも値が異なる場合がありますが、気にしないで自分の設定を使って自分の好みのロボットを作ってみましょう。
#define SLOW     35
#define LOW      40
#define MIDDLE   80
#define MIDDLE2 120
#define HIGH    160

以下のプログラムをコピーして、貼り付けてください。

「CdSPWM」プログラム
// ファイル名 CdSPWM.c
// CdSに手をかざすとそれに対応してモーターが動く
// 左前のCdSだけに手をかざすと押される感じに車体が右に旋回する(赤のLED点灯)
// 右前のCdSだけに手をかざすと押される感じに車体が左に旋回する(緑のLED点灯)
// 後ろのCdS場合はバックする(青のLED点灯)
// 全部かざすと前進する。(白のLED点灯)
// これをタイマー0割り込みを用いて実現する
// 関数にして機能分担化させる
// 自動閾値設定(初期値95%)を電源投入直後に行うのでCdSの上には障害物
// (手など)はあってはいけない。

#include <htc.h>
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

// マクロ宣言
#define true  1
#define false 0
#define _XTAL_FREQ 20000000
#define ANS4 0x10
#define ANS5 0x20
#define ANS6 0x40
#define AN4  0x11
#define AN5  0x15
#define AN6  0x19
#define PERIOD 100

/*********************************************/
/* モーターをPWMで動かす時に利用するグローバル変数の
   counter変数のupdate間隔の計算

20MHz/4 = 5MHz
TMR0 * prescale*counter/5MHZ
= (256-PERIOD) * 2*256/5MHZ 
= (256-100) * 2*256/5MHZ = 16m秒
*/
//***************************
#define SLOW     35
#define LOW      40
#define MIDDLE   80
#define MIDDLE2 120
#define HIGH    160
//**************************
typedef unsigned char byte;

// プロトタイプ宣言
void initialize();
byte checkCdS();
void setThreshold(byte th);
void overHand();
//******************

// グローバル変数の宣言
byte counter=0;
byte LeftCdS, CenterCdS, RightCdS;
byte ADch=0;
byte ConvEndFlag;
byte ADC[3]={AN4,AN5,AN6};
byte threshold[4];

//************************
byte Leftspeed=208;
byte Centerspeed=208;
byte Rightspeed=208;
//************************
// 割り込み処理ルーチン
void interrupt Intr(void)
{
  //  (256-PERIOD)*256/5MHz
  // =(256-100)*2/5MHz = 62.4μs
  // 62.4μs毎に割り込む
  if(T0IF) {       // 割り込みがtimer0からか?
    counter++;
    TMR0 = PERIOD;
    T0IF = 0;                 // 割り込みフラグクリア
    return;
  }
  if(ADIF) {      // 割り込みがADコンバーターからか?
    if(ADch==0) LeftCdS = ADRESH;
    else if(ADch==1) CenterCdS = ADRESH;
    else if(ADch==2) RightCdS = ADRESH;
    ConvEndFlag=true;         // データの更新あり
    ADIF = 0;                 // 割り込みフラグクリア
  }
}
  
main()
{
  initialize();               // 各種初期設定
  // 永久ループ
  while(true) {  
    overHand();               // CdSでモーターを動かす
     //checkCdS();
  }  
}

void initialize()
{
  PORTA = 0;                  // PORTAを0にする
  TRISA = 0;                  // PORTAを出力に設定する
  ANSEL = ANS4 | ANS5 | ANS6; // PORTAアナログ入力をAN4-6をON
  ADCON0 = AN4;               // ADFM=0(左詰) ADon
  OPTION = 0;                 // FOSC/4 プリスケーラ1/2
                              // 20MHz/4/2 = 2.5MHz
  PORTC = 0;                  // PORTCを0にする
  TRISC = 0x0f;               // PORTC下4bit入力、上4bit出力
  ANSELH = 0;                 // PORTCアナログ入力をOFF
  
  // Timer0 の設定
  T0IE=1;                     // Timer0 の割り込みON
  T0IF=0;                     // 割り込みフラグクリア
 
  setThreshold(65);
  
  // ADコンバーターの設定
  ADIE = 1;                   // ADコンバータの割り込み許可
  ADIF = 0;                   // 割り込みフラグクリア
  PEIE = 1;                   // 周辺装置の割り込み許可
  GIE  = 1;                      // CPUへの割り込み許可
  GODONE = 1;                 // AD変換スタート
}

byte checkCdS()
{
  byte status;

  if(ConvEndFlag) { // AD変換が終わったらすぐに次のチャンネルの変換を行う
    ConvEndFlag = false;
    if(++ADch==3) ADch=0;
      ADCON0 = ADC[ADch];     // ADチャンネル変更
      GODONE = 1;             // AD変換スタート
  }
    
  PORTA=0;
  if(RightCdS<threshold[2])  RA2=1;  // 赤
  if(CenterCdS<threshold[1]) RA1=1;  // 緑
  if(LeftCdS<threshold[0])   RA0=1;  // 青
  
  status = RightCdS<threshold[2];
  status += (CenterCdS<threshold[1])?2:0;
  status += (LeftCdS<threshold[0])?4:0;
  return status;
}

void setThreshold(byte th)    // 平均の指定%を閾値にする
{
  byte i, j, x, g;
  int average[3]={0,0,0};
  g = GIE;                    // 割り込み状態の保存
  GIE=0;                      // 割り込みは使わない
 // 明るいときの値 明るいと大きい値
  ADC[0]=AN4;
  ADC[1]=AN5;
  ADC[2]=AN6;
  for(j=0; j<40; j++) {       // 平均の明るさを求める
    for(i=0; i<3; i++) {
      ADCON0 = ADC[i];
      GODONE = 1;
      while(GODONE);
      x = ADRESH;
      average[i] = (average[i]*9+x)/10;
    }
  }
  for(i=0; i<3; i++)
    threshold[i]=average[i]*th/100;
  GIE = g;                    // CPUへの割り込み状態を戻す
}

void overHand()
{
  while(RA3) {
    switch(checkCdS()) {
    case 7: Rightspeed=Leftspeed=MIDDLE;    // 直進
            RC5 = RC7 = 0;
            break;
    case 6: Rightspeed = LOW;  Leftspeed = MIDDLE;
            RC5 = 0; RC7 = 0;
            break;

    case 4: Rightspeed = ~MIDDLE;  Leftspeed = MIDDLE;
            RC5 = 1; RC7 = 0;
            break;
    case 3: Rightspeed = MIDDLE; Leftspeed = LOW;
            RC5 = 0; RC7 = 0;
            break;
    case 1: Rightspeed = MIDDLE; Leftspeed = ~MIDDLE;
            RC5 = 0; RC7 = 1;
            break;
    case 2:
            Rightspeed=Leftspeed=~MIDDLE;
            RC5 = RC7 = 1;
            break;
    default:
    case 0: Rightspeed=Leftspeed=255;
            RC5 = RC7 = 1;
            break;

    }
    RC4 = counter<Rightspeed;
    RC6 = counter<Leftspeed;
  }
}

byte checkCdS()
ADコンバータの値、つまりCdSの手のかざし加減を見てどのCdSの上に手があるかを知らせます。
void overHand()
上記checkCdS()から知らされた値でモーターを指定通りに動かします。
RC4 = counter<Rightspeed;
RC6 = counter<Leftspeed;
ここで左右のモーターをPWM制御をしています。
counter<Rightspeedが正しかったらRC4に1、そうでなかったら0が代入されます。
Rightspeed = ~MIDDLE;
RC5 = 1;
モーターを逆転させるには、RC5を1にしなければいけませんが、同時にRC4の波形を逆転させなければいけません。例えば正転で100の期間Highだった場合はLowは156になります(8bit, 全部で256だから)。この逆転と言うことは156Highにして100Lowにすることです。問題は156をどうやって高速に求めるかです。256-100でいいのですが、非力のCPUでは時間とメモリーを使ってしまいます。以下に簡単でメモリーを使わないでプログラムをするかが問題です。これは簡単で、100のbit反転すればいいだけです。誤差が1出ますが(155になる)モーターを動かすな使用法では全く問題はありません。~MIDDLEはMIDDLEのbit反転を意味します。

目次へ戻る