Monthly Archives: 1月 2018

C言語による構造化/階層化設計 入門講座(第3回)

本講座のトップページでは、C言語仕様が自由である事の功罪と、特定の個人/組織でしか通用しないローカルなルールでソフトウェア設計を束縛する事の功罪について述べました。

以上の事を踏まえ、第3回では、オブジェクト指向設計の世界で常識とされている幾つかの習慣をC言語によるソフトウェア設計に取り入れる事を提案します。今回提案する習慣は、以下の5つです。

  1. 変数と関数の有効範囲(スコープ)を局所化する。
  2. グローバル関数の呼び出しを「ソフトウェア部品名_アクション名(引数…)」で表現する。
  3. 初期化関数を持つ。
  4. ソフトウェアの構造を階層化する。
  5. 上位アプリケーションをパソコン上で動かす。

変数と関数の有効範囲(スコープ)を局所化する

第1回では「特定の役割り」を担う複数の「部品」によってソフトウェア全体を構成する事の重要性について述べ、その部品が備えるべき特性として、他の「部品」からは見えない/見る必要の無い、隠された「変数」や「関数」を有するという特性を挙げました。これは、オブジェクト指向設計において、当たり前のように適用されている習慣です。

この習慣を適用するための第一歩は、「グローバル変数」を極力、使わない事です。重要なデータは、何れかのソフトウェア部品内でstatic宣言して外部から隠ぺいし、データを所有するソフトウェア部品によって許可された方法、すなわちグローバル関数呼び出しによってのみ、更新または参照できるようにするのです。

これにより、データ構造も隠ぺいできます。たとえば「0から始まるユニークな番号」と、「16ビット長のデータ」を「対」で沢山、保持するソフトウェア部品があったとします。この場合、データは以下のような構造体配列で管理できます。

#include <stdint.h>

#include <stdbool.h>

 

#define DF_XXX_DATA_NUM  1000

 

typedef struct {

uint16_t no;

uint16_t data;

} ST_XxxDataType;

 

static ST_XxxDataType lv_XxxDataTable[DF_XX_DATA_NUM];

がしかし、このデータを参照する側のソフトウェア部品では、このようなデータ構造の詳細は知る必要が無く、参照用の関数として用意された、以下のようなグローバル関数を呼べば良いだけです。

#include <stdint.h>

#include <stdbool.h>

 

extern bool DB_ReadData(uint16_t no, uint16_t *pData);

第1引数の番号に相当するデータが存在しない場合はfalseを返し、存在する場合は、第2引数のポインタが指す先にデータを格納し、trueを返せば良いのです。

こうすれば、今後、何かの事情でデータ構造を変更する必要が生じた場合でも、データを参照する外部のソフトウェア部品のソースコードは変更しなくて済みます。またデバッグの目的でデータ参照のログを取りたい…という場合は、前記のグローバル関数の中でログを出力するだけで済みます。更に外部のソフトウェア部品のグローバル関数を「スタブ」と呼ばれるテスト用の関数に差し替える事で、単体テストがし易くなります。

このように設計する事に対して、関数呼び出しが1段増え、実行時間やコードサイズが増える事を嫌がる人がいますが、本講座のトップページで述べたとおり、昨今のマイクロプロセッサの高速化/フラッシュメモリの大容量化/低価格化を考えれば、許容されるレベルと思います。

関数についても、グローバル関数を減らす事を心掛け、外部から呼ぶ事のできないstaticな関数を中心に処理します。

グローバル関数の呼び出しを「ソフトウェア部品名_アクション名()」で表現する

オブジェクト指向設計では、本記事で提案する「ソフトウェア部品」は「オブジェクト(インスタンスと呼ばれる場合もあります)」に相当します。クラスは、オブジェクトの「型」を意味します。

そのオブジェクト指向設計では、オブジェクトが所有する関数の内、外部のオブジェクトから呼び出せる関数は「publicなメンバ関数」と呼ばれ、その関数呼び出しは「オブジェクト名.関数名()」で表現します。オブジェクト指向設計では、publicなメンバ関数呼び出しは、そのオブジェクトに対して「メッセージ」を送る行為と見なせます(「メッセージパッシング」といいます)。たとえばデータベースの役割りを担うオブジェクト「dataBase」に対して「データを読みたい」というメッセージを送るには、「dataBase.Read()」という関数呼び出しを実行するのです。

このようにオブジェクト指向設計では、ソフトウェア部品間で何らかのメッセージやデータ(メンバ関数の引数で渡す)をやり取りする場合、必ず「dataBase.Read()」のように

  • ソフトウェア部品名(すなわちオブジェクト名)」
  • メッセージ内容(すなわちpublicメンバ関数名)

を「ペア」で表現する事がプログラミング言語仕様で義務付けられています。

これに対して、C言語ではグローバル関数の呼び出しに際して、そのようなルールは存在しません。従って、本記事で提案する「幾つかのソフトウェア部品で全体を構成する」ような設計を行い、その部品間でやり取りする際、どの部品に対するアクションなのか、Cの言語仕様としては、表現する手段が無いのです。

ではどうすれば良いでしょうか?

本記事では、グローバル関数名に特別な「命名規則」を設ける事で、この問題に対処する事を提案します。具体的には、「グローバル関数名のプリフィックス(接頭語)としてソフトウェア部品名を書く」事を提案します。更に、1つのソフトウェア部品は「部品名.c」と「部品名.h」の2つのソースファイルに記述する事を提案します。ヘッダには、そのソフトウェア部品のグローバル関数呼び出しに必要な「プロトタイプ宣言」と「型宣言」「マクロ宣言」等を記述します。

たとえばデータベースの役割りを担うソフトウェア部品をCのソースファイルで「DataBase.c」「DataBase.h」として記述したとします。この場合、ソフトウェア部品名は「DataBase」になりますので、そのグローバル関数名は、たとえば「DataBase_Read()」とします。

ただしこの命名規則だけでは、static関数名と区別できないので、

  • グローバル関数は、大文字で始め、static関数は小文字で始める。
  • static関数には、プリフィックス(接頭語)として「l_」を付ける。
  • static関数名には、アンダースコア「_」は使わない。

等、static関数にも「命名規則」を設ける必要があります。

以上のようなルールは、特定の個人や組織でしか通用しないルールなのですが、その根本は、オブジェクト指向設計のルールに基づいており、効果的なルールとして機能すると思います。

初期化関数を持つ

オブジェクト指向設計では、オブジェクトを動作させる際、予め「生成」というステップを踏む必要があるのですが、その際、「コンストラクタ」と呼ばれる、自身を初期化するための専用のメンバ関数が呼ばます。C言語によるソフトウェア設計でも、この習慣を取り入れる事を提案します。具体的には、「ソフトウェア部品名_Init()」という初期化関数を準備し、起動後にSchedulerから1度だけ呼び出すようにするのです。

ソフトウェアの構造を階層化する

C言語において、変数/関数の有効範囲(スコープ)として利用できる範囲は、以下の3種類しかありません。

  • ソースファイル内(すなわちコンパイル単位)
  • 関数内
  • { }で囲まれたブロック内

従って、これらを有効活用してソフトウェアを設計する事になるのですが、理想的な有効範囲は、それぞれ

  • 1ソースファイルは、千行以内
  • 1関数は、数十行くらい
  • 1ブロックは、数行くらい

だと思いますので、大規模なソフトウェア開発では、千行くらいのソースファイルを大量に書く事になります。

変数や関数に局所性を持たせたのと同じ理由で、ソフトウェア部品についても、全ての部品と関係を持つソフトウェア部品は少なくし、一部のソフトウェア部品間でのみ関係を持つように、局所化/グループ化する事をお勧めします。その際、同一グループのソースファイル群は、サブフォルダに分けて管理する事をお勧めします。

そのソフトウェア部品の局所化/グループ化に際しては、1階層の広い平面内で局所化/グループ化するだけで無く、多層化し、階層化された空間内で局所化/グループ化する事も重要です。

この階層化ですが、オブジェクト指向設計では、オブジェクト間で「継承(is-a)」や「包含(has-a)」という関係を持たせ、階層化する事が一般的ですが、C言語では、クラスが無いため、そのような設計を行う事は難しいです。(関数ポインタをメンバに持つ構造体を使えば、同じような設計ができなくは無いですが、本記事で提案したい方法ではありません)。ただ「オブジェクト指向設計では、階層化する事が一般的なのだ」という事を念頭に置いて、C言語によるソフトウェア部品の設計に際しても、階層化を意識する事は重要と考えます。

では、どのようにソフトウェア部品を階層化すれば良いでしょうか?

その1つのアイデアは、「詳細な仕事」を担うソフトウェア部品と、その機能を活用して「より抽象的な仕事」を担うソフトウェア部品を設ける事で、階層化する案があります。第2回の「擬似パソコン化」では、

  • Device Driver
  • Hardware Abstraction Layer

というソフトウェア部品について説明しましたが、これらは階層化された関係を持つと言えます。

上位アプリケーションをパソコン上で動かす

上位アプリケーションをパソコン上で動かす事は、オブジェクト指向設計とは直接、関係しないのですが、オブジェクト指向で設計されたソフトウェアの多くは、WindowsやLinuxが稼働するパソコン上で実行されるため、ここで説明させて頂きます。

これまで述べてきたような習慣に基づいてソフトウェアを設計した場合、階層化された設計の上位に位置するアプリケーション部は、実際のハードウェア上だけで無く、パソコン上で動かす事も容易です。この記事では、上位に位置するソフトウェア部品をWindowsやLinux上のソフトウェアとして開発される事、もしくは実際のハードウェア上でデバッグされる前に、上位に位置するソフトウェア部品だけをパソコン上で実行される事(デバッグされる事)をお勧めします。この設計方法は、一部の書籍では「デュアルターゲット」として紹介されています。

パソコン上のソフトウェアとして組み込みソフトウェアの一部を開発した場合、以下のようなメリットが得られます。

ハードウェアを入手するまでに上位アプリケーション部の単体テストが実施できる

昨今の組み込みシステムの開発では、ソフトウェアとハードウェアは同時並行開発される事が多いです。更に、ソフトウェア設計者にハードウェアが手渡される際、デバッグやテストを終えた信頼性の高いハードウェアが手渡される事は稀で、ソフトウェアのデバッグの際に、併せてハードウェアの動作検証を求められる事は少なくありません。

こういうケースでは、いきなり上位アプリケーションまで含めた全ソフトウェアをハードウェア上で動かすと、不正動作の原因が解らず、混乱を招く事が多いです。

これに対して、上位アプリケーション部は、別途、パソコン上で開発/デバッグ/単体テストまで進めておき、入手したハードウェア上では、本記事で「擬似パソコン化」として提案した一部のソフトウェア部品と、その上位に位置づけられる「試験用ソフト」だけを動作させ、別々に開発を進めた場合、前記のような混乱は、最小限に抑えられます。

高度なデバッグ機能が利用できる

たとえばVisual Studio上で開発した場合(Visual C++を利用)、未初期化のauto変数を参照したり、不正なメモリをアクセスした場合は例外が発生し、ブレークする事が多いため、この種の不具合は、初期段階で改修できる可能性が高いです。これに対して、前記のような不具合を含むソフトウェアを実機上で実行した場合、不正アクセスの直後でブレークさせる事は難しく、別の異常現象として現れますので、デバッグに時間を要する事になります。

組み込みソフトウェア技術者「以外」の技術者に協力を仰げる

これまで述べてきたような習慣に基づく上位アプリケーション部の開発は、組み込み用途で無い、一般的なソフトウェア開発と大きな違いはありません。よって、技術者を確保する際、組み込みソフトウェア技術者だけで無く、C言語特有の言語仕様(ポインタ等)を勉強してもらう前提で、C#/Java等の経験者に協力を仰ぐ事も可能になります。

ダウンロードが不要

パソコン上でソフトウェアを直接(ネイティブとして)実行する場合、ダウンロードという手順は不要で、ビルドが終われば、即、実行可能です。これに対して、組み込みソフトウェアを実機上で実行するには、ダウンロードという手順が必要となります。このダウンロードですが、累計時間で見れば、それ程、負荷の高い作業では無いですが、別の作業が実施できる程の時間では無く、思考が途絶える「待ち時間」になってしまう事が多いです。故に、ビルド時間も短く、ダウンロードが不要なソフトウェア開発は、設計者にとって生産性が格段に向上したような感覚をもたらします。

第3回は以上です。続く第4回では、これまで述べた習慣に基づき、実際に「電気ポット」のソフトウェアを設計します。

C言語による構造化/階層化設計 入門講座(第2回)

第2回では、Schedulerについて説明します。Schedulerは、第1回の「擬人化」「擬似パソコン化」の説明の中で、最後に登場したソフトウェア部品です。

Schedulerは、ソフトウェア部品が有するグローバル関数の内、並列実行の対象となる関数群のスケジューリング(実行順序制御)を担います。

グローバル関数が呼ばれるタイミング

各ソフトウェア部品は、それぞれ幾つかのグローバル関数を有しますが、それらのグローバル関数が呼ばれるタイミングは、以下の3種類に分類できます。

  1. 他のソフトウェア部品から直接呼ばれる。
    他のソフトウェア部品に対して、入出力、表示、生成等の機能を提供するグローバル関数は、その機能が必要になったタイミングでそれを必要とする他のソフトウェア部品から直接、呼ばれます。
  2. Event Generatorのコールバック関数として呼ばれる。
    ハードウェアの入出力、変換等、状態が変化したタイミングでEvent Generatorから呼ばれます。
  3. 並列処理関数として呼ばれる。
    他のソフトウェア部品と並列に実行しなければならない関数は、Schedulerと呼ばれるソフトウェア部品から呼ぶようにします。

この記事では、並列処理関数の設計手法である「ステートマシン」について説明した後、上記3のSchedulerによるスケジューリングについて説明します。

なお本記事では、リアルタイムOSなしでスケジューリングを行う事を想定しています。

並列処理を実現するための留意点

組み込みシステムでは、前記1~2のいずれの場合でも、1つの関数でCPU実行権を長時間、占有する事は許されません。なぜなら、そういう関数を作ってしまうと、並列動作させるべき他の処理が実行できないからです。

1回あたりの関数呼び出しで許容されるCPU占有時間(すなわち実行時間)は、開発するシステムの要件に依存します。故に、一概に「XXmsec以内」とは言えませんが、たとえばイベント発生待ちや、大量の計算/検索のために、実行時間の長い繰り返し処理を1回の関数呼び出しで処理する事は避けた方が良いと思います。

では、どうすれば良いでしょうか? この記事では、並列処理関数の設計手法として「ステートマシン」を紹介します。

ステートマシンとは

ステートマシンは、組み込みソフトウェアでは一般的な設計手法です。オブジェクト指向設計のデザインパターンでは「ステートパターン」に相当します。関数が呼ばれた際、毎回、同じ処理を実行するのでは無く、現在の「状態」に応じて異なる処理を実行するように設計します。

たとえばある処理を行う過程で、イベント発生を待たなければならないとします。この場合、関数の中の while文でイベント発生を待ってしまうと、目的のイベントが発生するまで、CPU実行権を占有してしまいます。この問題を回避するため、たとえば「前処理」「イベント発生待ち」「メイン処理」の3つのグローバル関数に分割し、Schedulerに呼び分けてもらう案も考えられますが、この方法では、ソフトウェア部品の内部仕様を外部にさらす事になり、更にグローバル関数の数も増えるため、望ましくありません。

こういうケースでは、ステートマシンの設計手法を利用します。イベント待ちが必要ならば「初期状態」「イベント待ち状態」の2つの状態を持つようにします。この「状態」を保持する変数は、enum型のstatic変数として定義します。

typedef enum {

e_Init = 0,

e_WaitEvent

} EN_State;

static EN_State lv_state = e_Init;  /* 関数の状態種別 */

関数の実装では、swtich~case文で、現在の状態に応じて、異なる処理を実行するようにします。

void 並列処理_その1(void)

{

switch(lv_state) {

case e_Init:

前処理();

lv_state = e_WaitEvent;

break;

case e_WaitEvent:

if (イベント発生チェック() == true) {  /* イベント発生済かチェック */

メイン処理();             /* イベントに応じたメイン処理 */

lv_state = e_Init;         /* 次回の呼び出しに備えて、状態を戻す */

}

else {

;           /* イベント未発生なら何もせずに直ぐに戻る */

}

break;

defalut:

内部エラー処理();      /* あり得ない(内部エラー処理) */

lv_state = e_Init;

break;

}

}

イベント発生は、上記の関数呼び出しとは非同期に、Event Generatorによってコールバック関数で通知してもらうようにしておき、その情報は、static変数に保存しておきます。イベント発生チェック()では、コールバック関数で通知され、static変数に保存された直近のイベント情報をチェックするようにします。

このように設計すれば、このソフトウェア部品と外部のソフトウェア部品間のインタフェースは、イベント通知のコールバック関数と、並列処理関数の2つに集約でき、更にこのソフトウェア部品がどういう状態を持つかは外部から隠ぺいできます。

Schedulerによるスケジューリング

前記のとおり、並列処理関数をステートマシンで設計した後、Schedulerと呼ばれるソフトウェア部品から、各並列処理関数を呼び出します。

その際の最も単純なスケジューリングの方法は、ラウンドロビンと呼ばれる方法です。ラウンドロビンでは、並列処理に相当するグローバル関数を単純に順番に、繰り返し呼び出します。たとえば、以下のような感じです。

void Scheduler_main(void)

{

while (true) {

並列処理_その1();

並列処理_その2();

並列処理_その3();

並列処理_その4();

}

}

上記関数は、関数mainから呼ばれる無限ループの関数です。これが最も単純なスケジューリングの方法です。

この方法に対して更に、イベントAが発生した場合は「並列処理_その1()」と「並列処理_その2()」は休止させて、代わりに「並列処理_その5()」を実行し、その処理が終わり次第、元の状態に戻すというようなスケジューリングを追加する事も可能です。この場合、Scheduler内のスケジューリング関数Scheduler_main自体をステートマシン化し、以下のように設計します。

typedef enum {

e_Normal = 0,

e_Emergency

} EN_State;

static EN_State lv_state = e_Normal;  /* 関数の状態種別 */

 

void Scheduler_main(void)

{

while (true) {

bool isComplete = false;

 

/* イベント発生チェック */

if (lv_state == e_Normal) {

if (イベントAの発生チェック() == true) {

lv_state = e_Emergency;

}

}

swtich (lv_state) {

case e_Normal:

並列処理_その1();    /* 定常時の処理 */

並列処理_その2();

break;

case e_Emergency:

isComplete = 並列処理_その5();   /* 緊急時の処理 */

if (isComplete == true) {

lv_state = e_Normal;     /* 緊急処理が完了したなら状態を戻す */

}

break;

}

並列処理_その3();  /* 定常時/緊急時 共に実行すべき処理 */

並列処理_その4();

}

}

上記の実装例では、イベントAの発生後、緊急処理の「並列処理_その5()」が開始されるまでの待ち時間は、最大で「並列処理_その1()」~「並列処理_その4()」の最大時間の「合計」になります。この待ち時間が長すぎる場合は、関数Scheduler_mainの状態管理を詳細化する事で、「並列処理_その1()」~「並列処理_その4()」の中の「最大時間」まで短縮できます。

以上のように、ステートマシンとSchedulerの設計手法を利用する事で、リアルタイムOSなしでも、ソフトウェア部品の独立性を維持しつつ、リアルタイム性を確保した設計が可能になります。

第2回はここまでです。次回からは、本記事で提案する以下の2つの習慣

  1. 「特定の役割り」を担う「部品」によってソフトウェア全体を構成する。
  2. オブジェクト指向プログラミングの習慣を取り入れる。

のうち、後者の「オブジェクト指向プログラミングの習慣を取り入れる」について説明します。

参考資料:ステートマシンによる並列プログラミング入門

C言語による構造化/階層化設計 入門講座(第1回)

第1回では、本講座で提案する「2つの習慣」

  1. 「特定の役割り」を担う「部品」によってソフトウェア全体を構成する。
  2. オブジェクト指向プログラミングの習慣を取り入れる。

のうち、1について解説します。このページでは、以下の内容について説明しています。

「特定の役割り」を担う「ソフトウェア部品」とは?

「特定の役割り」を担う「ソフトウェア部品」とは、たとえば以下のような「特性」を備えたC言語の「モジュール」、すなわちコンパイル/リンク単位です。

  • 他の「部品」は担わない、その部品ならではの「役割り」を担う。
  • 他の「部品」からは見えない/見る必要の無い、隠された「変数」や「関数」を有する。
  • 「部品」間は「グローバル関数呼び出し」によって情報交換(すなわち入出力)を行う。

グローバル変数は、基本、NGとしたいですが、グローバル変数を使わない代わりに、Getter/Setterと呼ばれる種のグローバル関数を乱用する事もNGとしたいです。システムを分析し、要件を満たす最適な部品と、その組み合わせを見つけ出した結果、グローバル変数の代わりに、少ないGetter/Setter関数を用意すれば済む…を目指します。

オブジェクト指向設計の用語を使って説明しますと、唯一のインスタンスしか生成できない(Singletonな)クラスを使ってソフトウェアを設計するイメージです。

ではどうやって、上記で説明したような、システムにおける最適な「部品」の組み合わせを見つけ出せば良いでしょうか?この記事では、そのためのアイデアとして「擬人化」「擬似パソコン化」という観点を提案します。

擬人化

「擬人化」では、たとえば我々の目の前に複雑で難解な仕事があった場合に、複数のメンバで協力し、各メンバはそれぞれ単純で解り易い仕事を担い、メンバ全員で協力して協調的に仕事する時のような発想で、役割りを決めます。たとえば「Watcher(見張り人)」「Generator(生成人)」「Converter(変換人)」「Viewer(表示人)」「Manager(管理人)」のような役割りを設けます。

  • Watcher(見張り人)は、何かを監視し、その結果を周りに伝える役割りを担います。
  • Generator(生成人)は、何かを生成し、その結果を周りに提供する役割りを担います。
  • Converter(変換人)は、何かから別の何かを生成し、その結果を周りに提供する役割りを担います。
  • Viewer(表示人)は、何かを表示する役割りを担います。
  • Manager(管理人)は、全体をまとめる役割りを担います。

こういうような役割りを設け、モジュール名として「xxxWatcher.c/.h」というよう名前を付ければ、仕様書を読まなくても「このモジュールはxxxの監視を担うモジュールだな」と、大よその推測ができます。

擬似パソコン化

Webアプリやゲームソフトは、予め決められた環境下で動作します。たとえばパソコン上のWebブラウザや、Androidスマホ上などです。これらの環境下でソフトウェアを開発する場合は、GUIボタン表示、そのボタンがクリックされた際のイベント発生等、殆どのソフトウェアが同様に必要とするようなソフトウェア部品は予め準備されており、プログラマは、それらを最初から利用できます。

しかし組み込みソフトウェアにおいては、そのような標準的な動作環境は存在せず、システム固有の入出力装置を備えたハードウェア上で動作させる必要があります。OS(オペレーティングシステム)も、プロジェクトの要件によって、使用できる場合と、使用できない場合があります。

このように、標準の動作環境が無い組み込みソフトウェアですが、そのソフトウェア開発において「まず動作環境から作成する」というようなステップが取られる事は稀です。ですので、たとえばアプリケーション処理から、直接、低レベルの入出力のためのレジスタを制御してしまう…というような事が行われがちです。このようなソフトウェアを開発してしまうと、その後、ソースコードを別のマイコン上で動作するように移植せねばならない…というような流用開発の時は、アプリケーション中に点在する入出力処理を書き換えないといけなくなります。

「擬似パソコン化」では、アプリケーションソフトが動作する「動作環境」を「特定の役割り」と位置づけ、部品化します。「擬似パソコン」は、たとえば以下のような部品(モジュール)で構成します。

  • Device Driver
  • Hardware Abstraction Layer
  • Event Generator
  • Scheduler

Device Driver とHardware Abstraction Layer

Device Driver(以下Driverと記す)は、入出力ポート、シリアルI/O、A/D変換器等、マイコンの周辺ハードウェア(Peripheralといいます)を制御する役割を担います。

またHardware Abstraction Layer (以下Halと記す)は、Driver内のグローバル関数を呼び出し、上位アプリケーションに対してアプリケーション固有の入出力機能を提供します。

DriverとHalの違いですが、たとえばDriverは入力ポート0番の第1ビット目から入力する 等、マイコン周辺のハードウェアに強く依存した機能を提供するのに対して、Halは周辺ハードウェアの更に先に接続されたハードウェアを制御する事に着目している点が違います。たとえば温度センサを扱うシステムの場合、上位アプリケーションは、Halとして準備された関数Hal_GetTemprature()を呼ぶ事になります。これを受けてHalでは、温度センサがシリアルI/Oで繋がっているシステムならば、Drv_ReadSio(uint8_t ch)のようなDriver関数を呼ぶ事になります。

このような役割分担にする事で、上位アプリケーションは、温度センサをA/D変換器で制御するのか、シリアルI/Oで制御するのか、意識する必要が無くなり、Halに委ねる事ができます。

Event Generator

Event Generatorは、ハードウェアの状態変化を検出し、該当するコールバック関数を呼び出す役割を担います。

パソコンやスマホでは、マウスやタッチパッドを操作した際、該当するコールバック関数が呼ばれます。たとえばマスクがクリックされた際は、関数OnClickが呼ばれる…という感じです。これと同じ要領で、組み込みシステムの周辺ハードウェアの状態変化をコールバック関数を呼び出す方法で上位アプリケーションに通知します。

Event Generatorは、たとえば「スタートボタンは1秒間、長押しされた際に検出される」というようなアプリケーション固有の要件があった場合、その要件に従って状態変化を検出します。こういう役割分担にする事で、上位アプリケーションは、状態変化の検出はEvent Generatorに任せ、それ以外の仕事に専念できるようになります。

Event Generatorは、ハードウェアに依存した処理を行いますので、Halと同じく、Driver関数を直接呼び出してもかまいません。

Event Generatorのコールバック関数を登録する方法としては、コールバック関数を登録するためのグローバル関数をEvent Generator側に用意し、他のソフトウェア部品から登録関数を呼ぶ際、呼び出して欲しいstatic関数のポインタを引数として渡す方法が考えられます。この場合、Event Generatorは、ポインタ間接でstatic関数を呼び出す事になります。

もしくはEvent Generatorの仕様書に「xxxイベント検出時は、zzzモジュール内のグローバル関数yyyを呼び出す事」というような仕様を記載しておき、そのとおりに実装する方法も考えられます。この2つの方法のいずれの場合でも、関数名の命名において、その関数は何のイベントが発生した際に呼ばれるコールバック関数であるか、第三者に解るよう配慮しておく事をお勧めします。たとえばcallback_StartButton()のような感じです。

第1回はここまでです。「特定の役割り」を担う「ソフトウェア部品」を選定するために、「擬人化」と「擬似パソコン化」の2つの観点で役割分担を検討してみました。
次回は、組み込みシステムにおいて極めて重要な「Scheduler」について説明します。