C言語でマイコン制御 その2
5.Cでプログラミング
使いようによっては、極めて技巧的にプログラミングが書けるC言語ですが、ここではできる限り基本に沿った、判り易いプログラミングを追求してみることにします。
ところで、「C言語」とは何なのでしょうか? 同じCにも「C++」、「Visual C++」、「Turbo C」、「Hitech-C」、「
Lattice C」、「LSI C」、、、とさまざまです。
C言語は、別項にも書いたように、ベル研究所のD・リッチーが作ったプログラミング言語で、日本でも出版されている 「プログラミング言語C
UNIX流プログラム書法と作法 B・W・カーニハン D・M・リッチー著 共立出版」 俗称(K&R) に詳しく規定さえている言語ですが、各社が各種CPUや開発環境用に、発展させたCコンパイラーを発売していく中で、かくも華々しいC言語のパレードとなったようです。
さて、C言語の入門書の導入部は判で押したように次の一文で始まります。
#include <stdio.h>
main()
{
printf("Hello,world\n");
}
これは
「UNIX流プログラム書法と作法」冒頭部の単なる模倣という訳ではなく、K&Rに対する敬意のあらわれなのですが、
無礼にも本書はこれを簡単に無視しています。
"Hello,world"をPCの画面に印字させるC言語プログラムはPC自体の上で働くもので良かったのですが、我々が必要としているのは、マイコンの上で動作するC言語プログラムを、PCを使って作りだすという環境です。
当然、我々が対象とする普通のマイコンチップには、キーボードもディスプレイも付いていません。ですから、printf("Hell,, を試しようにもできないという訳です。
つまり、我々が使いたいC言語は、スイッチ一つ、LED一つだけの環境の中でも開発を進めることができる裸の言語ということになります。そして裸だからこそ様々な装いを自由に重ねていくことができる言語でもあります。
ANSI準拠
さて、C言語と呼ばれる言語にも「標準語」はあって、それは例えばANSI準拠のC言語なのですが、しかし、人類の各国語程には違わないにしても、デバイスやCコンパイラメーカーや開発環境で細部がかなり違うものになってしまっています。ANSI規格は基本文法と、char
が8bitという規則程度は共通であるにせよ、ポートやタイマなどのインターフェースがデバイス毎に全部異なるのはもちろん、マクロ定義やアセンブラ記述法も全く異なるため、異なった環境で同じソースファイルをコンパイルしてみると、たちどころにエラーが続出してしまいます。移植性を高めるためには、プログラミング本文は可能な限りANSI準拠文法に限定して記述し、デバイスに依存する部分はなるべくfileの先頭に集めた#define文で変更できるようにすることが望まれます。
5-1 ヘッダーファイル
サンプルプログラムをざっと見てください。まず冒頭に
#include <avr/io.h>
のように書いてありますが、これは avr/io というヘッダーファイルをinclude(含め)という命令です。#記号は、コンパイル実行に先立つ処理命令記号で、プリプロセッサ文といいます。
includeされる側のヘッダーファイルも実は特別に用意されたファイルではなく、自分で作ることもできる点がC言語の特徴です。例えば、avr/io.h の中にATtiny2313の入出力アドレスが規定されていますが、そのヘッダーファイルの中に「PORTBは0x18番地」 というような定義が記述しておくことで、プログラム本文の中に入出力アドレスの定義を書き込む必要が無くなります。このようなプロセッサやマイクロコントローラのポートやポートのビットに関する定義(#define)は
"model名.h"としてメーカーから供給されていることが多いようです。
他のヘッダーファイルもおまじないのように include しておいてください。割り込みや、EEPROMを使うときに必要となる定義が書かれています。そういった機能を使わなくても特に副作用はありませんので、とりあえずインクルードしておきます。
【注】 WinAVRでは、予めポートアドレス等の定義がavr/io.hに組み込まれているため (実際は各種AVRコントローラの共通化するため、下層の sfr_defs.h や
iotn2313.h などに記述)、我々ユーザーはレジスタの実アドレス値を気にしないで済みます。ただしヘッダーで定義されているポートの更に下層のビット名定義などは細か過ぎて、必ずしもお薦めできないものもあります。同じAVRでも、ピン数が異なるデバイスでは必ずしも通用しない場合があるからです。ビット設定の際はデバイス毎のマニュアルを確認して記述すべきでしょう。
5-2 ソースファイルの記述
行の記述
C言語では基本的には改行や空白、タブは無視されます。ただし、
printf("ABCD");
とか、
printf ( "ABCD" ) ;
はOKですが
pri ntf("ABCD");
や
pri
ntf("ABCD");
のように書くことはできません。
C言語の実行単位
C言語の実行文の区切りは、改行ではなく ; (セミコロン)です。
a = b - 5;
のように、ひとつの文は ; で終わります。
1行の中に複数の文を書くこともできます。
a = b - 5; c = a * 2;
値の表現 例
10進表現 0, 128, 18500
2進表現 0b1100 , 0b00101000
16進表現 0xff , 0x01040ffc
文字 'A' 'B' 'C' 'a' 'b' 'c' '0' '1' など
文字列 "ABCD" "Hellow" "東京" など
特殊文字の表現(エスケープ・シーケンス)
文字として表すことができない特殊文字を表すのがエスケープシーケンス文字です。例えば、画面を改行したいときのコードは16進で、(OD OA) ですが、対応する文字がありません。C言語では、"ABCD\n"という文字を出力することで、「ABCD改行」を実行できるようにしています。
【注】 ちなみに、\記号は英語PC(日本語PCであっても英表示されるソフト場合)では、左上から右下への斜線のバックスラッシュで表現します。
エスケープシーケンス文字
\n 改行
\t 水平タブ
\\ 文字としての\
\? 文字としての?
\' シングルクォーテーション'
\" ダブルクォーテーション"
\0 ヌル(値0)
\x1f 16進数で0x1f
例えばROMの中のpaternというアドレスに 16進データ列 「E7 A5 BD 99 DB 5A 7E 66」 を生成する場合は
const char *patern = "\xe7\xa5\xbd\x99\xdb\x5a\x7e\x66";
のように宣言と同時に初期化することができます。
コメント文
/* と */ で囲まれた総ての文字は、プログラムではなく注釈とみなされます。プログラムを見やすくするために使います。何行に渡っても構いません。
なお、 /* 、、、/*、、、*/ 、、、*/ のように多重入れ子になってコメントも受け付けるCコンパイラもありますが、ダメなものもありますので、基本的には使わないほうが無難です。
// から改行までの文字も同じくコメントとみなされます。
5-3 define文
C言語はコンパイルを実行する前に、前処理として#で始まるプリプロセッサ文を解析します。
#define 文は、
#define ABC 0x10
のように定義すると、コンパイラは ABC という文字列が出てきたら 0x10 に置き換えてコンパイルします。
#define ZZZ (ABC + 0xf0)
このように多重に定義されてもOKです。文字列全体はカッコで囲みます。
引数使う #define 文
#define ANS(a,b) ((a)*(b)+10)
上のように#define文に、複数の引数a.bを渡すことができます。個々の引数はカッコで囲んだ方が安全です。
#define bit_rv(data,bit) ((data) ^ (bit))
はデータの指定ビットを反転します。
5-4 データ型
C言語で使えるデータのサイズ
型 サイズ
char 1バイト (-128から127)
int 2バイト (-32768〜32767) または4バイト
short 2バイト
long 4バイト (-2147483648〜+2147483647)
float 4バイト (1.17549e-38〜3.40282e+38)
double 8バイト
unsignedを付けると符号なしの範囲になります。
unsigned char (0〜255)
unsigned int (0〜65535)
unsigned long (0〜42944967295)
(符号付データは、signed long のように明示しても構いません。)
【注1】ただし int 以上のデータのサイズはコンパイラによって異なっている場合があります。
【注2】WinAVRでは、以下の定義済みのデータ型が使えます。
uint8_t 8ビット符号無し整数
uint16_t 16ビット符号無し整数
uint32_t 32ビット符号無し整数
int8_t 8ビット符号付き整数
int16_t 16ビット符号付き整数
int32_t 32ビット符号付き整数
特に、データ型を変換するキャストでは、
unsigned char b, unsigned int w;
b = (unsiged char)w;
ではエラーがでますので
uint_8 b; uint_16 w;
b = (uint_8)w;
のようにします。
【注3】距離(mm)の値を使って、ステッピングモーターのステップ数やクロック数に変換するには次のようにします。
ただし、ステップ数は16ビット、ステップ当たりの距離は1/1000mm単位とします。
#define L_BASE 125 // 1000クロック当たりの移動距離 (0.125 mm/clock)
#define L1 284 // 設定距離(mm)
距離L1に相当するクロック数は以下の値を用います。
(uint16_t) ( (uint32_t) L1 * 1000 / L_BASE )
この例では、定数L1で搬送されるステップ数は、284*1000/125 = 2272 ステップ (逆算すると 0.125*2272=284)
0.125mmのような小さな値はそのままでは扱えないので、1000倍した値で計算し、元に戻しています。1000倍した値は32ビット幅で計算し、割り算後にキャストで16ビットに戻しています。
このような方法は、プロセッサの中で負荷の大きな浮動小数点計算を実際に実行するのではなく、コンパイラの中で処理してしまう、より効率的な方法と言えます。
型と名前の宣言
変数は使用前にデータ型と名前を宣言しておきます。
unsigne char d; /* 符号なしバイト型変数dを宣言 */
int n1,n2; /* int型変数 n1とn2 を宣言*/
unsigned char dt[10]; /* 配列変数 dt[0]〜dt[9]の宣言 */
unsigned int d[3][4]; /* 2次元配列変数 d[0][0]〜d[2][3] の宣言 */
【注】別のソースファイルで宣言している変数は
extern char m_psw;
のように 外部で宣言していることを、コンパイラに伝えます。
#typedef 文
変数の型宣言は unsigned char のように、やや長いため
#typedef unsigned char uchar;
#typedef unsigned long ulong;
#typedef voletile uchar vuchar;
のように変数の型をtypedefして使う方法があります。勝手に再定義できて便利ですが、あまり勝手に命名すると混乱の元になりますので薦められません。
アセンブラでプログラミングした経験のある人は分かると思いますが、単に8ビットデータしか扱わないならまだしも、各種データを取り扱っての演算はとても大変な作業です。出来合いをサブルーチン化しておいても、データ長を取り違えたりした場合、エラーを発見するだけで一苦労です。Cの場合は使う前に変数の型を宣言しておくだけで、多くミスから救われることになります。
5-5 関数
C言語では、ひとつのまとまったプログラム単位に名前を付けて、「関数」と呼ばれる形式で、プログラムを組み立てていきます。Cコンパイラの中に予め用意されている関数もありますが、総て自分で作ることも可能です。これこそがC言語の最大の特徴で、他の言語は、用意された命令を一切使わないでプログラミングすることなどは、あり得ないことですが、Cではそれが可能なのです。むしろ、それが故に他の言語では到底不可能なプログラムを構築することもできるのです。
「関数」は普通(数学)で使われる言葉ですが、その昔、カンスウは「函数」とも書かれ、函(空の箱ではなく、中に役に立つものが入っている箱の意)の如く、機能(function)を持った箱ということになります。
数学では値 y がパラメーターである位置 x と時間 t によって変化する時、 y = f(x,t) のような関係式であらわしますが、c言もこれに似た方法で関数を作ることができます。
int add( int , int); // @ 使う関数のプロトタイプ宣言をします。
int main(void) // A main はプログラムをスタートする関数。
{ // B 関数は大括弧で括ります。
int a,b,ans; // D 内部で使う変数a,b,ansを宣言する
a = 25; // E 変数を初期化;
b = 18;
ans = add(a,b); // F 関数addを呼び出してその返値をansに代入
,,,;
} // C main関数のおしまいの大括弧
int add( int x, int y ) // G 関数 add の宣言 (返値はint、引数はint x、int y)
{
int z; // H 関数addの内部で使う変数zを宣言
z = x + y; // I 式
return z; // (11) 返値としてint型のzを返す
} // 関数addのおしまいの大括弧
Cで書かれたプログラムはmain() と書かれたプログラムの頭から開始することが決められています。
Gからは main() 関数とは別の関数 add()です。 関数 add() は、引数(パラメータ)として符号付き int 変数xとyを渡すと、int
変数が返ってくる関数であることを行Gは宣言しています。
返値型 関数名(引数型 引数名、引数型 引数名 、、)
{
プログラム;
}
なお、一番上に書かれた@は「プロトタイプ宣言」と呼ばれ、関数に使うデータ型の宣言です。実際の関数宣言に先立ち、関数に使う戻り値と引数のデータ型のプロトタイプを宣言しておきます。
返値や引数が無い場合は
void 関数名(void)
{
プログラム;
}
のようにします。
Hは関数 add() で使用される内部変数を宣言しています。内部変数は、後述しますが、この関数からプログラムが抜けて行くときは消えてしまう変数です。外部変数がRAMの固定のアドレスを占有してしまうのに対して、内部変数はレジスタまたはスタックメモリを一時的に占有するだけですから、RAMの負担になりません。
また、外部変数は名前がダブらないよう考えなければいけませんが、内部変数はiとかj,,d,のような良く使う変数を、他の関数で使用していても気にしないで使うことができます。
Iは演算式です。演算式は数学の式に似ています(演算の項参照)。
結果を、呼んだ関数に返す場合は、(11)のように return文で返します。
さて、main() 関数はどのように、子の関数を呼んでいるでしょうか。まず、Dのように、使う内部変数を宣言しています。同じ int 型を複数使う場合はカンマで区切って並べます。型が異なる変数は、別に書かなければいけません。
Eでは宣言した変数を初期化しています。宣言しただけでは内容が定まらないからです。
Fは初期化した a,b を引数として渡して関数 add() を呼んで、その返り値を左辺のansに代入しています。
関数を呼ぶときは、引数の順番と変数の型サイズが重要であって、変数の名称が変わっていても構わない、というのが大切なポイントです。ですから、@のプロトタイプ宣言文には変数の名称が無くてもいいのです。プロトタイプ宣言された関数のカタチと異なる使い方が出てくるとコンパイル時にエラーが発生します。
【注】 C言語で記述された統合開発環境下のマイコンシ・ステムの場合は、
int main(void)
{ }
のように書かれた、空のプログラムでも、コンパイラは最低限の環境プログラムを自動的に生成しています。それは、
ベクトル・テーブル (リセットや、各種割り込み発生時に実行されるアドレスの一覧)
リセット・プログラム (スタックポインタ設定後 main関数を呼ぶ)
です。統合開発環境を使わないCコンパイラでは、これらのプログラムは、アセンブラで書いて、Cプログラムとリンクしてやる必要があります。
【注】 ひとつの関数は大カッコ { } で括られますが、複数の実行文を { } で括ると、このブロックが実行単位になります。後述する if 構文などには欠かせません。C言語ではブロック毎に行の頭を揃えるのが無言の約束です。従来
void fnc(void){
実行文:
}
のような記法が本流ですが、私は、
void fnc(void)
{
実行文;
実行文;
{
実行文; 実行文; 実行文; //個別コメントが不要で明らかな一連の処理
{
実行文;
}
}
}
のような記法を使っています。構文が複雑になってくると、大カッコのタブ位置は大切な情報源であり、1行を惜しむべきではないと思います。
【cの関数と数学の関数の違い】
数学では関係式 y=3x+7において、x に何らかの値を入れると必ずyに値が返ってきます。xに値を入れず、yの答えも不用な数学というのはあり得ないのですが、cの概念ぶっ飛んでいて、
void buttobi(void);
という関数も十分役にたちます。それも当然で、
void motor_on(void)
{
、、、; /*モーター出力をon する記述*/
}
のように、モーターをONする働きの関数は、引き渡す値も返値も特に必用ではありません。
一方、後に詳しく出てきますが、
char func( *adr)
{
、、、
}
のように、必用な先頭アドレスを引き渡すことで、そのアドレス以下、0が出てくるまで、幾つものアドレスが指し示すにテーブルを使って超複雑な計算を指示する関数も書くこともできます。複数の計算結果を指定アドレス群の中に残して、計算が正常終了したら1を返し、エラーになってしまったら0を返す、というように使われることもあります。
5-6 メモリの種類と用途
プログラムを含む、あらゆるデータが、書換え可能な領域に存在するパソコンと違い、マイコンの場合はメモリの性質を意識して使い分けることが大切です。
ハード上から見たメモリの種類: 用途
------------------------------------------------------------
レジスタ: 演算結果の一時保管/データ転送の中継
通常、C言語ではレジスタを指定して使い分けることはしません。Cコンパイラが自動割り振りしてくれます。
SRAM: 書換え可能なデータで、長く保持するデータ
関数の外で宣言した
外部変数は固定したRAM領域が使われます。
SRAM: 書換え可能なデータで、一時的に使うデータ
関数の中で宣言した変数は
スタック・メモリが使われ、関数の外に出ると消滅します。
ROM: 書換え不可なデータで、プログラムや定数データ
関数の外で、
宣言と同時に値を初期化したデータはROMに生成されます。
スタック・メモリ
帳票や伝票の整理のしかたには二通りあって、第一の方法はデスクの上の書類トレイに重ねて置く方法、そして二番目の方法はインデックスの付いたファイルに綴じる方法です。私達は、回ってきた回覧板や、メモ書きをファイルに綴じてしまうことはありません。一方、保険証書や帳簿台帳を、山積みにしておくことも、普通はしません。
マイコンのメモリも同じで、大切なデータは、後述の外部変数を使って、特定のアドレスのメモリに格納しますが、一時的なデータは、関数の内部で宣言することで、積み上げ式のスタック・メモリが使われます。
マイコン内部にはスタックポインタというレジスタがあって、積み上げたスタック・メモリのアドレスをカウントする機能があります。データを積み上げるとスタックポインタは自動的に加算され、取り出すと自動的に減算されます。積み上げられたスタックのメモリ番地は、レジスタ同様、Cコンパイラが管理してくれるため、我々が意識することなく、大量のデータをスタックしたり、取り出したりすることができます。しかも、作業が終わった後は、スタック・ポインタが元の位置まで下がるため、後片付けの必要もありません。
【注】 C言語で記述された統合開発環境下のマイコンシ・ステムの場合は、
int main(void)
{ }
のように書かれた、空のプログラムでも、コンパイラは最低限の環境プログラムを自動的に生成しています。それは、
ベクトル・テーブル (リセットや、各種割り込み発生時に実行されるアドレスの一覧)
リセット・プログラム (スタックポインタ設定後 main関数を呼ぶ)
です。統合開発環境を使わないCコンパイラでは、これらのプログラムは、アセンブラで書いて、Cプログラムとリンクしてやる必要があります。注意)ただし、ATtiny2313のような容量の小さなコントローラーは、RAM全体でも2Kバイトしかないため、スタックへのデータを積み上げと、割り込み等が重複すると、外部変数領域とオーバーラップしてしまう危険性がありますので、注意しなければなりません。
ローカル変数、グローバル変数、 valatile 変数
関数の中で一時的に使う i とか j のような変数は、
void fnc(unsigned char *buf)
{
unsigned char i, j ;
for (i=0 ; i<7 ;i++ )
}
for (j=0 ; J<4 ; j++ )
}
,,,
}
}
のように関数の中で宣言すると、この変数は、関数の外では消えてしまう一時的な変数となります。従って、別な関数の中で同じ i とか j という変数を使っても干渉することはありません。これをローカル変数といいます。
一方、電源が入っている間は、ずっと保持していたいデータがあります。一般的にいうフラグは、この代表的な例でしょう。また割り込みとメインの双方で操作したり参照するデータは、一時的なスタックメモリではなく、安定なメモリ領域に変数を確保する必要があります。このため、関数の内部で変数を宣言するのではなく、関数の外で宣言します。これをグローバル変数(外部変数)と呼びます。
サンプル・プログラムの外部変数宣言の部分に書かれた5つの変数は何れも安定したメモリ領域に確保されるグローバル変数です。関数の外で宣言するだけでなく、static
を付けて
static unsigned int xyz,
のように積極的にグローバル変数を宣言すると、複数のファイルからもこの変数を使うことができるようになります。
static と似ているようで異なるのが volatile です。
volatile unsigned char sw_bf;
は「符号無しのキャラクタ(8ビット)型変数 sw_bf を、最適化しないメモリとして扱う」 ことを意味します。
volatile は「揮発性」を意味する言葉で、ガソリンやアルコールなどの揮発性物質の取り扱いに注意を促す警告に使います。この volatile が使われる代表的なオブジェクトには、
1)入出力ポートなどの特殊機能レジスタ
2)メインと割り込みの双方からアクセスされるメモリ
があります。これらの用途におけるプログラムでは
volatile 変数;
または
volatile static 変数;
の宣言をすることで、その変数はコンパイラが最適化のために不要と判断してしまうような変数に対する操作も確実に実行するようになります。例えば、ポートBから変数datに入力を繰り返すような場合、単に
unsigned char dat;
と宣言 しただけでは
dat = PINB;
dat = PINB;
dat = PINB;
のようなプログラムは無意味として飛ばされてしまう危険性がありますが、
volatile unsigned char dat;
と宣言することで、3回の入力が実際に実行されるようになり、この変数を割込み監視するようなプログラを書いた場合でもこれが有効になります。
ROM化データ
const unsigned char dt[] = "ABCD";
const char *ptn = "\xe7\xa5\xbd\x99\xdb\x5a\x7e\x66";
関数の外で、上のように、
const宣言と同時に値を初期化した場合、文字列"ABCD"や 16進データ列 E7,A5,BD,99,, はROMの中に書き込まれるため、電源を切っても消えず、書換え不可な変数になります。 以下の例もROM化データです。
const unsigned char tbl[] = { 'A','B','C','D','E','F','\\' }; /* 1次元配列定数
tbl[] の宣言と値の初期化*/
const int dt[][3] = /* 2次元配列定数 dt[][]の宣言と値の初期化 */
{
{ 56 , 100 , -12 },
{ 8 , 1200 , 0 },
、、、、
{ 87 , 654 , -13 }
};
【注】 C言語で記述された統合開発環境下のマイコンシ・ステムの場合は、
int main(void)
{ }
のように書かれた、空のプログラムでも、コンパイラは最低限の環境プログラムを自動的に生成しています。それは、
ベクトル・テーブル (リセットや、各種割り込み発生時に実行されるアドレスの一覧)
リセット・プログラム (スタックポインタ設定後 main関数を呼ぶ)
です。統合開発環境を使わないCコンパイラでは、これらのプログラムは、アセンブラで書いて、Cプログラムとリンクしてやる必要があります。
実は、H8のようなプロセッサは上記のような方法で簡単にROM化ができますが、AVRプロセッサのようなハーバード・アーキテクチャのプロセッサは、プログラム領域とデータ領域が分離されているため、Cコンパイラは、プログラム領域の初期化されたデータを、SRAM領域にコピーして、これを使って動作するようなプログラムが生成されてしまいます。
RAM領域が有り余っている場合は構いませんが、小さなシステムではSRAMを無駄使いしてしまいます。これを避けるためには、WinAVRの場合、以下のようにします。
#include <avr/pgmspace.h> //マクロを使うために必要なヘッダ
// 組み込みマクロPROGMEM を使った値の初期化の定義
PROGMEM unsigned char tbl[] = {1,2,3,4,5,6,7,8,9}; //数値テーブルのマクロ
PROGMEM char *PM_ON_ERR = "E01: PM ON "; //文字列のマクロ
// マクロ化されたバイト型データを使う場合は
int main(void)
{
char d;
d = pgm_read_byte(&tbl[5]); //使うデータをRAMから解放
pgm_read_byte(PM_ON_ERR); //文字列をRAMから解放
,,,,
dspmsg(L1,PM_ON_ERR); //ROMの文字列を使用
のようにします。
16ビット、32ビットの場合は以下のようにします。
pgm_read_word();
pgm_read_dword();
ただし、Atmel Studio 7 では、 PROGMEM を使うとエラーになるので
const char *PM_ON_ERR = "E01 PM ON ";
のようにしないといけないようです(先々修正されるかも知れませんが、、)
5-7-1 演算
変数や定数を演算記号を使っていろいろ演算することができます。 ここではa,b,cは変数とします。
2項演算
a = b + c; 加算
a = b - c; 減算
a = b * c; 乗算
a = 18 * c; 乗算
a = b / c; 除算
a = b % c; 余り (a ← b/cの余り)
例) a = (3+a) * (c-14); カッコを使って式の優先順位を整理する.。多重カッコも可
単項演算
a++, aを+1(インクリメント)
a--; aを-1(デクリメント)
b = -a ; aの符号を反転した値をbに代入
後置と前置演算の違い
++a; や --a; は前置
a++; や a--; は後置の演算子と呼ばれます。
例えば
b = 5;
a = b++;
この場合、a には5が入りますが
b = 5;
a = ++b;
では、aには6が入ります。
代入演算
a = b; a ←b bをaにコピー
必ず右辺から左辺に代入
a = 25; 定数を代入
例)a = b = c = d*2; 変数dの倍の値を、c,b,a にコピー
自分自身との代入演算
a += b; ← a = a + b;
a -= 2; ← a = a - 2;
a *= 2; ← a = a * 2;
a /= 2; ← a = a / 2;
a %= 2, ← a = a/2 の余り
ビット演算(ビット単位で演算)
a = b & 0x01; a ← b と 0x01のビットAND
例では bのビット0が1ならaには0x01が入り、bのビット0が1なら0x00になる。
この使い方はビット0以外をマスクしてビット0を調べるような時に使います。
a = b | 0xfe; a ← b と 0xfe のビットOR
ビット7〜1の全ビットが1になります。ビット0はbのビット0と同じです。
a = b ^ 0x01; a ← b と 0x01 のXOR(イクスクルーシブOR)
XORは二つの値が異なっているときにのみ1になる論理です。
例では、bのビット0が反転してaにセットされます。信号を反転したりする時に使います。
a = b << 2; a ←bを2回左にビットをシフトした値
a = b >> 1; a ←bを1回右にビットをシフトした値
a = ~b; a ← bのビットを反転した値
例) d = ~((1<<A) | (2>>B) | (1<<C));
定数Aを1回左シフトした値と、定数Bを2回右シフトした値と、定数Cを1回左シフトした値とをビットORした値を反転してdに代入
自分自身とのビット代入演算
a &= 0xf0; ← a = a & 0xf0;
a ^= 0xf0; ← a = a ^ 0xf0;
a |= 0xf0; ← a = a | 0xf0;
a <<= 2; ← a = a << 2;
a >>= 2; ← a = a >> 2;
論理演算と条件判断
論理演算は条件が成立しているか否かを調べるとき if文で使います。
Yes(0以外) か No(0) かで判断されます。
また、この時よく使われるのが比較式または論理式です。
> 大
>= 大または等しい
< 小
<= 小または等しい
== 等しい (代入文の=と区別するために二つ重ねます)
!= 等しくない
&& かつ
|| または
! ではない(否定)
こんなふうに使います。
if ( a>3 && b==5 ) // もし、aが3より大で、かつ、bが5なら
{ 実行文; ;; }
if ( !a || (b<=5 && c>d) ) // もし(aが0でない)または((bが5以下)かつ(cがdより大なら))
{ 実行文; ;; }
if (a) { } // aが0以外なら
if (!a) { } // aが0なら
式の評価には優先順位がありますので、長い式にはカッコを使って間違いを防ぎます。間違いやすいのは、=記号と==記号です。右辺から左辺へ値を代入する=と、値を判断するだけの
== ではまるで異なった働きを持つからです。
ビット論理操作
特定のビットを0にクリヤする
a &= 0b01110111; bit7とbit3を0にする。
特定のビットを1にセットする
a |= 0b10001000; bit7とbit3を1にする。
aの特定ビットをbからコピーする。
特定ビットが bit2 と bit1の場合
@ a &= 0b11111001; aから不用なビットを0にクリヤし
A b &= 0b00000110; bの必用なビット以外は0にクリヤし
B a |= b; aにbを合わせる
注)特定ビットを#defineすると反転ビットは ~ で表記可
5−7−2 演算子の優先順位
演算子の優先順位と評価順序
優先順位 |
演算子 |
用法 |
名称 |
評価順序 |
1 |
[] |
a[b] |
添字演算子 |
左→右 |
() |
a(b) |
関数呼出し演算子 |
. |
a.b |
ドット演算子 |
-> |
a->b |
メンバ選択(アロー)演算子 |
++ |
a++ |
後置増分演算子 |
-- |
a-- |
後置減分演算子 |
2 |
++ |
++a |
前置増分演算子 |
左←右 |
-- |
--a |
前置減分演算子 |
& |
&a |
単項&演算子、アドレス演算子 |
* |
*a |
単項*演算子、間接演算子 |
+ |
+a |
単項+演算子 |
- |
-a |
単項-演算子 |
~ |
~a |
補数演算子 |
! |
!a |
論理否定演算子 |
sizeof |
sizeof a |
sizeof演算子 |
3 |
() |
(unsigned char)a |
キャスト演算子 |
左←右 |
4 |
* |
a * b |
乗算演算子 |
左→右 |
/ |
a / b |
除算演算子 |
% |
a % b |
剰余演算子 |
5 |
+ |
a + b |
加算演算子 |
- |
a - b |
減算演算子 |
6 |
<< |
a << b |
左シフト演算子 |
>> |
a >> b |
右シフト演算子 |
7 |
< |
a < b |
比較<演算子 |
<= |
a <= b |
比較<=演算子 |
> |
a > b |
比較>演算子 |
>= |
a >= b |
比較>=演算子 |
8 |
== |
a == b |
等価演算子 |
!= |
a != b |
非等価演算子 |
9 |
& |
a & b |
ビットAND演算子 |
10 |
^ |
a ^ b |
ビット排他OR演算子 |
11 |
| |
a | b |
ビットOR演算子 |
12 |
&& |
a && b |
論理AND演算子 |
13 |
|| |
a || b |
論理OR演算子 |
14 |
? : |
a ? b : c |
条件演算子 |
左←右 |
15 |
= |
a = b |
代入演算子 |
+= |
a += b |
加算代入演算子 |
-= |
a -= b |
減算代入演算子 |
*= |
a *= b |
乗算代入演算子 |
/= |
a /= b |
除算代入演算子 |
%= |
a %= b |
剰余代入演算子 |
<<= |
a <<= b |
左シフト代入演算子 |
>>= |
a >>= b |
右シフト代入演算子 |
&= |
a &= b |
ビットAND代入演算子 |
^= |
a ^= b |
ビット排他OR代入演算子 |
|= |
a |= b |
ビットOR代入演算子 |
16 |
, |
a , b |
コンマ演算子 |
左→右 |
例えば、
a = *pt++;
のような式は ptの値が先ずインクリメントされ、次にptが指すアドレスの内容が変数aに代入されます。分解して書けば、
pt++;
a = *pt;
と同じです。これは後置き増分演算子++の優先順位が間接演算子*より高いためです。
なお、処理される順番に確信が持てず心配な場合は、
a = *(pt++);
のように( )を使って処理する括りを明示したり、上例のように分かち書きしたほうが、誤ってしまうよりマシという考え方もあります。
if文の中で演算する時は特に注意が必要です。
a = 0;
if(++a == 1 )
はYesが返されますが
a = 0;
if(a++ == 1 )
はNoが返されます。
5-8 制御文
制御文でプログラムの流れを分岐させることができます。
if、else 文
もし〜だったら〜する。というようにプログラムの流れを条件によって変える方法です。
if (a > 10)
{
b = 0; // 条件が成立した際に実行する全実行文を記述します。
}
もし a>10 ならbを0にする。
if (a > 10)
{
b = 0; // 条件が成立した際に実行する全実行文を記述します。
}
else
{
b = 0xff; // 条件が成立しなかった時に実行する全実行文を記述します。
}
もし a>10 ならbを0にする。そうでないなら、bを0xffにする。
if else で条件を多段にできます。
if ( ) // もし〜なら、
{
;;; // こうして。
}
else if ( ) // そうでなく、もし〜なら、
{
;;; // こうして。
}
else if ( ) // そうでなく、もし〜なら、
{
;;; // こうして。
}
else // そうでなかったら、
{
;;; // こうする。
}
while文
〜の間〜をする制御です。
a =100;
while ( a > 0 )
{
a--;
}
a が0より大なら、その間 a を-1し続け、0 より大でになかったら、抜けていきます。
while(1);
ここで止めてしまうとき使うプログラムです。
次の
do while文は doの中の文を先ず実行してから 次にwhile 文の評価をする構文です。
do
{
a--;
}
while( a>0 );
for文
初期化をして、条件が成立する間、動作を繰り返す制御です。
初期化; の間 ; をしながら
for ( i=1; i<10; ++i )
{
実行文; ;;
}
まず i は1にセットされ、i<10 の間、i が+1されながら、実行文が実行されます。
break文
for 文や while 文など繰り返し実行されているループから抜け出すには
for ( ループ条件式 )
{
if( 抜け出す条件式 )
break;
}
を使います。多重のループから抜け出すためには 面倒でも、ループ毎に同じ条件判断をして break;で抜け出す必用があります。
【注】実はC言語にも goto 文があるのですが、暗黙の内に使用禁止令が出ています。乱用するとスタックなどの回復が不可能になり、システムの暴走を起こすからです。
goto文を使わないでプログラムをジャンプする唯一の方法がリセットです。リセットに関しては
、 6-7 エラー処理 を見てください。
switch文
変数を制御スイッチに使い、一気に分岐する方法です。
switch ( a ) // 変数aをスイッチに使い
{
case 1: // a==1なら
実行単位1; // これを実行して
break; // 帰る
case 2: // a==2なら
実行単位2; // これを実行して
break; // 帰る
default; // aがそれ以外なら
実行単位d; // これを実行する
}
5-9 配列変数
unsugned char a[9];
のように宣言すると、並んだ10個の変数が使えることになります。
unsugned int abc[8][10];
のような変数は2次元配列です。
char a[4] = "ABC";
のように配列を宣言と同時に初期化すると
a[0]には'A'
a[1]には'B'
a[2]には'C'
a[3]には'\0' (C言語では文字列の終端はヌルが入る)
がセットされます
5-10 アドレスを使う
普通、プログラミング言語の主たる操作対象は変数の値です。プログラムは変数を演算したり転送する手順と言えます。この時、重要なのは変数の型と値であって、どこのアドレスに置かれるかではありません。必要となるメモリのアドレスを自動的に割り当て、管理してくれるのはそのプログラム言語の大切な機能です。
しかし、c言語は普通に変数を使うことは無論できますが、高級言語が一旦は手放した機能、プログラマーがメモリをアドレスで指定してアクセスするという手法をもう一度改めて持ち込んだ言語と言えます。この機能が役立つのは
@アドレスがハード的に決められたメモリ(I/Oレジスタなど)をアクセスする
A連続したアドレスにあるデータを効率的に取り扱う
B参照データをもとに間接的にデータをアクセスする
のような場合です。c言語は、アドレスのデータを何に使うかというと、「ポインタ」として使います。アドレスを指し示すためのポインタです。ポイントするアドレスは変数や配列のアドレスだけでなく、関数の開始アドレスをポイントさせることもできます。
5-11 ポインタ
変数に間接演算子
* をつけると、この変数は普通の変数ではなく、「アドレスを記憶する変数」となり。
ポインタと呼ばれます。メモリの目的番地をポイント(指し示す)働きをするのでポインタです。
使い方は、
char d; char型変数dを宣言
char *p; char型変数のアドレスを記憶する
ポインタのpを宣言
p = &d; 変数dのアドレスをpにセット。 これでポインタpは使えるようになります。
*p = 35; ポインタpが指し示すアドレス、つまり変数dに35を代入
のようになります。これは、
char d;
d = 35;
とまったく同じ結果、つまり、変数dに値を代入するだけのつまらない操作ですが、もし続けて
p++;
*p = 35;
としたら、どうなるでしょうか? これは、変数dの次のアドレスのメモリにも35を書き込むことを意味します。変数dには名前がありますが、変数dの隣のメモリには名前も付いていません。それでも、ポインタをシフトするだけで自由にアクセスできてしまいます。
変数を直接扱うのではなく、アドレスを使ってメモリを操作する、この機能は、Cを極めて便利で、そして危険な言語へと変えてしまいました。
ところで、上で宣言されたポインタ p のデータ型は何型でしょうか? 「char *p」 だからバイト型でしょうか? これがCに誤解を与え易い問題点で、
「*p」 はバイト型データを扱う変数だから char *p; のように宣言しますが 変数 p 自体はメモリ・アドレスを入れる変数なので、普通はバイト型ではありません。(ポインタのデータ型は、コンパイラが自動的に割り当てます)
よく、「ポインタ *p 」と言い勝ちですが、正しくは「ポインタ 変数p」です。 「*p は、ポインタ変数pが指し示す普通の変数」ということになります。
*p には普通の値が入りますが、ポインタ変数 p にはアドレスの値を入れて使います。
C言語はポインタが難しいと言われますが、その一因はアドレス表現と記号に、やや問題があったのではないか、と思われます。下例は何れも、関数の引数渡しに使えるアドレスなのですが、表現方法がいろいろあって、
&d :変数dのアドレス
"ABC" :文字列"ABC"の先頭アドレス
d :配列d[]の先頭アドレス
&d[0] :配列d[0]のアドレス
fnc :関数 fnc() を呼び出すアドレス
int * :int型変数を指し示すアドレス
の何れもがアドレスを表しているのですが、表記の仕方に統一性がなく混乱します。最後の例は宣言の仕方を
int* p;
のように書く方法があるようです。この方がピンと来ますが、 int* 型のデータがある訳では無いので、ポインタ変数を二つ宣言するつもりで、
int* p , q ;
のようにすると、まったく異なった意味になってしまいます。何れにしても、今となっては仕方ないため、慣れるしかありません。
ポインタ 例1:
文字列を表示する有名な関数 printf() は
printf("ABCD");
文字列"ABC"の先頭アドレスを引数として、関数 printf() に渡しています。これは、文字列の内容”ABC”を渡しているのではありません。 printf()
関数は、渡された最初のアドレスをポインタにセットして、ポインタを次々とシフトしながら文字を表示し、0が来たら終了する関数です (なお、cコンパイラは文字列の終端に自動的に
0 (NUL)を生成します)
ポインタ 例2:
関数のポインタ渡しの簡単な例
void fnc(int *p) // 関数 fnc()を宣言 、引数はポインタ変数の値(アドレス)
{
*p *= 2; // ポインタで示す変数の値を×2
}
void main()
{
int d; // メインで扱う変数 d の宣言
d = 3; // d に元の値を代入
fnc(&d); // 関数 fnc() に引数(変数dのアドレス)を渡して呼び出し
//結果、dは 6 になる。
,,,,
}
ポインタ 例3:
連続したメモリ領域を同じデータで埋め尽くす関数
void fill(unsigned char *pt , unsigned dat , unsigned int len ) // fill関数の宣言
{ // 引数はポインタ変数のアドレス、データ内容、個数
do //繰り返し
}
*pt++ = dat; // ポインタのアドレスを+1して、そのアドレスへ先へdatを転送
}
while (--len); //繰り返しカウンタが0になるまで
}
int main(void)
{
unsigned char buff[0xff]; //使用するバッファを配列変数として宣言(スタック使用)
fill( buff , 'A' , 100 ) //fill関数にbuffの先頭アドレス、データ、個数 を渡して呼び出し
// 結果→ 100個のアドレスに"A"を書き込み
,,,
}
【注】 上例プログラムはスタック領域が十分広いプロセッサに限定されます。
ポインタを使ってメモリやポートをアクセスする:
C言語は、アドレスが割り当てられた入出力ポートを次のように、アクセスすることができます。
*(char *)0x18 = 0x03; // ポート0x18番地へデータ0x03を書き込む
これは、(char *) という 強制的な型変換
キャストを使って、"0x18" が”バイト型ポインタ”、つまり、アドレスであることを指定しています。もし、
*0x18 = 0x03;
ならどうでしょうか? *0x18はアドレス 0x18番地 に置かれたポインタであることは分かりますが、それが、8ビット型データを示すポインタか、32ビット型データを示すポインタなのか分からないため、エラーになってしまいます。
なお、使う度に直接ポートアドレスを記述するのは面倒なので、ヘッダーファイルの中では
#define PORTB (*(volatile unsigned char *)0x18)
のように、必要なI/Oアドレスをdefineしてますので、以降 PORTB = ○○; のように使うことができます
メモリ・アドレスをアクセスする際でも同様で、以下のようにします。
dt = *(volatile signed int *)0xffcf80; // 変数dtへ0xffcf80番地から符号付16ビットデータを読み込み
なお、メーカーが用意するヘッダーファイルは長大で複雑な構造をしていますが、上例のような単純なポート・アドレスの定義だけでも十分使いものになります。メーカー製のヘッダーでは共用体(union)などが使われるケースが多いようですが、こういった高度なテクニックを採用すると、異なったプロセッサなどに移植する際は、逆に困難さを増してしまう場合もあります。
直接アドレスを呼出す:
特殊な使い方ですが、例えば、リセットアドレスを呼出すことで強制的にシステムをリセットする方法が
あります。下はベクトルテーブルの直下のリセットスタートアドレスが 2a 番地の場合のリセット関数です。
((void(*)(void))0x2a)();
次の記述も使えます。
((void(*) ())0x2a)();
コンパイラは次のように解釈しているようです。
(); 関数を呼出す、
0x2a という値を次のようにキャスト(型変換)でアドレスにして
キャストは
void 返値の無い
() 引数の無い
(*) アドレスである (カッコが無いとエラー)
下は、アセンブラへ出力で、確かに絶対番地2aが呼び出されています。
ldi r28, 0x2A
ldi r29, 0x00
movw r30, r28
icall
配列とポインタの関係
配列は複数の変数、ポインタは変数のアドレスを記憶する、ひとつの変数ですから、全くの別物なのですが、似たような使い方ができます。
int x[10], *p;
p = x;
のように、ポインタpのアドレスに、配列xの先頭アドレスをセットしてやると、
x[0] と *p は同じ
x[9] と *(p+9) は同じ内容になります。
5-12 論理判断の速度
プロセッサが最も素早く条件判断できるのは、「
0か?、0以外か?」です。 「変数を+1しながら、結果が1200と等しくなったか?」の判断より、「最初に変数へ1200をセットし、繰り返し-1の結果が0か?」の方がプロセッサは得意です。論理判断も0以外が1(YES)で、0がNOです。 論理判断は往々にして繰り返しループの中に入れられるため、なるべく「0か?、0以外か?」の判断の方が、高速で単純明快なプログラムになります。
if( in_hp() ) { // ホームポジション・センサーに入力があったら何れかのビットが1になる関数をテストする
if ( !in_hp() ) { // ホームポジション・センサーに入力がなかったら
if (!x--) { // x-1が0だったら
while (*pt++) { // インクリメントしたptが指し示すデータが0でない限り
5-13 構造体
配列変数は、同じ型の変数しか扱えませんが、異なったデータ型の集合を新しいデータ型として定義して扱うことができます。このデータ型を構造体といいます。変数の型を定義する typedef文といっしょに使います。下の例は表示する関数の都合で、たまたま同じunsigned
charを使いましたが、long int,char,,,などを混合してtypedefすることができます。
typedef struct //構造体の型を定義
{
const unsigned char name[10]; //氏名
const unsigned char age[4]; //年齢
} meibo; // この形式の構造体を meibo と名付ける
meibo d1 = {"KOBAYASHI"," 56"}; // データ型meiboでd1を初期化
meibo d2 = {"NAKAMURA "," 12"}; // データ型meiboでd2を初期化
int main(void)
{
init();
while(1)
{
clr_lcd(); //LCDのL1行とL2行をクリヤ
dspmsg(L1,d1.name); // d1の名前表示
dspmsg(L2,d1.age); // d1の年齢表示
lptm(2000);
clr_lcd();
dspmsg(L1,d2.name); // d2の名前表示
dspmsg(L2,d2.age); // d2の年齢表示
lptm(2000);
}
}
5-14 コンピュータの構造 (ノイマン型とハーバード型)
5-6(メモリの種類と用途)でも触れましたが、コンピュータには大きく分けると、ノイマン型とハーバード型に別れます。
上の例図に見られるように、その大きな違いは、アドレス空間の構造の違いです。ノイマン型は基本的に、命令プログラムを保存するアドレス空間と、データメモリのアドレス空間を区分しないのに対し、ハーバード型は別に取り扱います。それぞれ一長一短があるのですが、アドレス・バスを簡素にできるため小型のマイコンではハーバード型が採用されている場合が多いようです。ただし、コンパイラ側からみれば、基本的にシンプルなノイマン型の方に軍配が上がるかもしれません。
何れにしても、時折この構造の違いは、特にROM化が前提となる機器組み込み用のマイコンでは重要な違いになってきますので心に留めておく必要があります。
5-15 C言語
ここまで来て改めて、C言語とは何なのかを考えてみます。それには、Cと他のコンピュータ言語の違いを比べて見るのが一番いい方法です。
アセンブラを使うプログラミングの作業においては、8割以上の労力を、レジスタの遣り繰りに費やすことになります。BCレジはループカウンタ、DEは転送先アドレス、HLは転送元アドレスに使っているから、えーと、ここで例の参照テーブルを使うとすると、何をどこに一旦セーブして、どうすればいいのか、えーと
(;^_^
といった具合です。コンピュータの中核を成すCPU(中央処理ユニット)の中には、汎用目的レジスタ群、プログラムカウンタ、スタックポインタなどの重要なレジスタがあり、アセンブラ命令はこれらの中身を自由に操作することできます。
一方BASICやFORTRANなどの高級言語は、各種関数演算機能を予め持ち、大規模な変数を自由に操れる便利さがある一方、レジスタ操作どころか、別なポートに入出力を切り替えることもできません(少なくとも並の手段では)。
C言語は、CPU内のレジスタ群の操作をコンパイラに委ねただけの、そして、関数機能を本来、一切備えていない、ピュアな言語と言えます。 アセンブラを使えば、レジスタやスタックを確かに効率的に読み書きができるのですが(ただし名手がプログラッミングした場合に限り)、プログラミング作業の中でこれらの場所を頭の中で管理するのは大変過ぎる作業なのです。Cコンパイラはその点、後先のことは考えず、今出てきた変数は取りあえず空いているレジスタを使って、というように目先の割り振りで作業を進めます。しかしコンパイラの優れているところは、混乱も忘れることも無いことです。変数がどれ程増えようが、スタックの下に埋もれた値であろうが、コンパイラが自動的に振った仮ラベルは間違いなく引っ張り出されてくるのです。
C言語で書かれたプログラムの中で唯一、融通の利かない記述は入出力に係わる、例えば PINA や PORTB のような記法だと思われ勝ちですが、これでさえ、ヘッダーファイルの中で単にSFR(特殊機能レジスタ)のアドレがdefineされている作用効果でしかないのです。つまり、文法で使う言葉以外に予約された関数など何も無い、実にシンプルな言語なのです。このことが、Cを実に柔軟な言語にしている舞台裏でもあります。ポートアドレスやSFRアドレスが変わっても、ちょっとヘッダーファイルを書き換えるだけで対応可能なのです。
そういった意味で、C言語はやはり、レジスタ操作を断念したことで逆に、プログラマーを解放することに成功したシンプル言語だったのです。そして、CPU周りのレジスタを除く全メモリやSFRも自由に操れる機能を残したが故に、実に危うい機能も自在にが使える言語となったわけです。
C言語でマイコンその3へ続く Top page