TOPPERSのコンフィギュレータで独自静的APIを追加する
この記事ではTOPPERSのコンフィギュレータで独自静的APIを追加する方法を説明します。
一つ前のバージョンですが、こちらの資料「TOPPERSコンフィギュレータの進んだ使い方」に詳しい説明が書かれています。
まず、静的APIについて説明します。
静的APIとは
TOPPERSのRTOSはμITRON仕様を実装・発展したものとなっています。
μITRON仕様には静的APIというものがあります。例えば以下のようなものです。
CRE_TSK(TSKID_MAIN, {TA_HLNG|TA_ACT, 0, main, 1, 128, NULL});
ここを参照しました。
この例ではタスクを1つ定義しています。この記述はC言語のソースコードファイルではなくcfgファイルに書きます。
ビルドの際にコンフィギュレータというプロセスを通すことでC言語のソースコードに変換し、RTOSで必要な情報を定義することが出来ます。TOPPERSのRTOSの場合CRE_TSKは、kernel_cfg.cやkernel_cfg.hに出力されます。
TOPPERSのコンフィギュレータは2種類ありますが、この記事では第三世代カーネル向けを使用します。前の世代の仕様書はここにありますが、最新版の仕様書は見当たりませんでした。
コンフィギュレータの生成したコードを見てみます。kernel_cfg.cの該当部分の出力イメージです。
const ID _kernel_tmax_tskid = (TMIN_TSKID + TNUM_TSKID - 1);
static STK_T _kernel_stack_TSKID_MAIN[COUNT_STK_T(128)];
const TINIB _kernel_tinib_table[TNUM_TSKID] = {
{ (TA_HLNG|TA_ACT), (intptr_t)(0), (TASK)(main), INT_PRIORITY(1), ROUND_STK_T(128), _kernel_stack_TSKID_MAIN },
}
TCB _kernel_tcb_table[TNUM_TSKID];
カーネルの内部では、全てのタスクの情報を配列に入れて管理しています。ユーザーがIDを指定すると、カーネル内部では配列のインデックスとして使用するようになっています。
コンフィギュレータはその配列やIDをcfgファイルから生成する機能を担っています。
タスクの初期化情報は、_kernel_tinib_tableという配列にまとめられます。cfgファイルに定義した内容とともに初期値が設定されます。
タスクの実行時に変化する情報を入れる領域が_kernel_tcb_tableという配列で、全タスク数分確保されます。
タスクのスタック領域も指定がなければ、変数を定義して確保されます。
次にkernel_cfg.hの該当部分の出力イメージです。
#define TNUM_TSKID 3
#define TSKID_MAIN 1
静的APIで指定したTSKID_MAINを定義名とした、タスクIDが定義されます。IDが配列のインデックスになっています。
コーディング時にタスクIDを自分で割り当てなくても、コンフィギュレータが採番します。
このように静的APIをコンフィギュレータに掛けることで、cfgファイルに定義したい内容で、C言語のコードを出力し領域の確保やIDの採番、定義の出力などを行ってくれます。
また、TOPPERSのcfgファイルは別のcfgファイルをINCLUDEすることが出来るので、共通のcfgファイルを用意して複数のアプリから使用することが出来ます。
コーディングをしていると、何かオブジェクトを配列で複数管理することはよくあると思うので、静的APIを独自に定義すると便利になると思いませんか?
TOPPERSの第三世代カーネル(ASP3やFMP3など)向けのコンフィギュレータはRubyスクリプトになっていますので、独自の静的APIを追加するには、テキストファイルの出力プログラムとして書けばよいので、特殊な作法はいりません。VSCodeなどを使ってコンフィギュレータのデバッグも出来ます。
lwIPのTCP APIを静的APIにしてみる
lwIPのAPIを例に静的APIを追加し、それをコンフィギュレータで処理するコードを作ってみたいと思います。
説明用の試作なので完全なものではないですが、参考になればと思います。
組込み系のOSSでよく使われるlwIPというTCP/IPプロトコルスタックがあります。
lwIPではプロトコル動作のための情報を保存する領域をPCB(Protocol Control Block)と呼びます。一方μITRONではタスク動作のための情報を保存する領域をTCB(Task Control Block)と呼びます。似てます。
lwIPでTCP通信を行う場合はtcp_newでstruct tcp_pcbの領域を確保してtcp_bindで、通信の準備を始めます。
lwIPのtcp_newはメモリ領域を確保するだけなので、静的APIでTCP PCBの配列にメモリを割り当てます。その後TCP PCBを使う別のAPIには、IDで指定するようなμITRON的な使い方をするような設計にします。追加する静的APIをCRE_TCPとしてcfgファイルには下記のように書けるようにしたいと思います。
#include "main.h"
CRE_TCP(TCP_SERVER, {IP_ADDR_ANY, 0})
CRE_TCPについてコンフィギュレータで行う処理は、tcp_newで返る領域をTCP PCBの配列の一項目として確保し、tcp_bindの呼び出しで構造体に設定する値を、項目の初期値としてコードを出力することです。静的なコードを出力することでtcp_newとtcp_bindの呼び出しを不要にします。
使い方は下記のコードになります。まずmain.cです。
#include "main.h"
#include "lwip_cfg.h"
static err_t
server_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
return ERR_OK;
}
void main()
{
//struct tcp_pcb *TCP_SERVER;
struct tcp_pcb *lpcb;
//ここから
//TCP_SERVER = tcp_new();
//tcp_bind(TCP_SERVER, IP_ADDR_ANY, 0);
//ここまでをCRE_TCPで静的に変数定義してしまう。
lpcb = tcp_listen(TCP_SERVER);
tcp_accept(lpcb, server_accept);
}
mann.hは下記のようになります。TA_NULLはTOPPERSのヘッダーファイルに定義されていますが、説明のため書いてあります。
#include "lwip/opt.h"
#include "lwip/ip_addr.h"
#include "lwip/tcp.h"
#define TA_NULL 0
静的APIを使うと、実行時に行っていたメモリの割り当てと変数の初期化を、生成したコードに書いて静的に定義することが出来ます。
RTOS領域の組込みソフトでは、コードで静的に定義するとこがメリットになることがあります。
独自静的APIの仕様
今回説明するlwIP向けコンフィギュレータに必要なファイルは下記になります。
| ファイル | 概要 |
|---|---|
| lwip_api.def | 静的APIの定義を書くファイル |
| lwip_sym.def | 静的APIで使用する定義を書く |
| lwip.trb | コンフィギュレータのRubyスクリプト |
コンフィギュレータで出力するファイルは下記のようにします。これは、lwip.trbのRubyコードで出力します。
| ファイル | 概要 |
|---|---|
| lwip_cfg.h | TCP PCBのIDを定義する |
| lwip_cfg.c | TCP PCBの配列を定義する |
作成した静的APIを試すためのコードは下記のファイルになります。
| ファイル | 概要 |
|---|---|
| main.cfg | コンフィギュレータへの入力する用意するファイル |
| main.h | ソースコード |
| main.c | ソースコード |
静的APIの仕様的な定義は下記になります。これは人間向けでコンフィギュレータへの入力にはなりません。
CRE_TCP(ID tcpid, { ATR tcpatr, ip_addr_t ipaddr, u16_t portno })
コンフィギュレータで出力する内容
静的APIで出力する内容はlwip_cfg.cではTCP PCBの配列を定義し、下記のようにします。
#includeはコンフィギュレータのコードで固定的に出力する以外に、cfgファイルから拾ってきたものをGenerateIncludesで出力ることも出来ます。
/* lwip_cfg.c */
#include "lwip_cfg.h"
#include "main.h"
struct tcp_inib tcp_inib_table[TNUM_TCP_PCB] = {
{IP_ADDR_ANY, 0}
};
lwip_cfg.hではTCP PCBのIDを定義し、下記のようにします。lwIPのAPIが数値のIDではなくPCB構造体へのポインタを渡す仕様になっているので、IDを数値ではなく配列の1項目を返すようにしています。
話がそれますが、数値のIDを使うようなlwIPのラッパーAPIを作れば、lwIPが操作するメモリと、ユーザーの操作するメモリを分離できると考えています。CPUのメモリ保護機能を使えるようにするのも、RTOS領域の組み込みソフトではメリットがあるのではないかと思っています。
/* lwip_cfg.h */
#ifndef LWIP_CFG_H
#define LWIP_CFG_H
#include "lwip/ip_addr.h"
#include "lwip/tcp.h"
struct tcp_inib {
ip_addr_t *addr;
u16_t port;
};
#define TNUM_TCP_PCB 1
#define TCP_SERVER &tcp_pcb_table[1]
extern struct tcp_pcb tcp_pcb_table[TNUM_TCP_PCB];
#endif /* LWIP_CFG_H */
静的APIの実装
静的APIからコードを出力するコンフィギュレータの実装について説明します。
コンフィギュレータは、カーネルの配布物に含まれています。例えば、ASP3のasp3-3.6.0.tar.gzをダウンロードして、展開した中にあるcfgフォルダに一式入っています。
cfg.rbを呼び出すことで、コンフィギュレータを実行することが出来ます。cfg.rbでは静的APIやcfgファイルからコードを出力するための情報収集を行う処理を行い、カーネルオブジェクトごとに用意してある個別の*.trbに渡してコードを出力します。
今回の静的APIのコンフィギュレータのメインの処理はlwip.trbになります。
静的APIの定義はlwip_api.defに行います。人間向けの仕様的な定義から引数型やカンマを削除し、記号を追加したもになります。記号の意味は上で紹介した資料を参照してください。
CRE_TCP #tcpid* { .tcpatr &ipv4addr &portno }
メインの処理で使用する#define定義などC言語の値で、コンフィギュレータで参照したい定義名をlwip_sym.defに列挙します。コンフィギュレータではlwip_sym.defの定義をC言語で出力しコンパイル、リンク、ダンプして、シンボルのアドレスとダンプしたメモリから値を取得するために必要です。
TA_NULL
最後にメインの処理は以下のようになります。
$lwipCfgH = GenFile.new("lwip_cfg.h")
$lwipCfgH.add(<<EOS)
/* lwip_cfg.h */
#ifndef LWIP_CFG_H
#define LWIP_CFG_H
struct tcp_inib {
ip_addr_t addr;
u16_t port;
};
EOS
$lwipCfgH.add("#define TNUM_TCP_PCB #{$cfgData[:CRE_TCP].size}")
$cfgData[:CRE_TCP].each do |key, params|
$lwipCfgH.add("#define #{params[:tcpid].str}\t&tcp_pcb_table[#{params[:tcpid].val}]")
end
$lwipCfgH.add(<<EOS)
extern struct tcp_pcb tcp_pcb_table[TNUM_TCP_PCB];
EOS
$lwipCfgC = GenFile.new("lwip_cfg.c")
$lwipCfgC.add(<<EOS)
/* lwip_cfg.c */
#include "lwip_cfg.h"
EOS
GenerateIncludes($lwipCfgC)
$lwipCfgC.add("struct tcp_inib tcp_inib_table[TNUM_TCP_PCB] = {")
$cfgData[:CRE_TCP].each do |key, params|
$lwipCfgC.add("\t{#{params[:ipv4addr]}, #{params[:portno]}}")
end
$lwipCfgC.add("};")
$lwipCfgH.append(<<EOS)
#endif /* LWIP_CFG_H */
EOS
cfgファイルでCRE_TCPで定義した値は、$cfgData[:CRE_TCP]で取り出すことが出来ます。Rubyの配列になっているのでeachで、一項目の情報をparamsに取り出して、配列の初期値を出力するのに使用します。引数の内容はparams[:tcpid]のように引き数名で取り出せます。IDに関しては、strが定義名TCP_SERVERでvalが採番したIDの値になります。
そのほか実装に必要な知識はRubyでのプログラム知識なので、分岐や繰り返し制御、変換処理やチェック処理があればRubyの使い方を調べて実装することが出来ます。
コンフィギュレータの実行
コンフィギュレータは下記の手順で行われます。
- C言語の定義からの情報収集用コード(
cfg1_out.*)を出力(--pass 1) - (
cfg1_out.*)をビルドし、ダンプして*.dumpと*.symsを作成 - 1と2の情報からコードを出力(--pass 2)
上記の手順はMakefileに定義されているのですが、コンフィギュレータのデバッグのために、個別に実行できた方が良いと思いますので、コマンドの例を下記の紹介します。
1に対するコマンドは、下記のようになります。
ruby cfg/cfg.rb --pass 1 --kernel asp -T lwip.trb --api-table lwip_api.def --symval-table lwip_sym.def main.cfg
2に対するコマンドは、下記のようになります。
gcc -o cfg1_out.exe -I lwip/src/include -I contrib/examples/example_app -I contrib/ports/win32/include cfg1_out.c
nm -n cfg1_out.exe > cfg1_out.syms
objdump -s cfg1_out.exe > cfg1_out.dump
3に対するコマンドは、下記のようになります。
ruby cfg/cfg.rb --pass 2 --kernel asp -T lwip.trb
おわりに
簡単になるよう内容を絞ったので説明が足らない点があるかと思いますが、独自静的APIで静的なコードを生成できるようにすると、後続開発などの助けになるのではと思いますので、ぜひ試してみてください。