nRF SDK時代
BAS(Battery Service)のように定期的に呼び出す関数を実装する場合、nRF SDKではほぼ書き方が決まっていました。おそらくほとんどの人はapp_timer_start()という低精度タイマーを使ったタイマー割り込みによる実装をしていたと思います。
これはSDKに付属しているサンプルプロジェクトにそのように記述されているというのもありますが、バッテリー残量の更新程度ならそんなに緻密な割り込み精度は必要がないという設計理論の元にそういう設計をしているわけです。
別に中の人に確認したわけではないけれど、きっとそうだと信じたい・・・
Zephyrでの実装方法
ZephyrはRTOSということもあって実装方法が多岐に渡っています。いや、これだと語弊があるので言い直すと、手段がいっぱい用意されているので好きにして、と言ったところでしょうか。パッと思いつくだけでも以下のようなやり方があります。
mainループ内での実装
main関数内で初期化が終わったあとに出てくるwhile無限ループ句(別にforでもなんでもいいですが)の中に実装するというのが一番ポピュラーな実装ではないかと思われます。具体的には
void main(void)
{
// Initialize
hoge();
// Main loop
while (1)
{
bt_bas_set_battery_level(battery_level);
k_sleep(K_SECONDS(10));
}
}
こんな感じになると思います。mainループは専用のキューを持っているので上記のような記述であれば専用スレッドとなります。
タイマースレッドでの実装
nRF Connect SDK(以下、NCS)にもタイマーというのはもちろんありますが、nRF SDK時代にあった低精度タイマーという概念はありません。タイマーで実装する場合はこんな感じになると思います。
static void bas_handler(struct k_timer *timer)
{
bt_bas_set_battery_level(battery_level);
}
K_TIMER_DEFINE(timer_bas, bas_handler, NULL);
void main(void)
{
// Initialize
hoge();
// Battery Timer
k_timer_start(&timer_bas, K_NO_WAIT, K_SECONDS(5));
}
タイマースレッドはシステムキュー上で実行されるため、他にシステムキュー上で待つ何かがあると実行されなくなりますが、システムキュー上で無限待ちのセマフォを組んだりすることはないのでタイマー処理でも大丈夫だと思います。
一例としてI2CやSPIを非同期処理にするとコールバックがシステムキュー上に返ってきます。
ワーカースレッドでの実装
タイマースレッドでの実装とほぼ一緒ですが、タイマースレッドでの実装と違うのはユーザーキューを指定することができるという点です。デフォルトで使うとシステムキューに投げるので、それだとタイマースレッドでの実装と一緒です。
mainスレッドがおススメ
一応おススメ順という形で並べては見たのですが、ここまで見比べてみても分かるようにmainスレッドで実装するのが圧倒的にコード量が少ないです。
(ワーカースレッドはさらにユーザーキューを宣言する必要があります)
よほどこだわりがあるのでなければmainスレッド内に実装するのがおススメですし、なによりも分かり易いですよね。
余談
なんでこんなことを説明したかというと、NCSを触り始めて割と初期段階で疑問に思うことなんですよね。サンプルを漁るとmainスレッドで実装したサンプルがあるはずですが、いやいや俺はそんなのじゃなくてnRF SDK時代のようにタイマーで実装したいんだよ、とか厨二が発動しちゃったりするわけですよ、ええ(笑)。
もう少し理解が進んでくるとタイマーじゃなくてスレッド(タイマーもスレッドですがここでのスレッドはワーカースレッド)で実装できるんじゃね?とか思いついたりするわけです。
実際にやってみると確かに実装できますし、あまりにもあっさりできるので拍子抜けするくらいなのですが、じゃあこれっていったい何が違うの?という疑問に発展するわけです。
結論としては使っているキューが違うだけで、そのキュー内でデッドロックさえしなければ何を使っても変わらない、なのですがなかなかそこまで辿り着くのは時間がかかります(笑)。