カテゴリー「Windows 10 IoT」の記事

Windows 10 IoT Core用のBME280ライブラリ

Windows 10 IoT Core用に、BME280センサーのライブラリを作ってみました。BME280は3.3V動作の温度・湿度・気圧センサーで、秋月さんSwitch Scienceさんから購入できます。

ESP-WROOM-02 ArduinoでBME280を使った際は、Embedded Adventuresさんのライブラリを使っていました。Windows 10 IoT用のライブラリに関しては、類似品であるBMP280用のライブラリならNuGetで取得することができるのですが、BME280用のライブラリは見つかりませんでした。そのため、Adafruitのforumで紹介されていたBMP280用のライブラリをベースに自作(改造)してみました。

BMP280とBME280はレジスターの構成や設定はほんとんど共通のため、湿度読み取り機能を追加し、読み取り値の補正関数をBME280用に修正しています。

またオリジナルのAdafruit版は、I2Cのからのセンサーデーター読み取りなどで非同期処理(async/await)を多用していたのですが、データ読み取りの時間は瞬時で非同期処理を入れるオーバーヘッド(非同期用の別タスクを起こす処理)の方が大きそうな気がしたため、デバイスの初期化部分以外では非同期処理を使わないように変更しました。

作成したライブラリのコードを以下に示します。GitHubでもプロジェエクトファイルを公開しました:

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using Windows.Devices.Enumeration;
using Windows.Devices.I2c;

namespace Sensor.BME280
{
    public class BME280_CalibrationData
    {
        //BME280 Registers
        public ushort dig_T1 { get; set; }
        public short  dig_T2 { get; set; }
        public short  dig_T3 { get; set; }

        public ushort dig_P1 { get; set; }
        public short  dig_P2 { get; set; }
        public short  dig_P3 { get; set; }
        public short  dig_P4 { get; set; }
        public short  dig_P5 { get; set; }
        public short  dig_P6 { get; set; }
        public short  dig_P7 { get; set; }
        public short  dig_P8 { get; set; }
        public short  dig_P9 { get; set; }

        public byte   dig_H1 { get; set; }
        public short  dig_H2 { get; set; }
        public byte   dig_H3 { get; set; }
        public short  dig_H4 { get; set; }
        public short  dig_H5 { get; set; }
        public sbyte  dig_H6 { get; set; }
    }


    public class BME280
    {
        const byte BME280_Address   = 0x76;
        const byte BME280_Signature = 0x60;

        enum eRegisters : byte
        {
            BME280_REGISTER_DIG_T1 = 0x88,
            BME280_REGISTER_DIG_T2 = 0x8A,
            BME280_REGISTER_DIG_T3 = 0x8C,

            BME280_REGISTER_DIG_P1 = 0x8E,
            BME280_REGISTER_DIG_P2 = 0x90,
            BME280_REGISTER_DIG_P3 = 0x92,
            BME280_REGISTER_DIG_P4 = 0x94,
            BME280_REGISTER_DIG_P5 = 0x96,
            BME280_REGISTER_DIG_P6 = 0x98,
            BME280_REGISTER_DIG_P7 = 0x9A,
            BME280_REGISTER_DIG_P8 = 0x9C,
            BME280_REGISTER_DIG_P9 = 0x9E,

            BME280_REGISTER_DIG_H1 = 0xA1,
            BME280_REGISTER_DIG_H2 = 0xE1,
            BME280_REGISTER_DIG_H3 = 0xE3,
            BME280_REGISTER_DIG_H4_L = 0xE4,
            BME280_REGISTER_DIG_H4_H = 0xE5,
            BME280_REGISTER_DIG_H5_L = 0xE5,
            BME280_REGISTER_DIG_H5_H = 0xE6,
            BME280_REGISTER_DIG_H6 = 0xE7,

            BME280_REGISTER_CHIPID  = 0xD0,
            BME280_REGISTER_SOFTRESET = 0xE0,

            BME280_REGISTER_CONTROLHUMID = 0xF2,
            BME280_REGISTER_STATUS  = 0xF3,
            BME280_REGISTER_CONTROL = 0xF4,
            BME280_REGISTER_CONFIG  = 0xF5,

            BME280_REGISTER_PRESSUREDATA_MSB  = 0xF7,
            BME280_REGISTER_PRESSUREDATA_LSB  = 0xF8,
            BME280_REGISTER_PRESSUREDATA_XLSB = 0xF9, // bits <7:4>

            BME280_REGISTER_TEMPDATA_MSB  = 0xFA,
            BME280_REGISTER_TEMPDATA_LSB  = 0xFB,
            BME280_REGISTER_TEMPDATA_XLSB = 0xFC, // bits <7:4>

            BME280_REGISTER_HUMIDDATA_MSB = 0xFD,
            BME280_REGISTER_HUMIDDATA_LSB = 0xFE,
        };

        // Enables 2-wire I2C interface when set to ‘0’
        public enum interface_mode_e : byte
        {
            i2c = 0,
            spi = 1
        };

        // t_sb standby options - effectively the gap between automatic measurements 
        // when in "normal" mode
        public enum standbySettings_e : byte
        {
            tsb_0p5ms   = 0,
            tsb_62p5ms  = 1,
            tsb_125ms   = 2,
            tsb_250ms   = 3,
            tsb_500ms   = 4,
            tsb_1000ms  = 5,
            tsb_10ms    = 6,
            tsb_20ms    = 7
        };


        // sensor modes, it starts off in sleep mode on power on
        // forced is to take a single measurement now
        // normal takes measurements reqularly automatically
        public enum mode_e :byte
        {
            smSleep     = 0,
            smForced    = 1,
            smNormal    = 3
        };


        // Filter coefficients
        // higher numbers slow down changes, such as slamming doors
        public enum filterCoefficient_e : byte
        {
            fc_off  = 0,
            fc_2    = 1,
            fc_4    = 2,
            fc_8    = 3,
            fc_16   = 4
        };


        // Oversampling options for humidity
        // Oversampling reduces the noise from the sensor
        public enum oversampling_e : byte
        {
            osSkipped   = 0,
            os1x        = 1,
            os2x        = 2,
            os4x        = 3,
            os8x        = 4,
            os16x       = 5
        };


        //String for the friendly name of the I2C bus 
        private const string I2CControllerName = "I2C1";
        //Create an I2C device
        private I2cDevice bme280 = null;
        //Create new calibration data for the sensor
        private BME280_CalibrationData CalibrationData;

        // Value hold sensor operation parameters
        private byte int_mode = (byte)interface_mode_e.i2c;
        private byte t_sb;
        private byte mode;
        private byte filter;
        private byte osrs_p;
        private byte osrs_t;
        private byte osrs_h;

        public BME280(standbySettings_e t_sb = standbySettings_e.tsb_0p5ms, 
                      mode_e mode = mode_e.smNormal, 
                      filterCoefficient_e filter = filterCoefficient_e.fc_16,
                      oversampling_e osrs_p = oversampling_e.os16x, 
                      oversampling_e osrs_t = oversampling_e.os2x,
                      oversampling_e osrs_h = oversampling_e.os1x)
        {
            this.t_sb = (byte)t_sb;
            this.mode = (byte)mode;
            this.filter = (byte)filter;
            this.osrs_p = (byte)osrs_p;
            this.osrs_t = (byte)osrs_t;
            this.osrs_h = (byte)osrs_h;
        }


        //Method to initialize the BME280 sensor
        public async Task Initialize()
        {
            Debug.WriteLine("BME280::Initialize");

            try
            {
                //Instantiate the I2CConnectionSettings using the device address of the BME280
                I2cConnectionSettings settings = new I2cConnectionSettings(BME280_Address);
                //Set the I2C bus speed of connection to fast mode
                settings.BusSpeed = I2cBusSpeed.FastMode;
                //Use the I2CBus device selector to create an advanced query syntax string
                string aqs = I2cDevice.GetDeviceSelector(I2CControllerName);
                //Use the Windows.Devices.Enumeration.DeviceInformation class to create a collection using the advanced query syntax string
                DeviceInformationCollection dis = await DeviceInformation.FindAllAsync(aqs);
                //Instantiate the the BME280 I2C device using the device id of the I2CBus and the I2CConnectionSettings
                bme280 = await I2cDevice.FromIdAsync(dis[0].Id, settings);
                //Check if device was found
                if (bme280 == null)
                {
                    Debug.WriteLine("Device not found");
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine("Exception: " + e.Message + "\n" + e.StackTrace);
                throw;
            }

            byte[] readChipID = new byte[] { (byte)eRegisters.BME280_REGISTER_CHIPID };
            byte[] ReadBuffer = new byte[] { 0xFF };

            //Read the device signature
            bme280.WriteRead(readChipID, ReadBuffer);
            Debug.WriteLine("BME280 Signature: " + ReadBuffer[0].ToString());

            //Verify the device signature
            if (ReadBuffer[0] != BME280_Signature)
            {
                Debug.WriteLine("BME280::Begin Signature Mismatch.");
                return;
            }

            //Read the coefficients table
            CalibrationData = ReadCoefficeints();

            //Set configuration registers
            WriteConfigRegister();
            WriteControlMeasurementRegister();
            WriteControlRegisterHumidity();

            //Set configuration registers again to ensure configuration of humidity
            WriteConfigRegister();
            WriteControlMeasurementRegister();
            WriteControlRegisterHumidity();

            //Dummy read temp to setup t_fine
            ReadTemperature();
        }


        //Method to write the config register (default 16)
        //000  100  00 
        // ↑  ↑   ↑I2C mode
        // ↑  ↑Filter coefficient = 16
        // ↑t_sb = 0.5ms
        private void WriteConfigRegister()
        {
            byte value = (byte)(int_mode + (filter << 2) + (t_sb << 5));
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONFIG, value };
            bme280.Write(WriteBuffer);
            return;
        }

        //Method to write the control measurment register (default 87)
        //010  101  11 
        // ↑  ↑   ↑ mode
        // ↑  ↑ Pressure oversampling
        // ↑ Temperature oversampling
        private void WriteControlMeasurementRegister()
        {
            byte value = (byte)(mode + (osrs_p << 2) + (osrs_t << 5));
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONTROL, value };
            bme280.Write(WriteBuffer);
            return;
        }

        //Method to write the humidity control register (default 01)
        private void WriteControlRegisterHumidity()
        {
            byte value = osrs_h;
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONTROLHUMID, value };
            bme280.Write(WriteBuffer);
            return;
        }


        //Method to read a 16-bit value from a register and return it in little endian format
        private ushort ReadUInt16_LittleEndian(byte register)
        {
            ushort value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00, 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            int h = readBuffer[1] << 8;
            int l = readBuffer[0];
            value = (ushort)(h + l);
            return value;
        }

        //Method to read an 8-bit value from a register
        private byte ReadByte(byte register)
        {
            byte value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            value = readBuffer[0];
            return value;
        }

        //Method to read the caliberation data from the registers
        private BME280_CalibrationData ReadCoefficeints()
        {
            // 16 bit calibration data is stored as Little Endian, the helper method will do the byte swap.
            CalibrationData = new BME280_CalibrationData();

            // Read temperature calibration data
            CalibrationData.dig_T1 = ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T1);
            CalibrationData.dig_T2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T2);
            CalibrationData.dig_T3 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T3);

            // Read presure calibration data
            CalibrationData.dig_P1 = ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P1);
            CalibrationData.dig_P2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P2);
            CalibrationData.dig_P3 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P3);
            CalibrationData.dig_P4 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P4);
            CalibrationData.dig_P5 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P5);
            CalibrationData.dig_P6 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P6);
            CalibrationData.dig_P7 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P7);
            CalibrationData.dig_P8 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P8);
            CalibrationData.dig_P9 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P9);

            // Read humidity calibration data
            CalibrationData.dig_H1 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H1);
            CalibrationData.dig_H2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_H2);
            CalibrationData.dig_H3 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H3);
            short e4 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H4_L);    // Read 0xE4
            short e5 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H4_H);    // Read 0xE5
            CalibrationData.dig_H4 = (short)((e4 << 4) + (e5 & 0x0F));
            short e6 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H5_H);    // Read 0xE6
            CalibrationData.dig_H5 = (short)((e5 >> 4) + (e6 << 4));
            CalibrationData.dig_H6 = (sbyte)ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H6);

            return CalibrationData;
        }


        //t_fine carries fine temperature as global value
        int t_fine;

        //Method to return the temperature in DegC. Resolution is 0.01 DegC. Output value of “51.23” equals 51.23 DegC.
        private double BME280_compensate_T_double(int adc_T)
        {
            double var1, var2, T;

            //The temperature is calculated using the compensation formula in the BME280 datasheet
            var1 = (adc_T / 16384.0 - CalibrationData.dig_T1 / 1024.0) * CalibrationData.dig_T2;
            var2 = ((adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0) * 
                (adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0)) * CalibrationData.dig_T3;

            t_fine = (int)(var1 + var2);

            T = (var1 + var2) / 5120.0;
            return T;
        }


        //Method to returns the pressure in Pa, in Q24.8 format (24 integer bits and 8 fractional bits).
        //Output value of “24674867” represents 24674867/256 = 96386.2 Pa = 963.862 hPa
        private long BME280_compensate_P_Int64(int adc_P)
        {
            long var1, var2, p;

            //The pressure is calculated using the compensation formula in the BME280 datasheet
            var1 = (long)t_fine - 128000;
            var2 = var1 * var1 * CalibrationData.dig_P6;
            var2 = var2 + ((var1 * CalibrationData.dig_P5) << 17);
            var2 = var2 + ((long)CalibrationData.dig_P4 << 35);
            var1 = ((var1 * var1 * CalibrationData.dig_P3) >> 8) + ((var1 * CalibrationData.dig_P2) << 12);
            var1 = (((long)1 << 47) + var1) * CalibrationData.dig_P1 >> 33;
            if (var1 == 0)
            {
                Debug.WriteLine("BME280_compensate_P_Int64 Jump out to avoid / 0");
                return 0; //Avoid exception caused by division by zero
            }
            //Perform calibration operations as per datasheet: 
            p = 1048576 - adc_P;
            p = (((p << 31) - var2) * 3125) / var1;
            var1 = ((long)CalibrationData.dig_P9 * (p >> 13) * (p >> 13)) >> 25;
            var2 = ((long)CalibrationData.dig_P8 * p) >> 19;
            p = ((p + var1 + var2) >> 8) + ((long)CalibrationData.dig_P7 << 4);
            return p;
        }


        // Returns humidity in %rH as as double. Output value of “46.332” represents 46.332 %rH
        private double BME280_compensate_H_double(int adc_H)
        {
            double var_H;

            var_H = t_fine - 76800.0;
            var_H = (adc_H - (CalibrationData.dig_H4 * 64.0 + CalibrationData.dig_H5 / 16384.0 * var_H)) *
                CalibrationData.dig_H2 / 65536.0 * (1.0 + CalibrationData.dig_H6 / 67108864.0 * var_H *
                (1.0 + CalibrationData.dig_H3 / 67108864.0 * var_H));
            var_H = var_H * (1.0 - CalibrationData.dig_H1 * var_H / 524288.0);

            if (var_H > 100.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump out to 100%");
                var_H = 100.0;
            } else if (var_H < 0.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump under 0%");
                var_H = 0.0;
            }

            return var_H;
        }


        public float ReadTemperature()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the temperature from the BME280 registers
            byte tmsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_MSB);
            byte tlsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_LSB);
            byte txlsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int t = (tmsb << 12) + (tlsb << 4) + (txlsb >> 4);

            //Convert the raw value to the temperature in degC
            double temp = BME280_compensate_T_double(t);

            //Return the temperature as a float value
            return (float)temp;
        }

        public float ReadPreasure()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the pressure from the BME280 registers
            byte pmsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_MSB);
            byte plsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_LSB);
            byte pxlsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int p = (pmsb << 12) + (plsb << 4) + (pxlsb >> 4);

            //Convert the raw value to the pressure in Pa
            long pres = BME280_compensate_P_Int64(p);

            //Return the pressure as a float value
            return ((float)pres) / 256;
        }

        public float ReadHumidity()
        {
            //Read the MSB and LSB of the humidity from the BME280 registers
            byte hmsb = ReadByte((byte)eRegisters.BME280_REGISTER_HUMIDDATA_MSB);
            byte hlsb = ReadByte((byte)eRegisters.BME280_REGISTER_HUMIDDATA_LSB);

            //Combine the values into a 32-bit integer
            int h = (hmsb << 8) + hlsb;

            //Convert the raw value to the humidity in %
            double humidity = BME280_compensate_H_double(h);

            //Return the humidity as a float value
            return (float)humidity;
        }

        //Method to take the sea level pressure in Hectopascals(hPa) as a parameter and calculate the altitude using current pressure.
        public float ReadAltitude(float seaLevel)
        {
            //Read the pressure first
            float pressure = ReadPreasure();
            //Convert the pressure to Hectopascals(hPa)
            pressure /= 100;

            //Calculate and return the altitude using the international barometric formula
            return 44330.0f * (1.0f - (float)Math.Pow((pressure / seaLevel), 0.1903f));
        }
    }
}

ライブラリを使用したセンサーデーター読み取りアプリのサンプルは以下です:

using System;
using System.Threading;
using System.Diagnostics;
using Windows.UI.Xaml.Controls;
using Sensor.BME280;


namespace BME280_Test
{
    public sealed partial class MainPage : Page
    {
        private BME280 bme280;
        private Timer periodicTimer;

        public MainPage()
        {
            this.InitializeComponent();

            bme280 = new BME280();
            initBme280();
        }

        private async void initBme280()
        {
            await bme280.Initialize();
            periodicTimer = new Timer(this.TimerCallback, null, 0, 1000);
        }

        private void TimerCallback(object state)
        {
            var temp = bme280.ReadTemperature();
            var press = bme280.ReadPreasure() / 100;
            var humidity = bme280.ReadHumidity();
            var alt  = bme280.ReadAltitude(1013);   // 1013hPa = pressure at 0m
            Debug.WriteLine("Temp:{0:F2}℃ Humidity:{1:F2}% Press:{2:F2}hPa Alt:{3:F0}m", temp, humidity, press, alt);

            var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                tempValue.Text = temp.ToString("F2") + "℃";
                humValue.Text = humidity.ToString("F2") + "%";
                pressValue.Text = press.ToString("F2") + "hPa";
            });

        }
    }
}

電源オンの初回起動時に湿度情報が読み取れない問題があり(一旦アプリを終了して再度起動すると正常になる)、回避策としてレジスタの初期化を2回行っています(ライブラリソースの226行目)。Arudino用のライブラリコードを見ると設定を2回やるような処理は行っておらず、この点は謎です。

BME280のセンサーとしての精度ですが、手持ちのAM2302に比べて温度が高め、湿度が低めに出ます。湿度は5%以上の差分があり、センサー種別によって測定値に大きなばらつきがあります。温度はその他手持ちの温度計と比較するとAM2302の方が正確なため、湿度もAM2302の方が正確ではないかと思います。

Windows 10 IoT CoreでI2Cを使う

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クラスでもよいような気がします。

参考情報


Windows 10 IoT Core Build 10586を使ってみる

最近Raspberry Pi用のWindows 10 IoT Coreを触っています。当初はBuild 10556を使っていましたが、10586が最近リリースされました。Build 10586になってインストールの方法やデバック時の設定が変わっているため、以下の通り整理してみます。

Windows 10 IoT Coreのインストール

Windows 10が動くPCを用意します。まず、リンクのサイトにアクセスし、「Get the Windows 10 IoT Core Dashboard」をクリクしてWindows 10 IoT Core DashboardをPCにインストールします。

SetupWindowsIoT_Device.PNG

Build 10556ではISOファイルをダウンロードしてインストールを行いましたが、Build 10586ではIoT Core DashbordがイメージのダウンロードとSDカードへの書き込みを行ってくれます。Windows 10 IoT Coreのイメージを書き込むSDカードをPCにセットしてWindows 10 IoT Core Dashboardを起動すると以下の画面が表示されますので、デバイスの種類(Raspberry Pi 2)とSDカードのドライブを指定して「ダウンロードとインストール」ボタンを押します。

Windows10_IoT_Install.PNG

SDカードへの書き込みが始まると、以下の画面が表示されます。

Windows10_IoT_ImageFlush.PNG

イメージの書き込みが完了したら、SDカードをRaspberry Piにセットして起動します。Raspberry PiにはディスプレーとEthernetケーブルをつないでおきます。

初回の起動には結構時間がかかるので気長に待ちます。Build 10556では起動中に画面がブラックアウトした時間が長く、固まってしまったのかと思ったことがありましたが、Build 10586は画面がブラックアウトすることはなく処理中の表示が回っているので改善が見られます。

Windows 10 IoT Coreが立ち上がったら、ディスプレーにDHCPで割り当てられたIPアドレスが表示されているので、Webブラウザーから<IPアドレス>:8080にアクセスすると管理Web画面が表示されます(ユーザー名:Administrator、初期パスワード:p@ssw0rdでログイン)。ログインできたら、パスワードやデバイス名の変更が管理Webの画面から行えます。

Lチカプログラムの実行

お約束のLチカをやってみます。開発環境のVisual Studio 2015はこのリンクなどの手順に従ってセットアップされているとします。Build 10586のWindows 10 IoT Coreで開発を行う際は、以下の追加インストールが必要です:

  • Visual Studio 2015 Update 1のインストール:ここからプログラムをダウンロードしてインストール。Update 1をインストールしないとBuild 10586では、ターゲット(Raspberry Pi)へのプログラムのアップロードが行えませんので必ずUpdateが必要です
  • Windows IoT Core Project Templatesを最新版に更新:ここからテンプレートをダウンロード。すでに旧版がインストール済みの場合は、VS2015から一旦旧版を削除して再度インストールします

VS2015を起動したら以下の設定を行います

  • 新しいプロジェクトの作成を行い、テンプレートに「ユニバーサル」「空白のアプリ」を選択して、適当な名前をつける
  • 参照設定に「Windows IoT Extensions for the UWP」を追加

プロジェクトが作成できたら、MainPage.xaml.csに以下のコードを入力。

using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
using Windows.Devices.Gpio;

// 空白ページのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 を参照してください

namespace LED
{
    /// <summary>
    /// Lチカサンプル
    /// <summary>
    public sealed partial class MainPage : Page
    {
        private const int LED_PIN = 5;
        private GpioPin pin;

        public MainPage()
        {
            this.InitializeComponent();

            // GPIOの初期化メソッドを呼び出します
            InitGPIO();

            // LEDの ON / OFFのためのループ処理を呼び出します
            loop();
        }

        private void InitGPIO()
        {
            var gpio = GpioController.GetDefault();

            // GPIOコントローラーがない場合
            if (gpio == null)
            {
                pin = null;
                return;
            }
            // GPIOの5番ピンを開きます
            pin = gpio.OpenPin(LED_PIN);

            // 5番ピンをHighに設定します
            pin.Write(GpioPinValue.High);

            // 5番ピンを出力として使うよう設定します
            pin.SetDriveMode(GpioPinDriveMode.Output);
        }

        // 1秒おきでLEDをON/OFFさせるループの処理
        private async void loop()
        {
            while(true)
            {
                pin.Write(GpioPinValue.Low);
                await Task.Delay(500);
                pin.Write(GpioPinValue.High);
                await Task.Delay(500);
            }
        }
    }
}

ビルドができたら、以下の手順でデバックを開始し、Raspberry Piへプログラムをアップロードします
  • デバックセッションの接続先として「リモートコンピューター」を選択
  • Raspberry PiのIPアドレスを入力
  • 認証モードは「ユニバーサル」を選択。Build 10586では認証モードを「ユニバーサル」にしないとターゲット(Raspberry Pi)へのプログラムのアップロードが行えません
  • プロジェクトのプロパティーを開いて、デバックオプションの認証モードが「Universal」になっていることを確認(もしくはここで、Universalを選択)。

VS2015_DebugSetting.PNG

参考情報



2017年10月
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
無料ブログはココログ