PWMを利用したサーボモータ制御プログラミング:H8で学ぶマイコン開発入門(11)(2/3 ページ)
PWMを利用したモータ制御の仕上げとして、ロボットの関節などに使われるサーボモータのプログラミングを解説する。
ターゲットボードとサーボモータの接続
ターゲットボードとサーボモータは、図3のように接続されています。実はH8マイコンとサーボモータは直接接続されています。今回はPLDもドライバICも使用しておらず、接続自体はこれまでのモータの中では一番シンプルです。サーボモータはパルス信号の幅によって、どの角度に回転するかをモータ自身が決めるので、H8マイコンからのPWM信号がモータの駆動電圧の規格にきちんと収まっていれば、このような接続でも不都合はありません。
H8マイコンからの信号の出力はITUのチャネル0と1を使います。それぞれのチャネルで1つのサーボモータを動かすようにしています。ITUのチャネル0と1の信号はTIOCA0、1端子から出力されます。この信号はポートAの2番、4番の端子と兼用ですが、ITUのモード設定レジスタでITUをPWMモードにすることで、自動的にTIOCA信号として使用されるようになります。
サーボモータ初期化プログラム
それではPWMでサーボモータを制御するためのプログラムを説明していきます。処理としては前回のDCモータを制御するプログラムと大きくは変わりません。まずはサーボモータの初期化プログラムです(リスト1)。
// PWM制御のための構造体 struct s_pwm_motor { int on; // モータがONかOFFかを記憶しておくための変数 unsigned int cycle; // PWMの周期(usec、GRBの設定値) unsigned int trig; // PWM波をHighにするきっかけ(GRAの設定値) }; ……(1) struct s_pwm_motor ServoSet[2]; //---------------------------------------------------------------------- // ServoInit //---------------------------------------------------------------------- // [説明] // PWMを初期化します // PWM関数を使用する前に1度呼び出してください // PWM出力を使用する場合、H8のPA2、PA4はPWM出力ピンに設定されます // [入力] // なし // [出力] // なし // [戻り値] // なし //---------------------------------------------------------------------- void ServoInit(void) { int i; // 変数初期化 for(i=0; i<2; i++) ……(2) { ServoSet[i].on = FALSE; ServoSet[i].cycle = 0; ServoSet[i].trig = 0; } // ITU0/1の初期設定 ITU.TSTR.BIT.STR0 = 0; ……(3) ITU.TSTR.BIT.STR1 = 0; ITU.TSNC.BIT.SYNC0 = 0; ITU.TSNC.BIT.SYNC1 = 0; ITU.TMDR.BIT.PWM0 = 0; ……(4) ITU.TMDR.BIT.PWM1 = 0; ITU0.TCR.BYTE = 0x43; ……(5) // CCLR1を1(GRBでTCNTクリア)、 // TPSC1と0を1(内部クロックΦ/8) ITU0.TIOR.BYTE = 0x00; ITU0.TIER.BYTE = 0x00; ITU1.TCR.BYTE = 0x43; // GRBでTCNTクリア、Φ/8 ITU1.TIOR.BYTE = 0x00; ITU1.TIER.BYTE = 0x00; }
今回も前回と同様に、モータ制御用の構造体を初期化した後(1)(2)、H8マイコンのITU機能に関するレジスタを初期化しています。タイマスタートレジスタのスタートビットを0にすることで、ITUのタイマは停止状態になっています(3)。タイマモードレジスタのPWMビットもこの時点では0にしてあり、TIOCA端子はPWMモードではありません(4)。
前回と同様、タイマコントロールレジスタに0x43をセットすることでITUのカウンタが内部クロックの1/8でカウントするようにセットし、ITUのカウンタがGRBレジスタの値と一致したときにカウンタをクリアするようにしています(5)。
PWM波の周期とデューティ比を設定
そして、リスト2の関数でPWM波の周期とデューティ比を設定します。
//---------------------------------------------------------------------- // ServoSetCycle //---------------------------------------------------------------------- // [説明] // サーボモータ用のPWM波の周期とデューティ比を設定します // ITUのPWMモードによりサーボモータを駆動します // 制限事項としてduty=100を指定しても出力はHIGH固定となりません // 非常に短いLOWパルスが発生します // ただし、duty=0とした場合は出力はLOW固定となります // [入力] // ch :設定するモータを0,1で指定します // cycle :PWMの周期をusec単位で指定します(0〜21844) // duty :duty比を指定します(0〜100) // [出力] // なし // [戻り値] // なし //---------------------------------------------------------------------- void ServoSetCycle(int ch, unsigned int cycle, int duty) { // 設定値取得 Set_cycle(&ServoSet[ch], cycle, duty); if(ServoSet[ch].on) { // 一度停止してから再スタートする ServoOn(ch, FALSE); ServoOn(ch, TRUE); } }
この関数への引数として、サーボモータの番号とPWM波の周期とデューティ比を与えます。周期とデューティ比は初期化プログラムのリストにある構造体に記憶させます。また、サーボモータがすでに駆動中の場合、この関数に与えられたPWM波の設定を反映させるためにServoOn()関数を呼び出します。
構造体に周期とデューティ比を記憶させるSet_cycle()関数が何を行っているかは前回も説明しましたが、もう一度触れておきます(リスト3)。
void Set_cycle(struct s_pwm_motor *m, unsigned int cycle, int duty) { if(cycle > 0x5554) cycle = 0x5554; ……(1) // ターゲットの内部クロックΦ=24.576MHz // 初期化関数内でITUはΦ*1/8で動作するように設定しているので約3MHz // cycleはusec単位なので3倍する cycle = cycle * 3; m->cycle = cycle; ……(2) if(duty <= 0) { ……(3) // デューティ比が0のときはPWM波をHighにしないために周期より大きく取る m->trig = cycle + 1; } else if(duty>=100) { // デューティ比が100のときはPWM波を常にHighにする m->trig = 0; } else { // それ以外のときは周期*(100−デューティ比)%の間Lowとなる値をセットする m->trig = (cycle/100)*(100-duty); } }
変数cycleはPWM波の周期を表しますが、ITUのGRBレジスタにセットする値でもあります。ServoInit()関数の中で、ITUのカウンタがGRBレジスタの値と一致したらカウンタをクリアするように設定しましたので、PWM波は0から変数cycleの値を1周期として出力されます。また、カウンタがGRBレジスタと一致した際にPWM波はLowが出力されます。
次に引数dutyはPWM波のデューティ比を表します。引数cycleにこの値(0〜100)を掛け合わせた値が構造体に記憶され、最終的にはITUのGRAレジスタに設定されます(3)。ITUのカウンタがGRAレジスタの値と一致したとき、PWM波はHighが出力されますので、正確にはITUのカウンタ値がGRAからGRBの間のときが引数dutyで指定された割合となります。
リスト中の(1)では、構造体に設定するPWM波の周期について上限値を定めています。これは(2)の処理で引数cycleの値を3倍してもバグが起きないための処理です。ITUのGRA、GRBレジスタは16bitレジスタですので、0x5554は3倍しても0xffffを超えないギリギリの値ということになります(0x5555を3倍しても0xffffとなり、問題はなさそうですが、デューティ比を0にしたいときにGRAに設定できる値がなくなり、不都合が起こります)。
なぜ3倍しているかは主にハードウェア的な理由によります。あらためて説明しますが、前回の記事で理解されている方は読み飛ばしていただいて構いません。
本連載のターゲットボードは内部クロック24.576MHzで動作しています。そして、ServoInit()関数で、ITUのタイマは内部クロックの1/8でカウント動作を行うように設定しました。つまり3MHzでカウントを行うということです。これを別のいい方で表すと1秒間に3×10の6乗回(=3,000,000回)カウントするということです。1秒間に3,000,000回というと随分大きな数に見えるかもしれませんが、PWM波の周期やパルス幅はms(ミリセカンド)の単位で制御しますので、その世界で考えると実は大した数ではないことが分かります(1秒間に3,000,000回は1msでは3,000回、さらに1μsec(マイクロセカンド)では3回)。
少々脱線してしまいましたので話を戻します。ここで、このプログラムでの引数cycleは、単位をμsecとして考えています。つまり、H8マイコンが1μsec当たりにカウントする回数が3回なのだから、引数cycleの値を3倍してやればもともとのcycleの数字がそのまま現実時間になるじゃないかという発想です。例えば引数cycleに10進数で1000を与えるとして、それが1000μsec(=1ms)を表したいと思ったらH8マイコンのITUが3000回カウント動作を行うとちょうど1000μsecが経過したことになります。こういう細工を行うことで、プログラムが直感的に分かりやすくなります。直感的に分かりやすいということは、組み込みプログラミングに限らず大事なことといえるのではないでしょうか。
Copyright © ITmedia, Inc. All Rights Reserved.