信頼性の高い組み込みシステムを設計するには、コードを書くこと以上に、システムが時間とともにどのように振る舞うかを明確な心象図で捉えることが求められる。ステートマシン図は、その振る舞いのための設計図となる。抽象的な要件を、開発者が正確に実装できる視覚的な論理フローに変換する。このガイドでは、これらの図を描く際の基本を順を追って説明し、1行のコードも書く前に論理が正しく構築されていることを確認する。状態の構造、遷移のメカニズム、複雑さを管理しながらも明確さを保つための戦略についても探求する。 🧩
線形スクリプトからイベント駆動型アーキテクチャに移行する際、ステートマシン図は主なドキュメントツールとなる。競合状態を防ぎ、エラー状態を明確にし、システムが予期しない入力をスムーズに処理できることを保証する。モーターの制御、ネットワークプロトコルの管理、ユーザーインターフェースのワークフロー設計など、あらゆる場面で、この手法は安定性を確保するための構造を提供する。

📊 コアコンポーネントの理解
すべてのステートマシンは、いくつかの基本的な構成要素で構成される。これらの要素を理解することは、正確なモデル化に不可欠である。フローチャートが制御の流れに注目するのに対し、ステート図は特定の瞬間にシステムがどのような状態にあるかに注目する。システムは特定の状態に存在し、何らかの出来事の発生を待ってから、新たな状態へ移行する。
以下の表は、標準的な統合モデル化言語(UML)表記における必須の記号とその意味を概説している:
| 要素 | 説明 | 視覚的表現 |
|---|---|---|
| 状態 | システムが特定の条件を満たす、ある活動を実行する、またはイベントを待つという状態。 | ラベル付きの丸角長方形 |
| 遷移 | イベントによって引き起こされる、1つの状態から別の状態への移動。 | ラベル付きの矢印 |
| イベント | 遷移を引き起こす信号またはアクション。 | 遷移矢印上のテキスト |
| アクション | 状態に入ること、出ること、または状態内での活動。 | 状態ボックス内または遷移上のテキスト |
| 初期状態 | マシンの出発点。 | 黒で塗りつぶされた円 |
| 最終状態 | マシンの終了点。 | 二重線の円 |
これらの定義を明確に保つことで、図を確認する誰もが意図された振る舞いを理解していることを保証できる。状態の定義が曖昧になると、最終的な実装でバグが発生する原因となることが多い。
🔄 状態と遷移の定義
図の構築は、システムが占有しなければならない明確な状態を特定することから始まる。これらは単なるプログラム変数ではなく、ハードウェアまたはソフトウェアの運用モードを表すものである。適切に定義されたステートマシンは、必要なすべてのシナリオをカバーしつつ、必要な状態数を最小限に抑える。
状態を定義する際には、以下の原則を検討してください:
- 包括性:すべての可能な状態を網羅しなければなりません。システムが状態Aにない場合、状態BまたはCに存在しなければなりません。
- 排他性:システムは通常、同時に一つの状態にのみ存在するべきです(直交領域を使用しない限り)。
- 安定性:状態とは、システムがその状態で安定しており、変化を引き起こすトリガーを待っていることを意味します。
遷移はこれらの状態の間をつなぐ橋です。イベントによって引き起こされます。イベントは内部(タイマーの期限切れ)または外部(ボタン押下、センサー読み取り)のいずれかです。
遷移を描く際には、方向が明確であることを確認してください。矢印は元の状態から目標状態へ向かいます。矢印のラベルは移動を引き起こすイベントを説明します。複数のイベントが同じ遷移を引き起こす場合、それらをコンマで区切って記載できますが、個別に分けることで可読性が向上することが多いです。
⚙️ アクションとイベント:論理の生命線
イベントが状態機械を駆動しますが、アクションが変化中に何が起こるかを定義します。組み込みシステムでは、アクションがしばしばハードウェアレジスタやAPI呼び出しに直接対応します。イベントとアクションの違いを明確にすることは非常に重要です。
エントリーアクション、エグジットアクション、ドーアクション
複雑な状態では、異なるタイミングでロジックを実行する必要があります。UMLでは、状態内に3種類のアクションを指定できます:
- エントリーアクション:状態に入るとすぐに実行されます。ハードウェアの初期化、フラグの設定、タイマーのリセットに使用します。
- エグジットアクション:状態を離れる直前に実行されます。リソースのクリーンアップ、データの保存、出力の無効化に使用します。
- ドーアクション:システムがその状態に留まる間、継続的に実行されます。センサーのポーリングや特定のイベントを待たずに状態を監視する場合に多く使用されます。
たとえば、「モーター稼働」状態では、エントリーアクションで電源ドライバを有効化するかもしれません。ドーアクションでは電流センサーを継続的に読み取るかもしれません。エグジットアクションでは、電力の急上昇を防ぐために電力を徐々に低下させるかもしれません。
🏗️ 高度な表記技術
システムが拡大するにつれて、単純な線形状態図は管理が難しくなります。高度な表記法は視覚的なごちゃごちゃ(視覚的スパゲッティ)を避けながら複雑さを整理します。これらの機能により、ロジックをネストさせ、履歴を管理できます。
階層状態
すべての状態が等しいわけではありません。一部の状態は複合状態であり、サブ状態を含んでいます。これを複合状態と呼びます。複合状態内では、特定のサブ動作を定義できます。組み込みロジックにおいて、高レベルのモード(例:「アイドル」)が複数の低レベルの変化(例:「センサー待ち」、「タイマー待ち」、「ユーザー入力待ち」)を持つ場合に、これは非常に重要です。
階層構造を使用することで、遷移の数を減らすことができます。すべてのサブ状態から他のすべてのサブ状態へ線を引くのではなく、親レベルで遷移を定義できます。これにより、図を整理され、管理しやすくなります。
履歴状態
時折、システムが複合状態を離れて後で戻ってくる場合、最初から再開してはいけません。どこで離れたかを記憶していなければなりません。これが履歴状態の機能です。
- ディープ履歴:システムは、以前に存在していた特定のサブ状態を記憶します。
- シャロウ履歴: システムは複合状態を自身で記憶するが、その中にデフォルトのサブ状態に入ることになる。
これは特に電力管理システムにおいて有用である。デバイスが低消費電力モードに入り、起動した場合、タスクキュー内でちょうどその場所から再開すべきであり、全体のシーケンスを再起動すべきではない。
📝 ロジックフローの設計
スクラッチから図を描くことは恐ろしいものかもしれない。構造的なアプローチを取ることで、ロジックの穴を逃すことがない。空白のページから検証済みの設計へと進むためのワークフローに従ってください。
- 要件の収集:すべての入力、出力、期待される動作をリストアップする。何が変化を引き起こすのか?それに応じて何が起こるべきか?
- 状態の特定:動作の明確なモードを定義する。次のように尋ねる:「この特定のことをしているとき、システムはどのような状態にあるのか?」
- イベントの定義:移動を引き起こす可能性のあるすべての信号をリストアップする。エラーシグナルやタイムアウトも含める。
- 遷移のマッピング:矢印を描く。最終状態を除き、すべての状態が出るパスを持っていることを確認する。初期状態を除き、すべての状態が入るパスを持っていることを確認する。
- アクションの割り当て:関連する状態にエントリーアクション、エグジットアクション、および実行アクションを追加する。
- ガードの確認:どの遷移も条件(ガード)を必要とするか確認する。ガードとは、遷移が発火するためには真でなければならないブール式である。
🛠️ ロジックをコードにマッピングする
図が完成したら、コードへの変換は構造的な作業になる。図が仕様書として機能する。実装にはいくつかの一般的なパターンがある。
Switch-Case実装
最も直接的なマッピングは、状態変数とswitch文を使用するものである。各状態はcaseラベルに対応する。case内では、その状態のロジックと遷移のチェックを処理する。
- 状態変数:現在の状態を表す整数または列挙型。
- イベントハンドラ:イベントを受け取り、現在の状態に基づいて状態変数を更新する関数。
- アクション:図で定義されたエントリ/エグジット/実行アクションに対応する関数を、状態マシンのループ内で呼び出す。
状態テーブル実装
より複雑なシステムでは、照合テーブルで遷移を定義できる。各行には現在の状態、イベント、次の状態、実行するアクションが含まれる。これによりロジックと制御フローが分離され、コード構造を変更せずに振る舞いを変更しやすくなる。
| 現在の状態 | イベント | 次の状態 | アクション |
|---|---|---|---|
| アイドル | スタートボタン | 実行中 | モーターの初期化 |
| 実行中 | ストップボタン | アイドル | モーターの無効化 |
| 実行中 | オーバーライド | エラー | 故障ログの記録 |
このアプローチは非常に保守しやすいです。要件が変更された場合、条件付き論理を再書き直すのではなく、テーブルの行を更新するだけで済みます。
⚠️ 一般的な落とし穴とその解決策
経験豊富なデザイナーでさえ問題に直面します。一般的な罠に気づいておくことで、早期に回避できます。
- エラー状態が欠落している:デザイナーはしばしば順調な経路に注目しがちです。センサーが故障した場合、状態機械はどこへ行くべきでしょうか?常に故障を処理するためのERRORまたはSAFE状態を定義してください。
- 到達不可能な状態:初期状態からすべての状態が到達可能であることを確認してください。到達不可能な状態は設計上の欠陥を示しています。
- 状態が多すぎる:15個以上の状態がある場合は、階層構造を見直してください。ネストされた状態をグループ化すべきところをフラット化している可能性があります。
- ガードが欠落している:遷移が条件に依存する場合は、明示的にガードを設定してください。文脈が重要である場合は、イベントだけに頼ってはいけません。
- スパゲッティ状の遷移:線が交差しないようにしてください。図が読みにくくなった場合は、関連する論理をグループ化するために複合状態を使用してください。
🔍 状態遷移のデバッグ
組み込みシステムが予期しない動作をした場合、状態機械の図が最初に確認すべき場所です。デバッグとは、システムが経由した経路を追跡することです。
状態変更を記録するためにログを使用してください。エラーが発生した際には、ログを確認して以下を確認してください:
- どの状態がアクティブでしたか?
- 何が変化を引き起こしましたか?
- 遷移ガードは満たされていましたか?
- アクションは正しく実行されましたか?
実際の実行パスを図と照らし合わせることで、論理がどこで分岐したかがよくわかります。図に示されていないパスをコードがたどっている場合、実装は設計と一致していません。
📈 複雑なシステムへのスケーリング
大規模な組み込みアプリケーションでは、1つの図だけでは不十分な場合があります。システムを複数の相互作用する状態機械に分解する必要があるかもしれません。これを並行または直交状態設計と呼びます。
このパターンでは、システムの異なる部分が独立して動作するが、イベントを通じて同期します。たとえば、通信モジュールはモーター制御機械とは独立した独自の状態機械を持つことができます。必要なときだけ相互作用します。
- 関心の分離:ユーザーインターフェースの論理をハードウェア制御の論理から分離してください。
- イベントブロードキャスト:マシン間の通信にグローバルなイベントバスを使用し、結合を緩く保つようにしてください。
- 共有変数:共有データには注意してください。複数のマシンが同じリソースにアクセスする場合は、スレッドセーフを確保してください。
このアーキテクチャはテスト可能性を向上させます。モーター機械を通信機械から分離してテストできます。
✅ デザインの最終確認
実装に移る前に、図を元の要件と照らし合わせて確認してください。すべてのシナリオをカバーしていますか?論理は決定論的ですか?開発者が質問せずに理解できるでしょうか?
丁寧に作られた状態機械図は、技術文書と同じくらいコミュニケーションツールです。チームがシステムの振る舞いについて合意できるようにします。デバッグ時の認知負荷を軽減します。将来の保守のための参照資料になります。
これらのガイドラインに従うことで、信頼性の高い組み込み論理の堅固な基盤を築けます。白紙から動作するシステムへの移行は、推測のプロセスではなく、構造的な旅になります。明確さ、完全性、正確性に注力すれば、結果としてのコードもその厳密さを反映します。
基本から始めましょう。状態を明確に定義してください。遷移を正確にマッピングしてください。エラーを丁寧に処理してください。練習を重ねることで、状態機械の設計は開発ワークフローの自然な一部になります。これにより、組み込みシステムが現実世界で信頼性高く動作することが保証されます。 🛠️











