C
C++
Linux
embedded
googletest
9
どのような問題がありますか?

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

組込みに近いものをTDDで開発してみる〜準備編〜

はじめに

組込み開発でTDDをやるために勉強してきました。
今までアプリケーション中心の勉強だったため、少し組込みっぽい領域のものを作ってみます(組込みLinuxを想定)。
見切り発車なので、途中で方針変わるかもしれません。実際やらないとわからないので、温かい目で見守って下さい。

ご意見、ご要望は遠慮なくフィードバック下さい!とても励みになります。

作ろうとしているもの

やはり組込みと言えばLチカでしょう!
ただし、簡単に試せるようにしたいので、キーボードのLEDをチカチカさせます(caps lockとかのやつ)。
高機能なライブラリは使用せず、デバイスファイルの操作でなんやかんややって行きます。

あまり簡単すぎるとツマンナイですし、実践的でないため、次の機能実現を考えていきます。

  • キーボード入力で振る舞いを変える(ボタン)
  • 設定時間になると振る舞いを変える(タイマ)

組込みなので、別ハードへの移植性は常に考えるようにします。

開発環境

OS Virtual box ubuntu 16.04
コンパイラ gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609
ビルドツール autotools, pkg-config
ライブラリ libevdev-1.0
テストフレームワーク googletest/googlemock@62ba5d9
プロダクトコード C or C++(悩み中)

下調べ

何をするにも下調べは重要ですね。
とりあえず、自分の環境を調べてみます。

キーボードイベント取得

Linuxでキーボード入力イベントを取得するためには、/dev/input/event〇〇ファイルを監視します。
〇〇の部分は数字が入るのですが、環境や起動時に認識された順でキーボードが何番になるか変わります(udevで固定することが可能ですが、今回はとりあえず置いておきます)。

/proc/bus/input/devicesを覗いて、キーボードを探します。
現状は、event2(input2)がキーボードに対応しています。

cat /proc/bus/input/devices
== 途中省略 ==
I: Bus=0011 Vendor=0001 Product=0001 Version=ab41
N: Name="AT Translated Set 2 keyboard"
P: Phys=isa0060/serio0/input0
S: Sysfs=/devices/platform/i8042/serio0/input/input2
U: Uniq=
H: Handlers=sysrq kbd event2 leds
B: PROP=0
B: EV=120013
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
B: MSC=10
B: LED=7
== 以下省略 ==

/dev/inputのようなイベントデバイスのために、Linuxはevdev(Linux Input drivers v1.0)インタフェースを提供しています。
このevdevのラッパーライブラリがlibevdevです。
今回は、このlibevdevを使っていきます。

キーボードLED

次に、キーボードのLEDを確認します。
LinuxではLED classが用意されており、LinuxにLEDが認識されると、sysfsの/sys/class/leds下にexportされます。
capslock, numlock, scrolllockのLEDが認識されていますね。
(私のキーボードにはnumlockとscrolllockキーにはLEDついていないんですが…要調査)

ls /sys/class/leds/
input2::capslock  input2::numlock  input2::scrolllock

ライブラリ理解のためのテスト

ハード(キーボード)もライブラリ(libevdev)もある、という恵まれた状態からのスタートなので、対象ライブラリを理解するためにテストを書いていきます。
このテストを書くことで、自分のライブラリ理解をテストします。理解したことがテストとして残るのは、とても良いことに思えます。

これがホストマシン上で実行できる場合は、ラッキーです。
1. ターゲットボードにしか対象のハード/ライブラリがない、という状況も多いのではないでしょうか。
2. ターゲット上ですら動かない、物がない、っていうのもよくある話でしょう。

1の場合は素直にクロスビルドしながら進められます。
2の場合、APIがわかっているのであれば、モックを作りましょう。本物が出てきた時に、テストで書いた使い方が正しいかどうかテストできます。
APIもわからないとなると想像でモックを作ります。実装の詳細が分かっていない状態でモックを作ることにも利点はあって、APIが程よく抽象化できる可能性があります。

libevdev理解のために書いたテスト

前置きが長くなりましたが、今回書いたテストを見ていきましょう。
コードは一部省略して掲載します。全貌が見たい方はこちら

まず、ファイルディスクリプタから、evdevの制御データを取得します。
成功したら、0以上が返ってきます。
最初、このテストは失敗しました。ルート権限が必要だとわかったので、テスト名をわかるように(AsRootを追加)します。
これで、後日同じことでハマることはないでしょう。急に誰かに引き継ぐことになっても、root権限がないと動かせないことがわかるでしょう。

TEST_F(EvdevSampleTest, InputEventFileCanOpenAsRoot) {
  struct libevdev *dev = nullptr;
  int fd = open("/dev/input/event2", O_RDONLY|O_NONBLOCK);
  int rc = libevdev_new_from_fd(fd, &dev);

  EXPECT_GE(rc, 0);
}

evdevの制御データが取得できたので、何も入力がない状態では、EAGAINが返ってくることを確認します。
これは一発でパスしました。

TEST_F(EvdevSampleTest, ReturnsEAGAIN) {
  input_event ev {};
  int actual = libevdev_next_event(evdev_, LIBEVDEV_READ_FLAG_NORMAL, &ev);
  EXPECT_EQ(-EAGAIN, actual);
}

次にEnterキーを押すと、LIBEVDEV_READ_STATUS_SUCCESSが返ってきて、期待通りのinput_eventが取得できることをテストします。
テスト実行後、本テスト実行中にテストが止まるので、Enterキーを押してあげると、テストがパスします(もはやユニットテストでないが気にしません)。
最後のEXPECT_EQにたどり着いている時点で、取得したイベントが期待値通りであることが確定していますが、とりあえず気にしません。

TEST_F(EvdevSampleTest, CaptureEnterKeyPress) {
  auto expect = create_key_event(KEY_ENTER, KEY_PRESSED);
  input_event actual {};
  while (true) {
    int rc = libevdev_next_event(evdev_, LIBEVDEV_READ_FLAG_NORMAL, &actual);
    if ((rc == LIBEVDEV_READ_STATUS_SUCCESS) && actual == expect) break;
  }

  EXPECT_EQ(expect, actual);
}

テストを作りながら考えたこと

他の入力デバイスでも使えるようにすることを考えると、汎用的なコールバックインタフェースがあると良さそう。
libevdevはコールバックの仕組みを提供してくれないので、コールバックの仕組みは一から作るかな。

今後の予定

次:
組込みに近いものをTDDで開発してみる〜問題提起編〜

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
tomoyuki-nakabayashi
組込みLinux屋さん。広く浅くやってます。RustとhypervisorとFPGAを勉強中。

コメント

リンクをコピー
このコメントを報告

おお! 組み込みのテストに関して、まさに知りたかったことを記事にしていただいた感じです。
実例込みの記事で、とても分かりやすかったです。

これがホストマシン上で実行できる場合は、ラッキーです。
1. ターゲットボードにしか対象のハード/ライブラリがない、という状況も多いのではないでしょうか。
2. ターゲット上ですら動かない、物がない、っていうのもよくある話でしょう。
1の場合は素直にクロスビルドしながら進められます。
2の場合、APIがわかっているのであれば、モックを作りましょう。

たいていの場合は、残念ながらこれら2つのケースですよね。
お時間があるときに、これらのついても記事にしていただけると嬉しいです。(勝手なことを言ってみる。。。)

1
リンクをコピー
このコメントを報告

たいていの場合は、残念ながらこれら2つのケースですよね。

組込み開発する上で避けて通れない道ですね(嫌な話ですが)。
このような状況でも絶望せずに、テストで設計を駆動しよう!という趣旨にしたく、まさにリクエストして頂いたような内容にできるといいなぁ、と思っています。
こういうデバイス想定してやって欲しい、というのがあればおっしゃって下さい(自分で良いアイデアが浮かんでいないので)。必ずやる、とも、いつまでにやる、ともお約束はできませんが(笑)

2
(編集済み)
リンクをコピー
このコメントを報告

このような状況でも絶望せずに、

まさしくこれが理由で、効率的なテストを導入できていない現場が多々あると思います。特に既存システムをベースにした開発だと絶望しかないですね。

デザインパターンの本や記事もそうですが、こういうのの題材って難しいですよね。簡単なテーマだとありがたみを感じられないし、実践的なことだと本質とずれた内容になってしまうし。。。
個人的には、記事のメインはシンプルな題材で解説があって、補足説明で応用的なケースへのガイドがあるというパターンが好きです。

僕もあまりいいアイデアは浮かびませんが、GPIOを使ったシンプルなコードから始めていただけると、とっつきやすいと思います。例えば、下記のようなコード。

int main()
{
    while(1) {
        bool inA = GPIO入力(A);
        bool inB = GPIO入力(B);
        GPIO出力(C) = inA & inB;
    }
}

これだけでも、「テストはホストPCでやる? ターゲット基板でやる?」。「ホストPCでやる場合のGPIOモックは?」「ロジック部分(この場合は、andだけだが)だけホストPCでテストする?」、「ターゲット基板でやる場合の出力の確認は? 目視? レジスタダンプとの比較?」、「そもそも『GPIO入力』処理のテストは? ドライバはテストのスコープ外?」などなど、膨らませられそうです。(当然、読者側にも想像力(ここではロジックはandだけど、実際にはもっと複雑なロジックになるはず、など)が求められますが。)
これだと、Linuxでも、Non-OSのマイコンでも応用できると思います。

↑のコードでさらに割り込みが入った場合 も気になります。例えば、下記のようなコードだと、あえて冗長に書いていますが、リセットボタンの押されるタイミングによって、リセットが効かないというバグがあります。「こういうコードってテストできるの?」というのも気になります。テストに向かないケース集なんかもあると役立つと思います。

/* ボタンが押された回数をカウントする。カウントリセットボタンがおされたらカウントリセットする */
static int s_cntPush = 0;
int main()
{
    while(1) {
        if (GPIO立ち上がり入力(A)) {
            int cntTemp = s_cntPush;
            cntTemp += 1;
            cntPush = cntTemp;
            printf("GPIO Aの入力回数 = %d\n", cntPush);
        }
    }
}

ISR_GPIO_カウントリセットボタン()
{
    cntPush = 0;
}

また、上の例とは真逆で、「既存の大規模システム(しかもITronなどの組み込み独自のプラットフォーム)で、新規モジュールを開発することになった。どうやってTDDで開発する?」 みたいなことも興味あります。

とりあえず、思いつくままに自分勝手に羅列しちゃいました。まとまりがなくてすいません。。。そして、これらに対して絶対的な解を用意するのは不可能だとも思います(w これが出来たらコンサルとして独立、どころか本書けちゃいますね
本業優先で、お時間のあるときにでもシェアしてください~

2
リンクをコピー
このコメントを報告

アイデア頂き、ありがとうございます。
今やっている内容を、次の想定で進めるのもありかな、と思いました。

最終的には、ボタンとLEDがGPIOに繋がったハードで動かしたいが、ハードがないので、キーボードで代替している。
(これなら、最終的にシンプルなGPIOを使ったコードに落とせそう)

本当に組込みっぽいものの開発ができていたかどうか、の試金石にもなりますし、良い気がします。
一回これで内容考えてみます。

特に既存システムをベースにした開発だと絶望しかないですね。

絶望しなくて済むようにお互い頑張りましょうw

1
リンクをコピー
このコメントを報告

そうですね。この記事にあるように、キーボードでLチカというのも全然ありだと思います。
手元で簡単に確認できますしね。

1
どのような問題がありますか?
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
9
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー