2012/03/07

組み込み開発のリスクである「ソフトウェアとハードウェアの並行開発」の解決方法の一つが組み込みTDDじゃないだろうか?


私はSIerの人間であって、メーカーの人間ではないですが、メーカーの製品開発の中に深く入り込んで開発した経験があります。その時感じたのは組み込み開発の最大のリスクがハードウェア・ソフトウェアの並行開発だということです。

■未完成品のハードウェアに対してコードを書くのは大変><

一般的な組み込み開発の場合、開発PC上で開発→ 評価ボードでデバッグ → 本製品上での評価と開発が進みます。ベッタベタな開発の場合、最初はターゲットがない状態でごしごしコードを書いて、しばらくしたら評価ボードと言われる半完成品のハードウェアが届いて、そいつにソフトウェアをのっけてデバッグして、ソフトウェアのバグを取り除く作業をやります。

とにかく早く実機で動かすのが腕の見せ所!な状態なので、そもそも単体テストできることを考えていない場合も。だからハードウェアと密結合を起こして、後から単体テストできなくなってしまいます。

ここで
  • 開発PC、評価ボード・製品向けのライブラリやコンパイラが微妙に違う
  • 最初は評価ボードの完成度が低い
  • ソフトウェアのロジックのミスがある
  • ハードウェアが固まった後のしわ寄せがソフトウェアにくる
などなどのリスクがある状況で開発が進むので、とにかくデバッグに時間がかかったり、バグの原因の切り分けが難しくなったりして、ソフトウェア開発がものすごい困難になります。

(組み込みでアジャイル開発をやる場合は、ここがネックになると思います。)

Dual-Target Testing

そこで、Test-Driven Development for Embedded Cでは、最初からハードウェアへ依存しない設計にし、開発PCでも評価ボードでも単体テストが動くようにすることを推奨しています。これをDual-Target Testingと呼んでいます。これで、開発PCで徹底的に単体テストをし、ソフトウェアのロジックミスを排除して、さらに評価ボード上でも単体テストが動くことを確認します。これで、テストがかなりスムーズに行えます。

Dual-Target Testingに必要な要素は?

では、Dual-Targeting Testをやるには何が必要なんでしょうか?第一にJava界隈で育ってきたテストの技術を応用して、最初から単体テスト可能に設計することが重要だと思います。

次に、低レイヤーの開発の場合、メカ・エレキなど周辺領域の技術者との緊密なコミュニケーションが必要だと感じます。Test-Driven Development for Embedded Cでは、エレキの技術者とハードウェアとのインタフェース部分の設計をつめているシーンが書かれています。最初からハードウェアへ依存しない設計にするためにも、ハードウェアとのインタフェースをつめておかないと、Test Doubleの作り方も決められないからだと思います。

Test-Driven Development for Embedded Cではどうやっているのか?

最後に書籍のサンプルをのせていますので、そちらを参照してください。このサンプルの肝はコンストラクタでハードウェアへの依存性を注入している箇所です。変数ledsAddressとは、書き込みポートのアドレスですが、これをコンストラクタで与えることで、開発PCでも評価ボードでも単体テストできるようにしてます。

また、かなり芸が細かいですが、このハードウェアは読み込みポートがないという設定です。LEDの状態は一度ledsImageという変数に書き込んでおいて、あるタイミングでポートに書き込みます。読み込みポートがあるのかとかはエレキの人と早めに仕様を詰める必要があります。

今回の例は極めて単純なので、だからどうしたと思われるかもしれません。現場にはもっとテスト困難な例があると思います。そんな時にどうするのか?基本は同じで、ハードウェアおよび外部モジュールに対する疎結合な設計、早期のハードウェアとのインタフェースの確立だと思います。

詳しくは次回以降のTest-Driven Development for Embedded C読書会で学んでいきます。乞うご期待。

(サンプル)テストコード
#include "unity_fixture.h"
#include "LedDriver.h"
#include "RuntimeErrorStub.h"

#ifndef NULL
#define NULL @((void *) 0)
#endif

TEST_GROUP(LedDriver);

static uint16_t virtualLeds;

TEST_SETUP(LedDriver)
{
 LedDriver_Create(&virtualLeds);
}

TEST_TEAR_DOWN(LedDriver)
{
}

TEST(LedDriver, LedsOffAfterCreate)
{
 uint16_t virtualLeds = 0xffff;
 LedDriver_Create(&virtualLeds);
 TEST_ASSERT_EQUAL_HEX16(0, virtualLeds);
}

TEST(LedDriver, TurnOnLedOne)
{
 LedDriver_TurnOn(1);
 TEST_ASSERT_EQUAL_HEX16(1, virtualLeds);
}

TEST(LedDriver, TurnOffLedOne)
{
 LedDriver_TurnOn(1);
 LedDriver_TurnOff(1);
 TEST_ASSERT_EQUAL_HEX16(0, virtualLeds);
}

TEST(LedDriver, TurnOnMultipleLeds)
{
 LedDriver_TurnOn(9);
 LedDriver_TurnOn(8);
 TEST_ASSERT_EQUAL_HEX16(0x180, virtualLeds);
}

TEST(LedDriver, AllOn)
{
 LedDriver_TurnAllOn();
 TEST_ASSERT_EQUAL_HEX16(0xffff, virtualLeds);
}

TEST(LedDriver, TurnOffAnyLeds)
{
 LedDriver_TurnAllOn();
 LedDriver_TurnOff(8);
 TEST_ASSERT_EQUAL_HEX16(0xFF7F, virtualLeds);
}

TEST(LedDriver, LedMemoryISNotReadable)
{
 virtualLeds = 0xffff;
 LedDriver_TurnOn(8);
 TEST_ASSERT_EQUAL_HEX16(0x80, virtualLeds);
}

TEST(LedDriver, UpperAnbLowerBounds)
{
 LedDriver_TurnOn(1);
 LedDriver_TurnOn(16);
 TEST_ASSERT_EQUAL_HEX16(0x8001, virtualLeds);
}

TEST(LedDriver, OutOfBoundsChangesNothing)
{
 LedDriver_TurnOn(-1);
 LedDriver_TurnOn(0);
 LedDriver_TurnOn(17);
 LedDriver_TurnOn(3141);
 TEST_ASSERT_EQUAL_HEX16(0, virtualLeds);
}

TEST(LedDriver, OutOfBoundsTurnOffDoesNoHarm)
{
 LedDriver_TurnAllOn();
 LedDriver_TurnOff(-1);
 LedDriver_TurnOff(0);
 LedDriver_TurnOff(17);
 LedDriver_TurnOff(3141);
 TEST_ASSERT_EQUAL_HEX16(0xffff, virtualLeds);
}

TEST(LedDriver, OutOfBoundsProducesRuntimeError)
{
 LedDriver_TurnOn(-1);
 TEST_ASSERT_EQUAL_STRING("LED Driver: out-of-bounds LED", RuntimeErrorStub_GetLastError());
 TEST_ASSERT_EQUAL(-1, RuntimeErrorStub_GetLastParameter());
}

TEST(LedDriver, IsOn)
{
 TEST_ASSERT_FALSE(LedDriver_IsOn(11));
 LedDriver_TurnOn(11);
 TEST_ASSERT_TRUE(LedDriver_IsOn(11));
}

TEST(LedDriver, OutOfBoundsLedsAlwaysOff)
{
 TEST_ASSERT_TRUE(LedDriver_IsOff(0));
 TEST_ASSERT_TRUE(LedDriver_IsOff(17));
 TEST_ASSERT_FALSE(LedDriver_IsOn(0));
 TEST_ASSERT_FALSE(LedDriver_IsOn(17));
}

TEST(LedDriver, IsOff)
{
 TEST_ASSERT_TRUE(LedDriver_IsOff(12));
 LedDriver_TurnOn(12);
 TEST_ASSERT_FALSE(LedDriver_IsOff(12));
}
(サンプル)プロダクトコード
#include "LedDriver.h"
#include "RuntimeError.h"

enum {ALL_LEDS_ON = ~0, ALL_LEDS_OFF = ~ALL_LEDS_ON};
enum {FIRST_LED = 1, LAST_LED =16};
static uint16_t *ledsAddress;
static uint16_t ledsImage;

void LedDriver_Create(uint16_t* address)
{
 ledsAddress = address;
 ledsImage = ALL_LEDS_OFF;
 *ledsAddress = ledsImage;
}

void LedDriver_Destroy(void)
{
}

int convertLedNumberToBit(int ledNumber)
{
 return 1 << (ledNumber - 1);
}

static void updateHardware(void)
{
 *ledsAddress = ledsImage;
}

int IsLedOutOfBounds(int ledNumber)
{
    return (ledNumber < FIRST_LED) || (ledNumber > LAST_LED);
}

void setLedImageBit(int ledNumber)
{
    ledsImage |= convertLedNumberToBit(ledNumber);
}

void clearLedImageBit(int ledNumber)
{
    ledsImage &= ~convertLedNumberToBit(ledNumber);
}

void LedDriver_TurnOn(int ledNumber)
{
 if (IsLedOutOfBounds(ledNumber)) {
  RUNTIME_ERROR("LED Driver: out-of-bounds LED", ledNumber);
  return;
 }
 setLedImageBit(ledNumber);
 updateHardware();
}

void LedDriver_TurnOff(int ledNumber)
{
 if (IsLedOutOfBounds(ledNumber)) {
  RUNTIME_ERROR("LED Driver: out-of-bounds LED", ledNumber);
  return;
 }
 clearLedImageBit(ledNumber);
 updateHardware();
}

void LedDriver_TurnAllOn()
{
 ledsImage = ALL_LEDS_ON;
 updateHardware();
}

BOOL LedDriver_IsOn(int ledNumber)
{
 if (IsLedOutOfBounds(ledNumber)) {
  return FALSE;
 } else {
  return ledsImage & (convertLedNumberToBit(ledNumber));
 }
}

BOOL LedDriver_IsOff(int ledNumber)
{
 return !(LedDriver_IsOn(ledNumber));
}

0 件のコメント:

コメントを投稿