以前、ファームウエア開発は若くて柔軟な頭脳と体力が必要と言われていました。
私は若いときに頭が硬く、決してファームウエア開発に手を出す事はありませんでした。
最近、年をとってから、趣味や仕事でファームウエアを開発するようになりました。
(もちろん、個人でやれる程度の規模のものですが。)
年をとって、頭が柔らかくなった訳ではないし(脳軟化症気味ではありますが)、体力がついた訳でもありません。
ファームウエアの開発が非常にやりやすくなってきました。
理由はいろいろあります。
昔はZ80Aのクロック4MHzが標準でした。
現在では、小型のワンチップマイコンでも20MHz位のスピードで動作します。
動作が速くなれば、同じ処理でも短時間で実行出来、時間的な制約が緩くなるので、プログラム作成が楽になります。
私が初めて接したCPUは8085で、EPROM8Kバイト、SRAM2Kバイトのものでした。
これがCPUボードを構成し、周辺I/Oは別のボードとしてカードラックに差し込む方式でした。
現在ではメモリや周辺I/OがCPUに内蔵され、1チップ化されています。
メモリはフラッシュROMが512Kバイト、SRAM30Kバイト程度のものまで存在します。
昔はボード何枚かで構成されていたシステムの何倍かのものが、1チップに収まってしまいました。
これは、単に小さくなった以上の効果があります。
昔は周辺I/Oが別チップ、別ボードにあったので、データバスを外部に延ばさなければなりませんでした。
データバスのバッファリングは方向制御があるので、結構大変です。
バッファの遅れ時間が許容値ギリギリだったりすると、時々、誤動作するような、悪質なバグとなります。
現在では、ハードウエアのバグを気にせず、ソフトが組めます。
全体が小型になった事は、ノイズを受けにくく、出しにくくなったと言えます。
私は、この事が楽になった一番の原因だと感じています。
CPUのアドレッシングモードやアセンブラ命令を知らなくても、標準的なCの文法でプログラムが組めます。
CPUの種類が変わっても、プログラム内容は殆ど変わりません。
デバッグもCのソースレベルで出来るので非常に楽です。
デバッグ時にメモリの絶対番地やCPUレジスタを参照することは、まず、ありません。
特定の位置で止めて、変数の値を参照するくらいでOKです。
基本的にアセンブラレベルのバグはありません。
高価なICEを使わなくても、簡単なデバッガでデバッグ出来るようになりました。
アセンブラと比較して、リストの量が非常に少なくなりました。
印刷したり、参照したりする時間も劇的に少なくなりました。
もちろん、リストはアセンブラ展開されたものを印刷することも可能ですが、その必要は有りません。
アセンブラの生産性の悪さから高級言語が使われるようになったのは、間違いないと思います。
では、何故、C言語が選ばれたかというと、その理由は詳しくは知りません。
想像では、ポインタが使えるからではないかと思います。
変数名も関数名も構造体の名前もアドレスに置き換える事が出来ます。
ポインタというアドレスを収納する変数を使えば、それらのアドレスを参照する事が出来ます。
RAMで動作するように設計されたC言語としてはルール違反ですが、ポインタで絶対番地を指定することも可能です。
(#pragma ADDRESS という絶対番地を割り付けるプリプロセッサを持っているコンパイラもあります。)
周辺I/O関連のレジスタは実メモリ上に割り付けられています。
多くのCコンパイラでは、CPU毎に、I/Oレジスタを絶対アドレスで参照するヘッダファイルが用意されています。
昔は良き時代でもあり、CPUの性能も低く、システムも複雑ではなかったのでアセンブラで対応できました。
CPUもC言語を使う事を考慮されて作られてはいませんでした。
したがって、C言語を使うとメモリ効率がかなり悪くなり、スピードもかなり落ちました。
この為、Cを敬遠する風潮がありました。
現在ではC言語でプログラムを組むことが標準になっています。
小さなワンチップマイコンにも、必ずCコンパイラが用意されています。
CPUもC言語を使うことを考慮して設計されるようになり、メモリ効率やスピードの低下が少なくなりました。
現在、アセンブラで高速、省メモリのプログラムを書ける人間は、かなりハイレベルの人間です。
私のレベルでは、下手にアセンブラを使うと、Cで組むより遅くてサイズの大きいコードになってしまいます。
特にSHのようなRISCチップはアセンブラが面倒で、C言語又はC++で開発する事が推奨されています。
現在では制御関係のエンジニアにとってC言語は必須です。
私も、そのことを感じて、かなり高年齢になってからCを覚えました。
話が横道にそれますが、SHはRISCチップで命令が固定長です。
従って、ワード以上の即値を直接レジスタに読み込む事が出来ません。
そこで、プログラムカウンタ相対アドレッシングモードでデータを読み込みます。
現在の命令位置から、あまり遠くない位置にデータを配置しなければなりません。
つまり、プログラム領域にデータが点在することになります。
さらに、CPUがデータをコードとして読み込まないように、飛び越し操作をしなければなりません。
これは結構、煩わしく感じます。
これ以外にも、遅延分岐とか、いろいろ煩わしい内容があります。
しかしながら、Cを使えば、このような事は気にしないで済みます。
全く、アセンブラを使わなくても、プログラムが書ける開発環境も存在します。
しかし、スタートアップルーチンやフラグの操作で、若干、アセンブラの知識が必要な場合もあります。
私は、新しいCPUを使うときは以下の手順を実行します。
1 ハードウエアマニアルでCPUの構造、特徴を理解する。
2 アドレッシングモードを理解する。
3 アセンブラ命令に、一通り目を通す。
転送命令やフラグの操作は理解しておいた方が良いと思います。
これでも、アセンブラで演算ルーチンを組むわけではないので簡単です。
周辺I/O関連のレジスタはメモリに割り付けられているのでC言語からアクセス出来ます。
しかし、CPU本来のレジスタはメモリに配置されていないのでC言語からはアクセス出来ません。
例えば、割り込みフラグをクリアする事はC言語では出来ません。
ただし、これらの処理を組み込み関数として用意している開発環境も有ります。
この場合、アセンブで記述する必要はありません。
電源が入ってからC言語のメイン関数が動作するまでの処理はスタートアップルーチンと呼ばれ、アセンブラで書かれ
ます。
CPUがハードウエア演算命令を持っていても、Cコンパイラが一般的なCのアルゴリズムで処理している場合は
本来の性能が出せません。
この場合、アセンブラで組むしかありません。
スタートアップルーチンでは、CPUの動作モードの設定やスタックポインタの設定等を行います。
変数領域の初期化は初期化ライブラリーを呼んだり、直接、アセンブラで記述したりします。
(C言語に移ってから、初期化ルーチンで初期化しても良いと思います。)
最後にメイン関数へのジャンプ命令を書いて、スタートアップルーチンは終了します。
C言語で最初に動作する関数が main() 関数で無ければならない処理系と自由に名前が付けられる処理系があります。
尚、開発環境によっては、スタートアップルーチンを自動的に作成してくれる場合もあります。
(予め用意されているルーチンを自動的に組み込んでくれるので、意識しないで済むということ。)
アセンブラを使用するには2つの方法があります。
一つは関数にアセンブラを埋め込むインラインアセンブラという方法です。
ただし、この方法は全てのコンパイラで認められている訳ではありません。
もう一つはアセンブラで関数を作る方法です。
簡単な呼び出し規約さえ守れば、アセンブラでCの関数を作るのは容易です。
逆にアセンブラからCの関数を呼ぶことも出来ます。
この場合は、関数の開始アドレスにジャンプ命令を書きます。
易しいか難しいかというと、本人の能力とも関係する事ですので一概には言えません。
Cの基本的な部分はコンパクトで、それ程、量のあるものではありません。
この部分は標準化されているので、CPUが変わっても内容は変わりません。
ただし、CPU、開発環境によって異なる拡張された部分があります。
この部分は、開発環境毎に覚えなければなりません。
それよりも、私が最も面倒に感じるのは、開発環境の使い方を覚える事です。
最近の開発環境は高機能になり、使い方が難しくなりました。
スタートアップを自動作成してくれたり、便利になった部分はあるのですが、反面、オプション設定が複雑になりました。
使いこなせないような付加機能が付いたのでマニュアルの量も増えて見にくくなりました。
マニュアルに関して言えばM16Cに関するものは読みやすかったですが、H8関連のものは読みづらいと思いました。
(現在ではルネサスという同じメーカーになっていますが)
個々の機能の説明はあるのですが、実際、どのように使っていくのかと言う部分が不足しています。
同じ、H8でもイエローソフトのコンパイラは機能が少ない分、マニュアルも読みやすく、使いやすいです。
ビジュアルCでウインドウの操作をするのは結構、面倒です。
しかし、DOSプロンプト(コマンドプロンプト)で動作するプログラムを書くのは簡単で、標準的なCが使えます。
ファームウエアの論理的なブロックをビジュアルCでシミュレーションすると、時間の節約になる場合があります。
ポインタの使い方で悩んだ時もビジュアルCで試したりします。
数値計算で係数を求めたりするような場合にも使えます。
Cでもアセンブラと同じように簡単に割り込み処理が記述できます。
手順は、まず、その関数が割り込み関数であることを指定します。
指定の仕方はコンパイラによって違いますが、多くの場合[ #pragma INTERRUPT 割り込み関数名 ]というプリプロセッサ
を使います。
これにより、コンパイラは割り込み関数特有の構造(スタックフレーム)を作成します。
次に関数名を割り込みベクタに登録します。
登録位置は割り込み要因によって決まっています。
これで、通常の関数と同じように記述出来、デバッグも同じように出来ます。
ただし、一般的に割り込み関数は引数も戻り値も持てません。
データのやりとりはグローバル変数を使うか、ポインタを使います。
割り込み関数内で使用されるグローバル変数には、最適化を禁止する volatile修飾子が必要な場合があります。
M16Cはリトルエンディアン、H8はビッグエンディアンです。
エンディアンが変わっても、C言語自体は変わりません。
ただし、注意しなければならない点があります。
例えば変数をバイトとワードで参照する共用体を考えてみます。
リトルエンディアンではメンバーを下位バイトから積み、ビッグエンディアンは上位バイトから積みます。
これはC言語の違いではなく、CPUの構造の違いです。
ビットフィールドの指定の仕方は、CPUの違いと言うより、コンパイラの違いで異なる様です。
同じH8でもルネサスのコンパイラは上位ビットから積み、イエローソフトのコンパイラは下位ビットから積みます。
アセンブラにビット処理命令があっても、標準的なCではリードモディファイライトを使います。
一旦、バイトデータとして読み込み、論理的にビット処理をして書き戻す方法です。
しかし、コンパイラの中にはビット処理命令を実装しているものもあります。
尚、周辺I/Oレジスタの中には、転送命令しか受け付けないものがあり、この場合はビット処理は出来ません。
周辺I/Oレジスタの中には癖のあるものがあるのでプログラムやデバッグに注意が必要です。
* 読み出し専用レジスタ
* 書き込み専用レジスタ
* 転送命令でしか書き込めないレジスタ
* 一旦、読まないと書き込めないレジスタ
* 条件によって、アドレスが変わるレジスタ
等があります。
周辺I/Oレジスタの中には、書き込み専用のものがあります。
デバッグ時にこのレジスタを参照しようとするとハマります。
特に不満なのがH8のポート入出力の方向レジスタです。
用途によってはポートの入出力を頻繁に変える場合があります。
例えば、ソフトウエアで簡易IICバスを構成し、シリアルROMにデータを書き込む場合を考えてみます。
Hレベルを出力する場合、ポートを入力に切り換えてプルアップ抵抗でHを出力します。
(ポートがオープンドレインの場合は、Hレベルを出力しても良いです。)
Lレベルを出力する場合は、ポートを出力にして、Lレベルを出力します。
こうしないと、出力同士が、ぶつかった時に危険です。
上記の例では、ポートの入出力方向が頻繁に変わるのですが、現在の状態が読み出せません。
仕方がないので、ポートに対応するグローバル変数を用意し、この変数を読み書きした後でレジスタに転送します。
SHやM16Cのポート方向レジスタは読み書き出来るようになっています。
C言語では引数や自動変数をスタックに積んでいきます。
関数から関数を呼び、さらに、そこから関数を呼ぶというように、呼び出しのレベルが深くなると、スタックの使用量
が増えていきます。
ファームウエアの場合、RAMの量には制限があります。
スタックのサイズはリンク時に人間が指定します。
ここで、スタックの使用量が、設定したサイズを超えると、動作不良になったり、暴走したりします。
したがって、実行前にスタックのサイズと使用量を十分検討しておかなければなりません。
どの状態の時に、一番スタックを使用するか見極めるのは、結構、面倒です。
開発環境には、スタックの使用量を解析するツールが付属している場合があります。
私はリアルタイムモニタを作る能力は有りませんが、フリーのリアルタイムモニタを試した事があります。
一つ一つの処理(タスク又はスレッド)を順番に実行していく様なプログラムでは、リアルタイムモニタは不要です。
しかし、例えば、運転中に動作のパラメータをキースイッチで変更する場合を考えてみます。
人間がスイッチを操作するスピードは遅いので、下手にプログラムを作ると、この部分がCPUを占有して、本来の
運転動作が出来なくなってしまいます。
そこで、状態、手順を監視する変数を用意し、処理を細分化して、変数の値に対応した処理を細切れに実行します。
人間がキーを押し続けていても、状態が変化しなければ、処理を返して、他の作業を行います。
このようなプログラムを作るのは、非常に面倒です。
このような場合、リアルタイムモニタを使えば、楽にプログラムを組む事が出来ます。
リアルタイムモニタにも、いろいろな種類があります。
簡易的なものでは、タスクを細切れにし、短時間で終了するようにプログラムしなければならないものもあるようです。
タスクに何段階かの優先レベルを設定出来るものは、各タスクのプログラムが非常に楽になります。
最低レベルのタスクは、無限ループで動作していても構いません。
上位のタスクが起動するようなイベントが発生すれば、下位のタスクの実行権を奪います。
割り込みではありませんが、多重割り込みのような動作をします。
ただし、上位にCPUを占有するようなタスクを書くと、下位のタスクには永遠に実行権が廻ってきません。
ポインタを返す関数と関数へのポインタは全く別のものですが、記述が似ているので注意が必要です。
ポインタを返す関数は、例えば int *func() と記述します。
これは、int 型の変数が格納されたアドレスを返す関数です。
変数の型が char であれば char *func() となります。
文字列を処理する関数には多く用いられ、文字列中の文字の位置を返します。
関数へのポインタは、例えば int (* func)() と記述されます。
この場合、* func が括弧で括られます。
そして、これは関数ではなく、関数が配置されたアドレスを格納する変数になります。
例として、動作モードにより、使用する関数を切り換える時などに便利に使えます。
まず、関数へのポインタを定義しておき、動作時に実際に使用する関数名を代入します。
C言語は他の高級言語と比較して簡潔でコンパクトであると言われています。
ただ、C言語は、もともと大型コンピュータのRAM上で動作させる為に開発された言語で、マイクロコンピュータの
ファームウエアを意識していませんでした。
その後マイクロコンピュータが誕生し、ファームウエアをC言語で開発する為、言語仕様が拡張されました。
この拡張された部分は無数にあるCPUチップのハードウエアに合わせる為、互換性が無く、C本来の部分より覚えなければ
ならない事が多いです。
いきなりマイコン用のC言語を覚えようとすると挫折する可能性があります。
まず、標準的なC言語を学習し、理解します。
DOSの時代はTuboCという使い易いコンパイラがあり、関係する書籍も多数、販売されていました。
現在、教育用のCコンパイラが存在するかどうか、私には判りません。
もし、無ければ、例えばビジュアルC++のコマンドプロンプト用のコンパイラが使えるのでは無いでしょうか。
ビジュアルCでウインドウを出すのは面倒ですが、コマンドプロンプト版なら簡単に扱えます。
(項目「ビジュアルCの利用」参照)
ビジュアルCのコマンドプロンプト版を使った入門書籍が存在するかどうかも判りませんが、ネット上で解説しているサイトは、
あると思います。
単にC言語の入門書というなら、多数ありますが、実際にコンパイラを操作して、コンパイルエラー、リンクエラー、
実行時エラーを体験しないと身に付かないと思います。
標準的なCに馴染んだところで、初めてマイコン用のC言語に取り組みます。
そうすれば、どの部分が拡張されたか認識できると思います。
当然ながら、マイコンのアーキテクチャー、アドレッシングモード、アセンブラの命令等には目を通しておく必要があります。
一つのCPUに対して、いろいろなCコンパイラが存在します。
只で手に入るものや、高価なものもありますし、使い勝手も違います。
趣味で使うならフリーの物やホビー用の安価なものでも良いと思います。
ただし、多くの人に利用されているものでないと情報が入りませんし、そのうち無くなってしまうのではないかという不安を
感じます。
コンパイラ自体にバグがあると、悩みますので、信頼性に対する情報もチェックしたいところです。
仕事では多くの場合、チップメーカーの純正の統合開発環境、純正のコンパイラ、純正のデバッガーが使われます。
これらは高価ですが、しっかりメンテナンスされ、問い合わせに対してもキッチリと対応してくれます。
(私の場合、日立と東芝しか経験していませんが、他のメーカーも対応してくれるはずです。)
多くのメーカーは評価版の開発環境をフリーで配布しているので、趣味では、これを使うのも手です。
評価版は、コードサイズ、又は最適化レベルのどちらかが制限されますが、趣味では十分使えます。
私は趣味ではPIC18Fを純正C18の評価版でコンパイルしています。
最適化レベルが制限され、拡張命令が使えませんが趣味には問題なく使えます。
PIC12F683にはMikroCの評価版を使っています。
こちらはコードサイズが2Kワードの制限があります。
ところがPIC12F683のROMサイズは2Kワードしかないので、結果的に制限無しで使えることになります。
コンパイラは各種ありますが拡張部分の扱いが異なるので互換性はありません。
CCSCのように文法的に方言の強いコンパイラもあります。
しかしながらC言語という共通の要素の上に乗っているので、移植は比較的容易です。
もう一つ、組み込み関数の違いがあります。
各コンパイラはハードウエアの制御が簡単に記述出来るように、独自の組み込み関数を持っています。
組み込み関数を使えば、少ない記述で済み、間違いも少なくなります。
ただし、各コンパイラ間で互換性が無いため、移植性は悪くなります。
私は出来るだけ組み込み関数を使わず、直接、周辺レジスタを操作する事が多いです。
これは、特に移植性を考えての事ではなく、周辺ハードウエアを理解しようとすると、どうしてもレジスタを操作したくなる
という性格的な理由です。
マイコンの開発環境の中にはC++が使えるものもあります。
もちろん、普通のCだけで記述しても問題ありません。
昔、仕事でTurboC++を使ってパソコンソフトを作ったことがありますが、このとは普通のCで記述しました。
現在、ビジュアルC++を使い、趣味でWindowsソフトを作るときもCからAPI関数を呼ぶスタイルです。
C++の基本的な部分はCと共通ですが、大幅に拡張されています。
大きなシステムで頻繁にバージョンが変更されるような場合、威力を発揮すると思われます。
C++のコンセプトは資源の再利用と隠蔽です。
オブジェクトという概念が追加されていますが、これは構造体のメンバーに関数が加わったものです。
要素毎にクラスライブラリー(例:プリンタクラス)を構成していくのですが、これらを0から構築していくのは
大変な作業です。
ビジュアルCでは基本的なクラスを纏めたMFCという巨大なライブラリーが予め用意されていますので、これを利用すれば
比較的簡単に(といっても難しい)プログラムが作成出来るようです。
ファームウエアの開発環境では、状況に合わせてライブラリーを構築していかなければならないでしょうから、かなりハードルが
高くなります。
いずれにせよ、私が個人的に作れる程度のプログラムではCで十分です。
C以外の言語を使った開発環境も存在します。
例えばBASICコンパイラ等があります。
これらはCより優れているというのが謳い文句ではなく、Cより簡単で覚えやすいというのが謳い文句のようです。
私は仕事でC言語を使わざるを得ないので、他のコンパイラを覚えようという気は全くありません。
チップメーカーも殆どC言語を用意しているので、とにかくCを覚えれば活用出来ます。
逆に、チップメーカーがC言語以外を用意するのであれば、その言語を覚えなければなりません。
C言語以外のマイナーな言語は文法が独特だったり、特定のCPU専用だったりして、他への応用が利きません。
また、仕様を突然変えられたり、言語自体が無くなったりする可能性もあります。
現状では、とにかくCを覚えておけば間違いないと言えます。
尚、ここではコンパイラの話をしましたが、どのようなコンパイラを使おうとアセンブラの知識は必要です。