« 2010年4月 | トップページ | 2010年6月 »

2010年5月の記事

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++で書いたネイティプコードに対して実行速度はあまり期待できません。

mbed + GCCでprintfを使う

タイトルの通り、newlibをリンクしてgccベースでprintfなど標準Cライブラリを使えるようにしました。printf, putchar, puts, getchar, gets程度しか試していないのですが、newlibが使えるようになった過程を記載します。

syscalls.cを実装する

newlibやglibcを使うためには、ライブラリの基本入出力をOSのシステムコールに渡してやる必要があります。組み込みでOSを使っていない場合は、入出力のハードをたたくコードを書く必要があります。syscalls.cというモジュールに所定の関数を定義することで、ライブラリとのインタフェースを行うことができます。

syscalls.cの中では、read、write、sbrk(ヒープの管理)などの関数を実装しますが、関数に2種類あります。
マルチタスク(スレッド)環境で、中断中に別タスクからの呼び出しを可能とするリエントラント型と、非リエントラント型です。リエントラント型はread_rのように、関数名に_rのサフィックスがつきます。また、引数にstruct _reent構造体へのポインタを持ちます。

今回の用途ではリエントラント型にする必要はありませんが、リエントラント型で実装してみました。syscalls.cのコードを以下に示します。fileopenなどハード的に機能がない関数は、-1を返すだけとかの、最小限の実装にします。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/reent.h>
#include "lpc17xx_uart.h"

#define ECHOBACK

extern int errno;
extern unsigned char _end;
unsigned char *heap_end;
register unsigned char *stack_ptr asm ("sp");
LPC_UART_TypeDef *Uart = (LPC_UART_TypeDef *)LPC_UART0;

int _read_r(struct _reent *r, int file, char *ptr, int len)
{
    int  i;
    unsigned char *p = (unsigned char*)ptr;

    for (i = 0; i < len; i++) {
        UART_Receive(Uart, p, 1, BLOCKING);

        #ifdef ECHOBACK
            UART_Send(Uart, p, 1, BLOCKING);
        #endif

        if (*p++ == '\r' && i <= (len - 2)) /* 0x0D */
        {
            *p = '\n';                        /* 0x0A */
            #ifdef ECHOBACK
              UART_Send(Uart, p, 1, BLOCKING);     /* 0x0A */
            #endif
            return i + 2;
        }
    }
    return i;
}

int _lseek_r(struct _reent *r, int file, int ptr, int dir)
{
  return 0;
}

int _write_r(struct _reent *r, int file, const void *ptr, size_t len)
{
    int i;
    unsigned char *p = (unsigned char*) ptr;

    for (i = 0; i < len; i++) {
        if (*p == '\n' ) {
            UART_Send(Uart, (uint8_t*)'\r', 1, BLOCKING);
        }
        UART_Send(Uart, p++, 1, BLOCKING);
    }
    return len;
}

int _close_r(struct _reent *r, int file)
{
  return -1;
}

caddr_t _sbrk_r(struct _reent *r, int incr)
{
    unsigned char *prev_heap_end;

/* initialize */
    if( heap_end == 0 ) {
        heap_end = &_end;
    }
    prev_heap_end = heap_end;

#if 1
    if( heap_end + incr > stack_ptr ) {
       /* heap overflow  */
    UART_Send(Uart, (uint8_t*)"Heap Overflow\r\n", 15, BLOCKING);
    return (caddr_t) -1;
    }
#endif

    heap_end += incr;
#if 0 // Debug
    char buff[32];
    sprintf(buff,"incr:%d, heap:%x\n", incr, heap_end);
    UART_Send(Uart, (uint8_t*)buff, strlen(buff), BLOCKING);
#endif
    return (caddr_t) prev_heap_end;
}

int _fstat_r(struct _reent *r, int file, struct stat *st)
{
  st->st_mode = S_IFCHR;
  return 0;
}

int _open_r(struct _reent *r, const char *path, int flags, int mode)
{
  return -1;
}

int _isatty(int fd)
{
  return 1;
}

char *__exidx_start;
char *__exidx_end;

サンプルを動かしてみる

printfでもろもろの情報を表示した後で、getchar, putcharを使って文字の入出力を行う、ついでにTimer1割り込みを使ってLEDを1sec周期で点滅する、サンプルを作ってみました。ソースコードをここに置きます

ヒープやスタックが正しく動いているか(メモリーリークなどないか)を調べるためのコードを入れてみました。初回のprintfを実行した時点でheaptopが0x10001000となり、.data/.bssエリアと合計になりますが、4KbyteのRAMを消費しています。getcharを使用すると、read_r関数が呼ばれ引数に入力bufferのポインターと文字数が飛んできますが、getcharのような1文字入力でもread関数には1000byteのbuffer付きで呼び出しをかけてきます。メモリーリソースはバンバン使ってくると言えます。

BINファイルのサイズもprintfをリンクしただけでぐっと大きくなり、40Kbyteを超えます。LPC17XXクラスならROMの消費量は気になりませんが、ヒープを結構消費するので、RAMの使用量は注意が必要です。(AVRだとちょっと使えないですね)

試しに、mallocで2048byteのメモリーを確保すると、heaptopが0x10002000なり、heap領域を新たに4Kbyte確保します。確保したメモリーをfreeで開放してもheap領域は縮小せずそのままキープされます。mallocで確保したメモリーはnewlibのメモリー管理によってchunkと呼ばれる単位で管理され、一定の条件を満たすとheap領域の返却を行うようですが、頻繁にheapサイズの変更を行わない作りになっているみたいです。

サンプルを起動した際の、ヒープの変動を以下に示します。

Start
Now called printf
Stack:10007fe8
Heap:10001000

malloc 64byte
Heap:10001000
free 64byte
Heap:10001000
malloc 2048byte
Heap:10002000
free 2048byte
Heap:10002000

 

今回はまったこと

write_r関数は引数に送信バッファを取ります。関数内でCMSISライブラリの1文字出力(UART_SendData)をforループで呼んでいたのですが、UART_SendDataは送信FIFOが空になることを確認せずにreturnしてくるんです。それに気がつかず、UART_SendDataをforループで連続して呼び出すとFIFOがオーバーフローして出力文字のお尻が欠落する現象が発生します。出力文字列の長さが一定以上になると現象が発生するため、最初は、ヒープの管理やスタックの初期設定などの要因でメモリーを壊していないかなど色々調べて時間を要しました。

こんなことで悩むのも楽しみ(頭の体操)の内ではありますが、我ながら初歩的なところではまっているなぁと思います。ちゃんとしたデバック環境がない(mbedでは持てない)からだめなんだな、と言い訳してみる・・

参考情報

  1. Embedding GNU: Newlib, Part 2
  2. ねむいさんのブログ - 今頃LPC2388基板(CQ- FRK-NXPARM)とかいぢってみる ... 今回もお世話になりました
  3. malloc(3) のメモリ管理構造 VA Linux Systems Japan

GCCでmbedの実行ファイルをコンパイル(2)

前回に引き続き、GCC + mbedネタです。前回、外部変数のSystemCoreClockが正しく初期化されないと書きましたが、その原因が判明しました。変数領域の初期化やリンカーの動作に関わる部分でなかなかディープな世界でした。リンカーの動作やリンカースクリプトの書式はまだまだブラックボックス(先人の成果を拝借している)が多いのですが、分かったことを記載します。

メモリー領域の種別

先ずはCortex-M3(その他のMCUでも同様)のメモリ配置について記載します。コンパイラでC等のソースをコンパイルすると、コードの他に静的な変数を格納する領域が必要になります。組み込みの場合、コードはFlash ROMに配置し、静的変数はRAMに配置する必要があります。ソースを複数モジュールに分割した場合、モジュール単位にコードや変数領域が生成されますが、リンカーはメモリー領域の種別単位に並び替えを行ってプログラムやデーターのメモリ割付を行います。

リンカーが識別するメモリー領域(セクションと呼ばれる)には以下の種別があります。

  • .text: ROMに配置するプログラムコード
  • .data: 初期値付きの静的変数
  • .bss: 初期化を行わない変数(値を代入しない外部変数とか)
  • .heap: mallocで動的に確保するデーター領域
  • .stack: スタック

次に、データーの宣言と格納領域の関係を考えてみます。

以下の文字列定義を行うと、データーは.textセクションに配置されROMに格納されます。
static const char[] msg = "Hello World\n";
この場合、文字列は定数(const指定)ですので、ROMに配置しても問題ありません。

では、以下のように定義すると、初期値付き変数として、.dataセクションに配置されます。
static char[] msg = "Hello World\n";
この場合文字列はRAMに配置されます。

.dataセクションの初期化

ここで、.dataセクションに配置した文字列などのデーターをどうやって初期化するかという問題が出てきます。リンカーは、.dataセクションに設定すべき初期値をROMの最後(コード領域の次)に格納してくれるので、スタートアップコードの中でこの領域をRAMの.dataセクションにコピーする必要があります。コードの中で、以下の静的変数を定義したとします。

/*----------------------------------------------------------------------------
  Clock Variable definitions
*----------------------------------------------------------------------------*/
uint32_t SystemCoreClock = __CORE_CLK;/*!< System Clock Frequency (Core Clock)*/

/************************** PRIVATE VARIABLES *************************/
static uint8_t menu1[] = "Hello NXP Semiconductors \n\r";
static const uint8_t menu2[] = "UART polling mode demo \n\r\t MCU LPC17xx - ARM Cortex-M3 \n\r\t UART0 - 9600bps \n\r";
static const uint8_t menu3[] = "\r\nUART demo terminated!\r\n";

上記のコードでは、SystemCoreClockとmenu1が.dataセクションに配置されます。コンパイル・リンクしたbinファイルをダンプすると。末尾が以下となっています。

  1. 0x002604~のエリアにmenu2, menu3で定義した文字列が格納されていることが分かります
  2. 0x002690に格納されているワードデーター(0x05B8D800)がSystemCoreClock変数の初期値で、96Mになります
  3. 0x002694~のエリアにmenu1で定義した文字列が格納されています

CodoAllocation

2)3)のデータを、リセット時に.dataセクションにコピーしないと正しい変数として認識されません。前回使ったスタートアップコードにはこのコピー処理が入っていなかったため、SystemCoreClock変数が初期化されず、クロック周波数設定が正しくできなかったという顛末でした。

前回使ったスタートアップコードはCMSISライブラリのexampleに入っていたのですが、何で.dataセクションの初期化が入っていないんじゃ!(と、自分の無知を棚に上げて文句を言ってみる・・)

セクションの開始番地情報

では、セクションの開始番地情報をどうやって取得するかというと、リンカーがexportしているシンボル情報を使います。上記のbinファイルを生成したプロジェクトのmapファイルは以下となります。

                0x00002690                _etext = . ← .textエリアが終了し、以後.dataにコピーすべきデーターを配置する
                0x00080000                __cs3_region_size_rom = 0x80000
                0x00000001                __cs3_region_num = 0x1
                0x10000000                _data = ADDR (.data) ← .dataセクションの開始(RAMの先頭番地)

.data           0x10000000       0x20 load address 0x00002690
                0x10000000                __cs3_region_start_ram = .

                0x10000020                . = ALIGN (0x8)
                0x10000020                _edata = .  ← .dataセクションの終了番地
.bss            0x10000020       0x50 load address 0x000026b0

従ってスタートアップコードの中で、_etextから始まるROM領域を、_data~_edataにコピーすればよいことになります。加えて、.bssセクションは0でクリアしておきます。

上記のアドレス情報をリンカースクリプトで設定します。リンカースクリプトの詳細は末尾の参考情報を見てください、、基本はCMSISライブラリのexampleに入っていたスクリプトを使っています(一部修正)。一部抜粋を示します。

ENTRY(_start)    ← スタートアップコートの開始ラベル

SECTIONS
{
  .text :
{

} ← .textセクションが終了

.text.align :
{
  . = ALIGN(8);
_etext = .; ← 引き続き、.dataセクションの初期値を配置し、_etextのラベルをexport
} >rom

_data = ADDR(.data);  ← データーセクションの開始位置をexport
.data :
{

_edata = .;
} >ram AT>rom ← データーはramに配置

サンプルプログラム

今回のスタートアップコード修正を盛り込んだサンプルプログラムをここに置きました。SysTick割り込みを使って、LEDを1秒周期で点滅しながら、UART0にて文字の入出力を行います。UARTの初期化と入出力はCMSISライブラリを使っています。

最後に

サンプルを動かした際に、出力が文字化けしてしまうので散々悩みました。最初はCMSISライブラリを使ったボーレート設定がうまくいっていないのかと思い、インタフェース付録基板(LPC2388)のサンプルを参考に、DLL/DLM, FDRレジスタを直接設定すると文字化けが解消したため、ライブラリが原因と半分思いかけていました。実はこの時文字列定義にconstを付けたので、.dataセクションが初期化されていなくても「たまたま」うまく動いていたんですね。const宣言を抜くと、レジスタ直打ち設定でも文字化けすることが分かり、そこから色々と調べて、.dataセクションの初期化が必要なことにたどり着きました。

STM32 Primer2のスタートアップコードを見ると、なにやらデーターのコピー処理を行っているので、「何のためにこんなことやってるの?」と疑問に思ったのですが深く考えていなかったんですね。今となってはRAMデーターを初期化するためには至極当たり前の処理ですが・・

.etextから.dataセクションに初期値をコピーする処理はアセンブラで書いているのですが、STM32 Primer2のコードは何だが冗長に思えて(ループの先頭で毎回同じ値をレジスタにロードしたりしている)、自前で最適化版を作ったのですが、これがまた動かず。結局、STM32のコードをパクッています。

なんとかUARTまで動くようになったので、次はnewlibをリンクして、printfを使えるようにする予定です。

参考情報

  1. CMSISライブラリ
  2. GNUリンカーの使い方
  3. Building Bare-Metal ARM Systems with GNU
  4. STM32用リンカスクリプトを書く
  5. ねむいさんのブログ - 今頃LPC2388基板(CQ- FRK-NXPARM)とかいぢってみる .. LPC2388用のコードで検証することで開発環境要因がないことを切り分けることができ、大変重宝しました

GCCでmbedの実行ファイルをコンパイル

あまりメリットはないのですが、GCC(オフライン環境)でmbedのプログラムをコンパイルして実行することができました。GPIOを直接たたいてオンボードLEDを点滅するだけの単純なプログラムですが一発では動いてくれず苦労しました。以下、GCCを使用したmbedの開発手順です。

開発環境

Windows用のgccやAndroid SDKと同居していますが、ARM関係の開発環境は以下です。

  • Eclipse 3.4.2
  • CDT 5.0.2
  • Zylin Embedded CDT 4.11.1
  • CodeSourcery G++ 4.4.1

LPC 1768用のCMSISライブラリ、その他の入手

このWebページ(Compiling your own CMSIS Code for the mbed)のリンクをたぐってCMSIS Version 1.30をダウンロードし、以下のファイルをプロジェクトディレクトリに展開します。スタートアップコードやリンカースクリプトも入手できます。

  • CMSIS_V1P30/CM3/CoreSupport/core_cm3.c
  • CMSIS_V1P30/CM3/CoreSupport/core_cm3.h
  • CMSIS_V1P30/CM3/DeviceSupport/NXP/LPC17xx/system_LPC17xx.c
  • CMSIS_V1P30/CM3/DeviceSupport/NXP/LPC17xx/system_LPC17xx.h
  • CMSIS_V1P30/CM3/DeviceSupport/NXP/LPC17xx/LPC17xx.h
  • CMSIS_V1P30/CM3/Example/Sourcery G++Lite/LPC17xx/startup_LPC17xx.s
  • CMSIS_V1P30/CM3/Example/Sourcery G++Lite/LPC17xx/LPC17xx.ld
  • CMSIS_V1P30/CM3/Example/Sourcery G++Lite/LPC17xx/main_LPC17xx.c
  • Makefile

リセット時のデフォルト設定でも問題ないと思うのですが、NVICの初期設定も行っています。そのために、Cortex Microcontroller Software Interface Standard(file CMSISとある箇所)から追加のCMSISライブラリをダウンロードして、以下のファイルもプロジェクトディレクトリに展開します。

  • CMSIS\Drivers\include\pc17xx_libcfg_default.h
  • CMSIS\Drivers\source\lpc17xx_libcfg_default.c
  • CMSIS\Drivers\include\lpc17xx_libcfg.h
  • CMSIS\Drivers\include\lpc17xx_nvic.h
  • CMSIS\Drivers\source\lpc17xx_nvic.c

このサイトで手に入るライブラリはSTMicroのPeripheral Libraryと同様の内容でPeripheralをたたくのに便利なライブラリー類です。ソースを見ると作成はNXPのようなことが書いてあるのですが、NXP本家のWebサイトにはPeripheral libraryは見つからず今一素性が不明なのですが、便利なので使っています。
2010/07/27更新: LPC17xx用のCMSISライブラリは、ちゃんとNXP本家でも公開されていました。探し方が悪かった。

Lチカのサンプルコード

system_LPC17xx.cを若干修正しています(PLL0の設定パラメーターをmbedの設定値にあわせるなど)。ソース一式を以下に示します。SystemCoreClock変数初期化のバグを修正しました。
mbed_test_gcc_v2.zip

処理の流れですが、スタートアップコード(startup_LPC17xx.s)から、system_LPC17xx.cのSystemInit()を呼び出しここでPLLの初期設定を行った後で、main_LPC17xx.cのmain()関数を呼び出します。

サンプルプログラムはSysTick割り込みを1ms周期で発生させ、1000msのカウントを行うことによって、mbedオンボードのLED1を1s周期で点滅させます。点滅自体はすぐに動いたのですが、電源On/Offを繰り返すと起動タイミングによって異常に早い周期でLEDが点滅したりし、どうも動作が安定ませんでした。PLLの設定パラメーターを見直したり、NVICの初期化を追加したりしても状況変わらずで挫折しかかっていたのですが、main関数の先頭でSystemCoreClockUpdate()を呼び出すことで問題が解決しました。

原因は、クロック周波数を保持するSystemCoreClock変数が正しく初期化されておらず、誤ったクロック周波数を参照してタイマー周期を計算したためだと思います。具体的には、以下のコードが怪しいです。

/*----------------------------------------------------------------------------
  Define clocks
 *----------------------------------------------------------------------------*/
#define XTAL        (12000000UL)        /* Oscillator frequency               */
#define OSC_CLK     (      XTAL)        /* Main oscillator frequency          */
#define RTC_CLK     (   32000UL)        /* RTC oscillator frequency           */
#define IRC_OSC     ( 4000000UL)        /* Internal RC oscillator frequency   */


/* F_cco0 = (2 * M * F_in) / N  */
#define __M               (((PLL0CFG_Val      ) & 0x7FFF) + 1)
#define __N               (((PLL0CFG_Val >> 16) & 0x00FF) + 1)
#define __FCCO(__F_IN)    ((2 * __M * __F_IN) / __N)
#define __CCLK_DIV        (((CCLKCFG_Val      ) & 0x00FF) + 1)

/* Determine core clock frequency according to settings */
 #if (PLL0_SETUP)
    #if   ((CLKSRCSEL_Val & 0x03) == 1)
        #define __CORE_CLK (__FCCO(OSC_CLK) / __CCLK_DIV)
    #elif ((CLKSRCSEL_Val & 0x03) == 2)
        #define __CORE_CLK (__FCCO(RTC_CLK) / __CCLK_DIV)
    #else
        #define __CORE_CLK (__FCCO(IRC_OSC) / __CCLK_DIV)
    #endif
 #else
    #if   ((CLKSRCSEL_Val & 0x03) == 1)
        #define __CORE_CLK (OSC_CLK         / __CCLK_DIV)
    #elif ((CLKSRCSEL_Val & 0x03) == 2)
        #define __CORE_CLK (RTC_CLK         / __CCLK_DIV)
    #else
        #define __CORE_CLK (IRC_OSC         / __CCLK_DIV)
    #endif
 #endif


/*----------------------------------------------------------------------------
  Clock Variable definitions
 *----------------------------------------------------------------------------*/
uint32_t SystemCoreClock = __CORE_CLK;/*!< System Clock Frequency (Core Clock)*/

上記のコードで、19行目の__CORE_CLK、39行目のSystemCoreClockが正しく設定されないのではと思います。Cのマクロはどうも苦手です・・

2010/5/22追記:SystemCoreClockが初期化されない理由が判明。その2に原因を記載しました。

今後の予定

UARTを使ってコンソール出力ができるようにしたいと思っています。Printf関数をリンクできるようにするのが目標です。なぜこんなことをやっているかというと、mbedのオンラインIDEで動いているARM純正コンパイラとGCCで、最適化レベルやコードサイズを比較してみたいためです。

ちなみに、今回gccでコンパイルしたLチカコードはbinファイルで2,992バイトに対して、mbedオンラインIDEで作成した同等のコードサイズは9,876バイトになりました。ただ、gcc版はスタートアップコードや初期化コードで必要最小限の処理しか行っていないため、今回の条件ではコードサイズの比較はできないと思います。

« 2010年4月 | トップページ | 2010年6月 »

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