Windows IoT Coreで赤外線リモコンの信号を受信する

Receive infrared signal by Windows IoT Core
Windows IoT開発者向けフォーラムでは「WindowsはリアルタイムOSではないので、ArduinoやRaspbianみたいに赤外線信号を受信することは無理」とドヤ顔で回答する人がいます。確かに、単純にユニバーサルアプリを作っただけでは受信精度が低く、満足いく結果が得られないのは事実です。が、そもそもWindows IoT Coreはその名の通り、IoTデバイスで利用されるために開発されたOSです。マイクロソフトはラズパイでWindowsアプリを買わせるためだけに、このOSをわざわざ作ったのでしょうか?

知ったかぶりの回答者に対する嫌みはここまでにして、本題に入ることにします。Windows 10 IoT Coreには、OSを介さず、ハードウェアへ直接命令を渡す低レベルデバイスアクセスが用意されており、これを使うことでGPIOの処理速度を劇的に上げることができます。マイクロソフトによるオシロスコープを使った検証では、ネイティブコンパイルを行ったC#プログラムでRaspberry Pi 2に搭載されているGPIOのオン・オフが約1.45MHzによる周期で計測されたとしています。平たくいえば、1秒間にLEDを100万回は優に点滅させることができるようになるということになります。

低レベルデバイスを有効にするにはOSとアプリの両方で準備が必要になります。まずはパソコンより「Windows 10 IoT Core Dashboard」を実行して、「自分のデバイス」に表示されている機器より選択するか、ブラウザに直接IPアドレスを入力してデバイスポータルを開きます。ログインするには初期状態ではユーザー名は「Administrator」、パスワードは「p@ssw0rd」もしくはインストール時にユーザーが定義したものになります。
ポータルの「Devices」より「Default Contoller Driver」を「Inbox Driver」から「Direct Memory Mapped Driver」に変更してRaspberry Piを再起動します。もし、この選択肢が表示されていないようであればWindows IoTのバージョンを「10.0.10586(TH2に相当。ちなみにAnniversary Updateのバージョンは10.0.14393)」以降にアップデートしましょう。OSでの設定はこれだけです。
次はアプリの作成です。いつものようにユニーバーサルアプリプロジェクト(※JavaScriptやPythonなどのネイティブコンパイルができない言語では効果がほとんど発揮されないのでC#を推奨)を作成したら、Visual Studioの「ツール→NuGetパッケージマネージャー→ソリューションのNuGetパッケージの管理」を選択して表示されるページより、「Microsoft.IoT.Lightning」を参照、導入します。
プロジェクトのappxmanifestファイルをコードエディタで開き、低レベルデバイスの使用に関する宣言を追記します。

BeforeAfter
<?xml version="1.0" encoding="utf-8"?> <Package xmlns="//schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="//schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="//schemas.microsoft.com/appx/manifest/uap/windows10" IgnorableNamespaces="uap mp"> : : : <Capabilities> <Capability Name="internetClient" /> </Capabilities> </Package>
<?xml version="1.0" encoding="utf-8"?> <Package xmlns="//schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="//schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="//schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:iot="//schemas.microsoft.com/appx/manifest/iot/windows10" IgnorableNamespaces="uap mp iot"> : : : <Capabilities> <Capability Name="internetClient" /> <iot:Capability Name="lowLevelDevices" /> <DeviceCapability Name="109b86ad-f53d-4b76-aa5f-821e2ddf2141"/> </Capabilities> </Package>
MainPageのコンストラクタなど、GpioControllerを取得する前のコードでLightningProviderを有効にします。「using Microsoft.IoT.Lightning.Providers;」も忘れずに。
lprovider.cs
if (LightningProvider.IsLightningEnabled)
{
    LowLevelDevicesController.DefaultProvider = LightningProvider.GetAggregateProvider();
}
下準備は以上です。赤外線受信モジュールを設置しただけの非常にシンプルなものですが、ブレッドボードへの配線例を掲載しておきます。
リモコンから出力される赤外線は電灯などの光線との混同を防ぐため、信号がOnの状態でも点灯ではなく38kHzで常に点滅しているのが特徴です。ですが、リモコン用の受信モジュールであれば中にICが入っており、この点滅をひとつの信号と見なしてくれるので、特別なプログラムを組む必要はありません。電子パーツ店で赤外線受信モジュールといえば、まずリモコン用ですが、購入前に型番などを確認しておくとより確実でしょう。また、モジュールのメーカーによってはピンの配置が異なってくるので適時修正してください。

以下はプログラム例です(Visual Studioのフルプロジェクトはこちらからダウンロードできます)。XAMLで配置したボタンをクリックしてから5秒以内にリモコンのボタンを押して受信モジュールに信号を送ると、解析結果をテキストボックスに表示します。なお、受信結果の比較を容易にするため、あえておもしろまじめ電子工作という技術本のArduinoコードと同じ結果をテキストボックスに出力するようにしていますので、Arduinoをお持ちの方はそちらの参考書で書いたプログラムとの結果を見比べてみてください。

ちなみに、Arduino Unoは普通に買うと3000円前後ほどしますが、Arduinoは設計図が公開されていて、商標を使わない限りは互換品を自由に作ってもよいため、AmazonでArduino 互換と検索すると、Arduinoによく似た名前の同等の機能を持つ互換機をわずか数百円で購入することができます。
remocon.cs
private const long timeoutMicros = 5000000;
private const long waitMicros = 1000000;

private long micros()
{
    return (long)(sw.Elapsed.TotalMilliseconds * 1000.0);
}

private bool waitChange(GpioPinValue status, bool began)
{
    long waitStartMicros = micros();
    long waits = (began == true) ? timeoutMicros : waitMicros;
    while (irpin_in.Read() == status) {
        if (micros() - waitStartMicros > waits) return true;
    }
    return false;
}

private void OnClickReceive(object sender, RoutedEventArgs e)
{
    SetButtonEnable(false);

    Task.Run(async () => {
        GpioPinValue moduleStatus;
        long now, lastMicros;
        sw.Restart();

        now = micros();
        lastMicros = micros();

        bool began = false;
        int stcount = 0;

        moduleStatus = irpin_in.Read();

        while (stcount < ST_MAX) {
            bool waitStatus = waitChange(moduleStatus, began);
            if (waitStatus == true) {
                if (began == true) {
                    break;
                } else {
                    continue;
                }
            } else {
                Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                    txtSignal.Text = "Analyzing...";
                });
                began = true;
            }

            now = micros();
            signalTimes[stcount++] = (int)((now - lastMicros) / 10);
            lastMicros = now;

            moduleStatus = (moduleStatus == GpioPinValue.High) ? GpioPinValue.Low : GpioPinValue.High;
        }

        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i < stcount; i++) {
                sb.AppendFormat("{0},", signalTimes[i]);
            }
            txtSignal.Text = sb.ToString();

            SetButtonEnable(true);
        });
    });
}
結果は正しく取得できたでしょうか。精度のばらつきが大きいようであれば、受信モジュールにノイズが乗っかっている可能性もあるので、そのときはコンデンサーや抵抗を組み合わせるなどして、ハードウェア面でのノイズの除去も試してみるとよいでしょう。<
なお、Windows 10 IoT Coreにはこれよりもさらに精度の高いGPIOを制御する方法が存在しますが、それについては次回に解説。
2016/11/01