組み込みシステムにおけるC++とは、マイコンや専用機器などの限られた環境で動作するソフトウェアを開発するために使われるC++のことです。
組み込みシステムでは、一般的なPCアプリケーションやWebアプリケーションとは異なり、メモリ容量、CPU性能、消費電力、リアルタイム性、ハードウェア制御などを強く意識する必要があります。
そのため、C++の機能をすべて自由に使うというよりも、対象となる機器や開発方針に合わせて、使う機能と制限する機能を慎重に選ぶことが重要です。
C++は、C言語に近い低レイヤ制御を行える一方で、クラス、名前空間、型安全な列挙型、RAII、テンプレート、コンパイル時計算などの機能を利用できます。
これにより、組み込みソフトウェアの安全性、保守性、再利用性、テスト容易性を高めることができます。
ただし、組み込みC++では「便利だから使う」という発想だけでは不十分です。
実行時コスト、コードサイズ、メモリ使用量、初期化順序、割り込み処理、リアルタイム性への影響を常に確認する必要があります。
組み込みシステムの特徴
メモリやCPUに制約がある
組み込みシステムでは、使用できるRAMやFlashの容量が限られていることが多くあります。
小規模なマイコンでは、RAMが数KBから数十KB程度しかない場合もあります。
そのため、一般的なアプリケーション開発のように、必要になったら動的にメモリを確保するという設計が難しい場合があります。
バッファサイズ、スタック使用量、グローバル変数の量、標準ライブラリの利用範囲などを慎重に管理する必要があります。
CPU性能も限られていることが多く、処理時間の長いアルゴリズムや重い抽象化が問題になる場合があります。
特に、センサー制御、モーター制御、通信処理などでは、一定時間内に処理を完了させることが求められます。
リアルタイム性が求められる
組み込みシステムでは、「処理が最終的に終わる」だけでは不十分な場合があります。
決められた時間内に必ず処理を完了させる必要があるケースが多くあります。
たとえば、モーター制御では数百マイクロ秒から数ミリ秒単位で制御処理を繰り返すことがあります。
通信処理では、受信したデータを一定時間内に処理しなければならない場合があります。
医療機器や自動車制御では、処理遅延が安全性に直結することもあります。
そのため、組み込みC++では、動的メモリ確保、例外処理、ブロッキング処理、重いログ出力、予測しにくい処理時間を持つ機能を避けることがあります。
ハードウェアを直接制御する
組み込みソフトウェアでは、GPIO、UART、SPI、I2C、ADC、PWM、タイマー、DMAなどのハードウェアを直接制御することが多くあります。
このような制御では、メモリマップドI/Oやハードウェアレジスタを操作します。
C++でも、このような低レイヤの処理は可能です。
ただし、ハードウェアレジスタを扱う場合は、対象マイコンのリファレンスマニュアル、レジスタ幅、アクセス制約、割り込みとの競合などを正しく理解する必要があります。
また、メーカーが提供するCMSIS、HAL、ドライバライブラリなどを利用することも多くあります。
実務では、レジスタをすべて自作で定義するよりも、メーカー提供のヘッダやライブラリを利用し、その上にC++の薄い抽象化層を作る設計がよく使われます。
組み込みでC++を使うメリット
抽象化によってコードを整理しやすい
C++では、クラスや名前空間を使って、関連するデータと処理をまとめることができます。
これにより、GPIO制御、通信ドライバ、センサー制御、モーター制御などを部品化しやすくなります。
C言語でも構造体と関数を使って整理できますが、C++ではカプセル化やコンストラクタ、メンバ関数、アクセス制御などを使えるため、より意図を明確にした設計が可能です。
たとえば、LED、UART、センサー、モーターなどをそれぞれオブジェクトとして扱えば、利用側のコードは読みやすくなります。
低レイヤのレジスタ操作を隠し、上位層では「LEDを点灯する」「センサー値を読む」「モーターを停止する」といった意味のある操作として扱えるようになります。
型安全性を高められる
C++では、型安全な設計をしやすいことも大きなメリットです。
特に、設定値、状態、エラー種別、通信コマンドなどを扱う場合、単なる整数やマクロで表現すると、誤った値を渡してしまう可能性があります。
C++では型安全な列挙型を使うことで、意図しない値の混入を防ぎやすくなります。
また、クラスや型を分けることで、物理量や単位の取り違えを減らすこともできます。
たとえば、電圧、電流、温度、時間、ピン番号、通信速度などを適切に型で表現すれば、コードレビューやコンパイル時にミスを発見しやすくなります。
組み込みでは、小さなミスがハードウェアの誤動作や安全上の問題につながることがあります。
そのため、C++の型安全性は非常に有効です。
保守性を高められる
組み込みソフトウェアは、製品寿命が長く、長期間メンテナンスされることが多くあります。
家電、産業機器、自動車、医療機器などでは、一度作ったソフトウェアを数年から十年以上使い続けることもあります。
C++を使うことで、機能ごとに責務を分けたり、ハードウェア依存部とアプリケーションロジックを分離したりしやすくなります。
これにより、ハードウェア変更、仕様変更、機能追加、テスト対応がしやすくなります。
特に、ドライバ層、HAL層、サービス層、アプリケーション層を分ける設計では、C++のクラスやインターフェースが役立ちます。
上位層が具体的なマイコンやレジスタの詳細を知らなくても動作するように設計できるため、再利用性が高まります。
テストしやすい設計にできる
組み込み開発では、実機がないとテストできないと思われがちですが、C++をうまく使うと、PC上でロジック部分をテストしやすくなります。
たとえば、センサー、通信、ストレージ、タイマーなどのハードウェア依存部分を抽象化しておけば、テスト時には本物のハードウェアの代わりにテスト用の部品を差し込むことができます。
これにより、実機が完成する前でも制御ロジックや状態遷移を検証できます。
また、異常系や境界値のテストも行いやすくなります。
実機では再現しにくい通信エラー、センサー異常、タイムアウト、電圧低下なども、テスト用の実装を使えば確認しやすくなります。
組み込みC++でよく使われる機能
クラス
クラスは、組み込みC++でも非常によく使われます。
ハードウェアの制御対象やソフトウェア上の概念を、ひとまとまりの部品として表現できるからです。
たとえば、LED、ボタン、UART、SPI、I2C、センサー、モーター、タイマーなどは、クラスとして表現しやすい対象です。
クラスを使うことで、内部のレジスタ操作や状態管理を隠し、外部には必要な操作だけを公開できます。
これにより、利用側のコードが読みやすくなり、不適切な操作を防ぎやすくなります。
ただし、組み込みではクラスを使えば必ず良いというわけではありません。
抽象化の階層を深くしすぎると、処理の流れが追いにくくなったり、コードサイズが増えたりする可能性があります。
低レイヤでは薄い抽象化にとどめ、上位層で設計上の整理を行うのが現実的です。
名前空間
名前空間は、関数名やクラス名の衝突を避けるために有効です。
組み込みソフトウェアでは、ドライバ、ミドルウェア、アプリケーション、テストコード、メーカー提供ライブラリなど、多くのコードが混在します。
そのため、名前の衝突を避ける仕組みは重要です。
C言語では、関数名に長いプレフィックスを付けることがよくあります。
一方、C++では名前空間を使うことで、機能のまとまりを明確にしながら名前を整理できます。
たとえば、ドライバ関連、アプリケーション関連、通信関連、診断関連などを名前空間で分けると、大規模なコードベースでも見通しがよくなります。
型安全な列挙型
組み込みでは、状態、エラー種別、通信コマンド、制御モードなどを表現する場面が多くあります。
このような値を単なる整数で扱うと、関係のない値を渡してしまったり、別の種類の値と混同したりする危険があります。
型安全な列挙型を使えば、取り得る値を明確にし、誤用を防ぎやすくなります。
たとえば、モーターの状態、通信の結果、センサーの異常状態、システムの動作モードなどは、型安全な列挙型で表現すると読みやすくなります。
また、状態遷移を設計する際にも、取り得る状態を明示できるため、仕様の整理やレビューがしやすくなります。
RAII
RAIIは、C++の重要な考え方の一つです。
オブジェクトの生成時にリソースを取得し、破棄時にリソースを解放する設計です。
組み込みでは、RAIIを次のような用途に使えます。
割り込み禁止区間の管理、ミューテックスのロック管理、一時的なペリフェラル有効化、一時バッファの所有権管理、通信トランザクションの開始と終了、DMA転送の管理などです。
RAIIの利点は、スコープを抜けると自動的に後処理が行われることです。
途中でエラーが発生したり、早期リターンしたりしても、ロック解除や状態復元を忘れにくくなります。
ただし、割り込み禁止ガードのような仕組みを作る場合は、単純に「開始時に割り込み禁止、終了時に割り込み有効」とするだけでは不十分です。
もともと割り込みが無効だった場合に、終了時に勝手に有効化してしまう危険があります。
実務では、元の状態を保存し、最後にその状態へ復元する設計が必要です。
コンパイル時計算
C++では、コンパイル時に計算できる仕組みを利用できます。
組み込みでは、実行時の処理を減らすことが重要なため、コンパイル時計算は非常に有効です。
たとえば、ビットマスク、バッファサイズ、タイマー設定値、通信速度に関する定数、ピン設定などをコンパイル時に決定できれば、実行時の負荷を減らせます。
また、コンパイル時に値が確定することで、コンパイラが最適化しやすくなります。
適切に使えば、読みやすいコードと軽い実行コードを両立できます。
ただし、コンパイル時にすべてを決めようとしすぎると、テンプレートや型の設計が複雑になり、可読性が落ちることもあります。
処理速度やコードサイズに効果がある箇所を中心に使うのが現実的です。
テンプレート
テンプレートは、組み込みC++でも強力な機能です。
特に、ピン番号、レジスタアドレス、バッファサイズ、ポリシー、通信設定などをコンパイル時に扱う場合に有効です。
テンプレートを使うことで、実行時の分岐やデータ保持を減らし、コンパイラによる最適化を期待できます。
また、型を使って設定を表現できるため、誤った組み合わせをコンパイル時に検出できる場合もあります。
一方で、テンプレートを多用すると、コードサイズが増えることがあります。
異なる型や値ごとにコードが生成される場合があるためです。
また、エラーメッセージが複雑になり、学習コストが高くなることもあります。
組み込みでは、テンプレートは効果が大きい箇所に絞って使うのがよいです。
低レイヤの効率化、型安全な設定、固定サイズコンテナ、ポリシーベース設計などには向いていますが、過度な汎用化は避けるべきです。
組み込みC++で注意が必要な機能
例外処理
C++の例外処理は便利なエラー処理機構ですが、組み込みでは無効化されることが多いです。
理由としては、コードサイズが増える可能性、実行時間の予測が難しくなる可能性、ランタイムサポートが必要になること、リアルタイム性との相性が悪い場合があることなどが挙げられます。
特に、小規模マイコン、ベアメタル環境、厳しいリアルタイム制御、安全規格が関係する開発では、例外を禁止または制限することがよくあります。
ただし、すべての組み込みで例外が禁止されるわけではありません。
組み込みLinux、高性能SoC、GUIを持つ機器、上位アプリケーション層などでは、プロジェクト方針によって例外を使う場合もあります。
重要なのは、例外を使うかどうかをプロジェクトで明確に決めることです。
使わない場合は、戻り値、エラーコード、結果型、状態管理などでエラーを表現します。
RTTI
RTTIは、実行時に型情報を扱う仕組みです。
C++では、実行時型判定や安全なダウンキャストなどに使われます。
しかし、組み込みではRTTIも無効化されることが多くあります。
理由は、コードサイズや実行時コストが増える可能性があること、設計が複雑になりやすいこと、低リソース環境では不要な場合が多いことです。
RTTIを使わずに設計する場合は、仮想関数、明示的な状態や種別、テンプレート、静的ポリモーフィズム、設計上の責務分離などで対応します。
ただし、こちらも例外と同様、すべての組み込みで絶対に禁止されるわけではありません。
リソースに余裕のある環境や上位アプリケーションでは使われる場合もあります。
動的メモリ確保
組み込みC++では、動的メモリ確保に注意が必要です。
実行中にメモリを確保・解放すると、メモリ断片化、確保失敗、処理時間のばらつき、解放漏れ、二重解放、ダングリングポインタなどの問題が発生する可能性があります。
特に、長時間稼働する機器やリアルタイム性が重要な制御では、実行中の動的メモリ確保を避けることがあります。
一方で、動的メモリ確保が常に禁止というわけではありません。
起動時にまとめて確保し、その後は再確保しない設計、メモリプールを使う設計、最大サイズを厳密に管理する設計であれば、許容される場合もあります。
実務では、次のような方針がよく使われます。
静的確保を基本にする、固定長バッファを使う、起動時に必要なメモリを確保する、実行中の確保を禁止する、メモリプールを使う、リアルタイム処理内では動的確保をしない、といった方針です。
標準ライブラリ
C++の標準ライブラリには便利な機能が多くありますが、組み込みでは利用範囲を慎重に決める必要があります。
固定長配列や軽量な型補助機能は比較的使いやすい一方で、動的メモリを内部で使う可能性があるコンテナや文字列処理、重い入出力機能には注意が必要です。
たとえば、固定長配列を扱う機能は組み込みと相性がよいです。
一方、動的にサイズが変わる配列、文字列、連想コンテナ、関数ラッパーなどは、内部でメモリ確保が発生する場合があります。
ただし、標準ライブラリを使うこと自体が悪いわけではありません。
リソースに余裕がある環境や、実行時制約が緩い上位層では、標準ライブラリを使うことで開発効率や安全性が高まります。
重要なのは、どの機能が動的メモリを使うのか、コードサイズにどの程度影響するのか、例外無効時にどう動くのか、対象の標準ライブラリ実装がどうなっているのかを確認することです。
仮想関数
仮想関数は、実行時に処理を切り替えるための便利な仕組みです。
組み込みでも、インターフェース設計やテスト容易性の向上に役立ちます。
たとえば、センサー、通信、ストレージ、タイマーなどを抽象化することで、実機用の実装とテスト用の実装を差し替えられます。
これにより、アプリケーションロジックをハードウェアから切り離してテストしやすくなります。
ただし、仮想関数には間接呼び出しや仮想関数テーブルのコストがあります。
また、インライン化されにくい場合があり、リアルタイム性が厳しい箇所では不向きなこともあります。
したがって、仮想関数は「禁止」ではなく、「使う場所を選ぶ」ものです。
低頻度の処理、上位層、テスト用の抽象化には向いています。
一方、割り込み処理や数マイクロ秒単位の制御ループでは避ける判断が必要になる場合があります。
組み込みC++で重要な考え方
使う機能を選ぶ
組み込みC++では、C++の機能をすべて使う必要はありません。
むしろ、対象システムに合わせて使う機能を選ぶことが重要です。
小規模マイコンでは、例外、RTTI、動的メモリ、重い標準ライブラリを避けることが多いです。
一方、組み込みLinuxや高性能SoCでは、より一般的なC++に近い書き方が許容されることもあります。
同じプロジェクト内でも、レイヤによって使える機能を変える設計が現実的です。
割り込み処理やドライバ層では最小限の機能にとどめ、アプリケーション層やテストコードでは抽象化や標準ライブラリを活用する、といった使い分けが考えられます。
実行時コストを確認する
C++には、ゼロコスト抽象化という考え方があります。
これは、適切に使えば抽象的で読みやすいコードを書いても、実行時コストをほとんど増やさずに済むという考え方です。
ただし、常にコストがゼロになるわけではありません。
コンパイラ、最適化オプション、CPU、標準ライブラリ、書き方によって生成されるコードは変わります。
そのため、組み込みC++では、最終的に生成されたアセンブリ、mapファイル、バイナリサイズ、実行時間を確認することが重要です。
特に、制御周期が厳しい処理やメモリ制約が厳しいシステムでは、机上の判断だけでなく測定が必要です。
メモリ配置を意識する
組み込みでは、メモリ配置の理解が重要です。
プログラムコードや定数はFlashやROMに置かれ、実行時に変更されるデータはRAMに置かれます。
ローカル変数はスタックを使い、動的メモリ確保を行う場合はヒープを使います。
また、ハードウェアレジスタは通常のRAMとは異なるアドレス空間に配置されています。
C++では、オブジェクトの生成方法によって配置場所が変わります。
グローバルオブジェクト、静的オブジェクト、ローカルオブジェクト、動的確保されたオブジェクトでは、メモリ上の扱いが異なります。
特に、スタックサイズが小さい環境では、大きなローカル配列や大きなオブジェクトを関数内に置くとスタック不足を起こす可能性があります。
RTOSを使う場合は、タスクごとのスタックサイズも見積もる必要があります。
初期化順序に注意する
C++では、グローバルオブジェクトのコンストラクタがmain関数より前に実行されます。
組み込みでは、これが問題になることがあります。
main関数に入る前の段階では、クロック設定、周辺機器初期化、RTOS初期化、メモリ初期化などが完全に終わっていない場合があります。
そのため、グローバルオブジェクトのコンストラクタ内でハードウェアアクセスを行うと、意図しない動作になる可能性があります。
また、複数のグローバルオブジェクトが別々のファイルに定義されている場合、初期化順序が問題になることがあります。
あるオブジェクトが別のオブジェクトに依存している場合、依存先がまだ初期化されていない可能性があります。
実務では、コンストラクタでは軽量で失敗しない初期化にとどめ、ハードウェア初期化は明示的な初期化処理として実行する設計がよく使われます。
volatileを正しく理解する
ハードウェアレジスタや割り込みで変更される変数を扱う場合、volatileが必要になることがあります。
volatileは、コンパイラに対して、その値がプログラムの外部要因で変わる可能性があることを伝えるための指定です。
これにより、コンパイラが読み書きを勝手に省略したり、値をレジスタに保持し続けたりする最適化を抑制できます。
ただし、volatileは排他制御ではありません。
atomic性、スレッド安全性、割り込みとの競合回避、複数コア間の同期を保証するものではありません。
割り込み処理とメインループで同じ変数を共有する場合や、RTOSの複数タスクでデータを共有する場合は、必要に応じてクリティカルセクション、ミューテックス、atomic操作、メモリバリアなどを検討する必要があります。
割り込み処理は短く保つ
組み込みでは、割り込み処理が重要です。
割り込み処理は、外部イベントやタイマー、通信受信などに即座に反応するために使われます。
ただし、割り込み処理の中で重い処理を行うと、他の割り込みやメイン処理に影響を与える可能性があります。
そのため、割り込み処理では必要最小限の処理だけを行い、重い処理はメインループやRTOSタスクに渡す設計が一般的です。
割り込み処理の中では、動的メモリ確保、ブロッキング処理、重いログ出力、時間のかかる計算、長いループなどを避けるべきです。
また、C++で割り込みハンドラを書く場合、スタートアップコードやベクタテーブルとの関係で、Cリンケージが必要になることがあります。
これは、C++の名前修飾によってシンボル名が変わることを避けるためです。
ただし、具体的な書き方は開発環境やスタートアップコードの構成によって異なります。
組み込みC++の設計パターン
HAL層を設ける
HALは、Hardware Abstraction Layerの略です。
ハードウェア依存部分を抽象化する層を意味します。
HAL層を設けることで、上位のアプリケーションコードは、具体的なレジスタやマイコンの違いを意識せずに処理を書けます。
たとえば、GPIO、UART、SPI、I2C、ADCなどの制御をHAL層で包んでおけば、上位層では「データを送信する」「ピンをHighにする」「センサー値を読む」といった操作として扱えます。
これにより、マイコン変更やボード変更があった場合でも、影響をHAL層に閉じ込めやすくなります。
ドライバ層とアプリケーション層を分離する
組み込みソフトウェアでは、ハードウェアに近い処理と、製品仕様に関する処理を分けることが重要です。
ドライバ層は、通信、GPIO、タイマー、センサー、モーターなどの具体的な制御を担当します。
アプリケーション層は、製品としての動作や状態管理、エラー処理、ユーザー操作への応答などを担当します。
この分離ができていないと、ハードウェア変更がアプリケーション全体に影響したり、実機がないとロジックをテストできなかったりします。
C++では、クラスやインターフェースを使って層を分けやすいため、長期的な保守性を高めやすくなります。
状態機械を使う
組み込みシステムでは、状態管理が非常に重要です。
たとえば、機器の状態として、停止中、起動中、動作中、エラー中、復帰処理中、待機中などがあります。
通信処理でも、未接続、接続中、認証中、通信中、切断中などの状態があります。
状態が曖昧なままだと、不正な操作、予期しない遷移、エラー復帰漏れなどが発生しやすくなります。
C++では、状態を型安全な列挙型で表現し、状態ごとの処理をクラスや関数に分けて整理できます。
状態遷移表を作成し、それに基づいて実装すると、仕様とコードの対応関係が明確になります。
実務では、単に状態を分けるだけでなく、どのイベントで状態が変わるのか、不正遷移をどう扱うのか、タイムアウト時にどうするのか、エラーから復帰できるのか、状態遷移時にどの処理を行うのかを設計することが重要です。
ポリシーベース設計を使う
ポリシーベース設計は、振る舞いをテンプレートなどで差し替える設計です。
組み込みでは、ピンの極性、通信設定、バッファサイズ、タイムアウト方針、エラー処理方針などをコンパイル時に切り替えたい場面があります。
このような場合、ポリシーベース設計を使うと、実行時分岐を減らしながら柔軟性を持たせることができます。
ただし、ポリシーベース設計は強力な反面、設計が複雑になりやすいです。
テンプレートが多くなりすぎると、可読性やビルド時間、エラーメッセージの分かりやすさに影響します。
そのため、性能や型安全性に明確なメリットがある箇所に絞って使うことが望ましいです。
組み込みC++で避けられやすい書き方
実行中の無計画な動的メモリ確保
実行中に必要に応じてメモリを確保し、不要になったら解放するという設計は、一般的なアプリケーションではよくあります。
しかし、組み込みでは慎重に扱う必要があります。
長時間稼働する機器では、メモリ断片化が徐々に進み、ある時点でメモリ確保に失敗する可能性があります。
また、メモリ確保にかかる時間が一定でない場合、リアルタイム性に影響します。
そのため、実行中の無計画な動的メモリ確保は避けられやすいです。
必要なメモリは起動時に確保する、固定長バッファを使う、メモリプールを使う、最大使用量を明確にする、といった対策が重要です。
例外に依存したエラー処理
例外は便利ですが、組み込みではプロジェクト方針として禁止される場合があります。
特に、リアルタイム性やコードサイズが重要なシステムでは、例外に依存しないエラー処理が選ばれることが多いです。
エラー処理には、戻り値、エラーコード、結果型、状態遷移、診断情報の記録などを使います。
重要なのは、エラーが発生したときに安全な状態へ移行できるように設計することです。
重い入出力機能
C++の入出力機能の中には、組み込みでは重くなりやすいものがあります。
特に、標準ストリーム系の機能はコードサイズが増える可能性があり、小規模マイコンでは避けられることがあります。
組み込みでは、軽量なログ機構、リングバッファ、UART出力、RTOS対応のログ機能などを独自に用意することがよくあります。
ログは開発や保守に重要ですが、リアルタイム処理内で重いログを出すとタイミングに影響します。
そのため、ログレベル、出力先、バッファリング、割り込み内での扱いを設計する必要があります。
深すぎる継承
C++では継承を使えますが、深い継承階層は組み込みでは避けたほうがよい場合があります。
継承が深くなると、処理の流れや責務が追いにくくなり、コードレビューやデバッグが難しくなります。
また、仮想関数やRTTIと組み合わさると、実行時コストやコードサイズにも影響する場合があります。
組み込みでは、継承よりも合成を使う、インターフェースは必要最小限にする、低レイヤでは薄い抽象化にとどめる、といった設計が現実的です。
C++とCの違い
Cは単純で制御しやすい
C言語は、組み込み開発で長く使われてきた言語です。
シンプルで、生成されるコードを想像しやすく、低レイヤ制御に向いています。
多くのメーカー提供ライブラリ、スタートアップコード、RTOS、ドライバはCで書かれていることが多く、Cの知識は組み込み開発において非常に重要です。
Cでも、構造体、関数ポインタ、モジュール分割、命名規則、静的解析、コーディング規約を使えば、大規模な開発は可能です。
C++は設計を整理しやすい
C++は、Cの低レイヤ制御能力を保ちながら、より高度な抽象化や型安全性を利用できます。
クラスによるカプセル化、名前空間による整理、RAIIによるリソース管理、テンプレートによるコンパイル時最適化、型安全な列挙型による誤用防止などは、C++ならではの強みです。
特に、ソフトウェア規模が大きくなり、複数人で開発し、長期間保守する場合、C++の設計支援機能は大きなメリットになります。
ただし、C++は機能が多いため、使い方を誤ると複雑になりすぎることがあります。
組み込みでは、C++の機能を制御して使う姿勢が重要です。
組み込みC++の実務的な使い分け
小規模マイコンの場合
小規模マイコンでは、RAMやFlashが少なく、OSを使わないベアメタル環境で動作することも多くあります。
このような環境では、C++の利用はかなり制限される傾向があります。
例外やRTTIは無効化し、動的メモリ確保を避け、標準ライブラリも限定的に使うことが多いです。
一方で、クラス、名前空間、型安全な列挙型、コンパイル時計算、軽量なテンプレートなどは有効です。
低レイヤの制御を壊さずに、コードの見通しを良くできます。
RTOSを使う場合
RTOSを使う環境では、タスク、キュー、ミューテックス、セマフォ、タイマーなどを扱います。
C++では、これらを薄いクラスで包み、RAIIを使ってロック解除漏れを防ぐ設計ができます。
また、タスクごとに責務を分け、通信や状態管理を整理しやすくなります。
ただし、RTOS環境では、ISRから呼べるAPIと呼べないAPIの区別、タスク優先度、優先度逆転、スタックサイズ、デッドロック、タイムアウト設計などに注意が必要です。
組み込みLinuxの場合
組み込みLinuxでは、C++の利用範囲がかなり広がります。
メモリやCPUに比較的余裕があり、OSの機能も利用できるため、標準ライブラリ、動的メモリ、スレッド、ファイルシステム、ネットワーク機能などを使うことが一般的です。
ただし、組み込みLinuxであっても、リアルタイム性が求められる部分や、リソース制約がある部分では注意が必要です。
上位アプリケーションでは一般的なC++を使い、デバイス制御やリアルタイム処理では制限されたC++を使う、といった分担が現実的です。
組み込みC++で確認すべきポイント
コードサイズ
組み込みでは、FlashやROMの容量が限られているため、コードサイズの確認が重要です。
テンプレート、標準ライブラリ、仮想関数、例外、RTTI、ログ機能などは、コードサイズに影響する場合があります。
ビルド後のmapファイルやサイズレポートを確認し、不要なコードが含まれていないかを見る必要があります。
RAM使用量
RAMは、グローバル変数、静的変数、スタック、ヒープ、RTOSタスクスタック、バッファなどで消費されます。
特に、通信バッファ、画像バッファ、ログバッファ、センサーデータ配列などはサイズが大きくなりやすいため、上限を明確にする必要があります。
スタック使用量
C++では、ローカルオブジェクトや一時オブジェクトが作られることがあります。
大きなオブジェクトをスタック上に置くと、スタック不足を引き起こす可能性があります。
RTOSではタスクごとにスタックが分かれるため、各タスクの最大使用量を見積もる必要があります。
再帰処理はスタック使用量を予測しにくいため、禁止されることも多くあります。
実行時間
リアルタイム性が必要な処理では、最悪実行時間を把握する必要があります。
平均的に速いだけでは不十分で、最も遅い場合に期限内に処理が終わるかが重要です。
動的メモリ確保、ロック待ち、ブロッキングI/O、例外、キャッシュミス、割り込み遅延などが影響する場合があります。
生成コード
組み込みC++では、ソースコードだけでなく、生成されたコードも確認することがあります。
抽象化したコードが期待通りに最適化されているか、不要な関数が残っていないか、仮想関数呼び出しが発生していないか、標準ライブラリ由来の重い処理がリンクされていないかを確認します。
特に、性能やサイズが重要な箇所では、実測と生成コード確認が欠かせません。
組み込みC++のコーディング方針
プロジェクトごとにルールを決める
組み込みC++では、プロジェクトごとに使ってよい機能と禁止する機能を明確にすることが重要です。
たとえば、例外は使わない、RTTIは使わない、実行中の動的メモリ確保は禁止、割り込み内では最小限の処理のみ、標準ライブラリは固定長コンテナ中心、などのルールを決めます。
このようなルールがないと、開発者ごとに書き方がばらつき、予期しないコードサイズ増加やリアルタイム性の問題が発生しやすくなります。
レイヤごとに許可範囲を変える
すべてのコードに同じ制限をかけるのではなく、レイヤごとにルールを変えると現実的です。
割り込み処理では、動的メモリ確保、重いログ、ブロッキング処理を禁止します。
ドライバ層では、薄い抽象化、固定長バッファ、明確なエラー処理を重視します。
アプリケーション層では、状態機械、インターフェース、RAIIなどを活用できます。
テストコードでは、標準ライブラリやモックを比較的自由に使うこともあります。
このように、制約が厳しい部分と保守性を重視する部分で使い分けることが重要です。
静的解析やコードレビューを活用する
組み込みでは、バグが製品の安全性や信頼性に直結することがあります。
そのため、静的解析、コードレビュー、単体テスト、結合テスト、実機テストを組み合わせることが重要です。
C++では、意図しない暗黙変換、未初期化変数、境界外アクセス、未使用コード、仮想デストラクタの問題、キャストの誤用などを検出するために、コンパイラ警告や静的解析ツールを活用します。
また、安全性が重要な分野では、MISRA C++やAUTOSAR C++などのコーディング規約が採用される場合もあります。
組み込みC++でよくある誤解
C++は重いとは限らない
C++は重いと言われることがありますが、これは使い方次第です。
クラス、名前空間、型安全な列挙型、コンパイル時計算、軽量なテンプレートなどは、適切に使えば実行時コストをほとんど増やさずに利用できます。
一方で、例外、RTTI、動的メモリ、重い標準ライブラリ、過度な抽象化を無計画に使うと、コードサイズや実行時間に影響します。
つまり、C++そのものが重いのではなく、どの機能をどのように使うかが重要です。
C++なら安全になるわけではない
C++には型安全性やRAIIなど、安全性を高める機能があります。
しかし、C++を使えば自動的に安全なコードになるわけではありません。
ポインタの誤用、ライフタイム管理のミス、競合状態、初期化順序問題、スタック不足、メモリ破壊、未定義動作などはC++でも起こります。
C++の機能を正しく理解し、設計ルール、レビュー、テスト、静的解析と組み合わせることで、初めて安全性を高められます。
標準ライブラリは必ず禁止ではない
組み込みでは標準ライブラリを使ってはいけないと思われることがありますが、これも環境次第です。
小規模マイコンでは制限されることが多い一方、組み込みLinuxや高性能なRTOS環境では、標準ライブラリを活用することもあります。
重要なのは、その機能が内部で何をしているかを理解することです。
動的メモリを使うのか、例外に依存するのか、コードサイズが大きくなるのか、リアルタイム処理内で使ってよいのかを確認する必要があります。
組み込みC++のまとめ
C++は低レイヤ制御と設計力を両立できる
組み込みシステムにおけるC++は、ハードウェアに近い低レイヤ制御を行いながら、ソフトウェア設計の品質を高められる言語です。
C言語のようにレジスタやメモリを意識した処理ができる一方で、クラス、名前空間、型安全な列挙型、RAII、テンプレート、コンパイル時計算などを利用できます。
そのため、規模が大きく、長期間保守される組み込みソフトウェアでは、C++のメリットが大きくなります。
重要なのは機能の選び方
組み込みC++では、C++の機能をすべて使うことが目的ではありません。
対象ハードウェア、メモリ容量、CPU性能、OSの有無、リアルタイム性、安全要件、開発チームのスキル、保守期間に合わせて、使う機能と避ける機能を選ぶことが重要です。
小規模マイコンでは、軽量なC++機能を中心に使うのが現実的です。
RTOS環境では、RAIIやクラス設計を活用しつつ、タスクや割り込みとの関係に注意します。
組み込みLinuxでは、より広いC++機能を使える一方で、リアルタイム部分やデバイス制御部分では慎重な設計が必要です。
実務では測定と確認が欠かせない
組み込みC++では、コードが正しく見えるだけでは不十分です。
コードサイズ、RAM使用量、スタック使用量、実行時間、割り込み遅延、生成コード、初期化順序、メモリ配置を確認する必要があります。
特に、抽象化によるコストはソースコードだけでは判断できません。
最終的には、mapファイル、生成アセンブリ、実機測定、静的解析、テストによって確認することが重要です。
結論
組み込みシステムにおけるC++は、Cのような低レイヤ制御能力と、C++の設計支援機能を組み合わせられる強力な選択肢です。
ただし、C++を無制限に使うのではなく、組み込み特有の制約を理解したうえで、適切な機能を選んで使う必要があります。
組み込みC++の本質は、次のようにまとめられます。
ハードウェア制御、リアルタイム性、メモリ制約を守りながら、C++の型安全性、抽象化、リソース管理、テスト容易性を活用すること。
この考え方を持つことで、読みやすく、保守しやすく、信頼性の高い組み込みソフトウェアを開発しやすくなります。
以上、組み込みシステムにおけるC++についてでした。
最後までお読みいただき、ありがとうございました。
