So-net無料ブログ作成
検索選択
前の8件 | -

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

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

PSoC Creator で FM0+ の Lチカ (4) [FMx]このエントリーを含むはてなブックマーク#

FM0+ 評価ボード

前回まで、ソフトウェアループ、 PWM、タイマ割り込みを使って「Lチカ」を行ってきました。 今回は、 Direct Memory Access (DMA) を使って、「Lチカ」してみます。

使用するボードは、 FM0+ S6E1C-Series Starter Kit (FM0-64L-S6E1C3) です。

回路図に GPIO と RT と DSTC を配置する

GPIO と RT と DSTC を配置する

プロジェクトを作成するところまでは、前回までと同じです。 今回は、GPIO コンポーネントと Reload Timer (RT) コンポーネントに加えて Descriptor System data Transfer Controller (DSTC) コンポーネントを配置します。 この記事では、 DMA を使うと言いましたが、この MCU には DMA というブロックがありません。 「CPU を煩わせずにデータの転送を行う機能」として DSTC というブロックが DMA の代わりに搭載されています。 このブロックは、ディスクリプタベースのデータ転送機能を持っており、 DMA よりはるかに高機能で複雑な動作をさせることが出来ます。

このシステムでは、 RT でタイミング信号を発生し、このタイミング信号で DSTC にトリガをかけ、 GPIO の出力値を更新します。 出力するデータにトグルパターンを使用する事で、タイミング信号の周期で「Lチカ」が出来ます。

DSTC は "Component Catalog" の "System" から回路図に Drag&Drop します。 インスタンスの名前は、 "DSTC" としました。

コンポーネントの設定

RT の Interrupt タブ

まず、 RT コンポーネントを設定します。 この MCU では、 RT が発生した割り込み信号をタイミング信号として使用します。 そのため、割り込みを発生させるための設定を行わなくてはなりません。

「Interrupt タブ」で、 "bRtUnderflowIrq" を "true" に設定して、周期的な割り込み信号を発生させます。 ただし、コールバック関数を呼び出す必要はありません。 このような場合、 "pfnRtUnderflowIrqCb" を削除して空欄にしておくと関数の作成を省略できます。

"bTouchNvic" は "false" に設定します。 発生した信号を Nested Vector Interrupt Controller (NVIC) に渡す必要はありません。 以上で、タイミング信号が発生できます。


RT の RT タブ

「RT タブ」での変更点は、周期割り込みを使う場合と同じです。 "enRtPres" で RT のクロックに使用されるプリスケーラの分周比を "1/2048" に設定します。


DSTC の Basic タブ

DSTC コンポーネントはデフォルトのまま使用します。

これで回路図の設定は終わりです。

端子を割り当てる

端子の設定

次は、端子の割り当てを行います。 "Pin_Red" に "P3D" を設定します。

以上でハードウェアの設定は終わりです。 プログラムを記述する前に "Build" して、あらかじめ API を作成しておきます。

RT の初期設定

RTの初期設定

ソースコードのうち、 RT コンポーネントの初期設定は、以下のようになっています。

#include "project.h"

#define     CYCLE           (8000000/2048/2)    // 0.5秒を作る分周比

// RTコンポーネントの初期化処理
void RtInit(void) {
    Bt_Rt_Init(&RT_HW, &RT_Config);         // 初期化
    Bt_Rt_WriteCycleVal(&RT_HW, CYCLE);     // 割り込み周期
    Bt_Rt_EnableCount(&RT_HW);              // カウンタを起動
}

周期割り込みを使った時と同じように、0.5秒ごとに割り込み信号を発生させます。 この初期化関数では、カウンタを起動する所まで実行しますが、トリガはかけていません。 RT と DSTC 両方のコンポーネントの準備が出来てから、トリガをかけます。

DSTC の初期設定

DSTCの初期設定

DSTC の初期設定は、かなり複雑です。

// GPIOに出力するパターンデータ
uint32_t gpio_data[2] = {0xFFFFFFFF, 0x00000000};

gpio_data[] には、 GPIO の出力レジスタに代入する値が格納されています。 RT からタイミング信号を受け取るたびに、これらの値を一つずつ出力レジスタに入れる事により、 LED を点滅させます。 この配列には、ふたつの要素のみ定義されていますが、繰り返し機能を使う事によって、二番目の出力の後、一番目の出力に戻り、無限に LED を点滅させることが出来ます。

// DSTCのディスクリプタは、永続した記憶域に配置する
stc_dstc_des01234_t descriptor;     // DSTC descriptor

DSTC の制御に使われるデータは、ディスクリプタと呼ばれるメモリ領域に配置されます。 ここでは、 descriptor という変数をディスクリプタ領域として使用します。

DSTC のディスクリプタは、その内容によって可変長です。 ここで使用されているディスクリプタは、4ワードのディスクリプタです。

// DSTCコンポーネントの初期化処理
void DstcInit(void) {
    // 初期設定構造体の作成
    stc_dstc_config_t dstc_config = {
        0,                          // ディスクリプタの先頭は、後で設定する
        DSTC_Config                 // コンポーネントで設定されたパラメータ
    };

DstcInit() 関数の冒頭で、 dstc_config 構造体を宣言しています。 DSTC コンポーネントは、この構造体を初期化関数に渡すことで初期化します。

構造体の最初の要素には、ディスクリプタの先頭ポインタを入れます。 これは、後で設定します。 それ以降の要素は、コンポーネントの設定ダイアログで設定した内容を DSTC_Config というマクロで渡します。 このマクロは、複数の要素を並べた内容になっており、事実上、設定構造体の初期設定でしか使用する事ができません。 不自由なのですが、完結したライブラリの仕様ですので従いましょう。

    
    // ディスクリプタの設定
    PDL_ZERO_STRUCT(descriptor);
    descriptor.DES0.DV    = 3u;     // 転送終了後もDSTCが継続して権限を持つ
    descriptor.DES0.MODE  = 1u;     // リクエストごとに1ワード転送する
    descriptor.DES0.ORL   = 1u;     // 転送終了後カウンタパラメータを再設定する
    descriptor.DES0.TW    = 2u;     // 32-bitワード
    descriptor.DES0.SAC   = 1u;     // 転送元アドレスを1ワードずつインクリメントしてINNERで再設定する
    descriptor.DES0.DAC   = 5u;     // 転送先アドレスは固定する
    descriptor.DES0.CHRS  = 0u;     // ディスクリプタチェーンは使わない
    descriptor.DES0.DMSET = 0u;     // 転送終了後も転送信号を受け付ける
    descriptor.DES0.CHLK  = 0u;     // ディスクリプタチェーンは使わない
    descriptor.DES0.ACK   = 1u;     // データ転送後にACKを返す
    // DES0フィールドのチェックサムを計算する
    descriptor.DES0.PCHK  = DSTC_PCHK_CALC(descriptor.u32DES0);

    // 転送カウンタの設定
    descriptor.DES1_mode1.IIN = 2u; // 2ワード転送する
    descriptor.DES1_mode1.IRM = 2u; // 2ワード残っている
    descriptor.DES1_mode1.ORM = 1u; // INNER転送を1回実行する

    // 転送元・転送先アドレス
    descriptor.DES2 = (uint32_t)gpio_data;          // 出力パターンデータ
    descriptor.DES3 = (uint32_t)&FM_GPIO_PDOR3;     // GPIOの出力データレジスタ

    // 転送終了後に再設定されるカウンタ値
    descriptor.DES4_mode1 = descriptor.DES1_mode1;  // DES1のカウンタ値をコピーする

DSTC の転送単位は、 8bit, 16bit, 32bit から選びます。 転送単位を示すのが、 DES0.TW フィールドです。 今回は、 GPIO の出力レジスタに 32bit で書き込みを行います。

DSTC では、一つのディスクリプタで繰り返し転送を行う事が出来ます。 この繰り返しを行うためのループ二重になっており、それぞれのループを INNER (内側)と OUTER (外側)と呼んでいます。 INNER の繰り返し回数と OUTER の繰り返し回数を掛け合わせたものが、ディスクリプタの転送回数になります。 INNER カウンタの初期値と繰り返し回数を表すのが、 DES1_mode1.IIN と DES1_mode1.IRM です。 また、 OUTER カウンタの初期値は DES1_mode1.ORM です。

DSTC のディスクリプタには、 MODE0 と MODE1 の二種類があります。 モードを指定するのが、 DES0.MODE フィールドです。 MODE0 は、 INNER の繰り返しをまとめて実行します。 一方、 MODE1 は、 INNER の転送を1転送単位ずつ実行します。 今回は、 MODE1 を使い、1転送ずつ GPIO 出力を変更して LED の点滅を表現します。

転送元のデータは、 gpio_data[] 配列に入っており、 DES2 でアドレスを指定します。 また、転送先は P3 ポートの出力レジスタである FM_GPIO_PDOR3 を DES3 で指定します。 これらの転送元アドレスは、1転送ごとにアドレスをインクリメントして配列中の次の値を使用します。 また、アドレスは、 INNER の繰り返し回数のあと、元のアドレスに再設定されます。 このアドレスの振る舞いを指定しているのが、 DES0.SAC です。 一方、転送先アドレスは、出力レジスタに固定されたままです。 この振る舞いを指定しているのが、 DES0.DAC です。

ここで、転送先に FM_GPIO_PDOR3 を指定していますが、そもそも Build しないと GPIO コンポーネントが本当に P3 に割り当てられたかはわかりません。 本来であれば、 GPIO コンポーネントに配置された出力レジスタのアドレスが定義されれば良いのですが、そうなってはいません。 ぜひ、マクロを加えてほしい所です。

OUTER ループが既定の繰り返し回数に達したら、転送が終了します。 このとき、 DES1_mode1, DES2, DES3 のそれぞれの値を再設定する機能があります。 この機能を働かせるか否かを決めるのが DES0.ORL フィールドです。 今回の設定では、 DES1_mode1 の繰り返しカウンタだけを再設定しています。 再設定値は DES4_mode1 に設定しますが、 DES1_mode1 と同じ内容ですので、そのままコピーしています。

今回の使い方では、転送が終了しても続けて転送を行いたいので、終了後も転送を継続するための設定が必要です。 この設定を行っているのが、 DES0.DV です。 デフォルトの 0 にすると、転送終了後のディスクリプタのアクセス権限を CPU が持つのですが、 3 にすると継続して DSTC がアクセス権限を保持し、転送リクエストを待ち続けます。

DES0.PCHK には、 DES0 のチェックサムが入ります。 このチェックサムが合わないと、ディスクリプタは無効になります。 チェックサムは DSTC_PCHK_CALC マクロで計算させます。

    
    // ディスクリプタの先頭アドレス
    dstc_config.u32Destp = (uint32_t)&descriptor;
    
    // 設定構造体でDSTCを設定する
    Dstc_Init(&dstc_config);
}

最後に設定構造体にディスクリプタのアドレスを設定し、 DSTC を初期化します。 これで、 DSTC の設定ができました。

main() 関数の記述

main()関数

初期化ルーチンが揃ったら、 main() 関数を記述します。

int main(void) {
    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    // コンポーネントの初期設定
    Pin_Red_GpioInitOut(1u);
    RtInit();
    DstcInit();
    
    // DSTCの制御
    Dstc_SetCommand(CmdErclr);                      // DSTCのエラーをクリア
    Dstc_SetCommand(CmdRbclr);                      // 何かをクリア
    Dstc_SetHwdesp(DSTC_IRQ_NUMBER_BT0_IRQ0, 0u);   // RTのIRQ0で0番目のDESを起動する
    Dstc_SetDreqenbBit(DSTC_IRQ_NUMBER_BT0_IRQ0);   // RTのIRQ0信号をDSTCに引き込む

    // RTの制御
    Bt_Rt_EnableSwTrig(&RT_HW);                     // RTの起動

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

まず、三つのコンポーネントの初期化を行います。 GPIO の初期化は Pin_Red_GpioInitOut() 関数で、 RT と DSTC の初期化は、それぞれの初期化関数で行います。

続いて、 DSTC を起動します。 Dstc_SetCommand() 関数で、あらかじめフラグをクリアしておきます。 そして、 Dstc_SetHwdesp() 関数で割り込み信号がきたら、ディスクリプタのどの部分を実行すれば良いかを指定します。 今回の場合には、 Base Timer #0 (BT0) の IRQ0 がきたら、0番目のディスクリプタを実行します。

ここで、割り込み信号をインデックス DSTC_IRQ_NUMBER_BT0_IRQ0 で表現していますが、 RT コンポーネントが BT0 に割り当てられるのかどうかは、 Build してみなくてはわかりません。 にもかかわらず、このような表記になっているのは、 RT コンポーネントから特定の BT を指し示す方法が無いからです。 ぜひ、 PSoC Creator が生成するファイルにマクロを用意してほしい所です。

最後に Dstc_SetDreqenbBit() 関数で割り込み信号を CPU の代わりに DSTC に接続します。 これで、割り込み信号により DSTC が動作を始めるようになります。

最後のステップは、 RT の起動です。 すでに、割り込み信号が DSTC に接続されていますので、 Bt_Rt_EnableSwTrig() 関数で RT にトリガをかけると、周期的に DSTC に対して転送要求を行います。

初期設定のあと、プログラムは無限ループに入ります。 割り込み要求は発生しないので、 CPU は割り込みを受けることも無くループを回り続けます。 これで、ソースコードは終わりです。 "Build" して、 "Program" すると、 LED が点滅を始めます。 「Lチカ」できました。


前の8件 | -

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