IoTエンジニアになって半年が過ぎたkazuphです。
(IoTって言葉はいつごろまで使えるんですかね(・∀・)?)
PerlやRubyでWebアプリケーションを書いていたエンジニアが、C言語でゴリゴリ組み込みプログラミングをすることになったときに、どんなことを知っていたかったかなと思い出しながら書いてみたいと思います。
その辺にある普通のC言語の入門書には載ってない知識が中心です。
ちなみにArduinoの例が多いですが、実際にはほとんどの期間を某チップの某SDKを使って開発しました。
わかる方はそれを想像しながらだと面白いかもしれません(・∀・)
半年の半分はiOSアプリ作っていたので、実質3ヶ月くらいの知識だと思ってください。
間違いの指摘など是非々々お待ちしておりますm(_ _)m
こんな記事も書いてます。
すべてはmainから始まる、と思ったらSDKによっては隠蔽されていることもある
int main(void) { /* ... */ }
大抵はmainを探してそこから処理を追えばだいたい読めます。
そのはずなのですが、実際には、自分が選んだチップのSDKではmainが隠蔽されていたので、まったくこの知識は役には立ちませんでしたが、基本なので知っているといいと思います。
処理はwhile文の中に書く、がSDKによっては隠蔽されている
SDKに隠蔽されているとわかりづらいですが、APIとしてコールバックだけ定義されている場合は、そのイベントの発生源はどこかにあるwhile文です。
結局はwhileの中に常に何かを監視している処理があって、コールバック関数が呼ばれているだけです。
void setup() { /* 初期設定などをする */ }
void loop() { /* センサーの値の監視など継続的に行いたい処理を書く */ }
int main(void) {
setup();
while(1) { loop(); }
}
Arduinoだとsetupとloopとありますが、イメージは↑みたいになっているはずです。
が、自分が使ったチップのSDKではメインのwhile文は隠蔽されており、特定のイベント時に呼ばれるコールバック関数の中にだけ自分がやりたい処理を記述する必要がありました。
make
C自体の知識ではないですが、makeを知っておくと便利です。
Cのソースのビルドをするときに使います。
make clean && make build
某SDKではEclipceがデフォの開発環境ですが、僕は宗教上の理由でVimを使う必要がありましたので、makeコマンドを黒い画面に直に打ってビルドとチップへのダウンロードをしていました。
トレースログは単にシリアル通信をしているだけならソフトを選ばず取得・表示できる
某SDKではEclipce上でトレースログを表示できるため、宗教上の理由でVimを使っていてもデバッグ時にはEclipceを起動するという苦行が続いていました。
ですが、そこに救世主が現れどうやらUSB経由のシリアル通信は決まったプロトコルになっているため、通信速度さえ揃えればどのソフトでも表示できると教えてもらいました。
MacではCoolTermが便利です。
これでデバッグ時もEclipceとはおさらばです!!!!!
ディレイは全然使わない
処理と処理の間に100msの遅れを入れたい場合に使います。
Arduinoを使っていた時はまずLEDを光らせてモーターを回して、そのあとにビープを鳴らして、、、などを行いたいときに処理を全部シリアルで書いて、処理と処理の間に間を入れたい場合はdelay(100)などとしていました。
よく見るので多用するのだとばかり思っていたのですが、そうじゃなかったみたいです。
LEDをチカチカしながらモーターを回してビープもするとなったときに完全に詰みました。
なぜならdelayを実行している間は他のことができないからです。
経験者に聞いたら「whileとかdelayとかはそんなに使わない」と聞いたので、ソースをほぼほぼ書き換えるなんてこともしました。
あと後述ですが、delayを使うと某SDKがリセットしまくるという現象に悩まされたのもあって使用を控えました。
並行に色々やりたくなったらタイマー割り込みを使う
何ミリ秒かごとにある処理を行いたい、監視をしたい、LEDを点滅させたい、カウントしたいなど、並列にやりたいことが増えてきた時には、定期的に呼ばれるタイマーを定義してやるといいです。
Arduinoの例だと下のようになります。
#include <MsTimer2.h>
// 500msごとに呼ばれてLチカ
void flash() {
static boolean output = HIGH;
digitalWrite(13, output);
output = !output;
}
void setup() {
pinMode(13, OUTPUT);
// 500msごとにflashを呼ぶ
MsTimer2::set(500, flash);
MsTimer2::start();
}
void loop() {
// Lチカを気にせずになんらかのメインの処理を記述できる
}
タイマーを使わないとこんな感じでしょうか?
#include <MsTimer2.h>
// 500msごとに呼ばれてLチカ
void flash() {
static boolean output = HIGH;
digitalWrite(13, output);
output = !output;
}
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
flash();
delay(500);
// メインの処理がLチカのせいで500msごとにしか実行できなくなった!!
}
メインのloopスレッドが、どうでもいいLチカの処理でじゃまされてしまっていることがわかります。
メインのループとタイマーを使えば平行に色々なことができてかつソースも綺麗になるのでいいですね。
ウォッチドッグタイマー/watch dog timer(が、某SDKでは強すぎる)
組み込みの場合は大抵がシングルスレッドなので、一定以上処理が長くなると他の処理に支障がでます。また場合によっては無限に同じ処理を繰り返す状態に陥ってしまっているかもしれません。
そういう状態になっても正常にシステムを動かし続けるために、ある程度長い処理があったらそれをカウントして、カウンタが一定以上になったらリセット処理を行います。
これがウォッチドッグタイマーです。
自分の場合は、最初の頃に長い処理をしまくっていたら、watch dog timerがかかりまくってリセットしてしまい、「SDKのバグか?」って思ってしまっていました。
これの存在を知ったあとは、長い処理をするときは常にwatch dog timerのカウントをリセットする関数を呼んだり、そもそもwatch dogを停止させるなんてこともしました。
ですがwatch dog timerを無効にしてるが気持ち悪くなり結局一つの関数ができるだけ短い時間で終わるように書き換えました。
タイマー割り込みを使えば大抵長い処理は避けられるはずです。
float, doubleなどの実数は使えない!(場合もある)
組み込みに使っているアーキテクチャによっては浮動小数点型が使えません。
なので全部を整数で扱う必要がありました。
intは使わない
単にintと書くとアーキテクチャによってはintの大きさが変わるので、int8_t, int16_tのようにbit数を指定して整数を定義してやるといいです。移植性が高くなります。移植しないけど。
#include <stdint.h>
int8_t a;
staticは書く場所によって意味が違う!
- 関数内に書かれたstaticな変数は一度しか定義されないので定義時の代入が行われなくなる(変数の値を保持できる)
- 関数外に書かれたstaticな変数・関数は他のファイルから参照できない
staticが付いていると、ビルドしたあとのバイナリファイルに変数・関数名が書かれることがありません。これによって外部のバイナリが直接その関数を呼べなくなります。逆にstaticがついてないとバイナリにその変数・関数名がそのまま記述されることになります。これによって別のバイナリから呼べるようになります。
JavaやLLでいうところのprivateをつけているようなものですね。
ヘッダーの先頭と末尾にifndefをつけると定義が重複して実行されない
#ifndef HOGE_H
#define HOGE_H
// なんらかの定義群
#endif
いろんな場所で#include
される場合は、上のように記述していると一度しか定義が実行されないので安心してインクルードできます。
structとenumであれを省略できる
structやenumを使う場合は大抵typedefを使うと記述量が減って便利です。
// 列挙型を定義
enum HOGE {
HOGE1,
HOGE2
}
// 列挙型HOGEの変数を定義
enum HOGE hoge;
これでもいいけど、
// 列挙型を定義
typedef enum _HOGE {
HOGE1,
HOGE2
} HOGE;
// 列挙型HOGEの変数を定義
// enumを省略できた
HOGE hoge;
なんですけど、_HOGEは省略することができ、
// 列挙型を定義
// _HOGEを省略できた
typedef enum {
HOGE1,
HOGE2
} HOGE;
// 列挙型HOGEの変数を定義
// enumを省略できた
HOGE hoge;
すっきり書けます。
structも一緒です。
typedef struct {
int a;
int b;
} HOGE;
構造体の中に構造体をネストすると気持ちいぃ
Golangのときもハァハァしたのですが、cでもハァハァできます。
typedef struct {
int a;
} A;
typedef struct {
int b;
} B;
typedef struct {
A a;
B b;
} C;
これで普通に動くのすごい。
すごいょぉ。
(´д`;)ハァハァ
構造体を値も一緒に定義
ここの例を拝借します。
typedef struct {
char name[20];
char sex;
int age;
} person_t;
person_t p = {"Tom", 'M', 20};
(´д`;)ハァハァ
構造体の.と->
構造体がポインタの場合は値の参照・代入に.
ではなく->
を使います。
構造体を引数にしたときには、関数内では->
を使っていました。
void add(person_t *a, person_t *b, person_t *c)
{
c->age = a->age + b->age;
}
int main(void)
{
person_t x = {"Tom", 'M', 20};
person_t y = {"Alice", 'F', 19};
person_t z;
add(&x, &y, &z);
print(x.age); // 20
print(y.age); // 19
print(z.age); // 20 + 19 = 39
return 0;
}
(´д`;)ハァ...
inline
inlineで書いておくと, コンパイル時にメソッド呼び出し部分に定義した関数をそのまま展開します。
static inline int add(int a, int b) {
return a + b;
}
バイナリサイズは大きくなりますが、関数呼び出しコストが発生しなくなるので、頻繁に呼ぶ処理の少ない関数はinlineを使っておくと高速化が図れると思います。
C++とかで見かけましたね。C99じゃないとだめみたいです。
エラーハンドリング
最初if文とかで書いてました、こっちの方がスッキリ書けます。
列挙型として定義しておいて、関数の返却値とします。
#include <stdint.h>
/// エラーの定義
typedef enum {
HOGE_OK = (uint8_t)0,
HOGE_ERROR = !HOGE_OK
} HOGE_Error;
/// 戻り値としてエラーを返す
HOGE_Error hoge()
{
return HOGE_OK;
}
int main(void)
{
switch(hoge()) {
case HOGE_OK:
/* 成功時の処理 */
break;
case HOGE_ERROR:
/* 失敗時の処理 */
break;
}
}
ステートマシン
状態によって処理を変える考え方です。
#include <stdint.h>
/// 状態を定義
typedef struct {
START,
RUNNING,
STOP,
ERROR
} HOGE_STATE;
static HOGE_STATE state;
int main(void)
{
switch(state) {
case START:
/* 動作準備時の処理 */
break;
case RUNNING:
/* 動作時の処理 */
break;
case STOP:
/* 動作終了時の処理 */
break;
case ERROR:
/* エラーの処理 */
break;
}
}
最初if文だらけになりそうでしたが、これを導入してからファイルは縦長になりますが、複雑な状態の変化を記述しやすくなりました。可読性もあがりました。
n進数変換
考えたくないのでお得意のRubyで処理しちゃいました。
16進数→10進数
$ ruby -e 'p "%d" % 0x64'
100
10進数→16進数
$ ruby -e 'p "%x" % 100'
64
主にデバッグ時。
16bitを8bitの配列に
16bitの情報を8bitのintの配列に変換する場面が結構ありました。
int8_t data[2];
int16_t battery_voltage = get_battery_voltage();
data[0] = (UINT8)((battery_voltage >> 8) & 0xff);
data[1] = (UINT8)(battery_voltage & 0xff);
int8_tで定義した数字は"0000 0000"って感じに数字が見えるようになっていると考えやすいですね。
設定値フラグ
設定値を複数持たせたい場合はbitをシフトさせて設定値を持っておくと便利です。
typedef enum {
MALE = (1 << 0), /// 男性: 0000 0001
FEMALE = (1 << 1), /// 女性: 0000 0010
UNKNOWN = (1 << 2) /// 何か: 0000 0100
} HUMAN_MODE;
HUMAN_MODE human_mode;
int main(void)
{
human_mode = MALE; // 単に男: 0000 0001
human_mode = MALE | FEMALE; // 男であり女: 0000 0011
if (human_mode & MALE) {
// 実行される
}
if (human_mode & FEMALE) {
// 実行される
}
if (human_mode & UNKNOWN) {
// 実行されない
}
return 0;
}
この使い方はネットやObjective-Cをいじっているときに見かけたのですが、
自分でやってみて割りと感動しました。
一つの変数に複数の設定フラグを持たせることができて便利です。
doxygen
ソースのコメントを決まったフォーマットにしておくと、自動でHTMLなどでドキュメントを吐き出してくれます。
便利。
install
$ brew install doxygen graphviz
make Doxyfile
$ doxygen -g
ex) setting file
# 解析したいプロジェクトの名前
PROJECT_NAME = "Your Project Name"
# 再帰的にソースコードのファイルを探索する
RECURSIVE = YES
# LaTeX で出力しない
GENERATE_LATEX = NO
# Graphviz で出力するための DOT ファイルを作る
HAVE_DOT = YES
# DOT ファイルの生成をマルチスレッドで行う
DOT_NUM_THREADS = 4
# コールグラフ (呼び出す側) を作る
CALL_GRAPH = YES
# コールグラフ (呼び出される側) を作る
CALLER_GRAPH = YES
コメントの例
/**
* @brief バッテリーの値を取得するための関数
* 4回取得し平均を取る
* @param uint8_t *data: バッテリーの値を入れる
* @retval Error code [OK, ERROR]
*/
BATT_Errort get_battery_voltage(uint8_t* data);
run
$ doxygen
参考
おすすめ技術書
まとめ
実際にはSDKごとにGPIOやEEPROM、I2Cの利用方法、IoT製品ならBLEの知識・SDKでの使い方など、知らなければいけないことはまだまだありました。
他にもこれ知っとくといいよって知見がありましたら教えてくださいm(_ _)m