2025年5月26日月曜日

TOPPERS/ASP - Arduino UNO R4版 その11

前回からの続きです。

このテーマを最初からご覧になる場合はこちらからどうぞ。


RTOSを使用した利点とは?

前回までに「Arduino UNO R4」で、ターミナル・ソフトウェアで入力した文字をシリアル通信でそのまま返すという簡単なプログラムを作成してきました。

そして、同じようなプログラムをTOPPERS/ASPを使用しない場合(「ベアメタル版」と呼んでいます)と使用した場合(「RTOS版」とでもしましょう)の2種類を紹介させていただきました。

これらは何が違っていて、TOPPERS/ASPなどのRTOSを使った場合はどのような点で優れているのか?

苦労してRTOSを載っけても、メリットがなければ意味がありませんからね…。

早速比較してみましょう。

Renesas RA4M1


メインプログラムを比較します。

まずは、TOPPERS/ASPを使用しない場合…ベアメタル版のメインプログラムです。

この記事で作成した「Hinagata」プロジェクトの「hal_entry.c」ファイルの「hal_entry()」関数です。

以下のような内容でした。

  1. ...
  2. void hal_entry(void)
  3. {
  4.     uint8_t c;
  5.     // シリアル通信を開く
  6.     R_SCI_UART_Open(&g_uart2_ctrl, &g_uart2_cfg);
  7.     while (1) {
  8.         // 受信する
  9.         R_SCI_UART_Read(&g_uart2_ctrl, &c, 1);
  10.         // 受信するまで待つ
  11.         while (!recieved);
  12.         // 受信完了フラグをリセットする
  13.         recieved = false;
  14.         // 受信した一文字を送信する
  15.         R_SCI_UART_Write(&g_uart2_ctrl, &c, 1);
  16.     }
  17. }
  18. ...


ここで上のリストで言うところの11から14行目に注目しておいてください。


次は、TOPPERS/ASPを使用した場合…つまりRTOS版のメインプログラムです。

この記事前回の記事で作った「asp_arduino_uno_r4_gcc-template」プロジェクトの「OBJ_template」直下「main.c」ファイルの「main_task()」関数です。

  1. ...
  2. void main_task(intptr_t exinf)
  3. {
  4.     uint8_t c;
  5.     // 割り込み番号とイベント番号の紐付け
  6.     bsp_irq_cfg();
  7.     // 各割り込みを有効化
  8.     ena_int(INTNO_UART2_RXI);
  9.     ena_int(INTNO_UART2_TXI);
  10.     ena_int(INTNO_UART2_TEI);
  11.     ena_int(INTNO_UART2_ERI);
  12.     // シリアル通信を開く
  13.     R_SCI_UART_Open(&g_uart2_ctrl, &g_uart2_cfg);
  14.     while (1) {
  15.         // 受信する
  16.         R_SCI_UART_Read(&g_uart2_ctrl, &c, 1);
  17.         // タスクを待ち状態にする
  18.         slp_tsk();
  19.         // 受信した一文字を送信する
  20.         R_SCI_UART_Write(&g_uart2_ctrl, &c, 1);
  21.     }
  22. }
  23. ...


「bsp_irq_cfg()」とか「ena_int()」などの関数コール以外は、一見ベアメタル版と変わらないように見えます。

しかし、ここで上のリストで言うところの20から21行目に注目してください。

ベアメタル版では「received」グローバル変数がtrueになるまで空ループしていて、それがtrueになったら次の受信に備えて「received」グローバル変数を自らfalseにリセットしています。

一方、RTOS版では、これら一連の処理が「slp_tsk()」というシステムコールに置き換えられていますね?


…一体、これらに何の違いがあるのか?


ベアメタル版の場合「received」グローバル変数をtrueにする…つまり「received = true;」を実行しているのは「Hinagata」プロジェクトの同じ「hal_entry.c」に記述されている「uart2_callback()」コールバック関数内…以下のリストでは7行目です。

  1. ...
  2. void uart2_callback(uart_callback_args_t *p_args)
  3. {
  4.     if (p_args->event == UART_EVENT_RX_COMPLETE) {
  5.         // 受信が完了したら「r_sci_usrt.c」ファイルの
  6.         // 「sci_uart_rxi_isr()」割り込みハンドラからここに来る
  7.         recieved = true;
  8.     }
  9.     if (p_args->event == UART_EVENT_TX_COMPLETE) {
  10.         // 送信が完了したら「r_sci_usrt.c」ファイルの
  11.         // 「sci_uart_tei_isr()」割り込みハンドラからここに来る
  12.     }
  13.     if (p_args->event == UART_EVENT_RX_CHAR) {
  14.         // 一文字受信したら「r_sci_usrt.c」ファイルの
  15.         // 「sci_uart_rxi_isr()」割り込みハンドラからここに来る
  16.     }
  17.     if (p_args->event == UART_EVENT_ERR_FRAMING) {
  18.         // フレーミングエラーを検出した時に「r_sci_usrt.c」ファイルの
  19.         // 「sci_uart_eri_isr()」割り込みハンドラからここに来る
  20.     }
  21.     if (p_args->event == UART_EVENT_BREAK_DETECT) {
  22.         // ブレークを検出した時に「r_sci_usrt.c」ファイルの
  23.         // 「sci_uart_eri_isr()」割り込みハンドラからここに来る
  24.     }
  25.     if (p_args->event == UART_EVENT_TX_DATA_EMPTY) {
  26.         // 送信バッファが空になった時に「r_sci_usrt.c」ファイルの
  27.         // 「sci_uart_txi_isr()」割り込みハンドラからここに来る
  28.     }
  29. }
  30. ...


つまり、ベアメタル版の場合ではシリアル通信の受信割り込みが発生して「uart2_callback()」コールバック関数にて「received」変数がtrueにセットされるまでは、空ループをグルグル回り続けます。

この状態では、CPUは割り込みハンドラ内の処理以外は他に何もできません。

空ループとは言っても処理は処理です。

この間CPUは空ループを回すという非生産的なことだけに全力を傾けている状態です。


一方、RTOS版は…?

「received」変数の操作の代わりに「slp_tsk()」というシステムコールを置きました。

「slp_tsk()」は、これを呼び出したタスクを「起床待ち(スリープ)」にするシステムコールです。

「main_task()」関数は「MAIN_TASK」というIDのタスクであることは同じディレクトリにある「main.cfg」で設定されています。

ですので「slp_tsk()」を呼び出した時点で「MAIN_TASK」はスリープ状態となり、割り込みハンドラや他のタスクが「iwup_tsk(MAIN_TASK);」や「wup_tsk(MAIN_TASK);」してあげない限り、ずっと動作を停止した状態が続きます。

RTOS版で具体的にこれを行っているのは、ベアメタル版と同じく「hal_entry.c」に記述されている「uart2_callback()」コールバック関数内…以下のリストでは6行目です。

  1. ...
  2. void uart2_callback(uart_callback_args_t *p_args)
  3. {
  4.     if (p_args->event == UART_EVENT_RX_COMPLETE) {
  5.         // 待ち状態のタスクを起床させる
  6.         iwup_tsk(MAIN_TASK); // 追記!
  7.     }
  8.     if (p_args->event == UART_EVENT_TX_COMPLETE) {
  9.         // 送信が完了したら「r_sci_usrt.c」ファイルの
  10.         // 「sci_uart_tei_isr()」割り込みハンドラからここに来る
  11.     }
  12.     if (p_args->event == UART_EVENT_RX_CHAR) {
  13.         // 一文字受信したら「r_sci_usrt.c」ファイルの
  14.         // 「sci_uart_rxi_isr()」割り込みハンドラからここに来る
  15.     }
  16.     if (p_args->event == UART_EVENT_ERR_FRAMING) {
  17.         // フレーミングエラーを検出した時に「r_sci_usrt.c」ファイルの
  18.         // 「sci_uart_eri_isr()」割り込みハンドラからここに来る
  19.     }
  20.     if (p_args->event == UART_EVENT_BREAK_DETECT) {
  21.         // ブレークを検出した時に「r_sci_usrt.c」ファイルの
  22.         // 「sci_uart_eri_isr()」割り込みハンドラからここに来る
  23.     }
  24.     if (p_args->event == UART_EVENT_TX_DATA_EMPTY) {
  25.         // 送信バッファが空になった時に「r_sci_usrt.c」ファイルの
  26.         // 「sci_uart_txi_isr()」割り込みハンドラからここに来る
  27.     }
  28. }
  29. ...


割り込みが発生して「iwup_tsk(MAIN_TASK);」を行わない限り、メインプログラムが再開しないのはベアメタル版と同じです。

しかし、ベアメタル版の空ループではなく、RTOS版の場合はタスクをスリープ状態に移管しただけ…という点に違いがあります。

ベアメタル版の空ループの場合、この時実行できる他の処理は、何らかの割り込みが発生した時に呼び出される割り込みハンドラの処理だけです。

一方、RTOS版のスリープ状態の場合は、割り込みハンドラの処理に加えて、他のタスクに処理が移り、文字通り他の処理を行うことができます。

RTOS版で空ループを使った場合も、そこでCPU全体が固まってしまうということはなく、他に優先度の高いタスクが動いていれば、そちらに処理が移ります。

これがマルチタスクと呼ばれているもので、RTOSを使用する最大のメリットの一つです。

この辺りの動きの詳細は、μITRON4.0の仕様書か、TOPPERS/ASPのサンプルプログラムの動きを解説したこちらの記事を参照してください。

今回のRTOS版では、同時に動いているタスクは、メインプログラムである「MAIN_TASK」の他にログを管理する「LOGTASK」があります。

マルチタスクで動作している証拠として、RTOS版を実行した後、ターミナル・ソフトウェアによる操作を何も行わない状態で「LOGTASK」のプログラムにデバッガでブレークを仕掛けてみましょう。

具体的には「syssvc」ディレクトリ直下の「logtask.c」ファイルの140行目、ループの先頭「while ((rercd = syslog_rea_log(&logbuf)) >= 0) {」というコードの辺りが良いでしょうか?

e2 studio


引っかかりましたね?

この時「MAIN_TASK」タスクは「slp_tsk()」により止まっていますが、空ループに入ったベアメタル版とは違い「LOGTASK」という他のタスク、すなわち他の処理が実行できていることが分かります。


今回はRTOS版でも、システム全体で2つのタスクしかありません。

しかし、もっと複雑なアプリケーションの開発において、同時に処理しなければならないことが増えた時にこそ、RTOSを使用するメリットが増大します。

CPUに無駄な処理をさせることなく、待ち時間や休む暇なく、同時に多くの仕事を効率的に動いてもらうことがRTOSの役割です。

こういう言い方だと、ブラック企業のマネージャーみたいでCPUには気の毒ですがね…。


まとめ

組み込みソフトウェアにおいてTOPPERS/ASPなどのRTOSを使用するメリットは、以下の通りです(…だと個人的には思っています!)。


1.マルチタスクが使えること

今まで説明させていただいた通り、これが最大のメリットだと思います。

RTOSを使用しない場合、メインプログラムの処理の他に何かをやらせようとすると、何らかの割り込みハンドラに全ての処理を記述しなければなりません。

そうすると割り込みハンドラの処理が重くなり、他の割り込み処理など、システム全体のパフォーマンスを落としかねません。

ソフトウェア全体の設計も窮屈で難解なものになるでしょう。


2.ソースコードの移植性が高くなること

今回のターゲット「Arduino UNO R4」はARMアーキテクチャのマイコンを搭載しています。

例えば、あるソフトウェアをRTOSを使って開発していて、それをARMではなくRISC-VやRXなどの他のアーキテクチャのハードウェアに移植することを考えてみてください。

あるアーキテクチャでは簡単に実現できることが、別のアーキテクチャでは困難であるケースや、やり方が大きく違うことが多々あります。

本来、OSとはハードウェアとアプリケーションの間に入って、アプリケーションから見たハードウェアの差異を吸収する役割を持つものです。

したがって、もしRTOSを使っていなければ、ソフトウェアのかなりの部分の書き直しが必要となる可能性もあります。

当然、ハードウェア依存部分は使用するハードウェアやマイコンによってその都度書き直す必要のある部分はあるにしても、少なくともアプリケーション部分に関しては、そのまま流用できるはずです。

移植元と移植先のRTOSの仕様が同一であれば、アプリケーションは同じ動作のシステムコールを使用しますので、動作検証の工数も大幅に削減できます。

むしろ、この工数削減こそ貴重です!

が、このメリットは昔ほど重視されません。

こう…、なんでもかんでもARMの時代じゃあ…。


3.ソースコードの可読性が高くなること

RTOSを使用しないベアメタルなソフトウエアにおいては、その作者によってルールが異なります。

プログラマーの中には変わり者も多く(そういう人ほど天才が多いですが…)、俺俺ルール満載、カオスで難読のソースコードも数多くあります。

その点、RTOSを使用した場合はシステムコールの仕様やタスクの構成など、一定のルールが保たれる傾向があります。

(そうじゃなきゃ動かないしねぇ。)

見慣れたRTOSのシステムコールを起点にソースコードを読んでいけば、それが赤の他人が書いたものでも理解は格段に容易になります。

自分の書いたものであっても、しばらくすれば忘れてしまうもの。

その際にもメンテナンス性が向上しますよ。


RTOSの導入は多少手間がかかりますが、開発しているソフトウエアが複雑になればなるほど、手間を上回る十分なメリットが得られると思います。

とはいえRTOSも万能ではありませんので、特にROMやRAMなどのリソースが少ない状況でのご使用は、是非とも慎重なご判断を!


ライセンスについて

このカーネルは「TOPPERSライセンス」で配布しております。

無償ですが、使用に関しては自己責任です。

このカーネルを商用利用する方は、このリンク先の条項に従ってください。


さて、これで「TOPPERS/ASP Arduino UNO R4版」に関するテーマは終了です。

お付き合いいただき、ありがとうございました。

これでようやく次のアーキテクチャの説明に入れます!


<終わり>

0 件のコメント:

コメントを投稿

TOPPERS/ASP - Arduino UNO R4版 その11

前回からの続き です。 このテーマを最初からご覧になる場合は こちら からどうぞ。 RTOSを使用した利点とは? 前回 までに「Arduino UNO R4」で、ターミナル・ソフトウェアで入力した文字をシリアル通信でそのまま返すという簡単なプログラムを作成してきました。 そして、...