始めに
mrubyではmrbgemを利用して機能の追加が容易にできます。Cで書かれたライブラリであれば、インタフェースを定義する事でその機能をmrubyに組込めます。現在では多くのライブラリがmrbgemとして公開され、mrubyを試すハードルは低くなりました。
しかし、小規模なマイコンをターゲットにする場合、そもそも規模が大き過ぎるなど要求に適したライブラリがなく自作する事はしばしばあります。リソースの小さなマイコンであればライブラリそのものがなく、ハードウエア依存の箇所が多くなると同じく自作を選ぶ事になります。
それならばmrubyを組込むよりも全てをCで書いた方が容易だと感じられるかもしれません。しかし、ライブラリ作成とはC向けのAPIを提供する事と同義です。そしてmrubyはC向けのAPIを取り込む仕組みがあります。Cとmrubyの両方をターゲットとするライブラリとしてmrbgemを作れるならば、利用される幅が大きくなるかもしれません。
本稿ではハードウエア操作を目的としたmrbgemをハードウエア依存部分とインタフェース部分を切り分け、それぞれテストツールを利用してデバッグすることで自作ライブラリの作成とmrbgemを同時に作成する方法を紹介します。
mruby-directについて
説明の例として、mruby-directを使用します。mruby-directは、マイコンのレジスタを直接mrubyから操作する目的で作成したmrbgemです。
レジスタとはペリフェラルを操作するI/Oであり、特定アドレスに設定されたメモリを操作する事で制御します。例としてGPIOで説明します。GPIOはマイコンの端子のHigh/Lowを操作する事で、周辺機器を制御します。端子の先にLEDがあればON/OFF出力を制御し、端子の先にスイッチがあればON/OFF入力を制御します。これらは特定のメモリにマッピングされたアドレスに値をWrite/Readすることで操作できます。そのため「任意のアドレスにWrite/Readできる」mruby向けのライブラリが必要となります。
mrubyで手早くLEDとチカチカさせたいのにライブラリがない、それがmruby-directを作成した目的です。
コーディング時の問題
マイコンをOSなしで使用する場合、任意のアドレスにアクセスするコードを書くのは容易です。しかし、現代のOSの多くは任意のアドレスへのWrite/Readを許可しません。勿論、全てをターゲットマイコンでデバッグすれば問題ありませんが、スペックの小さなマイコンが相手の場合は良い手段ではありません。初期段階のチェックであればパワーのあるPC等で確認できた方が都合が良いのです。そのため、ハードウエア依存のデバッグをPCで解決する工夫が必要になります。
またmrubyにインタフェースを提供する部分についてはOSへの依存がありません。これらについては全てPC上で確認してからターゲットマイコンでのデバッグに移行すべきです。しかし、デバッグをするにはmrubyからインタフェースを叩く必要があり、こちらについても工夫が要ります。
ハードウエアに依存したデバッグと、mrubyに依存したデバッグ。これらを解決するにはどうすべきか。そこで以下の方針に基づいてコードを作成し、基板での動作前にデバッグを施すことにしました。
- ハードウエア操作部分とインタフェース部分を明確に分離する
- 分割したそれぞれの部分をテストツールにて事前にデバッグする
- 基板で動作確認する
ソースコードの分離
始めにソースコードを分離します。mruby-directではメモリへのアクセス部分だけですので一行だけになります。以下が8bit書き込みの部分を切り出したソースです。
uint8_t direct_write8(volatile uint8_t *addr, uint8_t dat)
{
*addr = dat;
return dat;
}
次にインタフェースのコードを示します。mrubyでは31bitを境界としてFIXNUMとFLOATが分離されてますので、アドレスの取得部分は少し大袈裟になります。
mrb_value
mrb_direct_write8(mrb_state *mrb, mrb_value self)
{
mrb_value *argv;
mrb_int argc;
int iargc;
volatile uint8_t *addr;
uint8_t dat;
mrb_get_args(mrb, "*",&argv, &argc);
iargc = (int)argc;
if (iargc == 2)
{
if (mrb_fixnum_p(argv[0]))
{
addr = (volatile uint8_t *)(mrb_cptr(argv[0]));
}
else
if (mrb_float_p(argv[0]))
{
uint32_t tmp = (volatile uint32_t)(mrb_float(argv[0]));
addr = (volatile uint8_t *)tmp;
}
else
{
mrb_raise(mrb, E_ARGUMENT_ERROR, "wrong argument");
}
dat = (uint8_t)(mrb_fixnum(argv[1]));
direct_write8(addr, dat);
}
else
{
mrb_raise(mrb, E_ARGUMENT_ERROR, "wrong number of arguments");
}
return self;
}
それぞれ個別にテストすることで、事前デバッグを片付けます。
テストツールの導入
事前デバッグをするためにテストツールを導入します。私が使用したのはceedling(unity/fff)ですが、他のテストツールでも方針は同じです。srcディレクトリにテストするmrbgemのソース一式をコピーして、mrubyのライブラリを結合するための記述をproject.ymlに追加します。
ハードウエア依存部分のテスト
ハードウエア依存部分のテストは、テスト用のローカルメモリへの操作が正しくできたかどうかで判定します。書き込みのテストは以下の通りとなります。
void test_direct_write8(void)
{
uint8_t virtualRegs = 0x00;
direct_write8(&virtualRegs,0xff);
TEST_ASSERT_EQUAL_HEX8(0xff,virtualRegs);
}
見ての通り、ローカルに用意したメモリアドレスを引き渡し、それが指定した値に書き込みできたかどうかをチェックします。ペリフェラルレジスタの操作は指定アドレスのメモリ操作と同じですので、上記のテストでも十分コードのチェックが可能です。
インタフェース部分のテスト
インタフェース部分のテストは、ハードウエア依存部分に比べると規模が大きくなります。少くとも以下のチェックが必要になります。
- mrubyからのメソッド呼び出しを正しく受理すること
- 呼び出し元の指定により正しくハードウエア依存部分を呼び出せること
- 戻り値が必要な場合は正しくmruby側に返せること
mrubyのメソッド呼び出しや値の確認が必要なのでmrubyでスクリプトを作成してテストするのが正着に見えますが、それではハードウエア依存部分と結合できたかどうかを確認するのが面倒です。この場合、一番重要なのはハードウエア依存部分が正しく呼び出せるかどうかです。既にハードウエア依存部分はテストできてますから、mrubyから正常に呼び出しできればmrbgemとして正しい事が判るからです。そこで以下の方針でテストをします。
- ハードウエア依存部分はfakeを用意する
- テスト用にmruby VMを起動する
- Cのテストコードからmrubyのメソッドを呼び出す
- fakeが正しく呼ばれたか確認する
- fakeに引き渡された値が正しいか確認する
- fakeからの戻り値がmruby側に正しく返されたか確認する
- テストが終了時にmruby VMを停止する
ハードウエア依存部分のfakeを用意する
fffを利用してfakeを作成します。fffについては配付元のgithubにチュートリアルがあります。
#include <string.h>
#include "fff.h"
DEFINE_FFF_GLOBALS;
FAKE_VALUE_FUNC(uint8_t,direct_write8,volatile uint8_t *, uint8_t);
テスト用にmruby VMを起動する
mruby VMを起動します。一つのテスト毎に起動するので、setUp()
に以下の記述をします。
mrb_state *mrb;
void setUp(void)
{
mrb = mrb_open();
mrb_mruby_direct_gem_init(mrb);
}
通常ですとmrbgemはmrubyのライブラリ作成時に結合するので初期化は不要ですが、今はテストですので自前で初期化します。
テストコード
ではテストを記述します。
void test_write8_min_fixnum_address_min_fixnum_value(void)
{
char cmd[] = "Direct::write8(0x00000000,0x01)";
mrb_load_string(mrb,cmd);
TEST_ASSERT_EQUAL(1,direct_write8_fake.call_count);
TEST_ASSERT_EQUAL_HEX32(0x00000000,direct_write8_fake.arg0_val);
TEST_ASSERT_EQUAL_HEX8(0x01,direct_write8_fake.arg1_val);
}
mrubyのメソッドを呼び出す方法ですが、スクリプトを文字列にしてmrb_load_string()
に引き渡します。マイコンですとバイトコードにコンパイルして結合が一般的ですが、PCはリソースが多いので必要がありません。またどんなスクリプトをテストするのかが一目で判るのでテンポ良くテストが書けます。
Cのコードが正しく呼び出された事は、fakeのcall_countで知る事ができます。call_countはテスト開始時に0、呼び出される毎にインクリメントされます。他のテストで同じfakeを繰り返し呼び出すと値はどんどん増えますが、「正しく呼び出されたか」の確認なら最初のテストだけやれば問題ありません。各テスト毎に値を確認したい場合は、テスト開始前にfakeをリセットする処理を入れます。
mrubyから引き渡された値が正しいかどうかは、fakeのarg_valで確認します。値の比較する際はビット幅を指定します。先にも延べた通り、mrubyはビット幅により値の持ち方が異ります。ですので望んだ値だけではなく、望んだビット幅の値なのかも確認するべきです。
最後に戻り値についてですが、一旦mrubyのオブジェクトとして値を受け取り、それをC側へコピーして確認します。戻り値を確認するサンプルとして、以下にreadのテストを示します。
void test_read8_min_vlaue(void)
{
uint8_t dat;
mrb_value val;
char cmd[] = "@val = Direct::read8(0x55555555)";
direct_read8_fake.return_val = 0x0;
mrb_load_string(mrb,cmd);
val = mrb_iv_get(mrb,mrb_top_self(mrb),mrb_intern_cstr(mrb,"@val"));
dat = mrb_fixnum(val);
TEST_ASSERT_EQUAL_HEX8(0x0,dat);
}
fakeの戻り値はreturn_valで設定できます。設定後にmrubyスクリプトを動作させ、戻り値をオブジェクトに保存します。保存したオブジェクトをC側へmrb_iv_get()
でコピーし、比較したい型(この場合はuint8_t)にキャストします。一連の操作で得られた値をテストします。
テスト終了時にmruby VMを停止する
テストが完了したら、後はVMを止めて終了です。tearDown()
に以下の記述をします。
void tearDown(void)
{
mrb_close(mrb);
}
基板での動作確認
テストが完了したら、マイコンに組み込んで動作を確認します。クロスコンパイルやスクリプトの組み入れについては割愛します。
終りに
本稿で取り上げたテスト手法は新規ライブラリ作成の他にも、既存のライブラリの品質を確認しつつmrbgemを作成したり、mrbgemのデバッグでライブラリとmrubyのどちらに問題があるかの切り分けにも活用できます。これがマイコン向けのmrbgemが増える一助になる事を期待します。御精讀、ありがたうございました。