So-net無料ブログ作成

USBUART の RX にも FIFO 機能を装備する [PSoC]このエントリーを含むはてなブックマーク#

回路図

PSoC Advent Calendar 2016の19日目の記事です。

前回の記事では、 USBUART の TX 側に FIFO を実装しました。 今回は、 RX 側にも FIFO を実装します。

RX 側 FIFO の考え方

RX 側の FIFO も TX 側と同じ考え方で実装します。

  1. 周期的にエンドポイントを監視して、データが届いていたらバッファが空である事を確認してバッファにデータを取り込む。
  2. 一文字取り出し関数でバッファから文字を取り出す。

実は、この動作そのものは FIFO を使わない場合でも同じです。 これは、エンドポイントに貯まったデータを一文字ずつ取り出す方法がなく、すべて取り出さなくてはならないためです。

エンドポイントの監視周期は、 TX 側と同じ 2kHz の割り込みを使います。

ファームウェア

ファームウェア

回路図およびコンポーネントの設定は、前回と同様です。 ファームウェアは、以下のようになりました。

#include "project.h"

// FIFO 機能のON/OFF
//#define NOFIFO

// USBUARTのパケットサイズ
#define     UART_TX_QUEUE_SIZE      (64)
#define     UART_RX_QUEUE_SIZE      (64)

冒頭の部分には、受信で使うパケットのサイズ定義が追加されています。 送信の場合と同じように、 USBUART が使用する BULK パケットのサイズをそのまま FIFO バッファのサイズとしています。

// USBUARTのTXキューバッファ
uint8       uartTxQueue[UART_TX_QUEUE_SIZE];    // TXキュー
uint8       uartTxCount = 0;                    // TXキューに存在するデータ数
CYBIT       uartZlpRequired = 0;                // 要ZLPフラグ
uint8       uartTxReject = 0;                   // 送信不可回数

// USBUARTのRXキューバッファ
uint8       uartRxQueue[UART_RX_QUEUE_SIZE];    // RXキュー
uint8       uartRxCount = 0;                    // RXキューに存在するデータ数
uint8       uartRxIndex = 0;                    // RXキューからの取り出し位置
CYBIT       uartRxCRDetect = 0;                 // CR検出フラグ

送信の時に定義したキューバッファと同様の定義が続きます。 受信の場合だけに宣言されている uartRxCRDetect は、行末記号として CR を受信した場合にセットされます。 行末記号として CR + LF を受信した場合、このフラグがセットされた状態で LF を受信する事になります。 このようなときには、 CR が送られてきた時に行末符号を返して、次の LF を無視しています。

この後、以前の記事と同じく送信に関する記述が続きます。 今回は、省略します。

#ifdef NOFIFO

// 1バイト受信する関数
int16 getch_sub(void) {
    int16 ch = -1;
    uint8 state = CyEnterCriticalSection();

    if (uartRxIndex >= uartRxCount) {
        // 受信キューが空かつ
        if (USBUART_DataIsReady()) {
            // データが到着していたら
            uartRxCount = USBUART_GetAll(uartRxQueue);  // バッファに取り込む
            uartRxIndex = 0;
        }
    }
    if (uartRxIndex < uartRxCount) {
        // 受信キューに文字が残っていたら
        ch = uartRxQueue[uartRxIndex++];    // 受信キューから一文字取り出す
    }
    CyExitCriticalSection(state);
    return ch;
}

FIFO を使わない場合、この関数が1バイトの受信に使用されます。 前半では、受信キューに確実にデータを準備しています。 具体的には、受信バッファが空の場合にはエンドポイントからデータをバッファに取り出しています。

後半では、受信キューから1バイトのデータを取り出して、関数の返り値としています。

この関数を使用した場合、この関数の中で次のパケットを受け取るため、文字が受信されるまで処理が止まってしまう可能性が有ります。

#else // define(NOFIFO)

// 受信側割り込みサービス制御
void uartRxIsr(void) {
    uint8 state = CyEnterCriticalSection();
    if (uartRxIndex >= uartRxCount) {
        // 入力バッファが空かつ
        if (USBUART_DataIsReady()) {
            // データが到着していたらバッファに取り込む
            uartRxCount = USBUART_GetAll(uartRxQueue);
            uartRxIndex = 0;
        }
    }
    CyExitCriticalSection(state);
}

これに対して、 FIFO を使う場合には、エンドポイントからデータを取り出す前半部分を周期割り込みで処理して、後半部分をメインループ内で処理しています。 この割り込みサービスルーチンも、 Critical Section をつくって、他の割り込みの介入を排除しています。

// 1バイト受信する関数
int16 getch_sub(void) {
    int16 ch = -1;
    uint8 state = CyEnterCriticalSection();
    
    if (uartRxIndex < uartRxCount) {
        // 受信キューに文字が残っていたら
        ch = uartRxQueue[uartRxIndex++];    // 受信キューから一文字取り出す
    }
    CyExitCriticalSection(state);
    return ch;
}

#endif // define(NOFIFO)

後半部分は、1バイト受信関数に記述されています。 受信キューからデータを取り出すだけの簡単な構成です。

// USBUARTから一文字受け取る
int16 getch(void) {
    int16 ch = getch_sub();
    if (uartRxCRDetect && ch == '\n') {
        uartRxCRDetect = 0;
        ch = getch_sub();
    } else if (ch == '\r') {
        ch = '\n';
        uartRxCRDetect = 1;
    }
    return ch;
}

実際に一文字を返す関数では、行末記号の処理を行っています。 この処理により、行末が CR、 LF、 CR+LF のいずれであっても、 '\n' を返す事ができます。

#ifndef NOFIFO
    
// 周期的にUSBUARTの送受信を監視する
CY_ISR(int_uartQueue_isr) {
    uartTxIsr();
    uartRxIsr();
}

#endif // !define(NOFIFO)

割り込み処理ルーチンには、送信に使われていた関数に加えて受信に使われる関数が追加されました。

int main(void) {
    uint32 nLine = 0;           // 行番号
    uint32 nChars = 0;          // 文字数
    
    CyGlobalIntEnable;                          // 割り込みの有効化    
    USBUART_Start(0, USBUART_5V_OPERATION);     // 動作電圧5VにてUSBFSコンポーネントを初期化

#ifndef NOFIFO
    
    int_uartQueue_StartEx(int_uartQueue_isr);   // 周期タイマを起動する

#endif // !define(NOFIFO)

    for(;;) {
        // 初期化終了まで待機
        while (USBUART_GetConfiguration() == 0);

        USBUART_IsConfigurationChanged();       // CHANGEフラグを確実にクリアする
        USBUART_CDC_Init();                     // CDC機能を起動する

        for (;;) {
            // 設定が変更されたら、再初期化をおこなう
            if (USBUART_IsConfigurationChanged()) {
                break;
            }

            // CDC-OUT : 行ごとに受信文字数を表示する
            {
                int16 ch = getch();
                if (ch >= 0) {
                    nChars++;
                    if (ch == '\n') {
                        putdec32(nLine, 7);
                        putstr(" - ");
                        putdec32(nChars, 7);
                        putstr("\n");
                        nLine++;
                        nChars = 0;
                    }
                }
            }
            
            // CDC-Control : 制御コマンドは無視する
            (void)USBUART_IsLineChanged();
        }
    }
}

メインループにこのアプリケーションの処理が記述されています。 このアプリケーションでは、受信したデータの行ごとに文字数を数えて、行番号と文字数を送信します。 文字数を見る事で受信データに抜けや重複が無いかを確認し、行番号を見る事で受信したデータ量を求めることができます。

実行してみた

FIFO有り出力

プロジェクトが出来たので実行してみました。 TeraTerm から一行59文字の巨大なテキストファイルを送り込んでみました。 行末が CR+LF になっているため、出力に表示される一行当たりの文字数は 58 バイトになっています。

10万行のデータを送って所要時間を測定したところ、98秒かかりました。 実行スループットは 59kiB/s と計算できます。

FIFO を使わなかったら

FIFO無し出力

前回と同様に FIFO を使わない設定も試してみましたが、やはりボロボロになってしまいました。 送信側がうまく働いていないのだから、あたりまえと言えばあたりまえですが。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。

関連商品

CY8CKIT-059 PSoC 5LP Prototyping Kit

CY8CKIT-059 PSoC 5LP Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス
SparkFun FreeSoC2 開発ボード - PSoC5LP

SparkFun FreeSoC2 開発ボード - PSoC5LP

  • 出版社/メーカー: Sparkfun
  • メディア: エレクトロニクス

USBUART の TX に FIFO 機能を装備する [PSoC]このエントリーを含むはてなブックマーク#

回路図

PSoC Advent Calendar 2016の12日目の記事です。

PSoC 5LP の USB インターフェイスを使うには、 USBUART コンポーネントを使った仮想 COM ポートを利用するのが簡単です。 ところが、 USBUART コンポーネントは、他の UART などのシリアルインターフェイスが持っているような FIFO を持っていません。 この記事では、 USBUART で FIFO を使う方法をご紹介します。 まずは、 TX 側から。

USBのややこしい所

USBUART には、1バイトのデータを送るための PutChar() メソッドが定義されています。 このメソッドを使うと、エンドポイントの送信が終わったのを確認してから、エンドポイントバッファに1バイトのデータを入れます。 そして、ホストから取り込み要求が来た時にバッファのデータを送信します。 エンドポイントバッファは64バイトの大きさがあるのですが、 PutChar() メソッドを呼び出した時には1バイトしか使われません。

データ送信のスループットを上げるためには、64バイトのバッファになるべく多くのデータを入れなくてはなりません。 そこで考えられるのが、64バイトのデータが貯まるまで送信を遅らせる方法ですが、この方法ではデータが先方に届くまでの遅延時間が大きくなってしまいます。 例えば、63バイトのデータを送りたい場合、永遠にデータが送りだされない可能性もあります。

このような事態を防ぐため、ここで作成する FIFO では、以下の方針を取り入れました。

  1. 送りたいデータは、バッファに積んでおく。
  2. 周期的にバッファを監視して、送信可能な状態であればバッファからデータを送信する。

ここで重要なのは、「バッファの監視周期」です。 USB Full-Speed では、 1ms のフレームと呼ばれる時間単位で区切られており、このフレームにパケットを詰めます。 ここでは、ひとつのフレームに必ずひとつ以上のパケットを入れられるように監視周期をフレームの半分の時間 0.5ms に設定しています。 具体的には、 2kHz のクロックにより割り込み "int_uartQueue" を発行しています。

クロックの設定

クロックの設定

クロックは、 USB バスからクロック成分を取り出して Internal Main Oscillator (IMO) の周波数を微調整する方式を採用しています。 これにより、 IMO の周波数は 24MHz±0.25% とすることができます。 CPU 等を駆動するためのクロック BUS_CLK は、システムでの最大周波数 79.5MHz を PLL により生成し使用しています。

ファームウェア

ファームウェア

ファームウェアは、以下のようになりました。

#include "project.h"

// FIFO 機能のON/OFF
//#define NOFIFO

冒頭、 include に続いてコメントアウトされた NOFIFO マクロ宣言があります。 このマクロを有効にすると、 FIFO を使わない設定も試す事ができます。

// USBUARTのパケットサイズ
#define     UART_TX_QUEUE_SIZE      (64)

// USBUARTのTXキューバッファ
uint8       uartTxQueue[UART_TX_QUEUE_SIZE];    // TXキュー
uint8       uartTxCount = 0;                    // TXキューに存在するデータ数
CYBIT       uartZlpRequired = 0;                // 要ZLPフラグ
uint8       uartTxReject = 0;                   // 送信不可回数

USBUART で使用する BULK パケットのサイズと FIFO で使用する変数を宣言しています。 USBUART コンポーネントには、ディスクリプタが含まれているのだから、パケットのサイズぐらい引っ張り出せそうなものなのですが、適当な方法が見つからなかったので、再定義しています。 このパケットサイズを条件としてキューバッファにデータを貯めておき、頃合いを見計らって USBUART コンポーネントから送り出します。

「要ZLPフラグ」は、 Zero Length Packet (ZLP: 長さゼロのパケット)が必要な状況であるかどうかを示します。 パケットの最大サイズは64バイトです。 この大きさを超えるデータを送りたい場合、複数の64バイトのパケットに続いて64バイト未満のパケット(Short Packet: ショートパケット)を送り出します。 このショートパケットでデータの終端を表すのです。

ところが、64バイトの倍数の長さのデータを送る場合には、すべてのパケットのサイズが64バイトなので、データの終端が見分けられません。 このような場合、データの終端を表すために使用されるのが ZLP です。 「要ZLPフラグ」は、データの最後のパケットが64バイトであった場合にセットされ、次回 ZLP を送信する必要があるかどうかを判断します。

バッファに貯まったデータを USB パケットとして送信する時、 USBUART コンポーネントの受け入れ準備が出来ていなければ、次の機会を待つことになります。 このような後回しの状態が続くとデータを送信したいアプリケーション側の処理が滞ってしまいます。 この例では、処理の停滞を防ぐために、受け入れを拒否された回数を uartTxReject で数えておき、繰り返し受け入れを拒否された場合にはバッファのデータを廃棄して処理を先に進ませるようにしています。

#ifdef NOFIFO
    
// 1バイトを送信する関数
static void putch_sub(const int16 ch) {
    // FIFOを使わない時は、PutChar()をそのまま使う
    USBUART_PutChar(ch);
}

FIFO を使わない場合、1バイトのデータを送るには USBUART_PutChar() 関数を使います。

#else // define(NOFIFO)

// 1バイトを送信する関数
static void putch_sub(const int16 ch) {
    uint8 state;
    for (;;) {
        // 送信キューが空くまで待つ
        state = CyEnterCriticalSection();
        if (uartTxCount < UART_TX_QUEUE_SIZE) break;
        CyExitCriticalSection(state);
    }
    // 送信キューに一文字入れる
    uartTxQueue[uartTxCount++] = ch;
    CyExitCriticalSection(state);
}

一方、 FIFO を使う場合には、送信キューに文字を積んでいきます。 もし、送信キューに空きが無かったら、空きが出来るまで待ちます。

送信キューに空きができるのは、周期割り込みによりバッファを送り出した時です。 つまり、この関数の実行中には割り込みがかかる事が期待されており、タイミングによっては、送信キューを構成する変数などが意図せず書き換えられる可能性があります。

このような事態を防ぐためには、 Critical Section (きわどい領域)という他のプログラムの介入を禁止する区間をつくって、変数などを保護してやります。 この Critical Section を確保するための関数が CyEnterCriticalSection() と CyExitCriticalSection() です。 これらの関数を使う事で、安全に割り込みを禁止する事ができます。

// 送信側割り込みサービス制御
void uartTxIsr(void) {
    uint8 state = CyEnterCriticalSection();
    if ((uartTxCount > 0) || uartZlpRequired) {
        // バッファにデータが存在する、または、ZLPが必要な時にパケットを送る
        if (USBUART_CDCIsReady()) {
            // 送信可能なら - パケットを送る
            USBUART_PutData(uartTxQueue, uartTxCount);
            // バッファをクリアする
            uartZlpRequired = (uartTxCount == UART_TX_QUEUE_SIZE);
            uartTxCount = 0;
            uartTxReject = 0;
        } else if (++uartTxReject > 4) {
            // 送信不可が続いたら - バッファのデータを棄てる
            uartTxCount = 0;
            uartTxReject = 0;
        } else {
            // 次回に期待
        }
    }
    CyExitCriticalSection(state);
}

#endif // define(NOFIFO)

周期割り込みの処理ルーチンでは、 USB のパケットを送信しています。 この処理も Critical Section に入れてあります。 これは、周期割り込みよりも優先順位の高い割り込みからデータ送信関数が呼ばれる場合を想定したものです。

// USBUARTに一文字送る
void putch(const int16 ch) {
    if (ch == '\n') {
        // LFをCRLFに変換する
        putch_sub('\r');
    }
    putch_sub(ch);
}

実際にアプリケーションから呼ばれる一文字送信関数は、 putch() です。 この関数の中では、 LF を CRLF に変換する処理が入っています。

// USBUARTに文字列を送り込む
void putstr(const char *s) {
    // 行末まで表示する
    while (*s) {
        putch(*s++);
    }
}

// 32-bit十進数表
static const uint32 CYCODE pow10_32[] = {
    0L,
    1L,
    10L,
    100L,
    1000L,
    10000L,
    100000L,
    1000000L,
    10000000L,
    100000000L,
    1000000000L,
};

// 32-bit数値の十進表示 - ZERO SUPPRESS は省略。
void putdec32(uint32 num, const uint8 nDigits) {
    uint8       i;
    uint8       k;
    CYBIT       show = 0;

    // 表示すべき桁数
    i = sizeof pow10_32 / sizeof pow10_32[0];
    while (--i > 0) {             // 一の位まで表示する
        // i桁目の数値を得る
        for (k = 0; num >= pow10_32[i]; k++) {
            num -= pow10_32[i];
        }
        // 表示すべきか判断する
        show = show || (i <= nDigits) || (k != 0);
        // 必要なら表示する
        if (show) {
            putch(k + '0');     // 着目桁の表示
        }
    }
}

出力処理を行うユーティリティ関数として、文字列を出力する putstr() と十進数を表示する putdec32() を用意しました。

#ifndef NOFIFO
    
// 周期的にUSBUARTの送受信を監視する
CY_ISR(int_uartQueue_isr) {
    uartTxIsr();
}

#endif // !define(NOFIFO)

FIFO を使う場合に使用される割り込み処理ルーチンが定義されます。 この中から送信キューの処理ルーチンを呼び出します。

int main(void) {
    uint32 nLine = 0;           // 行番号
    
    CyGlobalIntEnable;                          // 割り込みの有効化    
    USBUART_Start(0, USBUART_5V_OPERATION);     // 動作電圧5VにてUSBFSコンポーネントを初期化

#ifndef NOFIFO
    
    int_uartQueue_StartEx(int_uartQueue_isr);   // 周期タイマを起動する

#endif // !define(NOFIFO)

    for(;;) {
        // 初期化終了まで待機
        while (USBUART_GetConfiguration() == 0);

        USBUART_IsConfigurationChanged();       // CHANGEフラグを確実にクリアする
        USBUART_CDC_Init();                     // CDC機能を起動する

        for (;;) {
            // 設定が変更されたら、再初期化をおこなう
            if (USBUART_IsConfigurationChanged()) {
                break;
            }

            // CDC-IN : ホストにメッセージを送る
            putdec32(nLine++, 7);
            putstr(" - HELLO WORLD HELLO WORLD HELLO WORLD HELLO WORLD\n");
            
            // CDC-Control : 制御コマンドは無視する
            (void)USBUART_IsLineChanged();
        }
    }
}

メインループでは、これまでと同様に USB 特有の処理を行います。 アプリケーションとして行っているのは、全体で 59バイトの "HELLO WORLD" 文字列を永遠に出力し続ける処理です。 このとき、先頭に行番号を追加しているので、何番目の出力なのかがわかるようになっています。

実行してみたら

出力

プロジェクトが出来たら、実行してみます。 PSoC 5LP を USB ケーブルを介して PC に接続し、ターミナルソフトを接続すると、このスクリーンショットのように、文字列が延々と表示されます。

一行表示するごとに59バイトのデータが送信されていることになります。 10万行の送信を行った時に必要な時間を測定したところ、43秒かかりました。 ここから、実効スループットは 134kiB/s と計算されました。

周期割り込みの周期を短くすると、スループットは上がりました。 送信するデータの量にしたがって、割り込み周期を決めてやるとよいでしょう。

FIFO を使わなかったら

FIFO無し出力

マクロ NOFIFO を使って、 FIFO を使わない場合の動作も確認しました。 すると、表示が乱れて使い物になりません。 どこに問題が有るのか究明はしていませんが、スループットが高い場合には FIFO を入れないと話にならないという事がわかりました。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。

関連商品

CY8CKIT-059 PSoC 5LP Prototyping Kit

CY8CKIT-059 PSoC 5LP Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス
SparkFun FreeSoC2 開発ボード - PSoC5LP

SparkFun FreeSoC2 開発ボード - PSoC5LP

  • 出版社/メーカー: Sparkfun
  • メディア: エレクトロニクス

BLE Beacon Observer のユーザインターフェースを作る [PSoC]このエントリーを含むはてなブックマーク#

CY8CKIT-145-40XX

PSoC Advent Calendar 2016の7日目の記事です。

以前の記事で、 BLE Beacon を検出するアプリケーションを作成しました。 このアプリケーションでは、検出結果を UART から出力していましたので、出力を見るための PC が必要でした。

そこで、今回の記事では、 CY8CKIT-145-40XX に搭載された PSoC 4 S-Series、 CapSense ボタン、そして LED によるユーザインターフェイスを作成します。 ユーザインターフェイスが出来上がったら、この基板の裏側に搭載された EZ-BLE PRoC Module と組み合わせて、独立した BLE Beacon 検出アプリケーションに仕立てましょう。

回路図

回路図

回路図は、このようになっています。 これまでの記事に比べて、多くのコンポーネントが並んでいます。 それぞれのコンポーネントの設定は、後で述べるとして、最初にクロックの設定から見ていきます。


クロックの設定

このアプリケーションでは、 CapSense ボタンを使用するために SYSCLK の周波数を可能な限り高くします。 IMO を最大周波数である 48NHz に設定し、そのまま CPU で使用します。

このアプリケーションでは、低速クロックは使用しませんので、 "Low Frequency Clock" タブの設定はデフォルトのままです。

CapSense コンポーネント

Basic タブ

CY8CKIT-145-40XX 基板のすべての静電容量センサを使用するため、 CapSense には、3個のボタンと5極のスライダ1個を取り扱わせます。

この基板のパターンは、相互容量 (CSX) 方式を前提として作られていますが、ここでは 自己容量 (CSD) 方式を使用します。 そのため、 CSX の TX パターンは不要になりますが、この電位がフラフラしていると静電容量の検出に悪影響を与えます。 そこで、 TX パターンにつながる P1[3]/P2[6] 端子は、いずれも回路図で GND に固定しています。

このアプリケーションでは、面倒なチューニングは行わない方針で、 "CSD tuning mode" を "SmartSense (Full Auto-Tune)" に設定しています。 また、 "Finger capacitance" は、目分量でいずれも 0.3pF に設定しています。 たぶん、こんなもんでしょう。


Advanced - Generalタブ

SmartSense を使う事を前提にしているので、設定すべき項目は、ほとんどありません。 あとで、もし、ノイズに困ったら、フィルタなどを入れてみましょう。


Advanced - CSD Settingsタブ

このタブで変更するのは、 "Modulation clock frequency" だけです。 "Modulation clock" は、センサの静電容量を計算するのに使われるクロックで、この周波数が高ければ高いほど反応速度を早くすることが出来ます。 ここに、設定可能な最大周波数である 48000kHz (48MHz) を設定します。 これは、 SYSCLK に設定したクロックの周波数です。

その他、 "Enable Compensation IDAC" がチェックされています。 この機能を使うと、見かけの感度が上がるので、タッチ検出に余裕が出ます。

EZI2C コンポーネント

EZI2C Basic タブ

EZI2C コンポーネントは、 BLE Observer として使われる EZ-BLE PRoC Module との通信を行う I2C スレーブデバイスです。 "EZI2C Basic" タブでは、通信のプロトコルが定義されます。 以下のようなパラメータが設定されています。


項目備考
Data rate (kbps)400クロック信号SCLの最大周波数を示します
Number of addresses1この EZI2C コンポーネントが応答すべきスレーブアドレスの個数を示します
Primary slave address (7-bits)0x08この EZI2C コンポーネントのスレーブアドレスを示します
Sub-address size (bits)16EZI2C コンポーネントがデータを格納する仮想レジスタのアドレスのビット数を示します

UART コンポーネント

UART Basic タブ

"UART Basic" タブの設定は、前回の記事で使ったものと同じです。


UART Advanced タブ

"UART Advanced" タブの設定も、前回の記事で使ったものと同じです。

LED 出力コンポーネント

Pin_LED_top の設定

上側に配置された LED に使われている GPIO 出力の設定は、このようになっています。 LED は、 Active-LOW の設定で使用されるため、 LOW 出力の時には電流を流しますが、 HIGH 出力の時には電流を流しません。


Pin_LED_bot の設定

下側に配置された LED の設定も同様です。

PWM_BUZ コンポーネント

PWM タブ

PWM_BUZ は、 2kHz のブザー音を出すように設定されます。 ですが、ファームウェアでは、今のところ使われていません。 こんど実装しましょう。

端子の設定

端子の設定

それぞれの端子の割り当ては、このようになっています。 EZI2C の信号線は EZ-BLE PRoC Module と、 UART の信号線は KitProg2 と、接続されます。


CY8CKIT-145-40XX のブロック図

評価ボード CY8CKIT-145-40XX の内部配線は、このようになっています。 EZI2C の信号線 SCL/SDA は、 EZ-BLE PRoC にもつながっていますが、同時に KitProg2 にも接続されています。 そのため、できあがったアプリケーションは、 KitProg2 からも動作を確認する事が出来ます。

PSoC 4000SEZ-BLE PRoC の間には、 UART の信号線 TX/RX も接続されています。 しかしながら、これらの信号線は未実装の0Ω抵抗を追加すると使えるようになるものです。 このため、このアプリケーションでの使用はあきらめたのでした。

ファームウェア

ファームウェア

ファームウェアは、以下のようになりました。 ちょっと、短めです。

#include "project.h"
#include <stdio.h>

// I2C の送受信に使われるバッファ
struct I2cBuffer {
    uint8       temp;       // [RW] 温度データ
    uint8       humid;      // [RW] 湿度データ
    int8        rssi;       // [RW] RSSIデータ
    uint8       id;         // [RW] データのID番号
    uint8       newid;      // [RO] 見つけたいID番号
};

volatile struct I2cBuffer   i2cBuffer;  // バッファの実体

#define     RW_BOUNDARY     (4)         // RW領域のバイト数

EZI2C は、 RAM 上に仮想レジスタ領域を作り、 I2C のプロトコルをアクセスするという仕組みです。 そこで、最初に仮想レジスタ領域を宣言します。

アドレスラベル読み書き用途
0TEMPR/W温度データを受け取る
1HUMIDR/W湿度データを受け取る
2RSSIR/WRSSIデータを受け取る
3IDR/WID番号を受け取る
4NEWIDRID番号の変更を伝える

ここでは、5バイトのレジスタ領域が宣言され、頭から4バイトの部分が書き込み可能になっています。 書き込み可能領域の長さを示しているのが RW_BOUNDARY です。 ここでいう書き込み可能は、 I2C バスから書き込む事が出来るという意味で、どのレジスタも PSoC 4000S のファームウェアからは自由に読み書きできます。

これらのレジスタのなかで PSoC 4000S から発信されるのは、 ID 番号の変更要求 (NEWID) だけです。 EZ-BLE PRoC Module は、このレジスタから値を読み出して、次の「スキャン」で探す ID 番号として使います。

int main(void) {
    // 割り込みを許可する
    CyGlobalIntEnable;

    // UARTを起動する
    UART_Start();
        
    // EZI2C を起動する
    EZI2C_Start();
    EZI2C_EzI2CSetBuffer1(
        sizeof i2cBuffer,
        RW_BOUNDARY,
        (uint8*)&i2cBuffer
    );

    // CapSense を起動する
    CapSense_Start();
    CapSense_ScanAllWidgets();

    // 初期メッセージを表示する
    UART_UartPutString("Beacon Observer\r\n");

宣言がおわったら、すぐに main() 関数が始まります。 割り込みの許可と UART の初期化に続いて、 EZI2C の起動をおこないます。 EZI2C の初期化では、 EZI2C_EzI2CSetBuffer1() 関数に三つの引数を与えます。 それぞれ、仮想レジスタの長さ、読み書き可能レジスタの長さ、仮想レジスタのアドレスです。 外部から書き込みを行わせる場合には、二番目の引数の値を適切に設定する必要があります。

次に CapSense コンポーネントの初期化を行います。 CapSense_Start() に続いて CapSense_ScanAllWidgets() でセンサの初期スキャンを始めます。

    // メインループ
    for(;;) {
        // CapSense 関連の処理
        if (!CapSense_IsBusy()) {
            // CapSense の Scan がおわったら、
            // 各 Widget の状態を更新する
            CapSense_ProcessAllWidgets();
            
            // TOP ボタンで着目 ID を切り替える
            if (CapSense_IsWidgetActive(CapSense_BUTTON0_WDGT_ID)) {
                // ボタン0は、 ID=1 に関連付けられる
                i2cBuffer.newid = 1;
                Pin_LED_top_Write(0xFF - 0x01);     // TOPはLED0のみ点灯
                Pin_LED_bot_Write(0xff);            // BOTはすべて消灯
            } else if (CapSense_IsWidgetActive(CapSense_BUTTON1_WDGT_ID)) {
                // ボタン1は、 ID=2 に関連付けられる
                i2cBuffer.newid = 2;
                Pin_LED_top_Write(0xFF - 0x02);     // TOPはLED1のみ点灯
                Pin_LED_bot_Write(0xff);            // BOTはすべて消灯
            } else if (CapSense_IsWidgetActive(CapSense_BUTTON2_WDGT_ID)) {
                // ボタン2は、 ID=3 に関連付けられる
                i2cBuffer.newid = 3;
                Pin_LED_top_Write(0xFF - 0x04);     // TOPはLED2のみ点灯
                Pin_LED_bot_Write(0xff);            // BOTはすべて消灯
            } else {
                // その他の場合は現状維持
            }
            
            // 次の Scan を開始する
            CapSense_ScanAllWidgets();
        }

メインループは、ふたつの部分から構成されています。 前半は、 CapSense コンポーネントの処理です。 CapSense のスキャンが終わると CapSense_IsBusy() が false を返し、各センサの静電容量が求まった事を示します。 ここで、 CapSense_ProcessAllWidgets() を呼び出すと、それぞれの Widget について、タッチの有無やスライダでの指の位置などが計算されます。

基板上方にある三つのボタンは、 ID番号をそれぞれ 1, 2, 3 に変更するために使用されます。 ボタンのタッチを検出したら、フィードバックとして上方の LED の点灯パターンを変更し、 EZI2C の仮想レジスタ NEWID を変更します。

タッチされたボタンに関する処理がおわったら、ふたたび CapSense_ScanAllWidgets() を呼び出して、次のスキャンを開始します。

        
        // EZI2Cへの WRITE 動作の処理
        if (EZI2C_EzI2CGetActivity() & EZI2C_EZI2C_STATUS_WRITE1) {
            // WRITE 操作でレジスタが変更されたら、
            // RSSI の値によって LED バーの長さを変える
            if (i2cBuffer.rssi > 0) {
                Pin_LED_bot_Write(0xFF - 0x1F);     // LEDを5個点灯
            } else if (i2cBuffer.rssi > -60) {
                Pin_LED_bot_Write(0xFF - 0x0F);     // LEDを4個点灯
            } else if (i2cBuffer.rssi > -70) {
                Pin_LED_bot_Write(0xFF - 0x07);     // LEDを3個点灯
            } else if (i2cBuffer.rssi > -80) {
                Pin_LED_bot_Write(0xFF - 0x03);     // LEDを2個点灯
            } else if (i2cBuffer.rssi > -90) {
                Pin_LED_bot_Write(0xFF - 0x01);     // LEDを1個点灯
            } else {
                Pin_LED_bot_Write(0xff);            // 全消灯
            }
            // UART に受信結果を表示する
            {
                char buffer[64];    // UART出力のバッファ
                int16 t, h;         // 温度と湿度を格納する変数
                // 温度と湿度を1/10単位の整数で計算する
                t = ((17572L * i2cBuffer.temp) / 256 - 4685 + 5) / 10;
                h = ((12500L * i2cBuffer.humid) / 256 - 600 + 5) / 10;
                // Reportパケットの内容を表示する
                sprintf(buffer, "ID=%d TEMP=%d.%d HUMID=%d.%d RSSI=%d\r\n",
                    i2cBuffer.id, t/10, t%10, h/10, h%10, i2cBuffer.rssi);
                UART_UartPutString(buffer);
            }
        }        
    }
}

後半は、 EZI2C への書き込み処理です。 EZI2C に書き込み動作が行われると、 EZI2C_EzI2CGetActivity() が返す状態コードの EZI2C_EZI2C_STATUS_WRITE1 がセットされます。 これを検出して、書き込み通信後の処理を開始します。

書き込まれたデータの中で着目しているのは、 RSSI の値です。 この値の大小によって、下方の LED の点灯個数を変更します。 LED の個数で電波強度を表そうという動きです。

書き込み通信が行われた時、 UART への送信も行っています。 送信内容は、前の記事で表示していたものと同じです。 これで、 EZI2C が書き込み通信を受信したかどうかが判断できます。

実行結果

書き込みテスト

まずは、 I2C から書き込みを行ってみます。 PSoC Creator と一緒にインストールされた Beidge Control Panel を起動し、 KitProg2 と接続します。 そして、コマンド w 08 00 00 12 34 56 78 9A p を送ります。 このコマンドで、仮想レジスタのアドレス 0000 に5バイトのデータを書き込みます。 すると、コンソールに w 08+ 00+ 00+ 12+ 34+ 56+ 78+ 9A- p が返ってきます。 数字の最後の + は ACK をあらわし、 - は NAK をあらわします。 5バイト目のデータだけ NAK が返っているのは、5バイト目が書き込み不可のレジスタだからです。

これに対して、 UART は ID=120 TEMP=-34.-4 HUMID=19.4 RSSI=86 を返しました。 温度と湿度のデータは意味がありませんが、 RSSI=86($56) ID=120($78) になっているのが確認できます。 同時に下方の LED が点灯しました。

さらに w 08 00 00 23 45 67 89 p を送ると UART の出力が変わりました。


読み出しテスト

さらに I2C から読み出しを行ってみます。 コマンド r 08 x x x x x p を送るとコンソールに r 08+ 23+ 45+ 67+ 89+ 00+ p が返ってきました。 これは、さきほど I2C に書き込みを行った値です。

ここで、 BTN0 をタッチしてから、読み出しコマンドを送ると r 08+ 23+ 45+ 67+ 89+ 01+ p が返ってきました。 最後のバイト "01" が、タッチをしたことによって新たに設定された NEWID の値です。 同様に BTN1 をタッチすると "02" が返ってきました。

どうやら、ユーザインターフェイスが出来たようなので、次は BLE Beacon Observer を I2C に対応させてユーザインターフェイスと結合します。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。

参考サイト

CY8CKIT-145-40XX PSoC® 4000S CapSense Prototyping Kit
今回の記事で使用したハードウェアです。 PSoC 4000S だけを使いました。

参考商品

PSoC 4 BLE

PSoC 4 BLE

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

BLE Beacon を探そう [PSoC]このエントリーを含むはてなブックマーク#

BLEセンサ5個セット

PSoC Advent Calendar 2016の5日目の記事です。

3日目の記事で作成した BLE Beacon は Advertisement パケットを発信します。 発信されたパケットは、 Central または Observer 役のデバイスが「スキャン」することによって受信します。 今回の記事では、 CYALKIT-E03 Solar-Powered BLE Sensor 5 Pack から発信された BLE Beacon パケットを受信する Observer 役のアプリケーションを作成します。

使用するハードウェア

BLE開発キット

この記事では、上記のセンサキットに加えて、 CY8CKIT-042-BLE Bluetooth® Low Energy (BLE) Pioneer Kit も使用します。 CY8CKIT-042-BLE のベースボードにドータボード CY8CKIT-142: PSoC 4 BLE Module を搭載し、受信したパケットの情報を UART で出力し、 KitProg の仮想シリアルを通じて PC で観測します。

回路図

回路図

必要なコンポーネントは、 BLE と UART だけです。 あとは、ファームウェアで BLE を制御し、取得した情報を UART で表示させます。

クロック設定

高速クロックの設定

CY8CKIT-142 モジュールには、 24MHz と 32.768kHz の二つの水晶が搭載されています。 このアプリケーションでは、高速クロックとして、 24MHz 水晶で作ったクロックをそのまま使用します。 内蔵クロックの IMO は使用しないので、止めてしまっています。


低速クロックの設定

低速クロックも外部水晶で作ったクロックを使用します。 内蔵クロックの ILO は止めています。 このアプリケーションでは、低速タイマは使用していません。

BLE コンポーネントの設定

Generalタブ

先に述べたように、 BLE コンポーネントは、 "Observer" 役として作成します。 General タブで、 "Broadcaster/Observer" を選択し、さらに "Observer" を選択します。 これで、 "Observer" 役のデバイスを作成できるようになります。

この時に「GAP Settings の内容を削除しても良いか」というダイアログが現れますが、このプロジェクトでは必要ありませんので削除されても大丈夫です。


GAP Settings - General

次は、 "GAP Settings" タブの "General" ノードを設定します。 ここでは、 "Device name" を設定できますが、 "Observer" 役は電波を受信するだけで送信する事がありません。 したがって、ここに何らかの名前を入れても使われる事がありません。 使われないとはわかっていますが、 "Observer" と入れてみました。


GAP Settings - Scan settings

"Scan settings" ノードでは、 Advertisement パケットの「スキャン」を行う際の設定を行います。

"Scanning state" は "Active" として、常にスキャンを行わせます。 また、 "Filter policy" は、 "All" として、すべての Advertisement パケットを受信できるようにします。 受信したパケットを使うか否かは、ファームウェアで判断します。

"Fast scan parameters" で "Scan timeout" のチェックをはずすと、永遠に "Fast scan" が実行されるようになります。 この時、どのようなタイミングで受信機を動作させるかを表すのが "Scan window" と "Scan interval" です。 これらの時間を調整する事で、スキャンのタイミングを制御する事ができます。


Advancedタブ

"Advanced" タブでは、 "Use BLE low power mode" のチェックを外します。 ここで作成するアプリケーションでは消費電力など考えず、動き続ける事を目的としているので、 "low power mode" に入らないようにしておきます。

以上で、 BLE コンポーネントの設定は終わりです。

UART コンポーネントの設定

UART Basicタブ

UART コンポーネントの "UART Basic" タブでは、通信に使われるフォーマットを指定します。 ボーレートには、最近よく使われる 115200bps を使用しています。

このダイアログでは、 "Oversampling" の項目も変更しています。 "Oversampling" は、デフォルトでは 12 に設定されているのですが、 12 のままだと実際のボーレートが 117647bps (+1.2%) となってしまい、 UART で通信するには誤差が大きくなってしまいます。

そこで、 "Oversampling" を 16 に設定しました。 これにより、ボーレートは 115385bps (+0.2%) となっています。


UART Advancedタブ

"UART Advanced" タブでは、バッファのサイズと割り込みの使用方法を設定します。 まず、 "TX buffer size" を 64 に設定します。 これは、受信したパケットの情報を表示する間も BLE の通信を継続させるためです。 これにより、長いメッセージを表示させる場合でも、 UART の通信を待つことがなくなります。 一方、 "RX buffer size" は、デフォルトの 8 のままにしておきます。

"TX buffer size" を設定する事で、割り込みのモードが "None" から "Internal" に変わります。 これは、バッファの処理に内部で定義した割り込み処理ルーチンを使うためです。 アプリケーションでは、割り込みを使いませんので、 "Interrupt sources" のすべてのチェックをはずしておきます。

端子の設定

端子の設定

割り当てを設定しなくてはならない端子は、 UART の RX/TX だけです。 それぞれ P1[4]/P1[5] に割り当てます。 これらの端子は、 CY8CKIT-042-BLE キットに搭載された KitProg の UART 信号に接続されています。 これで、 KitProg の仮想シリアルを使って通信を行う事ができます。

ファームウェア

ファームウェア

ユーザのファームウェアは、いつものように main.c にまとめられています。

#include <project.h>
#include <stdio.h>

// Beaconのパケット構造体
struct BeaconPacket {
    uint8   length1;            // 02
    uint8   adType1;            // 01
    uint8   adData1;            // 04
    uint8   length2;            // 1A
    uint8   adType2;            // FF
    uint8   companyId[2];       // 4C 00
    uint8   deviceType;         // 02
    uint8   length3;            // 15
    uint8   uuid[16];           // 00 05 00 01 00 00 10 00 80 00 00 80 5F 9B 01 31
    uint8   major[2];           // 00 xx
    uint8   minor[2];           // xx xx
    uint8   rssi;               // xx
};

最初に Beacon パケットの構造体を宣言しています。 このアプリケーションでは、この構造体のフォーマットにそったパケットのみを取り扱います。 これ以外の長さやフォーマットのパケットは無視されます。 パケットの詳細は、 CYALKIT-E02 Solar-Powered BLE Sensor Beacon Reference Design Kit (RDK) の関連文書を参照してください。

// Advertisementパケットの情報を格納する変数
CYBLE_GAPC_ADV_REPORT_T advReport;
                
// 受信したパケットを格納する変数
struct BeaconPacket     beaconPacket;

Advertisement パケットを受信した時、パケットの中身以外にパケットの送信元などの情報が引き渡されます。 ここでは、その情報を受け取るための変数として advReport を宣言しています。 さらに、 beaconPacket として、パケットの中身を受け取る変数も宣言しています。

// 探すべきUUID
const uint8 targetUuid[16] = {
    0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00,
    0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x01, 0x31
};

// 探すべきID(major)
uint8   id = 0;             // 着目するID

受信したパケットが、目的の Beacon から送られてきたものか否かを判断するために、 UUID と ID を確認します。 ここで宣言された targetUuid と受信したパケットの uuid とが比較されます。 一方の ID は、8ビットの変数になっています。 このアプリケーションでは、 ID を0から255までの値に限定しており、 UART から指定する事ができます。

// Reportの中身を格納する変数群
volatile CYBIT  reportUpdated = 0;  // Report更新フラグ
uint8           temp;               // 温度データ
uint8           humid;              // 湿度データ
int8            rssi;               // RSSIデータ

パケットを受信したら、そこから必要な情報を取り出して、これらの変数に格納しておきます。 そして reportUpdated フラグをセットして、変数が更新されたことをメインループに通知します。

// BLEのイベントを処理するハンドラ
void StackEventHandler(uint32 event, void *eventParam) {
    switch (event) {
        //======================================================
        //  必須イベント
        //======================================================
        case CYBLE_EVT_STACK_ON:
            // BLEスタックが構築された
    		// 高速なBLEスキャンを開始する。スキャンは止まらない。
    		CyBle_GapcStartScan(CYBLE_SCANNING_FAST);
            break;

BLE のスタックからイベントを受け取り、処理を行うハンドラです。 BLE スタックが構築されたら、イベント CYBLE_EVT_STACK_ON が発行されます。 ここでは、 Advertisement パケットを受信する「スキャン」動作を開始します。 このアプリケーションでは、一度始めた「スキャン」は、止めません。 電源の続く限り、「スキャン」を行い、 Advertisement パケットを探し続けます。

        //======================================================
        //  GAPイベント
        //======================================================
        case CYBLE_EVT_GAPC_SCAN_START_STOP:
            // BLEスキャンが開始・停止された
            // 何もしない
            break;

GAP 関連のイベント CYBLE_EVT_GAPC_SCAN_START_STOP は、「スキャン」動作が開始または停止された時に発行されます。 このアプリケーションでも、「スキャン」が開始された時にこのイベントが発行されますが、特に何も行いません。

        case CYBLE_EVT_GAPC_SCAN_PROGRESS_RESULT:
            // BLEスキャンがデバイスを見つけた
            {
                // Reportデータをコピーする
                advReport = *(CYBLE_GAPC_ADV_REPORT_T *)eventParam;
                
                // Reportパケットの長さが期待される長さであるか確認する
                if (advReport.dataLen == sizeof beaconPacket) {
                    // Reportパケットをバッファにコピーする
                    memcpy(&beaconPacket, advReport.data, advReport.dataLen);
                    
                    // 見つけたBeaconが着目しているBeaconか確認する
                    // UUIDフィールドとMAJORフィールドだけを検証する
                    if (
                        (memcmp(beaconPacket.uuid, targetUuid, sizeof targetUuid) == 0)
                        && (beaconPacket.major[0] == 0x00)
                        && (beaconPacket.major[1] == id)
                    ) {
                        // 取り出したい情報をコピーする
                        rssi = advReport.rssi;
                        humid = beaconPacket.minor[0];
                        temp = beaconPacket.minor[1];
                        // Report更新フラグをセットする
                        reportUpdated = 1;
                    }
                }
            }
            break;

Advertisement パケットを受信した時に発行されるのが、イベント CYBLE_EVT_GAPC_SCAN_PROGRESS_RESULT です。 受信したパケットに関する情報は、ハンドラの引数 eventParam で渡されます。 着目している Beacon から発信されたパケットである事を確認したら、必要な情報を変数にコピーして、更新フラグをセットします。

        //======================================================
        //  汎用イベント
        //======================================================
        case CYBLE_EVT_TIMEOUT:
            // 時間切れイベント
            // 何もしない
            break;
            
        //======================================================
        //  処理されないイベント
        //======================================================
        default:
            // その他のイベントは単に無視する
            break;
    }       
}

その他のイベントが発行される事もありますが、処理せずにハンドラを終えます。 イベントの処理は、これだけです。

int main(void) {
    // 割り込みを許可する
    CyGlobalIntEnable;

    // UARTを起動する
    UART_Start();
        
    // BLEを起動する
    {
        CYBLE_API_RESULT_T apiResult;
        apiResult = CyBle_Start(StackEventHandler);
        // BLEスタックの初期化が成功したか確認する
        CYASSERT(apiResult == CYBLE_ERROR_OK);
    }
    
    // 初期メッセージを表示する
    UART_UartPutString("Beacon Observer\r\n");

main() 関数の冒頭では、割り込みを許可し、 UART コンポーネントと BLE コンポーネントの初期化を行います。 次に、最初のメッセージを UART に出力します。

    for(;;) {
        // BLEスタックのイベントを処理する
        CyBle_ProcessEvents();
        
        // Reportパケットが到着したか確認する
        if (reportUpdated) {
            char buffer[64];    // UART出力のバッファ
            int16 t, h;         // 温度と湿度を格納する変数
            // 更新フラグをクリアする
            reportUpdated = 0;
            // 温度と湿度を1/10単位の整数で計算する
            t = ((17572L * temp) / 256 - 4685 + 5) / 10;
            h = ((12500L * humid) / 256 - 600 + 5) / 10;
            // Reportパケットの内容を表示する
            sprintf(buffer, "ID=%d TEMP=%d.%d HUMID=%d.%d RSSI=%d\r\n",
                id, t/10, t%10, h/10, h%10, rssi);
            UART_UartPutString(buffer);            
        }

メインループの前半では、 BLE スタックに関連した処理を行い、着目した Advertisement パケットが見つかった場合は、 UART への出力処理を行います。 Beacon から届いたパケットには、温度と湿度の情報がセンサから取り出したままのデータが含まれています。 このまま表示されても人間には理解できませんので、センサの仕様にしたがって摂氏温度とパーセント湿度を計算して表示しています。

出力には、 RSSI という電波強度を示す数値も表示されます。

        // 着目するBeaconのIDを変更する
        {
            uint32 ch = UART_UartGetChar();
            if (ch) {
                // UARTから有効な文字を検出した
                char buffer[64];
                // IDを計算して格納する
                // 0..9 が有効 その他の文字も使おうと思えば使える
                id = ch - '0';
                // 新しいIDを表示する
                sprintf(buffer, "SET ID=%d\r\n", id);
                UART_UartPutString(buffer);
            }
        }
    }
}

UART の入力を使って、着目するセンサの ID が変更できるようになっています。 ID を計算するために、単純に文字コードから '0' ($30) を減じています。 このため、 '1' を入れると ID=1 に設定され、 'A' を入れると ID=17 に設定されます。 ID が設定されたら、設定後の ID が表示されます。

実行結果

実行結果

実行すると、仮想シリアルにこのようなレポートが上がってきます。 この例では、1から6まで UART から文字を送り、それぞれのセンサの値を取得しています。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。

参考サイト

CY8CKIT-042-BLE Bluetooth® Low Energy (BLE) Pioneer Kit
この記事で使用した開発キットです。 BLE 関連アプリケーションの作成が可能です。
CY8CKIT-142: PSoC 4 BLE Module
上記のキットに含まれているドータボードです。 今回は、 PSoC 4 BLE を使ってアプリケーションを作成していますが、 PRoC BLEEZ-BLE モジュールでも使用できます。
CYALKIT-E02 Solar-Powered BLE Sensor Beacon Reference Design Kit (RDK)
Wireless Sensor Node (WSN) の実験に使用できるリファレンスキットです。 このキットには、センサーノードがひとつ入っています。 このノードには、 ID=1 が付いています。
CYALKIT-E03 Solar-Powered BLE Sensor 5 Pack
上記キットのセンサーノードだけを5個パックにしたキットです。 ID=2 から ID=6 までが、あらかじめ割り当てられています。

参考商品

PSoC 4 BLE

PSoC 4 BLE

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

BLE Beacon の初歩 [PSoC]このエントリーを含むはてなブックマーク#

PSoC Advent Calendar 2016の3日目の記事です。

BLE Beacon というアプリケーションは、複数の BLE Beacon からの電波を受信して、受信場所の座標を推定するというものです。 そのため、 BLE Beacon は、 BLE Beacon の座標または座標を調べる事のできる一意な ID を発信します。 この記事では、可能な限り簡単に BLE Beacon を作成する方法をさぐります。

使用するハードウェア

CY8CKIT-145-40XX

今回使用するハードウェアは、 CY8CKIT-145-40XX PSoC® 4000S CapSense Prototyping Kit です。 今回は、このキットの裏側に搭載された EZ-BLE PRoC Module だけを使います。

BLEコンポーネントの設定

回路図

回路図に配置するのは、 BLE コンポーネントだけです。 今回作成する BLE Beacon は、データを入力せず、電波以外の物を出力しないので、端子の設定も必要ありません。


General タブ

まず、 General タブで作成するデバイスの振る舞いを決定します。 BLE Beacon は、他のデバイスとデータのやり取りをするものではなく、一方的にパケットを送信します。 このような用途で使われるデバイスを Broadcaster と呼んでいます。

これに対して、 Broadcaster の電波を受け取る事を目的としているデバイスは Observer と呼ばれます。 BLE Beacon の受信側はスマートフォンなどで行う事が多いのですが、 EZ-BLE PRoC Module を Observer として受信専用にする事もできます。


GAP Settings - General

GAP Setting タブでは、主に送信するパケットの詳細を設定します。 General ノードでは、デバイス ID とデバイス名を設定しています。 ここで設定されたデバイス名は、 BLE Beacon では使われないのでデフォルトのままにしています。


GAP Settings - Advertisement settings

Advertisement settings ノードでは、 Advertisement パケットの送信方法を規定します。 データのやり取りをするわけではないので、 Non-connectable に設定します。 Advertisement パケットの送信周期が指定されていますが、実際には、この周期は使用されていません。

BLE Beacon のパケット構成

GAP Settings - Advertisement packet

Advertisement packet では、BLE Beacon として送信するパケットの構成を指定します。 このパケットは、必須の Flags フィールドと Manufacturer Specific Data フィールドから構成されています。


Manufactrer Specific Data

Manufacturer Specific Data は、 Company ID と Data から構成されています。 Company ID には、 "Apple Inc." の 0x004C が入っています。 別の ID を使うと Android 版のアプリケーションでは認識できるのですが、 iOS 版のアプリケーションでは認識できないようです。 なんでかな?


Data フォーマット

Data には、 BLE Beacon で定められたフォーマットでデータを格納します。 CYALKIT-E02 Solar-Powered BLE Sensor Beacon Reference Design Kit (RDK) が Wireless Sensor Node (WSN) として使用される場合、16バイトの UUID には、アプリケーションの識別子として定められた値 "00050001-0000-1000-8000-00805F9B0131" が入ります。 それ以外に BLE Beacon からの情報を入れられるのは、 Major と Minor と呼ばれる合わせて4バイトのフィールドだけです。

WSN では、 Major 部分に識別 ID が入り、 Minor 部分にセンサから得られた値を入れるようになっています。 サイプレスのアプリケーション BLE-Beacon では、決められた UUID を持つパケットだけを検出するように作成されています。

クロックの設定

高速クロック設定

CPU および周辺回路は、24MHz外部発振子で作られた 24MHz のクロックをそのまま使用します。


低速クロック設定

32.768kHz 外部発振子で作られた WCO クロックは、 Timer0 で 16384 分周され 500ms ごとに割り込みを発生させ、ファームウェアのステートマシンを駆動します。

ソフトウェア

ソフトウェア

ソフトウェアは、以下のようになっています。

#include <project.h>

// Major/Minor フィールドの位置
#define     MAJOR_OFFSET    (26u)
#define     MINOR_OFFSET    (28u)

// Minor フィールドの最小・最大値および変化ステップ
#define     MINOR_MIN       (69u)   //  0℃相当の値
#define     MINOR_MAX       (126u)  // 40℃相当の値
#define     MINOR_STEP      (1u)    // Minor値の変化量

最初は、定数が定義されます。 このプログラムでは、温度情報が入っている場所にある値を変化させて、データの取りこぼしが無いかを確認しています。 MAJOR_MIN から MAJOR_MAX まで MAJOR_STEP 刻みで値を変化させます。 ここでは、0℃相当の値から40℃相当の値まで変化させているので、受信側ではノコギリ波が観測されるはずです。

// Major/Minor フィールドの値
uint8   f_minor = MINOR_MIN;        // Minorフィールドの値
uint8   f_major = 0;                // Majorフィールドの値

// BLE の初期化終了フラグ
uint8   initialized = 0;

// ソフトウェアタイマのカウンタ
uint8   tick = 0;

これだけの大域変数が宣言されています。 f_major/f_minor は、それぞれ Major/Minor フィールドに与えられる値を示しています。 initialized は、 BLE デバイスの初期化が終わったかどうかのフラグになっています。 tick は、タイマ割り込みで使用されるソフトウェアタイマのカウンタになっています。

// 500msごとに Advertisement パケットを開始・停止する
void Wdt_Callback(void) {
    if (initialized) {
        // 初期化がされた
        if (tick == 0) {
            // 最初の500msで Advertisement パケットを送信する
            
            // Major フィールドの設定
            cyBle_discoveryData.advData[MAJOR_OFFSET] = f_major;
            
            // Minor フィールドの設定
            if ((f_minor < MINOR_MIN) || (f_minor > MINOR_MAX)) {
                f_minor = MINOR_MIN;
            }
            cyBle_discoveryData.advData[MINOR_OFFSET] = f_minor;
            f_minor += MINOR_STEP;

            // Advertisement パケットの送信を開始する
            CyBle_GappStartAdvertisement(CYBLE_ADVERTISING_FAST);
            tick = 1;
        } else {
            // 次の500msで Advertisement ポケットの送信を止める
            CyBle_GappStopAdvertisement();
            tick = 0;
        }
    }
}

500ms ごとに割り込みがかかると、このコールバックルーチンが呼び出されます。 コールバック関数の設定は、メインループで行われています。

BLE の初期化が終わっていたら、 BLE の動作処理をおこないます。 一周期は二つの 500ms 割り込みで構成されています。 前半の 500ms では、 Advertisement パケットの送信を開始します。 送信内容は、コンポーネントの設定で使用した値に Major/Minor フィールドの値を加えたものです。 最後に CyBle_GappStartAdvertisement() 関数で Advertisement パケットの送信を開始します。 実際には、割り込み処理が終了してメインループに戻った時にパケットの送信が行われます。

一方、後半の 500ms では、 Advertisement パケットの送信を禁止しています。 通常 Advertisement パケットを送信する時には、コンポーネントで設定された周期にしたがって、 Advertisement パケットを送信します。 このアプリケーションでは、 Advertisement パケットの送信間隔をタイマで管理することで、精密な送信間隔を実現しようとしています。

こうして、1秒間に一回、 BLE Beacon から Advertisement パケットが送信されます。

// BLE スタックの処理ハンドラ
void StackEventHandler(uint32 event, void *eventParam) {
    switch (event) {
        // スタックが立ちあがったまたはデバイスが切断されたら初期化終了とみなす
        case CYBLE_EVT_STACK_ON:
        case CYBLE_EVT_GAP_DEVICE_DISCONNECTED:
            initialized = 1;
            break;
        // それ以外のイベントは無視する
        default:
            break;
    }
}

BLE が状態の変化を検出した時に呼び出されるのがイベント処理ハンドラですが、ここで対応しているのは、ふたつのイベントのみです。 いずれも、 BLE の初期化が終わった時に発生するイベントなので、 initialized フラグを立てて、初期化終了を知らせます。

// メインループ
int main() {
    CYBLE_API_RESULT_T apiResult;       // エラーコード

    // 割り込みを許可する
    CyGlobalIntEnable;
    
    // 低速タイマ Timer0 のコールバック関数を設定する
    CySysWdtSetInterruptCallback(CY_SYS_WDT_COUNTER0, Wdt_Callback);

    // BLE デバイスの初期化を行う
    apiResult = CyBle_Start(StackEventHandler);
    // 初期化が正常に終わったかを確認する
    CYASSERT(apiResult == CYBLE_ERROR_OK);

    for(;;){
        // BLE スタックへのイベントを処理する
        CyBle_ProcessEvents();
    }
}

メインループでは、タイマのコールバック関数を指定し、 BLE コンポーネントの初期化を行ったあと、 BLE のイベント処理を行うループに突入します。 これだけで、 BLE デバイスの出来上がりです。

観測結果

観測結果

プロジェクトが完成したら、スマートフォンで観測してみましょう。 使用するのは、 Cypress が提供する BLE-Beacon というアプリケーションで、 Android 版は Google Play から、 iOS 版は Apple Store から入手できます。

私の Android 端末では、このように表示されました。 ここで作成したデバイスは、1秒ごとにパケットを送信しています。 Android 端末でも Log 上では、1秒ごとにパケットを受信している事がわかるのですが、 Graph 表示にすると5秒に一回しかプロットしてくれません。 どうも、描画処理が間に合っていないそうです。 一方、 iOS 版では、1秒ごとにプロットが出力されます。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。

参考サイト

S6SAE101A00SA1002 Solar-Powered IoT Device Kit
太陽電池で動作する Wireless Sensor Node (WSN) を開発するためのキットです。 BLE Beacon としての動作には代わりがありませんが、サイプレスの Company ID が使用されています。
CYALKIT-E02 Solar-Powered BLE Sensor Beacon Reference Design Kit (RDK)
WSN をばらまく時に使われるハードウェアを備えた設計例です。 今回の記事は、このキットから BLE Beacon の部分を抜き出しています。
CY8CKIT-145-40XX PSoC® 4000S CapSense Prototyping Kit
今回の記事で使用したハードウェアです。 本来は、 PSoC 4 S-Series の開発キットなのですが、裏側に EZ-BLE PROC Module が付いているので、これを利用させてもらいました。
EZ-BLE PRoC Module (Bluetooth Smart)
今回の記事のターゲットとなったデバイスです。 無線モジュールとして供給されていますが、 PSoC Creator では、単体のデバイスと同等に扱われます。

参考商品

PSoC 4 BLE

PSoC 4 BLE

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

PSoC 5LP で BULK 転送 (2) [PSoC]このエントリーを含むはてなブックマーク#

回路図

前回の記事では、 USBFS コンポーネントを "Manual" モードで使ってみました。 今回は、 "DMA" モードを使ってみます。 DMA 使ったら、もっと速くなるよね。

コンポーネントの設定

転送モード設定

回路図は、前回と同じなのですが、コンポーネントの設定を一か所だけ変えました。 "Device Descriptor" タブの "Descriptor Root" ノードで "Endpoint Buffer Management" を "DMA with Manual Buffer Management" に設定しています。

"Manual" モードでは、エンドポイントバッファと RAM 上のバッファとのデータ転送を CPU で行っていました。 "DMA" モードでは、この転送に DMA を用います。 CPU の代わりに DMA を使うのだから、当然、早くなるよね。

プログラム

プログラム

DMA を使用すると、 CPU が介在することなく処理が進む時間が生まれます。 このような時間をムダにしないために、プログラムは、 "BULK-IN" と "BULK-OUT" を取り扱う二つのステートマシンとして実装しました。

#include "project.h"

#define     IN_EP               (0x02u)
#define     OUT_EP              (0x01u)
#define     BUFFER_SIZE         (64u)

uint8       buffer_in[BUFFER_SIZE] = "@@ABCDEFGIHJKLMNOPQRSTUVWXYZ";
uint8       buffer_out[BUFFER_SIZE];
uint16      length_out;

冒頭の定数とバッファの定義は、前回と同じです。

#define     ST_ACKWAIT          (1u)
#define     ST_READING          (2u)
#define     ST_GETWAIT          (3u)
#define     ST_DISCARDING       (4u)
#define     ST_PREPARING        (5u)

uint8       state_in;           // State code for BULK-IN
uint8       state_out;          // State code for BULK-OUT

今回のプロジェクトは、ステートマシンを取り入れたので、状態コードと状態変数を定義しています。 ふたつのステートマシンをひとつの状態コードで取り扱う横着な構成になっています。

int main(void) {
    CyGlobalIntEnable;                  // 割り込みの有効化    
    USB_Start(0, USB_5V_OPERATION);     // 動作電圧5VにてUSBFSコンポーネントを初期化

    for (;;) {
        // 初期化終了まで待機
        while (USB_GetConfiguration() == 0);

        USB_IsConfigurationChanged();   // CHANGEフラグを確実にクリアする

デバイスが SET_CONFIGURATION を受けて初期化を行う所までは、前回と同じです。

        // BULK-OUT: OUTエンドポイントでホストからデータを受信する
        state_out = ST_DISCARDING;

        // BULK-IN: 初期状態を決定する
        state_in = ST_PREPARING;

その後、双方のステートマシンの初期状態を決定しています。 前回のプロジェクトでは、 "BULK-OUT" エンドポイントをイネーブルする処理が入っていましたが、この処理もステートマシンの中に取り込んでしまっているので、完全に初期状態を決めるだけになりました。

        for (;;) {
            // 設定が変更されたら、再初期化をおこなう
            if (USB_IsConfigurationChanged()) {
                break;
            }

内側のループで、 SET_CONFIGURATION を検出してループを脱出するところは前回と同じです。

            // BULK-OUT ステートマシン
            switch (state_out) {
                case ST_DISCARDING:
                    // OUTバッファのデータを破棄する
                    USB_EnableOutEP(OUT_EP);
                    state_out = ST_ACKWAIT;
                    break;
                case ST_ACKWAIT:
                    // ホストからのパケットの到着を待つ
                    if (USB_GetEPState(OUT_EP) == USB_OUT_BUFFER_FULL) {
                        state_out = ST_READING;
                    }
                    break;
                case ST_READING:
                    // 受信バイト数を取得する
                    length_out = USB_GetEPCount(OUT_EP);
                    // OUTバッファからデータを取り出す
                    USB_ReadOutEP(OUT_EP, &buffer_out[0], length_out);
                    state_out = ST_GETWAIT;
                    break;
                case ST_GETWAIT:
                    // OUTバッファからの転送を待つ
                    if (USB_GetEPState(OUT_EP) != USB_OUT_BUFFER_FULL) {
                        state_out = ST_DISCARDING;
                    }
                    break;
                default:
                    break;
            }

"BULK-OUT" のステートマシンは、よっつの状態から構成されています。 まず、 ST_DISCARDING では、"BULK-OUT" エンドポイントを有効にして次のパケットを受信できるようにします。 "Manual" モードでは、エンドポイントに届いたデータを USB_ReadOutEP() 関数で引き取るだけで次のパケットを受信できるようになりました。 "DMA" モードでは、明示的に USB_EnableOutEP() を呼んで、次のパケットを受け入れます。

ST_ACKWAIT では、ホストからパケットが到着するのを待ちます。 ステートマシンで構成してあるので、パケットが到着していない場合は到着を待たずに別の処理を行う事が出来るようになっています。

ST_READING では、到着したパケットを buffer_out[] に転送を始めるため USB_ReadOutEP() 関数を呼び出します。 この後、データの転送が DMA で実行されますが、その間、 CPU は別の処理を行う事ができます。

ST_GETWAIT では、 DMA 転送の終了を待ちます。 転送の終了は、 USB_GetEPState() で示されます。 転送が完了したら、次のパケットを待つために状態遷移します。

            // BULK-IN ステートマシン
            switch (state_in) {
                case ST_PREPARING:
                    // 空きバッファにデータを準備する
                    buffer_in[0]++;
                    // INバッファのデータを送信する
                    USB_LoadInEP(IN_EP, &buffer_in[0], BUFFER_SIZE);
                    state_in = ST_ACKWAIT;
                    break;
                case ST_ACKWAIT:
                    // ホストからのデータ受信確認を待つ
                    if (USB_GetEPState(IN_EP) == USB_IN_BUFFER_EMPTY) {
                        state_in = ST_PREPARING;
                    }
                    break;
                default:
                    break;
            }
        }
    }
}

"BULK-IN" もステートマシンで構成されています。 ST_PREPARING では、ホストに送信すべきデータをバッファに準備します。 USB_LoadInEP() によって DMA 転送が開始されて、 buffer_in[] に準備されたデータがエンドポイントに送られます。

ST_ACKWAIT では、ホストからの受信確認を待ちます。 DMA 転送が終わって、ホストから受信確認が到着したら、次のパケットの準備を行います。

転送速度の測定

BULK IN 転送速度

プロジェクトが出来たら、転送速度を測定します。 "BULK-IN" は、 860kB/s でした。 前回の 850kB/s とほとんど変わりませんね。


BULK OUT 転送速度

"BULK-OUT" は、 690kB/s でした。 前回の 700kB/s よりも遅めになっています。

DMA を使っても転送速度が上がらなかったのは、扱っているデータが64バイトと DMA にとっては小さすぎるのが原因と考えられます。 もっと、パケットサイズが大きければ、効果があるのでしょう。


送受信転送速度

"BULK-IN" と "BULK-OUT" を同時に動かしたところ、双方とも 560kB/s になりました。 ステートマシンを使っても代わる代わる転送が行われる状況に変化はないようです。

以上、 "DMA" モードを使った場合の転送速度を測定しましたが、単純にパケットを送受信するだけでは、 "Manual" モードとの差は出ないという事がわかりました。 きっと、演算を行わせながら使うと、全体のスループットに貢献するのでしょう。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、 このファイル の拡張子を "zip" に変更すると再現できます。

関連商品

CY8CKIT-059 PSoC 5LP Prototyping Kit

CY8CKIT-059 PSoC 5LP Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

PSoC 5LP で BULK 転送 (1) [PSoC]このエントリーを含むはてなブックマーク#

回路図

PSoC 5LP には、 Full-Speed USB のインターフェイスが搭載されています。 今回からの記事では、 PSoC 5LP でどのくらいの転送速度が実現できるか、実験してみます。

USBFS コンポーネントの設定

CY8CKIT-059 Pioneer kit

実験に使用するハードウェアは、 CY8CKIT-059 PSoC® 5LP Prototyping Kit です。 USB のコネクタも実装されていますから、今回の実験にはピッタリです。

回路は、この記事のタイトルに掲げた通りです。 USBFS コンポーネントを配置して、インスタンス名を "USB" に変更しています。


Descriptor Root ノード

USBFS コンポーネントの設定ダイアログでは、デバイス・ディスクリプタを設定します。 すべて、 "Device Descriptor" タブの中で設定できます。

最初は、 "Descriptor Root" ノードの設定です。 ここには、エンドポイントバッファの取り扱い方法を四つの選択肢から選びます。 まずは、 "Manual (Static Allocation)" を試してみます。 他の方法は、続く記事で取り扱います。


Device Descriptor ノード

次は、 "Device Descriptor" ノードの設定です。 ここで重要なのは、 "Vendor ID" と "Product ID" です。 それぞれ "4B4" と "F1" に設定しています。

この VID/PID は、サイプレスが "Bulk Loop" というデバイスに設定した番号です。 そのため、新たに作成することなくデバイスドライバを使いまわすことができます。 デバイスドライバおよび関連するアプリケーションのインストールは、後ほど説明します。

"Manufacturing String" および "Product String" が設定されていますが、これらは必須ではありません。


Configuration Descriptor ノード

"Configuration Descriptor" ノードでは、 "Max Power (mA)" を 100 に変更しています。 また、 "Device Power" を "Bus Powered" にしています。 この実験の使い方では、消費電流の申告値を大きくする必要は、あまりありません。 また、 "Bus Powered" 申告をしてもしなくても、動作に影響はありません。 "100mA" であれば、一般的なバスパワーの USB ハブでも接続できるでしょう。


Alternate Setting 0 ノード

次は、 "Alternate Setting 0" ノードです。 このノードでは、設定はデフォルトのままですが、ひとつだけやることが有ります。 それは、エンドポイントを追加する事です。

これから作ろうとしているプロジェクトでは、 "BULK-IN" と "BULK-OUT" のエンドポイントをそれぞれひとつ使用します。 ところが、デフォルト状態では、エンドポイントが一つしか定義されていません。 ふたつ目のエンドポイントを追加するためには、 "Add Endpoint" ボタンをクリックします。


Endpoint Descriptor ノード (1)

ひとつめのエンドポイントは、 EP#1 を BULK-OUT で使用します。 パケットの最大サイズは、 Full-Speed 規格上の最大値である 64 とします。


Endpoint Descriptor ノード (2)

ふたつ目のエンドポイントは、 EP#2 を BULK-IN で使用します。 パケットの最大サイズは、 EP#1 と同じく 64 とします。

以上で USBFS コンポーネントの設定は終わりです。


この説明で使われた "BULK-IN" と "BULK-OUT" は、 USB インテーフェイスの用語です。

USB 規格では、ホストとファンクションと言う役割が有ります。 ほとんどの場合、 PC がホストになり、 PSoC 5LP で作るデバイスはファンクション側になります。 "BULK-IN" は、ホストから見た時のデータ方向を表していて、ホストがデータを受信する手順を表します。 同様に "BULK-OUT" はホストがデータを送信する手順を表します。

ファンクションから見るとデータの方向が逆になりますので、注意が必要です。

クロックの設定

クロック設定

USB を使用する上で気を付けなくてはならないのが、クロックの設定です。 USB の規格では、比較的高精度なクロックを要求されています。 ところが、 CY8CKIT-059 にはクリスタルなどの高精度なクロック源は搭載されていません。 こんな場合でも困らないように、 PSoC 5LP の場合、 USB ホストから送信された USB パケットのクロック情報を基準として、内蔵クロックである Internal Main Oscillator (IMO) を微調整し、最終的に ±0.25% の精度のクロックを得る機能があります。

微調整のタイミングをとるために 100kHz の低速クロックが使用されます。 このような事情から低速クロックもこの通りに設定しなくてはなりません。

IMO クロックは、 PLL に導入されて高速なバスクロックを生成しています。 このとき、 PLL の出力周波数を 80MHz に設定すると、 IMO が +0.25% 側に振れたときにバスクロックの最大値である 80.01MHz を超えてしまいます。 ここでは、 IMO が +0.25% の時でも最大値を超えない最大周波数設定ということで、 79.5MHz に設定しています。 これで、 IMO の誤差が大きい時でも安心して使用できます。

プログラム

プログラム

ハードウェアが整ったら、プロジェクトを Build して、ソースコードを記述します。 ソースコードは、すべて "main.c" に記述していきます。

#include "project.h"

#define     IN_EP               (0x02u)
#define     OUT_EP              (0x01u)
#define     BUFFER_SIZE         (64u)

ソースコードの最初の部分で、マクロにより定数を定義しています。 "IN_EP" は、 "BULK-IN" 転送に使用するエンドポイントの番号です。 エンドポイントは、 EP1 から EP8 までの最大8個を使用できます。 同様に、 "OU_EP" で "BULK-OUT" 転送に使用するエンドポイントを表します。

"BUFFER_SIZE" は、エンドポイントバッファのサイズを表します。 コンポーネントの設定で使用した最大サイズである "64" を使用しています。

uint8       buffer_in[BUFFER_SIZE] = "@@ABCDEFGIHJKLMNOPQRSTUVWXYZ";
uint8       buffer_out[BUFFER_SIZE];
uint16      length;

"BULK-IN" 転送では、 "buffer_in" に格納されたデータを少しずつ変化させながらホストに送信します。 一方 "BULK-OUT" 転送では、ホストから受信したデータの長さを "length" に受け取り、データ本体を "buffer_out" に引き取ります。 今回のプロジェクトは転送速度の上限を探ることを目的としています。 そのため、送受信するデータの加工は行われません。

int main(void) {
    CyGlobalIntEnable;                  // 割り込みの有効化    
    USB_Start(0, USB_5V_OPERATION);     // 動作電圧5VにてUSBFSコンポーネントを初期化

"main()" 関数の戦闘で割り込みを許可し、 USB コンポーネントの初期化を行っています。 CY8CKIT-059 評価ボードでは、ホストから供給された VBUS がそのまま PSoC 5LP に供給されます。 そのため、電源電圧が 5V である事を明記して初期化を行っています。

    for (;;) {
        // 初期化終了まで待機
        while (USB_GetConfiguration() == 0);

        USB_IsConfigurationChanged();   // CHANGEフラグを確実にクリアする

        USB_EnableOutEP(OUT_EP);        // OUTエンドポイントでホストからデータを受信する

初期化が終わったら、無限ループに入ります。 この無限ループでは、ホストから SET_CONFIGURATION で USB デバイスとしての初期設定が行われるのを待ちます。 "USB_GetConfiguration()" 関数で、 SET_CONFIGURATION が行われるのを待ちます。

さらに "USB_IsConfigurationChanged()" 関数が呼ばれて返り値が捨てられています。 この関数により、設定が変更された事を示す内部フラグが確実にクリアされます。 一部のオペレーティングシステムでは、 USB デバイスの検出後、二回 SET_CONFIGURATION が発行されます。 この関数呼び出しは、そういった SET_CONFIGURATION が複数回発行される場合に対処するためのものです。

デバイスが初期化された後は、受信が禁止されているため、 "BULK-OUT" データが受信できません。 そのため、最後の "USB_EnableOutEp()" 関数で "BULK-OUT" データの受信を許可しています。

        for (;;) {
            // 設定が変更されたら、再初期化をおこなう
            if (USB_IsConfigurationChanged()) {
                break;
            }

さらに内側にループが構成されています。 このループでは、実際のデータの送受信を行いますが、ホストから SET_CONFIGURATION が発行されたら外側のループに脱出して、初期設定を行います。 SET_CONFIGURATION を "USB_IsConfigurationChenged()" 関数で検出したら break でループを脱出します。

            // BULK-OUT : データが受信されたか調べる
            if (USB_GetEPState(OUT_EP) & USB_OUT_BUFFER_FULL) {
                // 受信バイト数を取得する
                length = USB_GetEPCount(OUT_EP);

                // OUTバッファからデータを取り出す
                USB_ReadOutEP(OUT_EP, &buffer_out[0], length);
            }

次に "BULK-OUT" データの受信を行います。 "BULK-OUT" からデータを受信すると、 "USB_GetState()" 関数で返される USB_OUT_BUFFER_FULL フラグがセットされます。 フラグがセットされているのを検出したら、受信したデータの長さを "USB_GetEPCount()" 関数から受け取り、その長さ分のデータを "USB_ReadOutEP()" 関数で受け取ります。 受け取ったデータは "buffer_out[]" に格納されますが、格納されたデータは使用されず破棄されます。 以上でデータの受信は終わりです。

もし、データをバッファに受け取る前に次の "BULK-OUT" パケットが届いてしまったら、 PSoC 5LP の USB インターフェイスハードウェアは自動的に NAK を返し、受信できない事をホストに示します。 NAK を受けたホストは、次の機会を待って、ふたたび "BULK-OUT" パケットを送信し、データが受信できた事を示す ACK をファンクションが返すまでデータを送り続けます。

このような動作を行っているので、 "BULK-OUT" 転送ではホストから送信されたデータが確実にファンクションに届くことが保証できます。

            // BULK-IN : 送信バッファが空いたか調べる
            if (USB_GetEPState(IN_EP) & USB_IN_BUFFER_EMPTY) {
                // INバッファにデータを送り込む
                buffer_in[0]++;
                USB_LoadInEP(IN_EP, &buffer_in[0], BUFFER_SIZE);
            }
        }
    }
}

最後は、 "BULK-IN" 転送です。 "BULK-IN" からデータを送信できるようになると、 "USB_GetState()" 関数で返される USB_IN_BUFFER_EMPTY フラグがセットされます。 フラグがセットされているのを検出したら、 "buffer_in[]" のデータを準備し "USB_LoadInEP()" 関数でデータを送信します。 ホスト側が受信したパケットをそれぞれ区別できるように、送信されるデータの最初の1バイトの値を変化させています。

準備されたデータは、ホストから要求が来たらホストに送信されます。 ファンクションのデータは、ホストが受信した事を示す ACK が来るまで保存されます。 このため、 "BULK-IN" のデータもホストに確実に届くことが保証されます。

ソースコードは以上です。 PSoC 5LP にプログラムしたら、 USB デバイスの出来上がりです。

転送速度の測定

デバイスが出来上がったら、転送速度を測定してみます。 PSoC 向けに測定を行うツールは、ありませんが、 EZ-USB FX3 向けに作られたツールならあります。 今回は、ありがたく、このツールを利用させてもらいます。

ツールは、 Cypress USB Suite というソフトウェア群に含まれており、 EZ-USB FX3 Software Development Kit からダウンロードできます。 インストールするには、 "EZ-USB FX3 SDK" インストーラをダウンロードして "" をまるごとインストールするか、 "USB Suite Zip" をダウンロードして使う方法があります。 ハードディスクに余裕が有れば、インストーラをご利用ください。

プログラムした PSoC 5LP の USB コネクタを PC に接続すると、デバイスドライバがインストールされます。 もし、自動的にインストールされない場合には、ダウンロードまたはインストールしたファイルからデバイスドライバを探してインストールしてください。


Streamer

転送速度を測定するツールは、 "Streamer" と呼ばれています。 ツールを起動すると、 "Connected Device" に "Cypress FX3 USB Streamer Example" というデバイスが見えます。 これが、さきほどプログラムした "PSoC 5LP" デバイスです。 "Endpoint" で "BULK IN" または "BULK OUT" を選び、 "Start" ボタンをクリックすると転送が始まり転送速度が表示されます。


BULK IN 転送速度

"BULK-IN" を選んだ場合、私の PC では 850kB/s (毎秒850kバイト)と表示されました。 Full-Speed のバス速度が 12Mb/s ですから妥当な値でしょう。


BULK OUT 転送速度

"BULK-OUT" の転送速度は 700kB/s と表示されました。


送受信転送速度

Streamer アプリケーションをふたつ開いて、 "BULK-IN" と "BULK-OUT" を同時に動かす事も出来ます。 この場合、 "BULK-IN" も "BULK-OUT" も転送速度が 560kB/s になりました。 ソースコードの構成から、転送速度が速い状態では、 "BULK-IN" と "BULK-OUT" が代わる代わる処理されます。 そのため、双方の転送速度が等しくなったと考えられます。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。

関連商品

CY8CKIT-059 PSoC 5LP Prototyping Kit

CY8CKIT-059 PSoC 5LP Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

continue 文の次に実行されるのは? [PSoC]このエントリーを含むはてなブックマーク#

本日のお題

Twitterで、お題をいただきました。 これは、いっちょ試してみなくては。 題材に取り上げたのは、 Cortex-M0 搭載の PSoC 4 M-Series です。

GCC の場合

まず、 PSoC Creator のデフォルトツールチェインである GCC で試しました。 使用したコードは以下の通りです。

int main(void) {
    CyGlobalIntEnable; /* Enable global interrupts. */

    for (;;) {
        int i;
        for (i = 0; i < 10; ++i) {
            if (i == 0) continue;
        }
    }
}

for 行の中に変数宣言が入れられないので、外で宣言しています。 あれ、もしかしたら、これは C のお題じゃなかったのかな? 気にせず、 LST ファイルを見てみます。

  30 0000 62B6     		CPSIE   i
  33              	.L4:
  37 0002 0023     		mov	r3, #0
  38 0004 00E0     		b	.L2
  40              	.L3:
  42 0006 0133     		add	r3, r3, #1
  44              	.L2:
  46 0008 092B     		cmp	r3, #9
  47 000a FCDD     		ble	.L3
  48 000c F9E7     		b	.L4

変数 i は、 r3 に割り当てられています。 i を 0 と比較する部分は、存在しません。 すでに最適化されてしまっているようです。

continue 文の次に実行されるものは、そもそも if 文さえ存在しないので、問い自体が無意味である。

最適化しない GCC の場合

この結論じゃ面白くないので、最適化されないように -O0 オプションを付けました。

  28 0000 80B5     		push	{r7, lr}
  32 0002 82B0     		sub	sp, sp, #8
  34 0004 00AF     		add	r7, sp, #0
  38 0006 62B6     		CPSIE   i
  41              	.L5:
  44 0008 0023     		mov	r3, #0
  45 000a 7B60     		str	r3, [r7, #4]
  46 000c 06E0     		b	.L2
  47              	.L4:
  49 000e 7B68     		ldr	r3, [r7, #4]
  50 0010 002B     		cmp	r3, #0
  51 0012 00D1     		bne	.L3
  52 0014 C046     		mov	r8, r8
  53              	.L3:
  55 0016 7B68     		ldr	r3, [r7, #4]
  56 0018 0133     		add	r3, r3, #1
  57 001a 7B60     		str	r3, [r7, #4]
  58              	.L2:
  60 001c 7B68     		ldr	r3, [r7, #4]
  61 001e 092B     		cmp	r3, #9
  62 0020 F5DD     		ble	.L4
  65 0022 F1E7     		b	.L5

すると、変数をレジスタに割り当てる事もしないコンパイル結果となりました。 i を 0 と比較する部分は、50行目にあります。 その後、51行目の "bne" で条件分岐を行います。 continue 文に相当するのは、52行目の "mov r8, r8" です。 その後は、55行目から始まる ++i が実行されます。 つまり、 continue 文の次に実行されるのは、 "++i" であるという事がわかりました。

continue 文の次に実行されるものは、 ++i である。

MDK-ARM の場合

同じプロジェクトを MDK-ARM でコンパイルしてみました。

0x000006E4 BF00      NOP      
0x000006E6 B662      CPSIE    I
0x000006E8 BF00      NOP      
0x000006EA BF00      NOP      

0x000006EC 2000      MOVS     r0,#0x00
0x000006EE E004      B        0x000006FA

0x000006F0 2800      CMP      r0,#0x00
0x000006F2 D100      BNE      0x000006F6
0x000006F4 E000      B        0x000006F8

0x000006F6 BF00      NOP      
0x000006F8 1C40      ADDS     r0,r0,#1

0x000006FA 280A      CMP      r0,#0x0A
0x000006FC DBF8      BLT      0x000006F0

0x000006FE E7F5      B        0x000006EC

機能制限版では、 LST ファイルが出力されないようなので、デバッガの "Disassembly" ウィンドウから取ってきました。 変数 i は、 r0 に割り当てられています。

if 文は 0x000006F0 番地にあります。 continue 文に相当するのは、 0x000006F4 番地の "B 0x000006F8" です。 分岐先の 0x000006F8 番地には、 ++i があります。

continue 文の次に実行されるものは、 ++i である。

IAR Embedded Workbench の場合

IAR のコンパイラでも、同じようにコンパイルしてみました。

   \                     main: (+1)
   \   00000000   0xB500             PUSH     {LR}

   \   00000002   0xB662             CPSIE   i
   \                     ??main_0: (+1)
   \   00000004   0x2000             MOVS     R0,#+0
   \                     ??main_1: (+1)
   \   00000006   0x280A             CMP      R0,#+10
   \   00000008   0xDAFC             BGE      ??main_0
   \   0000000A   0x2800             CMP      R0,#+0
   \                     ??main_2: (+1)
   \   0000000C   0x1C40             ADDS     R0,R0,#+1
   \   0000000E   0xE7FA             B        ??main_1

変数 i は、 R0 レジスタに割り当てられています。 変数 i と 0 を比較する部分は、 0000000A 番地の "CMP R0,#+0" です。 しかし、比較をしているだけで後に続く条件分岐が見当たりません。 次に実行されるのは、 0000000C 番地の ++i です。

continue 文に相当する部分が存在しないが、結果的に次に実行されるものは、 ++i である。

最適化をがんばったら

これまでは、 "-O0" オプションで最適化を抑制していました。 普通は、多少の最適化を行わせますので、ためしに GCC の再弱最適化オプションである "-O1" を使ってコンパイルさせてみました。

  30 0000 62B6     		CPSIE   i
  33              	.L2:
  35 0002 FEE7     		b	.L2

すると、最適化の結果、何も残りませんでした。 後にあるのは、一番外側の無限ループのみです。

最適化されると、 for 文がまるごと省略される。

関連商品

PSoC 4200M CY8CKIT-043 Prototyping Kit

PSoC 4200M CY8CKIT-043 Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

Clock_Start() メソッドでクロックが起動しない [PSoC]このエントリーを含むはてなブックマーク#

ボタンでブザーを操作する回路

PSoC 4200 を使って、ブザーをボタンにより操作しようとしているのだけど、うまく動かないと相談を受けました。 さて、何が原因でしょうね。

ソフトウェアでで Clock を制御する

使われた回路は、上にある通りです。 ソフトウェアで入力端子の状態を読み出し、それにしたがって、 Clock コンポーネントを動かしたり止めたりするというコンセプトです。 ソフトウェアでの判断は、以下のようになっています。

#include <project.h>

uint8   SW2_state;

int main() {
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */

    for (;;) {
        /* Place your application code here. */
        SW2_state = Pin_SW2_Read();
        if (SW2_state) {
            // SW2 OFF
            Clock_880Hz_Stop();
        } else {
            // SW2 ON
            Clock_880Hz_Start();
        }
    }
}

メインループの中で入力端子の状態を読み取り、状態にしたがって Stop() メソッドまたは Start() メソッドを呼び出すという仕組みです。 ボタンを押し続けた場合、このループでは繰り返し Start() メソッドが呼び出されます。 「 Start() メソッドにより、 Clock コンポーネントが起動される」という目的で作られているのであれは、これでも問題ないのですが、どうも起動する以外の副作用が有りそうです。

実験開始

実験回路

そこで、実験回路を作成しました。

左半分の回路では、 Pwm_Start タイマにより周期的な割り込み int_Start を発生させて Pin_LED 端子に接続されている Clock コンポーネントの Start() メソッドを発行しています。 LED は、 Clock コンポーネントの周期 200ms で点滅を行います。 タイマに PWM コンポーネントを使用しているのは、出力を波形で見るためです。

一方、右側の回路は、左の回路で生成された信号を取りこむロジック・アナライザとして機能します。 信号を取りこむタイミングは、割り込み int_Sample で決定します。 Status Register で取りこんだ信号は、 UART コンポーネントで送り出します。 UART で送信されたデータは Bridge Control Panel で波形として表示します。 この動作は、 CY8CKIT-042 でロジアナを作った ~UART編~ で紹介した通りです。

// Re-start Clock component ISR
CY_ISR(int_Start_isr) {
    Clock_5Hz_Start();
}

// Logic sampling ISR
CYBIT  int_Sample_flag = 0;
uint8   probe = 0;

CY_ISR(int_Sample_isr) {
    probe = SR_Probe_Read();
    int_Sample_flag = 1;    
}

int main() {
    CyGlobalIntEnable; /* Enable global interrupts. */

    // Start method calling timing
    Pwm_Start_Start();
    Pwm_Start_WritePeriod(489);
    int_Start_StartEx(int_Start_isr);
    
    // Logic sampling timing
    int_Sample_StartEx(int_Sample_isr);
    
    // Logic analyzer output
    UART_Start();

    // Main loop
    for(;;) {
        // Sample logic level periodically        
        if (int_Sample_flag) {
            UART_UartPutChar(probe);
            int_Sample_flag = 0;
        }
    }
}

ソフトウェアは、上記のとおりです。 Start() メソッドを発行する周期を Pwm_Start タイマで規定しています。 この実験では、周期を変化させて動作を確認するために、周期をソフトウェアで決定しています。 こうすると、プロジェクトを Build した時に回路の配置配線を行う必要が無いため、実験に必要な時間を減らす事ができます。

実験の結果

490ms周期

まず、 Start() メソッドの周期を 490 とした場合、このような波形になりました。 Start() メソッドを発行してから、ちょうど 100ms 後に LED 出力が立ち下がり、さらに 100ms 後に立ちあがっているのがわかります。 100ms は、 LED を駆動する Clock コンポーネントの周期の半分です。 この様子から、 Start() メソッドの発行により、 Clock コンポーネントの内部カウンタがリセットされたと考えられます。 内部カウンタがリセットされたのに Clock 出力そのものがリセットされないのは妙です。 そこで、 Start() メソッドの周期を 390ms としてみました。


390ms周期

すると、 Start() メソッド発行後 100ms の位置にあった立ち下がりエッジが無くなりました。 さらに 100ms あと、つまり Start() メソッド発行後 200ms には立ち上がりエッジがあります。 これらの実験から以下の推論が導き出されます。

  1. Start() メソッドを発行すると Clock の内部カウンタがリセットされるが、クロック出力は以前の状態を保持する。
  2. Start() メソッドの発行からクロック半周期分の時間が経過すると、クロック出力が "0" に設定される。
  3. さらに半周期分の時間が経過するとクロック出力は "1" に設定される。
  4. 以降、クロック出力を "0" と "1" で交互に設定するとクロック出力が変化する。

以上の推測から、 Start() メソッドの発行周期を 200ms よりも短くすると、クロック出力は "0" に設定されたままで、クロックとしては機能しなくなると考えられます。


199ms周期

そこで、 Start() メソッドの周期を 199ms にしました。 すると、みごとに LED が点滅しなくなりました。 ブザーの音が出なくなったのは、 Start() メソッドの周期が短すぎて、クロック出力が出なくなってしまったものと推測されます。

解決策

ここからは、どうやったらブザー出力が出てくるかという解決策を探ります。 実験の結果から、 Start() メッソドの呼び出し周期が 1.2ms (880Hz) よりも短ければ、クロックは出力されます。 そこで、ボタンの判定の後、20ms ほど遅延を入れて Start() メッソドの呼び出しが頻繁に発生しないようにします。

    for (;;) {
        /* Place your application code here. */
        SW2_state = Pin_SW2_Read();
        if (SW2_state) {
            // SW2 OFF
            Clock_880Hz_Stop();
        } else {
            // SW2 ON
            Clock_880Hz_Start();
        }
        CyDelay(20);
    }

これで、ボタンに連動してブザーが鳴るようになります。 しかし、音が「にごる」ようになってしまいました。 これは、追加した遅延時間が短すぎて、クロック出力が歪んでしまったのが原因です。

遅延時間を長くすると、音の濁りが少なくなりますが、ボタンを押してから音が出る・止まるまでの遅延も大きくなり、ボタンに対する反応が遅くなったように感じます。

#include <project.h>

uint8   SW2_state;
uint8   SW2_last_state = 1u;    // OFF as default

int main() {
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */

    for (;;) {
        /* Place your application code here. */
        SW2_state = Pin_SW2_Read();
        if (SW2_state && !SW2_last_state) {
            // SW2 ON to OFF
            Clock_880Hz_Stop();
        } else if (!SW2_state && SW2_last_state) {
            // SW2 OFF to ON
            Clock_880Hz_Start();
        }
        SW2_last_state = SW2_state;
    }
}

根本的な解決法として、以前のボタンの状態を記憶しておいて、状態が変化したら Clock コンポーネントの制御を行うようにしました。 ボタンの状態が変化した時だけ Start()/Stop() メソッドを呼び出すので、頻繁にメソッドの呼び出しが起こる事も無くなりました。 また、ボタンに対する反応も良くなりました。

参考商品

PSoC 4200 Prototyping Kit

PSoC 4200 Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

なぜ、 PSoC 5LP はクロックを LED に直結できるのか。 [PSoC]このエントリーを含むはてなブックマーク#

クロック直結 LED

PSoC 5LP で L チカをやるとき、一番簡単なのはクロック出力を出力端子に接続する事です。 これだけで、 LED を点滅させることが出来ます。 でも、このワザは、 PSoC 4 では使えません。 この記事では、なぜ PSoC 5LP だけがクロックを LED に直結できるのかを探ります。

クロックは、どこへ行く

まず、クロックを出力端子に直結して、回路を合成してみます。 合成の結果は、 "rpt" ファイルに詳しく書かれています。

Clock group 0: 
    Clock Block @ F(Clock,0): 
    clockblockcell: Name =ClockBlock
        PORT MAP (
            imo => ClockBlock_IMO ,
            pllout => ClockBlock_PLL_OUT ,
            ilo => ClockBlock_ILO ,
            clk_100k => ClockBlock_100k ,
            clk_1k => ClockBlock_1k ,
            clk_32k => ClockBlock_32k ,
            xtal => ClockBlock_XTAL ,
            clk_32k_xtal => ClockBlock_XTAL_32KHZ ,
            clk_sync => ClockBlock_MASTER_CLK ,
            clk_bus_glb => ClockBlock_BUS_CLK ,
            clk_bus => ClockBlock_BUS_CLK_local ,
            dclk_glb_0 => Net_1 ,
            dclk_0 => Net_1_local );
        Properties:
        {
        }

ディジタルクロックブロックが一つ使用されており、 dclk_0 出力に 2Hz のクロック信号 Net_1_local が出てくるようになっています。 この信号を出力端子に引きこむ事で、 LED を点滅させることが出来ます。

ペリフェラルクロック

クロックシステムのブロック図

PSoC 5LP で任意のクロックを生成する場合、8系統のクロック生成器でペリフェラルクロックと呼ばれるクロックを生成します。 クロック生成器の入力は、7種類のクロック源に加えて、一般のディジタル信号 (Digital Signal Interconnect: DSI) からも取り入れる事が出来ます。 これを使うと、ユーザの作成した回路から出てくる信号を別のシステムのクロックに使ったりなど、かなり融通の利く(ゆるい)取りまわしが出来ます。


クロック生成器

入力されたクロックは、16ビットの分周器で分周されます。 この出力は、一般のディジタル信号と同じように出てゆきます。 つまり、クロックと一般の信号の区別が非常にゆるい構成になっているのです。

出て行った信号が出力端子に接続されれば、 LED に直結する事も出来ます。 非常に簡単です。

PSoC 42xx のクロック生成器

PSoC 42xx のクロックシステム

一方、 PSoC 42xx の場合、 HFCLK をクロック源とする分周器が4系統あります。 クロックを必要とするペリフェラルは、16個のブロックに分割されます。 そして、それぞれのペリフェラルが、この4系統からクロックを選んで使用するという構成になっています。


ペリフェラルクロックの行き先

それぞれのペリフェラルに供給されたクロックの用途は、クロックに限られます。 PSoC 5LP のように一般の信号としては使用できません。 これが、クロックを LED に直結できない最大の理由です。 直結するルートが無いので直結できないのです。

PSoC 42xxM のクロック生成器

PSoC 42xxM のクロックシステム

クロックシステムは、 PSoC 4 であっても品種によって少しずつ異なっています。 この図は、 PSoC 42xx M-Series のブロック図です。 25ブロックのクロックを21系統の分周器から選択するようになっています。 もちろん、これらの用途もクロックに限られていますので、一般の信号としては使用することが出来ません。 いずれにしても、ペリフェラルクロックは必ず HFCLK を分周した信号なので、 HFCLK で同期しやすくなっています。

それでも L チカしたい

PSoC 4 でも L チカ

それでも、お手軽に PSoC 4 で L チカをしたいとなったら、 Toggle Flip-Flop (TFF) を使ってクロックを一般の信号に変換する方法があります。 この方法を使うと、 TFF を実現するためのマクロセルを一個消費してしまいます。 さらに、この TFF が含まれる UDB は、このクロック以外は使う事ができません。 条件によっては、厳しくなるでしょう。

同様にクロックを Interrupt コンポーネントに接続して周期割り込みを構成する場合なども、この手法が使えます。 もし、 UDB が足りなくなったら、別の方法が無いか考えましょう。

参考文献

PSoC® 5LP Architecture TRM
PSoC の内部構成に関する情報は、 Technical Reference Manual (TRM) という文書に記述されています。 これは、 PSoC 5LP の Architecture TRM です。
PSoC 4100 and 4200 Family: PSoC® 4 Architecture Technical Reference Manual (TRM)
これは、 PSoC 42xx の Architecture TRM です。 兄弟である PSoC 41xx の情報も入っています。
PSoC 4100M/4200M Family: PSoC® 4 Registers Technical Reference Manual (TRM)
これは、 PSoC 41xx M-SeriesPSoC 42xx M-Series の Architecture TRM です。

関連商品

SparkFun FreeSoC2 開発ボード - PSoC5LP

SparkFun FreeSoC2 開発ボード - PSoC5LP

  • 出版社/メーカー: Sparkfun
  • メディア: エレクトロニクス
PSoC 4200 Prototyping Kit

PSoC 4200 Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス
PSoC 4200M CY8CKIT-043 Prototyping Kit

PSoC 4200M CY8CKIT-043 Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

サイクルタイムを測定しよう (9) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

これまで、単純な分岐命令に着目して、サイクルタイムが長くなる場合と短くなる場合について調べてきました。 今回は、いわゆるサブルーチンコールのサイクルタイムについて調べてみました。

メインループ

実験には、以下のようなメインループを使用しました。 前回と同じように、 func9() 関数を呼び出したら、このメインループにはもどってきません。

int main(void) __attribute__((aligned(32)));
int main(void) {
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    PWM_1_Start();
    PWM_2_Start();

    for (;;) {
        /* Place your application code here. */
        func9(CR1_Control_PTR, 1u);
    }
}

被測定ループ

今回もループの周期を外部周波数カウンタで測定して、サイクルタイムを計測します。 ループ中の "nop" 命令の数を調節して、関数を呼び出す分岐命令のアドレスを変化させます。

void func9(reg8 *reg, uint8 val) __attribute__((aligned(32)));
void func9(reg8 *reg, uint8 val) {
    for (;;) {
        __ASM(
            "nop\n"         // 10
            "nop\n"         // 9
            "nop\n"         // 8
            "nop\n"         // 7
            "nop\n"         // 6
            "nop\n"         // 5
            "nop\n"         // 4
            "nop\n"         // 3
            "nop\n"         // 2
            "nop\n"         // 1
            ".label_2:"
        );
        func1_0();
        *reg = val;
    }
}

呼び出される関数は、この例では "func1_0()" となっていますが、飛び先を変える事で、分岐先のアドレスを変化させます。 ループの先頭アドレスは固定されていますので、ループの分岐先に起因する要因を除外する事ができます。

呼び出される関数

呼び出される関数は、以下のように "nop" 命令が並んだ構造になっていますが、これは "nop" の数を調整するためのものではありません。

void func1(void) __attribute__((aligned(32)));
void func1(void) {
    __ASM(
        "func1_0: nop\n"        // 8
        "func1_1: nop\n"        // 7
        "func1_2: nop\n"        // 6
        "func1_3: nop\n"        // 5
        "func1_4: nop\n"        // 4
        "func1_5: nop\n"        // 3
        "func1_6: nop\n"        // 2
        "func1_7: nop\n"        // 1
        "func1_8:\n"            // 0
    );
}

void func1_0(void);
void func1_1(void);
void func1_2(void);
void func1_3(void);
void func1_4(void);
void func1_5(void);
void func1_6(void);
void func1_7(void);
void func1_8(void);

func1() 関数は、独立した関数なのですが、内部に複数のラベルを定義する事で、飛び込み先の異なる複数の関数となるように細工されています。 例えば、 func1_0() を呼んだ場合はアドレス00に分岐し、 func1_4() を呼んだ場合はアドレス08に分岐します。 この部分のコンパイル結果は、以下のようになっています。

  74              	func1:
  80 0000 C046     		func1_0: nop
  81 0002 C046     	func1_1: nop
  82 0004 C046     	func1_2: nop
  83 0006 C046     	func1_3: nop
  84 0008 C046     	func1_4: nop
  85 000a C046     	func1_5: nop
  86 000c C046     	func1_6: nop
  87 000e C046     	func1_7: nop
  88              	func1_8:
  94 0010 7047     		bx	lr

実験結果

"nop" の数を変える事で分岐命令のアドレスを、関数の名前を変える事で分岐先を、それぞれ変化させてサイクルタイムを測定しました。 それぞれのサイクルタイムからは、実行された "nop" 命令の数だけサイクルタイムを減じて、サブルーチンコールに起因する違いが見えるようにしています。

分岐先
00020406080A0C0E10
分岐命令06191920211919202116
08191920211919202116
0A202021222020212219
0C212122232121222316
0E212122232121222316
10212122232121222316
12222223242222232421
14212122232121222316
16212122232121222316
18212122232121222316
1A222223242222232421

特徴的なのは、分岐先が "10" になっているとサイクルタイムが短くなっている点です。 このアドレスには、サブルーチンから戻るための命令が配置されています。 そのため、分岐先が分岐命令であった場合の特別なルールが働いたのではないかと推測しています。

また、分岐先のアドレスに応じてサイクルタイムが変化しているのがわかります。 アドレスの下3ビットが 110 である場合にサイクルタイムが長くなるというのは、前回までに判明したサイクルタイムが分岐先アドレスに依存しているのと合致します。 サブルーチンの分岐先についても例外ではなさそうです。

一方、分岐命令のアドレスについても依存関係が見られます。 こちらはアドレスの下3ビットが 010 であった場合にサイクルタイムが長くなっています。

この原因を考えた所、サブルーチンコールから戻ってきた時のアドレスに依存しているのではないかと推測しました。 サブルーチンコールに使用される分岐命令は4バイトです。 そのため、分岐命令のアドレス下3ビットが 010 であった場合、戻りアドレスの下3ビットは 110 となり、前回までの実験結果と一致します。

つまり、ここでも分岐先のアドレスに依存してサイクルタイムが長くなるという現象として説明できます。 分岐命令のアドレスに関連してサイクルタイムが伸びる条件は、いずれの場合も分岐先アドレスの下3ビットが 110 となる場合であると説明できます。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できるようになります。


構造体を返す関数と構造体を受け取る関数 [PSoC]このエントリーを含むはてなブックマーク#

twitter にて、 C の関数が構造体を返す事を教えてもらいました。 私が知ってる C と違う? GCC ARM と Cortex-M0 を題材に深めに調べてみました。

24ビットの構造体の場合

まず、みっつの8ビットフィールドが含まれた例を調べます。

struct point8 {
    int8    x, y, z;
};

そして、この構造体を返す関数を作成しました。

struct point8 createPoint8(int8 x, int8 y, int8 z) {
    struct point8 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}

三つの引数を受け取って、構造体を作成して返す単純な関数です。 コンパイルの結果、以下のようなコードが作成されました。

  23              	createPoint8:
  30 0000 FF23     		mov	r3, #255
  31 0002 1940     		and	r1, r3
  33 0004 1840     		and	r0, r3
  35 0006 0902     		lsl	r1, r1, #8
  36 0008 1340     		and	r3, r2
  37 000a 1B04     		lsl	r3, r3, #16
  38 000c 0843     		orr	r0, r1
  40 000e 82B0     		sub	sp, sp, #8
  43 0010 1843     		orr	r0, r3
  45 0012 02B0     		add	sp, sp, #8
  47 0014 7047     		bx	lr

三つの引数 x, y, z は、三つのレジスタ r0, r1, r2 にそれぞれ格納されます。 そして、8ビットマスクと左シフトを使いながら、24ビットの値を構成して、 r0 レジスタに格納して返します。 計算式は、以下のようになります。

r0 = ((x & 0xFF) | ((y & 0xFF) << 8) | ((z & 0xFF) << 16))

呼び出し側で関数からの返り値を変数に代入すると、以下のようなコードが生成されます。

 270 000a 0B20     		mov	r0, #11
 271 000c 1621     		mov	r1, #22
 272 000e 2122     		mov	r2, #33
 273 0010 FFF7FEFF 		bl	createPoint8
 275 0014 144C     		ldr	r4, .L14
 276 0016 2070     		strb	r0, [r4]
 277 0018 030A     		lsr	r3, r0, #8
 278 001a 6370     		strb	r3, [r4, #1]
 279 001c 000C     		lsr	r0, r0, #16
 280 001e A070     		strb	r0, [r4, #2]

構造体を8ビットごとに分解してからメモリ領域に格納しています。 コストが高そうです。

引数で構造体を渡す場合を以下の関数で確認します。

void printPoint8(struct point8 p) {
    printPoint(p.x, p.y, p.z);
}

これは、三つの値を表示する関数ですが、実際に表示する部分は別の関数になっています。 この関数からは、以下のようなコードが生成されます。

 103              	printPoint8:
 107 0000 011C     		mov	r1, r0
 108 0002 00B5     		push	{lr}
 111 0004 020C     		lsr	r2, r0, #16
 112 0006 090A     		lsr	r1, r1, #8
 113 0008 83B0     		sub	sp, sp, #12
 116 000a 40B2     		sxtb	r0, r0
 117 000c 49B2     		sxtb	r1, r1
 118 000e 52B2     		sxtb	r2, r2
 119 0010 FFF7FEFF 		bl	printPoint
 122 0014 03B0     		add	sp, sp, #12
 124 0016 00BD     		pop	{pc}

受け取った構造体は32ビットの値として扱う事ができるので、実際の処理は、32ビットの値に対するものと等価になります。 レジスタ r0 の値を8ビットごとに分解してレジスタ r0, r1, r2 に格納し、関数 printPoint() を呼び出します。 何だか、これもコストが高そうです。

 306 004c 2068     		ldr	r0, [r4]
 307 004e FFF7FEFF 		bl	printPoint8

呼び出し側では、レジスタ r0 に構造体を一括で読み出して関数 printPoint8() を呼び出します。 ここでは、構造体を構成するような事はしないようです。

48ビットの構造体の場合

次は、構造体のサイズを二倍にしてみました。

struct point16 {
    int16   x, y, z;
};

全体で48ビットありますので、レジスタひとつには入りません。 これも構造体を返す関数を作成してコードを生成させてみました。

struct point16 createPoint16(int16 x, int16 y, int16 z) {
    struct point16 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}
 134              	createPoint16:
 140 0000 0180     		strh	r1, [r0]
 141 0002 4280     		strh	r2, [r0, #2]
 142 0004 8380     		strh	r3, [r0, #4]
 145 0006 7047     		bx	lr

なんだか、ずいぶん簡単になってしまいました。 16ビットの格納命令が三つ並んでいるだけです。 それぞれのレジスタに何が入っているかは、呼び出し側を確認するとわかります。

 282 0020 04A8     		add	r0, sp, #16
 283 0022 1249     		ldr	r1, .L14+4
 284 0024 124A     		ldr	r2, .L14+8
 285 0026 134B     		ldr	r3, .L14+12
 286 0028 FFF7FEFF 		bl	createPoint16
 288 002c 201D     		add	r0, r4, #4
 289 002e 04A9     		add	r1, sp, #16
 290 0030 0622     		mov	r2, #6
 291 0032 FFF7FEFF 		bl	memcpy

三つの引数が、レジスタ r1, r2, r3 で渡されているほか、レジスタ r0 には、スタック上に確保されたメモリ領域のアドレスが渡されます。 このメモリ領域は、関数が返した構造体を格納するための一時メモリ領域です。 その後、一時メモリ領域を memcpy() 関数を使って変数に格納します。 これも受け渡しのためのコストが高そうです。

引数で構造体を渡す関数を作成してコードを生成させました。

void printPoint16(struct point16 p) {
    printPoint(p.x, p.y, p.z);
}
 155              	printPoint16:
 159 0000 00B5     		push	{lr}
 163 0002 03B2     		sxth	r3, r0
 165 0004 0A1C     		add	r2, r1, #0
 166 0006 83B0     		sub	sp, sp, #12
 169 0008 0114     		asr	r1, r0, #16
 170 000a 12B2     		sxth	r2, r2
 171 000c 181C     		mov	r0, r3
 172 000e FFF7FEFF 		bl	printPoint
 175 0012 03B0     		add	sp, sp, #12
 177 0014 00BD     		pop	{pc}

引数は、レジスタ r0, r1 で渡されて、三つの16ビットの値に再構成されて printPoint() 関数に渡されています。

 310 0052 6068     		ldr	r0, [r4, #4]
 311 0054 A168     		ldr	r1, [r4, #8]
 312 0056 FFF7FEFF 		bl	printPoint16

呼び出し側では、構造体変数の値をレジスタに r0, r1 に格納して関数 printPoint16() を呼び出しています。 呼び出し側のコストは抑制されているようです。

96ビット構造体の場合

構造体のサイズをさらに倍にしました。

struct point32 {
    int32   x, y, z;
};

構造体を返す関数と生成コードは、以下のようになりました。

struct point32 createPoint32(int32 x, int32 y, int32 z) {
    struct point32 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}
 202              	createPoint32:
 208 0000 0160     		str	r1, [r0]
 210 0002 4260     		str	r2, [r0, #4]
 212 0004 8360     		str	r3, [r0, #8]
 215 0006 7047     		bx	lr
 360 0036 04AD     		add	r5, sp, #16
 361 0038 281C     		mov	r0, r5
 362 003a 1F49     		ldr	r1, .L16+16
 363 003c 1F4A     		ldr	r2, .L16+20
 364 003e 204B     		ldr	r3, .L16+24
 365 0040 FFF7FEFF 		bl	createPoint32
 367 0044 231C     		mov	r3, r4
 368 0046 0C33     		add	r3, r3, #12
 369 0048 2A1C     		mov	r2, r5
 370 004a 43CA     		ldmia	r2!, {r0, r1, r6}
 371 004c 43C3     		stmia	r3!, {r0, r1, r6}

返された構造体を格納する場所をスタックに確保して、そのアドレスを r0 で渡しているのは、48ビットの構造体の場合と同じです。 違っているのは、返された値を変数に格納し直す部分です。

48ビット構造体では、 memcpy() 関数を使用して格納を行っていましたが、96ビットのばあいには、 ldmia, stmia 命令を使用しています。 この命令は、指定されたアドレスに対してレジスタ群を読み出し・書き込みを行います。 つまり、この1命令で96ビット分(12バイト)の読み書きができます。 ただし、32ビット単位でアクセスをするので、48ビットの構造体に対しては使用できませんでした。

引数で構造体を渡す関数と生成コードおよび呼び出し側のコードは、以下のようになりました。

void printPoint32(struct point32 p) {
    printPoint(p.x, p.y, p.z);
}
 225              	printPoint32:
 229 0000 00B5     		push	{lr}
 232 0002 85B0     		sub	sp, sp, #20
 235 0004 FFF7FEFF 		bl	printPoint
 240 000a 00BD     		pop	{pc}
 402 0082 E068     		ldr	r0, [r4, #12]
 403 0084 2169     		ldr	r1, [r4, #16]
 404 0086 6269     		ldr	r2, [r4, #20]
 405 0088 FFF7FEFF 		bl	printPoint32

構造体は、三つのレジスタ r0, r1, r2 で渡されます。 この時、レジスタにはそれぞれ x, y, z が入ってくるため、関数 printPoint() にはレジスタをそのまま渡せば良い事になります。 そのため、関数 printPoint32() では、一切のデータ操作を行っていません。

192ビット構造体の場合

さらに構造体のサイズを大きくします。

struct point64 {
    int64   x, y, z;
};

ついに一時レジスタで値を渡せるデータ量を超えました。 構造体を返す関数とそのコードおよび呼び出し側のコードは、以下のようになりました。

struct point64 createPoint64(int64 x, int64 y, int64 z) {
    struct point64 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}
 250              	createPoint64:
 256 0000 0260     		str	r2, [r0]
 257 0002 4360     		str	r3, [r0, #4]
 259 0004 009A     		ldr	r2, [sp]
 260 0006 019B     		ldr	r3, [sp, #4]
 262 0008 8260     		str	r2, [r0, #8]
 263 000a C360     		str	r3, [r0, #12]
 265 000c 029A     		ldr	r2, [sp, #8]
 266 000e 039B     		ldr	r3, [sp, #12]
 267 0010 0261     		str	r2, [r0, #16]
 268 0012 4361     		str	r3, [r0, #20]
 271 0014 7047     		bx	lr
 373 004e 1D4A     		ldr	r2, .L16+28
 374 0050 1D4B     		ldr	r3, .L16+32
 375 0052 0092     		str	r2, [sp]
 376 0054 0193     		str	r3, [sp, #4]
 377 0056 1D4A     		ldr	r2, .L16+36
 378 0058 1D4B     		ldr	r3, .L16+40
 379 005a 0292     		str	r2, [sp, #8]
 380 005c 0393     		str	r3, [sp, #12]
 381 005e 281C     		mov	r0, r5
 382 0060 1C4A     		ldr	r2, .L16+44
 383 0062 1D4B     		ldr	r3, .L16+48
 384 0064 FFF7FEFF 		bl	createPoint64
 386 0068 201C     		mov	r0, r4
 387 006a 1830     		add	r0, r0, #24
 388 006c 291C     		mov	r1, r5
 389 006e 1822     		mov	r2, #24
 390 0070 FFF7FEFF 		bl	memcpy

この場合でも、スタック上に確保された一時メモリ領域のアドレスがレジスタ r0 を介して渡されます。 引数のうち、 x はレジスタ r2, r3 で渡されますが、 y, z は、スタック上のメモリを使用します。 r1 が使用されませんが、これは64ビットの値を扱う場合には r2:r3 のペアを使うという規約によるものです。

関数の処理は、単純になりました。 スタックで渡された引数をスタックに確保された一時メモリ領域にコピーするだけの処理が行われます。

一時メモリ領域に返された値は、関数 memcpy() で変数に格納されます。

一方、構造体を引数で渡す関数は以下のようになりました。

void printPoint64(struct point64 p) {
    printPoint(p.x, p.y, p.z);
}
 281              	printPoint64:
 285 0000 84B0     		sub	sp, sp, #16
 287 0002 10B5     		push	{r4, lr}
 291 0004 0290     		str	r0, [sp, #8]
 292 0006 0391     		str	r1, [sp, #12]
 293 0008 111C     		mov	r1, r2
 294 000a 0492     		str	r2, [sp, #16]
 295 000c 0593     		str	r3, [sp, #20]
 297 000e 069A     		ldr	r2, [sp, #24]
 298 0010 FFF7FEFF 		bl	printPoint
 302 0014 10BC     		pop	{r4}
 303 0016 08BC     		pop	{r3}
 304 0018 04B0     		add	sp, sp, #16
 305 001a 1847     		bx	r3
 408 008c 211C     		mov	r1, r4
 409 008e 2831     		add	r1, r1, #40
 410 0090 6846     		mov	r0, sp
 411 0092 0822     		mov	r2, #8
 412 0094 FFF7FEFF 		bl	memcpy
 414 0098 A069     		ldr	r0, [r4, #24]
 415 009a E169     		ldr	r1, [r4, #28]
 416 009c 226A     		ldr	r2, [r4, #32]
 417 009e 636A     		ldr	r3, [r4, #36]
 418 00a0 FFF7FEFF 		bl	printPoint64

引数は、レジスタ r0, r1, r2, r3 の128ビットとスタックの64ビットに分割して渡されます。

まとめ

本日のまとめです。

  1. 構造体が32ビット以内で表現できる時には、パッキングされて通常の変数と同じようにやり取りされる。パッキング・アンパッキングのコストは安くない。
  2. 32ビットを超えるサイズの構造体を返す関数は、呼び出し前にスタックにメモリ領域を確保し、そのアドレスをレジスタ r0 に与えて呼び出す。残りの引数は、 r1, r2, r3 レジスタ、スタックの順に格納される。
  3. 関数の引数に構造体を与えた場合、通常の引数と同じルールでレジスタおよびスタックに展開される。
こんなところでしょうか。

参考サイト

Chapter 3. The Cortex-M0 Instruction Set
ARM が提供するサイトで、 Cortex-M0 の命令が参照できます。

参考文献

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

  • 作者: B.W. カーニハン
  • 出版社/メーカー: 共立出版
  • 発売日: 1989/06/15
  • メディア: 単行本
苦しんで覚えるC言語

苦しんで覚えるC言語

  • 作者: MMGames
  • 出版社/メーカー: 秀和システム
  • 発売日: 2011/06/24
  • メディア: 単行本

サイクルタイムを測定しよう (8) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

これまで、実行サイクル時間を自身のタイマで測定してきました。 この方法を使うと、測定結果を出力するためのインターフェイスが必要になります。 今回は、外部の周波数カウンタを使って、純粋にループの周期を測定します。

実験回路

ループの周期を測定するためには、ループ内に外部信号を出力する命令を入れて、この外部信号の周期を測定してループの周期とします。 外部信号は、 Control Register の Pulse 出力を使用しており、レジスタへの書込み操作だけでパルスを発生させることができます。 このパルスは高速であるため、人間の目では直接観測できません。 そこで、 1000 サイクル周期の PWM コンポーネントを二つ使って百万分の一分周器を構成し、 LED を点滅させて人間用のモニタとしています。

メインループ

メインループは、以下のようになっています。 main() 関数では、メインループの体裁を残していますが、実際には、このループは使用しません。 コンポーネントの初期化の後、 func9() 関数を呼び出すと、 func9() 内の無限ループに入り、 main() には戻ってきません。

int main(void) __attribute__((aligned(32)));
int main(void) {
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    PWM_1_Start();
    PWM_2_Start();

    for (;;) {
        /* Place your application code here. */
        func9(CR1_Control_PTR, 1u);
    }
}

被測定ループ

測定対象のループは、以下のようになっています。 前回の記事と同様、ループの前とループの中に "nop"命令を挿入し、プログラムの配置位置を制御しています。 文 "*reg = val;" で Control Register を叩き、パルスを発生させます。 Control Register のアドレスと書き込むべき値は、この関数への引数としてレジスタを介して渡されます。 これで、 Control Register を叩くときに余計なコストがかからなくなります。

void func9(reg8 *reg, uint8 val) __attribute__((aligned(32)));
void func9(reg8 *reg, uint8 val) {
    __ASM(
        "nop\n"         // 10
        "nop\n"         // 9
        "nop\n"         // 8
        "nop\n"         // 7
        "nop\n"         // 6
        "nop\n"         // 5
        "nop\n"         // 4
        "nop\n"         // 3
        "nop\n"         // 2
        "nop\n"         // 1
        ".label_1:\n"
    );
    for (;;) {
        __ASM(
            "nop\n"         // 10
            "nop\n"         // 9
            "nop\n"         // 8
            "nop\n"         // 7
            "nop\n"         // 6
            "nop\n"         // 5
            "nop\n"         // 4
            "nop\n"         // 3
            "nop\n"         // 2
            "nop\n"         // 1
            ".label_2:"
        );
        *reg = val;
    }
}

"nop" を全く入れない場合には、以下のように2命令のループになりました。 すべての命令がプリフェッチバッファに入るため、純粋な命令実行時間が見えます。

  35              	.L2:
  29:.\main.c      ****     for (;;) {
  43:.\main.c      ****         *reg = val;
  42 0000 0170     		strb	r1, [r0]
  44:.\main.c      ****     }
  44 0002 FDE7     		b	.L2

測定結果

測定結果は、以下のようになりました。 それぞれの数値は、 "nop" によるサイクル数を減じた上記の2命令の正味実行時間を示しています。 "nop" を全く入れない場合の所要サイクル数は、8サイクルでした。

分岐先
00020406080A0C0E101214
分岐命令028
0488
06888
088888
0A88898
0C888988
0E8889888
1088898888
12888988898
148889888988
1688898889888
18 8898889888
1A 898889888
1C 98889888
1E 8889888
20 889888
22 89888
24 9888
26 888
28 88
2A 8

分岐先が近い場合には、プリフェッチの影響が見えません。 それ以外では分岐先アドレスの下3ビットが 110 になっている場合にサイクル数が1サイクルだけ伸びています。 分岐先のアドレスを気にしたプログラムを作成すると、サイクル数の節約が出来るようです。 普通は、しないけどね。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できるようになります。


サイクルタイムを測定しよう (7) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

サイクルタイムを測定した結果、プログラムの配置や分岐先のアドレスによって実行時間が異なっている事がわかりました。 今回は、もっと詳しくデータをとります。

実験に使ったソフトウェア

これまでと同じように関数に "nop" 命令を並べて、プログラムの配置位置を変更します。

// Measure the execution cycle time
uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    asm(
        "nop\n"   // 10
        "nop\n"   // 9
        "nop\n"   // 8
        "nop\n"   // 7
        "nop\n"   // 6
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_1:\n"
    );
    s = *reg;
    asm(
        "b label_2\n"
        "nop\n"   // 10
        "nop\n"   // 9
        "nop\n"   // 8
        "nop\n"   // 7
        "nop\n"   // 6
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_2:\n"
    );
    e = *reg;
    
    return e - s;
}

前半のアセンブラ表記で "nop" の数を変えると、分岐命令の配置アドレスが2バイト単位で変えられます。 さらに、後半のアセンブラ表記で "nop" の数を変えると、分岐先アドレスを2バイト単位で変えられます。 分岐先のアドレスは、分岐命令のアドレスにも依存します。 "nop" の数を変えながらサイクル数を測定し、分岐命令の位置と分岐先アドレスによってで測定されたサイクル数を並べると、以下の表のようになります。

分岐命令
020406080A0C0E10121416
分岐先047/7/7
067/7/77/7/7
087/7/77/7/77/7/7
0A7/7/77/7/77/7/77/7/7
0C7/7/77/7/77/7/77/7/79/8/7
0E8/7/78/7/78/7/78/7/79/8/79/8/7
109/8/79/8/79/8/79/8/77/7/77/7/77/7/7
129/8/79/8/79/8/79/8/77/7/77/7/77/7/77/7/7
14A/8/7A/8/7A/8/7A/8/77/7/77/7/77/7/77/7/79/8/7
16B/9/7B/9/7B/9/7B/9/78/7/78/7/78/7/78/7/79/8/79/8/7
189/8/79/8/79/8/79/8/79/8/79/8/79/8/79/8/77/7/77/7/77/7/7
1A 9/8/79/8/79/8/79/8/79/8/79/8/79/8/77/7/77/7/77/7/7
1C A/8/7A/8/7A/8/7A/8/7A/8/7A/8/77/7/77/7/77/7/7
1E B/9/7B/9/7B/9/7B/9/7B/9/78/7/78/7/78/7/7
20 9/8/79/8/79/8/79/8/79/8/79/8/79/8/7
22 9/8/79/8/79/8/79/8/79/8/79/8/7
24 A/8/7A/8/7A/8/7A/8/7A/8/7
26 B/9/7B/9/7B/9/7B/9/7
28 9/8/79/8/79/8/7
2A 9/8/79/8/7
2C A/8/7

この表からは、以下の事がわかります。

  1. 分岐先が分岐命令に近く、アドレスの下3ビットが 000 から 100 である場合、サイクル数は最小を保ちます。 これは、追加でプリフェッチを必要とするためです。
  2. アドレスの下3ビットが 110 であるアドレスに分岐する場合、どの条件であってもコストが最大になります。 これは、分岐先の8バイトブロックに続いて次の8バイトブロックもプリフェッチする必要があるためと考えられます。
  3. その次にコストが高いのは、アドレスの下3ビットが 100 である場合です。 プリフェッチのタイミングを1クロックだけ遅らせる事ができるので、一回分のプリフェッチが見えなくなるためと考えられます。
  4. 分岐先が遠い場合、分岐命令のアドレスとは無関係に、分岐先のアドレスに依存したサイクル数を要します。 これにより、純粋にプリフェッチの時間が見えているのだとわかります。

以上の考察より、サイクル数を減らしたければ、分岐先のアドレスが8バイトブロックの前半になるように配置を考える必要があります。 関数内で分岐先アドレスを制御するのは困難ですが、せめて関数の入り口アドレスを8バイト境界の前半に配置するようにオプションを付けると処理時間が短くなりそうです。


PSoC 3 で作る周波数カウンタ [PSoC]このエントリーを含むはてなブックマーク#

カウンタ部

サイクルタイムを測定するために、これまで同一チップのカウンタを利用してきました。 これでも測定は出来るのですが、外部で周期または周波数を測定して処理時間を測定しようと考えました。 測定のためには測定器が必要ですが、ここでは自分で作ってみます。 材料は、最近めっきり使わなくなってしまった評価ボード CY8CKIT-030 です。

コンセプト

周波数カウンタを作るにあたって、ふたつの測定方法が考えられます。

ひとつは、被測定パルスをカウンタで数えさせておき、カウンタの値を周期的に取得する方法です。 この方法では、たとえば1秒周期でカウンタの値を読み込むと 1Hz 単位で周波数を知ることが出来ます。 被測定パルスの周波数が高いとカウンタの動作周波数を上回ってしまう場合があります。 こんな場合には、プリスケーラと呼ばれる分周器に被測定パルスを与えて、分周された出力の周波数を測定し、分周比率を掛けて被測定パルスの周波数とします。

もう一つは、被測定パルス1周期分の時間を測定する方法です。 本当に1周期分の時間を測定すると測定側のクロック周波数を非常に高くしなくてはなりません。 そのため、被測定パルスをプリスケーラで分周して、分周された信号の周期を測定します。 今回は、こちらの方法をとります。

プリスケーラ

プリスケーラ

プリスケーラは、百分周から十億分周を行うことが出来る多段分周器で構成されています。 最後の百分周は、入りきらなかったので別のページに配置しています。

Pin_Probe 入力をクロックとして使って、分周を行います。 そのため、 Pin_Probe コンポーネントにはクロック同期機能を付けていません。

PSoC Creator には、 Frequency Divider と呼ばれるコンポーネントがあります。 このコンポーネントを多段に接続する事で、簡単に分周比の大きな分周器が出来そうですが、そうはいきませんでした。 これは、 Frequency Divider の div 出力のパルス幅が1クロック分ではなくもっと長くなってしまうのが原因でした。 そこで、パルス幅を1クロックパルスに制限して分周器のカスケード接続が簡単に行えるように新たに CascadeDivider コンポーネントを作成しました。


カスケード可能分周器

div 出力のパルス幅を制限するために en 入力と AND をとって簡単に次段に接続できるようにしました。 出力部に D Flip Flop が付いていますが、このコンポーネントが無くても分周自体は行えます。

D Flip Flop が無い状態で分周器を多段に接続すると、初段の div 出力から各段の AND 回路によって、 div 出力がどんどん遅れます。 すると、後段の en 入力のセットアップ時間が削られて、対応可能な周波数が下がります。 そこで、 div 出力に D Flip Flop を追加して出力を遅延させて、次段の en 入力でセットアップ時間をかせぎます。 この回路の目的は分周をする事なので、遅延が増えても問題になりません。 D Flip Flop を入れた効果により、最大周波数は 31MHz から 35MHz に上がりました。


分周比レジスタ

分周比は、 Control Register を使用して設定します。 Control Register は、 BUS_CLK で駆動されており、 Pin_Probe で駆動されるプリスケーラとはクロックが異なっています。 使用するクロックによって分割された範囲の事をクロックドメインと呼んでいます。 通常、クロックドメインを越えて信号をやり取りすると、タイミング上の問題が発生するため、間に受け側のクロックドメインに合わせるための仕掛けが必要です。

Control Register の出力に追加された Sync コンポーネントを使用すると、 BUS_CLK に同期した信号を Pin_Probe に同期した信号に変換します。 これで、クロックドメインを越えることができるので、安心して、プリスケーラの設定を CPU から行うことが出来ます。


キー入力回路

プリスケーラの分周比を変更するために評価ボード上のタクトスイッチを使用します。 実際の処理は、ソフトウェアで行います。 ここでは、スイッチの状態を正しく伝えるための回路が追加されています。

Debouncer コンポーネントは、機械的スイッチに特有のチャタリング(英語で Bounce)を除去(Debounce)するために使用されます。 タクトスイッチは、押すと "0" になる負論理で構成されているので、 Debouncer の出力にインバータを追加しています。 そして、 Status Register に導入して CPU からキーの状態を読み取ります。 キーによる分周比の設定変更は、ソフトウェアで行います。

タイマ

タイマの回路

タイマ部分では、プリスケーラの最終段の100分周で出力された信号を Sync コンポーネントを介して Timer コンポーネントの Capture 入力に導きます。 そして、分周された信号の立ち上がりエッジごとにタイマカウンタの値を記録していきます。 ふたつの立ち上がりエッジの時間差から信号の周期を知ることができます。

記録されたカウンタの値は、 Timer コンポーネントの FIFO に入ります。 FIFO の値を割り込み int_Capture の発生ごとにソフトウェアで読み取って、差分を計算します。 計算した結果は、ソフトウェアで LCD モジュールに表示させます。

このシステムでは、32ビットの Timer コンポーネントが律速となり、28MHzが最大駆動周波数となりました。

ソフトウェア

ソフトウェアは、かなり長くなりました。

#include <project.h>

#define     KEY_UP      (0x01)
#define     KEY_DOWN    (0x02)
#define     MAX_RANGE   (7)
#define     MAX_POWER   (10)
#define     CPU_FREQ    (48e6)

KEY_UP と KEY_DOWN で、 Status Register でのキーの配置を示しています。 MAX_RANGE は、分周器の設定の最大インデックスを示します。 インデックスは、0から始まります。 MAX_POWER は、 LCD に表示可能な十進数の最大桁数を示します。 CPU_FREQ は、 LCD に表示されるサイクル数の想定周波数を示します。

// Interrupt handler
CYBIT int_Capture_flag = 0;

CY_ISR(int_Capture_isr) {
	int_Capture_flag = 1;
}

割り込み処理は、フラグを立てる操作のみを行い、実際の処理はメインループで行います。

// Decimal number generation
CYCODE const uint32 power10[MAX_POWER+1] = {
1UL,
1UL,
10UL,
100UL,
1000UL,
10000UL,
100000UL,
1000000UL,
10000000UL,
100000000UL,
1000000000UL,
};

void LCD_PrintDecUint32(uint32 d, uint8 digits) {
	uint8  m;
	uint8  v;
    static char numbuf[32];
    
    for (m = MAX_POWER; m > 0; m--) {
	    for (v = 0; d >= power10[m]; ) {
	  	  d -= power10[m];
		  v++;
	    }
	    if (m <= digits) {
            numbuf[digits-m] = '0' + v;
	    }
	}
    numbuf[digits] = 0;
    LCD_PrintString(numbuf);
}

LCD に十進数で整数を表示します。 基になる値は、 uint32 の32ビット整数です。 8051 のために、極力乗除算を行わない方式としました。

// Parameters for frequency range
CYCODE const struct {
    double  divisor;
    uint8   mux;
} params[MAX_RANGE+1] = {
    {1e2,  0x0},    // x100 prescaler
    {1e3,  0x1},    // x1k prescaler
    {1e4,  0x2},    // x10k prescaler
    {1e5,  0x3},    // x100k prescaler
    {1e6,  0x4},    // x1M prescaler
    {1e7,  0x5},    // x10M prescaler
    {1e8,  0x6},    // x100M prescaler
    {1e9,  0x7},    // x1G prescaler
};

プリスケーラの設定は、この構造体で定義される8種類です。 ふたつのフィールドは、分周比(divisor)と Control Register に設定する値(mux)を示しています。

// Frequency range control
uint8   range;
double  resolution;
double  cpks;  // cycles per kilo-second
uint8   required;

void setRange(uint8 p_range) {
    range = p_range;
    int_Capture_Disable();
    CR1_Write(params[range].mux);
    resolution = 1e0 / (BCLK__BUS_CLK__HZ * params[range].divisor);
    cpks = CPU_FREQ * 1e3;
    required = 2;
    LCD_ClearDisplay();
    LCD_Position(0,15);
    LCD_PutChar('0' + range);
    LCD_Position(0,10);
    LCD_PrintString("Hz");
    LCD_Position(1,10);
    LCD_PrintString("mc");
    Timer_ClearFIFO();
    int_Capture_ClearPending();
    int_Capture_Enable();
}

プリスケーラの設定を変更する時には、 newRange() 関数を呼び出します。 この関数では、プリスケーラの分周比設定と周波数を計算する際に使用される係数、そして LCD の表示の初期化を行っています。

// Frequency and Period calculation
uint32 capture_last = 0;
uint32 capture_now;
uint32 capture_period;
double period;
uint32 freq;
uint32 cycles;

int main()
{
    uint8  key;
    
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    int_Capture_StartEx(int_Capture_isr);
    LCD_Init();
    Timer_Start();
    setRange(0);

    for(;;) {
        /* Place your application code here. */
        if (int_Capture_flag) {
            int_Capture_flag = 0;
            capture_now = Timer_ReadCapture();
            capture_period = capture_last - capture_now;
            capture_last = capture_now;
            if (required == 0) {
                period = (double)capture_period * resolution;
                freq = (uint32)(1e0 / period);
                cycles = (uint32)(period * cpks);
                LCD_Position(0,0);
                LCD_PrintDecUint32(freq, 10);
                LCD_Position(1,0);
                LCD_PrintDecUint32(cycles, 10);
            } else if (required == 1) {
                LCD_Position(1,15);
                LCD_PutChar('*');
                required = 0;
            } else {
                LCD_Position(1,14);
                LCD_PutChar('*');
                required = 1;
            }
        }
        key = SR1_Read();
        if (key == KEY_UP) {
            if (range < MAX_RANGE) {
                setRange(range + 1);
            }
            while (SR1_Read());
        }
        if (key == KEY_DOWN) {
            if (range > 0) {
                setRange(range - 1);
            }
            while (SR1_Read());
        }
    }
}

main() 関数のメインループでは、フラグを監視して周波数を計算・表示する機能とキー入力を検出してプリスケーラの設定を変更する機能が入っています。

プリスケーラの設定変更直後は、タイマから取得した値に正しい値が入っていないため、二回ほど計算と表示を見送っています。 LCD には、入力信号の周波数と CPU_FREQ のクロックで駆動したと仮定した場合のサイクル数が表示されます。

LCD への表示

LCD への表示

できたので、さっそく、 PSoC 4 M-Series のループ周期を測定してみます。 ほぼ、何も行っていない状態ですが、速度が 2.086MHz で、23サイクルを要している事がわかります。 下の段の表示の単位はミリサイクルとなっています。

右上は、プリスケーラの設定インデックスを示しています。 この場合、 "4" と表示されているので、プリスケーラの分周比は x1M である事がわかります。 また、表示間隔が約0.5秒になっており、この時間を利用して数値計算と LCD への表示を行っています。

数値計算と LCD への表示にかなり時間を取られていますので、あまり分周比を低くすると正しい周波数が表示されなくなります。 この約 2MHz の信号の場合には、プリスケーラの分周比を x10k よりも低くすると、上記の処理中に次の割り込みが発生してしまい、正しい値が得られませんでした。

逆にプリスケーラの分周比を x1M よりも大きくすると、表示間隔が数秒以上に伸びてしまい、使い勝手が悪くなってしまいます。 適切な分周比の設定を選ぶ必要があります。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できるようになります。


サイクルタイムを測定しよう (6) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

前回の実験で、分岐命令のサイクルタイムが命令が配置されたアドレスに依存する事がわかりました。 今回は、さらに、他の要素が無いか調べます。

分岐距離による違い

前回は分岐命令のアドレスによりサイクルタイムが変化する様子が観測されました。 今回は、分岐命令のアドレスを固定して、分岐先のアドレスを変化させてみます。 分岐先を変化させるために、以下のようなソースコードを作成し、 NOP 命令の数で分岐距離を調整します。

// Measure the execution cycle time
uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    s = *reg;
    asm(
        "b label_2\n"
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_2:\n"
    );
    e = *reg;
    
    return e - s;
}

実験結果

NOP 命令の数を最大14まで変えてサイクルタイムを測定しました。

NOP数サイクル数
48/24/12
分岐先アドレス
07/7/70004
17/7/70006
27/7/70008
37/7/7000A
47/7/7000C
58/7/7000E
69/8/70010
79/8/70012
8A/8/70014
9B/9/70016
109/8/70018
119/8/7001A
12A/8/7001C
13B/9/7001E
149/8/70020

このように分岐命令を同じアドレスに配置した場合でも、分岐先アドレスによってサイクルタイムが影響を受ける事がわかりました。 おおよそ8バイトごとの繰り返しになっています。 中には、11サイクル (B) という結果も出ていました。 つまり、分岐命令の実行時間として3サイクルから7サイクルまで幅が観測された事を示しています。

NOP が7個までの部分では、それほどサイクル数が長くなっていません。 これは、プリフェッチがうまく機能したためと考えられます。


サイクルタイムを測定しよう (5) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

今回は、分岐命令の実行サイクルタイムを測定します。

分岐命令を入れる

前回の記事で、 "nop" 命令を入れた所に "b" 命令を入れます。

    uint32  s;
    uint32  e;
    
    s = *reg;
    asm(
        "b label_2\n"
        "label_2:\n"
    );
    e = *reg;

分岐先は、 "label_2" ラベルで指示します。 まずは、次の命令に分岐した場合について調べます。

このプログラムをコンパイルすると、このようなコードが生成されます。

  39:.\main.c      ****     uint32  s;
  40:.\main.c      ****     uint32  e;
  41:.\main.c      ****     
  59:.\main.c      ****     s = *reg;
  95 0000 0268     		ldr	r2, [r0]
  60:.\main.c      ****     asm(
  99 0002 FFE7     		b label_2
 100              	label_2:
  61:.\main.c      ****         "b label_2\n"
  72:.\main.c      ****         "label_2:\n"
  73:.\main.c      ****     );
  74:.\main.c      ****     e = *reg;
 105 0004 0368     		ldr	r3, [r0]

次の命令に分岐するという事は、動作としては "nop" と変わりありません。 ところが、サイクルタイムは7サイクルと表示されました。

Freq:48
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

Freq:24
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

Freq:12
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

つまり、分岐命令に要したのは、3サイクルという計算になります。 この結果から、分岐命令の処理に3サイクル必要だと判断できます。

命令の配置アドレスを変えてみた

ところが、いかなる場合でも3サイクルになったわけではありませんでした。 分岐命令の配置アドレスによって、サイクルタイムが変わってきたのです。

  39:.\main.c      ****     uint32  s;
  40:.\main.c      ****     uint32  e;
  41:.\main.c      ****     
  42:.\main.c      ****     asm(
  90 0000 C046     		nop
  91 0002 C046     	nop
  92 0004 C046     	nop
  93 0006 C046     	nop
  94              	label_1:
  95              	
  53:.\main.c      ****         "nop\n"   // 4
  54:.\main.c      ****         "nop\n"   // 3
  55:.\main.c      ****         "nop\n"   // 2
  56:.\main.c      ****         "nop\n"   // 1
  57:.\main.c      ****         "label_1:\n"
  58:.\main.c      ****     );
  59:.\main.c      ****     s = *reg;
  99 0008 0268     		ldr	r2, [r0]
  60:.\main.c      ****     asm(
 103 000a FFE7     		b label_2
 104              	label_2:
 105              	
  61:.\main.c      ****         "b label_2\n"
  72:.\main.c      ****         "label_2:\n"
  73:.\main.c      ****     );
  74:.\main.c      ****     e = *reg;
 109 000c 0368     		ldr	r3, [r0]

例えば、このプログラムでは、先頭に "nop" 命令を4個挿入して、コードの位置を8バイト後ろにずらしました。 すると、以下のように表示されました。

Freq:48
0009 0009 0009 0009 0009 0009 0009 0009
0009 0009 0009 0009 0009 0009 0009 0009

Freq:24
0008 0008 0008 0008 0008 0008 0008 0008
0008 0008 0008 0008 0008 0008 0008 0008

Freq:12
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

このようにバス周波数によってサイクル数が異なっている原因は、 Flash ROM のウェイトサイクルが影響しているものと推測されます。 挿入する "nop" 命令の数を変えながらサイクル数を測定したのが、以下の表です。

NOP数サイクル数
48/24/12
分岐先アドレス
07/7/70004
17/7/70006
27/7/70008
37/7/7000A
49/8/7000C
59/8/7000E
67/7/70010
77/7/70012
89/8/70014
99/8/70016
107/7/70018
117/7/7001A
129/8/7001C
139/8/7001E
147/7/70020

このように NOP 命令が4個(8バイト)増えるごとにサイクルタイムの長いパターンが発生します。 この結果から、プリフェッチバッファは、8バイトで構成されているらしいことが推測されます。 さらに分岐先アドレスに着目すると、飛び先アドレスの LSB 部分が X1X0 であった場合に限りサイクルタイムが長くなっている事がわかります。 これは、8バイトバッファの後半に分岐した場合には、次の8バイトのプリフェッチが追加されているものと推測されます。

今回の実験で、分岐命令の分岐先のアドレスにより、必要なサイクル数が異なってくるらしい事がわかってきました。 実験で見えてきた違いは、1サイクルまたは2サイクルです。 このくらいだったら気にすることも無いですかね。


サイクルタイムを測定しよう (4) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

今回は、個別のインストラクションについて、実行サイクルを測定します。

NOP の実行サイクル

最初は、何もしない命令 "NOP" を入れてみます。 測定関数 measure() は、以下のようになりました。

uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    s = *reg;
    asm(
        "nop\n"
    );
    e = *reg;
    
    return e - s;
}

"asm()" を使って、インストラクションを直接入れます。 この実験では、関数の最初の部分に別の NOP 列を入れることにより配置箇所を変化させましたが、いずれの場合も5サイクルになりました。 NOP 命令が無い場合は4サイクルでしたから、 NOP 命令の実行は1サイクルであったことがわかります。

イミディエイト MOV 命令

次は、イミディエイト・アドレッシングモードの MOV 命令を入れてみます。 Cortex-M0 では、8ビットで表現できる値をレジスタに入れるイミディエイト・アドレッシングモードの MOV 命令があります。

    s = *reg;
    asm(
        "mov r3,#170\n"
    );
    e = *reg;

この命令で CPU の r3 レジスタに170という値を入れます。 値に意味はありません。 また、使用したレジスタも他の部分に影響のないものという事で r3 を選んでいます。 実験の結果、 NOP の場合と同様にすべて5サイクルとなりました。 この命令も1サイクルで実行されます。

かけ算命令 MUL の場合

三つ目は、かけ算命令 MUL を実行してみます。

    s = *reg;
    asm(
        "mul r3,r0\n"
    );
    e = *reg;

ここで実行しているかけ算の内容にも意味はありません。 実験の結果は、これも5サイクルになりました。 かけ算も1サイクルで実行してしまいます。

メモリアクセスさえ無ければ

以上のように、メモリアクセスの無い命令であれば、ほとんど1サイクルで実行する事が出来ます。 しかし、実行順序に影響を与える場合には、そうはいきません。


サイクルタイムを測定しよう (3) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

タイマを使用して、サイクルタイムを測定するしかけができたので、具体的にサイクルタイムを測定していきます。 今回は、命令フェッチのサイクル数にこだわってみます。

測定ルーチンをちょっと変更

前回の記事では、 measure() 関数の中でカウンタレジスタを直接読み出していました。 そのため、以下のように .L5 に格納されたカウンタレジスタのアドレスを CPU のレジスタ r3 に格納する命令が入ってしまいます。

  38:.\main.c      ****     uint32  s;
  39:.\main.c      ****     uint32  e;
  40:.\main.c      ****     
  41:.\main.c      ****     s = Timer_COUNTER_REG;
  88 0000 024B     		ldr	r3, .L5
  89 0002 1868     		ldr	r0, [r3]
:
:
  43:.\main.c      ****     e = Timer_COUNTER_REG;
  92 0004 1B68     		ldr	r3, [r3]
  44:.\main.c      ****     
  45:.\main.c      ****     return e - s;
  95 0006 181A     		sub	r0, r3, r0
  46:.\main.c      **** }
  98              		@ sp needed
  99 0008 7047     		bx	lr
:
:
 102              	.L5:
 103 000c 08012040 		.word	1075839240

インストラクションもレジスタアドレスも Flash ROM に格納されますので、レジスタアドレスの取り出しが命令フェッチに影響を与える可能性があります。 そこで、レジスタアドレスを関数の引数として渡す方式に改めました。

// Measure the execution cycle time
uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    s = *reg;
:
:
    e = *reg;
    
    return e - s;
}

:
:

    // Measure the cycle time
    for (i = 0; i < N_DIFF; i++) {
        diffs[i] = measure(Timer_COUNTER_PTR) & Timer_16BIT_MASK;
    }

こうすると、レジスタアドレスは CPU のレジスタに格納されたまま使用されます。

  39:.\main.c      ****     uint32  s;
  40:.\main.c      ****     uint32  e;
  41:.\main.c      ****     
  42:.\main.c      ****     s = *reg;
  89 0000 0268     		ldr	r2, [r0]
:
:
  43:.\main.c      ****     e = *reg;
  92 0002 0368     		ldr	r3, [r0]

また、 "aligned(256)" という属性を追加して、関数の配置アドレスがブレないようにしています。

レジスタ読み出しが連続した場合

まず、タイマレジスタのアクセスが連続した場合について調べます。

  42:.\main.c      ****     s = *reg;
  89 0000 0268     		ldr	r2, [r0]
  43:.\main.c      ****     e = *reg;
  92 0002 0368     		ldr	r3, [r0]

この場合、タイマカウンタの値の差分をとると、 "ldr" 命令一回分のサイクル数を求めることが出来ます。 UART への出力は、以下のようになりました。

Freq:48
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004

Freq:24
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004

Freq:12
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004

CPU の周波数が変わっても4サイクルでした。 もしかしたら、プログラムの配置アドレスに依存するかもしれないと考えて、これらの前に "nop" 命令を入れて、配置を変更してみました。

    asm(
        "nop\n"   // 14
        "nop\n"   // 13
        "nop\n"   // 12
        "nop\n"   // 11
        "nop\n"   // 10
        "nop\n"   // 9
        "nop\n"   // 8
        "nop\n"   // 7
        "nop\n"   // 6
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_1:\n"
    );
    s = *reg;
    e = *reg;

結果は、いずれの場合も4サイクルのままでした。 PSoC 4 の Flash ROM は、バス周波数が高くなる場合には最大2サイクルのウェイトサイクルが入ります。 しかし、いずれのバス周波数の場合でも同じサイクル数を示しています。 これは、 Flash ROM から読み出した命令を先読みするプリフェッチ機構が存在するためです。 プリフェッチのおかげで、命令を連続して実行する場合には、 Flash ROM のアクセス時間が見えなくなります。


サイクルタイムを測定しよう (1) [PSoC]このエントリーを含むはてなブックマーク#

サイクルタイム測定キット

PSoC 4 にも CPU として Cortex-M0 が搭載されています。 普段は気にすることもないのですが、プログラムの実行時間をタイマを使って測定してみました。

実験回路

回路は、タイマと UART で構成されています。 タイマを16ビットのフリーランニングカウンタとして使用して、プログラムの実行前と実行後に読み出したカウンタの値の差を計算します。 この測定を何回か繰り返したら、まとめて UART から測定値を表示します。

実験では、 Internal Main Oscillator (IMO) の周波数を変更しながら、実行時間を測定します。 そのため、 UART の駆動クロックは、外部から与える構成にしました。

メイン関数

プログラムは、いくつかの部分で構成されています。 まずは、メイン関数から見ていきます。

int main(void) {
    // CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    Timer_Start();
    UART_Start();
        
    loop(48);
    loop(24);
    loop(12);

    for(;;) {
        /* Place your application code here. */
    }
}

コンポーネント "Timer" と "UART" の初期化を行ったら、 loop() 関数で測定を三回行います。 loop() 関数の引数には、 IMO の周波数を MHz 単位で与えます。 この実験では、 48MHz, 24MHz, 12MHz の三種類の周波数について測定を行います。 測定が終わったら、無限ループに突入します。

測定ループ

loop() 関数は、三つの部分で構成されています。

// Measure the cycle time repeatedly
// The CPU frequency is set as specified in argument
void loop(uint32 freq) {
    uint32  i;
    char    sbuf[32];
    
    // Set CPU frequency
    setFreq(freq);
    
    // Measure the cycle time
    for (i = 0; i < N_DIFF; i++) {
        diffs[i] = measure() & Timer_16BIT_MASK;
    }

    // Show the measurement results
    sprintf(sbuf, "\r\nFreq:%ld\r\n", freq);
    UART_UartPutString(sbuf);
    for (i = 0; i < N_DIFF; i++) {
        sprintf(sbuf, "%04X", diffs[i]);
        UART_UartPutString(sbuf);
        if ((i & 7) < 7) {
            UART_UartPutChar(' ');
        } else {
            UART_UartPutCRLF(' ');
        }
    }

    // Wait for UART communication completed
    while (!(UART_GetTxInterruptSource() & UART_INTR_TX_UART_DONE));
}
IMO の設定
最初は、 IMO の周波数を設定するために setFreq() 関数を呼び出します。 中身は、のちほど。
サイクルタイムの測定
サイクルタイムの測定を measure() 関数で行い、 測定結果は diff[] 配列に格納していきます。 測定は、 N_DIFF で示される回数だけ繰り返されます。
測定結果の表示
測定結果は、 UART から出力されます。 出力形式は、16進数です。

IMO の設定関数

IMO の設定を変える時には、発振周波数の変更以外に、いくつかパラメータを調整する必要があります。

// Set the CPU clock frequency
void setFreq(uint32 freq) {
    // Select a longest wait cycle
    CySysFlashSetWaitCycles(48);
    
    // Change the IMO and related parameters
    CySysClkWriteImoFreq(freq);
    CySysFlashSetWaitCycles(freq);
    CyDelayFreq(freq * 1000000u);

    // Reconfigure the UART clock frequency
    Clock_UART_SetDivider(cydelayFreqHz/BAUDRATE/UART_UART_OVS_FACTOR-1);
}

まず、 CySysFlashSetWaitCycles(48) で、 Flash ROM のウエイトサイクル数を最大設定にします。 PSoC 4 に内蔵されている Flash ROM は、システムクロックが早い場合には、ウエイトサイクルを入れる必要があります。 この関数で、最大周波数 48MHz の時に使用されるウエイトサイクル、具体的には2サイクルのウエイトが挿入されます。

次に CySysClkWriteImo() 関数で IMO の周波数を変更します。 さらに、この周波数に適切なウエイトサイクルを Flash ROM に設定します。 引き続き、 CyDelayFreq() 関数を使って、サイクルタイムの測定で使用されている CyDelay() 関数などで使用される周波数パラメータを変更します。

最後に UART に使用されるクロックの周波数を変更します。 この時に、別途宣言されている BAUDRATE を使用して、 UART のボーレートを調整しています。 この実験では、 IMO を 12MHz まで落としているので、 UART のボーレートは 9600bps と低めの値に設定されています。

サイクルタイムの測定

プログラムのサイクルタイムを測定するには、プログラムの実行前と実行後にカウンタの値を読み出します。

// Measure the execution cycle time
uint32 measure(void) {
    uint32  s;
    uint32  e;
    
    s = Timer_COUNTER_REG;
    CyDelay(1);
    e = Timer_COUNTER_REG;
    
    return e - s;
}

実行前のカウンタ値を s に格納し、実行後のカウンタ値を e に格納します。 そして、これらの差分を関数の値として返します。

この実験では、 CyDelay(1) の実行時間(約1ミリ秒)を測定しています。

実行結果

この実験し使用した機材は、 CY8C4247AZI-M485 を搭載した CY8CKIT-044 です。 実行結果は、以下のようになりました。

Freq:48
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8

Freq:24
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0

Freq:12
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A

48MHz のとき 0xBBB8 (48056) サイクル、 24MHz のとき 0x5DF0 (24048) サイクル、 12MHz のとき 0x2F0A (12042) サイクルを要しています。 CyDelay() 関数は、 CPU の実行時間を利用して遅延時間を作り出していますので、このくらいの精度が有れば十分でしょう。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。


この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。