前回からの続きです。
このテーマを最初からご覧になる場合はこちらからどうぞ。
TOPPERS/ASPとFSPを組み合わせて使う~その2
前回ビルドまで通した「OBJ_template」に対し、実際にシリアル通信プログラムを肉付けしていきましょう。
「TOPPERS/ASP - Arduino UNO R4版 その8」で作ったベアメタル版と同じ動作のプログラムをTOPPERS/ASP上に実装します。
結果「OBJ_template」の「main.c」は、以下のようなものとなります。
- ...
- #include <kernel.h>
- #include <t_syslog.h>
- #include <t_stdlib.h>
- #include "kernel_cfg.h"
- #include "hal_data.h" // 追記!
- #include "main.h"
- /*
- * 割り込みをOSで受け取り FSP のハンドラへ処理を渡す
- * ( FSP のハンドラは vector_data.c に記述されている)
- */
- // UART受信割り込みハンドラ
- void _sci_uart_rxi_isr(void) {
- // FSP のUART受信割り込みハンドラ
- sci_uart_rxi_isr();
- }
- // UART送信割り込みハンドラ
- void _sci_uart_txi_isr(void) {
- // FSP のUART送信割り込みハンドラ
- sci_uart_txi_isr();
- }
- // UART送信完了割り込みハンドラ
- void _sci_uart_tei_isr(void) {
- // FSP のUART送信完了割り込みハンドラ
- sci_uart_tei_isr();
- }
- // UARTエラー割り込みハンドラ
- void _sci_uart_eri_isr(void) {
- // FSP のUARTエラー割り込みハンドラ
- sci_uart_eri_isr();
- }
- /*
- * メインタスク
- */
- void main_task(intptr_t exinf)
- {
- uint8_t c;
- // 割り込み番号とイベント番号の紐付け
- bsp_irq_cfg();
- // 各割り込みを有効化
- ena_int(INTNO_UART2_RXI);
- ena_int(INTNO_UART2_TXI);
- ena_int(INTNO_UART2_TEI);
- ena_int(INTNO_UART2_ERI);
- // シリアル通信を開く
- R_SCI_UART_Open(&g_uart2_ctrl, &g_uart2_cfg);
- while (1) {
- // 受信する
- R_SCI_UART_Read(&g_uart2_ctrl, &c, 1);
- // タスクを待ち状態にする
- slp_tsk();
- // 受信した一文字を送信する
- R_SCI_UART_Write(&g_uart2_ctrl, &c, 1);
- }
- }
- ...
まずは「_sci_uart_xxx_isr()」という関数が4つ並んでいますね。
関数名の頭にアンダーバー(_)が付いている点がポイントです。
これらはコメントの通り、シリアル通信ポートの各種割り込みハンドラです。
実は、前々回のFSP(Flexible Software Package)を使用したベアメタルでも同様の働きをする割り込みハンドラを使っていました。
これらは「e2 studio」で「Hinagata」プロジェクトを作成したときにFSPのコンフィギュレーションツールが吐き出したUARTドライバの中で実装されていて、それが影で動いていましたので特に意識はしなくても良かったのです。
その後、吐き出されたソースは前回TOPPERS/ASPのソースツリーへコピーしましたよね?
それらのハンドラは、未だに以下のパスにその実体があります。
C:\cygwin64\home\<ユーザー名>\asp_arduino_uno_r4_gcc-template\target\arduino_uno_r4_gcc\ra\fsp\src\r_sci_uart.c
この「r_sci_uart.c」の中にUARTの各割り込みハンドラがあります。
その名も…
〇void sci_uart_rxi_isr(void) … UART受信割り込みハンドラ
〇void sci_uart_txi_isr(void) … UART送信割り込みハンドラ
〇void sci_uart_tei_isr(void) … UART送信完了割り込みハンドラ
〇void sci_uart_eri_isr(void) … UARTエラー割り込みハンドラ
…関数名の頭にアンダーバー(_)が付いてない点がポイントです。
つまり「main.c」の冒頭に追記した4つのアンダーバー付き割り込みハンドラは、本来の処理を行うアンダーバー無し割り込みハンドラへ処理を渡すだけの関数ということになります。
なんでこんな面倒くさいことをやっているのか?と言えば…。
TOPPERS/ASPを含むμITRON系のRTOSは、複数のタスクを並行して実行可能にする機能に加え、割り込みも管理します。
したがって、割り込みは一度RTOS側で受け取る必要があります。
(そうじゃないと管理できませんよね。)
RTOSのあずかり知らぬところで、頭越しに勝手にFSPの割り込みハンドラを呼び出されては色々とマズいのです。
そのために、本来の割り込みハンドラの関数名の頭にアンダーバーを付けた形で、RTOSが割り込みを受け取るためのハンドラを作っているのです。
各々の本来の割り込みハンドラへバイパスするだけなんですけどね。
次に「main.c」の「main_task()」関数の中に入ります。
「main.cfg」の記述より、RTOSが立ち上がると自動的にこの「main_task()」が実行されることになっています。
そして、いきなり「bsp_irq_cfg()」という関数を呼んでいますね?
これに関しては、ちょっとだけ長い説明が必要でしょう。
一般的なマイコンは、割り込み番号とそれが何の割り込みなのか?(すなわち「イベント」)の関係性は固定されています。
たとえば、以下はSilicon Labs社の「EFM32PG23」というマイコンのデータシートの1ページです。
これは、割り込み(IRQ)番号と、それが引き起されるイベント名の一覧表です。
この中で、このマイコンのシリアル通信の受信割り込み「USART0_RX」の割り込み番号は「9」です。
同様に送信割り込み「USART0_TX」の割り込み番号は「10」ですね?
大半のマイコンは、このように割り込み番号とイベントの関係は固定です。
つまり、この型番のマイコンにおいてはシリアル通信の受信割り込み「USART0_RX」の割り込み番号は「9」って言ったら「9」であり、この世の終わりまでこれが変更されることはありません。
ところが、今回の「Arduino UNO R4」に搭載されているRA4M1マイコンはさにあらず。
上にあげた「EFM32PG23」の割り込み(IRQ)番号とイベント名の一覧表と同内容のRA4M1マイコンの表を以下に示します。
すると、割り込み(IRQ)番号に対し具体的なイベント名が書いておらず「ICU.IELSRxレジスタで選択されたイベント」などと記述されています。
結論から言えば、RA4M1マイコンは割り込み番号とイベントの関係は固定されておらず、自由に設定することができるのです。
例えば、UARTの各種割り込み(IRQ)番号を0~31まで、好きなように割り振ることができるのです。
それを行うためには「ICU.IELSRx」というレジスタに「イベント番号」というものを書き込みます。
では「イベント番号」ってどこで調べればいいの?ってことですが、ちゃんとデータシートに記載されています。
今回使用するシリアル通信ポートは「SCI2」ですので、これが引き起こす各種イベントは赤枠で囲った部分であり、それぞれ左端の「イベント番号」項目の数値に対応します。
今回は、以下のようにイベントに割り込み番号(IRQ#)を紐付けます。
〇SCI2_RXI(sci_uart_rxi_isr()) … IRQ#0番
〇SCI2_TXI(sci_uart_txi_isr()) … IRQ#1番
〇SCI2_TEI(sci_uart_tei_isr()) … IRQ#2番
〇SCI2_ERI(sci_uart_eri_isr()) … IRQ#3番
紐付けは「ICU.IELSRx」レジスタに「イベント番号」を書き込むのでしたね?
ですので、本来は以下のようなコードを書かなくてはならないのですが…
R_ICU->IELSR[<割り込み番号>] = <イベント番号>
- ...
- R_ICU->IELSR[0] = (uint32_t)0x0A3 // IRQ#0 に SCI2_RXI(イベント番号 : 0x0A3)を設定
- R_ICU->IELSR[1] = (uint32_t)0x0A4 // IRQ#1 に SCI2_TXI(イベント番号 : 0x0A4)を設定
- R_ICU->IELSR[2] = (uint32_t)0x0A5 // IRQ#2 に SCI2_TEI(イベント番号 : 0x0A5)を設定
- R_ICU->IELSR[3] = (uint32_t)0x0A6 // IRQ#3 に SCI2_ERI(イベント番号 : 0x0A6)を設定
- ...
…この処理をやってくれるのが「bsp_irq_cfg()」という関数なのです。
そもそも、この紐づけはFSPコンフィギュレーションツールで設定したものなので、当然設定内容はFSPが知っています。
なので、同じくFSPコンフィギュレーションツールが吐き出した「bsp_irq_cfg()」をコールします。
「bsp_irq_cfg()」の下の行からは「ena_int(INTNO_UART2_XXX)」という記述が4つ並んでいます。
「ena_int()」は、指定された割り込み番号の割り込みを有効にするというμITRONのシステムコールです。
「INTNO_UART2_XXX」の値は「main.h」で定義されています。
その後はベアメタル版と同様にシリアル通信を開いた後、これまたベアメタル版と似たような処理を持つwhileループに入ります。
次に「OBJ_template」の「main.h」を以下の通り修正します。
(「ここから~ここまで」のコメントの範囲で2ヵ所です。)
- ...
- /*
- * ターゲット依存の定義
- */
- #include "target_test.h"
- /*
- * 各タスクの優先度の定義
- */
- #define MAIN_PRIORITY 5 /* メインタスクの優先度 */
- /* HIGH_PRIORITYより高くすること */
- #define HIGH_PRIORITY 9 /* 並行実行されるタスクの優先度 */
- #define MID_PRIORITY 10
- #define LOW_PRIORITY 11
- /*
- * ターゲットに依存する可能性のある定数の定義
- */
- #ifndef TASK_PORTID
- #define TASK_PORTID 1 /* 文字入力するシリアルポートID */
- #endif /* TASK_PORTID */
- #ifndef STACK_SIZE
- #define STACK_SIZE 4096 /* タスクのスタックサイズ */
- #endif /* STACK_SIZE */
- #ifndef LOOP_REF
- #define LOOP_REF ULONG_C(1000000) /* 速度計測用のループ回数 */
- #endif /* LOOP_REF */
- // ここから ----------------------------------------------------------------
- #define INHNO_UART2_RXI (SCI2_RXI_IRQn + 16)
- #define INTNO_UART2_RXI (SCI2_RXI_IRQn + 16)
- #define INHNO_UART2_TXI (SCI2_TXI_IRQn + 16)
- #define INTNO_UART2_TXI (SCI2_TXI_IRQn + 16)
- #define INHNO_UART2_TEI (SCI2_TEI_IRQn + 16)
- #define INTNO_UART2_TEI (SCI2_TEI_IRQn + 16)
- #define INHNO_UART2_ERI (SCI2_ERI_IRQn + 16)
- #define INTNO_UART2_ERI (SCI2_ERI_IRQn + 16)
- #define INTPRI_UART2 -4
- // ここまで ----------------------------------------------------------------
- /*
- * 関数のプロトタイプ宣言
- */
- #ifndef TOPPERS_MACRO_ONLY
- extern void main_task(intptr_t exinf);
- // ここから ----------------------------------------------------------------
- extern void _sci_uart_rxi_isr(void);
- extern void _sci_uart_txi_isr(void);
- extern void _sci_uart_tei_isr(void);
- extern void _sci_uart_eri_isr(void);
- // ここまで ----------------------------------------------------------------
- #endif /* TOPPERS_MACRO_ONLY */
「main.c」において、RTOSが割り込みを受け取るためのハンドラを4つ追加しました。
そこで、ソースコードの関数の中で「どれが」割り込みハンドラなのか?「何の」割り込みなのかをRTOSに申告する必要があります。
この申告を行うのがコンフィギュレーションファイルです。
「OBJ_template」アプリケーションのコンフィギュレーションファイルは「main.cfg」です。
「main.cfg」を以下の通り修正します。
(「ここから~ここまで」のコメントの範囲です。)
- ...
- INCLUDE("target_timer.cfg");
- INCLUDE("syssvc/syslog.cfg");
- INCLUDE("syssvc/banner.cfg");
- INCLUDE("syssvc/serial.cfg");
- INCLUDE("syssvc/logtask.cfg");
- #include "main.h"
- CRE_TSK(MAIN_TASK, { TA_ACT, 0, main_task, MAIN_PRIORITY, STACK_SIZE, NULL });
- // ここから ----------------------------------------------------------------
- DEF_INH(INHNO_UART2_RXI, { TA_NULL, _sci_uart_rxi_isr });
- CFG_INT(INTNO_UART2_RXI, { TA_NULL, INTPRI_UART2 });
- DEF_INH(INHNO_UART2_TXI, { TA_NULL, _sci_uart_txi_isr });
- CFG_INT(INTNO_UART2_TXI, { TA_NULL, INTPRI_UART2 });
- DEF_INH(INHNO_UART2_TEI, { TA_NULL, _sci_uart_tei_isr });
- CFG_INT(INTNO_UART2_TEI, { TA_NULL, INTPRI_UART2 });
- DEF_INH(INHNO_UART2_ERI, { TA_NULL, _sci_uart_eri_isr });
- CFG_INT(INTNO_UART2_ERI, { TA_NULL, INTPRI_UART2 });
- // ここまで ----------------------------------------------------------------
コンフィギュレーションファイルは冒頭で「#include "main.h"」とされていて「INHNO_UART2_XXX」、「INTNO_UART2_XXX」などのパラメータは、修正を行った「main.h」に定義されています。
各パラメータは、それぞれ以下の意味を持ちます。
〇INHNO_UART2_RXI:
〇INHNO_UART2_TXI:
〇INHNO_UART2_TEI:
〇INHNO_UART2_ERI:
今回使用するシリアル通信ポート「UART2」の各種割り込みハンドラ番号を示します。
なお、今回のターゲットである「Arduino UNO R4」はARM-Mコアです。
ARM-Mコアの場合は、この「割り込みハンドラ番号」と下記の「割り込み番号」は同一の値となります。
ところで「main.c」の項でも説明した通り、4つの割り込み番号は0~3と指定しました。
これらは「main.h」の「SCI2_XXX_IRQn」の値を見ていただければ一致していることがお分かりいただけると思います。
しかし、その後ろの「+16」って何でしょう?
ARM-Mコアを採用しているマイコンの場合、ベンダーや型番に関わらず実装しなければいけない16個の割り込みが予め規定されています。
もう一度RA4M1マイコンの割り込み一覧表を見てみましょう。
例外番号0~15までの16個がその割り込みです。
TOPPERS/ASPは「IRQ番号」ではなく、上の表で言うところの「例外番号」で割り込みを認識する仕様となっています。
なので「+16」なのです。
他のアーキテクチャのマイコンを使って同時開発している時など、これを忘れてハマることが結構ありますのでご注意を。
(つい最近やっちまいました…。)
〇INTNO_UART2_RXI:
〇INTNO_UART2_TXI:
〇INTNO_UART2_TEI:
〇INTNO_UART2_ERI:
今回使用するシリアル通信ポート「UART2」の各種割り込み番号を示します。
前述の通り「割り込み番号」と上記の「割り込みハンドラ番号」は同一の値となります。
「+16」の意味も前述と同様です。
〇TA_NULL
何も指定しないという意味のμITRONで定められた定数です。
今回はあまり重要ではないので、説明を省きます。
興味のある方はこちらのμITRON4.0の仕様書を読んでみてください。
〇INTPRI_UART2
使用するシリアル通信ポート「UART2」の割り込み優先度を示します。
今回の場合はFSPコンフィギュレーションツールで設定した時と同じ値にしておきましょう。
もし違う値が設定された場合は、このコンフィギュレーションファイルの値ではなく、FSPでの設定値が優先されます。
ただし、注意点としては値の変換が必要なことです。
例えば、今回UART2の割り込み優先度は「Hinagata」プロジェクト作成時に「12」としました…っていうか、デフォルトでそうなっています。
しかし、μITRONの割り込み優先度の数値は以下の計算式で求めたマイナスの値を指定する必要があります。
<μITRONの優先度の数値>=<FSPの優先度の数値>-<1<<割込み優先度のビット幅>
<FSPの優先度の数値>は今回の場合「12」です。
そして<割込み優先度のビット幅>については「Arduino UNO R4」に搭載されているRA4M1マイコンは4ビットですので、「1<<4」で「16」となります。
すなわち、知りたい<μITRONの優先度の数値>PRIは…
PRI = 12 - (1 << 4)
PRI = 12 - 16
PRI = -4
…と求まります。
したがって「main.h」の「INTPRI_UART2」を「-4」で定義しているのです。
因みに、値の変換の前後に関わらず「数の少ない方が優先度が高い」設定となります。
さて、最後の最後。
割り込みコールバックルーチンの修正です。
以下のパスにある「hal_entry.c」の「uart2_callback()」関数に注目です。
C:\cygwin64\home\<ユーザー名>\asp_arduino_uno_r4_gcc-template\target\arduino_uno_r4_gcc\src\hal_entry.c
これを以下のように修正します。
(「ここから~ここまで」のコメントの範囲で2ヵ所です。)
- // ここから ----------------------------------------------------------------
- #include <kernel.h> // 追記!
- #include "kernel_cfg.h" // 追記!
- // ここまで ----------------------------------------------------------------
- #include "hal_data.h"
- FSP_CPP_HEADER
- void R_BSP_WarmStart(bsp_warm_start_event_t event);
- FSP_CPP_FOOTER
- volatile bool recieved = false; // 受信完了フラグ
- void uart2_callback(uart_callback_args_t *p_args)
- {
- if (p_args->event == UART_EVENT_RX_COMPLETE) {
- // 受信が完了したら「r_sci_usrt.c」ファイルの
- // 「sci_uart_rxi_isr()」割り込みハンドラからここに来る
- // ここから ----------------------------------------------------------------
- //recieved = true; // コメントアウト!
- // 待ち状態のタスクを起床させる
- iwup_tsk(MAIN_TASK); // 追記!
- // ここまで ----------------------------------------------------------------
- }
- if (p_args->event == UART_EVENT_TX_COMPLETE) {
- // 送信が完了したら「r_sci_usrt.c」ファイルの
- // 「sci_uart_tei_isr()」割り込みハンドラからここに来る
- }
- if (p_args->event == UART_EVENT_RX_CHAR) {
- // 一文字受信したら「r_sci_usrt.c」ファイルの
- // 「sci_uart_rxi_isr()」割り込みハンドラからここに来る
- }
- if (p_args->event == UART_EVENT_ERR_FRAMING) {
- // フレーミングエラーを検出した時に「r_sci_usrt.c」ファイルの
- // 「sci_uart_eri_isr()」割り込みハンドラからここに来る
- }
- if (p_args->event == UART_EVENT_BREAK_DETECT) {
- // ブレークを検出した時に「r_sci_usrt.c」ファイルの
- // 「sci_uart_eri_isr()」割り込みハンドラからここに来る
- }
- if (p_args->event == UART_EVENT_TX_DATA_EMPTY) {
- // 送信バッファが空になった時に「r_sci_usrt.c」ファイルの
- // 「sci_uart_txi_isr()」割り込みハンドラからここに来る
- }
- }
- ...
※「hal_entry.c」は「target」ディレクトリ直下のファイルです。
本来、アプリケーション毎に変更の可能性がある関数やファイルの修正は「OBJ~」ディレクトリ直下に存在するものに留めるべきです。
今回は説明を簡便にするために「target」ディレクトリ直下の「hal_entry.c」を書き換えています。
冒頭の「kernel.h」や「kernel_cfg.h」のインクルードは、以降の「iwup_tsk(MAIN_TASK);」という処理のために必要です。
まず「iwup_tsk()」は、指定された番号のタスクを起床させるというμITRONのシステムコールです。
元々「wup_tsk()」という同様のシステムコールが存在しますが、こちらはタスクからコールする時に使用します。
「uart2_callback()」関数はタスクではなく割り込みハンドラから直接跳んできています。
ですので、ここでは頭に「i」を付けた「iwup_tsk()」を使用します。
もし「i」を付けないとどうなるか?
少なくともTOPPERS/ASPのこのバージョンでは、正しく動作しません。
タスクからコールしているのに「i」を付けてしまった場合も同様です。
但し、同じμITRON準拠のRTOSの中でもこれらを区別しない実装もあります。
株式会社ミスポさんの「NORTi」などがそうです。
しかし、移植性を考えるとこれらを正しく使い分けるコーディングを推奨します。
(ちなみに「NORTi」のユーザーズガイドは良くまとまっていて「NORTi」に限らずμITRONのリファレンスとして超分かり易いです!)
話を戻して…。
「iwup_tsk()」に指定するタスク番号「MAIN_TASK」は「kernel_cfg.h」で定義されています。
今回の場合は「2」番ですね。
ご想像の通り、この番号が示すタスクは「main.c」の「main_task()」関数がその実体です。
- /* kernel_cfg.h */
- #ifndef TOPPERS_KERNEL_CFG_H
- #define TOPPERS_KERNEL_CFG_H
- #define TNUM_TSKID 2
- #define TNUM_SEMID 4
- #define TNUM_FLGID 0
- #define TNUM_DTQID 0
- #define TNUM_PDQID 0
- #define TNUM_MBXID 0
- #define TNUM_MPFID 0
- #define TNUM_CYCID 0
- #define TNUM_ALMID 0
- #define LOGTASK 1
- #define MAIN_TASK 2
- #define SERIAL_RCV_SEM1 1
- #define SERIAL_SND_SEM1 2
- #define SERIAL_RCV_SEM2 3
- #define SERIAL_SND_SEM2 4
- #endif /* TOPPERS_KERNEL_CFG_H */
以上で修正作業は完了です。
修正箇所は忘れずに保存してから、こちらの記事を参考にプログラムをビルドしてターゲット上で動かしてみてください。
但し、シリアル通信用の信号線はデバッガ⇔ターゲット・ケーブルから出ているもの(SCI9)ではなく、こちらの記事のようにArduinoのピンソケットから取り出したもの(SCI2)をUSB/シリアル通信変換ケーブルに接続してください。
上手く動けば、ベアメタル版と同じ内容のプログラムが走ります。
「TeraTerm」で入力した文字がそのまま表示されるプログラムでしたね!
次回は、よ~やく最終回。
このRTOS版の解説とベアメタル版との比較です。
ここまで苦労してRTOSを乗っけたけど、それに見合うメリットってあるの?
あるんですよ!(…多分ね。)
<続く>