組み込みまするβ

組み込みソフトウェア初学者向けブログ

小規模組み込みソフトウェアの共通プラットフォーム

組み込みソフトウェアも一般的なソフトウェアと同じように、 一度作成したサブシステム、ミドルウェア等のコンポーネント(モジュール)を当然再利用しています。 デバイスドライバであっても過去に採用した同系統のCPUであれば周辺回路も互換性があるので再利用することができます。

ではCPUの変更等により周辺回路が異なる場合には再利用できないでしょうか。

周辺回路の変更による影響

周辺回路の違いは制御対象が異なるので同じドライバモジュールは使用不可となり、 当然新規作成などで差し替える必要が出てきます。

ドライバモジュールを差し替える必要が出た場合、使用する側はどのような影響を受けるでしょうか。 その周辺回路のハードウェア仕様に依存した関数インターフェイスになっている場合は、 使用する側も影響を受けてしまって修正が伝播していってしまいます。

では同系統の周辺回路を同じ関数インターフェイスで設計すれば、 使用する側は影響を受けないため、ドライバモジュールの差し替えだけで対応ができることになりますよね。

抽象化

UART、Timer、GPIO等の具体的な周辺回路で考えてみましょう。 例外はあると思いますが、どの周辺回路も組み込み向けCPUなら搭載しているものと思います。

UARTの抽象化

例えばUARTですが、Nios2のUARTとMicroBlazeのUartLiteは以下のような違いがあります。

※詳細には送信割り込みがレベルでないとかデータビットの対応等いろいろとあるのですが本筋ではないで書きません。

単純に考えるとUARTはデータを送信、受信それぞれ独立に動作する回路です。 そしてputやget、readやwriteといった関数インターフェイスが採用されることが多いので、 それらの関数名を取り入れることにします。

/* uart.h (UartBase) */

/*! @brief Get a data */
virtual int get(uint8_t* data) = 0;

/*! @brief Write a data */
virtual int put(uint8_t data) = 0;

/*! @brief Read data into buffer */
virtual int read(uint8_t data_buff[], unsigned int data_count) = 0;

/*! @brief Write data buffer */
virtual int write(const uint8_t data_buff[], unsigned int data_count) = 0;

/*! @brief Clear receive/transmit buffer and errors */
virtual void clear() = 0;

/*! @brief Flush TX-Buffer */
virtual int flush() = 0;

/*! @brief Overrun error occurred */
virtual bool overrunErrorOccurred() const = 0;

/*! @brief Framing error occurred */
virtual bool framingErrorOccurred() const = 0;

/*! @brief Parity error occurred */
virtual bool parityErrorOccurred() const = 0;

興味があればGitHubにアップ済のC/C++実装を参照してみてください。 このインターフェイスによりUARTの違いを吸収しましたので同じ制御方法で使用できるようになりました。 このように周辺回路単位で抽象化して汎用的な使い方ができるインターフェイスを考えて設計していきます。

GPIOの抽象化

UARTだけでなく、GPIOでも同様です。

Nios2とMicroBlazeの周辺回路としてはそれぞれPIO、GPIOと呼ばれています。 通常GPIOはデータレジスタディレクションレジスタで構成され、 外部に出ている端子単位でHigh/Low制御、IN/OUT制御が可能となっています。

ところがこのPIOとGPIOはディレクションの対応付けに以下のような違いがあります。

  • Nios2のPIOのdirection
    • 1:output, 0:input
  • MicroBlazeのGPIOのdirection
    • 1:input, 0:outinput

これでは単純に差し替えることはできません。 このため、GPIO側でdirection値をビット反転してread/writeすることでPIO仕様に合わせ、インターフェイスを共通化するとどうでしょうか。

/* nios_gpio.cpp */

/*! @brief Write direction [1:output, 0:input] */
void NiosGpio::writeDirection(const uint32_t direction)
{
    IOWR_ALTERA_AVALON_PIO_DIRECTION(kBASE_ADDR, direction);
}

/*! @brief Read direction [1:output, 0:input] */
uint32_t NiosGpio::readDirection() const
{
    return IORD_ALTERA_AVALON_PIO_DIRECTION(kBASE_ADDR);
}
/* mb_gpio.cpp */

/*! @brief Write direction [1:output, 0:input] */
void MbGpio::writeDirection(const uint32_t direction)
{
    XGpio_WriteReg(kBASE_ADDR, XGPIO_TRI_OFFSET, ~direction);
}

/*! @brief Read direction [1:output, 0:input] */
uint32_t MbGpio::readDirection() const
{
    return ~XGpio_ReadReg(kBASE_ADDR, XGPIO_TRI_OFFSET);
}

この実装によりPIO/GPIOの違いを吸収しましたので同じ制御方法で使用できるようになりました。

共通のプラットフォーム

このように各周辺回路を抽象化していくと、様々なCPU環境で共通に使用できるプラットフォームになります。 周辺回路のドライバモジュールの他にはコンテナや、Tickerのような時間サービスを提供するコンポーネントデバッグシェル等、 小規模な組み込みソフトウェア開発であれば、部品となるモジュールを組み立てるだけでほとんどコードを書かずに対応できることも多くなります。

会社組織として自社プロジェクト用のプラットフォームを作成、メンテナンスできれば良いのですが、 再利用の有効性が理解されておらず、技術者が個人的レベルでプロジェクトの合間に対応しているような場合も多いのではないでしょうか。 また「作ったら終わり」と認識されて、継続的なメンテナンスの必要性を理解してもらえないことも多いと感じています。

対象とする環境

共通プラットフォームを作成するにしても、 対象とする環境をある程度限定しなければパフォーマンス面はもちろんですが設計が難しくなってしまいますので、 ひとまずは次のような条件などで対応するとよいでしょう。

  • 小規模な組み込みシステム
  • CPUは32bit(20MHz~)
    • 命令キャッシュを利用可能
  • Timerを利用可能(可能なら32bit)
  • プログラミング言語(いずれか)
    • C言語はC99以降のサポート
    • C++C++03以降のサポート
  • シングルタスク(OSレス、タスクレスともいう)
    • ソフトマクロCPUを採用する場合が多いので最新の開発ツールへの対応スピードが理由です
    • 新規CPUの場合でもアセンブリ記述を必要としないので移植が楽になります
  • 設計
    • C言語でもC++でもコーディング可能な設計とする

なお、私が持っている環境では以下のCPUくらいですね(他のCPU評価ボードは規模や環境面で除外)。 RXマイコンは電源も未投入のまま...