カテゴリー「mbed」の記事

mbed TY51822r3でmbed OSを使う

スッッチサイエンスさんからBLEつきのmbedボード、mbed TY51822r3を買いました。mbed TY51822r3はまだオフィシャルにmbed OSのサポート対象になっていないのですが、mbed OS対応ボードのNordic nRF51-DKと互換性があります(差分は動作クロックが32MHzとなっており、Nordic nRF51-DK の16MHzと異なること)。差分がクロック周波数だけならきっとmbed OSが動いてBLEで遊べるだろうと思い、ポッチてみた次第です。

ということで、mbed TY51822r3でmbed OSを動かしてみました。最初はNordic nRF51-DK をターゲットボードに指定して、ダウンロードしたSDKのSystemInit関数を書き換えて32MHzクロックを有効にすれば良いのかと思っていたのですが、スイッチサイエンスさんの @ytsuboi さんがご自身のGitHubでmbed TY51822r3用のターゲット定義情報や32MHzクロックに対応したSDKをすでに公開されていることを教えていただき、このリソースを活用させていただきました。

mbed OSで開発を行う際には、最初にターゲットにするボードを指定して定義ファイルをダウンロードする必要があります。ターゲットとするボードがmbed OSのオフィシャルサイトに登録されている場合は、そのボード名を指定すればよいのですが(例えば、Nordic nRF51-DK の場合はnrf51dk-gcc)、プライベートなGitHubに登録されている定義情報をurlやgitリポジトリーを指定してダウンロードする方法がわかりませんでした(色々と試したのですがうまくいかず)。そのため、mbed TY51822r3用の定義ファイルを一旦ローカルにダウンロードして、ローカルレボジトリとしてターゲット指定する方法でビルドしています。このあたりについては、もっとうまいやり方があればコメント下さい。

以下、mbed TY51822r3でmbed OS対応のアプリをビルドする手順を示します。

ビルドはOS Xで行っています。
mbed OSのフィルを格納するディレクトリを~/mbedOSとします。

$ cd ~/mbedOS

# mbed TY51822r3用の定義ファイルをダウンロード
$ git clone https://github.com/ytsuboi/target-ty51822r3-gcc

# mbed TY51822r3用のSDKをダウンロード
$ git clone https://github.com/ytsuboi/nrf51-sdk

$ mkdir ty51822-blink                  # 作成するプロジェクトのフォルダーを作成
$ cd ty51822-blink 
$ yotta init                           # プロジェクトの初期化

$ cd ~/mbedOS/target-ty51822r3-gcc/    # DLしたターゲットファイルのフォルダーに移動
$ yotta link-target                    # ローカルリンクを設定
$ cd ~/mbedOS/ty51822-blink/           # プロジェクトフォルダーに戻る
$ yotta link-target ty51822r3-gcc    # ローカルリンクのターゲット名として、ty51822r3-gccを登録
$ yotta target ty51822r3-gcc           # ローカルリンクty51822r3-gccをターゲトに指定

$ yotta target                         # 関連ファイルDL後にターゲットが設定されていることを確認
ty51822r3-gcc 1.0.0 -> /Users/kenshi/mbedOS/target-ty51822r3-gcc
nordic-nrf51822-gcc 1.0.0
mbed-gcc 1.2.2

$ yotta install mbed-drivers           # ドライバーのダウンロード
$ cd ~/mbedOS/nrf51-sdk/script         # ローカルにDLしたSDKファイルのフォルダーに移動

# スクリプトを実行してSDKをプロジェクトフォルダーにコピー
# command line: pick_nrf51_files.py  <SDKフォルダーのパス> <プロジェクトフォルダーのパス>
$ python pick_nrf51_files.py ~/mbedOS/nrf51-sdk/ ~/mbedOS/ty51822-blink/

$ cd ~mbedOS/ty51822-blink/            # プロジェクフォルダーに移動
<add source/app.cpp>                   # ソースファイルを書く 
$ yotta build                          # ビルド

# 出来上がったhexファイルをmbedにコピーして実行
$ cp build/ty51822r3-gcc/source/ty51822-blink.hex /Volumes/MBED/

サンプルーコード(普通のLチカです…)

#include "mbed-drivers/mbed.h"

static void blinky1(void) {
    static DigitalOut led1(LED1);
    led1 = !led1;
    printf("LED1 = %d \r\n",led1.read());
}

void app_start(int, char**) {
    minar::Scheduler::postCallback(blinky1).period(minar::milliseconds(100));
}

無事mbed OSでLチカが動きました。mbed TY51822r3はGPIOのドライブ能力が0.5mAと小さく、LEDを直接ドライブすることができないため、FETを使ってスイッチングする必要があります。

mbed OSでLチカ

既に記事がポストされていますが、mbed OS(執筆時点では、mbed OS 15.11 Technology Previewとなっています)を使ってLチカを行ってみました。ターゲットのmbedボードはFRDM-K64Fを使っています。Lチカができるまでの手順を以下に示します。

yottaのインストール

先ずは、mbed OSのビルドツールyottaをインストールします。私はOS Xを使っていますが、このページの手順に従ってインストール。

執筆時点のyottaバージョンは、0.9.4でした。

ビルド手順

①プロジェクト用のサブディレクトリを作成しyotta initを実行

$ mkdir led
$ cd led

$ yotta init
Enter the module name:  
Enter the initial version: <0.0.0> 
Is this an executable (instead of a re-usable library module)?  yes
Short description: LED blink using D12 pin
Author: todotani
What is the license for this project (Apache-2.0, ISC, MIT etc.)?   

Is this an executableと聞かれるので、yesを入力します。参考にしたサイトの情報では、生成されたmodule.jsonファイルのマニュアル修正が必要とありましたが、執筆時点の環境では不要になっていました。yotta initの挙動も微妙に違っているようで、最終的なリリース版ではまた変更があるかもしれません。

②ターゲットの設定と、ドライバーのインストール

$ yotta target frdm-k64f-gcc
$ yotta install mbed-drivers

③Lチカコード

sourceディレクトリにソースファイルを格納します。ファイル名はなんでもよいのですが、今回の例では、led.cppとしました。mbed OSのチュートリアルには、以下に示すような、mbed OSのスケジューラーminarを使ってLチカルーチンを周期起動するコードが載っています。

#include "mbed-drivers/mbed.h"

static void blinky(void) {
    static DigitalOut led(LED1);
    led = !led;
    printf("LED = %d \r\n",led.read());
}

void app_start(int, char**) {
    minar::Scheduler::postCallback(blinky).period(minar::milliseconds(500));
}

このコードがmbed OS流の書き方なのだと思いますが、以下のような、従来のmbed風の書き方でも動作しました。mbed OSではプログラムのエントリポイントはmain()でなく、app_start()になるようです。

#include "mbed-drivers/mbed.h"

DigitalOut myLed(D12);

void app_start(int, char**) {
    while(1) {
        myLed = 1;
        wait(0.5);
        myLed = 0;
        wait(0.5);
    }
}

DigitalOutクラスを使った出力ピンの指定やピン番号(D12など)の指定は、従来のmbedと共通です。まだ使ったことはないですが、ヘッダーファイルの定義を見ると、I2Cクラスなども従来のmbedと同様の使い方ができそうです。基本的な入出力は従来のmbedと同様に出来そうです。

④ビルドとターゲットボードへの書き込み

$ yotta build
$ cp ./build/frdm-k64f-gcc/source/led.bin /Volumes/MBED/

ビルドが完了すると、build/<target名>/soruce/ディレクトリ配下にbinファイルが生成されるので、mbedにコピーします。リセットボタンを押すとプログラムが動き出します。

参考情報

各種mbedのベンチマークテスト

mbedも2.0でオープン化され、対応ハードがずいぶん増えました。Myコレクションも少々増えたので、各mbedのベンチマークテストを行いました。

ベンチマークに使ったソフトは、CoreMarkです。CoreMarkのサイトからソースをダウンロードできるのですが、mbedにポーティングされたものが、ClockControlとして公開されています。このポーティングからLPC1768 CPUクロック変更用のコードを取り去ってCoreMarkだけにして使っています。

LPC1768の場合、インポートしたままのmbedライブラリ版数(かなり古いです)で動くのですが、LPC1768以外のmbedはライブラリを最新化する必要があります。しかし、ライブラリを最新化するとコンパイル時に謎のエラーが大量に発生し、当初は、LPC1768以外でCoreMarkを動かすのをあきらめていました。

あるときCoreMark/core_porteme.cにmbed.hがincludeされていることに気がつき、mbed.hで定義されている各種クラスとCoreMarkのCコードがコンフリクトしている可能性に思い至りました。このパートで、mbed.h全体をincludeする必要はなく、時間を測定するためにclock()関数を使えるよう、time.hをincludeするだけでOKなはずです。この部分のコードを変えるとビンゴで、最新のmbedライブラリでもエラーが出なくなりました。

また、main.cppでは、以下のように、#include “CoreMark/CoreProteme.h”をextern “C”でくくっています(これもコンパイルエラー対策)。

#include "mbed.h"
extern "C" {
#include "CoreMark/core_portme.h"
}

int main() 
{
    printf("Run CoreMark\n");
    printf("CPU clock: %d MHz\n", SystemCoreClock/1000000);
    mainCoreMark();
    printf("CoreMark End \n\n");
}

修正1:SystemCoreClockを表示してCPU周波数を確認するコードを追加

時間測定は機種非依存ののclock()関数を使っているため、コードは全機種で共通です。CoreMarkのLicence条件では、CoreMarkサイト以外でcodeを配布してはいけないようなことが書いてあるので、私の全codeをpublishするのは控えます…

 

実行結果

実行結果(CoreMark値)は以下の通りで、上位から記載すると(mbedライブラリは執筆時点で最新の88版を使用。O3/Otimeの最適化。);

  機種名 コア CPUクロック(MHz) CoreMark値
1 FRDM-K64F Cortex-M4 120 286.204923
2 LPC4088 Cortex-M4 120 263.435195
3 NucreoF401RE Cortex-M4 84 220.167327
4 LPC1768 Cortex-M3 96 217.485863
5 LPCXpresso11U68 Cortex-M0+ 5048 74.316290
6 LPC1114FN28 Cortex-M0 5048 68.119891
7 HRM1017 Cortex-M0 16 30.175015

修正2:LPC1114FN28のCPUクロックを48MHzに修正
修正3:LPCXpresso11U68のCPUクロックを48MHzに修正

 

考察

  • 120MHz Cortex-M4同士の対決は、FRDM-K64Fの勝ち
  • 第二グループの100MHzクラスは、NucreoF401REが84MHzながら、96MHzのLPC1768を僅かに押さえて3位に。NucreoF401REはCortex-M4コアのため、M3コアのLPC1768を上回ったか
  • 第三グループの50MHzクラスは、Cortex-M0+を積むLPCXpresso11U68がLPC1114FN28を僅かに上回り5位。クロック周波数差見合いということか。

以前書いた、「LPCXpresso LPC1769でCoreMarkを動かす」では、mbed LPC1768のCoreMark値は、最適化をかけて188.394876でしたので、15%程度スコアが伸びています。これは、コンパイラのバージョンが上がり、最適化が進んだということでしょうか。<追記>@toyowataさんより、「mbedオンラインコンパイラのarmcc 4.1→5ではCortex-M系の最適化が向上しています。」とのコメントをいただきました。

mbedユーザーは常に最新版のコンパイラが使えるなんてステキです。

iPhoneからmbedをBluetooth LE (BTLE)で制御する

久しぶりの更新です。ずっと弾切れでしたが、最近Bluetooth LE (BTLE)を使ったiPhone (iOSデバイス)とマイコンボード間の通信に興味が湧き、mbedとの接続実験を行いました。

以前から、スマホとmbedやArduinoをつないで連携できるとよいなと思っていました。どうせならUSBなどの有線でなく、無線でつなぎたい、あとAndroidよりiOS派の自分としてはiOSとつなぎたい、というのが目標でしたが、皆様ご存知の通り以下の制約で挫折していました:

  • 以前のiOS (4.x以前)ではBluetoothのSPPが使えない
  • xBee WiFiを使って、TCP/IPのソケット通信でつなぐ方法はありそう。でも、APがないとダメ。

で、iOS5とiPhone 4SからBTLEがサポートされ、BTのプロファイル縛りから解放されていることをつい最近知りました。それで、色々と資料をあさり、iPhoneからmbedのLEDをon/offするサンプルを作ってみました。BTLEのGATT/ATTなど、プロトコルの仕組みはまだよく分かっていないのですが、動かしならが確かめて行きたいと思います。


素材

iOS側はiOS5からサポートされたCoreBluetooth Frameworkを使えばOKです。

マイコン側も色んな選択肢がありそうです:

  • ユカイ工学さんのkonasi :  BluetoothモジュールにGPIO/PWM/ADCなどを組み込んで、iOSから制御できる。Bluetoothのサービス・キャラクタリスティック操作など下回りを意識させないiOS用ライブラリも提供されており、すぐに使えそう。ただ少々お高いのと、執筆現在は品切れ中・・勉強がてら、CoreBluetoothやBTLEを意識する低レイヤーなところから始めても悪くない
  • ランニングエレクトロニクスさんのSBDBT : BTLEとシリアルインタフェースのブリッジとして動き、価格がお手頃。mbedやArduinoなどのマイコンボートと通信するためにはシリアルポート経由でコマンドを送受信するようなインタフェースを作る必要がありそう。モジュールが小型なので、ロボットの無線操縦なんかに向いているのかしら
  • RedBearLabさんのArduino BLE shield : Arduino用のライブラリやiOS用のAppもあり、お手軽に使えそう。お値段も手ごろですが、日本の技適がないため法的に使えません。
  • btstack : 組み込み向けのBluetooth stack。もともとはSPPをサポートしていたが、BTLEにも対応。HCI (Host-Controller Interface)から上の層を実装しており、下位層であるLL (Link Layer)/PHYは自分で持ってくる必要がある。下位層はBluetoothドングルを使うことを想定しているので、USB周りのドライバを用意する必要がある。

今回は、mbedを直接iOSから制御できるようにしようということで、btstackを使いました。問題はUSBのドライバをどうするかです。SBDBTもbtstackを使っていますが、PIC用なのでそのままでは使えません。一方、mbedではSPP版のbtstackをNorimasa Okamotoさんがmbedに移植されていました。

実装(移植)の方針として、OkamotoさんのSPP版btstackをベースに、上位プロトコル部分をBTLE (GATT/ATT)に差し替えました。mbedのリソース(現状はLED1だけですが・・)をBTLEのサービスやキャラクタリスティックとして見せる部分の処理は、ランエレさんのSBDBT用コード(ble-server.c)を参考にさせていただきました。


実装

mbed側のコードは、mbed.orgにポストしてあります。SBDBT同様に以下の動作を行います:

  • サービス(UUID: 0xFFF0)配下に3つのcharacteristicsがありcharacteristic 0xFFF2に00 or 01をライトすることでLED1のon/offを制御できます
  • LED3はbtstackが初期化されアドバタイズを始めた時点で点灯、マスターと接続が完了すると点滅します
  • LED2はアトリビュートのリードで点灯、ライトで消灯します。今回のiOS Appからの制御ではライトのみなので、消えっぱなしです

iOS側のコードは、以下の通りです(iOS 6.1 & Xcode 4.6を使用)。チュートリアルなどを参考にして書いてみました:

//
//  ViewController.h
//  mbed Control
//
//  Created by todotani on 2013/02/11.
//  Copyright (c) todotani. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <CoreBluetooth/CoreBluetooth.h>

@interface ViewController : UIViewController <CBCentralManagerDelegate, CBPeripheralDelegate>

@property (weak, nonatomic) IBOutlet UISwitch *led1Sw;
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
@property (weak, nonatomic) IBOutlet UISegmentedControl *connectButton;

@end

----

//
//  ViewController.m
//  mbed Control
//
//  Created by todotani on 2013/02/11.
//  Copyright (c) todotani. All rights reserved.
//

#import "ViewController.h"

#define CONECTED     0
#define DISCONNECTED 1

@interface ViewController ()

@property (strong) CBCentralManager *manager;
@property (strong) CBPeripheral *peripheral;
@property (strong) CBCharacteristic *led1Characteristic;

- (void) updateLed1;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}


#pragma mark CoreBuletooth delegate

- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
    NSLog(@"State Update:%d",[self.manager state]);
}

- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)aPeripheral
      advertisementData:(NSDictionary *)advertisementData
                   RSSI:(NSNumber *)RSSI
{
    self.statusLabel.text = [NSString stringWithFormat:@"Found(RSSI %@)", RSSI];
    [central stopScan];
    self.peripheral = aPeripheral;
    [central connectPeripheral:self.peripheral options:nil];
}

- (void) centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral
                  error:(NSError *)error
{
    NSLog(@"Failed:%@",error);
}

- (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral
{
    NSLog(@"Connected:%@",aPeripheral.UUID);
    self.statusLabel.text = @"Connected";
    [aPeripheral setDelegate:self];
    [aPeripheral discoverServices:@[[CBUUID UUIDWithString:@"FFF0"]]];
}

- (void) centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral
                  error:(NSError *)error
{
    NSLog(@"Disconnected");
    self.statusLabel.text = @"Disconnected";
    self.connectButton.selectedSegmentIndex = DISCONNECTED;
    [self.manager stopScan];
}


// mbed Sericeを検出
- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error
{
    for (CBService *aService in aPeripheral.services) {
        NSLog(@"Service UUID:%@", aService.UUID);
        if ([aService.UUID isEqual:[CBUUID UUIDWithString:@"FFF0"]]) {
            [aPeripheral discoverCharacteristics:@[[CBUUID UUIDWithString:@"FFF2"]] forService:aService];
        }
    }
}

// LED1制御用characteristicsを検出
- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service
              error:(NSError *)error
{
    for (CBCharacteristic *aChar in service.characteristics){
        NSLog(@"Characteristic UUID:%@",aChar.UUID);
        if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"FFF2"]]) {
            self.led1Characteristic = aChar;
            [self updateLed1];
        }
    }
}


#pragma mark ActionMethods

- (IBAction)changeConnection:(id)sender
{
    UISegmentedControl *mySegmented = sender;
    switch (mySegmented.selectedSegmentIndex) {
        case 0:
            [self.manager scanForPeripheralsWithServices: @[[CBUUID UUIDWithString:@"FFF0"]]
                                              options:@{ CBCentralManagerScanOptionAllowDuplicatesKey:@YES }];
            self.statusLabel.text = @"Scanning";
            break;
        case 1:
            if ([self.peripheral isConnected]) {
                [self.manager cancelPeripheralConnection:self.peripheral];
            } else {
                [self.manager stopScan];
                self.statusLabel.text = @"Disconnected";
            }
            break;
        default:
            break;
    }
}


- (IBAction)changeLed1:(id)sender
{
    if ([self.peripheral isConnected]) {
        [self updateLed1];
    }    
}


#pragma mark Private methods

- (void) updateLed1
{
    const unsigned char onData[]  = {0x01};
    const unsigned char offData[] = {0x00};
    
    if ([self.led1Sw isOn]) {
        [self.peripheral writeValue:[NSData dataWithBytes:onData length:1] forCharacteristic:self.led1Characteristic
                               type:CBCharacteristicWriteWithoutResponse];
    } else {
        [self.peripheral writeValue:[NSData dataWithBytes:offData length:1] forCharacteristic:self.led1Characteristic
                               type:CBCharacteristicWriteWithoutResponse];
    }
}

@end

動作の概要は以下です:

  • 105行目:Connectボタンが押されると、CBCentralManagerクラスのscanForPeripheralsWithServicesメソッドを呼び出してBTLEアドバタイズのスキャンを開始。この際、mbedの制御用に定義している、サービス UUID 0xFFF0を検索対象として指定(Arrayに複数の検索対象UUIDをセットできるが、このサンプルでは0xFFF0のみを指定)
  • 39行目:ペリフェラルが見つかると、centralManager:didDiscoverPeripheral: デリゲートがコールバックされます。ここでは何も考えずに、見つかったペリフェラルに接続要求を発行しています。スキャンを行う際に、mbedのサービスを指定していますので、mbed以外と繋がることはないですが、どのmbedかまでは区別していません。接続先をもっと厳密に制御したい場合は、認証などのセキュリティー機能を使うのでしょうか(このあたりはよく分かっていません)
  • 55行目:ペリフェラルとの接続が完了すると、centralManager:didConnectPeripheral:デリゲートがコールバックされます。ここで、見つかったペリフェラルのインスタンスに対して、discoverServicesメッセージを送って、サービスを検索します。引数として、UUID 0xFFF0を指定することで、mbed制御のサービスのみを検出するようにしています(引数をnilにすると、GATT基本サービスの1800や1801が見つかった際にもコールバックされるのでフィルターしておきます)
  • 74行目:サービスが見つかると、peripheral:didDiscoverServices:デリゲートがコールバックされます。今度は、ペリフェラルクラスに対して、discoverCharacteristicsメッセージを送ってキャラクタリスティックの検索を行います。サービス0xFFF0には3つのキャラクタリスティックが定義されていますが、LED1の制御に使うのは0xFFF2のみのため検索条件を指定します。
  • 85行目:キャラクタリスティックが見つかると、peripheral:didDiscoverCharacteristicsForService:デリゲートがコールバックされます。discoverCharacteristicsでフィルター条件をかけているつもりなのですが、3回コールバックされるため、処理対象とする0xFFF2を判定しています。これで、LED1の制御に必要なキャラクタリスティックがViewControllerクラスのプロパティとして設定されました
  • 133行目:スイッチの状態が変化すると、LED1制御用のキャラクタリスティックにon/offに該当する値をライトします

動作中の写真はこんな感じです:

IMG_0123

IMG_0984

 

今後の予定

今回は書き込みのみですので、mbed側のGPIO状態の読み取りを実装するつもり。


参考資料

Debug printf用の可変長引数マクロ

Cプログラマな方には既知だと思いますが、printfデバッグに便利なマクロ定義を発見したので備忘録も兼ねてポストします。


Printfデバッグとは

プログラムのデバッグのために、printf関数を使用して変数の内部状態をコンソールに表示したい場合があります。JTAGデバッグが使えないmbedやArduinoでは必須のテクニックです。今更言うまでもないですね。


デバック用マクロ

デバックが終了してプログラムをリリースする段階に進むと、デバック用のprintfは無効にしたいものです(printfは結構ROM/RAMを消費するなどMCU資源の無駄使いするため)。リリース時にいちちprintfをコメントアウトするのは面倒なので、Cのマクロを使って、デバック用のprintfを一括して無効化できると便利です。

Arduinoでは以下のマクロを定義していました。

#define DEBUG

#ifdef DEBUG
#define DEBUG_PRINTLN(x) Serial.println(x)
#else
#define DEBUG_PRINTLN(x)
#endif
  • プログラム中でDEBUG_PRINTLN(hoge)と書くと、4行目のマクロで Serial.println(hoge)に変換されます。
  • 1行目をコメントアウトすると、DEBUG_PRINTLN(hoge)は空行に変換され、コンソール出力が無効になります。

Arduinoのように、print関数の引数が固定個数の場合は、この方法でもよいのですが、mbedやLPCXpressoのようにフルセットのprintfが使える場合、引数の数が可変長になるためうまくいきません。

で、マクロで可変長の引数を扱うための機能がないかと調べると、すぐに出てくるんですね。便利な時代だ。


可変長引数マクロ

__VA_ARGS__というパラメータを使うと、可変長引数をマクロ内で展開してくれます。mbedでの使い方はこんな感じです。

#include "mbed.h"

#define DEBUG

// Debug Macro
#ifdef DEBUG
#define DBG(fmt, ...) printf(fmt, __VA_ARGS__)
#else
#define DBG(...)
#endif

int i = 0;
int j = 1000;

int main() {
    while(1) {
        i++;
        j--;
        printf("Loop: %d\n", i);
        DBG("Debug Print: i=%d j=%d\n", i, j);
        // DBG("Degub");       // Cause compiler error
        wait(1.0);
    }
    
    return 0;
}

7行目の、...が可変長引数を示し、 __VA_ARGS__で実際の引数に展開します。
そのため、20行目のマクロは;
  printf("Debug Print: i=%d j=%d\n", i, j);
に展開されます。

この方法では、引数を1つ以上指定しないと、マクロの展開後カンマが1つ残ってしまいコンパイルエラーになります。そのため、21行目のような使い方はできません。


LPCXpressoでは

LPCXpressoのIDEでは、Debugビルドを指定した場合、コンパイラの-DオプションにDEBUGが追加されるため、プログラム中で#define DEBUGを定義する必要はありません。

Releaseビルドに切り替えた場合、-DオプションからDEBUGが自動的に外れます。


おわりに

Cのマクロはコードの可読性が下がるので(特にネストしたマクロの場合)嫌いだったのですが、少し見直しました。ただ、引数をつけなかった場合のエラーの発生原因が直感的に分からないのはマクロの欠点ではあります。


参考資料:

XBeeをAPIモードで使う

前回の続きとして、APIモードへの設定変更と、APIモードを使用したデバイス状態の読み取り・センサー出力(ADCポート)の読み取りを行います。マイコンにはmbedを使っています。


APIモードの種別

前回XBeeモジュールを透過モードに設定しているため、APIモードに設定変更を行います。APIモードには2つのバリエーションがあるため、先ずはこの点を説明します。APIモードのフレーム形式は以下となります(XBeeマニュアルの抜粋)

ApiFrame

このままでは、フレームデーター中に0x7Eが出現するとフレームの先頭を誤認識してしまいます。そのため、フレームデーター中に0x7Eが出現した際に、0x7Eを「0x7Dと0x7E xor 0x20」の2バイトに置き換えるエスケープモードが存在します。0x7D(エスケープコード)自体がフレームデーターに出現した場合は、0x7Dと0x7D xor 0x20に置き換えます。

エスケープモードにはATコマンドATAP2を発行することで遷移します。そのため、エスケープモードは、AP=2 or ATAP=2と記載されることがあります。


APIモードへの設定変更

マイコンからAPIモードの制御を行う際、Arduino用ドライバ(xbee-arduinoプロジェクト)を使用しています。まあ当たり前ですが、xbee-arduinoではエスケープモード(AP=2)での使用が前提となっています。そのため、エスケープモードへの設定変更の手順を示します。以下は透過モードCoordinatorをAP2にする手順です。

①現状の設定情報を読み出します

01_CurrentConfig

 

②Function SetをAPIに変更し、Show Defaultsボタン→ Writeボタンを押す

02_WriteAPImodeDefault

 

③このフェーズでは書き込みが始まる前にエラーで停止することが多々ありますが、デバイスをリセットしたりしてしつこくリトライします。書き込みまで進んだ場合でも、自分の環境では必ず以下のエラーダイアログが出ます(サポート外のWindows 7だからなのかも)。

03_ErrorIndication

 

④OKボタンを押した後で下側のテキスト表示を見ると、パラメーター書き込みは成功しているような感じ

04_ErrorButFwWiteComplete

 

⑤Enable APIをチェックしてTest/Queryを行うと、APIモード対応ファームの2170で認識しています

05_ApiModeEnabled

 

⑥設定パラメーター(デフォルト値)の読み出しもできたのでAPIモードで立ち上がっています。エスケープモードにするために、APパラメーターに2を設定。DH/DLはデフォルトのブロードキャストのままにしておきます(マイコンから制御する際にユニキャストアドレスを指定するので、ここではブロードキャストでも問題なし)

06_SetAPI2

 

⑦X-CTUのUse escape charactersをチェックし、X-CTU側もエスケープモード動作にします

07_X-CTU_AP2mode


End Device側も同様の手順で設定します。End DeviceのDH/DLもデフォルトのDH=0/DL=0(Coordinator宛てを意味する)のままとしておきます。End Deviceでは、サンプルプログラムで使うADC3を有効にしておきます。

08_Adc3Cofnig

 

APIモードを使ったサンプルプログラム

サンプルプログラムを動かすマイコンとしてmbedを使いました。APIモードを動かすためのドライバですが、h.sugaさんがxbee-arduinoプロジェクトをmbedに移植して下さっているので、ありがたく利用させていただきました。

サンプルプルグラムは、mbedが繋がったXBeeにATDBコマンドを投げて電波強度(RSSI)の取得と、リモート側のXBeeにATISコマンドを投げてADCポートのサンプルデーターを取得しています。ADCの分解能は10bitで、温度センサーをつないだポートの生データーを表示しています。

何はともあれ、コードを以下に示します。全体は、mbed.orgにパブリッシュしました。xbee-arduinoはオブジェクト構造が結構複雑ですが、Arduino用のライブラリ添付のサンプルコードを見ながら使い方を覚えるのが手っ取り早そうです。

#include "mbed.h"
#include "XBee.h"
#include "TextLCD.h"

TextLCD lcd(p25, p26, p24, p23, p22, p21); // RS, E, DB4, DB5, DB6, DB7

/*-- AT command and parameters --*/
uint8_t atISCmd[] = {'I', 'S'};      // Forces a read of all enabled digital and analog input lines
uint8_t atDBCmd[] = {'D', 'B'};      // Received Signal Strength
uint8_t cmdVal0[] = {0};             // Clear RSSI regisger

/*-- Create instanse of Xbee object --*/
XBee xbee(p13, p14);
XBeeAddress64 remoteAddress(0x0013A200, 0x406B7111);    // Specify your XBee address

/*-- Create instanse of Command and Response object --*/
// Remot ATIS command to read ADC value (ADC3 is enabled by X-CTU tool)
RemoteAtCommandRequest remoteSampleRequest(remoteAddress, atISCmd);
// Local ATDB command to read signal strength (RSSI)
AtCommandRequest atDB(atDBCmd);
// Local ATDB0 command to clear RSSI
AtCommandRequest atDB0(atDBCmd, cmdVal0, sizeof(cmdVal0));
// Create instanse to handle command response
AtCommandResponse response = AtCommandResponse();
RemoteAtCommandResponse remoteResp = RemoteAtCommandResponse();


/* Receive command response packet
 * If OK response recieved, return pointer to the Response Data Frame
 */
uint8_t* GetResponse() {
    // Read response
    if (xbee.readPacket(5000)) {
        // Got a response! Check if response is AT command respose
        if (xbee.getResponse().getApiId() == AT_COMMAND_RESPONSE) {
            xbee.getResponse().getAtCommandResponse(response);
            if ( response.getStatus() == AT_OK ) 
                return response.getValue();
        } else  if (xbee.getResponse().getApiId() == REMOTE_AT_COMMAND_RESPONSE) {
            xbee.getResponse().getRemoteAtCommandResponse(remoteResp);
            if ( remoteResp.getStatus() == AT_OK ) {
               // Debug print
                printf("Response Data:");
                   for (int i = 0; i < remoteResp.getValueLength(); i++)
                   printf("%02X ", remoteResp.getValue()[i]);
                printf("\n");
                
                return remoteResp.getValue();
            } else {
                printf("Remote Command Error:0x%X\n", response.getStatus());
            }
        }
    }

    return 0;
}


/* Get ADC data
 * Data frame structure of ATIS
 * Offset
 *   0   : Number of Samples (Always 1)
 *   1-2 : Digital Channel Mask
 *   3   : Analog Channel Mask
 *   4-5 : Digital Samples (Omit if no DIO enabled)
 *   6-7 : First ADC Data
 */
uint16_t getAnalog(uint8_t *FrameData, int ADC) {
    // ADC data feild starts 4 bytes offest, if no DIO enabled
    uint8_t start = 4;

    // Contains Digital channel?
    if (FrameData[1] > 0 || FrameData[2] > 0) {
        // make room for digital i/o
        start+=2;
    }

    // start depends on how many ADCs before this ADC are enabled
    for (int i = 0; i < ADC; i++) {
        // Is Analog channel Enabled ?
        if ( (FrameData[3] >> i) & 1 ) {
            start+=2;
        }
    }

    return (uint16_t)((FrameData[start] << 8) + FrameData[start + 1]);
}


int main() {
    unsigned int loop = 0;

    xbee.begin(9600);
    lcd.printf("RSSI:");
    lcd.locate(0, 1);
    lcd.printf("ADC :");
    printf("\nStart.\n");

    while (true) {
        uint8_t *responseVal;
        uint8_t rssiVal = 0;
        uint16_t adcVal = 0;

        // Send ATDB command (Read RSSI register from local Xbee)
        xbee.send(atDB);
        responseVal = GetResponse();
        if ( responseVal != 0 )
            rssiVal = responseVal[0];
        lcd.locate(5, 0);
        if (rssiVal == 0)
            lcd.printf("No Signal");
        else
            lcd.printf("-%ddBm   ", rssiVal);

        // Clear RSSI register, because Xbee hold RSSI value of last received packet even after radio disconneded
        xbee.send(atDB0);
        GetResponse();

        // Read ADC3 value by sending ATIS command
        xbee.send(remoteSampleRequest);
        responseVal = GetResponse();
        if ( responseVal != 0 ) {
            adcVal = getAnalog(responseVal, 3); // Assume ADC3 is enabled
        }
        lcd.locate(5, 1);
        if (adcVal == 0)
            lcd.printf("-     ");
        else
            lcd.printf("%d", adcVal);
    
        printf("Loop:%d\n", loop++);
        wait(1.0);
    }
}

コードの概要は以下です:

  • xbeeオブジェクトを起こす(13行目)
  • xbeeに投げるコマンドやレスポンスのオブジェクトを起こす(16行目から)
  • xbee.send(コマンドオブジェクト)でコマンドを送信
  • xbee.readPacket()でレスポンスを受信(31行目~)
  • xbee.getResponse()でレスポンスデーターを取得、AtCommandResponseオブジェクトに渡してレスポンス情報を取得する、などなど

サンプルプログラムの動作構成ですが、以下のように、XBee 3個構成で行いました。(実験中に壊したXBeeの代替とあわせて追加1個を昨日秋月さんで調達したのです)

SampleProgramRun

真ん中のXBeeがCoordinator(兼Router)で、左右はEnd Deviceです。
mbedがRemoteAtCommandRequestを投げると、真ん中のRouterで左側に中継され、ADCの読み取り値がRouterノード経由で戻ってきます。

End Device間も電波が届く範囲にありますが、直接通信はしてない筈です。ZigBee規格ではEnd Deviceは必ず上位のRouter経由で通信するため、真ん中のrouterをバイパスして左右のEnd Device間が直接通信するパスはないと思っています。このあたりのルート選択の概念やAODV (Ad-hoc On-demand Distance Vector) Routingについては別途お勉強して実験してみたいと思います。リンクコスト(品質)に基づくDistance Vector型の最適ルート選択とか、IPのルーティングと通じるところがあり、通信屋の自分には面白いエリアです。


参考情報

mbedでリニア温度センサーを使う

MTM06で@shintamainjpさんよりStarBord Orange購入記念のMPC9700(アナログ温度センサー)をいただきました。mbedのADCを使ってセンサーの読み取りと温度の表示を行うサンプルプログラムを作って、本家のmbed.orgのNotebookにポストしたのですが、自分のブログにも概要を記載します。

   
温度センサーの読み取り 

MCP9700等のアナログ温度センサーは、温度に比例して出力電圧が変化します(温度係数と呼びます)。そのため、出力電圧を測定することによって、温度を得ることができます。MPC9700の場合、1℃毎に10mV出力が変化します。また、0℃で500mVの出力があるため、出力電圧は以下の式で表せます: 

Vout(出力電圧) = Tc(温度係数) x Ta(温度)  + V0(0℃の出力) 

そのため、温度は以下の式で計算が可能です: 

Ta = (Vout - V0) / Tc = (Vout - 500) / 10 

上記の式を使用して数秒単位で温度を測定すると、測定値に結構なばらつきが出てしまいました(1℃の単位で表示がふらついて、うっとうしい状態になります)。そのため、10回サンプル(測定)を行った平均値を示すようにしてあります。この対策を行うと、温度表示のばたつきはかなり軽減されるのですが、小数点まで表示すると0.1℃の単位は若干ばたつきます。 

センサーの出力電圧をテスターで直接計ると、ばたつきは1mVの単位しかなくADCの読み取り値に比べて安定しています。そのため、測定値のばらつきはセンサー入力の処理にも問題がありそうです。センサーの出力が20℃で700mVと低めのため、OPアンプを入れて出力を2倍程度に増幅してやればADCの分解能を生かせるようになり、ばたつきが軽減できるかも。あとノイズ対策も必要か。 

   
サンプルプログラム 

センサーの読み取りは、あまり意味がないですが、mbedのオブジェクト風に書いてみました。コード全体は、mbed.orgを参照して下さい。センサーオブジェクトを使ったサンプルを以下に示します。

  
#include "mbed.h"
#include "TextLCD.h"
#include "LinearTempSensor.h"

TextLCD lcd(p24, p26, p27, p28, p29, p30, TextLCD::LCD16x2);  // RS, E, DB4, DB5, DB6, DB7
LinearTempSensor sensor(p20);                                 // With default parameters
//LinearTempSensor sensor(p20, 5, LinearTempSensor::MCP9700); // With option parameters

int main() 
{
    float Vout, Tav, To;

    lcd.cls();
    lcd.printf("TEMP:");
    
    while(true)
    {
        Vout = sensor.Sense();          // Sample data (read sensor)
        Tav  = sensor.GetAverageTemp(); // Calculate average temperature from N samples
        To   = sensor.GetLatestTemp();  // Calculate temperature from the latest sample

        lcd.locate(5, 0);
        lcd.printf("%4.1f", Tav);
        printf("Vout:%f  Tav:%f  To:%f\n\r", Vout, Tav, To);    // Debug print

        wait(2.0);
    }
}

6行目で、AnalogInとしてp20を使用する、LinearTempSensorオブジェクトのインスタンスを起こしています。  
18行目でセンサー出力をサンプリングし、19行目でN回サンプル(defaultは10回)から得られる平均温度を取得しています。

StarBoard Orangeを使ってセンサーを動作させた写真を以下に示します。

SensorWithMbed

mbedでDMAの実験

8-bitマイコンのArduinoでは味わえない32-bitマイコンの醍醐味(?)としてDMAがあります。NXP提供のCMSIS DMAライブラリのデモコードをmbedに移植して実験をしてみました。


実験の内容

  1. SRAM→SRAM,  Flash ROM→SRAM間で32 wordのDMAを起動
  2. DMA起動後、メインルーチンはループカウンタのインクリメントを行うだけの無限ループに入る。
  3. DMA完了割り込みルーチン内で再度DMAを起動することで、DMAによるメモリコピーとループが平行して動く(筈)
  4. DMAが1000回動いた時点でプログラムを止めて、起動からの経過時間とループカウンタ値を表示
  5. DMAのソースがSRAMとFlash ROMの場合の差分をチェック

ポイントは5項でありまして、図に示す通り、①のSRAM→SRAM間DMAでは、DMAと命令フェッチでメモリアクセスが競合しないと考えられます。一方、②のROM→SRAMのDMAではROMアクセスが競合するためループ処理が①に比べてまわらないというのが期待値です。

LPC1764_DMA

実験コード

DMA完了の割り込み処理ルーチンとメインルーチンを以下に示します。コード全体は、mbed.orgにポストしてあります。

static uint32_t DMASrc_Buffer[DMA_SIZE]=
{
    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40
};

uint32_t DMADest_Buffer[DMA_SIZE];


/*-------------------------MAIN FUNCTION------------------------------*/
extern "C" void DMA_IRQHandler (void);      // extern "C" required for mbed


/*----------------- INTERRUPT SERVICE ROUTINES --------------------------*/
/*********************************************************************//**
 * @brief        GPDMA interrupt handler sub-routine
 * @param[in]    None
 * @return       None
 **********************************************************************/
void DMA_IRQHandler (void)
{
    // check GPDMA interrupt on channel 0
    if (GPDMA_IntGetStatus(GPDMA_STAT_INT, 0)) { //check interrupt status on channel 0
        // Check counter terminal status
        if(GPDMA_IntGetStatus(GPDMA_STAT_INTTC, 0)) {
            // Clear terminate counter Interrupt pending
            GPDMA_ClearIntPending (GPDMA_STATCLR_INTTC, 0);

            if (++TC_count < DMA_CYCLES) {
                // Setup channel with given parameter
                GPDMA_Setup(&GPDMACfg);
                // Run DMA again
                GPDMA_ChannelCmd(0, ENABLE);
                return;
            } else {
                /* DMA run predetermined cycles */
                int elapsedTime =  GetTickCount();
                Buffer_Verify();
                pc.printf("%s", compl_menu);
                pc.printf("DMA %d cycles, %d loop executions\n\r", TC_count, loop);
                pc.printf("Elapsed time %d ms\n\r\n\r", elapsedTime);

                while(1);   // Halt program
            }
        }
        
        if (GPDMA_IntGetStatus(GPDMA_STAT_INTERR, 0)){
            // Clear error counter Interrupt pending
            GPDMA_ClearIntPending (GPDMA_STATCLR_INTERR, 0);
            pc.printf("DMA Error detected.\n\r");
            while(1);       // Halt program
        }
    }
}


/*-------------------------MAIN FUNCTION--------------------------------*/
/*********************************************************************//**
 * @brief        c_entry: Main program body
 * @param[in]    None
 * @return       int
 **********************************************************************/
int c_entry(void)
{
    pc.baud(9600);

    // print welcome screen
    pc.printf("%s", menu);

    /* Disable GPDMA interrupt */
    NVIC_DisableIRQ(DMA_IRQn);
    /* preemption = 1, sub-priority = 1 */
    NVIC_SetPriority(DMA_IRQn, ((0x01<<3)|0x01));

    /* Initialize GPDMA controller */
    GPDMA_Init();

    // Setup GPDMA channel --------------------------------
    // channel 0 (highest priority
    GPDMACfg.ChannelNum = 0;
    // Source memory
    GPDMACfg.SrcMemAddr = (uint32_t)DMASrc_Buffer;
    // Destination memory
    GPDMACfg.DstMemAddr = (uint32_t)DMADest_Buffer;
    // Transfer size
    GPDMACfg.TransferSize = DMA_SIZE;
    // Transfer width
    GPDMACfg.TransferWidth = GPDMA_WIDTH_WORD;
    // Transfer type
    GPDMACfg.TransferType = GPDMA_TRANSFERTYPE_M2M;
    // Source connection - unused
    GPDMACfg.SrcConn = 0;
    // Destination connection - unused
    GPDMACfg.DstConn = 0;
    // Linker List Item - unused
    GPDMACfg.DMALLI = 0;
    // Setup channel with given parameter
    GPDMA_Setup(&GPDMACfg);

    /* Enable GPDMA interrupt */
    NVIC_EnableIRQ(DMA_IRQn);
    
    pc.printf("Start transfer...\n\r");
    GetTickCount_Start();   
    
    // Enable GPDMA channel 0
    GPDMA_ChannelCmd(0, ENABLE);

    /* Wait for GPDMA processing complete */
    while (1) {
        loop++;
    }

    return 1;
}

DMAを何回も起動しながら、平行して117行目のloopが何回まわるかを確認します。CMSISライブラリのexampleからの変更点は以下です:

  • 割り込みハンドラー(17行目)のプロトタイプ宣言にextern "C"をつける。これがないとハングしてしまう
  • コンソールへの文字出力はmbedライブラリのprintf関数に変更
  • 実行時間の計測は1ms周期のSysTick割り込みを使用


実行結果と考察

1行目のDMASrc_Bufferをstatic宣言するとSRAM上に領域が確保され①のSRAM→SRAM DMAになります。1行目をconst宣言するとFlash ROM上に領域が確保され②になります。

①:SRAM→SRAM
DMA 1000 cycles, 22962 loop executions
Elapsed time 5 ms

②Flash ROM→SRAM
DMA 1000 cycles, 7985 loop executions
Elapsed time 5 ms

上記より、期待通り①の方がループの実行回数が多いことが分かります。実行時間は①②とも5msで共通のため、DMA 1回(32 wordの転送)に要する時間は同じで、DMA期間中にDMACとMCUでバスの使用権を融通し合ういわゆるサイクルスチールモードになっていると思われます。

①ではDMACがSRAMアクセス中でもMCUは独立してループ処理の命令フェッチを進めることができ、DMACがバスマスタ権を譲ってくれた隙にloopカウンターの更新値をSRAMに書き戻せるのに対して、②はDMACがROMアクセス中は命令フェッチが止まるためループ回数が低下すると思われます。


おまけ

試しに以下のコードで、DMAと同様のコピーをmemcpyライブラリを使って行いました。

#include "mbed.h"
#include "GetTickCount/GetTickCount.h"

#define LOOP       1000
#define BUF_SIZE   32

Serial pc(USBTX, USBRX); // tx, rx

/* ---- Source data to be copied ---- */
// Allocated to Flash ROM
const  uint8_t __align(8) array1[BUF_SIZE] = {
    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40
};

// Allocated to SRAM
static uint8_t __align(8) array2[BUF_SIZE] = {
    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40
};

/*
 * My Memcpy function
 * If source and destination data is aligned at DWROD, performe copy by 8 byts
 */
void Memcpy(void *dst, const void *src, size_t len) 
{
    if ( (uint32_t)dst % sizeof(uint64_t) == 0 &&
         (uint32_t)src % sizeof(uint64_t) == 0 &&
                   len % sizeof(uint64_t) == 0 ) 
    {
        uint32_t *d   = (uint32_t*)dst;
        uint32_t *s   = (uint32_t*)src;
        uint32_t *end = (uint32_t*)(s + len/sizeof(uint64_t));

        while (s != end) {
            *(d++) = *(s++);    // Copy first 4 bytes
            *(d++) = *(s++);    // Copy second 4 bytes
        }

    } else {
        uint8_t *d   = (uint8_t*)dst;
        uint8_t *s   = (uint8_t*)src;
        uint8_t *end = (uint8_t*)(s + len);
        while (s != end) {
            *(d++) = *(s++);    // Copy each byte
        }
    }
}

int main() 
{
    unsigned int startTime, endTime;
    int i,len;
    char __align(8) array3[BUF_SIZE * sizeof(uint32_t)];

    pc.baud(9600);      // set baud rate
    len = BUF_SIZE * sizeof(uint32_t);
    pc.printf("Data Length: %d\n", len);

    GetTickCount_Start();

    /* ---- Flash ROM to SRAM copy ----- */
    pc.printf("Memory copy Flash->SRAM using memcpy lib %d bytes.\n", LOOP*len);
    startTime =  GetTickCount();
    for (i = 0; i < LOOP; i++) {
        memcpy(array3, array1, len);
    }
    endTime = GetTickCount();
    pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);

    pc.printf("Memory copy Flash->SRAM using My Memcpy %d bytes.\n", LOOP*len);
    startTime = GetTickCount();
    for (i = 0; i < LOOP; i++) {
        Memcpy(array3, array1, len);
    }
    endTime = GetTickCount();
    pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);


    /* ---- SRAM to SRAM copy ----- */
    pc.printf("Memory copy SRAM->SRAM using memcpy lib %d bytes.\n", LOOP*len);
    startTime =  GetTickCount();
    for (i = 0; i < LOOP; i++) {
        memcpy(array3, array2, len);
    }
    endTime = GetTickCount();
    pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);

    pc.printf("Memory copy SRAM->SRAM using My Memcpy %d bytes.\n", LOOP*len);
    startTime = GetTickCount();
    for (i = 0; i < LOOP; i++) {
        Memcpy(array3, array2, len);
    }
    endTime = GetTickCount();
    pc.printf("Elapsed time: %d ms.\n\n", endTime - startTime);
}

なんと、SRAM→SRAM、Flash ROM→SRAM共に、1msで終了。loop処理を行っていないという差分はありますが、ひょっとして単なるmemoryコピーならDMAを使うよりMCUでやった方が早いのか?

DMACの設定にSoftware Burst Requestというヤツがあり、こいつを使うとDMACがバスを占有するバーストモードとなり、DMA時間を短縮できるのではと思ったのですが、動かし方が分からず断念でした。


参考情報

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用のコードで検証することで開発環境要因がないことを切り分けることができ、大変重宝しました
2017年2月
      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        
無料ブログはココログ