組み込みソフトウェアのアーキテクチャ設計
機能を1つ足すだけのはずが、気付けば5つのファイルを修正していた―組み込み開発では珍しくありません。その原因はコードの品質というより、モジュール同士のつながり方にあります。層をどこで切り、依存の矢印をどちらへそろえるのかという判断を先送りにするほど、変更コストは膨らみます。
- 組み込みソフトウェアにアーキテクチャ設計が必要な理由
- 4層構造の一例とその責務
- 依存方向の原則と依存性逆転(DIP)
- Clean ArchitectureとHexagonal Architectureの考え方
- AUTOSAR Classic Platformに学ぶ層構造
- 段階的にアーキテクチャを整える手順
組み込みソフトウェアにアーキテクチャ設計が必要な理由
アーキテクチャ設計は大規模開発だけの話だと受け取られることがあります。ところが小規模なマイコン開発でも、後回しにした影響は早く表面化します。階層と依存方向を先に決めておけば、変更コストと再利用性は両立しやすくなります。
機能追加のたびに膨らむ結合を防ぐ
設計を後回しにしたコードベースでは、モジュール同士が互いを参照し合う結合が生まれます。相互参照が増えるほど、機能追加のときの影響範囲は見積もりにくくなります。依存関係を行列で表す手法(Design Structure Matrix:DSM)を用いると、変更がどこへ波及し得るかを評価しやすくなります。結合を防ぐ第一歩は、依存関係を可視化して分析できる形にしておくことです。
安全規格が要求する構造化(ISO 26262-6・IEC 61508-3)
構造化は設計者の好みだけで決めるものではなく、安全規格でも考慮すべき事項として扱われています。車載の機能安全規格であるISO 26262のパート6は、ソフトウェアアーキテクチャ設計の要求を第7章に定めています。米国運輸省道路交通安全局(NHTSA)の報告書は、ISO 26262-6(第1版、2011)のソフトウェアアーキテクチャ設計(Clause 7.4.2)について説明しています。検証可能性、テスト容易性、モジュール性、最小複雑性などを考慮する旨が述べられています。出典は「Assessment of Safety Standards for Automotive Electronic Control Systems」(Report No. DOT HS 812 285、2016年)です。なお同規格は2018年に第2版が発行されており、条番号は版によって異なります。産業機器向けのIEC 61508-3のAnnex Bには、モジュール化を含むソフトウェア設計に関する各種技法や方策が示されています。
4層構造の一例とその責務
層の分け方に決まった正解はありませんが、組み込みソフトウェアでは責務を分担するためにレイヤ構造を採用する設計があります。ここでは、代表的な適用例として4層構造を取り上げます。責務の分担を整理すると、この4層モデルでは、上位ほど何をするかを担い、下位ほどハードウェアに近い処理を受け持ちます。
アプリケーション層・サービス層・HAL層・ドライバ層
アプリケーション層に製品固有の業務ロジックを配置し、サービス層には通信やロギングなど、アプリケーションから共通して利用する機能を配置します。続くHAL層(Hardware Abstraction Layer:ハードウェア抽象化層)は、ペリフェラルの違いを吸収したAPIを上位へ提供する層です。この記事では、レジスタアクセスを担う最下層をドライバ層と定義します。この4層モデルでは、最下層のドライバ層だけがレジスタに触れることをルールとしているため、ハードウェア変更の影響をドライバ層に閉じ込めやすくなります。
層の粒度は機能単位ではなく関心事の分離で決める
層を切るときに迷いやすいのが粒度です。センサ機能、UI機能というように機能単位で層を切ると、どの層にも似た処理が現れて重複が生まれます。通信、計測、表示、永続化といった変更理由の異なる関心事を分離すると、責務を整理しやすくなります。アーキテクチャ記述法の資料でも、実装単位のモジュール視点と実行時の視点は別のビューとして定義されています。
依存方向の原則と依存性逆転(DIP)
層を分けただけでは、変更に強い構造にはなりません。上位層が下位層の実装を直接呼び出していると、ハードウェアの変更が上位まで波及してしまいます。依存性逆転の原則(DIP:Dependency Inversion Principle)は、依存方向の矢印の先を実装から抽象へ付け替えます。
上位層は下位層の実装ではなく抽象に依存する
上位層が依存する先を、具体的な送信関数ではなく送信インタフェースへ変えます。C言語では、関数ポインタをまとめた構造体を介して実装を切り換える方法があります。実装を差し替えても、上位層が参照する型は変わりません。たとえばLinuxカーネルのVFS(Virtual File System:仮想ファイルシステム)では、struct file_operationsのように関数ポインタ表で実装の差し替えを実現しています。
インタフェースをどこに置くかで依存方向を整理する
同じ抽象を使っていても、インタフェースの置き場所しだいで結果は変わります。上位のロジックが必要とするインタフェースは、上位側で定義します。そのインタフェースを下位層側で実装すれば、実装の差し替えによる影響が上位へ波及しにくくなります。
Clean ArchitectureとHexagonal Architectureの考え方
汎用ソフトウェアで育ったアーキテクチャ論は、組み込みとは前提が違うように見えます。それでも依存の向きをそろえるという主張は、マイコン開発にも応用できます。
同心円で示された4層とThe Dependency Rule
Clean Architectureと呼ばれるアーキテクチャパターンは、業務ルールを中心に置いた同心円として描かれます。内側から順に、業務ルール、ユースケース、インタフェースの変換層、フレームワークとドライバという4層です。重要なのは層の数ではなく、依存方向のルール(The Dependency Rule)と呼ばれる制約です。ソースコード(コンパイル時)の依存は内側(高レベル方針)へ向ける、という一文です。組み込みの場合、業務ロジックがレジスタ定義ヘッダをインクルードした時点で、このルールは破られます。
Ports and Adaptersとテスト容易性
Hexagonal Architecture(Ports and Adapters)と呼ばれるアーキテクチャパターンがあります。アプリケーションの内部を外部のUIやデータベースなどから分離する発想です。アプリケーションの周囲にポートを並べ、アダプタを介して外部とつなぎます。効果を発揮するのは、たとえば実機がそろわない開発初期です。ポートの向こう側をモック実装に差し替えれば、業務ロジックの検証はパソコン上で進み、テスト容易性が高まります。
AUTOSAR Classic Platformに学ぶ層構造
理屈だけでは層の切り方は決まりません。車載ソフトウェアで標準化された層構造をなぞると、判断の基準が見えてきます。車載向けの標準ですが、その層構造は他の組み込みシステムを設計する際の参考にもできます。
ApplicationからRTEを経てBSWへ至る3階層
AUTOSARは、車載ソフトウェアの業界標準仕様です。AUTOSAR公式のClassic Platform概要でも、Application、RTE(Runtime Environment:実行環境)、BSW(Basic Software:基本ソフトウェア)の3層分離をハードウェア非依存化の基本コンセプトとしています。さらにBSWは、いくつかの副層に分かれる構造です。Services Layer、ECU Abstraction Layer(ECU:Electronic Control Unit、電子制御ユニット)、Microcontroller Abstraction Layerなどです。Microcontroller Abstraction Layerがマイコン内部のペリフェラルへのアクセスを抽象化し、ECU Abstraction LayerがECU上のデバイス位置の違いを吸収します。
車載以外の産業機器でも応用できる考え方
この標準をそのまま導入できる現場は限られます。それでも構造の考え方だけは一般化できます。たとえば、マイコン抽象に相当する部分をベンダー提供のHALに置き換え、その上の層を自社のサービスAPIとして持てば、車載以外の産業機器でも、同様の考え方を適用できます。AUTOSARの仕様でも、上位ソフトウェア層をECUのハードウェア構成から独立させることを層構造の目的として示しています。
段階的にアーキテクチャを整える手順
既存のコードへいきなり理論を当てはめても、多くの場合、うまくいきません。稼働している製品を止めずに構造を変えるには、段階的な手順が必要です。まず現状の依存を見えるようにします。
まず依存関係グラフを描いて可視化する
最初に必要なのは設計の議論ではなく、現状の棚卸しです。参照関係を分析するツールを使えば、モジュールとファイルのインクルード関係を依存関係グラフとして可視化できます。アーキテクチャ依存の分析手法では、モジュール視点の依存モデルへ実行時や配置の視点を重ねて可視化します。単一の視点では依存を取りこぼす点も、同じ分析手法の資料で指摘されています。
循環依存を解消し依存方向を整理する
グラフを描くと、循環依存など、依存方向の問題が見つかることがあります。たとえば、AがBを呼び、BがまたAを呼ぶ構造です。この循環依存を解消するには、抽象インタフェースを導入して依存方向を整理したり、モジュールの責務や分割方法を見直したりします。冒頭の、1つの機能追加で5つのファイルを修正する状況は、依存関係や責務分担を見直すきっかけです。層を関心事で切り、矢印を抽象へ向けるという2点を意識しながら、影響の大きい部分から順に整えていきます。


