Windows 10 IoT Core for Raspberry Pi2 と周辺機器をシリアル通信(RS232c)でつなぐ

このポストでは、Windows 10 IoT Coreで動くRaspberry Pi2を、大昔からの定番、RS232cシリアル通信で周辺機器と接続する方法を解説します。

※本ポストは、https://ms-iot.github.io/content/en-US/win10/samples/SerialSample.htm を元に書いています。
※本ポストは、2015/10/30現在の情報を元に記載しています。OSの機能サポートの状況など、適宜確認してくださいね。

昔からPCや制御用コンピュータと周辺機器をつないで装置を開発していた皆さんなら全員ご存じのはずのRS232Cによるシリアル通信で、かつ、評価ボード系の周辺デバイスをリード線等でつなぐケースを解説します。

まずはつなぎ方です。

図の様に、

  • GND ‐ Pin 6
  • TX - Pin 8
  • RX - Pin 10

でつなぎます。TXは送信、RXは送信なので、接続先の周辺機器はつなぎ方が逆になります。
この接続で、Windows 10 IoT Coreは、URT0というポートでシリアル接続されたと認識します。

※この投稿を実際に試すには、10/30現在、https://ms-iot.github.io/content/en-US/Downloads.htm から公開中のInsider Previewのほうをダウンロードして作成したOSイメージを使ってくださいね

プログラムの大まかな流れは、

  • シリアルポートを探す
  • シリアルポートを開く
  • データ送信・受信を行う

です。アプリケーションは、Windowsのユニバーサルアプリ形式で開発してください。

※GPIOやI2Cを使ったアプリを組むのに必要だった、IoT Extensionの参照追加は必要ありません。

シリアルポートを利用するには、Package.appxmanifestファイルにシリアルポート利用の宣言が必要です。プロジェクトが格納されているフォルダーを開き(ソリューションエクスプローラーで、ソリューション、もしくは、プロジェクトを右クリックして、”エクスプローラーでフォルダーを開く”を選択すれば開けます)、あらかじめ起動しておいたメモ帳にファイルエクスプローラーでPackage.appxmanifestを選択しドラッグ&ドロップし、<Capability>…</Capability>を以下の様に修正します。

  <Capabilities>
    <Capability Name="internetClient" />
    <DeviceCapability Name="serialcommunication">
      <Device Id="any">
        <Function Type="name:serialPort" />
      </Device>
    </DeviceCapability>
  </Capabilities>

赤字の部分を追加

修正したら保存を忘れずに。

さてプログラムです。
まず、シリアルポートを探すロジックですが、

        List<DeviceInformation> listOfDevices = new List<DeviceInformation>();
  
        private async void ListAvailablePorts()
        {
            try
            {
                string aqs = SerialDevice.GetDeviceSelector();
                var dis = await DeviceInformation.FindAllAsync(aqs);
                for(int i = 0; i < dis.Count; i++)
                {
                    listOfDevices.Add(dis[i]);
                }
            }
            catch(Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

Windows.Devices.SerialCommunicationという名前空間にあるSerialDeviceクラスを使って、Windows.Devices.Enumerationという名前空間のDeviceInforamtionクラスで定義されたシリアル通信のデバイス情報を取り出します。名前空間のusing宣言は適宜追加してくださいね。
※例えば、DeviceInformationという文字列にマウスカーソルを持って行ったときに表示される豆電球のようなアイコンをクリックすると、using宣言を追加する項目が表示され、選択するとソースコードに追加されます。

冒頭の図のような接続で、かつ、USB接続によるシリアルデバイスがつながっていなければ、URT0ポートに関する情報が一つだけ、listOfDevicesに格納されます。
※ここで説明されている内容は、Windows 10 IoT Coreだけでなく、通常のWindows 10でも使えるコードです。

次にシリアルポートを開くコードです。RS232c由来のシリアル通信では、通信のボーレート、パリティ、ストップビット、データビット、ハンドシェークを指定する必要があります。ここでは、9600bps、パリティ無し、ストップビット1ビット、データビット8ビット、ハンドシェークなしのコードを紹介しています。

        private async Task OpenPort()
        {
            DeviceInformation entry = (DeviceInformation)listOfDevices[0];
            serialPort = await SerialDevice.FromIdAsync(entry.Id);
            serialPort.WriteTimeout = TimeSpan.FromMilliseconds(1000);
            serialPort.ReadTimeout = TimeSpan.FromMilliseconds(1000);
            serialPort.BaudRate = 9600;
            serialPort.Parity = SerialParity.None;
            serialPort.StopBits = SerialStopBitCount.One;
            serialPort.DataBits = 8;
            serialPort.Handshake = SerialHandshake.None;

            dataWriteObject = new DataWriter(serialPort.OutputStream);
            dataReaderObject = new DataReader(serialPort.InputStream);

        }

途中でタイムアウト設定もしています。1000msecなので、1秒でタイムアウトと設定されています。ボーレートやパリティも含め、それぞれのケースに応じて適切な値を設定してください。
SerialDevice.FromIdAsync(entry.Id)で取得されたserialPortのプロパティを覗いてみると、UART0とポート名が格納されています。最後の二行は、データの送信、受信を行うためのWriterとReaderです。

データを送信するには、

            string data = "Hello - from RPI2";
            dataWriteObject.WriteString(data);
            Task<UInt32> storeAsyncTask = dataWriteObject.StoreAsync().AsTask();
            UInt32 bytesWritten = await storeAsyncTask;

こんな風にコードを書きます。実際にデータが周辺機器に送信されるのは、最後の行です。Writerに書き込んで、ストアして、送信完了を待つ、というステップを踏みます。最後の行で、実際に送信されたデータ長(バイト)がbytesWrittenに格納されます。このコード例では文字列を送信していますが、組込みアプリっぽく、バイト列のデータを送信することももちろん可能です。バイト列の場合は、

            byte[] sendBuffer = バイト列データの格納
            dataWriteObject.WriteBytes(sendBuffer);
            storeAsyncTask = dataWriteObject.StoreAsync().AsTask();
            bytesWritten = await storeAsyncTask;

という形式をとります。sendBufferの配列長分を全て送信するので、適切な配列長のByte列を用意してくださいね。WriterにはStringやBytes以外の型のデータを送信するメソッドが用意されているので適宜選択してお使いください。

残りは、データ受信です。

            uint readBufferLength = 1024;
            cancellationToken.ThrowIfCancellationRequested();
            dataReaderObject.InputStreamOptions = InputStreamOptions.Partial;
            Task<UInt32> loadAsynctask = dataReaderObject.LoadAsync(readBufferLength).AsTask();
            UInt32 bytesRead = await loadAsynctask;
            if (bytesRead > 0)
            {
                var buffer = new byte[bytesRead];
                dataReaderObject.ReadBytes(buffer);
            }

受信の場合は、周辺機器が送ってくるデータ長に合わせて(この場合は1024バイト以下のデータを送ってくると想定)、ReaderのLoadAsync()メソッドを使って読込みを行います。それに続くコードで、実際にデータが受信されるまで待ちます。
※ポートを開くときに設定したTimeout期間が過ぎれば制御が戻ります。
受信が完了したら、受信するデータの型に合わせてReadXXXでデータを読み込みます。上のコードは、バイト列の読込みです。文字列が送られてくる場合には、ReadString()メソッドを使って読み込むことが可能です。

説明したフラグメントを組み合わせて、

await ListAvailablePorts();
await OpenPort();

とコール後に、データの送受信ロジックを組めば、周辺機器とのデータ送受信が可能です。どんなフォーマットのデータがどんな順序で送受信されるかは、周辺機器とのプロトコル次第です。全二重通信なので、

  • 双方送りっぱなしで、お互い受信しあう
  • どちらかがデータを送信して相手が受信した旨Ackを返す
  • 決められた手順、タイミングに従ってデータの送受信を行う

といった形態が考えられます。非同期プログラミングをうまく使いながら、上に説明した送受信ロジックを組み立てれば、どの形態での実装も可能です。
通信相手の周辺機器も自作の場合、機器間通信のプロトコルは自分で自由に決められるわけですが、下手なプロトコルを定義すると、実装が複雑になったり、システム全体のパフォーマンスが悪くなったりするので、じっくりと考えて定義をしてみてくださいね。