基礎実験 4.7 ではtimer0インターバルタイマとして使用し、割り込み処理を用いてある程度正確な周期で動作するラジコンサーボ制御プログラムを作ってみました。 今回は、ラジコンサーボの制御から少しはなれて、前回作成したプログラムをもう少し一般化して簡易スケジューラを作ります。
普段私たちがPCを使用するときは、意識しなくてもOSのお世話になっています。OSはシステムプログラムの集合体なので役割は多岐にわたりますが、OSのもっとも根底にあるカーネル(kernel)と呼ばれる部分では、主にタスクのスケジューリング、メモリ管理、デバイス管理、ファイルシステム管理などが行われています。特に、組み込みシステムで使用されるリアルタイムOSでは、タスクのスケジューリングのみを提供するOSも存在します。そもそもOSはコンピュータのリソースを有効に利用するために導入されたもので、タスクのスケジューリングを行うスケジューラはこのOSの根幹を成しているものといえます。
スケジューラはタスクを並行処理するためのスケジューリング機能を提供します。いわゆる、マルチタスク処理が可能になるわけです。マルチタスクのプログラムが書けるようになると、これまで処理の流れが一つしかない中に埋め込んできた複数の異なる種類の処理を分離できるため、プログラムの構造をすっきりさせることができるようになります。また、あるタスクの待ち時間の間に別のタスクの処理が行えるため、シングルタスクより処理効率を向上させることが可能になるなど、マルチタスクスケジューラを導入する利点は沢山あります。
PICがOSを積むほど高性能かどうかという議論はありますが、PIC用のOS(PICROS)は存在します。しかし、ここではあえてそれには手を出さず、自分で簡易スケジューラを作ってみました。基本的なつくりは基礎実験4.7の interrupt_1a.c と同じですが、余計なものを取り払って一般化してあります。ちょちょいとつくってみただけなので、バグがないかといわれると自信がありませんが、とりあえずそれなりには動いています。
PICの処理結果を確認するには、ハイパーターミナルが便利です。プログラムを実行すると、1秒毎に
| ABAAAABAAAABAAAABAAAABAAAC 1sec |
と表示されます。Task Aの実行周期は50msなので、1secの間に A が20回表示されます。同様に、Task Bの実行周期は200msなのでBは5回、Task Cの実行周期は1000msなので1回表示されます。ただし、実行周期はそれほど厳密ではなく、1〜3%程度の誤差があります。この誤差は、timer0のオーバーフロー割り込みを利用していることに起因するものだと思います。
一般的なOSは、task control block (TCB) というものを使ってタスクの管理をしているそうなので、真似して作ってみました。
// task control block
typedef struct{
unsigned long wupPeriod; // wake up period (16bit)
unsigned long wupCnt; // wake up count
// Because CSS-PCM compiler does not permit pointer to function,
// TBC(task control block) and tasks are connected with task ID.
}t_tcb;
|
// task control block
typedef struct{
unsigned long wupperiod;
unsigned long wupcnt; // 次にタスクが起動されるまでの残り時間
void (*entry)(void); // タスクのエントリポイント
}t_tcb;
|
表の上段が今回作成したTCBで、下段が本当はこうしたかったというTCBの構造です。TCBはタスクの制御情報を格納しておくための構造体です。TCBに関数へのポインタを入れておけば、
// body of task scheduler -----------------------------
while(1){
for(i=0; i<TASK_NUM; i++){
if( 0 == tcb[i].wupcnt ){
tcb[i].wupcnt = tcb[i].wupperiod;
tcb[i].entry();
}
}
}
// ----------------------------------------------------
|
といったように main 関数の無限ループの部分を簡単に書けるようになります。しかし、CSS-PCMは関数へのポインタをサポートしていません。しかたがないので、task ID を使ってタスクの実行管理を行うようにしました。
タスクIDを管理するための小技です。
// task ID
enum {
TASK_A,
TASK_B,
TASK_C,
TASK_NUM
}taskID;
|
このように列挙子enumを使うと、コンパイラによって先頭から 0、1、2、3と数字が振られるため、タスクIDを自動的に生成することができ、IDの競合を防ぐことができます。また、最後にあるTASK_NUM にはタスクがいくつあるかが自動的に入るため、タスクを追加したり削除したりする際に楽になります。
各タスクには実行までの待ち時間( wupCnt )をあたえます。1ms毎にこの待ち時間が減り、TCBのwupCnt が 0 になったタスクが実行されます。 まったく同一のタイミングで複数のタスクが実行タイミングになった場合、タスクIDの小さいタスクから優先して処理を実行します。
それでは、簡易スケジューラを使って基礎実験4.7 のラジコンサーボ制御プログラムを改良してみましょう。マルチタスク化するついでに、サーボ信号のON時間を delay で作っていたところも、timer1 をつかってON時間を管理するように改良します。基本的なつくりはほとんど基礎実験4.7のサンプルプログラムと変わりません。(なぜなら、基礎実験4.7のサンプルは、最初からスケジューラの仕組みを盛り込んであったから・・・)
サンプルプログラム全体の説明は、これまでの説明してきたことを繰り返すことになるだけなので止めておきます。ここでは、基礎実験4.7で宿題になっていた delay を使わないON時間の管理についてだけ説明します。
サーボの出力を行っている関数は、以下のようになっています。
void Servo_Output(void)
{
unsigned long onWidth, t1_cnt;
onWidth = SERVO_ON_WIDTH_US_PER_DEG * (signed long)servoAngle
+ SERVO_NEUTRAL_ON_WIDTH_US;
t1_cnt = 0xffff - 5 * onWidth;
// here, 5 = 1us / (50ns * 4)
output_high( SERVO_0 );
setup_timer_1( T1_INTERNAL | T1_DIV_BY_1 );
set_timer1( t1_cnt );
enable_interrupts( INT_TIMER1 );
}
|
この関数では、
という処理を行っています。timer1 のカウント値設定方法はtimer0 の場合とほぼ同じですが、カウンタが8bitから16bitに増えています。timer1のカウンタがオーバーフローすると #int_timer1 で定義した割り込みハンドラが実行されます。割り込みハンドラは以下のようになっています。
// ============================================================================
// servo low output timer (timer1 interrupt handler)
// ============================================================================
#int_timer1
void ServoLowTimer(void)
{
output_low( SERVO_0 );
setup_timer_1( T1_DISABLED );
disable_interrupts( INT_TIMER1 );
}
|
関数 Servo_Output ではサーボ制御信号を H にしたまま終わってしまいました。 timer1 の割り込みハンドラでは、この H になったままの信号を L に戻し、timer1 が勝手に動き続けないように止めて、timer1 の割り込みも禁止して割り込み処理を終了します。
このように、 timer1 のオーバーフロー割り込みを使えば delay 関数を使わずにサーボ制御信号のON時間を管理することが可能になり、無駄な待ち時間が減るため、他の処理にもっと沢山のCPU時間を使うことが可能になります。