So-net無料ブログ作成
前の8件 | -

Smart I/O でワンショットパルスを作る [PSoC]このエントリーを含むはてなブックマーク#

回路図

貧弱な PSoC 4000S でワンショットパルスを出したい。 こういう要望があったので、 Smart I/O で実現してみました。 与えたクロック数発分のパルスをソフトウェアでトリガします。

このプロジェクトは、 CY8CKIT-145-40XX PSoC 4000S Prototyping Kit で動作させています。

大まかな動作原理

Smart I/O の設定

Smart I/O の内部は、このように構成してみました。 ソフトウェアのトリガは、 data4 から LUT5 に入り、立ち上がりが検出されます。 立ち上がりが検出されると LUT4 のステートマシンが動作を始めます。 LUT4 は、 LUT6 を介して、 Data Unit (DU) のダウンカウンタを起動します。 カウンタがゼロになったら、 LUT4 に通知し、カウンタもリセットされます。

コントロールレジスタは無いけれど

ソフトウェアでトリガをかけるためには、 PSoC 5LP でいうところの Control Register のようなものが必要です。 ところが、貧弱な PSoC 4000S には UDB が搭載されていません。 そこで、 Control Register にかわる代替案を持ってきました。

トップレベル回路図を見るとわかりますが、 data4 という入力端子が浮いたままになっています。 ここには、 TCPWM などのペリフェラルを接続するのが通常の使い方なのですが、 data4 の入力設定はあえて "Undefined" にしてあります。 こうすると、 data4 には、ある信号が入ってきます。 それは、 gpio4 に接続されている Pin_3_4 端子のデータレジスタです。

Pin_3_4 端子の出力は、 Smart I/O に接続されているので、データレジスタは用がありません。 この用のないデータレジスタが、故意なのか偶然なのか data4 入力に自動的に接続されてしまうのです。 そのため、 data4 を操作したかったら、 Pin_3_4_Write() 関数で論理を指定できます。 これは、便利。

立ち上がり検出

立ち上がり検出

data4 から入ってきた信号は、 LUT5 を使って立ち上がりを検出します。 これには、エッジ検出に使ってくれと言わんばかりの "Registered input" というモードを使います。 input2 にだけフリップフロップが付いているので、クロックの前後で信号が変化した事を検出することができます。

メインのステートマシン

メインのステートマシン

LUT4 は、パルスを出力するステートマシンです。 まあ、ステートマシンと言っても、たったの2状態ですが。 ステートマシンの攻勢は単純です。 出力が 0 の時に LUT5 にパルスが入ったら、出力を 1 に変化させます。 また、出力が 1 の時に DU にパルスが入ったら、出力を 0 に変化させます。

LUT5 は、立ち上がり検出で作られたパルス出力の開始を示す信号です。 また、 DU は、 Data Unit が出力するカウンタがゼロになった事を示す信号です。 これら2つの信号により、カウント開始からカウンタがゼロになるまでの時間、出力を 1 にしてパルスを作成します。

リセット論理

リセット論理

LUT6 は、 Data Unit のカウンタをリセットするための信号を作る組み合わせ論理です。 カウンタは、 LUT4 の出力が 0 の間はリセットされます。 また、カウンタがゼロになって DU が出力された次のサイクルでもリセットされます。 これら2つのリセット条件を持たせているのが LUT6 です。

この LUT は、組み合わせ論理回路になっていますが、これは、カウンタのリセット入力が同期リセットであるためです。

ダウンカウンタ

ダウンカウンタ

ダウンカウンタは、 DU_reg で決められた値からゼロまでカウントを行います。 このため、 LUT4 が出力パルスの幅は、 DU_reg に設定した数よりもひとつ多くなります。 ここでは、 0x05 を設定しているので、6クロックのパルスが生成されることになります。

トップレベル回路図

トップレベル回路図

この Smart I/O ブロックには、 data0 からクロックを与えているのですが、そのクロック源は PWM による 1Hz のクロックです。 1Hz で動作してくれるので、目でデバッグできます。

実際に使用する際には、適宜クロックを与えます。 また、 gpio5 と gpio6 は、デバッグのために出力させているため、必須ではありません。

ソフトウェア

ソフトウェア

テストソフトウェアは、 Pin_3_4_Write() 関数で data4 をトリガしているだけです。 LUT5 を使って立ち上がり検出をしているので、トリガパルスは、クロック周期よりも長くする必要があります。 そのため、 Pin_3_4_Write(1) と Pin_3_4_Write(0) の間にソフトウェアディレイを入れてあります。 クロックの周波数がもっと速ければ、ディレイは必要ないでしょう。

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

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

    for(;;)
    {
        /* Place your application code here. */
        Pin_3_4_Write(1u);
        CyDelay(1500);
        Pin_3_4_Write(0u);
        CyDelay(10000);
    }
}

プロジェクトアーカイブ

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


nice!(0)  コメント(0)  このエントリーを含むはてなブックマーク#

汎用レジスタの作成 [PSoC]このエントリーを含むはてなブックマーク#

インスタンス

「ペリフェラル領域に記憶域が欲しい」という要望を受けました。 レジスタが自由に使えるコンポーネントは無いかと探しましたが、面倒なので作ってしまいました。

シンボル

シンボルを作る

まずは、シンボルから。 シンボルには、クロック入力 clock だけを付けました。 データパスを使うときには、クロックは必須なんだそうです。

その他には、 Property の中で APIPrefix と CatalogPlacement を設定しました。 APIPrefix は、 API を作るときには有ったほうが良いよね。

Verilogファイル

シンボルから "Generate Verilog" で Verilog ファイルを作ります。 Datapath Config Tool を使って、 8-bit データパスを配置し、 clock だけ設定したら終了です。 データパスを配置したけれど、単なるレジスタファイルとして使うので、何も動かす必要はありません。

//`#start header` -- edit after this line, do not edit this line
// ========================================
//
// Copyright YOUR COMPANY, THE YEAR
// All Rights Reserved
// UNPUBLISHED, LICENSED SOFTWARE.
//
// CONFIDENTIAL AND PROPRIETARY INFORMATION
// WHICH IS THE PROPERTY OF your company.
//
// ========================================
`include "cypress.v"
//`#end` -- edit above this line, do not edit this line
// Generated on 07/08/2019 at 22:59
// Component: NtanRegister_v1_0
module NtanRegister_v1_0 (
	input   clock
);

//`#start body` -- edit after this line, do not edit this line

//        Your code goes here

cy_psoc3_dp #(.cy_dpconfig(
{
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM0: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM1: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM2: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM3: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM4: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM5: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM6: */
    `CS_ALU_OP_PASS, `CS_SRCA_A0, `CS_SRCB_D0,
    `CS_SHFT_OP_PASS, `CS_A0_SRC_NONE, `CS_A1_SRC_NONE,
    `CS_FEEDBACK_DSBL, `CS_CI_SEL_CFGA, `CS_SI_SEL_CFGA,
    `CS_CMP_SEL_CFGA, /*CFGRAM7: */
    8'hFF, 8'h00,  /*CFG9: */
    8'hFF, 8'hFF,  /*CFG11-10: */
    `SC_CMPB_A1_D1, `SC_CMPA_A1_D1, `SC_CI_B_ARITH,
    `SC_CI_A_ARITH, `SC_C1_MASK_DSBL, `SC_C0_MASK_DSBL,
    `SC_A_MASK_DSBL, `SC_DEF_SI_0, `SC_SI_B_DEFSI,
    `SC_SI_A_DEFSI, /*CFG13-12: */
    `SC_A0_SRC_ACC, `SC_SHIFT_SL, 1'h0,
    1'h0, `SC_FIFO1_BUS, `SC_FIFO0_BUS,
    `SC_MSB_DSBL, `SC_MSB_BIT0, `SC_MSB_NOCHN,
    `SC_FB_NOCHN, `SC_CMP1_NOCHN,
    `SC_CMP0_NOCHN, /*CFG15-14: */
    10'h00, `SC_FIFO_CLK__DP,`SC_FIFO_CAP_AX,
    `SC_FIFO_LEVEL,`SC_FIFO__SYNC,`SC_EXTCRC_DSBL,
    `SC_WRK16CAT_DSBL /*CFG17-16: */
}
)) dp(
        /*  input                   */  .reset(1'b0),
        /*  input                   */  .clk(clock),
        /*  input   [02:00]         */  .cs_addr(3'b0),
        /*  input                   */  .route_si(1'b0),
        /*  input                   */  .route_ci(1'b0),
        /*  input                   */  .f0_load(1'b0),
        /*  input                   */  .f1_load(1'b0),
        /*  input                   */  .d0_load(1'b0),
        /*  input                   */  .d1_load(1'b0),
        /*  output                  */  .ce0(),
        /*  output                  */  .cl0(),
        /*  output                  */  .z0(),
        /*  output                  */  .ff0(),
        /*  output                  */  .ce1(),
        /*  output                  */  .cl1(),
        /*  output                  */  .z1(),
        /*  output                  */  .ff1(),
        /*  output                  */  .ov_msb(),
        /*  output                  */  .co_msb(),
        /*  output                  */  .cmsb(),
        /*  output                  */  .so(),
        /*  output                  */  .f0_bus_stat(),
        /*  output                  */  .f0_blk_stat(),
        /*  output                  */  .f1_bus_stat(),
        /*  output                  */  .f1_blk_stat(),
        
        /* input                    */  .ci(1'b0),     // Carry in from previous stage
        /* output                   */  .co(),         // Carry out to next stage
        /* input                    */  .sir(1'b0),    // Shift in from right side
        /* output                   */  .sor(),        // Shift out to right side
        /* input                    */  .sil(1'b0),    // Shift in from left side
        /* output                   */  .sol(),        // Shift out to left side
        /* input                    */  .msbi(1'b0),   // MSB chain in
        /* output                   */  .msbo(),       // MSB chain out
        /* input [01:00]            */  .cei(2'b0),    // Compare equal in from prev stage
        /* output [01:00]           */  .ceo(),        // Compare equal out to next stage
        /* input [01:00]            */  .cli(2'b0),    // Compare less than in from prv stage
        /* output [01:00]           */  .clo(),        // Compare less than out to next stage
        /* input [01:00]            */  .zi(2'b0),     // Zero detect in from previous stage
        /* output [01:00]           */  .zo(),         // Zero detect out to next stage
        /* input [01:00]            */  .fi(2'b0),     // 0xFF detect in from previous stage
        /* output [01:00]           */  .fo(),         // 0xFF detect out to next stage
        /* input [01:00]            */  .capi(2'b0),   // Software capture from previous stage
        /* output [01:00]           */  .capo(),       // Software capture to next stage
        /* input                    */  .cfbi(1'b0),   // CRC Feedback in from previous stage
        /* output                   */  .cfbo(),       // CRC Feedback out to next stage
        /* input [07:00]            */  .pi(8'b0),     // Parallel data port
        /* output [07:00]           */  .po()          // Parallel data port
);
//`#end` -- edit above this line, do not edit this line
endmodule
//`#start footer` -- edit after this line, do not edit this line
//`#end` -- edit above this line, do not edit this line

コメントも何も書いてない。

APIヘッダ

ヘッダファイルには、レジスタアドレスの読み替えマクロと、関数宣言が入っています。 ここでは、 A0 と A1 の二つのレジスタしか定義していないので、 D0 と D1 が欲しい方は追加してください。

/* ========================================
 *
 * Copyright YOUR COMPANY, THE YEAR
 * All Rights Reserved
 * UNPUBLISHED, LICENSED SOFTWARE.
 *
 * CONFIDENTIAL AND PROPRIETARY INFORMATION
 * WHICH IS THE PROPERTY OF your company.
 *
 * ========================================
*/

#if !defined(`$INSTANCE_NAME`_H)
#define `$INSTANCE_NAME`_H
    
#include <cytypes.h>

#define `$INSTANCE_NAME`_A0_PTR ((reg8 *)`$INSTANCE_NAME`_dp__A0_REG)
#define `$INSTANCE_NAME`_A1_PTR ((reg8 *)`$INSTANCE_NAME`_dp__A1_REG)

extern uint8 `$INSTANCE_NAME`_ReadA0(void);
extern void `$INSTANCE_NAME`_WriteA0(uint8 value);
extern uint8 `$INSTANCE_NAME`_ReadA1(void);
extern void `$INSTANCE_NAME`_WriteA1(uint8 value);

#endif // `$INSTANCE_NAME`_H

/* [] END OF FILE */

API本体

API の本体は、関数定義を書いておしまいです。 何のことはない、 8-bit の値をデータパスのレジスタと読み書きするだけです。

/* ========================================
 *
 * Copyright YOUR COMPANY, THE YEAR
 * All Rights Reserved
 * UNPUBLISHED, LICENSED SOFTWARE.
 *
 * CONFIDENTIAL AND PROPRIETARY INFORMATION
 * WHICH IS THE PROPERTY OF your company.
 *
 * ========================================
*/
#include "`$INSTANCE_NAME`.h"

uint8 `$INSTANCE_NAME`_ReadA0(void) {
    return CY_GET_REG8(`$INSTANCE_NAME`_A0_PTR);
}
void `$INSTANCE_NAME`_WriteA0(uint8 value) {
    CY_SET_REG8(`$INSTANCE_NAME`_A0_PTR, value);
}

uint8 `$INSTANCE_NAME`_ReadA1(void) {
    return CY_GET_REG8(`$INSTANCE_NAME`_A1_PTR);
}
void `$INSTANCE_NAME`_WriteA1(uint8 value) {
    CY_SET_REG8(`$INSTANCE_NAME`_A1_PTR, value);
}

/* [] END OF FILE */

DMA Capabilityファイル

このファイルは、 DMA Wizard から参照してもらい、 DMA 制御プログラムの断片を作ります。 作りはしたけど、まだ使ってないから、誰か試してくださいな。

<?xml version="1.0" encoding="us-ascii"?>


<!--
      DMACapability needs to contain 1 or more Category tags. Category needs to contain 1 or more Location tags.
      
      Category Attributes
      ===================
  
        name:       The name of the cataegory to display to the user in the DMA Wizard. (If only one category is entered
                    it will not be displayed as a sub-category in the wizard. Instead it will just be used when the
                    user selects its associated instance.)
        
        enabled:    [OPTIONAL] "true" or "false". If not provided it defaults to true. If false, 
                    this category and its locations are not included in the DMA Wizard. Note: this value can be set 
                    to an expression referencing parameters by using `=` (e.g. `="Your Expression here"`).
        
        bytes_in_burst: Integer between 1 and 127. The number of bytes that can be sent/recieved in a single burst.
        
        bytes_in_burst_is_strict: "true" or "false". Determines whether the bytes_in_burst is a maximum value (false)
                                  or a specific value that must be used (true).
                            
        spoke_width:        Integer between 1 and 4. The spoke width in bytes. 
        
        inc_addr:           "true" or "false". Specifies whether or not the address is typically incremented.
     
        each_busrt_req_request: "true" or "false". Specifies whether or not a request is required for each burst.
     
      Location Attributes
      ===================
      
        name:      The name of the location to display to the user in the DMA Wizard.
       
        enabled:  [OPTIONAL] "true" or "false". If not provided it defaults to true. If false, this 
                  location is not included in the DMA Wizard. Note: this value can be set to an expression 
                  referencing parameters by using `=Your Expression here`.
       
        direction: "source", "destination", or "both".
  -->

<DMACapability>

  <Category name="" 
            enabled="true" 
            bytes_in_burst="1"
            bytes_in_burst_is_strict="true" 
            spoke_width="2" 
            inc_addr="false" 
            each_burst_req_request="true">
    <Location name="`$INSTANCE_NAME`_A0_PTR" enabled="true" direction="both"/>
    <Location name="`$INSTANCE_NAME`_A1_PTR" enabled="true" direction="both"/>
  </Category>
  
</DMACapability>

使い方

使い方

使い方は、簡単。 コンポーネントを回路図の上に置いて、クロックをつないだら、何も設定せずにすぐ使えます。

/* ========================================
 *
 * Copyright YOUR COMPANY, THE YEAR
 * All Rights Reserved
 * UNPUBLISHED, LICENSED SOFTWARE.
 *
 * CONFIDENTIAL AND PROPRIETARY INFORMATION
 * WHICH IS THE PROPERTY OF your company.
 *
 * ========================================
*/
#include "project.h"
#include <stdio.h>

int main(void)
{
    uint8 i;
    char sbuf[128];
    
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    UART_Start();
    UART_PutString("\r\nHELLO WORLD\r\n");
    sprintf(sbuf, "INIT A0=%d, A1=%d\r\n", PR1_ReadA0(), PR1_ReadA1());
    UART_PutString(sbuf);
    
    for (i = 0; i < 10; i++) {
        sprintf(sbuf, "WRITE A0=%d, A1=%d\r\n", i, 9-i);
        UART_PutString(sbuf);
        PR1_WriteA0(i);
        PR1_WriteA1(9-i);
        sprintf(sbuf, "READ A0=%d, A1=%d\r\n", PR1_ReadA0(), PR1_ReadA1());
        UART_PutString(sbuf);
    }    

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

/* [] END OF FILE */

API 関数を使って、レジスタの読み書きができます。 DMA とも連携するはずです。

コンポーネント

ここで作ったコンポーネントは、このリンクからダウンロードしたファイルの拡張子を cycomp に変えると Import できるようになります。


nice!(1)  コメント(0)  このエントリーを含むはてなブックマーク#

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

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

前の8件 | -