RTAI入門(7)どう書くか?その1
この記事からの続きです。
今回から、RTAI用のアプリをどう書くか?編です。
とにかくテンプレートのように、コピペしてすぐ使える!という形式を目指しています。新たにRTAIアプリを組む時は、今回からの記事から必要な部分だけコピペして下さい。
なお、ここで使用するサンプルコードはすべてこちらの記事で構築した環境にて確かめています。
今回の記事で出来るようになる事は・・・
- Makefile
- ベースコード
- メッセージ送受信
- メッセージ送受信(長いメッセージ)
- 定期的な処理
Makefile
RTAI用のインクルードディレクトリを指定する必要があるため、Makefileを使用しましょう。
CC = g++ CFLAGS = -O0 -pthread -I/usr/src/rtai/base/include -I/usr/src/rtai all: rtai_howto rtai_howto: rtai_howto.cpp $(CC) $(CFLAGS) -o rtai_howto rtai_howto.cpp
ここで、アプリ名を rtai_howto としています。適宜修正してください。
ベースコード
最低限、1つはリアルタイムタスクを作る事でしょう。ですから、次のコードを出発点にすると良いと思います。
より詳細な事は、RTAI User Manualをご参照ください。
リアルタイムタスクについてざっくりお知りになりたい方は、こちらの記事を参照してください。
#include <pthread.h> #include <rtai.h> #include <rtai_sched.h> #include <rtai_msg.h> #include <rtai_mbx.h> #include <rtai_netrpc.h> #include <netinet/in.h> #include <arpa/inet.h> void *thread1( void* arg ) { void* result = NULL; RT_TASK *task1 = NULL; /* Create new realtime task */ task1 = rt_task_init_schmod( nam2num("MYTHD1"), /* ID */ 0, /* Priority */ 1024, /* Stack size */ 1024, /* Max message size */ SCHED_FIFO, /* Task scheduling policy */ 0x0f /* Processor affinity */ ); if( NULL == task1 ) { printf("Failed to create task1\n"); return( result ); } /* Make this task realtime */ rt_make_hard_real_time(); /* do realtime job */ ; ; /* Delete realtime task */ rt_task_delete( task1 ); /* Exit thread */ return( result ); } main() { pthread_t th1; RT_TASK *mainTask = NULL; /* Enable non-root hrt */ rt_allow_nonroot_hrt(); /* Create realtime task */ mainTask = rt_task_init_schmod( nam2num("MAINTH"),/* ID */ 10, /* Priority */ 1024, /* Stack size */ 1024, /* Max message size */ SCHED_FIFO, /* Task scheduling policy */ 0x0f /* Processor affinity */ ); if( NULL == mainTask ) { printf("Failed to create main task.\n"); return( 0 ); } /* Create user thread */ pthread_create( &th1, NULL, thread1, NULL ); pthread_join( th1, NULL ); /* Delete MAINTH */ rt_task_delete( mainTask ); }
タスクの生成と削除
タスクの生成と削除は、次のような流れで行います
- [main]スレッドを作成する( pthread_create )
- [スレッド]スレッドをリアルタイムタスク化する( rt_task_init_schmod )
- [スレッド]リアルタイムタスクをハードリアルタイム化する ( rt_make_hard_real_time )
- [スレッド]リアルタイム処理をする
- [スレッド]リアルタイムタスクを削除する (rt_task_delete)
- [main]スレッド終了を待ち合わせる ( pthread_join )
ここで特に重要なのは、タスク生成関数である rt_task_init_schmod と、タスク削除関数である rt_task_delete です。これらは必ず1セットで使用します。
また、rt_task_init_schmodの戻り値がNULLの時は、タスク生成に失敗しています。必ずタスク生成に成功するとは限らないので、ここのチェックは必須です。
(API)rt_task_init_schmod
- 概要
現在のスレッドをリアルタイム化します
- 形式
RT_TASK* rt_task_init_schmod( unsigned long name, int priority, int stack_size, int max_msg_size, int policy, int cpus_allowed )
- 引数
第一引数 name: 新たに作成するリアルタイムタスクのIDを指定します。 nam2num の中身を、重複しないように6文字で付けて下さい
第二引数 priority: プライオリティを指定します。0が最高、0x3fffffff が最低です。
第三引数 stack_size: リアルタイムタスクで使用できるスタックのサイズを指定します。
第四引数 max_msg_size: 受信する事ができるメッセージサイズの上限を指定します。
第五引数 policy: スケジューリングの方針を指定します。SCHED_FIFO か SCHED_RR で指定します。これは同一プライオリティのタスクがある場合に関係します。同一プライオリティのタスクが2つあったとして、SCHED_FIFOであれば片方のタスクが(ブロックされるまで)ずっと走り続けます。一方、SCHED_RRであれば、(ブロックしなくても)2つのタスクが交代に走ります。
第六引数 cpus_allowed : このタスクを実行するCPUを指定します。最下位ビットからそれぞれCPUその1、CPUその2・・・と対応します。ビットを1にすればそのCPUでの実行を許可します。
- 戻り値
タスク構造体へのポインタ(RT_TASK*)。タスクの生成に失敗した際にはNULLが返ります。
(API)rt_make_hard_real_time
- 概要
現在のリアルタイムタスクを、ハードリアルタイム化します。(ハードリアルタイムとソフトリアルタイムの違いは、こちらの記事をご参照ください)
関連するAPIとして、void rt_make_soft_real_time( ) があります。こちらは、タスクをソフトリアルタイム化します。
- 形式
void rt_make_hard_real_time( )
- 引数
なし
- 戻り値
なし
(API)rt_task_delete
- 概要
現在のリアルタイムタスクを削除し、単なるスレッドに戻します。rt_task_init系のAPIでリアルタイムタスクを作成した際には、必ずこのAPIを呼び出す必要があります。
- 形式
int rt_task_delete( RT_TASK *task)
- 引数
第一引数 task : 削除するリアルタイムタスクの、タスク構造体を指定します。これは、rt_task_init_schmodの戻り値を使用します
- 戻り値
0であれば成功、EINVALであれば task が不正であったために失敗。
(API) nam2num
- 概要
与えられた文字列の先頭6文字から、IDを生成します。詳しくはこちらの記事の下のほうをご覧下さい。
- 形式
unsigned long nam2num( const char* name)
- 引数
第一引数 name : 変換する文字列を6文字以下で指定します。1文字でもかまいません。7文字目以降は単純に無視されます。
- 戻り値
nameから変換されたID。タスクやメールボックスに名前をつけるのに使用します。
メッセージ送受信
ここでは、ごくごく基本的な使い方をご紹介します。
詳細は<RTAIのソース>/doc/generated/html/api/group__msg.html をご参照ください。
メッセージとは何かざっくり知りたい方は、こちらの記事を参照してください。
メッセージ送信
メッセージを送信するには、送信先のタスクアドレスを取得し、そのアドレスを元に rt_send 関数を使用します。
ここで、送り手も、受け手も、ともにリアルタイムタスクである必要がある事に注意してください。
{ /* get address of main task */ RT_TASK *mainTask = (RT_TASK*)rt_get_adr( nam2num("MAINTH") ); unsigned int msg = 0x1234; /* send message to main task */ if( NULL != mainTask ) { rt_send( mainTask, msg ); } }
上記の例では、MAINTHというタスクに対して、0x1234 というメッセージを送信しています。
次のような処理を行っています。
- メッセージ送信先のRT_TASK* を取得する (rt_get_adr)
- メッセージを送信する( rt_send )
メッセージ受信
メッセージを受信するには、次のようにします。
ここで、送り手も、受け手も、ともにリアルタイムタスクである必要がある事に注意してください。
{ RT_TASK *sender = NULL; unsigned int msg = 0; /* receive message */ rt_receive( sender, &msg ); }
上記の例では、どんなタスクからのメッセージでも受け取ります。
もしも、送信側のタスクを特定したければ、sender 変数に、rt_get_adr で取得したタスクを入れて下さい。そうすると、そのタスクからのメッセージのみを受け取ります。
(API)rt_get_adr
- 概要
RTAIのタスク(RT_TASK*)やメールボックス(RT_MBX*)の構造体を取得します。タスクにメッセージ送信したり、メールボックスとデータ送受信したりする場合、まずこのAPIでRT_TASK*やRT_MBX*を取得し、それを元にメッセージ送受信(データ送受信)を行います。
- 形式
void* rt_get_adr( unsigned long name )
- 引数
タスクやメールボックスのID。 nam2num( ) で変換したものを使うのが良いでしょう。例: rt_get_adr( nam2num(“TASK1”) )
- 戻り値
タスクやメールボックスの構造体へのポインタを返します。失敗するとNULLが返ります。
void* 型なので、(RT_TASK*) や、(RT_MBX*) などでキャストして使用します。
(API)rt_send
- 概要
よそのタスクにメッセージを送ります。
- 形式
RT_TASK* rt_send( RT_TASK* task, unsigned int msg )
- 引数
第一引数 task : 送信対象となるタスク。この値は、rt_get_adrで取得しておきましょう。
第二引数 msg : 送信するメッセージ。32ビット環境では32ビット幅になります。
- 戻り値
成功すれば、引数 task の値がそのまま返ります。失敗するとNULLや0xFFFFが返ります。
NULLは、送信先タスクがメッセージを送る前に削除されてしまった、というように相手の都合で送信失敗した場合に返ります。
0xFFFFは、そもそもtaskが正しいタスクをさしていない場合に返ります。
(API)rt_receive
- 概要
よそのタスクからメッセージを受け取ります。
- 形式
RT_TASK* rt_receive( RT_TASK* task, unsigned int& msg )
- 引数
第一引数 task : 送信元タスクを指定したい場合、ここで指定します。NULLを渡すと、すべてのタスクからメッセージ受信します。
第二引数 msg : 受け取ったメッセージの内容を保存するためのバッファ。 unsigned int 型の変数を宣言し、そのポインタを渡すと良いです。
- 戻り値
成功すれば、送信元タスクのRT_TASK*が返ります。失敗するとNULLや0xFFFFが返ります。
NULLは、送信元タスクがメッセージを送る前に削除されてしまった、というように相手の都合で受信失敗した場合に返ります。
0xFFFFは、そもそもtaskが正しいタスクをさしていない場合に返ります。
メッセージ送受信(長いメッセージ)
先ほどは、unsigned int を1つだけメッセージ送信しました。実は、RTAIではさらに長いデータを送信する事も可能です。
詳細は<RTAIのソース>/doc/generated/html/api/group__msg.html をご参照ください。
メッセージとは何かざっくり知りたい方は、こちらの記事を参照してください。
メッセージ送信
送り手も、受け手も、ともにリアルタイムタスクである必要がある事に注意してください。
{ /* get address of main task */ RT_TASK *mainTask = (RT_TASK*)rt_get_adr( nam2num("MAINTH") ); unsigned char msg[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; /* send message to main task */ if( NULL != mainTask ) { rt_sendx( mainTask, msg, sizeof(msg) ); } }
上記の例では、MAINTHというタスクに対して、8バイトのメッセージを送信しています。
メッセージ受信
長いメッセージを受信するには、次のようにします。
ここで、送り手も、受け手も、ともにリアルタイムタスクである必要がある事に注意してください。
{ RT_TASK *sender = NULL; unsigned char msg[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; long actualLength = 0; /* receive message */ rt_receivex( sender, msg, sizeof(msg), &actualLength ); }
上記の例では、どんなタスクからのメッセージでも受け取ります。
もしも、送信側のタスクを特定したければ、sender 変数に、rt_get_adr で取得したタスクを入れて下さい。そうすると、そのタスクからのメッセージのみを受け取ります。
(API) rt_sendx
- 概要
よそのタスクに長いメッセージを送ります。
- 形式
RT_TASK* rt_sendx( RT_TASK* task, void* msg, int size )
- 引数
第一引数 task : 送信対象となるタスク。この値は、rt_get_adrで取得しておきましょう。
第二引数 msg : 送信するメッセージ。メッセージを格納したバッファを指定します。バッファ長は引数 size で指定します。
第三引数 size : 送信するメッセージのサイズ(バイト)
- 戻り値
成功すれば、引数 task の値がそのまま返ります。失敗するとNULLや0xFFFFが返ります。
NULLは、送信先タスクがメッセージを送る前に削除されてしまった、というように相手の都合で送信失敗した場合に返ります。
0xFFFFは、そもそもtaskが正しいタスクをさしていない場合に返ります。
(API) rt_receivex
- 概要
よそのタスクから長いメッセージを受信します。
- 形式
RT_TASK* rt_receivex( RT_TASK* task, void* msg, int size, int* len )
- 引数
第一引数 task : 送信元タスクを指定したい場合、ここで指定します。NULLを渡すと、すべてのタスクからメッセージ受信します。
第二引数 msg : 受け取ったメッセージの内容を保存するためのバッファ。
第三引数 size : msgのバッファ長(バイト)
第四引数 len : 実際に受信したメッセージ長。指定した長さ(引数 size ) より短いこともありうるので、チェックが必要です。
- 戻り値
成功すれば、送信元タスクのRT_TASK*が返ります。失敗するとNULLや0xFFFFが返ります。
NULLは、送信元タスクがメッセージを送る前に削除されてしまった、というように相手の都合で受信失敗した場合に返ります。
0xFFFFは、そもそもtaskが正しいタスクをさしていない場合に返ります。
定期的な処理
1ms毎に何か処理をしたい、などというようにリアルタイムシステムには定期的な処理が付き物です。
RTAIでは、追加のタイマーボード等なしにこういった処理が実現できます。
詳細は、<RTAIのソース>/doc/generated/html/api/api_8c.html をご参照ください。
定期的な処理についてざっくりお知りになりたい方は、こちらの記事を参照してください。
タイマーを開始する
タイマーを開始したいリアルタイムタスク上で、以下のようにします。
start_rt_timer( 0 ); /* Make this task periodic */ rt_task_make_periodic_relative_ns( task1, /* This task */ 0, /* start delay in nanoseconds */ 1e+6 /* period in nanoseconds */ );
rt_task_make_periodic_relative_nsの第三引数が、周期をナノ秒単位で表したものです。
処理の流れは次のとおりです。
- タスクをリアルタイム化しておく(上記サンプルにはありませんが、rt_task_init_schmod を使います)
- とりあえずタイマーを開始する ( start_rt_time )
- タイマーの詳細を指定する ( rt_task_make_periodic_relative_ns )
定期的に処理を行う
リアルタイム化し、タイマーを開始したタスクにて、以下のようにループを回します。
while( 1 ) { rt_task_wait_period(); /* do realtime job here */ }
rt_task_wait_period() が無いと、タイマーを無視してガンガンループがまわってしまいます。
そこで、1回の処理を終える度に rt_task_wait_period関数を呼び出します。すると、次のタイマーイベントが発生するまでの間は、タスクがブロックします。その結果、rt_task_make_periodic_relative_ns で指定した時間ごとに1回処理を行う、という風にできます。
タイマーを停止する
タイマーを開始したタスクにて、以下のようにします。
stop_rt_timer();
(API)start_rt_timer
- 概要
タイマーを開始します。
- 形式
RTIME start_rt_timer( int period )
- 引数
第一引数 period : タイマー周期を指定します。単位はRTAIの内部カウンタの値です。rt_task_make_periodic_relative_nsと組み合わせるときは気にせずに0を指定しておくと良いです。
- 戻り値
タイマー周期が返ります。単位はRTAIの内部カウンタの値です。
(API)rt_task_make_periodic_relative_ns
- 概要
タスクを周期的に動作するようにします
- 形式
int rt_task_make_periodic_relative_ns( RT_TASK* task, RTIME start_delay, RTIME period )
- 引数
第一引数 task : 周期動作させるタスクを指定します
第二引数 start_delay : タイマー開始までのディレイを指定します。単位はナノ秒です。
第三引数 period : タイマー周期を指定します。単位はナノ秒です。
- 戻り値
成功すると0が返ります。task が不正な場合には EINVAL が返ります。
(API) rt_task_wait_period
- 概要
タイマー周期の頭が来るまで、待機します。rt_task_wait_periodで待機→何か処理をする→rt_task_wait_periodで待機・・・といった使い方をすることで、タイマー周期ごとに何か処理をさせる事が出来るようになります。
- 形式
void rt_task_wait_period( void )
- 引数
なし
- 戻り値
なし
(API)stop_rt_timer
- 概要
タイマーを停止します
- 形式
void stop_rt_timer( )
- 引数
なし
- 戻り値
なし
サンプルプログラム
・メッセージ送受信 スレッドからメイン関数に対してメッセージを送信します
・長いメッセージ送受信 スレッドからメイン関数に対して長いメッセージを送信します
・定期的な処理 1ms毎にカウンターをインクリメントします。
現在モーター制御装置のプラットフォームとしてLinux+RTAIの利用を検討している者です。
記事の内容は大変参考になり、感謝しています。
特にRTAIマニュアルのサンプルコードがそのままでは動かなくて困っていたところ、本記事のプログラム例を拝見し、修正したところ動くようになって本当に助かりました。
問題が一つありまして、定期的な処理起動で1msをタイマー間隔として設定するのですが、測定してみるとなぜか2.5ms間隔になっており、このとき画面処理の負荷を高くすると設定どおりの1ms周期になるという不思議な現象が発生しています。RTAI3.9.1+kernel2.6.32.20なのですが、RTAIはなかなか奥が深そうです。
カーネルモードにしてみることと、あとはRTAI4にしてみるとか試行錯誤が続きます…^^;
追加です。
その後、本連載最終回の記事を参考にさせていただき、外付けの1msタイマーからの割込みでトリガーをかけるようにしたところ、きれいな動作をするようになりました。
最初の構成でなぜタイマー周期が乱れるのかという謎は残りましたが、とりあえずこれで可能性が大きく開けました。
あらためてお礼申し上げます。
正確な記事の公開、本当にありがとうございました。
表示には出てきませんが、Emailに記入したアドレスを間違えていたことに気付いたので訂正しました。
コメントありがとうございます。また、貴重な事例をありがとうございます。
2.5ms周期の件、単なる思い付きですが、
・CPUクロックが負荷に応じて変動している
・RTAIがCPUクロックを元に周期を測っている
のかも分かりません。
外付けタイマーの利用は良さそうですね。私もいつかやってみます。