C言語


アセンブリから派生した言語の一つで、機械語に近いアセンブリに対して、人が見やすい言語として開発された。

今ではプログラムの中心として用いられることが多く、発展型としてC#、C++など別のものに特化させたものがある。
今ある言語の基本的な部分はC言語と同じなので、C言語を理解しておくと、それなりのプログラムが読むことができるようになる。

はずであるが、人間に近すぎて逆に難しい言語でもある。

よく言われるのが、書き方。
ターミネータ(セミコロン)により、文節という概念がさらに細かくなり、文字列中でなければ、改行、スペース使い放題である。


if(
a==0
)
{
a++;
b++;
}


if(a==0){a++;b++;}

if(a==0){
 a++;
 b++;
}



どれも同じ意味をあらわすが、大事なのは読みやすさでもある。

ここでは、使い方から、読みやすいプログラムまで、順を追って説明していきたい。




構文


プログラムの構文は次の通り。これはC言語に限らない。
順次構文
最も基本的な構文で、上から下に一行ずつ処理していく。
分岐構文
条件によって分岐し、行う処理と行わない処理に別れる。
反復構文
特定のところまで戻り、再び同じ作業を行う。

これらをうまくつなぎ合わせて、プログラムを作る。
フローチャート作成を行うことで、最も適切なプログラムを作成できる。


命令


プログラムは命令の集まりである。
プログラムにおける命令とは、特定の動作、作業をさせるもので、計算から画面の表示までいろいろな命令がある。
ちなみに、C言語の命令の中で最も多く使われる記号は=(イコール)で、意味は代入である。
命令はターミネータ(;)で区切って使用する。つまり、ターミネータの数は命令の数に等しくなる。


命令の例

A=A+B;
AにA+Bの結果を代入する。決してAがA+Bに等しいわけではない。

init();
initという関数を実行する。関数については後ほど。


全部使えてナンボなものたち


分岐、反復など

if(条件文)
else
switch(判断文)
case 定数:
break 
while(条件文)
for(初期分;条件文;繰り返し文)

演算子

=
++
+
+=
--
-
-=
&
|
>>
<<
!
^

条件式

==
!=
<
<=
>
>=
&&
||


関数

ある一定の命令を集めた命令で、そこに引数と戻り値があることから関数と呼ばれる。
main関数内で用いるためには、その上に関数の内容を書くか、先に宣言をしておいてあとで書く方法がある。
例外を除いて好きな名前をつけることができ、何度も呼び出して実行させることが出来る。

雰囲気は次の通り。
int max(int x, int y);
main(){
...
c=max(a,b);
...
}

int max(int x,int y){
...
}

関数を使うメリットとして、
  • main内がすっきりする
  • 同じ作業を一つにまとめられる
  • 複数行にわたるコメントアウトを省略できる
何度も使うものは、関数として宣言しておくとよい。


変数の名前

パソコンなどに向けたプログラム用の変数には,ハンガリアン記法というものがある.
これは変数名を見ただけでこの変数がどんな型でどんな目的に使われているか明確にするという理由がある.

ただし,マイコンでは変数の型にほとんど選択肢がなく,型が違ってもどうにか変換することも可能であるため,そこまで意識する必要はない.

しかし,安易にi,j,kなどの変数を宣言すると,何に使っているかわからなくなる.
また,デバッグ時に検索したときも,iはwhile,kはbrakeに含まれているため違うものがヒットしてしまう.
極力アンダーバーやハイフンを使って変数にバリエーションを加えておくことをお勧めする.
ただし同じプログラム内でアンダーバーとハイフンを使ってしまうと他の人が見たときに見分けがつきにくいので片方にすること.


マクロ

C言語には、マクロと呼ばれる様々な機能が用意されている。
マクロはソースに記述するものの、直接プログラムになるものではない。
コンパイル時に読み込まれ、プログラムに影響を与えるものである。
ちなみに#で始まるものがマクロである。
例えば、
#include<stdio.h>
stdio.hというファイルを読み込む
#define K 10
Kを10に置き換えてコンパイル

など。
コンパイラによって依存がある場合があるが、マイコンでは関数や機能の設定に用いられることもある。


コメント

他人に読まれることがあるプログラムで、最も大事なのはコメント。
コメントは、コンパイラやアセンブラを通るときに無視され、動作には全く影響を与えない。
プログラムなどの文を残しておきながら実行させないようにするためにコメントにすることを、コメントアウトという。
コメントアウトする方法は次の通り。
行の最初に//(スラッシュ、スラッシュ)を挿入する
/*(スラッシュ、アスタリスク)と*/(アスタリスク、スラッシュ)で囲む
この中に書くのは、プログラムと直接関係ないことでもかまわない。
例えば、ピン配置や計算の式や結果など。
特に開発者、プログラムの内容と更新日は入れておくべき。
わかりにくい構文も、いれておくべきだがいれにくい。
コメントが入っていないプログラムを他人が読むと苦痛。


ところで


C言語がわかっている人は、次の3通りが同じ意味を成すことがわかる。

a++;
a+=1;
a=a+1:

何にも考えなければ、これはどれを使っても答えは一緒である。


実はまったく違ったのである
(ただし、例外を含む)

次の3つのアセンブリを見てくれ。

INCF main_a_L0,1
BTFSC STATUS,Z
INCF main_a_L0+1,1

MOVF main_a_L0,0
ADDLW 1
MOVWF STACK_0
MOVLW 0
BTFSC STATUS,C
ADDLW 1
ADDWF main_a_L0+1,0
MOVWF STACK_0+1
MOVF STACK_0,0
MOVWF main_a_L0
MOVF STACK_0+1,0
MOVWF main_a_L0

INCF STACK_0,1
BTFSC STATUS,Z
INCF STACK_0+1,1
MOVF STACK_0,0
MOVWF main_a_L0
MOVF STACK_2+1,0
MOVWF main_a_L0+1


これは、先ほどの3つをコンパイルし、出力されたアセンブリファイルである。
順番しだいで少々長さは変わるが、ほぼ一緒と思ってもらっていい。

アセンブリがわからない人でもわかるように言うと、

アセンブリは行の数が実行する時間である。

となる。つまり、同じ処理をするためにかかる時間が、書き方によって違うのである。
驚いたのは、a+=1よりもa=a+1のほうが時間が短いこと。
ここで注意すべきは、今回は1を足したことで、2以上ならインクリメントは使えなくなるため、また必要な時間は変わってくる。
おそらくa+=2はa=a+2よりも短いと考えられる。実際に試してみると面白いかもしれない。

このようなことが、他にも多数ある。(if(a==0)とif(!a)など)
いろいろ探して、プログラムを改良してみると面白い。


mikroCの弱点

PICの統合環境の中でも、体験版で簡単に導入できるのがmikroC。
木更津高専で大流行中のmikroCだが、いろいろとクセがある。
まず他のコンパイラと違ってプログラム中にコンフィグレーションビットを記述する必要がない。
つまりプログラムを見てもコンフィグレーションを再現することが出来ない。
もし他人のプログラムを参照するときは、プロジェクトファイルごともらっておくことをお勧めする。

次に、用意されているmikroC関数。
プログラム中に記述しておくと、プログラムの最初に書き加えてくれると思いきや、関数を呼び出した場所にそのままプログラムが書き込まれる。
一番わかりやすい例が、連続で同じ関数を呼び出したとき。
Delay_ms(1);
Delay_ms(1);
と記述すると,そのままDelay_ms(1)の処理が二回書かれる。
hexファイルの行数も増加するので、なるべく使わないことをお勧めする。
おそらく体験版であるのが原因。きっと最適化が行われればなくなると信じる。

一番困ったのは、PICでSPI関数を使用したとき。
関数内に入出力ピンの設定が入っているため、スレーブ側でクロック端子を入力に設定していたにも関わらず、勝手に出力端子に変わっていた。
アセンブリを確認してやっと発見されました。
これで3日くらい取られてたら、何も完成しません。

やはり、どんな環境でもコンパイルできるような記述をすることをお勧めします。


型宣言と比較

unsigned型とsigned型では,比較演算子を用いたときの処理法が異なる.
signed型では,まず符号ビットの分岐を行った後に,引き算などによる分岐を行う.
一方でunsigned型では符号ビットの確認なしに分岐を行う.

8ビット変数では0x80以上つまり128以上または負の数の比較に影響が出る.
ただし,==には影響せず,主に>や<である.

同じバイト数の型でも,符号ビットの有無で判定文が変わるので注意すること.


型を使いこなす


型による影響は比較に限らない.
例えば違う型への代入.

unsigned int型の変数xがあるとすると,

y1=(signed int)((singed char)x);
y2=(signed int)(x);

の二つの答えは違ってくる.
y1には-128から127がintに代入されるが,y2には0から255が代入される.
例えば0x80を代入すると,0xFF80と0x0080となる.

マイコンのレジスタには8bitのものも多く,よくunsigned char型を使うことも多い.
しかし便利なint型へ変換するときに手順を誤ると,正しいデータとして扱うことができなくなる.

基本的にはsigned型を使わなければいい.
必要になったら型変換のタイミングを意識してみるといい.


ただし,float型やdouble型は対応してないマイコンで計算を行うと非常に時間がかかる.
固定小数点型に対応しているマイコンであっても時間が長くなる.
パソコンのように自由に型を使っていると計算時間が長くなって遅れが生まれたりするので注意.

ちなみにデータバスが8bitのマイコンで16bitの計算を行うと,上8bitと下8bitに分けて計算する.
またその繰り上がりなども計算する.
よって倍以上の時間がかかることになる.

マイコンに合わせた変数の型を宣言するといいだろう.


関数化とヘッダファイル


マイコンをある程度使っていくと,実は同じようなプログラムを毎回作っていることに気が付くかもしれない.
例えばI/Oピンの設定や,タイマーの周期.
これらを毎回コピーして使う手間も結構なもので,今までのプログラムを探して,似たような画面の中でコピーと貼り付けを行わなくてはならない.

そこでまず行われるのが関数化.
初期設定には,Initialize(初期化)の略であるInit()という関数を作成し,この中に設定をすべて書いておく.
するとこの関数をコピーするだけで,同じマイコンをすぐに使うことが可能となる.

しかし,これでもまだコピーをすることに変わりない.

そこで行うのが,別ファイル化.
includeマクロにより,事前に別ファイルを読み込むことが可能である.
初期設定の関数を別ファイルに書いておくことにより,コピーせず読込先を設定するだけで,初期設定を行うことが可能になる.

特に,様々なプログラムで使用できるように改良したものは,ヘッダファイルと呼ばれていることもある.
stdio.hなどは有名だろう.


しかしこのヘッダファイル,一つ弱点がある.
意味もなく関数を読み込むと,プログラムメモリを圧迫していくのである.
またその影響でコンパイルも長くなることがある.(設定によっては回避可能)

ここで使われるもの,マクロ.
#で始まるマクロは,他にもifなどがある.
これはコンパイル時に,読み込んだり読み込まなかったりする設定が可能である.

例えば今回,UARTは使わないがUARTの設定がヘッダファイルに合ったほうがいい場合は,次のようにするとよい.

original.h内
...
#if UART == 1
...(UARTの設定)
#endif
...


main.c内
#define UART 0//書かなくても読み込まない
//#define UART 1  //1にするとUARTの設定を読み込む 
#include<original.h>
...
main(){
...
}

宣言されていない名前は自動的に0になる模様.
設定しなければ読み込まないので,使いたいときのみ関数を読み出すことができる.

このマクロの中にdefineを入れることも可能であり,この関数が読み込まれたらこの関数を読み込む,といった設定も可能である.
ただし,読み込まれる順序は必ずソースの上から下なので,記述する順序には注意しなければならない.

コンパイラが持つバグや仕様


多くの人が使用するgccなどのパソコン向けコンパイラは,多くの修正が加えられバグや誤った仕様などはほとんどない.
しかしマイコンに使われるコンパイラは専用のものであるため,バグが残っていることがある.
実際にあった例で言えば,ルネサスのHEWでif else文の構文解析が間違って行われたことがある.

極力コンパイラの性能に左右されないように文を書くためには,計算の優先順位や区切り方をしっかり示すとよい.
ちゃんというと,

  • 判定分は一つずつ括弧で囲む
  • 一行での掛け算や足し算はしっかり括弧で囲って優先順位を示す
  • if分岐する先が一行でも中括弧を使う

などである.

正しくプログラムを動かすためにも,少し気にしてみるとよい.
最終更新:2012年01月12日 13:19