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

By | 2018年1月24日

第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」について説明します。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です