以下の記事を和訳したものをメモ.
mbedOSのEventQueueがよく分からなかったので…
翻訳にはDeepLを使用.
https://os.mbed.com/docs/mbed-os/v6.9/apis/scheduling-tutorials.html
The EventQueue API
Arm Mbed OSのオプション機能の一つに、コードの実行を別のコンテキストに延期するために使用できるイベントループ機構があります。特に、イベントループの一般的な使用法は、割り込みハンドラからのコードシーケンスの実行をユーザコンテキストに延期することです。これは、割り込みハンドラ内で実行されるコードに特有の制約があるために有効です。
・特定の関数(特にCライブラリの一部の関数)の実行は安全ではありません。
・割り込みコンテキストから、さまざまなRTOSオブジェクトや関数を使用することはできません。
・原則として、コードはできるだけ早く終了し、他の割り込みを処理できるようにする必要があります。
イベントループは、これらの問題を解決するために、コードの実行を割込みコンテキストからユーザコンテキストに延期することができるAPIを提供します。もっと一般的に言えば、イベントループはプログラムのどこでも(必ずしも割り込みハンドラの中でなくても)、コードの実行を別のコンテキストに延期するために使用することができます。
Mbed OSでは、イベントは関数へのポインタ(オプションで関数の引数も可)です。イベントループは、キューからイベントを抽出して実行します。
Creating an event loop
イベントループを手動で作成し、開始する必要があります。これを実現する最も簡単な方法は、スレッドを作成し、イベントキューのディスパッチメソッドをスレッド内で実行することです。
# include "mbed.h"
// Create a queue that can hold a maximum of 32 events
EventQueue queue(32 * EVENTS_EVENT_SIZE);
// Create a thread that'll run the event queue's dispatch function
Thread t;
int main () {
// Start the event queue's dispatch thread
t.start(callback(&queue, &EventQueue::dispatch_forever));
}
なお、このドキュメントでは、システムにイベントループが1つ存在することを前提としていますが、プログラマが複数のイベントループを実行することを妨げるものではなく、それぞれのイベントループに対して上記のcreate/startパターンを実行すればよいのです。
Using the event loop
イベントループを開始すると、イベントをポストできるようになります。InterruptInのrise関数とfall関数を使って、InterruptInオブジェクトに2つの割り込みハンドラをアタッチするプログラムの例を考えてみましょう。riseハンドラは割り込みコンテキストで実行され、fallハンドラはユーザコンテキスト(より具体的には、イベントループのスレッドのコンテキスト)で実行されます。この例の完全なコードを以下に示します。
# include "mbed.h"
DigitalOut led1(LED1);
InterruptIn sw(SW2);
EventQueue queue(32 * EVENTS_EVENT_SIZE);
Thread t;
void rise_handler(void)
{
queue.call(printf, "rise_handler in context %p\n", ThisThread::get_id());
// Toggle LED
led1 = !led1;
}
void fall_handler(void)
{
printf("rise_handler in context %p\n", ThisThread::get_id());
// Toggle LED
led1 = !led1;
}
int main()
{
// Start the event queue
t.start(callback(&queue, &EventQueue::dispatch_forever));
printf("Starting in context %p\r\n", ThisThread::get_id());
// The 'rise' handler will execute in IRQ context
sw.rise(rise_handler);
// The 'fall' handler will execute in the context of thread 't'
sw.fall(queue.event(fall_handler));
}
上記のコードでは、2つのハンドラ関数(rise_handlerとfall_handler)を2つの異なるコンテキストで実行しています。
- SW2 (rise_handler)で立ち上がりエッジが検出されると、割り込みコンテキストになります。
- イベントループのスレッド関数のコンテキストで、SW2に立下りエッジが検出されたとき(fall_handler)、queue.event()がfall_handlerを引数にして呼び出され、fall_handlerが割り込みコンテキストではなくユーザーコンテキストで実行されることが指定されます。
これは、FRDM-K64Fボード上の上記プログラムの出力です。ボードをリセットして、SW2ボタンを2回押しました。
Starting in context 20001fe0
fall_handler in context 20000b1c
rise_handler in context 00000000
fall_handler in context 20000b1c
rise_handler in context 00000000
プログラムはmain関数を実行するスレッドのコンテキストで開始されます(20001fe0)。ユーザーがSW2を押すと,fall_handlerが自動的にイベントキューにキューイングされ,後にスレッドt(20000b1c)のコンテキストで実行されます.ユーザーがボタンを離すと,rise_handler が直ちに実行され,コードが割り込みコンテキストで実行されたことを示す 00000000 が表示されます.
rise_handlerのコードは、printfを割り込みコンテキストで呼び出しているため、安全でない可能性があるという問題があります。幸いなことに、これはまさにイベントキューが解決できる種類の問題です。この行を置き換えることで、(fall_handlerですでに行っているように) rise_handlerをユーザーコンテキストで実行することで、コードを安全にすることができます。
sw.rise(rise_handler);
を
sw.rise(queue.event(rise_handler));
に変更すれば.
これでコードは安全になりましたが、レイテンシーという別の問題が発生してしまったかもしれません。上記の変更後、rise_handlerの呼び出しはキューイングされることになり、割り込みが発生した直後に実行されなくなります。今回のサンプルコードでは問題ありませんが、アプリケーションによっては、割り込みに対してできるだけ早く応答する必要があるかもしれません。
rise_handlerは、ユーザーがSW2を操作したことに応じて、できるだけ早くLEDを切り替えなければならないと仮定しましょう。そのためには、割り込みのコンテキストで実行しなければなりません。しかし、 rise_handler はハンドラが呼ばれたことを示すメッセージをプリントする必要があります。これは問題です。
解決策は、rise_handlerを2つの部分に分割することです。時間的に重要な部分は割り込みコンテキストで実行し、重要でない部分(メッセージの表示)はユーザーコンテキストで実行します。これは、queue.callを使えば簡単にできます。
void rise_handler_user_context(void) {
printf("rise_handler_user_context in context %p\r\n", Thread::gettid());
}
void rise_handler(void) {
// Execute the time critical part first
led1 = !led1;
// The rest can execute later in user context (and can contain code that's not interrupt safe).
// We use the 'queue.call' function to add an event (the call to 'rise_handler_user_context') to the queue.
queue.call(rise_handler_user_context);
}
rise_handlerのコードを上記のように置き換えると、この例の出力は次のようになります。
Starting in context 0x20002c50
fall_handler in context 0x20002c90
rise_handler_user_context in context 0x20002c90
fall_handler in context 0x20002c90
rise_handler_user_context in context 0x20002c90
上記のシナリオ(割込みハンドラのコードをタイムクリティカルなコードとそうでないコードに分割する)は、イベントキューを使って簡単に実装できるもう一つの一般的なパターンです。割り込みセーフではないコードをキューに入れることは、イベントキューを使える唯一のことではありません。
上記の例ではInterruptInを使用していますが、SDKのattach()に似た関数であれば、同じようなコードを使用することができます。例えば、Serial::attach()、Ticker::attach()、Ticker::attach_us()、Timeout::attach()などです。
Prioritization
EventQueueには、イベントの優先順位の概念がありません。イベントを同時に実行するようにスケジュールした場合、イベントの相対的な実行順序は定義されません。EventQueueは、時間に基づいてイベントをスケジュールするだけです。イベントを異なる優先度に分けたい場合は、優先度ごとにEventQueueをインスタンス化する必要があります。また、各EventQueueインスタンスをディスパッチするスレッドの優先度を適切に設定する必要があります。
EventQueue memory pool
EventQueueのインスタンスを作成する際には、そのメモリの固定サイズを指定します。汎用ヒープからの割り当てはIRQセーフではないため、EventQueueはその作成時にこの固定サイズのメモリブロックを割り当てます。イベントキューのメモリサイズは固定されていますが、イベントキューは様々なサイズのイベントをサポートしています。
さまざまなサイズのイベントは、メモリ領域にフラグメント(断片化)をもたらします。このフラグメンテーションにより、EventQueueがディスパッチできるイベントの数を決定することが困難になります。EventQueueは多くの小さなイベントをディスパッチすることができるかもしれませんが、フラグメンテーションによって1つの大きなイベントを割り当てることができない場合があります。
Calculating the number of events
プロジェクトで固定サイズのイベントしか使用しない場合は、EventQueueがディスパッチしたイベントの数を追跡するカウンターを使用することができます。
プロジェクトで可変サイズのイベントを使用している場合、正常に割り当てられたメモリはそれ以上断片化されないため、特定のサイズの利用可能なイベントの数を計算することができます。しかし、手つかずのスペースでは、サイズに合ったイベントを処理することができるため、計算が複雑になります。
// event size in words: 9 + callback_size (4) + arguments_size (where 9 is internal space for event data)
void func1(int);
void func3(int, int, int);
EventQueue queue(2*(9+4+3)*sizeof(int)); // 32 words of storage (store two callbacks with three arguments at max)
queue.call(func1, 1); // requires 14 words of storage (9+4+1)
queue.call(func3, 1, 2, 3); // requires 16 words of storage (9+4+3)
// after this we have 2 words of storage left
queue.dispatch(); // free all pending events
queue.call(func, 1, 2, 3); // requires 16 words of storage (9+4+3)
queue.call(func, 1, 2, 3); // fails
// storage has been fragmented into two events with 14 and 16 words
// of storage, no space is left for an another 16 word event even though two words
// exist in the memory region
Failure due to fragmentation
次の例では、フラグメント(断片化)のために失敗します。
// event size in words: 9 + callback_size (4) + arguments_size (where 9 is internal space for event data)
void func0();
void func3(int, int, int);
EventQueue queue(4*(9+4)*sizeof(int)); // 52 words of storage
queue.call(func0); // requires 13 words of storage (9+4)
queue.call(func0); // requires 13 words of storage (9+4)
queue.call(func0); // requires 13 words of storage (9+4)
queue.call(func0); // requires 13 words of storage (9+4)
// 0 words of storage remain
queue.dispatch(); // free all pending events
// all memory is free again (52 words) and in 13-word chunks
queue.call(func3, 1, 2, 3); // requires 16 words of storage (9+4+3), so allocation fails
52ワードのストレージが空きますが、13ワード以下の割り当てに限られます。この障害を解決するには、EventQueueのサイズを大きくする必要があります。適切なサイズのEventQueueを持つことで、将来的にイベント用のスペースが足りなくなるのを防ぐことができます。
More about events
これは、Mbed OSでのイベントキューの仕組みのほんの一部に過ぎません。mbed-eventsライブラリのEventQueue、Event、UserAllocatedEventクラスは、引数付きの関数の呼び出し、遅延後に呼び出される関数のキューイング、定期的に呼び出される関数のキューイングなど、このドキュメントではカバーしていない多くの機能を提供しています。mbed-eventsライブラリのREADMEには、イベントやイベントキューのより詳しい使い方が書かれています。イベントライブラリの実装については、equeueライブラリをご覧ください。
Static EventQueue
EventQueue APIは、スタティック・キューを作成するためのメカニズムを提供しています。スタティック・キューとは、動的なメモリ割り当てを使用せず、ユーザが割り当てたイベントのみを受け付けるキューです。静的キューを作成した後(コンストラクタにサイズとしてゼロを渡す)、UserAllocatedEventをそれにポストすることができます。静的なEventQueueとUserAllocatedEventを併用することで、キューの作成、イベントのポストやディスパッチの際に、動的なメモリの割り当てが行われないことが保証されます。また、キューやイベントをスタティック・オブジェクト(C++の意味でのスタティック)として宣言すれば、コンパイル時にそれらのためのメモリが確保されることになります。
# include "mbed.h"
// Creates static event queue
static EventQueue queue(0);
void handler(int count);
// Creates events for later bound
auto event1 = make_user_allocated_event(handler, 1);
auto event2 = make_user_allocated_event(handler, 2);
// Creates event bound to the specified event queue
auto event3 = queue.make_user_allocated_event(handler, 3);
auto event4 = queue.make_user_allocated_event(handler, 4);
void handler(int count)
{
printf("UserAllocatedEvent = %d \n", count);
return;
}
void post_events(void)
{
// Single instance of user allocated event can be posted only once.
// Event can be posted again if the previous dispatch has finished or event has been canceled.
// bind & post
event1.call_on(&queue);
// event cannot be posted again until dispatched or canceled
bool post_succeed = event1.try_call();
assert(!post_succeed);
queue.cancel(&event1);
// try to post
post_succeed = event1.try_call();
assert(post_succeed);
// bind & post
post_succeed = event2.try_call_on(&queue);
assert(post_succeed);
// post
event3.call();
// post
event4();
}
int main()
{
printf("*** start ***\n");
Thread event_thread;
// The event can be manually configured for special timing requirements.
// Timings are specified in milliseconds.
// Starting delay - 100 msec
// Delay between each event - 200msec
event1.delay(100);
event1.period(200);
event2.delay(100);
event2.period(200);
event3.delay(100);
event3.period(200);
event4.delay(100);
event4.period(200);
event_thread.start(callback(post_events));
// Posted events are dispatched in the context of the queue's dispatch function
queue.dispatch_for(400ms); // Dispatch time - 400msec
// 400 msec - Only 2 set of events will be dispatched as period is 200 msec
event_thread.join();
}