カテゴリー「dotNet MF」の記事

.Net Micro Frame WorkやFEZ Domino/netduino

.Net Micro Frameworkで配列を高速コピーする

前回の「NET Micro FrameworkでSPIを使う」にて作成したFEZ用SPI Flashメモリドライバ中に、byte配列をforループでまわしてコピー・結合するコードがあったのですが、FEZのフォーラムでArray.Copy()メソッドを使うべしという指摘をいただきました。試験的なコードを作って試してみると確かに劇的に早くなりました。


実行速度比較用のテストプログラム

以下のコードで、要素数500の配列のコピーを1000回繰り返す処理の実行時間を、   
①CopyByLoop()の呼び出し → forループを使った処理    
②System.Array.Copy()の呼び出し → NETMF組み込みのライブラリ呼び出し    
の2パターンで比較します。

using System;
using Microsoft.SPOT;

namespace ArrayCopy
{
    public class Program
    {
        public static void Main()
        {
            long begin, end, elapsed;
            int[] Array1 = new int[500];
            int[] Array2 = new int[500];
            const int N = 1000;
            int  n;

            Debug.Print("Number of Elements:" + Array1.Length.ToString());
            Debug.Print("Number of Loops   :" + N.ToString());

            InitArray(Array1, Array2);
            begin = DateTime.Now.Ticks;
            for (n = 0; n < N; n++)
            {
                CopyByLoop(Array1, Array2);
            }
            end = DateTime.Now.Ticks;
            elapsed = (end - begin) / TimeSpan.TicksPerMillisecond;
            Debug.Print("Time of CopyByLoop():" + elapsed.ToString());

            InitArray(Array1, Array2);
            begin = DateTime.Now.Ticks;
            for (n = 0; n < N; n++)
            {
                Array.Copy(Array2, Array1, Array2.Length);
            }
            end = DateTime.Now.Ticks;
            elapsed = (end - begin) / TimeSpan.TicksPerMillisecond;
            Debug.Print("Time of Array.Copy():" + elapsed.ToString());

        }

        static void InitArray(int[] Array1, int[] Array2)
        {
            for (int i = 0; i < Array1.Length; i++)
            {
                Array1[i] = 0;
                Array2[i] = i;
            }
        }

        static void CopyByLoop(int[] Array1, int[] Array2)
        {
            for (int i = 0; i < Array1.Length; i++)
                Array1[i] = Array2[i];
        }
    }
}


実行結果

以下Netduinoで実行した結果(単位はms)ですが、なんと、200倍近い性能差があります!

Number of Elements:500  
Number of Loops   :1000  
Time of CopyByLoop():31120  
Time of Array.Copy():164

   
結果の考察

極端な性能差が出る理由は、Array.Copy()はネイティブコードで動き、CopyByLoop()はforループをMSIL(中間コード)の逐次翻訳で動かしているためだと思われます。

ちなみに、PCで動作するFull NET Frameworkで同様のプログラムを動かすと、CopyByLoop(forループ)とArray.Copy()の呼び出しで殆ど差がつきません。この理由は、Full NET FrameworkははJITコンパイル方式のため、実行時にMSILをネイティブコードにコンパイルした結果がキャッシュされ2回目のループからはネイティブコードで動くためだと思われます。

Array.Copy()がネイティブコードで動くとすれば、NETMF PKのどこかにソースがある筈なので、そいつを探してみました。先ず、C#のコードがどのようなMSIL(中間コード)に翻訳されているのかをIldasm.exeを使って調べてみます。Ildasm.exeは.NET SDKに入っているツールで、.NET Framework の.exeファイルをを解析し、人間が読むことのできるMSILのコードを表示します。

以下にMain()関数を逆アセンブルした結果をを示します。

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // コード サイズ       282 (0x11a)
  .maxstack  3
  .locals init ([0] int64 begin,
           [1] int64 end,
           [2] int64 elapsed,
           [3] int32[] Array1,
           [4] int32[] Array2,
           [5] int32 n,
           [6] int32 CS$0$0000,
           [7] int32 CS$0$0001,
           [8] valuetype [mscorlib]System.DateTime CS$0$0002,
           [9] valuetype [mscorlib]System.DateTime CS$0$0003,
           [10] valuetype [mscorlib]System.DateTime CS$0$0004,
           [11] valuetype [mscorlib]System.DateTime CS$0$0005)
  IL_0000:  ldc.i4     0x1f4
  IL_0005:  newarr     [mscorlib]System.Int32
  IL_000a:  stloc.3
  IL_000b:  ldc.i4     0x1f4
  IL_0010:  newarr     [mscorlib]System.Int32
  IL_0015:  stloc.s    Array2
  IL_0017:  ldstr      "Number of Elements:"
  IL_001c:  ldloc.3
  IL_001d:  ldlen
  IL_001e:  conv.i4
  IL_001f:  stloc.s    CS$0$0000
  IL_0021:  ldloca.s   CS$0$0000
  IL_0023:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0028:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_002d:  call       void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string)
  IL_0032:  ldstr      "Number of Loops   :"
  IL_0037:  ldc.i4     0x3e8
  IL_003c:  stloc.s    CS$0$0001
  IL_003e:  ldloca.s   CS$0$0001
  IL_0040:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0045:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_004a:  call       void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string)
  IL_004f:  ldloc.3
  IL_0050:  ldloc.s    Array2
  IL_0052:  call       void ArrayCopy.Program::InitArray(int32[],
                                                         int32[])
  IL_0057:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_005c:  stloc.s    CS$0$0002
  IL_005e:  ldloca.s   CS$0$0002
  IL_0060:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_0065:  stloc.0
  IL_0066:  ldc.i4.0
  IL_0067:  stloc.s    n
  IL_0069:  br.s       IL_0079
  IL_006b:  ldloc.3
  IL_006c:  ldloc.s    Array2
  IL_006e:  call       void ArrayCopy.Program::CopyByLoop(int32[],
                                                          int32[])
  IL_0073:  ldloc.s    n
  IL_0075:  ldc.i4.1
  IL_0076:  add
  IL_0077:  stloc.s    n
  IL_0079:  ldloc.s    n
  IL_007b:  ldc.i4     0x3e8
  IL_0080:  blt.s      IL_006b
  IL_0082:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0087:  stloc.s    CS$0$0003
  IL_0089:  ldloca.s   CS$0$0003
  IL_008b:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_0090:  stloc.1
  IL_0091:  ldloc.1
  IL_0092:  ldloc.0
  IL_0093:  sub
  IL_0094:  ldc.i4     0x2710
  IL_0099:  conv.i8
  IL_009a:  div
  IL_009b:  stloc.2
  IL_009c:  ldstr      "Time of CopyByLoop():"
  IL_00a1:  ldloca.s   elapsed
  IL_00a3:  call       instance string [mscorlib]System.Int64::ToString()
  IL_00a8:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_00ad:  call       void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string)
  IL_00b2:  ldloc.3
  IL_00b3:  ldloc.s    Array2
  IL_00b5:  call       void ArrayCopy.Program::InitArray(int32[],
                                                         int32[])
  IL_00ba:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_00bf:  stloc.s    CS$0$0004
  IL_00c1:  ldloca.s   CS$0$0004
  IL_00c3:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_00c8:  stloc.0
  IL_00c9:  ldc.i4.0
  IL_00ca:  stloc.s    n
  IL_00cc:  br.s       IL_00e0
  IL_00ce:  ldloc.s    Array2
  IL_00d0:  ldloc.3
  IL_00d1:  ldloc.s    Array2
  IL_00d3:  ldlen
  IL_00d4:  conv.i4
  IL_00d5:  call       void [mscorlib]System.Array::Copy(class [mscorlib]System.Array,
                                                         class [mscorlib]System.Array,
                                                         int32)
  IL_00da:  ldloc.s    n
  IL_00dc:  ldc.i4.1
  IL_00dd:  add
  IL_00de:  stloc.s    n
  IL_00e0:  ldloc.s    n
  IL_00e2:  ldc.i4     0x3e8
  IL_00e7:  blt.s      IL_00ce
  IL_00e9:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_00ee:  stloc.s    CS$0$0005
  IL_00f0:  ldloca.s   CS$0$0005
  IL_00f2:  call       instance int64 [mscorlib]System.DateTime::get_Ticks()
  IL_00f7:  stloc.1
  IL_00f8:  ldloc.1
  IL_00f9:  ldloc.0
  IL_00fa:  sub
  IL_00fb:  ldc.i4     0x2710
  IL_0100:  conv.i8
  IL_0101:  div
  IL_0102:  stloc.2
  IL_0103:  ldstr      "Time of Array.Copy():"
  IL_0108:  ldloca.s   elapsed
  IL_010a:  call       instance string [mscorlib]System.Int64::ToString()
  IL_010f:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0114:  call       void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string)
  IL_0119:  ret
} // end of method Program::Main

100行目の、  
call       void [mscorlib]System.Array::Copy(...)
が、System.Array.Copy()メソッドの呼び出し部分と思われます。

NETMF PKのソースコードから、Array::Copyでgrepすると、  
MicroFrameworkPK_v4_1\CLR\Core\CLR_RT_HeapBlock_Array.cppの中に、  
HRESULT CLR_RT_HeapBlock_Array::Copy( )という関数が存在し、こいつが呼び出されていると思われます。この関数内で、ヒープ内のメモリーコピーを行うことで配列のコピーを行っています。この関数はネイティブコードで動作するファームウェアの一部なので当然高速動作します。

ということでNETMFでは、配列の操作についてはSystem.Array配下の組み込みメソッドを利用することが、高速化のポイントということになります。

.NET Micro FrameworkでSPIを使う

NETMFのSPIを使って、ATMELのAT45DB161D Data Flashを動かしてみました。NETMFのSPIライブラリの使い方がよく分からず苦戦したのですが、FEZ Domino用のドライバをベースに手直しを行うことでやっと動作に成功しましたので、今回の試行錯誤から分かったことを記載します。

コードはFEZ Domino用に作ってありますが(GHIのコードをベースにしているため)、ちょっと手直しするだけで、Netduinoでも動作可能です。


NETMFでのSPIコマンドの発行方法

SPIデバイスとの通信は、コマンドを発行してレスポンスを受け取る形態が一般的です。また、ArduinoやmbedのSPIライブラリでは、1 Frame(8bitとか)のデーターをSPIデバイスと送受信する基本的なメソッドが用意されているのみです。SPIは仕組み上データーを受信するためには何か適当な値(ダミーデーター)をデバイスに送り込む必要があります。Arduinoやmbedでは、SPIライブラリのメソッドを何回も呼び出して、コマンド送信+ダミーデーター送信(レスポンスを受信するため)をframe単位にくり返します。

最初は(Netdunoで開始)、NETMFのSPIライブラリを上記Arduino/mbedの作法で使っていたのですが、NetduinoではSPI Flashのステータスレジスタが読めないという事象に悩まされました。すなわちNetduinoでは、コマンドを送信するメソッド呼び出しと、レスポンス(ステータス値)を受信するためのメソッド呼び出しを2回に分けると、正しくデーターが受信できませんでした。なぜか(今でも原因はよく分からないのですが)、FEZ Dominoだと上記のコードが正しく動作するため、NETMFではライブラリ呼び出しの作法が異なることに気がつけませんでした。

自力で問題が解決できずNetduinoのフォーラムに質問したら、FEZ DominoにAT45DB161Dを使ったFlash Shieldがあるので、そのドライバーが使えるのではというコメントをいただき、試してみたら一発で動きました。ドライバ自体は後で示す問題があったのですが、コードを読んでみると、NETMFではコマンド・レスポンスに対応した一本のバイト列を作って、1回のSPIメソッド呼び出しで処理を済ませています。以下の図に、NETMFでの処理イメージを示します。

SpiDataFormat

今回のように、受信データーが比較的でかい場合はRAMの消費量が多くなりますが、SPIメソッドの呼び出し回数を減らすというのがNETMFの設計思想(お作法)のようです。勝手な想像ですが、マネージドコードのオーバーヘッドが大きいNETMF故のパフォーマンス対策なのかもしれません。ひょっとすると、コマンド送信・レスポンス受信で処理を分けた最初のコードがFEZ Dominoで動作するがNetduinoでは動作しなかった原因は、MCUの処理性能差なのかもです。

 
ドライバコードとサンプルアプリ

今回使用したAT45DB161Dドライバのコードを以下に示します。AT45DB161Dへの書き込みは、1)SRAMの一次バッファへの書き込み、2)バッファからフラッシュメモリセルへの書き込みという2段階で行う必要があるのですが、GHIのオリジナルコードは1)のバッファ書き込みしか行っていませんでした。読み出しはその逆で、メモリセルからバッファに読み出し、バッファからMCUに転送する必要があるのですが、やはりバッファを読んでいるのみでした。GHI提供のサンプルアプリは同じページを読み書きしているだけなので、見た目動いているように見えるという、おいおいなヤツでした。

そのため、上記のバッファを介した読み書き部分を修正しました。Arduino RSSリーダーでAT45DB161Dを使った経験が生きたということで、、

/*
Copyright 2010 GHI Electronics LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 

Modofied by todotani 2010/9/28 
*/

using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using GHIElectronics.NETMF.Hardware;

namespace GHIElectronics.NETMF.FEZ
{
    public static partial class FEZ_Shields
    {
        public static class FlashShield
        {
            const byte Status_Register_Read = 0xD7;
            const byte Continuous_Array_Read = 0x03;    // Continuous Array Read (Low Frequency). Changed from Legacy command (0xE8)
            const byte Buffer_1_Read = 0xD1;            // Buffer 1 Read (Low Frequency)
            const byte Buffer_1_Write = 0x84;           // Buffer 1 Write
            const byte DB_BUF1_PAGE_ERASE_PGM = 0x83;   // Buffer to Main Memory Page Program with Built-in Erase
            const byte AT45DB161D_density = 0x2C;
            static private uint pageSize = 0;
            static private SPI Spi = null;

            static public void Initialize()
            {

                SPI.Configuration SPICfig = new SPI.Configuration((Cpu.Pin)FEZ_Pin.Digital.Di10, 
                    false, 0, 0, false, true, 16000, SPI.SPI_module.SPI1);
                Spi = new SPI(SPICfig);
                byte status = ReadStatusRegister();
                if ((status & 0x3C) != AT45DB161D_density)
                    throw new Exception("Not Supported Device or DataFlash");
                if ((status & 0x01) == 0x01)
                {
                    pageSize = 512;
                }
                else
                {
                    pageSize = 528;
                }
            }

            static public uint PageSize
            {
                get
                {
                    return pageSize;
                }
            }

            static private void SendCommandAndData(byte[] command, byte[] data)
            {
                if (command != null && data != null)
                {
                    byte[] temp = new byte[command.Length + data.Length];

                    Array.Copy(command, temp, command.Length);
                    Array.Copy(data, 0, temp, command.Length, data.Length);
                    Spi.WriteRead(temp, temp);
                    Array.Copy(temp, command, command.Length);
                    Array.Copy(temp, command.Length, data, 0, data.Length);

                    //for (int i = 0; i < command.Length; i++)
                    //{
                    //    temp[i] = command[i];
                    //}
                    //for (int i = 0; i < data.Length; i++)
                    //{
                    //    temp[i + command.Length] = data[i];
                    //}
                    //Spi.WriteRead(temp, temp);
                    //for (int i = 0; i < command.Length; i++)
                    //{
                    //    command[i] = temp[i];
                    //}
                    //for (int i = 0; i < data.Length; i++)
                    //{
                    //    data[i] = temp[i + command.Length];
                    //}
                }
                else
                {
                    if (command != null)
                        Spi.WriteRead(command, command);
                    if (data != null)
                        Spi.WriteRead(data, data);
                }
            }

            static private byte ReadStatusRegister()
            {
                byte[] cmd = new byte[2] { Status_Register_Read, 0 };
                SendCommandAndData(cmd, null);
                return cmd[1];
            }

            static public void ReadPage(uint PageNumber, byte[] buffer)
            {
                if (buffer.Length < PageSize)
                    throw new Exception("Buffer Size is smaller than Page Size");

                byte[] cmd = new byte[4];

                cmd[0] = Continuous_Array_Read;
                /*
                 * 3 address bytes consist of :
                 *  - 2 don't care bits
                 *  - 12 page address bits (PA11 - PA0) that specify the page in 
                 *    the main memory to be written
                 *  - 10 buffer address bits (BA9 - BA0) --> Allways set to 0
                 */
                cmd[1] = (byte)(PageNumber >> 6);
                cmd[2] = (byte)(PageNumber << 2);
                cmd[3] = 0;
                SendCommandAndData(cmd, buffer);
                for (int timeout = 0; timeout < 100; timeout++)
                    if ((ReadStatusRegister() & 0x80) > 0)
                        return;
                throw new Exception("Error reading data");

            }

            static public void WritePage(uint PageNumber, byte[] buffer)
            {
                if (buffer.Length < PageSize)
                    throw new Exception("Buffer Size is smaller than Page Size");
                byte[] cmd = new byte[4];

                // Write data to buffer 1
                cmd[0] = Buffer_1_Write;
                /*
                 * 3 address bytes consist of :
                 *  - 14 don't care bits
                 *  - 10 buffer address bits (BA9 - BA0) --> Allways set to 0
                 */
                cmd[1] = 0;
                cmd[2] = 0;
                cmd[3] = 0;
                SendCommandAndData(cmd, buffer);

                cmd[0] = DB_BUF1_PAGE_ERASE_PGM;
                /*
                 * 3 address bytes consist of :
                 *  - 2 don't care bits
                 *  - 12 page address bits (PA11 - PA0) that specify the page in 
                 *    the main memory to be written
                 *  - 10 don’t care bits
                 */
                cmd[1] = (byte)(PageNumber >> 6);
                cmd[2] = (byte)(PageNumber << 2);
                cmd[3] = 0;
                SendCommandAndData(cmd, null);
                for (int timeout = 0; timeout < 100; timeout++)
                    if ((ReadStatusRegister() & 0x80) > 0)
                        return;
                throw new Exception("Error writeing data");
            }
        }
    }
}

AT45DB161Dはバッファを2面持っており、ダブルバッファにすることで読み書きを高速化することができますが、このコードではバッファは1面のみを使用しています。また、読み出しはバッファを使用せずメモリーセルのページアドレスを指定して1ページ分のデーターを直接MCUに転送するContinuous Array Readという機能を使っています。この機能を使えば数百KBあるようなでかいデーターを一気に読み出すような用途にも使えます(FEZ DominoやNetduioには読んだデーターを格納できるRAMがありませんが、、)。

ライブラリを使用したサンプルアプリを以下に示します。

using System;
using System.Threading;
using Microsoft.SPOT;
using GHIElectronics.NETMF.FEZ;

/* --------------------------------------------------------
 * Pin assignment FEZ Domio -- AT45DB161D
 *            CS    D10     -- 4 (Pull-up with 10k ohm R)
 *            RESET RESET   -- 3 (Pull-down with 10k ohm R)
 *            MOSI  D11     -- 1
 *            MISO  D12     -- 8
 *            SCK   D13     -- 2
-----------------------------------------------------------*/

public class Program
{
    public static void Main()
    {
        // Initialize DataFlash
        FEZ_Shields.FlashShield.Initialize();
        Debug.Print("Page size is " + FEZ_Shields.FlashShield.PageSize);
        byte[] write_buffer = new byte[FEZ_Shields.FlashShield.PageSize];
        byte[] read_buffer = new byte[FEZ_Shields.FlashShield.PageSize];

        // Test storing data
        for (uint page = 0; page < 16; page++)
        {
            Debug.Print("Page " + page.ToString() + " Write");
            for (int i = 0; i < FEZ_Shields.FlashShield.PageSize; i++)
            {
                write_buffer[i] = (byte)page;
            }
            FEZ_Shields.FlashShield.WritePage(page, write_buffer);
        }

        // Test retrieving data
        for (uint page = 0; page < 16; page++)
        {
            Debug.Print("Page " + page.ToString() + " Read");
            FEZ_Shields.FlashShield.ReadPage(page, read_buffer);
            for (int i = 0; i < 16; i++)
            {
                Debug.Print(read_buffer[i].ToString());
            }
        }
    }
}

おわりに

今回はNetduinoのフォーラムメンバーに助けられました。最初、FEZで動くのにNetduinoで動かない事象に悩んだ際は、AT45DB161Dはあまりポピュラーなデバイスではなく使用例が少ないと思ったため、質問するにしてももう少し事象を切り分けてからと思ったのですが、結果的には(既に実装例があり)もっと早く聞いておくのが正解でした。

またまた、GHIの成果物をパクッてしまいましたが、オリジナルコードの問題点をFEZ Dominoのフォーラムにフィードバックしておきましたのでご容赦をお願いします・・・GHIのドライバは当初、GHI製品でのみ使えるという論争があったみたいですが、最終的にApacheライセンスになり改変・再配布が自由になったので、Netduinoでの利用も許されるのかと思います。

mbedのSPIライブラリはデーターのフレーム長を4~16bitの間で1bit単位に指定できますが、NETMFは8 or 16bitの2択になります。そのため、Nokia液晶のようにフレーム長が9bitのデバイスは使えません。Netduinoのフォーラムでも議論が上がっていましたが、今のところサポート予定はないようです。Nokia液晶を使ってみたかったのですがこの点は残念です。

参考文献


2010/10/2更新:

SendCommandAndData()メソッドで配列のコピーをforループで行っていたのですが、Array.Copy()メソッドの呼び出しに変更しました(FEZ Dominoフォーラムのご指摘により)。Array.Copyの方が劇的に早くなります(Netduinoで要素数500の配列コピーを1000回繰り返す処理で比較すると200倍近い差が出ます)。

Netduino Get

話題の、.NET Micro Frameworkが動きC#でプログラミングができる、Netduinoをスイッチサイエンスさんから購入しました。初回販売分が残暑見舞い特別価格の2,800円(Arduino Duemilanoveより安い!)ということで速攻でGetしてしまいました。ということで、2日ほどですが触ってみた感想を記載します。

   
Netduinoの特徴

開発環境のインストールなどは、他の方が書いているので、今回は手抜きをしてNetduioの特徴をさらっと...

  • Visual Studio 2010 Expressを使用でき、インテリセンスなどの補助機能でコード入力が楽ちん。mbedのクラウド開発環境も新鮮ですが、高機能IDEの使い心地も捨てがたい    
  • Visual Studioのデバック機能を使って、ソースコードレベルのデバッグが可能。Arduinoやmbedのprintfデバックから開放されて嬉しい    
  • Arduinoとピン互換のため、Arduino用のシールドが使える    
  • 同じくNETMFが動くFEZ Dominoに比べると、CPUはちょっと遅くなりますが(72MHz vs 48MHz)、RAM容量が多いため(96KB vs 128KB)ユーザーエリアが60KBと広い    
  • 何よりお安い(同類のFEZ Dominoに対して半分以下で、通常価格でもArduino並みの安さ!)    
  • ライブラリ(ファームとして提供される部分)の機能は、FEZ Dominoの方が豊富。FEZは、MSが提供していないUSB HostやSDカードなど、便利な機能がライブラリとして提供されているのに対して、Netduino提供ライブラリはA/D変換,PWMなど、現状は最低限の機能のみ。ただし、FEZのライブラリはソースが非公開でGHIの開発に依存してしまうが、Netduinoはライブラリもオープンソースなので、今後のコミュニティーによる開発に期待    
  • コードの実行速度はあまり期待できない(NETMFは中間コードを逐次翻訳するインタープリター方式で、PCで動くFull .NETのようにJITコンパイル機能がないため)。NETMFはC++で書いたNativeコードを呼び出すRLP(Runtime Loadable Procedures)という機能があるのですが、FEZ DominoやNetduinoではメモリー量の制約から現状は未サポート。そのため、FFTのような数値演算系の処理はFEZ Dominoでは手が出ないと思いますが、NetduinoならライブラリにNativeコードとして組み込むことで(力技ですが)処理性能を必要とするプロジェクトに対応できるかも

 
FEZ Dominoのドライバを移植

Netduinoは入出力ピンの配置がArduino互換ですが、使用しているMCUや実行環境が異なるためArduino用のライブラリコードは使えません。手っ取り早く周辺装置を動かす手段として、同じくNETMFを使いかつArduinoとピン互換になっているFEZ Dominoのドライバを拝借することにしました。FEZ Dominoのドライバ(ファームに組み込まれていない部分)はNETMF用のC#で書いてあり、最小限の変更で動作することが期待できます。

先ずは、Arduinoでもおなじみの16x2キャラクタLCDを動かしてみます。手順は以下です;  

  1. FEZ DominoのWebページからLCD & Keypad Shieldを探してドライバをダウンロード    
  2. 新規のプロジェクトを起こし、Program.csと同じフォルダにドライバファイル(FEZ_Shield_KeypadLCD_Red.cs)を格納    
  3. ソリューションエクスプローラーで      
    プロジェクト名を右クリック→追加→既存の項目から、ドライバファイルを指定してVSに登録    
  4. ドライバファイルを開くといくつかエラーが出るので、エラー発生箇所を修正    
  5. Programs.csをコーディング 

4項の修正箇所は以下です; 

①namespaceの変更   
using GHIElectronics.NETMF.Hardware;    
↓    
using SecretLabs.NETMF.Hardware;    
using SecretLabs.NETMF.Hardware.Netduino;    
cpuピン指定のパラメーター名をFEZ(GHI)からNetduino(SecretLabs)に変更するため、namespaceを変更します。 

②OutputPortのピン識別子の変更
LCD_RS = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.Di8, false);    
↓    
LCD_RS = new OutputPort(Pins.GPIO_PIN_D8, false);    
GHIElectronics.NETMF.Hardwareで定義しているFEZ用のPin指定から、SecretLabs.NETMF.Hardware.Netduino名前空間で定義している識別子に変更します。同様に、D9, D7~D4も変更 

③ADCのクラス名を変更   
AnKey = new AnalogIn((byte)FEZ_Pin.AnalogIn.An0)    
↓    
AnKey = new AnalogInput(Pins.GPIO_PIN_A0);    
今回は使用していませんが、KeyPadの値を読み取るためにADCを使っています。FEZとNetduinoでクラス名が異なるため修正します。ピン指定もOutputPortと同様に修正します。 

Keypadやバックライトの制御は使っていないのですが、あえて削除はせず、エラーが出る部分だけ修正しました。コード全体はここです。 

続いて、5項のProgram.csをコーディングします。コードは以下です。 

using System.Threading;
using GHIElectronics.NETMF.FEZ;

namespace NetLCD
{
    public class Program
    {
        public static void Main()
        {
            FEZ_Shields.KeypadLCD.Initialize();

            while (true)
            {
                FEZ_Shields.KeypadLCD.Print("Hello World");
                Thread.Sleep(2000);
                FEZ_Shields.KeypadLCD.SetCursor(1, 0);
                FEZ_Shields.KeypadLCD.Print("Hello Netduino");
                Thread.Sleep(2000);
                FEZ_Shields.KeypadLCD.Clear();
                Thread.Sleep(2000);
            }
        }

    }
}

ドライバは、GHIElectronics.NETMF.FEZという名前空間で定義されているため、2行目のusingで指定します。最初は名前空間を変えようかと思ったのですが、名前空間もAPIの一部と考え、APIを拝借したGHIに敬意を表してそのままにしてあります。同様にドライバのクラス名がFEZ_Shields.KeypadLCDのため、この名前もそのままにしてあります。

このドライバは、static classとして定義してあるため、コンストラクタを呼んでインスタンスを起こす必要がなく、10行目のように、FEZ_Shields.KeypadLCD.メソッド名で処理を呼び出します。

最後にLCDとの結線は以下となります(ドライバの中で決め打ちになっています): 

  • RS --> D8
  • E --> D9
  • RW --> GND
  • LCD_D4 --> D4
  • LCD_D5 --> D5
  • LCD_D6 --> D6
  • LCD_D7 --> D7

めでたく動作しました。

 
Arduino Ethernet Shieldの使用

つぎは、FEZ DominoでもやったArduino Ethernet Shieldを動かすネタです。Ethernet Shieldは純正品ではなく、NKC Electronicsの互換品を使っています。このShieldはArduino Megaでも使えるように、SPIの信号をICSPコネクタから取る作りになっています。NetduinoはICSP接続用のヘッダピンが実装されていないため、手持ちの部品を半田付けしました。

Ethernet ShieldのドライバはFEZ Domino用を使います。こいつは、FEZ Domino+Arduino Ethernt Shieldで動作した実績があります。LCDドライバと同様に、以下の変更を加えます。

①namespaceの変更  
using GHIElectronics.NETMF.Hardware;  
↓  
using SecretLabs.NETMF.Hardware;  
using SecretLabs.NETMF.Hardware.Netduino;

②CS信号用OutputPortのピン識別子の変更 
SPI.Configuration config = new SPI.Configuration((Cpu.Pin)FEZ_Pin.Digital.Di10, false, ....  
↓ 
SPI.Configuration config = new SPI.Configuration(Pins.GPIO_PIN_D10, false, ......

ドライバ全体のファイルをここに置きます。

Program.csのコードは以下で、最小限のHttpクライアント動作を行います。

using System;
using System.Text;
using System.Threading;
using Microsoft.SPOT;
using GHIElectronics.NETMF.FEZ;


namespace NetEthernet
{
    public class Program
    {
        static byte[] IpAddress = { 192, 168, 0, 111 };
        static byte[] Subnet = { 255, 255, 255, 0 };
        static byte[] Gateway = { 192, 168, 0, 1 };
        static byte[] Mac = { 00, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };  //自分のMACアドレスを設定
        static byte[] RemoteServer = { 192, 168, 0, 10 };

        public static void Main()
        {
            FEZ_Shields.Ethernet.Initialize(IpAddress, Subnet, Gateway, Mac);

            while (true)
            {
                httpGet();
                Thread.Sleep(2000);
            }
        }

        // FEZ Ethernet Sheild用ドライバーを使用したHttpページの取得
        static void httpGet()
        {
            FEZ_Shields.Ethernet.uSocket socket =
                new FEZ_Shields.Ethernet.uSocket(FEZ_Shields.Ethernet.uSocket.Protocol.TCP, 80);
            Encoding enc = Encoding.UTF8;

            socket.Connect(RemoteServer, 80);
            string request = "GET /index.html HTTP/1.0\r\n\r\n";
            socket.Send(enc.GetBytes(request), request.Length);

            int length;
            while ((length = socket.AvilableBytes) > 0)
            {
                byte[] buff = new byte[length];
                socket.Receive(buff, length);
                String printStr = new String(enc.GetChars(buff));
                Debug.Print(printStr);
            }

            socket.Close();
        }
    }
}

基本動作のみの検証になりますが、ちゃんと動いています。

 
おわりに

FEZ Domino用のドライバ2例は、エラーが出た箇所を修正するだけで苦もなく動作してくれ、FEZ DominoとNetduino間の移植性の高さを実感しました。修正が必要な箇所は、GHI, SecretLabs固有ライブラリの差分が染み出す部分ですが、今回の例では僅かでした。

FEZ Dominoをいじった際は、GHI提供のライブラリをチュートリアル通りに動かすだけでしたので、正直ワクワク感が少なかったです。Netduinoでは人様が作ったドライバですが、自前で移植作業を行うことで、開発してる感が増しワクワク感がありました。Netduinoは、ファーム(NETMFのネイティブドライバ類)もオープンソースですので、コミュニティーの成果を借用したりして(あわよくば自分でも何か作って)、内部がいじれるようになればもっと面白くなる予感があります。

これからますます盛り上がってくれるとよいなと期待です。

2010/9/24 修正
GHIのライセンス条件からドライバコードの公開はNGのため、Netduinoへの移植版のダウンロードリンクは削除しました。

2010/10/2 更新
GHI提供ドライバがApacheライセンスになっています(ドライバファイルのソースに記載があります)。改変と再配布(Web公開含む)がこれで許されたと考えられますが、FEZ Ethernet Shieldのドライバは本日時点ではアクセスできなくなっているこもあり、コードの公開はやめておきます。Netduinoフォーラムでも独自のEthernet Shield用ドライバの開発が進んでいますので、Netduinoユーザーはこちらに期待しましょう。

FEZ DominoでArduino用Ethernet Shieldを動かしてみた

.NET Micro Frameworkが使えるプロトタイピングボードFEZ Dominoはシールドのピン配置がArduino互換です。さらに、FEZ Domino用Ethernet ShieldはArduinoと同じW5100チップを使っています。FEZ DominoのMCU(LPC2388)にEthernet Interface機能があるのにわざわざEthernt/TCP-IPチップを使うのは無駄のようにも見えますが、Arduino用Ethernet Shieldが流用できるのではないかという期待が出てきます。

そこで、FEZ DominoでArduino用のEthernet Shieldが動作するかを実験してみました。使用したのはオフィシャルEthernet Shieldです。Arduino Ethernet Shield、FEZ用ドライバとも無修正で正常に動作しました。


ドライバファイルの入手

FEZ Domino用ドライバファイルをここからダウンロードします。現在のβドライバはC#のクラスライブラリとして提供されており、CLR(インタープリタ)で逐次中間コードを翻訳しながらの実行となるため、速度はあまり期待できません。将来的にはファームウェアに統合されたネイティブドライバが提供されるようですので、速度面はそちらに期待。

ドライバファイルに、2箇所ほどDebug.printがコメントアウトされれずに残っていたため、コメントアウトが必要でしたww


FEZ Dominoとの接続

Arduino Ethernet ShieldをFEZ Dominoに取り付けるだけです。

Domino-ArduinoEthSield1

Domino-ArduinoEthSield2

FEZ Domino用Ethernet Shieldは割り込み信号(Di2)を使っているとWebページに記載がありますが、Arduino Ethernet Shieldは割り込み信号の配線がありません(割り込みをつなぐための、半田ジャンパはあるみたい)。ForumのQAによると、現在のドライバでは割り込みは使用していないとのことなので、ジャンパ作業は不要です。


サンプルコード

Webサーバーからhttpページを取得してデバックコンソールに表示するサンプルを作ってみました。

using System;
using Microsoft.SPOT;
using System.Text;
using System.Threading;
using GHIElectronics.NETMF.FEZ;

namespace FezEthernet
{
    public class Program
    {
        static byte[] IpAddress = { 192, 168, 0, 111 };
        static byte[] Subnet = { 255, 255, 255, 0 };
        static byte[] Gateway = { 192, 168, 0, 1 };
        static byte[] Mac = { 00, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };  //自分のMACアドレス
        static byte[] RemoteServer = { 192, 168, 0, 10 };

        public static void Main()
        {
            FEZ_Shields.Ethernet.Initialize(IpAddress, Subnet, Gateway, Mac);

            while (true)
            {
                httpGet();
                Thread.Sleep(2000);
            }
        }

        // FEZ Ethernet Sheild用ドライバーを使用したHttpページの取得
        static void httpGet()
        {
            FEZ_Shields.Ethernet.uSocket socket =
                new FEZ_Shields.Ethernet.uSocket(FEZ_Shields.Ethernet.uSocket.Protocol.TCP, 80);
            Encoding enc = Encoding.UTF8;

            socket.Connect(RemoteServer, 80);
            string request = "GET /index.html HTTP/1.0\r\n\r\n";
            socket.Send(enc.GetBytes(request), request.Length);

            int length;
            while ( (length = socket.AvilableBytes) > 0)
            {
                byte[] buff = new byte[length];
                socket.Receive(buff, length);
                String printStr = new String(enc.GetChars(buff));
                Debug.Print(printStr);
            }

            socket.Close();
        }
    }
}

サンプルは、socketをオープンし、socket.AvilableBytesで取得した受信データー量分のバッファを用意し、さらにstringクラスのインスタンスに情報をコピーしてDebug.printするというメモリー消費的には厳しいコードになっているため、情報量の多いwebページを取得するとメモリー枯渇になるかもしれません。ローカルPC上で動作するWebサーバーが返す1600byte程度のページは問題なく取得できます。

.NET Micro FrameworkにはSystem.Net.HttpWebRequestやSystem.Net.HttpWebResponseクラスがありますが、FEZ Dominoでは動作しませんでした(ビルドはできますが、実行時に例外を吐いてしまいます)。おそらく、FEZ用のドライバがuSocketクラスという固有のSocketインタフェースで実装しているためだと思います。今後の正式ドライバでは、NETMFが期待するSocketの実装になればこのあたりのHttpクラスも使えるようになるかもしれません。


その他

例によってEthernet Shieldが電源投入時に正しく動作しないことがあり、その場合はマニュアルリセットで動作しました(FEZ DominoはArduinoよりまじめにパワーオンリセット回路が作ってあり、ひょっとすると自分のEthernet Shieldはリセット対策のコンデンサを追加したため、その悪影響が出ているのかも?)。

今回の実験を行って思ったことですが、NETMFにはシリアルポート出力用のクラスがないため、テキスト情報を表示するようなケースがちと不便だと思いました。Debug.printでもよいのですが、必ず改行を入れてしまうため、せめて改行をしないオプションがあるとよいのですが。

サンプルでは、最初はバッファによるメモリ消費を節約しようと思い、socketからの受信データーを、256byteのバッファに分割して読み込むようにしました。Debug.printが引数としてstringクラスしか取らないため、256byte単位でstringに変換してprintするとそのたびに改行が入ってしまうため、最終的にはメモリーを消費してしまうのですが受信バッファの分割を行わないコードにしました。

Full NetのStreanReaderクラスを使ったサンプルでは、
Console.WriteLine(sr.ReadToEnd());
みたいに、「ストリームクラスを起こし、全部読み込んでから一気に表示」的な書き方になるのかと思いますが、NETMFではメモリーを喰わない工夫も必要かと思いました。まあ、NETMFでもグラフィックLCDにWPFを使って表示を行うような使い方では(NETMFがもともとターゲットにした領域でしょうか)、ROM/RAM共に数MBは必要になるため、そこまでケチらなくてもよいのかもしれません。

.NET Micro Framework 4.1を導入

NET MF 4.1を導入して、FEZ Dominoの開発環境を更新しました。NET MF 4.1からVS2010が使えるようになるのですが、NET MF 4.1をインストールしてしまうと、VS2008ではプロジェクトを開くことができなくなる(VS2008で作成したプロジェクトも含めて)ため要注意です。また、執筆時点ではFEZ DominoのファームがNET MF 4.0ベースですが、もうしばらくするとNET MF 4.1対応のファームがリリースされるみたいですので、それまで待つのがよいかもしれません。

TinyCLRのWebでは、VS2010はまだ未サポートと書いてある箇所があるのに、ダウンロードのリンクはNET MF 4.1 + VS2010になっており、なんだか混乱した状態になっています(2010/8/7時点)。


インストール作業

インストール自体は特に注意する点はありません。

  • Visual C# 2010 Expressをここからダウンロード。NET Framework (Full Net) 4.0も同時にインストールされます。VS2008との共存もできています
  • NET Micro Framework 4.1 SDKをここからダウンロード
  • FEZ DominoのファームとGHI NETMF SDKは現時点の最新版(1.0.5)が使えるため、インストールは不要。このファームはNET MF 4.0ベースのため、こいつに足を引っ張られて、現時点ではNET MF 4.0ベースでの解発になります
  • NET Micro Framework Porting Kit 4.0をインストールしている場合、事前にアンインストール


VS2008で作成したプロジェクトをVS2010で開く

NET MF 4.1とVS2010をインストールした後で、VS2008で作成したNET MFのプロジェクトを開くと以下のエラーダイアログが出て、ファイルを開けません。

VS2008_ErrorDlg

FullNetのプロジェクトは、.NET Framework 4.0をインストールした後でもVS2008で開くことができるのですが、NET MFのプロジェクトはNET MF 4.1をインストールするとVS2010が必須になってしまう様です。ひよっとすると、VS2008で開く方法があるかもしれないのですがVS2010に乗り換えることにします。

VS2010でVS2008にて作成したプロジェクトを開くと、プロジェクト形式の変換ダイアログが出ます。インストール直後、いくつかのプロジェクトで、変換エラーが発生するケースがありました。その後エラーが出なくなったため、何か環境要因があったのかもしれません。(実はPorting Kit 4.0をアンインストールしていなかったため、何か影響があったとか・・)

エラーが発生する場合の回避策としては以下が有効でした:

  1. csprojファイルをテキストエディターで開く
  2. <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>をv4.1に変更
  3. 変換を実施
  4. プロジェクトのプロパティを開いて、アプリケーション→対象のフレームワークを4.1→ 4.0に変更
    この変更を行わないと、ビルドでエラーが発生します(DominoのファームがNET MF 4.0ベースのため)

いくつかのプロジェクトで試した限り、VS2010に変換後のビルドや実行で問題が出たケースはありません。

VS2010

FEZ Domino Tips (3) - USB Host機能の使い方

FEZ Dominoの特徴の一つはUSB Host制御のライブラリが充実していることです。USB Hostとは、USBデバイス(キーボード、マウス、USBメモリーなど)への読み書きを行うPC相当の動作を行う機能です。

mbedでもLPC1768にUSB Host機能がありますが、ライブラリが十分にそろっていません。そのため、USB Host機能を使ってUSB機器を制御したい場合、Dominoは有力な選択肢になりそうです。今回、USB Host関連のサンプルコードをいくつか動かしてみたので結果を記載します。


USBデバイスの認識

以下は、USBデバイス挿抜時のイベントを検出するコードです。ビルドするためには、次のアセンブリが必要です。

  • GHIElectronics.NETMF.System
  • GHIElectronics.NETMF.USBHost
  • GHIElectronics.NETMF.Hardware →USBH_Device classの定義がここあるため
  • Microsoft.SPOT.Naitve
using System.Threading;
using GHIElectronics.NETMF.USBHost;
using Microsoft.SPOT;

namespace FezUsbHost
{
    public class Program
    {
        public static void Main()
        {
            // Subscribe to USBH events.
            USBHostController.DeviceConnectedEvent += OnDeviceConnected;
            USBHostController.DeviceDisconnectedEvent += OnDeviceDisconnected;

            // Sleep forever
            Thread.Sleep(Timeout.Infinite);
        }

        // USB DeviceConnectedEvent Handler
        static void OnDeviceConnected(USBH_Device device)
        {
            Debug.Print("Device connected...");
            Debug.Print("ID: " + device.ID + ", Interface: " + 
                device.INTERFACE_INDEX + ", Type: " + device.TYPE);
        }

        // USB DeviceDisconnectedEvent Handler
        static void OnDeviceDisconnected(USBH_Device device)
        {
            Debug.Print("Device disconnected...");
            Debug.Print("ID: " + device.ID + ", Interface: " + 
                device.INTERFACE_INDEX + ", Type: " + device.TYPE);
        }
    }
}

コードのポイントは以下です。

  • 20, 28行目がデバイス挿抜時のイベントハンドラーです
  • イベント発生時に上記のハンドラーを呼び出すための登録が、12, 13行目になります。"+="でイベントハンドラーを登録できるのはC#独自の書式で、Javaより簡潔で分かりやすいのではないかと思います

実行するとデバイスIDやタイプをDebugコンソールに出力します。USBマウス・キーボードは挿抜を検出したのですが、手持ちのUSBメモリー(4GB)は認識しませんでした。本来このコードで認識してくれないとダメなので、こちらは継続調査です。


UBSマウスの読み取り

デバイスの挿抜に加えて、マウスの移動・ボタン押下のイベントハンドラーを追加しています。

using System.Threading;
using Microsoft.SPOT;
using GHIElectronics.NETMF.USBHost;

namespace Test
{
    public class Program
    {
        static USBH_Mouse mouse;
        public static void Main()
        {
            // Subscribe to USBH event.
            USBHostController.DeviceConnectedEvent += OnDeviceConnected;

            // Sleep forever
            Thread.Sleep(Timeout.Infinite);
        }

        static void OnDeviceConnected(USBH_Device device)
        {
            if (device.TYPE == USBH_DeviceType.Mouse)
            {
                Debug.Print("Mouse Connected");
                mouse = new USBH_Mouse(device);
                mouse.MouseMove += OnMouseMove;
                mouse.MouseDown += OnMouseDown;
            }
        }

        static void OnMouseMove(USBH_Mouse sender, USBH_MouseEventArgs args)
        {
            Debug.Print("Absol(x, y) = (" + sender.Cursor.X + ", " + 
                sender.Cursor.Y + ")");
            Debug.Print("Delta(x, y) = (" + args.DeltaPosition.X + ", " + 
                args.DeltaPosition.Y + ")");
        }

        static void OnMouseDown(USBH_Mouse sender, USBH_MouseEventArgs args)
        {
            Debug.Print("Button down number: " + args.ChangedButton);
        }
    }
}
  • 25, 26行目でマウスイベントのハンドラーを登録しています
  • 30, 38行目がイベントハンドラーになります
  • 32行目はマウスの位置を絶対座標(0, 0)~(512, 512)で表示します
  • 34行目は移動量(差分)を表示します
  • 読み取りスケールをscale()メソッドで変更できるのですが、まだ使い方が分かっていません


FTDIチップとのUSBシリアル通信

最後が、FTDIのUSBシリアル変換チップとの接続です。以下は、ArduinoとFEZ DominoをUSBケーブルで接続し、USBシリアル変換(ArduinoのFT232RLチップ)を介して、Arduino - FEZ Domino間でシリアル通信を行うコードです(Arduino→ FEZ方向)。

このサンプルをビルドするためには、アセンブリ参照として、Microsoft.SPOT.Hardwareの追加が必要です。

using System.Threading;
using Microsoft.SPOT;
using GHIElectronics.NETMF.USBHost;

namespace FezFTDI


{
    public class Program
    {
        static USBH_SerialUSB serialUSB;
        static Thread serialUSBThread;      // Receive data from Arduino

        public static void Main()
        {
            // Subscribe to USBH event.
            USBHostController.DeviceConnectedEvent += DeviceConnectedEvent;
            // Sleep forever
            Thread.Sleep(Timeout.Infinite);
        }

        static void DeviceConnectedEvent(USBH_Device device)
        {
            Debug.Print("Device connected");
            switch (device.TYPE)
            {
                case USBH_DeviceType.Serial_FTDI: // FTDI connected
                    serialUSB = new USBH_SerialUSB(device, 9600, System.IO.Ports.Parity.None, 8,
                    System.IO.Ports.StopBits.One);
                    serialUSB.Open();
                    serialUSBThread = new Thread(SerialUSBThread);
                    serialUSBThread.Start();
                    break;
                case USBH_DeviceType.Unknown: // SiLabs but not recognized
                    // force SiLabs
                    USBH_Device silabs = new USBH_Device(device.ID, device.INTERFACE_INDEX,
                    USBH_DeviceType.Serial_SiLabs, device.VENDOR_ID, device.PRODUCT_ID,
                    device.PORT_NUMBER);
                    serialUSB = new USBH_SerialUSB(silabs, 9600, System.IO.Ports.Parity.None, 8,
                    System.IO.Ports.StopBits.One);
                    serialUSB.Open();
                    serialUSBThread = new Thread(SerialUSBThread);
                    serialUSBThread.Start();
                    break;
            }
        }

        static void SerialUSBThread()
        {
            // Print received data from Arduino
            byte [] data = new byte[8];
            char c;
            string receivedMsg = "";

            while (true)
            {
                if (serialUSB.Read(data, 0, 1) != 0)
                {
                    c = (char)data[0];
                    if (c >= 0x20)
                    {
                        receivedMsg += c.ToString();
                    }
                    else if (c == '\n')
                    {
                        Debug.Print(receivedMsg);
                        receivedMsg = "";
                    }
                }
            }
        }
    }
}
  • 27行目以降で、FTDIチップを認識した際の初期化処理を行っています
  • Arduino Nanoと接続して正常に受信ができることを確認しました。
  • 34行目以降は、Silicon LabのCP210Xに対応した処理なのですが、Japaninoをつなぐと39行目で例外が発生してしまいました


参考資料

FEZ Domino Tips (2) - .Net Micro Frameworkでms単位の時間計測を行う

.Netな人から見ると初歩的なネタですが、時間計測の方法について書きます。これは、サンプルコードを見てもらうのが一番早いですね。


サンプルコード

using System;
using Microsoft.SPOT;

namespace FezTimerTick
{
    public class Program
    {
        public static void Main()
        {
            long begin, end, elapsed;
            DateTime time_begin, time_end;
            int i;
            long sum = 0;

            // Method-1
            begin = DateTime.Now.Ticks;
            for (i = 0; i < 100000; i++)
                sum += (long)i;
            end = DateTime.Now.Ticks;
            elapsed = (end - begin) / TimeSpan.TicksPerMillisecond;
            Debug.Print("Elapsed time:" + elapsed.ToString());

            sum = 0;
            // Method-2
            time_begin = DateTime.Now;
            for (i = 0; i < 100000; i++)
                sum += (long)i;
            time_end = DateTime.Now;
            elapsed = (time_end - time_begin).Ticks / TimeSpan.TicksPerMillisecond;
            Debug.Print("Elapsed time:" + elapsed.ToString());
        }
    }
}

時間の取得にはDateTime構造体を使います。

  • 16行目にあるように、DateTime.Now.Ticksでその時点のCPU tick値が取得できます
  • DateTime.Now.Ticks / TimeSpan.TicksPerMillisecondでmsが取得できます。TimeSpan.TicksPerMillisecondの値は10000であるため、タイマーの粒度は100nsとなります
  • DateTime構造体のインスタンスは引き算(足し算も)ができるため、29行目のように経過時間を計算することもできます

FEZ Domino Tips (1) - プログラム実行を少し早くする方法

FEZ Dominoの販売がスイッチサイエンスさんで始まっていますが、Arduinoやmbedに比べるとサンプルコードやライブラリの使い方に関する解説が不足しています。ですので、備忘録も兼ねてDominoを触っている中で分かったことをTipsとして紹介していきます。体系化して書いていくというより、私が疑問に思ったことの順になりますのでその点はご容赦下さい。


プログラムの配置(Deploy)

VC++だとビルドにdebugとreleaseの二種類があり、releaseビルドの方がコードサイズが小さくなりかつ実行速度も上がります。FEZのチュートリアルではIDE(Visual Studio)のF5キーでプログラムをFEZに転送・実行するとありますが、F5はデバック開始キーですのでdebugビルドに相当するように思え、releaseビルドに相当するコードの転送方法ってないんだろうかと思っていました。

.NET Micro FrameworkのWhite Paperを読んでいたら、以下のように「ビルド→配置(deploy)」メニューを使ってプログラムの転送ができることが分かりました(こちらは転送だけで、実行は別途リセットを行う必要があります)。これだと早くなるかもです。

ProjectDeployment


実行速度のお試し

ループを使って大量の計算を行うサンプルプログラムで実行速度の違いを計ってみます。プログラム実行時間の表示にDebug.print()メソッドを使っているため、MFDeployを使って表示を行います。

(1) F5による転送(転送後、MFDeployで再度実行)

Matrix add (float)
End: 4150ms

Matrix add (ingeger)
End: 3908ms

Matrix add (float) with pointer
End: 6115ms

Simple add, mul and div
End: 6268ms

(2) ビルド→配置メニュー

Matrix add (float)
End: 3524ms

Matrix add (ingeger)
End: 3284ms

Matrix add (float) with pointer
End: 5490ms

Simple add, mul and div
End: 5022ms

上記の例では、(2)の方法だと、15%前後実行速度が上がりました。

FEZ Dominoでマルチスレッドを動かす

TinyCLR.comに.NET Micro Frameworkの使い方を解説したビギナーズガイドを見つけました。まだ読みかけですが、この内容を理解すればFEZの機能を一通り使うことができそうです。今回は、マルチスレッド機能を使って2つのLEDを異なるインターバルで点滅するサンプルを書いてみました。


サンプルコード

サンプルコードをコンパイルするためには、アセンブリ(コンパイル済みのライブラリ)への参照を追加設定する必要があります。今回は以下のアセンブリを設定しました。詳しくは、ビギナーズガイドの7.2章を参照して下さい。

  • FEZDomino_GHIElectronics.NETMF.FEZ(FEZMini_GHIElectronics.NETMF.FEZも参照設定に加えると、名前空間の定義が重複してエラーとなります。使っているハードに合わせて、FEZDomino or FEZMiniの何れかを設定)
  • GHIElectronics.NETMF.Hardware
  • GHIElectronics.NETMF.System
  • Microsoft.SPOT.Hardware
  • Microsoft.SPOT.Native
  • System

サンプルコードは以下です:

using System;
using System.Threading;
using GHIElectronics.NETMF.FEZ;
using GHIElectronics.NETMF.Hardware;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace FezMultiThred
{
    public class Program
    {
        static OutputPort LED1;
        static OutputPort LED2;
        static OutputPort OnboardLED;

        public static void Main()
        {
            LED1 = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.Di0, false);
            LED2 = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.Di1, false);
            OnboardLED = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.LED, false);
            Thread thread1 = new Thread(Led1Thread);
            Thread thread2 = new Thread(Led2Thread);

            thread1.Start();
            thread2.Start();

            Thread.Sleep(Timeout.Infinite);
        }


        // Thread to blink LED1
        public static void Led1Thread()
        {
            while (true)
            {
                LED1.Write(!LED1.Read());   // Read LED1 port -> Invert -> Write new status
                Thread.Sleep(100);
            }
        }

        // Thread to blink LED2
        public static void Led2Thread()
        {
            while (true)
            {
                LED2.Write(!LED2.Read());   // Read LED2 port -> Invert -> Write new status
                Thread.Sleep(1000);
            }
        }

    }
}
  • 18~19行:LED1としてDi0、LED2としてDi1ピンを割り当てています
  • 21~22行:LED1とLED2を制御するスレッドを生成
  • 24~27行:LED制御スレッドを起動し、自身(メインスレッド)はスリープ状態に遷移
  • 31行以降:Led1Threadは100ms周期でLED1を点滅、Led2Threadは1000ms周期でLED2を点滅させます
  • 36行目のコードはドキュメントから持ってきましたがちょっとトリッキー。先ず、現在の出力ポートの状態を読み取って(Pin value register: FIOPINを読み取っているのではないかと想像)、それを反転して書き込むことによって、LEDの点滅状態を反転します

ピン番号の指定は、OutputPort((Cpu.Pin)FEZ_Pin.Digital.まで入力すると、インテリセンス君がDigitalOutで使えるポート(この場合は全ピンが対象)を候補として示してくれるので楽ちんです。

ちなみにPWMを使いたい場合は、PWM((PWM.Pin)FEZ_Pin.PWM.まで入力すると、PWMが使用できるピンの候補(Di3, Di5, Di6, Di9, Di10 LED)を出してくれます。Visual Studioを使った方ならご存知だと思いますが、関数・パラメーター名の補完機能も非常に賢いため、実際のタイプ量は文字数の半分以下で済みます。自分はこのせいで、メンバー変数・メソッド名をいつまでたっても覚えられないです・・

LEDは写真のように、電流制限抵抗をかましてMCUから直接ドライブしています。電流はLEDあたり10mA程度にしているため、2個くらいならMCUで直接ドライブしてもOKでしょう。

TwoLED_Control


Digitalピンの初期値

20行目のコードを入れずにこのサンプルを動かすと、起動後もオンボードLEDが半点灯状態となります。(制御対象のLEDもリセット中は半点灯状態)。オンボードLEDの電圧を計ると1.6Vちょい出ています。LPC2388はリセット時全ピンの機能選択がGPIO、入力、プルアップとなるため、プルアップ抵抗経由の電圧が見えているのではと思います。LEDを消すために、20行目のコードでオンボードLEDをつないでいるポートを出力、Lowレベルに初期化しています。

上記のLED制御くらいファーム(CLR)の初期化処理でやってくれてもよいのにと思いますが、、ちなみに、mbedでもオンボードLEDがうっすらと(自分の固体では電気を消さないと見えないくらい)点灯します。


マルチスレッドはRTOS相当?

自分はRTOSを使ったことがないため比較ができないのですが、マルチスレッドを使うと、今回のように複数の制御を行う処理がエレガントに記述できますね。割り込みを使えばリアルタイム処理もできるのではないかと思います。TinyCLR.comのFAQには、「FEZ(.NET MF)はリアルタイムか?」という問いに対して、厳密なリアルタイムではないが、1~10ms程度のレスポンスは可能でありこの範囲では「ほぼリアルタイム」だと言っています。

FEZ Domino Get

MTM05でスイッチサイエンスさんから、FEZ Dominoをゲットしてしまいました。実は前から気になっていたのですが、イベント会場で買えると分かって誘惑に耐え切れず。.NET Micro Framework SDKやFEZ SDKはだいぶ前にインストールしていたのですが、環境構築・ファーム更新・お決まりのLチカまでをまとめました。

TinyCLR.comにFEZ Tutorialドキュメントやビデオチュートリアルがありますが日本人には英語の壁が立ちはだかりそうな環境ではあります。

2010/5/23追記: Tutorialドキュメントの場所が分かったため、記載を修正しました。

環境設定

以下のコンポーネントをインストールします。

  1. Visual C# 2008 Express (現時点は、.Net MFはVS2010には未対応です)
  2. .NET Micro Framework 4.0 SDK
  3. GHI NETMF SDK (FEZ固有部分のアセンブリー等)
    TinyCLR.comのダウンロードページからSDKをダウンロード(執筆時点はVersion 1.0.5が最新)
  4. FEZのDebug Interfaceドライバーをインストール
    GHI NETMF SDKをインストール後FEZをPCにUSB接続すると、不明なデバイスが現れるので(ひょっとすると、この時点でデバイスドライバーのインストールを要求してくるかも)、C:\Program Files\GHI Electronics\GHI NETMF v4.0 SDK\USB Drivers\GHI_NETMF_Interfaceのドライバーをマニュアルインストール

USBケーブルをつなぐと、出荷時に書き込まれたLチカが動作しました。

2010/5/23追記VS2010の対応はあと2ヶ月ぐらいで行うそうです。


ファームウェアの更新

<ファームウェア版数の確認>

最新のGHI NETMF SDKはFEZのファームウェアとして、USBizi V4.0.3.0を要求します。本体のファームウェア版数は、C:\Program Files\Microsoft .NET Micro Framework\v4.0\Tools\MFDeploy.exeで確認が可能です。DeviceプルダウンからUSBを選択し、Target→Device Capabilitiesを選択すると以下の表示が出ます。

FEZ_Ver_Initial

SolutionReleaseInfo.solutionVersionがファームウェア版数となり、4.0.2.0となります。即ち、V4.0.3.0へのアップデートが必要ということですが、ファームのバージョンアップ手順が少々複雑でつまづきました。

<バージョンアップ手順>

ファームのバージョンアップはXMODEMを使うのですが、ターミナルソフトとしてTeraTermが推奨となっています。ビデオではそんなこと言っていないと思うのですが、TutorialドキュメントではTeraTermのバージョンによってはXMODEMを使ったファイル転送で問題が出る場合があるため、メーカ側で確認が取れている、TinyCLR.comのダウンロードページにあるTeraTermを使用することと書いてあります。私がインストールしていたTeraTermよりバージョンが古いのですが、念のために推奨版をインストールしました。

いよいよ、ファームウェアの更新です。まず、FEZのLDRボタンを押しながらRESETします。この操作でブートローダーが起動しますが、ブートローダー用の仮想COMポートドライバーをインストールする必要があります。ここでWindowsがドライバーを要求してくるので、C:\Program Files\GHI Electronics\GHI NETMF v4.0 SDK\USB Drivers\GHI_Bootloader_Interfaceのドライバを指定します(環境設定でインストールした、Debug Interfaceドライバーとフォルダーが異なりますので要注意です)。私の環境では、ドライバーインストールが勝手に始まってエラー終了してしまい、ドライバーを聞いてこなかったのですが、その場合はデバイスマネージャーを開くと不明なデバイスが見えているので、マニュアルで上記フォルダーのドライバーをインストールします。

ドライバーインストール後、TeraTermを起動してシリアル接続を行うと、仮想COMポート経由で接続ができます(私の環境ではCOM11でした)。マニュアルやビデオでは、仮想COMポートドライバインストール前に、PC組み込みの物理COMポート(COM1とか)が見えることを確認する手順になっていますが、私のPCはBIOSでCOMポートを殺していたため(かつこのことを忘れており)、物理COMポートが見えないところでしばし足踏みしてしまいました。物理COMポートの確認なんて本質的には関係ないので、すぐに仮想COMポートの確認に行けばよいのに・・

仮想COMポート経由で接続ができると、TeraTermからブートローダーコマンドの投入が可能となります。以下のようにコマンドを投入して、ファームウェアをFEZに転送します。

  1. b(小文字)を入力して、BLが返ることを確認
  2. V(大文字)を入力して、ブートローダーバージョンを確認
  3. X(大文字)を入力するとCが連続して表示されます。これは、FEZがXMODEMのデーター受信待ち状態であることを示します
  4. TeraTermのファイル→ 転送→ XMODEM→ 送信メニューから、C:\Program Files\GHI Electronics\GHI NETMF v4.0 SDK\USBizi\Firmware\USBizi_CLR.GHIを選択します。この際、転送オプションとして「1K」を選択します
  5. 転送が終了したら接続を切断します。ここでbとかのコマンドを入れるとファーム更新に失敗します
  6. MFDeploy.exeでファームウェアバージョンを確認

FEZ_FrimUP_XMODEM_Option

手順2)と3)の間で、Eコマンドを入力して旧ファームを消去することもできるのですが、ビデオチュートリアルは直接上書きしていたので、消去の必要はないと思います。
 

サンプルプログラム

ビデオチュートリアルとにらめっこして、以下の通り、チュートリアル通りのサンプルLチカコードを作成しました。

using System.Threading;
using GHIElectronics.NETMF.FEZ;

namespace FezTest
{
    public class Program
    {
        public static void Main()
        {
            // Use FEZ_Componets driver class 
            FEZ_Components.LED onBoradLED = new FEZ_Components.LED(FEZ_Pin.Digital.LED);

            onBoradLED.StartBlinking(500, 100);     // On 500ms, Off 100ms

            Thread.Sleep(Timeout.Infinite);         // Sleep forever
        }

    }
}

LEDドライバークラスを使うと、すごく簡潔にコードが書けます。このあたりは、.NET Frameworkの威力ですが、ハードをどのように抽象化(クラスライブラリ化)しているのかまだよく分かっておらず、お勉強が必要です。IDEのイメージとLチカ動作中の写真を以下に示します。

FEZ_LED_blink

FEZ_LED_Flash

 

おわりに

デバックも軽く試してみましたが、レスポンスもそこそこでした。.NETの基本ライブラリがMicro Frameworkでどこまで使えるのかお勉強する必要がありますが、これがほぼ使えて、かつ色んなデバイスのドライバーがコミュニティーレベルでそろえば最強の環境かもと思えてきました。

FEZ Dominoのハードは、Arduinoのフォームファクターをあわせていることによる面積的な制約がありますが、144ピンのLPC2388を使っているのに、使用できるピン数がちと少ないと思います。まあ、COMポートが2ポートあることや、拡張コネクタの8ピンもDigital I/Oとして使えることを考えると通常の使用で不足することはなかろうかと思いますが、未配線のピンがいっぱいあってもったいないなぁと・・

プログラム実行面では、.NETなので中間コード(MSIL)をCLRがJITコンパイルインタープリターベースで実行しています。そのため、ネイティブコードで書いたプログラムとパフォーマンスの違いをベンチマークできればと思っています。インタフェース付録基板とFEZはLPC2388 72MHzで同等のMCUを積んでいるため、比較ができないかと思っています。

2010/5/29追記: MicrosoftのWhite Paperによると、NETMFは少メモリー環境で動かすために、JITコンパイルではなくインタープリター(逐次翻訳)ベースで動作します。そのため、C/C++で書いたネイティプコードに対して実行速度はあまり期待できません。

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        
無料ブログはココログ