Windows 10 IoT CoreでI2Cを動かしてみました。Windows 10 IoT Coreは執筆時点の最新版であるBuild 10586を使用しています。Windows 10 IoT CoreではRaspberry Piのような組み込み系ボードを使う場合でも、アプリ作成はUniversal Windows Platform (UWP)の作法に従う必要があります。コードは参考文献のものを流用していますが、当方が初めて見る非同期処理などが使われており、最初はコードの意味がわかりませんでした。
当方、Windowsのプログラムは、VS2008の時代に.Net Frameworkを使ったWindows Formプログラムを少々かじりましたが、確かC# 2.0が出た頃でラムダ式はありましたが、非同期プログラミングの概念はまだなかったと思います。ですので、UWPの流儀はWindowsプログラミングの経験的にも初めての要素が多かったです。(注:.NET 1.0の時代からDelegateを使った非同期プログラミングの概念はあったとのコメントをいただきました。ただし大変煩雑だったようです)
非同期プログラミングは、WindowsストアアプリやiOSアプリを作っている方には当たり前の処理だと思うので今更ではありますが、サンプルコードから私が理解したことを記載します(間違っていたらごめんなさい)。シングルスレッドのmbed/Arduino用プログラムを見慣れた(というか、これしか知らない)私には結構新鮮でした。
サンプルコード
TMP102温度センサーを使用して、測定値を画面に表示します。画面はTextBlockを一つ表示するだけの単純なものです。
using System; using System.Diagnostics; using System.Threading; using Windows.UI.Xaml.Controls; using Windows.Devices.I2c; using Windows.Devices.Enumeration; using Windows.ApplicationModel.Core; namespace TMP102 { /// <summary> /// TMP102を使用した温度測定 /// </summary> public sealed partial class MainPage : Page { private I2cDevice TMP102; private Timer periodicTimer; public MainPage() { this.InitializeComponent(); Unloaded += MainPage_Unload; InitTMP102(); } private void MainPage_Unload(object sender, object args) { TMP102.Dispose(); } private async void InitTMP102() { try { // Get a selector string for bus "I2C1" string aqs = I2cDevice.GetDeviceSelector("I2C1"); // Find the I2C bus controller with our selector string var dis = await DeviceInformation.FindAllAsync(aqs); if (dis.Count == 0) { Debug.WriteLine("No I2C bus found"); CoreApplication.Exit(); } string deviceID = dis[0].Id; var setting = new I2cConnectionSettings(0x48); setting.BusSpeed = I2cBusSpeed.FastMode; // Create an I2cDevice with our selected bus controller and I2C settings TMP102 = await I2cDevice.FromIdAsync(deviceID, setting); } catch(Exception error) { Debug.Write("TMP102 Instantiation Error: " + error.Message); CoreApplication.Exit(); } periodicTimer = new Timer(this.TimerCallback, null, 0, 1000); } // スレッドプールにキューキングされ、Timerクラスをインスタンス化した // スレッド(UIスレッド)とは異なるスレッドで実行される private void TimerCallback(object state) { byte[] readBuf = new byte[2]; try { TMP102.Read(readBuf); } catch(Exception error) { Debug.Write("I2C Read Error: " + error.Message); CoreApplication.Exit(); } float temperature = CalcTemperature((int)readBuf[1], (int)readBuf[0]); string temperatureText = String.Format("Temperature : {0:F2}℃", temperature); // 直接 textBlock.Text = temperatureText; と書くと例外が発生する // UIスレッドを操作するためには以下のように記述する var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { textBlock.Text = temperatureText; }); Debug.WriteLine("Temperature:{0:F2}℃", temperature); } private float CalcTemperature(int valh, int vall) { int res = (vall << 4 | valh >> 4); float temperature = (float)res * 0.0625f; return temperature; } } }
async/awaitによる非同期処理
Windows 10 IoT CoreもWindowsのファミリーなので、MainPageクラスはメインスレッドで動き、UI操作などのイベントが配信されます。メインスレッド内で重たい処理を実行してしまうと、処理が完了するまでの間他のイベントを処理できなくなり、UIが固まってしまいます。そのため、メインスレッドが長時間ブロックされるのを防ぐために、重たい処理を別スレッドで実行する非同期処理を使います。Windows 10 IoT Coreのデバイス操作でも非同期処理が多用されるようです。非同期処理の概要を以下に記載します。
40行目で「await演算子」がDeviceInformation.FindAllAsync(aqs)の呼び出しに付与されています。awaitを指定することによって、このメソッドを別スレッドで非同期に実行します。メソッド内に非同期処理が含まれる場合は、メソッド名(この場合はInitTMP102())に「async修飾子」をつけます。
40行目のawait DeviceInformation.FindAllAsync(aqs);を実行すると、InitTMP102()メソッドは一旦終了し(処理を呼び出し元に戻し)、他のイベントを受け取れるようにします。DeviceInformation.FindAllAsync(aqs)の処理が終了すると、41行目以降の処理を自動的に再開します。従来の非同期プログラムスタイルだと、非同期処理終了後の後処理(41〜51行目に相当する部分)を別のブロック(終了時に呼び出されるコールバックやクロージャー)に記述したりしますが、C# 5.0では一連の処理として記述できるためコードの見通しがよく大変便利です。
周期タイマー処理
このサンプルでは、Timerクラスを使って、64行目のTimerCallback()メソッドを1000ms周期で実行します。タイマ・メソッドは.NET Frameworkが管理するスレッド・プールにキューイングされて実行されるため、TimerCallback()メソッドはTimerクラスをインスタンス化したメインスレッドとは異なるスレッドで実行されます。
TimerCallback()メソッド内で測定した温度を画面に表示していますが、ここで注意が必要です。上記に示した通り、タイマー処理は別スレッドで動いているため、メインスレッドで処理すべきUIの操作ができません。例えば、このタイマースレッド内で、textBlock.Text = temperatureText;を実行すると例外が発生します。
タイマースレッド内でUIの操作依頼を行うために、83行目のDispatcher.RunAsyncによって、クロージャーとして指定した処理ブロックをUIスレッドにキューイングしています。
メインスレッド内で動くDispatcherTimerクラスを使えば上記のDispatcher.RunAsync処理は不要で、タイマー処理の中で直接UIを操作できますが、重たい処理を記述するとUIが固まってしまうため注意が必要です。Windows 10 IoT Coreを使った組み込み系のプログラムでは複雑なUIを使うことはないため、DispatcherTimerクラスでもよいような気がします。
参考情報
- Raspberry Pi 2とWindows 10ではじめるIoTプログラミング
- C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門
- タイマにより一定時間間隔で処理を行うには?(スレッド・タイマ編)