私が最近生存している業界は、アナログ最後の砦と言われているほどIT化の遅い世界ですが、ここ数年でジワリジワリとソフトの力が増してきている、という業界です。
あ、なお、お前のブログ読んでるとまるで働いているみたいだな!とか言われましたが、私はクソニートですので誤解なきよう。お仕事くらさい。
で、そんなもんですから、今までアナログ屋だった人が突然ディジタル屋に突っ込まれたりするわけで、コーディング技術何それおいしいの?状態だっりします。
そんな方々が書くCコードを見ると、ひたすらコピペの連続のような力業プログラミングだったりするわけです。
そんな方々に送るちょっとしたアドバイス記事です。別段目新しかったり、難しい内容はありません。
なお、この記事におけるコーディング技術は主に「保守管理」に関わる様な技術を指しており、アルゴリズムの話や、超絶技法の様な話は含んでおりません。
コーディング技術は何故必要なのか?
そもそも、なぜコーディング技術が組み込み屋に必要なのでしょうか?
大規模組み込みならまだしも、ちっこいマイコン相手にしているだけだと、コーディング技術とかただの理想論にしか聞こえないかも知れません。そういった方々にとって大事なことは「動くこと」だけになっています。
むろん、動くことは何よりも重要です。どれほど素晴らしく、美しいコードであっても、実機で動かなければウンコほどの価値もありません。
しかし、「簡単に書く」、「簡潔に書く」、「わかりやすく書く」という観点が抜けてしまうと、何故ソフトウェア化するのか?の意義の一つが減ってしまいます。
ソフトウェア化する意義とは何か?
今、我々の業界で進んでいるソフトウェア化は、基本的にはハードをソフトで置き換えているだけです。となると、現状あるソフトウェア化のメリットは、「部品点数削減」これだけです。
ソフト化による製品開発期間の短縮?「今」のあなた方はそれが出来るほど、今のあなた方にソフトの技術がありますか?
ディジタル化することによる、アナログで出来ない高性能化?それが既に達成されている業界では最も重要なことだと思いますが、我々の業界ではまだ理想論であり、現状は物にするほど理論は完成されていません。それは数年という期間を経て実現することです。もし、それが本当にソフトウェア化のキーワードであるのであれば、短い開発のサイクルで製品化するようなプロジェクトではなく、数年という長い期間を経て市場に出すべきでしょう。
それでもなお、今、慣れないディジタル技術を用い、短い開発期間で製品開発を強いられている意義は、部品点数削減のためです。
たしかに、ソフト化することで、コンデンサの数が減れば、物理的な耐久性の向上、部品コストの削減が図れます。しかし、ここで一つ考えなければならないのは、製品の部品は減っていないということです。なぜなら、物理的部品が減った代わりに、プログラムコードという部品が増えたからです。
コードの価値というのは低く見積もられがちですが、コードは部品であり、使わないコードや重複するコードがあれば、それは在庫です。
このコードという部品をどう管理するのか?これは当然考えなくてはならない事です。
果たして、製品コードに問題があると判明したとき、貴方はそのコードを後で見返して、すぐにどこに問題があるかわかるでしょうか?貴方が修正するならまだしも、貴方が忙しくて他人に頼む場合、そもそも貴方がそこにいなくなっていた場合、他人はすぐにどこに問題があるか分かるでしょうか?
問題がなかったとしても、別な製品でそのプログラムを、使い回したい場合、それがすぐできますか?
これらが出来る場合、当然出来ない場合に比べて製品のコストが下がることになります。逆にこれが出来ない場合、もしかしたらソフト的なコストで、むしろ製品コストが増加することすら、あるかも知れません。
そこで必要になるのが、如何に簡潔に、綺麗に、わかりやすく書くかというコーディング技術です。
コーディング技術なんて捨てて良い
コーディング技術?残念だが、そんなものは糞食らえだ。今すぐ捨てて貰って構わない。
さっきと言っていることが、真逆で申し訳ないのだが、やはりこれは最初に書いておく必要があると思う。コーディング技術に頼ってはならない。
組み込みの世界はどうしても、使えるリソースが少ないです。リソースというのは、メモリであったり、実行ステップ数(実行時間)だったり、得られる情報などを指します。
これらの条件と、コーディング技術は相反する場合があります。そういった場合、迷うことなくコーディング技術を捨てて、泥臭くて汚い力業の世界に持ち込んでください。
如何に綺麗に書くかを悩むだけ時間の無駄です。
ステップ数が足りない?そうか、そこの関数呼び出し止めて展開しようか。そのループを展開しようか。ポインタ操作の最適化しようか。そこの計算アセンブリにしようか。
メモリが足りない?なら、この変数とあの変数は同時に使わないから、同じ変数にしよう。
組み込みでは、どうしてもこういう事態が発生します。
わかりやすく書くコーディング技術は大切なことですが、捨てて構わないものです。
大切なのはバランス感覚です。
早すぎる最適化はしない。コーディング技術を大切に
関数呼び出しをやめて展開する。ループを展開する。ポインタの最適化。メモリ配置の最適化。
こういったものを行いたいという欲求は、早すぎる最適化を呼んでしまいます。こういった手段は、最後の奥の手です。
最適化をしなくても動くのであれば、最適化などする必要がありません。汎用性を失うだけです。
例えば、設定する変数A、B、Cがあり、A,Cが隣り合うアドレスだったとしましょう。
このとき、「A=1;B=2;C=3;」よりも、「A=1;C=3;B=2;」の方が上手い具合に最適化される可能性が高いですね。
コンパイラが信用できないなら、こう書けば完璧です。
char *p = &A;
*p = 1;
*(p+1)=3;
B=2;
さて、このコード、使用しているマイコンの次バージョンを使用するときにも使えるのでしょうか?
ソースコードレベルでの後方互換があったとしても、アドレスまでは完全に同じとは限りません。そうなると、このコードは再利用は出来ない在庫コードとなります。
省メモリ、小ステップは確かに大切なことですが、クリティカルな問題にならない限りは、最初は放っておいても良いのです。
大切なのはバランス感覚です。
数値はdefineで名前を付けましょう
マジックナンバーと言う言葉をご存じでしょうか?マジックナンバーとは、処理を知っている人だけが理解できる、プログラム中に現れる他人から見たら謎の数値のことです。
組み込みではこのマジックナンバーが大量に現れます。
MODULE0 = 0x6c;
MODULE0OV = 1234;
MODULE0EN = 0x71;
さて、この0x6c,1234,0x71って何でしょうね。意味が分からないですね。
これを理解するには、マイコンの仕様書を片手に各ビットが何なのかを調べなくては、読むことが出来ません。吐き気がします。
それを回避するためにこんなコメントが書かれてたりします。
MODULE0 = 0x6c;
MODULE0OV = 1234;
MODULE0EN = 0x71;
確かに少し読めるようになりました。しかし、根本的に何も解決していません。
例えば、MODULE0の結果転送先を変えたいとき、0x6cはどう数値を弄れば良いのでしょうか?
なに?仕様書をみろ?
バカじゃないの?マイコンの仕様書とか分厚いのに、ちょっと数値弄るためだけに何度も見直すなんて。
こういう場合は、数値の意味を分解します。
例えば、クロックを決定しているのが、上位2bit(7,6bit)、Event設定がその次のビット(5bit)、4bitは例えばモード切り替えで、下位4bitが転送先設定としましょう。
すると、次のように意味単位で数値を分離できます。
MODULE0 = 0x40 | 0x20 | 0x00 | 0x0c;
別にこの場合は+(加算)でも構わないのですが、|(ビットor)を良く使います。どのみち、組み込み屋はビット演算からは逃げられないので、諦めてビットorに慣れてください。
さらに、これらの数値にdefineで名前を与えると次のようになります。
#define MODULE_CLOCK
#define MODULE_CLOCK1 0x40
#define MODULE_CLOCK2 0x80
#define MODULE_CLOCK3 0xc0
#define MODULE_SEND_EVENT 0x20
#define MODULE_UNSEND_EVENT 0x00
#define MODULE_MODE
#define MODULE_MODE1 0x10
#define MODULE_TRANSFER_C 0x0c
MODULE0 = MODULE_CLOCK1 | MODULE_SEND_EVENT | MODULE_MODE0 | MODULE_TRANSER_C;
随分わかりやすくなりました。もし、モジュールからイベントを送りたくなくなった場合は以下のように変えれば良いだけです。
MODULE0 = MODULE_CLOCK1 | MODULE_UNSEND_EVENT | MODULE_MODE0 | MODULE_TRANSER_C;
ちなみに、このように分離することで、無駄な処理が入るから使えないと思うかも知れませんが、0x6cと各コードとこのコードは全くコンパイル結果は同じです。
定数式は最適化オプションを切っていてもコンパイラが勝手に計算してくれます。
え、マイコン対応コンパイラがガチで計算してくれないドマイナー糞コンパイラだって………?
どれほど安いのか知りませんが、そのマイコン使うの辞める事を提案するところから始めませんか?
コメントを書く様にする
コードを書いた時点ではわかっていることでも、3日もすればすぐに分からなくなるものです。
明日の自分は他人です。何も知らない人に優しく説明するように、明日の自分のためにコメントと言う名のドキュメントを残しましょう。
特に、グローバル変数や関数などには、使い方などを残しておくのが吉です。
呼び出し条件は?この引数の意味は?引数に渡して良い値は?渡してはならない数値は?などはキッチリ残しておかないと、絶対後悔します。
Javadocの様に後でドキュメント化する場合は、Doxygenを使うと良いでしょう。
volatile unsigned int hoge_interrupt_counter;
void reinitModule(char mode,char offset_value)
{
なるべく呼び出し元の責任が減るように関数内で判断するのが良いのですが、組み込みだとそうもいかないこともあると思いますので、責任関連は絶対残してください。
コメントを書かない様にする
ソースコード中に現れるコメントは大別すると以下の3つしかありません。
- 有害なコメント
- いずれ有害になるコメント
- ソースコードとしては意味のないコメント
従って、書いても良いコメントは無害な「ソースコードとしては意味のないコメント」だけです。
例えば、以下のようなコメントは書いても良いです。
その他のコメントは有害なので、極力書かないことをお勧めします。
有害とはどういうことか?それは、コメントに書いてあることと、実際のコードが異なっている状態を指します。
先ほどの例を再度挙げてみましょう。
MODULE0 = 0x6c;
MODULE0OV = 1234;
MODULE0EN = 0x71;
この時点ではまだ有害ではありません。
さて、仕様が変わり、Event出力をoffにしました。
MODULE0 = 0x4c;
MODULE0OV = 1234;
MODULE0EN = 0x71;
この瞬間、コメントが有害化しました。Event出力をしなくなったはずなのに、コメントは修正されず、「Event出力あり」となっています。しかもこれ、コンパイルエラーにはならないので、結構直すの忘れるんですよね。
生ものは冷蔵庫に放っておくのではなく、すぐに食べるなりして残さない、もしくは完全に凍結してしまうかのどちらか以外扱い方はありません。
コメントも同じです。コメントは生ものです。いずれ腐ります。コメントは残さない、もしくはソースコードを二度と弄らない凍結状態とすることが鉄則です。
コメントに頼らなくても、わかりやすく、「読めるコード」を書くことを心がけましょう。
変数名がa,b,c,dなんかだったりして、コメントがなくて読めるでしょうか?関数名がhogeだったりして、何の関数なのか分かるでしょうか?
何百行もひたすら、一つの関数のコードだけ書いてあって、果たして全体が把握できるでしょうか?
コメントに頼らず、読みやすいコードを書こうとすると、自然と構造がわかりやすいプログラムになっていきます。
読めるコード、「リーダブルコード」というキーワードを頭の片隅に置いておいてください。
マジックナンバーを消すのもリーダブルコードの一つですね。
カレーは飲み物、コードは読み物です。
意味の区切りでなるべく関数に分ける 繰り返し出てくる処理は関数にまとめる
組み込みではないのですが、サブルーチンも使わずひたすら同じ事をコピペによって繰り返すコードを見たことがあります。
なんでサブルーチンにしないのか?と聞いてみたら、サブルーチンにするより、コピペの方が簡単だし、安心だからとの返答をいただいた。残念ですが、私には何を言っているのか理解できませんでした。
まず、コピペは危険です。コピペコードの修正し忘れによるバグの発生はよくありますし、そもそもコピペコードがバグってたときは悲惨で、あちこちに散乱したコピペコードを全て修正する必要があります。どう考えても一カ所にまとめて管理した方が安全です。バグってた場合はその一カ所だけ直せば良いのですから。
次に、コピペでない場合でも、ひたすら処理が何百行と続いているコードよりも、関数で適切に区切りがあるコードの方が読みやすいです。
でもこれに対しては、あちこちに処理が散らばるよりも、一カ所の関数だけで書いてある方が読みやすいと言われたことがあります。しかし、関数で区切ることは、文章で言えば、漢字や句読点、章立てと同じです。
なんぜんぺーじもあるほんがかんじもくとうてんもつかわずましてやもくじもないばあいをかんがえてみてくださいあなたはそのほんをさいごまでよみとおすことができますかわたしはこのぶんしょうですらたぶんあしたにはよめません
また、長すぎるコードはテストが大変です。問題がどこにあるのかの切り分けが難しいのです。(組み込みでのテストの作り方は私は詳しくないので突っ込みませんが………)
関数は10~100行ぐらいにまとめると良いと思います。
あとは、ファイルスコープなどを駆使して、グローバル変数が見える範囲とかも最小限にするなど………まぁ、これはいいや………。
マクロを使って、同じ内容を繰り返し書かない様にする
マイコンでは、各モジュールの設定手順は、変数名レベルでほとんど同じって事がよくあります。
A0PROP =11;
A0PROP2=22;
A0PROP3=33;
A1PROP =111;
A1PROP2=222;
A1PROP3=333;
B0PROP =1111;
B0PROP2=2222;
B0PROP3=3333;
こういう場合はマクロでさっくりまとめられます。
#define SetModuleProps(name,n,p1,p2,p3) name##n##PROP = p1;\
name##n##PROP2= p2;\
name##n##PROP3= p3;
SetModuleProps(A,0,11,22,33);
SetModuleProps(A,1,111,222,333);
SetModuleProps(B,0,1111,2222,3333);
マクロは関数ではなく、ソースコード文字列の置換なので、これら二つのコードは全く同じコードです。
参考:
プリプロセッサ
#と##演算子
マクロは使わない
マクロは定数マクロ以外使わないでください。(定数マクロは「#define A 10」の様に定数に名前を付けるマクロです。)
次のように、ほとんど書いている内容が同じだからと言うことで、マクロを使ってまとめるアホがいますが、お勧めは出来ません。
(マクロでまとめることはお勧めしませんが、関数でまとめることはお勧めします)
#define SetModuleProps(name,n,p1,p2,p3) name##n##PROP = p1;\
name##n##PROP2= p2;\
name##n##PROP3= p3;
SetModuleProps(A,0,11,22,33);
SetModuleProps(A,1,111,222,333);
SetModuleProps(B,0,1111,2222,3333);
このように書くぐらいなら、下手なことをせず、以下のように書きましょう。
A0PROP =11;
A0PROP2=22;
A0PROP3=33;
A1PROP =111;
A1PROP2=222;
A1PROP3=333;
B0PROP =1111;
B0PROP2=2222;
B0PROP3=3333;
なぜなら、これら二つのコードは等価ではありません。………ん?同じじゃないかって?
これね、ブレークポイントが非常に扱いにくいんですよ。
マクロを展開すると、以下のようになるから、A0PROP2の所にブレークをおきたくても、エディタ上からは設定できなくて、直にブレークするアドレス探して設定するなんて手間がかかる。
A0PROP =11;A0PROP2=22;A0PROP3=33;
A1PROP =111;A1PROP2=222;A1PROP3=333;
B0PROP =1111;B0PROP2=2222;B0PROP3=3333;
コンパイラやデバッガが頑張ってくれて、A0PROP2,A2PROP2,B0PROP2の場所にブレークポイントを設置できる場合があったとしても残念なことになる。マイコンなんてしょぼいデバイスは大概ブレークポイントも一、二個しかおけないから、今回のように3箇所に展開されてしまうと、どうもならない。
その他の理由としてはマクロは難しいし、色々面倒くさいという理由もあります。C++ではプリプロセッサ界は魔界と呼ばれております。
触らぬ神にたたりなし。不必要に関わらないようにしましょう。
ローカル変数は書き換えない、使い回さない
状態が変化するものを完璧に把握し、完璧にプログラムを組み上げるのは中々難しいものです。
「AならばB」という明快なプログラムは、ミスしにくいです。しかし、「AならばBが基本だけど、状態によってはCで、まれにDとかEにする」ってなると、一気にバグが入り込みやすくなります。
なので、本当は「状態に依存しないプログラムを書くことを心がける」と書きたかったのです。なのですが……、組み込みで状態に依存しないプログラムなんて不可能です。基本的にほとんど全ての処理が状態に依存しまくりですよね。
しかし、状態の変化は諦めるとしても、ローカル変数を書き換えない、使い回さないという意識は出来るとおもいます。
何カ所にも渡って変数が書き換えられると、その都度、変数の意味を再解釈しなくてはなりません。どこで変化し、どのような意味を持つのかを、全て把握するのは大変です。さらに、もうこの変数はここから先で使わないから、再利用しようと変数を初期化したものの、後で前の結果が必要になることもあります。しかし、そのコードを書く頃には、変数を初期化したなんて忘れててバグった………、なんてもうよくありがちですね。
ローカル変数の再利用は、変数を定義するのが面倒だったのかも知れません。しかし、後々、もっと大きな面倒を運んできてしまいます。面倒だったのではなく、メモリの使用量の最適化だったのかも知れませんが、それは早すぎる最適化です。
ローカル変数の値は一度決定したら、書き換えないことは徹底しましょう。
int temp = 1;
………
temp += hoge(1);
………
moge(temp);
………
temp = 0;
....
if(temp == a){
...
}
まとめ
- 数値には名前を付けましょう。ビット単位で区切るとなお良し
- コメントをちゃんと書きましょう。どうせ明日には分からなくなります
- コメントを書いてはいけません。どうせ明日には腐ります
- 関数で小さく意味の単位に分けましょう
- マクロを使って処理をまとめましょう
- マクロは地雷踏む可能性高いので使うのは止めましょう
- 状態が変わらないほどバグりにくいです。せめてローカル変数だけでも変化しないようにしましょう
- 最適化は重要だけど、最後の手段。早すぎる最適化は駄目です
綺麗なコードを書いて苦しい組み込みライフを楽しんでください。
(´・д・`)ノシ