マイコンのプログラムをC言語で書く際に心がけるべきこと、知っておくべき知識について述べます。
目次
多くの C言語初学者は PC を使って C言語を学びますが、マイコンのプログラムでは、PC での C言語とは少し毛色が違うところがあります。
マイコンと PC の違いと心がけるべきこと
マイコンは「マイクロコンピュータ」の略であり、貧者なコンピュータです。それゆえ、マイコンのプログラミングでは PC でのそれとは異なり以下の特徴があります。
マイコンは PC と比べて CPU が貧弱
マイコンは CPU が貧弱です。
まず、動作周波数が違います。マイコンが 数MHz~数百MHz なのに対して,PC は 数GHz です。一般に動作周波数が早いほど高速に処理を遂行できるので、マイコンは PC に比べて 10~1000倍 遅いです。
次に、ビット幅が違います。多くのマイコンが 8ビットや 32ビットであるのに対し、PC は 64ビット です。ビット幅が大きいほど一度に実行できる計算の規模が大きいので、この点でもマイコンは PC に劣ります。
さらに、マイコンだと浮動小数点演算器とかが未搭載なことも多いです。浮動小数点演算器があるのと無いのとでは、小数の加減算・乗算の速さがまるで違います。PC に搭載されている CPU であれば確実に浮動小数点演算器は搭載されているでしょう。
CPU が貧弱なマイコンでは、常に 軽い動作で済むプログラム を書くように心がける必要があります。割り算や浮動小数点演算を乱発させるようなプログラムを書いたり、printf など複雑な処理が必要な関数を頻繁に呼ぶようなプログラムは避けるべきです。
マイコンは PC と比べて RAM・ROM が小さい
マイコンは RAM・ROM が貧弱です。
多くのマイコンは、ROM・RAM 共に 数KB~数十KBです。対して PC は RAMが 数GB あります。PC に搭載されているような CPU 中のキャッシュですら 数MB あるので、それと比較してもマイコンの RAM 容量は少ないです。
RAM・ROM 容量が小さいマイコンでは、とにかく コンパクトなプログラム を書くように心がける必要があります。プログラムが長すぎると、ROM が足りなくなります。配列が多すぎると、RAM が足りなくなります。
また、マイコンでは メモリの動的確保 や 再帰 を使うのは避けたほうが良いとされます。どちらも、RAM が十分にある環境でないと動かないからです。
マイコンには OS が無い
PC には OS がありますが、マイコンは OS が存在しない場合がほとんどです。
PC なら低レイヤーの処理は OS がガッツリ担当してくれるので意識する必要がほとんどありませんが、マイコンでは、C言語という高級言語を使ったとしても、IOレジスタをいじる ことで GPIO や タイマなどのハードウェアを直接操作する必要があります。
しかし、近年は Arduino や mbed などの抽象度の高いライブラリが用いられることも多くなり、IOレジスタをいじらなくてもマイコンが使えてしまいます。また、RTOS など、複数スレッドの処理に対応したプチOS もあります。
いずれにせよ、マイコンは、その貧弱さゆえ PC に比べると制約が大きいのです。
変数型とそのサイズ
一般的なC言語では、「整数は c💩int
型」が定石になっていると思います。しかし、色々と貧弱なマイコンの場合は、それではマズいです。変数型とそのサイズを意識する必要があります。
C言語 の変数型とそのサイズ
C言語 の一般的な変数型とそのサイズは、次のとおりです。
この表で注目すべきが、『※コンパイラ・処理系により変数のサイズは異なる』です。例えば、int
型は、VS C++ や STM32 などのマイコンでは 4バイト整数型として定義されますが、AVR では 2バイト整数型として定義されていた記憶があります。コンパイラや環境によって変数型のサイズが異なるというクソ仕様です。
このクソ仕様を避けるべく、stdint.h
というヘッダファイルで次のような変数型が定義されています。
stdint.h の変数型とそのサイズ
stdint.h
に定義されている変数型とそのサイズは、次のとおりです。
名前から、符号の有り/無しとそのサイズが分かってしまいます。素晴らしい。
マイコンのプログラムでは、特段意識せずとも stdint.h
がどこかでインクルードされていることが多いです。要するに Arduino.h
mbed.h
avr/io.h
stm32f3xx.h
など、stdio.h
的な位置付けのヘッダファイルをインクルードしただけで勝手にインクルードされているというわけです。
今後、マイコンで C言語を扱う際は、この変数型を使うことを推奨します。
マイコンは貧弱ゆえ変数型は小さくすべし
先ほど説明した通り、マイコンの RAM 容量は小さいです。従って、例えば最大値でも100に満たないような数を格納する変数を、2バイト/4バイト整数型である int
型で確保するのは RAM の無駄遣いと言えます。この場合、1バイト整数型である int8_t
型、正数だけなら uint8_t
型を用いれば十分です。変数が一つだけなら対して問題にならなさそうですが、これが要素数100~1000の配列となれば、RAM の消費量の差は歴然です。
また、8ビットマイコンの場合、サイズの大きな変数型を用いると、計算時間が余計にかかります。例えば 4バイト整数型の計算は、32ビットマイコンであれば一度に実行可能ですが、8ビットマイコンの場合は、1バイトごとに行われます。したがって、int8_t
型で十分な所で 2バイト/4バイト整数型である int
型を使っただけで、処理に 2~4倍の余計な時間がかかることになります。
32ビットマイコンの場合、4バイト整数型である int
型を用いても計算時間が変わらないですし、単独で 1~2バイト整数型を確保した場合でも、実は内部では 4バイト分の RAM/レジスタ を食っている説すらあるので無闇に小さな変数型を用いる必要はないかも知れません。しかし、配列などの変数型は、RAM 消費量を減らすように必要最低限の変数型を選択する必要があります。
変数の格納先
変数の格納先についてついでに述べます。変数は、汎用レジスタと呼ばれる領域か、SRAM のいずれかに保持されます。頻繁にアクセスする変数は、よりCPUに近い位置にある、小容量で高速な汎用レジスタに 保持されます。一方、配列など規模の大きなデータは、大容量だが少し低速なSRAMに保持されます。これらの配分は Cコンパイラが勝手にやってくれるので意識する必要がありません。これがアセンブリ言語と比較した高級言語「C言語」の良いところとも言えます。
ビット操作
C言語でのマイコンプログラミングにおける最大の特徴は、ビット操作を多用する ことでしょう。
2進数表記とビット
C言語では、0x
から始まる数値は、16進数表記と解釈されます。例えば、0x37D
は、10進数では $3\times 16^2 + 7\times 16^1 + 13 = 893$ となります。
gcc コンパイラや、2014年以降の C/C++ の仕様では、16進数に加え、2進数表記も可能です。0b
で始まる数値は、2進数表記と解釈されます。例えば、0b1100101010
は、10進数では $2^9+2^8+2^5+2^3+2^1=810$ となります。
ここで、「ビット」とは何を意味するのか説明します。ビットとは、数値を2進数表記したときの 1桁1桁に対応します。
今後「変数 A の nビット目」という言い方を多用します。例えば,2進数 0b1100101010
の2ビット目は,右,すなわち下位ビット側,すなわち下の桁から順番に 0ビット目の 0
,1ビット目の 1
,2ビット目の 0
といった具合です。一番下の桁は 0ビット目 であることに注意です。
ビット演算子
C言語には、+
-
*
/
%
という算術演算子があります。算術演算子と同じように、C言語にはビット演算子というものも存在します。
ビットごとの AND | & |
ビットごとの OR | | |
ビットごとの EX-OR | ^ |
ビットごとの 反転 | ~ |
左シフト | << |
右シフト | >> |
ビット演算子の具体的な挙動をみていきます。
ビットごとの AND
ビットごとの OR
ビットごとの EX-OR
ビットごとの 反転
~
はビットごとの反転を意味します。
例えば、~0b1100
は 0b0011
です。
左シフト
<<
は左シフト演算子です。A << B
と書かれていた場合、A の値を B ビットだけ左にシフトさせます。
例えば、0b10011010 << 3
は 0b11010000
です。
このように、n ビット左シフトさせると、空いた下位 n ビットには、上のように 0
が入ります。
実は、正整数の nビット左シフトは、$\times 2^n$ と同義です。昔は $\times 2^n$ の計算は 左シフト で書けと積極的に言われていたようです。
右シフト
>>
は右シフト演算子です。A >> B
と書かれていた場合、A の値を B ビットだけ右にシフトさせます。
例えば、0b10011010 >> 2
は 0b00100110
です。
左シフトと同様、n ビット右シフトさせた時は、空いた上位 n ビットには、上のように 0
が入ります。
実は、正整数の nビット右シフトは、$\div 2^n$ と同義です。
複合代入演算子
複合代入演算子は、代入演算子 = と算術演算子・ビット演算子( ~
以外)を組み合わせた演算子です。
複合代入演算子 | 同義 |
A &= B | A = A & B |
A |= B | A = A | B |
A ^= B | A = A ^ B |
A <<= B | A = A << B |
A >>= B | A = A >> B |
次に、これまで説明したビット演算子・複合代入演算子を使った便利な“公式”を3つ紹介します。
ビットセット
変数 A の n ビット目を 1 にしたい。どうすれば良いだろうか……
このようなシチュエーションでは、“ビットセット”と呼ばれる“公式”を使うと便利です。
A |= 1 << n;
この“公式”は、レジスタ操作をする過程で超頻繁に用いられます。“公式”として暗記しましょう。
ビットクリア
変数 A の n ビット目を 0 にしたい。どうすれば良いだろうか……
このようなシチュエーションにも、“公式”が便利です。
A &= ~(1 << n);
この“公式”は“ビットクリア”と呼ばれ、超頻繁に用いられます。暗記しましょう。
ビットチェック
「変数 A の n ビット目が 1 のとき、条件を満たす」といった条件式を書きたい。どうすれば良いだろうか……
こんな時は“ビットチェック”と呼ばれる次の“公式”が便利です。
A & (1 << n)
これを if 文や while 文の ()
の中に書けば OK です。
では逆に、「変数 A の n ビット目が 0 のとき、条件を満たす」という条件式は、
!(A & (1 << n))
となります。
それでは、ビットセット・ビットクリア・ビッチチェック を使ったプログラム例を示します。
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t A = 0; // A == 0b00000000
uint8_t B = 0xFF; // B == 0b11111111
A |= 1 << 3; // A == 0b00001000
B &= ~(1 << 5); // B == 0b11011111
if( A & (1 << 3) ) {
printf("Aの3ビット目は1なのでコレが表示される\n");
}
if( !(B & (1 << 5)) ) {
printf("Bの5ビット目は0なのでコレが表示される\n");
}
return 0;
}
IOレジスタ
PC との違いの説明でも少し触れましたが、マイコンは C言語という高級言語を使ったとしても、IOレジスタをいじる ことで GPIO や タイマなどのハードウェアを直接操作する必要があります。ただし、Arduino などの抽象度の高いライブラリを用いた場合はこの限りではありません。
IOレジスタについては、マイコンの中身をより詳しく知ってからの方が理解が進む気がするので、別の記事で具体例も交えて説明します。
その他の特徴
インクルードするヘッダファイルが違う
C言語では stdio.h
をインクルードすることが多いですが、マイコンでは違います。マイコンの場合 printf
する先である標準入出力ストリームすら存在しないことが多いので、当然といえば当然です。代わりに avr/io.h
stm32f3xx.h
など、マイコンによって独自のヘッダファイルをインクルードします。
main関数を終わらせてはならない
PC の場合 main関数が終わらないとプログラムが終了しない、すなわち main関数が永遠に終わらないと暴走とみなされますが、マイコンの場合は違います。マイコンの場合は main 関数を終わらせてはなりません。
#include "stm32f3xx.h"
int main(void){
/* プログラムはここに書く */
while(1){
}
return 0;
}
このように、main関数の末尾に無限ループを配置し、プログラムが終了しないようにする必要があります。
main関数が終了するとレジスタ総リセット等の予期せぬ挙動を示すという話を聞いたことがありますが、実際の所はわかりません。main関数の呼び出し元であるスタートアップルーチンの末尾に無限ループが挿入されている場合もあるようなので、main関数を終わらせた所でマイコンがぶっ壊れるわけでも世界が終了するわけでもなさそうです。ですが、慣例的にmain関数の末尾に無限ループは必ず挿入します。
なお、Arduino の場合には main 関数の代わりに setup と loop という関数があります。こいつらは実は main 関数から呼び出されており、無限ループが形成されています。
int main(void){
// なんか別の処理
setup();
// なんか別の処理
while(1){
loop();
// なんか別の処理
}
return 0;
}
割り込み処理の書き方
マイコンは割り込み処理の記述も他にない特徴だと思います。
この書き方はマイコンやライブラリによって異なりますが、例えば AVR マイコンのタイマ1の割り込み処理は、
ISR (TIMER1_OVF_vect) {
}
内に記述する、といったように決められています。