Windows IoT CoreのArduino APIをフォアグラウンドアプリに埋め込む

Using Arduino APIs with Windows IoT Core foreground apps
過去に何度も取り上げているArduino Wiring on Windows 10 IoT Coreですが、ドライバーの開発における勉強の過程で、「Arduino APIは所詮C++ラッパー。別にバックグラウンドアプリでなくても使えるのではないか」と思い、UWPプロジェクトでArduino関数を使う方法を試してみました。

まずはVisual StudioでUWPアプリプロジェクトを作成します。しかし、メインアプリの開発にC++/CXを使うのは相当な困難が伴いますので、GUI部分はC#言語で開発し、C++/CXランタイムコンポーネントでGPIOを制御することにしました。

そのため、Visual C++ユニバーサルWindowsランタイムライブラリプロジェクトを追加することにします。このプロジェクトに「Windows Desktop Extensions for the UWP」と「Windows IoT Extensions for the UWP」参照を追加し、NuGetより「Microsoft.IoT.Lightning」を登録します。

コンポーネントにはマニフェストの概念がないため、低レベルアクセスのパーミッションはC#アプリのappxmanifestに追加することになります。具体的な追加方法は過去の当該記事をご覧ください。

続いてC++/CXプロジェクトでラッパークラスを作ります。Windows IoT CoreのArduino APIはMicrosoft.IoT.Lightningに含まれる「Arduino.h」をインクルードするだけで、C++/CXファイルからでも簡単に呼び出すことができます。
lightning.h
namespace ArduinoLibrary
{
public enum class GpioPin
{
    P2 = 3, P3 = 5, P4 = 7, ...
};

public ref class Arduino sealed
{
public:
    static void pinMode(GpioPin pin, PinMode mode);
    static PinValue digitalRead(GpioPin pin);
    static void digitalWrite(GpioPin pin, PinValue state);
};
}
lightning.cpp
#include <arduino.h>
#include "ArduinoFunctions.h"

using namespace Platform;
using namespace ArduinoLibrary;

void Arduino::pinMode(GpioPin pin, PinMode mode)
{
    ::pinMode((byte)pin, (uint8_t)mode);
}

PinValue Arduino::digitalRead(GpioPin pin)
{
    return (PinValue)::digitalRead((byte)pin);
}

void Arduino::digitalWrite(GpioPin pin, PinValue state)
{
    return ::digitalWrite((byte)pin, (uint8_t)state);
}
C#から呼び出す例です。
lightning.cs
private async void RuntimeTest()
{
    await Task.Run(() => {
        Arduino.pinMode(GpioPin.P5, PinMode.OUTPUT);
        Arduino.pinMode(GpioPin.P6, PinMode.OUTPUT);
        Arduino.digitalWrite(GpioPin.P5, PinValue.LOW);
        Arduino.digitalWrite(GpioPin.P6, PinValue.LOW);

        while (true) {
            uint m = Arduino.millis();
            Arduino.digitalWrite(GpioPin.P5, PinValue.HIGH);
            Arduino.delay(10);

            while (Arduino.millis() - m < 10000) {
                Arduino.digitalWrite(GpioPin.P6, PinValue.HIGH);
                Arduino.delayMicroseconds(50);
                Arduino.digitalWrite(GpioPin.P6, PinValue.LOW);
            }

            Arduino.delay(10);
            Arduino.digitalWrite(GpioPin.P5, PinValue.LOW);

            Arduino.delay(500);
        }
    });
}
……と、ここまで書いておいてなんですが、この方法だとWindowsランタイムが頻繁に介入するため、実際に動かしてみると、速度やタイミングが不安定で、ダイレクトメモリーマッピングの恩恵をあまり受けられないのが実情です。そのため、以下のようにGPIOを制御するルーチンはできる限りランタイムライブラリにまとめた方が良いでしょう。
freq.cpp
void Arduino::frequencyTest1()
{
    ::pinMode(GPIO5, DIRECTION_OUT);
    ::pinMode(GPIO6, DIRECTION_OUT);
    ::digitalWrite(GPIO5, LOW);
    ::digitalWrite(GPIO6, LOW);

    while (true) {
        unsigned long m = millis();
        ::digitalWrite(GPIO5, HIGH);
        ::delay(10);

        while (millis() - m < 10000) {
            ::digitalWrite(GPIO6, HIGH);
            ::delayMicroseconds(50);
            ::digitalWrite(GPIO6, LOW);
        }

        ::delay(10);
        ::digitalWrite(GPIO5, LOW);

        ::delay(500);
    }
}
おまけ:inoファイルをC++/CXプロジェクトに追加する方法
inoファイルをVisual C++にC言語と見なさせることで、ランタイムライブラリでもinoファイルを直接プロジェクトに追加することができます。任意の名前を持つinoファイルをプロジェクトに追加したら、ソリューションエクスプローラーより、inoファイルのプロパティを開きます。ここより「全般」の「項目の種類」に「C/C++コンパイラ」を選択し、「C/C++詳細設定」の「必ず使用されるインクルードファイル」に「Arduino.h」を追加します。
これにより、inoファイルに特別な定義を追加せずとも、Arduino IDE同様の記述ができるようになるため、Arduino向けのプログラムの移植がより容易になります。
freq2.cpp
// C++/CX file
void setup();
void loop();

void Arduino::frequencyTest2()
{
    setup();
    while(true) loop();
}
test.ino
// test.ino
void setup()
{
    pinMode(GPIO5, OUTPUT);
    pinMode(GPIO6, OUTPUT);
    digitalWrite(GPIO5, LOW);
    digitalWrite(GPIO6, LOW);
}

void loop()
{
    unsigned long m = millis();
    digitalWrite(GPIO5, HIGH);
    delay(10);

    while (millis() - m < 10000) {
        digitalWrite(GPIO6, HIGH);
        delayMicroseconds(50);
        digitalWrite(GPIO6, LOW);
    }

    delay(10);
    digitalWrite(GPIO5, LOW);

    delay(500);
}
以前紹介したバックグラウンドアプリと通信する方法だと、リソースが不足したときにOSがバックグラウンドアプリを止めてしまう可能性があります。そのため、アプリが動いている間はGPIO制御の動作も保証されるという点を考慮すると、フォアグラウンドアプリとして埋め込む利点は十分にあると思います。

ちなみにArduino UnoとRaspberry Piを接続して簡易的な性能テストをしてみましたが、C++/CXにAPIを埋め込む方法と、inoファイルに記述した関数を呼び出す方法に目立つ性能差はありませんでしたので、どちらを採用するかは開発者の好みでよいかと思います。サンプルプログラムにはテスト内容やinoファイルまで今回紹介した内容一式がすべて含まれているので、オシロスコープなどの高度な計測ができる機器で試されたら、結果を私にも教えてください。

2017/05/19