組み込みまするβ

組み込みソフトウェア初学者向けブログ

組み込みソフト技術者へ転向する前に知っておきたいこと

非組み込みのソフトウェア技術者が、組み込みソフト技術者へ転向した後に後悔しないよう書いてみました。 自主的なのか上司の指示なのか、理由は様々だと思いますが、 私が思っていた以上に組み込みソフト技術者へ転向する方は多いようですね。

言語

アセンブリ

スタックポインタ設定、ジャンプルーチン、キャッシュ制御、高速化等をアセンブリ言語で作成しなければならない場合があります。 但し、昨今ではmain関数から記述できるように、スタートアップルーチンやHALが提供されていることも多いと思います。

C言語C++

開発を請け負う場合は顧客要望でC言語またはC++が指定される場合があります。 また、主にメモリ量がシビアなため、C言語を採用する場合もあります。 古い環境の場合、コンパイラC言語のみという場合があります。

なお、現在のプロジェクトではコンパイラがC11/C++11であったとしても、次のプロジェクトではC99/C++03になることもあります。 自社製品の後継プロジェクトであれば退化しないかもしれませんが、自社の他のプロジェクトでは退化する可能性もあるでしょう。 また、共通ライブラリやフレームワーク等は当然古い環境でも利用できることが重視されます。

OSレスやリアルタイムカーネルを使う規模の場合、 組み込み開発で使われるC言語C++というのは「制限された」C言語C++である場合がほとんどだと思います。

デバッグビルドとリリースビルド

ハードウェアを制御しない一般的なプログラムは、(最適化レベルの異なる)デバッグビルドとリリースビルドで実行スピードが変わっても気にすることはあまりないと思います。 ですが組み込みソフトウェアはリアルタイム性が求められるハードウェア制御や、他のCPUとの通信等をすることもあるため、 (組み込みシステムとして)デバッグビルドでは動いたのにリリースビルドでは動かないということが起こりえます。 逆にリリースビルドでは動いたのにデバッグビルドでは動かない場合もあります。 ハードウェア制御上の隠れた仕様が原因ということもあります(デバイスへのリセット解除後、一定時間はレジスタアクセスができない等)。

もちろん影響が出ないような設計やコンパイル設定(主に最適化レベル)にすれば、それほど神経質にならなくてもよいのですが、 それには設計やコンパイル設定がハードウェア制御等のリアルタイム性が求められる処理にどのような影響を与えるのかを正しく理解している必要があります。

ビットフィールド

開発環境側でビット定義が用意されている場合を除き、 移植性の問題があるのでビットフィールドは使いません(使ってしまうと移植時に大変な労力を伴う場合があります)。 ルネサスのCPUは周辺回路のIO定義としてビットフィールドで制御可能なインクルードファイルが提供されています。 このインクルードファイルをそのまま利用するのは問題ありませんが、 ルネサスのCPUとは別チップの回路制御には当然ビットフィールドを使いません。

thread

組み込みOSはたくさんありますが、標準のカーネルはもちろん代表的なOSというものを決めることができません。 このため、コンパイラがC11/C++11に対応していても、threadが未実装であることが多いです。 つまり、組み込みLinuxWindows Embedded等のそれなりに規模の大きな組み込みOSでないと、 C11/C++11のthreadの恩恵は受けられないと思っています(私の経験した環境ではという条件付きですが)。

このため、未だにC99/C++03までの仕様で実現する場合が多いと感じています。

C++のiostream、STL

コード量の増加を伴うため、扱えるメモリ量によっては使うことができません。 iostream、STLが使えることを前提として設計すると移植が困難になることも考慮しなければなりません。 小規模組み込みシステムの場合は使えないと考えておいた方が無難でしょう。

C++の例外

例外発生時のコスト見積もりが難しいことや、コード量の増加等、 様々な理由から組み込みソフトウェアとして不向きであるため、C++の例外を使用しない場合がほとんどです。

※例外を投げる可能性がある標準のnewは使えませんので、placement newを使用するか、例外なしを指定します。

組み込み開発のC言語C++

非組み込み技術者が認識している汎用OS上で動作するプログラムを開発するためのC言語C++についてですが、 上記のような理由から組み込みシステム開発で使われるC言語C++とは別物と思っておいた方がよいでしょう。 threadがない、sleepがない、便利なライブラリがない、という環境でプログラムを作成するという状況を考えてみてください。

インクルードガード

言語の分類ではないのですが、コンパイラ依存となるのでインクルードガードに「#pragma once」は使えませんので、 従来から使われているインクルードガードを採用する場合がほとんどだと思います。

移植性

同じ製品シリーズであっても、廃品種になる部品が出たり、システム構成の変更など様々な理由から、 FPGA、CPU、OSの変更がありますので、常に移植性を意識しておかなければなりません。 とはいえ数GHzのマルチコアCPUから8bitのワンチップマイコンへ移植するといった極端なことはあり得ませんので、 常識の範囲内でという条件をつけてもよいとは思いますが。

シングルタスク/マルチタスク(組み込みOSの有無)

組み込みOS

小規模組み込みシステムで採用される「組み込みOS」は、 メモリ保護のないリアルタイムカーネルを意味し、マルチタスク/スレッドの提供が主な機能です(昨今では様々なミドルウェアを無償で利用できる場合もあります)。

シングルタスク

シングルタスクベースで設計する事もまだまだ多いです。 今時はマルチタスクでシングルタスクは古いとか、そういった話ではないのです。 組み込みシステムを構成する上で、処理能力の高い数GHzで動作するハードマクロCPUを採用した方がよい場合もあれば、 数十~数百MHzのCPUを複数採用してマルチプロセッサ構成で実現する方がよい場合もあるのです(機能や電源によるモジュール分離、消費電力、コスト、物理サイズ、排熱方法等)。

開発期間や予算、出荷数量等の兼ね合いもありますが、 組み込みシステムは求められる機能を低コストで実現することが求められます。 もちろん開発コストや期間の関係で極限まで低コストを求められることはほとんどありませんが、余裕のある開発は実際少ないでしょう。

マルチタスク

小規模組み込みシステムで採用されるOS(リアルタイムカーネル)は優先度ベーススケジューリングが主なので、 実行可能状態の中で最も優先度の高いタスクがずっと走り続けます(一般的に組み込みシステムは実時間制約があるため、優先度ベースのスケジューリング方式が採用される)。 優先度ベーススケジューリングをきちんと理解した上で設計しなければなりません。

汎用OSのスケジューリング方式を選択可能な小規模向けの組み込みOSもありますが、 小規模なシステムほどリアルタイム性を求められる傾向なので利用する機会はほぼないでしょう。

設計

個人的な経験だけで述べると、組み込みの業界標準といったものは無く、入社した会社によって設計技術レベルは大きく異なると認識しています。 C++を採用し、ドライバやコンテナ、上位のサブシステム等を部品として流用可能なように高度な設計をしている会社もあれば、 ソフトウェアの部品化ができておらず、流用が非常に困難な状態でつぎはぎ設計を延々と続けている会社もあります。 当然前者の設計品質は圧倒的に高く、仮に不具合があってもモジュールの特定も容易で、凝集度が高いので修正も伝搬しません。 逆に後者はグローバル変数の多用や、モジュールの概念のないコードの結合、修正の伝搬、設計や動作を把握できていないための意味不明なコード等、挙げればきりがありません。 継続的に学習しているエンジニアが中心となっている組織と、過去のソフトからコピペでつぎはぎしている組織との技術的差は開く一方ですが、 このような差異を理解できている技術者が改善を決意して行動を起こせるかどうかで変えていくことはできると思います。 もちろん容易なことではありませんが、それは転向してきたあなたにしか出来ない仕事かもしれませんよ:-)

プログラムの終了

「プログラムの終了=電源断」であるシステムが多いので、 メモリ量がシビアな場合は終了処理を実装しないこともあります。 言い換えれば終了時の処理を考慮する必要がない場合が多いということになります。

不揮発性メモリ(FlashROM、各種メモリカード、小容量ROM等)

不揮発性メモリは装置の動作設定パラメータや、ファームウェアFPGAのコンフィグデータ等の保存に利用されます。 このうち装置の動作設定パラメータの変更は一般的にユーザへ解放されていますが、 設定パラメータの変更中に電源が落ちた場合はデータ破損することがあり、それを考慮した設計が必要となります。

  • 壊れた場合の自動復帰手段(デフォルトに戻る等)を考えておく
  • 壊れた場合にユーザへ通知する(デフォルトに戻るメッセージ表示等)
  • 念のため書いておきますが、正常な通電状態で壊れた場合は不具合です(書き込み、読み出し、破損判定等、あるいはハードウェア、何らかの問題があると思われます)

また、ファームウェアFPGAのコンフィグデータ等のアップデートはユーザへ解放されていない場合もありますが、 電源事情の悪い地域での利用も考慮しておかなければなりません。 書き換え中に電源が落ちてもやり直しが可能でなければならないということです(二重化もしくは書き換え不可のブートイメージを設ける等)。

PCでいうところのストレージデバイスSSD、HDD等)と考えても良いですが、ファイルシステムを介さないで利用することも多いため、 その場合は直接ドライバを介して読み書きを行うことになります(ドライバが無ければ当然開発しなければなりません)。

メモリ(RAM)

メモリの断片化を抑制するため、初期化処理後はmalloc/newを禁止にしている開発組織も多いと思います(ごく稀にメモリ管理機構が提供されない環境もあります)。 また、通電状態でのプログラム終了がない場合はメモリの解放を記述しないこともあるので、 malloc/newの管理領域の無駄を嫌って単純なメモリ管理を自作することもあります。 ※解放がないので非常に単純な処理で実現できます。

それでも動的メモリを使用したい場合は、固定長メモリブロックの配列をプログラムの開始時にmalloc/newで確保し、 固定長メモリブロックを管理するコードを自作することもあります。 固定長メモリブロック単位での確保/解放なので(枯渇の心配はありますが)断片化の心配はありません。 ※uITRON4.0仕様にあるような機能です。

メモリ保護

組み込みOSに組み込みLinuxWindows Embedded等を採用している場合はCPUのMMUを利用したメモリ保護を利用できますが、 小規模なリアルタイムカーネル程度の組み込みOSではメモリ保護はありません。 このため、メモリ保護のない環境ではメモリ破壊によってCPU例外にとんだり、 正常に動いているように見えていても裏では非アクティブなコードやデータを破壊していることもあります。 つまり、このようなメモリを破壊するバグも自身で解決する必要があるということです。

仮想メモリを使わなくとも、メモリ保護を使いたいためだけにMMUを使うこともあります(もちろん自分でMMUを制御しなければなりませんが)。

※例外としてμClinuxはMMUを必要としないのでメモリ保護なし

実時間制約

実時間制約があるシステムが多いと思います。 何かのイベントが発生してから、一定時間以内に応答しなければならない等。

ハード/ソフトリアルタイムなイベントが複数あり、 割り込み最小間隔等が最悪な状況下でも、すべてのイベントに対してリアルタイム制約を守れる設計が求められます。 システムによっては優先順位の低いイベントの応答を諦めるということもあります。 これは言い換えると、論理的な動作は問題なくとも実行スピードが間に合わなければ(組み込みシステムとして)不具合と認識されるということです。 モジュールの単体テストなどが無駄とは言いませんが、実機やタイミングシミュレーションでないと動作テストは難しいです。

装置によっては起動時間が重視されることもあります。 電源投入から数秒で起動し、使える状態にならなければいけない等。 汎用OSに近いLinux等では厳しい場合もあるでしょう。 ソフトウェアだけでなく、大規模なFPGAを採用している場合はコンフィギュレーション時間も設計時から考慮する必要があります(システム設計者)。

デバッグ

JTAGエミュレータ等のデバッグ機器でターゲットと接続し、 汎用OS上のプログラムと同じようにトレースログ、ブレークポイント、ステップ実行でデバッグします。 但し、ハードウェアとの連携やマルチプロセッサ構成のリアルタイムシステムの場合は、 ブレークによる停止やステップ実行を使用するとリアルタイム制約が守れず、 システムとして異常状態となってデバッグにならないこともあります。 ※一概には言えませんが、いつでもデバッガが使えるシステムならば難易度はそれほど高くないと思います

異常発生を捕捉できる場合は、リアルタイム制約を無視してステップ実行等で調査が可能な場合もあります

また、発生頻度の低い不具合の解析では、通電状態のまま、後からデバッガを接続して確認するということができないため、 解析すらままならないといったこともあります(最近では後からデバッガを接続可能なものもあるようです)。 こういった場合を考慮するとUART等を利用したデバッグ用のシェルを作成して組み込んでおくと、 稀な不具合発生時にも不具合原因を探る手段として非常に有効です。

とにかく、デバッガが使えないという状態を想定して対策しておくことが重要です。 個人的な経験という前提条件付きですが、リアルタイムシステムの場合はデバッガが使えるのは開発当初のみで、 ある程度システムとして動作した後のデバッグはログによるリアルタイムデバッグが主となります。 ※新規デバイス等があまりない小規模なシステムなら、デバッガを使うことなく組み上げるだけで終わる事もあります

ハードウェアブレークポイント

ブレークポイントとは異なるハードウェアブレークを使用できるCPUもあります。 例えば動作中は変更禁止のレジスタが変更されていた場合のデバッグで、 レジスタアドレスを指定したライトアクセスでブレークさせることができる等、組み込みソフト開発ならではのデバッグ機能があります。

ハードウェアとソフトウェアでの問題切り分け

ハードウェアの問題ならハードウェアで解決してほしいと思うかもしれませんが、 システム規模が大きくなるとソフトウェアの協力も必要になってきますので、 原因がわかるまでは積極的にデバッグ協力をしなければなりません。

ハードウェアの問題だと思っていたが、実際はソフトウェアの問題だったということや、その逆ももちろんあります。 システム規模が大きく複雑になればなるほど問題の切り分けも難しいですから、 ハードウェアとソフトウェアで分けて考えず、システムを開発しているチームメンバーとして協力しましょう。 嘆かわしいことですが、ベテランになるほど自分の不具合ではない言って協力しない人が多くなるものです。

システムとしてのフェイルセーフ

人命に関わるシステム等では、何かの異常時に安全側に倒れるような設計が求められます。 ※車や家電(火災の危険性)等

ウォッチドッグ

フェイルセーフの仲間に入れましたが、 プログラムの暴走等でウォッチドッグタイマからシステムリセットが発行され、 システムが再立ち上げとなるのは責任を果たせない状況のままになってしまうよりは、 再起動してでも責任を果たせる状況に戻すためです。

まとめ

PCに代表される汎用OS上で動作するプログラムとの違いは、 実時間制約の中で求められる機能を低コストで実現しなければならないということでしょうか (もちろん出荷数量等にも依存しますが)。