Go to list of users who liked
More than 5 years have passed since last update.
IoTエンジニアになって半年が過ぎたkazuphです。
(IoTって言葉はいつごろまで使えるんですかね(・∀・)?)
PerlやRubyでWebアプリケーションを書いていたエンジニアが、C言語でゴリゴリ組み込みプログラミングをすることになったときに、どんなことを知っていたかったかなと思い出しながら書いてみたいと思います。
その辺にある普通のC言語の入門書には載ってない知識が中心です。
ちなみにArduinoの例が多いですが、実際にはほとんどの期間を某チップの某SDKを使って開発しました。
わかる方はそれを想像しながらだと面白いかもしれません(・∀・)
半年の半分はiOSアプリ作っていたので、実質3ヶ月くらいの知識だと思ってください。
間違いの指摘など是非々々お待ちしておりますm(_ _)m
こんな記事も書いてます。
すべてはmainから始まる、と思ったらSDKによっては隠蔽されていることもある
intmain(void){/* ... */}
大抵はmainを探してそこから処理を追えばだいたい読めます。
そのはずなのですが、実際には、自分が選んだチップのSDKではmainが隠蔽されていたので、まったくこの知識は役には立ちませんでしたが、基本なので知っているといいと思います。
処理はwhile文の中に書く、がSDKによっては隠蔽されている
SDKに隠蔽されているとわかりづらいですが、APIとしてコールバックだけ定義されている場合は、そのイベントの発生源はどこかにあるwhile文です。
結局はwhileの中に常に何かを監視している処理があって、コールバック関数が呼ばれているだけです。
voidsetup(){/* 初期設定などをする */}voidloop(){/* センサーの値の監視など継続的に行いたい処理を書く */}intmain(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チカvoidflash(){staticbooleanoutput=HIGH;digitalWrite(13,output);output=!output;}voidsetup(){pinMode(13,OUTPUT);// 500msごとにflashを呼ぶMsTimer2::set(500,flash);MsTimer2::start();}voidloop(){// Lチカを気にせずになんらかのメインの処理を記述できる}
タイマーを使わないとこんな感じでしょうか?
# include <MsTimer2.h>// 500msごとに呼ばれてLチカvoidflash(){staticbooleanoutput=HIGH;digitalWrite(13,output);output=!output;}voidsetup(){pinMode(13,OUTPUT);}voidloop(){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を使うと記述量が減って便利です。
// 列挙型を定義enumHOGE{HOGE1,HOGE2}// 列挙型HOGEの変数を定義enumHOGEhoge;
これでもいいけど、
// 列挙型を定義typedefenum_HOGE{HOGE1,HOGE2}HOGE;// 列挙型HOGEの変数を定義// enumを省略できたHOGEhoge;
なんですけど、_HOGEは省略することができ、
// 列挙型を定義// _HOGEを省略できたtypedefenum{HOGE1,HOGE2}HOGE;// 列挙型HOGEの変数を定義// enumを省略できたHOGEhoge;
すっきり書けます。
structも一緒です。
typedefstruct{inta;intb;}HOGE;
構造体の中に構造体をネストすると気持ちいぃ
Golangのときもハァハァしたのですが、cでもハァハァできます。
typedefstruct{inta;}A;typedefstruct{intb;}B;typedefstruct{Aa;Bb;}C;
これで普通に動くのすごい。
すごいょぉ。
(´д`;)ハァハァ
構造体を値も一緒に定義
ここの例を拝借します。
typedefstruct{charname[20];charsex;intage;}person_t;person_tp={"Tom",'M',20};
(´д`;)ハァハァ
構造体の.と->
構造体がポインタの場合は値の参照・代入に.
ではなく->
を使います。
構造体を引数にしたときには、関数内では->
を使っていました。
voidadd(person_t*a,person_t*b,person_t*c){c->age=a->age+b->age;}intmain(void){person_tx={"Tom",'M',20};person_ty={"Alice",'F',19};person_tz;add(&x,&y,&z);print(x.age);// 20print(y.age);// 19print(z.age);// 20 + 19 = 39return0;}
(´д`;)ハァ...
inline
inlineで書いておくと, コンパイル時にメソッド呼び出し部分に定義した関数をそのまま展開します。
staticinlineintadd(inta,intb){returna+b;}
バイナリサイズは大きくなりますが、関数呼び出しコストが発生しなくなるので、頻繁に呼ぶ処理の少ない関数はinlineを使っておくと高速化が図れると思います。
C++とかで見かけましたね。C99じゃないとだめみたいです。
エラーハンドリング
最初if文とかで書いてました、こっちの方がスッキリ書けます。
列挙型として定義しておいて、関数の返却値とします。
# include <stdint.h>/// エラーの定義typedefenum{HOGE_OK=(uint8_t)0,HOGE_ERROR=!HOGE_OK}HOGE_Error;/// 戻り値としてエラーを返すHOGE_Errorhoge(){returnHOGE_OK;}intmain(void){switch(hoge()){caseHOGE_OK:/* 成功時の処理 */break;caseHOGE_ERROR:/* 失敗時の処理 */break;}}
ステートマシン
状態によって処理を変える考え方です。
# include <stdint.h>/// 状態を定義typedefstruct{START,RUNNING,STOP,ERROR}HOGE_STATE;staticHOGE_STATEstate;intmain(void){switch(state){caseSTART:/* 動作準備時の処理 */break;caseRUNNING:/* 動作時の処理 */break;caseSTOP:/* 動作終了時の処理 */break;caseERROR:/* エラーの処理 */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_tdata[2];int16_tbattery_voltage=get_battery_voltage();data[0]=(UINT8)((battery_voltage>>8)&0xff);data[1]=(UINT8)(battery_voltage&0xff);
int8_tで定義した数字は"0000 0000"って感じに数字が見えるようになっていると考えやすいですね。
設定値フラグ
設定値を複数持たせたい場合はbitをシフトさせて設定値を持っておくと便利です。
typedefenum{MALE=(1<<0),/// 男性: 0000 0001FEMALE=(1<<1),/// 女性: 0000 0010UNKNOWN=(1<<2)/// 何か: 0000 0100}HUMAN_MODE;HUMAN_MODEhuman_mode;intmain(void){human_mode=MALE;// 単に男: 0000 0001human_mode=MALE|FEMALE;// 男であり女: 0000 0011if(human_mode&MALE){// 実行される}if(human_mode&FEMALE){// 実行される}if(human_mode&UNKNOWN){// 実行されない}return0;}
この使い方はネットや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_Errortget_battery_voltage(uint8_t*data);
run
$ doxygen
参考
おすすめ技術書
まとめ
実際にはSDKごとにGPIOやEEPROM、I2Cの利用方法、IoT製品ならBLEの知識・SDKでの使い方など、知らなければいけないことはまだまだありました。
他にもこれ知っとくといいよって知見がありましたら教えてくださいm(_ _)m
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme