ミドルウェア
ミドルウェアとは
今回はミドルウェアについて説明します。ミドルウェア(middleware)とは、リアルタイムOSとアプリケーションの中間(ミドル)に位置するソフトウェアです。具体的にはファイルシステムやネットワークのプロトコルスタック(TCP/IP)のように、リアルタイムOSに機能を追加するモジュールのことです。デバイスドライバはミドルウェアに含めない場合もありますが、共通点も多いため、ここでは含めて考えることにします。
組み込み開発ではリアルタイムOSとミドルウェアの上にアプリケーションを開発する形になりますが、組み込み機器のハードウェアは一般にその機器に固有のものですので、ミドルウェアやデバイスドライバ自体もユーザーが自分で開発したり移植したりするケースが少なくありません。今回は、ミドルウェアのうち、前半ではT-Kernel におけるデバイスドライバの動作や仕組みについて説明し、後半ではT-Kernel用に開発されたオープンソースの基本ミドルウェアであるT2EXを取り上げます。
デバイスドライバの呼び出し
デバイスドライバとは
組み込み機器には、例えば、表1に示すようにさまざまなデバイスが組み込まれています。このようなデバイスのハードウェアを制御するソフトウェアを、デバイスドライバ と呼びます(以下、単に「ドライバ」と呼ぶ場合があります)。各ドライバは、組み込み機器全体を制御するアプリケーションから呼ばれて動作します(*1)。
(*1)ドライバはアプリケーションからだけでなく、他のミドルウェアからも呼び出されますが、考え方は同じですので、ここではアプリケーションから呼び出される場合を例として説明します。
(a)入力デバイス | ボタン、タッチパネル、マイクなど |
---|---|
(b)出力デバイス | LED、液晶画面、スピーカーなど |
(c)通信デバイス | 無線 LAN、Bluetooth、シリアルポート(RS-232C)など |
(d)ストレージデバイス | フラッシュメモリ、ハードディスクなど |
デバイスドライバの仕様書の作成
組み込み機器の開発では、その機器に特有のハードウェアやデバイスを制御することが多いため、アプリケーションプログラムに加えて、デバイスドライバ自体も自分で開発するケースがあります。そうなると、ドライバの開発に取りかかる前に、まずドライバの仕様書を作成する必要があります。
デバイスドライバを呼び出すためのAPI(Application Program Interface)はT-Kernelの仕様書で定められていますが、これはどのデバイスにも共通となる「枠組み」にあたる部分であり、個々のデバイスに依存した仕様はデバイス毎に決める必要があります(*2)。
T-Kernelのデバイスドライバでは、デバイスに依存した特殊な処理を指定するために、「属性データ番号」と呼ばれる負の整数値を使用します。デバイスからの読み込みを行うtk_rea_dev
、tk_srea_dev
やデバイスへの書き込みを行うtk_wri_dev
、tk_swri_dev
などのAPIには、読み込みや書き込みの開始位置を示すstart
というパラメータがありますが、ここに属性データ番号を指定することにより、デバイスに特有の処理を行います。
したがって、デバイスドライバの仕様を決めることは、そのデバイスで使用する属性データ番号の値と、その値に対するデバイス固有の処理を定義することに相当します。また、そのデバイスをオープンするときの名称として使用するデバイス名も決める必要があります。これらの定義をデバイスドライバの仕様書として文書化するとともに、属性データ番号には分かりやすいニモニックを付け、プログラム中で使用するヘッダファイルを作成します。デバイスドライバの仕様やヘッダファイルを明確にすることにより、アプリケーションの開発とデバイスドライバの開発を分担して作業できるようになります。
ここでは、比較的シンプルなデバイスドライバの例として、LEDの点灯を制御するドライバを開発します。表2に、このデバイスドライバの外部仕様書の例を示します。
(*2)LANやディスクなどの標準的なデバイスに対しては、それぞれのデバイスに依存した仕様も定められています。T-Engineフォーラムから公開している「T-Engine標準デバイスドライバ仕様書」をご覧ください。
デバイス名 | “LED” |
---|---|
属性データ番号 | DN_LED_CHIP = -100 : 16ビット(UH型)データ 各ビットがチップLEDの状態に対応 |
DN_LED_SEG = -101 : 16ビット(UH型)データ 7セグメントLEDに表示する数字に対応 |
デバイスドライバのAPI
まず、このドライバを使う側となるアプリケーション開発者の立場から、T-KernelのAPIを使ったドライバの呼び出し方法を見てみましょう。
【リスト1:アプリケーションからのドライバ呼び出し例】
ID dd; W asize; UH data; dd = tk_opn_dev("LED", TD_UPDATE); data = 4; // 書き込みデータ tk_swri_dev(dd, DN_LED_SEG, &data, sizeof(data), &asize); tk_cls_dev(dd, 0);
tk_opn_dev
(T-Kernel open deviceの意味)でドライバをオープンします。文字列 “LED” はデバイス名です。tk_srea_dev
(T-Kernel synchronous read device)でデバイスからデータを読み込むか、またはtk_swri_dev
(T-Kernel synchronous write device)でデバイスにデータを書き込みます。定数DN_LED_SEG
は属性データ番号です。tk_cls_dev
(T-Kernel close device)でドライバをクローズします。
同期型と非同期型
ここでtk_srea_dev
やtk_swri_dev
の”s”は同期型(synchronous)を意味します。この場合、ドライバ内部で読み書きが終了するまで、アプリケーションは実行が中断します。一方、”s” のつかないtk_rea_dev
やtk_wri_dev
は非同期型(asynchronous)で、ドライバ内部で読み書きを行っている間も、アプリケーションは並行して動作します。この場合は、tk_wai_dev
(T-Kernel wait device)によって読み書きの完了を待ちます(図1)。
非同期型を活用すると、CPUを効率よく使って性能を向上できる場合があります。例えば圧縮された音声データを展開しながら出力する場合、ドライバでの音声出力中も、DMA転送等でCPUが空いている時間を利用して、その間にアプリケーション側で先回りして次の音声データを展開しておくことができます。
デバイスドライバの内部構造
デバイスドライバの3層構造
次に、デバイスドライバを開発する側の立場から、ドライバの内部構造を見てみましょう。ドライバは図2のような3層構造になっています。
- アプリケーションから読み込みや書き込みの要求を出すと、T-Kernel/SM(System Manager)を経由して、ドライバのインタフェース層が要求を受け付けます。
- インタフェース層では、複数の要求を排他制御しながら、論理層を呼び出します。
- 論理層では、デバイスのハードウェアの詳細に依存しない共通処理を行います。論理層は物理層を呼び出します。
- 物理層では、デバイスのハードウェアを直接制御します。
基本的な考え方として、ドライバの論理層はデバイスの種類ごとに実装し、物理層は個々のハードウェアごとに実装します。例えばLANドライバの場合、LANドライバとして共通の処理を行う部分を論理層として実装し、個々のNIC(NetworkInterface Card)やLANの制御チップに依存する部分は物理層として実装します。
インタフェース層の選択
インタフェース層としては、T-Kernel用のライブラリとして次の2種類が提供されていますので、通常はそのどちらかを選んでそのまま利用します。
SDI(単純デバイスドライバインタフェース層:simple device driver interface layer)
ドライバに渡された要求パケットは、その場ですぐに処理されます。論理層は関数呼び出しの構造になります。ドライバ内で不定期の待ちに入ることはできず、すみやかに読み書きが終わることが前提です。渡された要求に対して、処理を途中で中止することはできません。読み書きは同期型のみです(*3)。
GDI(汎用デバイスドライバインタフェース層:general device driver interface layer)
ドライバに渡された要求パケットは、いったんキューに格納されます。ドライバ内のタスクが、キューから要求パケットを取り出して処理します。ドライバ内で不定期の待ちに入ることが可能で、待ちに入った要求の中止も可能です。同期型だけでなく非同期型の読み書きもできます。
「不定期の待ち」というのは、デバイスや通信相手などの状況の変化を待つことによって発生する待ち状態のことです。例えば無線 LANドライバ内での受信処理では、通信相手からの送信データが到着しないと処理が進みませんので、実際のデータ到着まで不定期の待ちに入ります。このようなケースでは、GDIを使ってドライバを実装します。
これに対して、例えば今回の例で説明するLEDドライバの処理は、LEDが接続されたメモリアドレスへのデータ設定を行うだけです。すなわち、ドライバ側のプログラムが一方的に処理を進めるだけでよく、デバイス側の状態変化を待つ必要はありません。このようなデバイスドライバは、SDIで実装可能です。
(*3)SDIで実装したドライバに対して、”s”のつかないtk_rea_dev
やtk_wri_dev
を使うことも可能ですが、その場合でも読み書き終了までアプリケーションは中断し、同期型で動作します。
SDIを使ったデバイスドライバの実装
論理層の実装
SDIを使ったドライバの実装例として、LEDドライバの論理層のプログラムの主要部分をリスト2に示します。コールバック関数として、読み込みを担当するリード関数と、書き込みを担当するライト関数を定義して、 SDefDevice(SDI definedevice)で登録しています。リード関数とライト関数では、アプリケーションから渡された属性データ番号に対応する物理層を呼び出します。
【リスト2:SDIによる論理層の実装例(要点のみ抜粋)】
// リード関数 INT read_fn( ID devid, INT start, INT size, void *data, SDI sdi ) { switch (start) { // 属性データ番号で場合分け case DN_LED_CHIP: return read_chip( (UH*)data ); // 物理層:チップLED読み込み case DN_LED_SEG: return read_seg( (UH*)data ); // 物理層:7セグメントLED読み込み default: return E_PAR; // パラメータエラー } } // ライト関数 INT write_fn( ID devid, INT start, INT size, void *data, SDI sdi ) { switch (start) { // 属性データ番号で場合分け case DN_LED_CHIP: return write_chip( *(UH*)data ); // 物理層:チップLED書き込み case DN_LED_SEG: return write_seg( *(UH*)data ); // 物理層:7セグメントLED書き込み default: return E_PAR; // パラメータエラー } } // ドライバ開始処理 ER init_led_driver( void ) { SDefDev ddev = { .devnm = "LED", // デバイス名 .blksz = 1, // ブロックサイズ .read = read_fn, // リード関数 .write = write_fn, // ライト関数 }; init_chip(); // 物理層の初期化 init_seg(); return SDefDevice( &ddev, NULL, &sdi ); // ドライバ登録 }
物理層の実装
物理層では、上記の論理層から呼ばれる各ルーチン(init_chip、init_seg、read_chip、write_chip、read_seg、write_seg
)を実装します。この部分はLEDのハードウェアの詳細に依存します。
リスト3は、あるハードウェアでのwrite_seg
(7セグメントLED書き込み)の一例です。7セグメントLEDは、図3のようにaからgの7個(小数点を含めて8個)のセグメントで構成されます。例えば「4」を表示する場合は、b、c、f、gを点灯し、残りのa、d、e、小数点を消灯します。この実装例では、点灯と消灯のパターンを、メモリ空間上にマップされたI/Oの特定アドレスにout_w
で書き込むことで、LEDに出力しています。
【リスト3:物理層の実装例(要点のみ抜粋)】
const UW ptn[] = { // 7セグメントLEDの表示パターン SEGa | SEGb | SEGc | SEGd | SEGe | SEGf, // '0' SEGb | SEGc, // '1' SEGa | SEGb | SEGd | SEGe | SEGg, // '2' SEGa | SEGb | SEGc | SEGd | SEGg, // '3' SEGb | SEGc | SEGf | SEGg, // '4' SEGa | SEGc | SEGd | SEGf | SEGg, // '5' SEGa | SEGc | SEGd | SEGe | SEGf | SEGg, // '6' SEGa | SEGb | SEGc, // '7' SEGa | SEGb | SEGc | SEGd | SEGe | SEGf | SEGg, // '8' SEGa | SEGb | SEGc | SEGd | SEGf | SEGg, // '9' }; INT write_seg( UH data ) // 7セグメントLED書き込み { if (data > 9) // 範囲外 return E_PAR; UW p = ptn[data]; out_w( PIO_SODR(SEG_PIO), p ); // 点灯部分 out_w( PIO_CODR(SEG_PIO), ~p & SEG_ALL ); // 消灯部分 return sizeof(data); }
GDIを使ったデバイスドライバの実装
要求処理タスク
GDIを使ったドライバの実装例として、無線LANなどの通信系デバイスのドライバの例を図4に示します。ここではイベントフラグ(第3回参照)と割り込み(第10回参照)を使っています。
- ドライバの論理層では、「要求処理タスク」をアプリケーションと並行動作させます。
- 要求処理タスクは
GDI_Accept
で要求パケットを1つずつキューから取り出し、要求パケットの内容に応じて、対応する物理層を呼び出します。なお、要求パケットの中には、要求の種類(読み込み/書き込みの区別)や、tk_rea_dev
などのAPIのパラメータとしてアプリケーションから渡された情報が入っています。 - 物理層では、不定期の待ちに入ることが可能です。例えば受信要求を処理する場合、通信相手からのデータが来たときに割り込みが発生するように設定した上で、イベントフラグのいずれかのビットがセットされるまで(
TWF_ORW
を指定)、tk_wai_flg
で待ちに入ります。 - データが到着して割り込みが発生したら、割り込みハンドラの中で
tk_set_flg
を発行し、イベントフラグをセットします。 tk_wai_flg
の待ちが解除されますので、要求処理タスクが受信されたデータを取り出します。- 要求処理タスクは、
GDI_Reply
でアプリケーションに受信データを返します。
要求の中止
一方、受信待ちをしている間にそのデバイスがクローズされたり、アプリケーション側に終了要求などのタスク例外が発生した場合は、ドライバ内の待ちを中止し、要求元のアプリケーションにエラーを戻す必要があります。GDIでは、中止の処理を使って、こういった状況に対応します。中止の処理は次のように実装します。
- 「4」割り込みが発生する前に中止要求があった場合は、
tk_set_flg
でイベントフラグの別のビットをセットします。 - 「5」これによって、要求処理タスクの
tk_wai_flg
の待ちが解除されます。イベントフラグのビットパターンを参照することにより、データを受信できたのか、中止要求があったのかが分かります。 - 「6」要求処理タスクは、
GDI_Reply
でアプリケーションに中止のエラーを返します。
ここまでのまとめ
アプリケーションからは、オープン/リード/ライト型のAPIでデバイスドライバを呼び出します。デバイスドライバを実装するには、インタフェース層としてSDIまたはGDIを選択します。SDIを使う場合は、コールバック関数の定義だけでドライバを実装できます。一方 GDIを使う場合は、ドライバ内の要求処理タスクが処理を行います。このため、ドライバ内で待ちに入ることができるなど、柔軟な処理を実現できます。
T2EXとは?
組み込みシステムの世界は進化し続けています。Armコアのプロセッサをみても、Cortexファミリの登場はもちろん、最近ではbig.LITTLE処理や仮想化のための機能(virtualization extension)など多くの新しい機能や実装が次々と登場しています。なぜこのように組み込み向けのプラットフォームが進化するかといえば、実際の組み込みシステムがどんどん進化しているからです。
近年の組み込みシステムにおいては、情報処理の高度化と大規模化の進行が顕著です。例として電子レンジをとってみても、最近ではターンテーブルの代わりにセンサーを用いて均等に調理を行う仕組みになっていたり、様々な調理メニューを持っていたりするものが多くなっています。さらには、レシピを提案して表示する機能や、そのレシピに従って調理時間や温度等の設定を行い、ある程度自動的に調理を進めてくれるような製品も出ています。
そこで、リアルタイム性や省メモリといった組み込み独自の要求を満たしつつも、情報系OSに見られるような高度な情報処理機能を持ったソフトウェアが必要になってきます。これを実現するのが、ここで紹介するT2EXです。第1回の特集でも概要を紹介しましたが、T2EXはT-Kernel 2.0 Extensionの略で、直訳すると「T-Kernel 2.0(の)拡張」です。その名が示す通り、T2EXは、T-Kernel 2.0というリアルタイムOSに情報系OSの高度な機能を追加するための拡張機能のコレクションとなっています。
T2EXとアドオン・アーキテクチャ
T2EXはT-Kernel 2.0上で動作するサブシステムと、それを補完するライブラリの集合体として実現されています。T-Kernel 2.0にはOSの機能拡張を行う仕組みとして「サブシステム管理機能」 という機能が搭載されており、T2EXではこの機能を使って、機能モジュール単位に分割されたOS拡張機能が提供されています。T2EXが提供する機能は以下の通りです。
- File management(ファイル管理機能)
- Network communication(ネットワーク通信機能)
- Memory protection(メモリ保護機能)
- Calendar(カレンダ機能)
- Program load(プログラムロード機能)
- Standard C compatible library(標準C互換ライブラリ(標準入出力を除く))
- Standard input/output(標準入出力機能)
図5はT2EXアーキテクチャの全体構造を示したものです。「T2EX」と書かれた灰色の囲みがT2EX全体を、その中に入っている赤色のブロックがT2EXに含まれる各モジュールを表します。
T2EXに含まれるモジュールは、図5の中のブロックの上下関係により示された依存関係を満たす範囲で、 追加したり取り外したりすることが可能です。使用しない機能を取り外すことでROM/RAMの使用量を減らすことができますし、起動時間を短縮することも可能です。たとえば、デジタルカメラを例に考えてみましょう。このデジタルカメラでは、撮影した写真データを SDカードに保存するためにファイル管理機能を使用し、撮影日時の表示や記録のためにカレンダ機能を 使用しますが、ネットワーク通信機能やプログラムロード機能は使用しないものとします。この場合は、システムの構成を 図6のように軽量化することができます。
このように、モジュール単位で機能を追加したり取り外したりできるように設計されているのが、T2EXのアドオン・アーキテクチャの特徴です。これに対して、Linux等の情報系OSでは、ファイルやネットワークなどの機能がOSの基本的な機能(スケジューリングなど動作の根幹に関わる部分)と深く絡み合っており、機能の取り外しが簡単ではありません。そのため、組み込みLinuxと言われる製品でも一般には数MB以上のROM/RAMを必要とします。一方、T2EXでは各モジュールのプログラムサイズが小さいことに加えて(たとえばファイル管理機能は100KB以下)、使用しない機能モジュールの取り外しができますので、前述のデジタルカメラのようなシステムでも 数百KB程度のROM/RAM容量で済む場合が多いでしょう。
デジタルフォトフレームを作ってみよう
T2EXを用いた組み込みアプリケーションの具体例として、以下のような動作をするデジタルフォトフレームを作成してみましょう。
- 本体に差し込まれたSDカードの中のJPEGファイルを一つ読み出す。
- 読み出したJPEGファイルの画像を画面に表示する。(スクリーンドライバを利用)
- 一定時間そのままの状態を保持する。
- 「1」に戻る
必要なT2EXモジュールの選択
まず、本システムで使用するT2EXのモジュールを選択します。ここでは、ファイル管理機能、標準入出力機能のほか、外部データを扱うことから、メモリ保護機能も使用することにします。これら3つのモジュールのみを使用するため、kernel/sysmain/build_t2ex/tef_em1d/Makefileを編集し、使用しないモジュールの指定行の先頭に ‘#’ を追加してコメントアウトします。具体的には
# use T2EX file management T2EX_FS = yes # use T2EX network communication T2EX_NET = yes # use T2EX calendar T2EX_DT = yes # use T2EX program load T2EX_PM = yes
のような指定がある部分を
# use T2EX file management T2EX_FS = yes # use T2EX network communication #T2EX_NET = yes # use T2EX calendar #T2EX_DT = yes # use T2EX program load #T2EX_PM = yes
と書き換えることで、ネットワーク通信機能、カレンダ機能、プログラムロード機能を取り外すことができます。
ドライバやライブラリについても、kernel/usermain_t2ex/Makefile.usermainを編集し、使用しないものをコメントアウトすることで、システムから外すことができます。今回はネットワークドライバ( netdrv.o
)は使用しないので、以下の行をコメントアウトします。
#I_OBJ += $(BD)/driver/$(TETYPE)_$(MACHINE)/netdrv.o
T2EXを用いたプログラミングT2EXはT-Kernel 2.0の拡張機能ですので、T2EXを用いたシステムのプログラミングの基本スタイルは、 T-Kernel 2.0のみを用いたシステムと同じです。T-Kernel 2.0ではusermain関数がユーザープログラムの本体となっていますので、その中身を書き換える形でアプリケーションを作成します。
usermain関数の中では、T-Kernel 2.0のAPIとT2EXのAPIを混在してプログラミングすることが 可能です。T-Kernel 2.0のAPIについては、すでにタスク管理や同期・通信の基本機能をご紹介していますが、 それらの機能とT2EXの機能を組み合わせることで、リアルタイムな処理を行いながらもファイルシステムや TCP/IPなどの高度な機能を用いたアプリケーションを作ることができるようになります。
なお、T2EXのAPIはPOSIXのAPIに似ていますが、以下のような相違点もありますので、注意が必要です。
T2EXのAPIの名称には、T2EXのモジュール名の短縮名がプレフィックスとして付いています。たとえば、ファイル管理機能のAPIにはfs_
、ネットワーク通信機能のAPIにはso_
のプレフィックスが付いています。T2EXのAPIの戻り値は、T-Kernel 2.0のAPIとの混在を考え、原則として0以上の場合が成功(正常終了)、 負の値の場合がエラーコードを表します。また、エラーコードは、エラーの意味に応じていろいろな値をとります。一方、POSIXの多くの関数では、エラーの場合の戻り値として(-1)のみを使用します。次のPOSIXのプログラムを例として考えてみましょう。
fd = open("/path/to/file", O_RDONLY); if ( fd == -1 ) { fprintf(stderr, "open failed: errno = %d\n", errno); goto error; }
T2EXでこれと同じ処理を行うプログラムは、以下のようになります。
er = fs_open("/path/to/file", O_RDONLY); if ( er < E_OK ) { fprintf(stderr, "open failed: errno = %d\n", ERRNO(er)); goto error; } fd = er;
このプログラムは、これまで紹介してきたT-Kernel 2.0用のプログラムの中に混在して記述することができます。
プログラミング例
あとは、前述のデジタルフォトフレームの動作に対応する処理を、usermain関数の中に一つ一つ実装していきます。
なお、ここでは誌面の都合から、実際のプログラムリストや解説の掲載は省略させていただきます。T-Engine フォーラムのAPS ACADEMY連携RTOS講座「第6回 ミドルウェア」に デジタルフォトフレームの実装例を掲載していますので、そちらも合わせてご覧ください。
T-Kernel 2.0のデバイス管理機能と、スクリーンドライバの仕様、そしてT2EXのファイル管理機能の利用例を含んだサンプルとなっていますので、参考になる点が多いと思います。
こちらも是非
“もっと見る” RTOS編
Toppers/ASP3の使い方
SOLID-OSによる割り込みは、Toppers/ASP3がベースになっており、カーネルの管理下で割り込みの処理を行いますので、「割り込みサービスルーチン(ISR)」または「管理内割り込み」と呼んだりします。それ以外のものは、「割り込みハンドラ」もしくは「カーネル管理外割り込み」と呼びます。
組み込みソフトウェア開発にRTOSを採用する場合の「4つの心得」
リアルタイムOSを使った組み込みソフトウェア開発を行う際、覚えておいたほうがよい「4つの心得」を紹介しましょう。
組み込みOSに最適なのは?リアルタイムOSとLinuxの違い
組み込みシステムの構成を考えていく上で、どのOSを採用するかは、開発初期段階においてとても重要です。一般的には、時間的制約があるシステムの場合はリアルタイムOSが、ネットワークやファイルシステム、高度なグラフィカル表示が必要な場合はLinuxが向いていると言われています。