サーチ…


BLEスレーブデバイスへの接続

前書き

Texas Instruments(TI)のCC26XXシリーズSoCは、Bluetooth Low Energy(BLE)アプリケーションを対象とした、容易に利用可能なワイヤレスMCUです。 TIは、MCUとともに、ツールチェーンを使用して開発者を迅速に開始するために必要なAPIとサンプルコードを提供する本格的なソフトウェアスタックを提供しています。しかし、初心者にとっては、いつも参照文書とコードの長いリストの前でどこから始めるべきかという疑問があります。このメモは、最初のプロジェクトが進行するのに必要な手順を記録することを目的としています。

シンプルペリフェラルプロファイルは、BLEスタックの「Hello World」の例です。ここでは、MCUがアップストリームホストのBLEペリフェラル、またはPCやスマートフォンなどのBLEサービスクライアントとして動作しています。 Bluetoothヘッドフォン、Bluetooth温度センサーなど、実世界の一般的なアプリケーションがあります。

はじめに、プログラミングとデバッグのための基本的なソフトウェアとハ​​ードウェアツールを最初に収集する必要があります。

  1. BLEスタック

    TIのBLE-STACK-2-2-0を公式サイトからダウンロードしてインストールしてください。デフォルトの場所 'C:\ ti'にインストールされているものとします。

  2. IDE - 2つのオプションがあります:

  • ARM用IAR Embedded Workbenchこれは、無料の評価期間が30日間の商用ツールです。

  • TIのコード・コンポーザ・スタジオ(CCS)。 TIの公式IDEであり、無料のライセンスを提供しています。この例では、CCS V6.1.3

  1. ハードウェアプログラミングツール

    TIのXDS100 USBインタフェースJTAGデバイスを推奨します。

CCSでのサンプルプロジェクトのインポート

Simple Peripheral Profileサンプルコードには、BLE-Stackのインストールが付属しています。このサンプルプロジェクトをCCSにインポートするには、以下の手順に従ってください。

  1. CCSを起動し、ワークスペースフォルダを作成します。次に、ファイル - >インポートをクリックします。 [インポート元の選択]で、[コード作成スタジオ - > CCSプロジェクト]オプションを選択し、[次へ]をクリックします。 CCSプロジェクトのインポート
  2. 'C:\ ti \ simplelink \ ble_sdk_2_02_00_31 \ examples \ cc2650em \ simple_peripheral \ ccs'を参照してください。 2つのプロジェクトが発見されます。すべてを選択し、下の両方のオプションにチェックを入れてください。次に、「Finish」をクリックします。プロジェクトをワークスペースにコピーすると、以降のすべての変更に対して元のプロジェクト設定は変更されません。 ここに画像の説明を入力

Simple Peripheral Profileの例には、2つのプロジェクトがあります。

  • simple_peripheral_cc2650em_app
  • simple_peripheral_cc2650em_stack

'cc2650em'はTIのcc2650評価ボードのコード名です。 _stackプロジェクトには、Bluetoothの広告、ハンドシェイク、周波数同期などを処理するTIのBEL-Stack-2-2-0のコードとバイナリが含まれています。これは比較的安定していて、したくないコードの一部ですほとんどの場合、開発者が触れています。 _appプロジェクトは、開発者が独自のタスクとBLEサービスを実装する場所です。

ビルドとダウンロード

両方のプロジェクトをビルドするには、メニュー[Project-> Build All]をクリックします。コンパイラがリンク時に何らかの内部エラーを報告した場合は、リンカの 'compress_dwarf'オプションを無効にしてみてください:

  • プロジェクトを右クリックし、[Propoerties]を選択します。
  • [ビルド - > ARMリンカ]で、[フラグの編集]ボタンをクリックします。
  • 最後のオプションを '--compress_dwarf = off'に変更してください。

両方のプロジェクトが正常にビルドされたら、スタックとアプリケーションの両方のイメージをMCUにダウンロードするには、[実行 - >デバッグ]をクリックします。

コードをタッチする

サンプルコードを積極的に変更するには、開発者はBLEスタックの階層構造に関する詳細な知識を得る必要があります。温度読み取り/通知などの基本タスクの場合、PROFILES / simple_gatt_profile.c(.h)とApplication / simple_peripheral.c(.h)の2つのファイルのみに焦点を当てることができます。

simple_gatt_profile.c

すべてのBluetoothアプリケーションは特定の種類のサービスを提供し、それぞれは一連の特性で構成されています。単純な周辺プロファイルは、5つの特性からなる0xFFF0というUUIDを持つ1つの単純なサービスを定義します。このサービスはsimple_gatt_profile.cで指定されています。シンプルなサービスの概要を以下に示します。

データサイズ UUID 説明プロパティ
simplePeripheralChar1 1 0xFFF1 特性1 読み書き
simplePeripheralChar2 1 0xFFF2 特性2 読み取り専用
simplePeripheralChar3 1 0xFFF3 特性3 書き込み専用
simplePeripheralChar4 1 0xFFF4 特性4 通知する
simplePeripheralChar5 5 0xFFF5 特性5 読み取り専用

5つの特性は異なる特性を有し、様々なユーザの事例の例として役立つ。たとえば、MCUはsimplePeripheralChar4を使用して、クライアントのアップストリームホストに情報の変更について通知できます。

Bluetoothサービスを定義するには、属性テーブルを作成する必要があります。

/*********************************************************************
 * Profile Attributes - Table
 */

static gattAttribute_t simpleProfileAttrTbl[SERVAPP_NUM_ATTR_SUPPORTED] = 
{
  // Simple Profile Service
  { 
    { ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
    GATT_PERMIT_READ,                         /* permissions */
    0,                                        /* handle */
    (uint8 *)&simpleProfileService            /* pValue */
  },

    // Characteristic 1 Declaration
    { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &simpleProfileChar1Props 
    },

      // Characteristic Value 1
      { 
        { ATT_UUID_SIZE, simpleProfilechar1UUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE, 
        0, 
        &simpleProfileChar1
      },

      // Characteristic 1 User Description
      { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
            0, 
            simpleProfileChar1UserDesp 
          }, 
        ...
    };

属性テーブルは、デフォルトの 'primaryServiceUUID'で始まり、サービスのUUID(この場合は0xFFF0)を指定します。その後、サービスに含まれるすべての特性の宣言が続きます。各特性はいくつかの属性、すなわちアクセス許可、値およびユーザ記述などを有する。このテーブルは後でBLEスタックに登録される。

// Register GATT attribute list and CBs with GATT Server App
status = GATTServApp_RegisterService( simpleProfileAttrTbl, 
                                      GATT_NUM_ATTRS( simpleProfileAttrTbl ),
                                      GATT_MAX_ENCRYPT_KEY_SIZE,
                                      &simpleProfileCBs );

サービスの登録時に、開発者は特性の「読み込み」、「書き込み」、「承認」の3つのコールバック機能を提供する必要があります。サンプルコードでは、コールバック関数のリストを見つけることができます。

/*********************************************************************
 * PROFILE CALLBACKS
 */

// Simple Profile Service Callbacks
// Note: When an operation on a characteristic requires authorization and 
// pfnAuthorizeAttrCB is not defined for that characteristic's service, the 
// Stack will report a status of ATT_ERR_UNLIKELY to the client.  When an 
// operation on a characteristic requires authorization the Stack will call 
// pfnAuthorizeAttrCB to check a client's authorization prior to calling
// pfnReadAttrCB or pfnWriteAttrCB, so no checks for authorization need to be 
// made within these functions.
CONST gattServiceCBs_t simpleProfileCBs =
{
  simpleProfile_ReadAttrCB,  // Read callback function pointer
  simpleProfile_WriteAttrCB, // Write callback function pointer
  NULL                       // Authorization callback function pointer
};

したがって、サービスクライアントがBluetooth接続経由で読み取り要求を送信すると、simpleProfile_ReadAttrCBが呼び出されます。同様に、書き込み要求が行われると、simpleProfile_WriteAttrCBが呼び出されます。これらの2つの機能を理解することは、プロジェクトのカスタマイズの成功の鍵です。

以下は、読み取りコールバック関数です。

/*********************************************************************
 * @fn          simpleProfile_ReadAttrCB
 *
 * @brief       Read an attribute.
 *
 * @param       connHandle - connection message was received on
 * @param       pAttr - pointer to attribute
 * @param       pValue - pointer to data to be read
 * @param       pLen - length of data to be read
 * @param       offset - offset of the first octet to be read
 * @param       maxLen - maximum length of data to be read
 * @param       method - type of read message
 *
 * @return      SUCCESS, blePending or Failure
 */
static bStatus_t simpleProfile_ReadAttrCB(uint16_t connHandle,
                                          gattAttribute_t *pAttr,
                                          uint8_t *pValue, uint16_t *pLen,
                                          uint16_t offset, uint16_t maxLen,
                                          uint8_t method)
{
      bStatus_t status = SUCCESS;

      // If attribute permissions require authorization to read, return error
      if ( gattPermitAuthorRead( pAttr->permissions ) )
      {
        // Insufficient authorization
        return ( ATT_ERR_INSUFFICIENT_AUTHOR );
      }

      // Make sure it's not a blob operation (no attributes in the profile are long)
      if ( offset > 0 )
      {
        return ( ATT_ERR_ATTR_NOT_LONG );
      }

      uint16 uuid = 0;
      if ( pAttr->type.len == ATT_UUID_SIZE )
        // 128-bit UUID
        uuid = BUILD_UINT16( pAttr->type.uuid[12], pAttr->type.uuid[13]);
      else
        uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);

        switch ( uuid )
        {
          // No need for "GATT_SERVICE_UUID" or "GATT_CLIENT_CHAR_CFG_UUID" cases;
          // gattserverapp handles those reads

          // characteristics 1 and 2 have read permissions
          // characteritisc 3 does not have read permissions; therefore it is not
          //   included here
          // characteristic 4 does not have read permissions, but because it
          //   can be sent as a notification, it is included here
          case SIMPLEPROFILE_CHAR2_UUID:
              *pLen = SIMPLEPROFILE_CHAR2_LEN;
              VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR2_LEN );
              break;

          case SIMPLEPROFILE_CHAR1_UUID:
              *pLen = SIMPLEPROFILE_CHAR1_LEN;
              VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR1_LEN );
              break;

          case SIMPLEPROFILE_CHAR4_UUID:
            *pLen = SIMPLEPROFILE_CHAR4_LEN;
            VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR4_LEN );
            break;

          case SIMPLEPROFILE_CHAR5_UUID:
            *pLen = SIMPLEPROFILE_CHAR5_LEN;
            VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR5_LEN );
            break;

          default:
            // Should never get here! (characteristics 3 and 4 do not have read permissions)
            *pLen = 0;
            status = ATT_ERR_ATTR_NOT_FOUND;
            break;
        }

      return ( status );
}

私は元のバージョンからコードを少し変更しました。この関数は、ヘッダーコメントで説明されている7つのパラメーターを取ります。この関数は、属性のアクセス許可をチェックすることによって開始します。たとえば、読み取り許可があるかどうかです。次に、if(offset> 0)という条件をテストすることによって、これがより大きなBLOB読み取り要求のセグメント読み取りであるかどうかをチェックします。当然のことながら、この関数はBLOBの読み込みをサポートしていません。次に、要求された属性のUUIDが抽出されます。 UUIDには、16ビットと128ビットの2種類があります。サンプルコードでは16ビットのUUIDを使用してすべての特性を定義していますが、128ビットのUUIDはPCやスマートフォンなどのアップストリームホストでより一般的でよく使用されています。したがって、128ビットのUUIDを16ビットのUUIDに変換するために、数行のコードが使用されます。

 uint16 uuid = 0;
  if ( pAttr->type.len == ATT_UUID_SIZE )
    // 128-bit UUID
    uuid = BUILD_UINT16( pAttr->type.uuid[12], pAttr->type.uuid[13]);
  else
    uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);

最後に、UUIDを取得した後、どの属性が要求されているかを判断できます。開発者側の残りの仕事は、要求された属性の値を宛先ポインタ 'pValue'にコピーすることです。

switch ( uuid )
    {          
      case SIMPLEPROFILE_CHAR1_UUID:
          *pLen = SIMPLEPROFILE_CHAR1_LEN;
          VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR1_LEN );
          break;

      case SIMPLEPROFILE_CHAR2_UUID:
          *pLen = SIMPLEPROFILE_CHAR2_LEN;
          VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR2_LEN );
          break;

      case SIMPLEPROFILE_CHAR4_UUID:
        *pLen = SIMPLEPROFILE_CHAR4_LEN;
        VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR4_LEN );
        break;

      case SIMPLEPROFILE_CHAR5_UUID:
        *pLen = SIMPLEPROFILE_CHAR5_LEN;
        VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR5_LEN );
        break;

      default:
        *pLen = 0;
        status = ATT_ERR_ATTR_NOT_FOUND;
        break;
    }

書き込みコールバック関数は、GATT_CLIENT_CHAR_CFG_UUIDのUUIDを持つ特別なタイプの書き込みがある点を除いて同様です。これは、上流のホストからの特性通知または通知のための登録要求です。 API GATTServApp_ProcessCCCWriteReqを呼び出して、要求をBLEスタックに渡すだけです。

case GATT_CLIENT_CHAR_CFG_UUID:
        status = GATTServApp_ProcessCCCWriteReq( connHandle, pAttr, pValue, len,
                                                 offset, GATT_CLIENT_CFG_NOTIFY | GATT_CLIENT_CFG_INDICATE ); // allow client to request notification or indication features
        break;

MCU上のコードのアプリケーション側は、書き込み許可の特性への変更を通知する必要がある場合があります。開発者はこの通知を好きなように実装できます。サンプルコードでは、コールバック関数が使用されています。

  // If a charactersitic value changed then callback function to notify application of change
  if ( (notifyApp != 0xFF ) && simpleProfile_AppCBs && simpleProfile_AppCBs->pfnSimpleProfileChange )
  {
    simpleProfile_AppCBs->pfnSimpleProfileChange( notifyApp );
  }

一方、BLEペリフェラルがアップストリームホストにその特性の変更を通知したい場合は、API GATTServApp_ProcessCharCfgを呼び出すことができます。このAPIはSimpleProfile_SetParameter関数で説明されています。

/*********************************************************************
 * @fn      SimpleProfile_SetParameter
 *
 * @brief   Set a Simple Profile parameter.
 *
 * @param   param - Profile parameter ID
 * @param   len - length of data to write
 * @param   value - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate 
 *          data type (example: data type of uint16 will be cast to 
 *          uint16 pointer).
 *
 * @return  bStatus_t
 */
bStatus_t SimpleProfile_SetParameter( uint8 param, uint8 len, void *value )
{
      bStatus_t ret = SUCCESS;
      switch ( param )
      {
        case SIMPLEPROFILE_CHAR2:
          if ( len == SIMPLEPROFILE_CHAR2_LEN )
          {
            VOID memcpy( simpleProfileChar2, value, SIMPLEPROFILE_CHAR2_LEN );

          }
          else
          {
            ret = bleInvalidRange;
          }
          break;

        case SIMPLEPROFILE_CHAR3:
          if ( len == sizeof ( uint8 ) )
          {
            simpleProfileChar3 = *((uint8*)value);
          }
          else
          {
            ret = bleInvalidRange;
          }
          break;

        case SIMPLEPROFILE_CHAR1:
          if ( len == SIMPLEPROFILE_CHAR1_LEN )
          {
              VOID memcpy( simpleProfileChar1, value, SIMPLEPROFILE_CHAR1_LEN );
          }
          else
          {
            ret = bleInvalidRange;
          }
          break;

        case SIMPLEPROFILE_CHAR4:
          if ( len == SIMPLEPROFILE_CHAR4_LEN )
          {
            //simpleProfileChar4 = *((uint8*)value);
            VOID memcpy( simpleProfileChar4, value, SIMPLEPROFILE_CHAR4_LEN );
            // See if Notification has been enabled
            GATTServApp_ProcessCharCfg( simpleProfileChar4Config, simpleProfileChar4, FALSE,
                                        simpleProfileAttrTbl, GATT_NUM_ATTRS( simpleProfileAttrTbl ),
                                        INVALID_TASK_ID, simpleProfile_ReadAttrCB );
          }
          else
          {
            ret = bleInvalidRange;
          }
          break;

        case SIMPLEPROFILE_CHAR5:
          if ( len == SIMPLEPROFILE_CHAR5_LEN )
          {
            VOID memcpy( simpleProfileChar5, value, SIMPLEPROFILE_CHAR5_LEN );
          }
          else
          {
            ret = bleInvalidRange;
          }
          break;

        default:
          ret = INVALIDPARAMETER;
          break;
      }

      return ( ret );
}

したがって、シンプルなペリフェラルアプリケーションがSIMPLEPROFILE_CHAR4の現在の値をピアデバイスに通知したい場合、単純なSimpleProfile_SetParameter関数を呼び出すことができます。

要約すると、PROFILES / simple_gatt_profile.c(.h)は、BLEペリフェラルがクライアントに提示したいサービスの内容と、サービス内のそれらの特性にアクセスする方法を定義します。

simple_peripheral.c

TIのBLEスタックは、ライトマルチスレッドOSレイヤの上で実行されています。 MCUにワークロードを追加するには、まず開発者がタスクを作成する必要があります。 simple_peripheral.cは、タスクの作成、初期化、ハウスキーピングを含むカスタムタスクの基本構造を示します。温度の読み取りや通知などの基本的な作業から始めて、以下のいくつかの重要な機能に焦点を当てます。

ファイルの先頭には、Bluetooth接続の動作に影響を与えるパラメータのセットが定義されています。

// Advertising interval when device is discoverable (units of 625us, 160=100ms)
#define DEFAULT_ADVERTISING_INTERVAL          160

// Limited discoverable mode advertises for 30.72s, and then stops
// General discoverable mode advertises indefinitely
#define DEFAULT_DISCOVERABLE_MODE             GAP_ADTYPE_FLAGS_GENERAL

// Minimum connection interval (units of 1.25ms, 80=100ms) if automatic
// parameter update request is enabled
#define DEFAULT_DESIRED_MIN_CONN_INTERVAL     80

// Maximum connection interval (units of 1.25ms, 800=1000ms) if automatic
// parameter update request is enabled
#define DEFAULT_DESIRED_MAX_CONN_INTERVAL     400

// Slave latency to use if automatic parameter update request is enabled
#define DEFAULT_DESIRED_SLAVE_LATENCY         0

// Supervision timeout value (units of 10ms, 1000=10s) if automatic parameter
// update request is enabled
#define DEFAULT_DESIRED_CONN_TIMEOUT          1000

// Whether to enable automatic parameter update request when a connection is
// formed
#define DEFAULT_ENABLE_UPDATE_REQUEST         TRUE

// Connection Pause Peripheral time value (in seconds)
#define DEFAULT_CONN_PAUSE_PERIPHERAL         6

// How often to perform periodic event (in msec)
#define SBP_PERIODIC_EVT_PERIOD               1000

パラメータDEFAULT_DESIRED_MIN_CONN_INTERVAL、DEFAULT_DESIRED_MAX_CONN_INTERVAL、およびDEFAULT_DESIRED_SLAVE_LATENCYは、Bluetooth接続の接続間隔を定義します。これは、一対のデバイスが情報を交換する頻度です。接続間隔を短くすると反応が速くなりますが、消費電力も高くなります。

パラメータDEFAULT_DESIRED_CONN_TIMEOUTは、接続が失われたとみなされる前にピア応答を受信する期間を定義します。パラメータDEFAULT_ENABLE_UPDATE_REQUESTは、スレーブデバイスが実行時に接続間隔を変更できるかどうかを定義します。ビジーアイドルフェーズとアイドルフェーズのための異なる接続パラメータを持つことは省電力の点で有用です。

パラメータSBP_PERIODIC_EVT_PERIODは、タスクが関数呼び出しを定期的に実行できるようにするクロックイベントの期間を定義します。これは、温度を読み取ってサービスクライアントに通知するためのコードを追加するのに最適な場所です。

定期クロックはSimpleBLEPeripheral_init関数で開始されます。

  // Create one-shot clocks for internal periodic events.
  Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
                      SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);

これにより、期間がSBP_PERIODIC_EVT_PERIODのクロックが作成されます。タイムアウトすると、パラメータSBP_PERIODIC_EVTを持つSimpleBLEPeripheral_clockHandlerの関数が呼び出されます。その後、クロックイベントは

Util_startClock(&periodicClock);

Util_startClockキーワードを検索すると、この定期クロックがGAPROLE_CONNECTEDイベント(SimpleBLEPeripheral_processStateChangeEvt関数内)で最初にトリガされることがわかります。これは、タスクがホストとの接続を確立すると、定期的なルーチンを開始することを意味します。

定期クロックがタイムアウトすると、登録されたコールバック関数が呼び出されます。

/*********************************************************************
 * @fn      SimpleBLEPeripheral_clockHandler
 *
 * @brief   Handler function for clock timeouts.
 *
 * @param   arg - event type
 *
 * @return  None.
 */
static void SimpleBLEPeripheral_clockHandler(UArg arg)
{
  // Store the event.
  events |= arg;

  // Wake up the application.
  Semaphore_post(sem);
}

この関数は、イベントベクトルにフラグを設定し、OSタスクリストからアプリケーションを起動します。推奨されていないため、このコールバック関数では特定のユーザーワークロードは行いません。ユーザーワークロードには、しばしばBLEスタックAPIの呼び出しが含まれます。 コールバック関数内でBLEスタックAPIコールを実行すると、システム例外が発生することがよくあります。代わりに、タスクのイベントベクタにフラグを設定し、それがアプリケーションコンテキストの後半で処理されるのを待ちます。サンプルタスクのエントリポイントはsimpleBLEPeripheral_taskFxn()です。

/*********************************************************************
 * @fn      SimpleBLEPeripheral_taskFxn
 *
 * @brief   Application task entry point for the Simple BLE Peripheral.
 *
 * @param   a0, a1 - not used.
 *
 * @return  None.
 */
static void SimpleBLEPeripheral_taskFxn(UArg a0, UArg a1)
{
  // Initialize application
  SimpleBLEPeripheral_init();

  // Application main loop
  for (;;)
  {
    // Waits for a signal to the semaphore associated with the calling thread.
    // Note that the semaphore associated with a thread is signaled when a
    // message is queued to the message receive queue of the thread or when
    // ICall_signal() function is called onto the semaphore.
    ICall_Errno errno = ICall_wait(ICALL_TIMEOUT_FOREVER);

    if (errno == ICALL_ERRNO_SUCCESS)
    {
      ICall_EntityID dest;
      ICall_ServiceEnum src;
      ICall_HciExtEvt *pMsg = NULL;

      if (ICall_fetchServiceMsg(&src, &dest,
                                (void **)&pMsg) == ICALL_ERRNO_SUCCESS)
      {
        uint8 safeToDealloc = TRUE;

        if ((src == ICALL_SERVICE_CLASS_BLE) && (dest == selfEntity))
        {
          ICall_Stack_Event *pEvt = (ICall_Stack_Event *)pMsg;

          // Check for BLE stack events first
          if (pEvt->signature == 0xffff)
          {
            if (pEvt->event_flag & SBP_CONN_EVT_END_EVT)
            {
              // Try to retransmit pending ATT Response (if any)
              SimpleBLEPeripheral_sendAttRsp();
            }
          }
          else
          {
            // Process inter-task message
            safeToDealloc = SimpleBLEPeripheral_processStackMsg((ICall_Hdr *)pMsg);
          }
        }

        if (pMsg && safeToDealloc)
        {
          ICall_freeMsg(pMsg);
        }
      }

      // If RTOS queue is not empty, process app message.
      while (!Queue_empty(appMsgQueue))
      {
        sbpEvt_t *pMsg = (sbpEvt_t *)Util_dequeueMsg(appMsgQueue);
        if (pMsg)
        {
          // Process message.
          SimpleBLEPeripheral_processAppMsg(pMsg);

          // Free the space from the message.
          ICall_free(pMsg);
        }
      }
    }
    
    if (events & SBP_PERIODIC_EVT)
    {
      events &= ~SBP_PERIODIC_EVT;

      Util_startClock(&periodicClock);

      // Perform periodic application task
      SimpleBLEPeripheral_performPeriodicTask();
    }

  }
}

これは、タスクのスタックおよびアプリケーションメッセージキューをポーリングし続ける無限ループです。また、さまざまなフラグのイベントベクタもチェックします。そこでは定期的なルーチンが実際に実行されます。 SBP_PERIODIC_EVTが検出されると、タスク関数は最初にフラグをクリアし、直ちに同じタイマーを開始し、ルーチン関数SimpleBLEPeripheral_performPeriodicTask()を呼び出します。

/*********************************************************************
 * @fn      SimpleBLEPeripheral_performPeriodicTask
 *
 * @brief   Perform a periodic application task. This function gets called
 *          every five seconds (SBP_PERIODIC_EVT_PERIOD). In this example,
 *          the value of the third characteristic in the SimpleGATTProfile
 *          service is retrieved from the profile, and then copied into the
 *          value of the fourth characteristic.
 *
 * @param   None.
 *
 * @return  None.
 */
static void SimpleBLEPeripheral_performPeriodicTask(void)
{
      uint8_t newValue[SIMPLEPROFILE_CHAR4_LEN];
      // user codes to do specific work like reading the temperature
      // .....
      SimpleProfile_SetParameter(SIMPLEPROFILE_CHAR4, SIMPLEPROFILE_CHAR4_LEN,
                               newValue);
}

定期的な関数の中では、温度を読み取ってUARTリクエストなどを生成するという特別な作業を行います。次に、SimpleProfile_SetParameter()APIを呼び出して、Bluetooth接続を介してサービスクライアントに情報を伝えます。 BLEスタックは、低レベルのすべてのジョブが、ワイヤレス接続を維持してから、Bluetoothリンクを介してメッセージを送信するまで処理します。すべての開発者が行う必要があるのは、アプリケーション固有のデータを収集し、それをサービス表の対応する特性に更新することです。

最後に、書込み要求が書込み許容特性で実行されると、コールバック関数が呼び出されます。

static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
  SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}

ここでも、このコールバック関数は、ユーザータスクのアプリケーションメッセージをエンキューするだけです。このメッセージは、後でアプリケーションコンテキストで処理されます。

static void SimpleBLEPeripheral_processCharValueChangeEvt(uint8_t paramID)
{
  uint8_t newValue[SIMPLEPROFILE_CHAR1_LEN];

  switch(paramID)
  {
    case SIMPLEPROFILE_CHAR1:
      SimpleProfile_GetParameter(SIMPLEPROFILE_CHAR1, &newValue[0]);
      ProcessUserCmd(newValue[0], NULL);
      break;

    case SIMPLEPROFILE_CHAR3:
      break;

    default:
      // should not reach here!
      break;
  }
}

上記の例では、SIMPLEPROFILE_CHAR1が書き込まれると、ユーザーコードはSimpleProfile_GetParameter()を呼び出して新しい値を最初に取得し、ユーザー定義コマンドのデータを解析します。

要約すると、simple_peripheral.cは、カスタム作業負荷のためのユーザー・タスクを作成する方法の例を示しています。アプリケーションのワークロードをスケジュールする基本的な方法は、定期的なクロックイベントによるものです。開発者は、サービステーブルの特性との間で情報を処理する必要がありますが、BLEスタックは、Bluetooth接続を介して、サービステーブルからピアデバイスに情報を伝達する(またはその逆)情報を処理します。

実世界センサーの接続

BLEスレーブデバイスが有用な仕事をするためには、ワイヤレスMCUのGPIOはほとんど常に関与しています。例えば、外部センサから温度を読み取るには、GPIOピンのADC機能が必要な場合があります。 TIのCC2640 MCUは、異なるパッケージング・タイプで最大31個のGPIOを備えています。

ハードウェア側では、ADC、UARTS、SPI、SSI、I2Cなどの豊富なペリフェラル機能を提供します。ソフトウェア側では、TIのBLEスタックは、さまざまなペリフェラルに対して、デバイスに依存しない一様なドライバ・インターフェイスを提供しようとします。均一なドライバインタフェースは、コードの再利用可能性を向上させる可能性がありますが、一方、学習曲線の傾きも増加します。このメモでは、例としてSPIコントローラを使用して、ソフトウェアドライバをユーザアプリケーションに統合する方法を示します。

基本的なSPIドライバの流れ

TIのBLEスタックでは、周辺デバイスドライバは、ドライバAPIのデバイスに依存しない仕様、ドライバAPIのデバイス特有の実装およびハードウェアリソースのマッピングを含む。

SPIコントローラの場合、ドライバの実装には3つのファイルが含まれます。

  • <ti / drivers / SPI.h> - これはデバイスに依存しないAPI仕様です
  • <ti / drivers / spi / SPICC26XXDMA.h> - これはCC2640固有のAPI実装です
  • <ti / drivers / dma / UDMACC26XX.h> - これはSPIドライバが必要とするuDMAドライバです

(注:TIのBLEスタックのペリフェラルドライバに関する最善のドキュメントは、ほとんどの場合、この場合はSPICC26XXDMA.hなどのヘッダーファイルにあります)

SPIコントローラの使用を開始するには、まず上記の3つのヘッダファイルを含むカスタムcファイル、つまりsbp_spi.cを作成しましょう。自然な次のステップは、ドライバのインスタンスを作成して起動することです。ドライバインスタンスは、SPI_Handleというデータ構造体にカプセル化されています。別のデータ構造 - SPI_Paramsは、ビットレート、転送モードなど、SPIコントローラのキーパラメータを指定するために使用されます。

#include <ti/drivers/SPI.h>
#include <ti/drivers/spi/SPICC26XXDMA.h>
#include <ti/drivers/dma/UDMACC26XX.h>

static void sbp_spiInit();

static SPI_Handle spiHandle;
static SPI_Params spiParams;

void sbp_spiInit(){
    SPI_init();
    SPI_Params_init(&spiParams);
    spiParams.mode                     = SPI_MASTER;
    spiParams.transferMode             = SPI_MODE_CALLBACK;
    spiParams.transferCallbackFxn      = sbp_spiCallback;
    spiParams.bitRate                  = 800000;
    spiParams.frameFormat              = SPI_POL0_PHA0;
    spiHandle = SPI_open(CC2650DK_7ID_SPI0, &spiParams);
}

上記のサンプルコードは、SPI_Handleインスタンスを初期化する方法の例を示しています。内部データ構造を初期化するには、API SPI_init()を最初に呼び出す必要があります。関数呼び出しSPI_Params_init(&spiParams)は、SPI_Params構造体のすべてのフィールドをデフォルト値に設定します。開発者は、特定のケースに合わせてキーパラメータを変更できます。たとえば、上記のコードでは、SPIコントローラをビットレート800kbpsのマスターモードで動作するように設定し、非ブロッキングメソッドを使用して各トランザクションを処理するので、トランザクションが完了するとコールバック関数sbp_spiCallbackが呼び出されます。

最後に、SPI_open()を呼び出すと、ハードウェアSPIコントローラが開き、後で実行されるSPIトランザクションのハンドルが返されます。 SPI_open()は2つの引数をとります。最初はSPIコントローラのIDです。 CC2640には2つのハードウェアSPIコントローラが内蔵されているため、このID引数は下に定義するように0または1のいずれかになります。 2番目の引数は、SPIコントローラの望ましいパラメータです。

/*!
 *  @def    CC2650DK_7ID_SPIName
 *  @brief  Enum of SPI names on the CC2650 dev board
 */
typedef enum CC2650DK_7ID_SPIName {
    CC2650DK_7ID_SPI0 = 0,
    CC2650DK_7ID_SPI1,
    CC2650DK_7ID_SPICOUNT
} CC2650DK_7ID_SPIName;

SPI_Handleのオープンが成功した後、開発者はSPIトランザクションを直ちに開始することができます。各SPIトランザクションは、データ構造(SPI_Transaction)を使用して記述されます。

/*!
 *  @brief
 *  A ::SPI_Transaction data structure is used with SPI_transfer(). It indicates
 *  how many ::SPI_FrameFormat frames are sent and received from the buffers
 *  pointed to txBuf and rxBuf.
 *  The arg variable is an user-definable argument which gets passed to the
 *  ::SPI_CallbackFxn when the SPI driver is in ::SPI_MODE_CALLBACK.
 */
typedef struct SPI_Transaction {
    /* User input (write-only) fields */
    size_t     count;      /*!< Number of frames for this transaction */
    void      *txBuf;      /*!< void * to a buffer with data to be transmitted */
    void      *rxBuf;      /*!< void * to a buffer to receive data */
    void      *arg;        /*!< Argument to be passed to the callback function */

    /* User output (read-only) fields */
    SPI_Status status;     /*!< Status code set by SPI_transfer */

    /* Driver-use only fields */
} SPI_Transaction;

たとえば、SPIバス上で書き込みトランザクションを開始するには、送信するデータが入った「txBuf」を用意し、送信するデータバイトの長さに 'count'変数を設定する必要があります。最後に、SPI_transfer(spiHandle、spiTrans)を呼び出すとSPIコントローラに信号が送られ、トランザクションが開始されます。

static SPI_Transaction spiTrans;
bool sbp_spiTransfer(uint8_t len, uint8_t * txBuf, uint8_t rxBuf, uint8_t * args)
{    
    spiTrans.count = len;
    spiTrans.txBuf = txBuf;
    spiTrans.rxBuf = rxBuf;
    spiTrans.arg   = args;

    return SPI_transfer(spiHandle, &spiTrans);
}

SPIは送信と受信の両方が同時に発生するデュプレックスプロトコルであるため、書き込みトランザクションが終了すると、対応する応答データはすでに 'rxBuf'で利用可能です。

転送モードをコールバックモードに設定するので、トランザクションが完了するたびに、登録されたコールバック関数が呼び出されます。ここで、私たちは応答データを処理したり、次のトランザクションを開始したりします。 (注:コールバック関数内で必要なAPI呼び出し以上のことをしないでください)。

void sbp_spiCallback(SPI_Handle handle, SPI_Transaction * transaction){
    uint8_t * args = (uint8_t *)transaction->arg;
    
    // may want to disable the interrupt first
    key = Hwi_disable();    
    if(transaction->status == SPI_TRANSFER_COMPLETED){
        // do something here for successful transaction...
    }
    Hwi_restore(key);
}

I / Oピンの構成

今のところ、SPIドライバを使用するのはかなり簡単です。しかし、ソフトウェアAPI呼び出しを物理SPI信号にどのように接続すればよいでしょうか?これは、SPICC26XXDMA_Object、SPICC26XXDMA_HWAttrsV1、およびSPI_Configの3つのデータ構造によって行われます。通常、 'board.c'のような別の場所でインスタンス化されます。

/* SPI objects */
SPICC26XXDMA_Object spiCC26XXDMAObjects[CC2650DK_7ID_SPICOUNT];

/* SPI configuration structure, describing which pins are to be used */
const SPICC26XXDMA_HWAttrsV1 spiCC26XXDMAHWAttrs[CC2650DK_7ID_SPICOUNT] = {
    {
        .baseAddr           = SSI0_BASE,
        .intNum             = INT_SSI0_COMB,
        .intPriority        = ~0,
        .swiPriority        = 0,
        .powerMngrId        = PowerCC26XX_PERIPH_SSI0,
        .defaultTxBufValue  = 0,
        .rxChannelBitMask   = 1<<UDMA_CHAN_SSI0_RX,
        .txChannelBitMask   = 1<<UDMA_CHAN_SSI0_TX,
        .mosiPin            = ADC_MOSI_0,
        .misoPin            = ADC_MISO_0,
        .clkPin             = ADC_SCK_0,
        .csnPin             = ADC_CSN_0
    },
    {
        .baseAddr           = SSI1_BASE,
        .intNum             = INT_SSI1_COMB,
        .intPriority        = ~0,
        .swiPriority        = 0,
        .powerMngrId        = PowerCC26XX_PERIPH_SSI1,
        .defaultTxBufValue  = 0,
        .rxChannelBitMask   = 1<<UDMA_CHAN_SSI1_RX,
        .txChannelBitMask   = 1<<UDMA_CHAN_SSI1_TX,
        .mosiPin            = ADC_MOSI_1,
        .misoPin            = ADC_MISO_1,
        .clkPin             = ADC_SCK_1,
        .csnPin             = ADC_CSN_1
    }
};

/* SPI configuration structure */
const SPI_Config SPI_config[] = {
    {
         .fxnTablePtr = &SPICC26XXDMA_fxnTable,
         .object      = &spiCC26XXDMAObjects[0],
         .hwAttrs     = &spiCC26XXDMAHWAttrs[0]
    },
    {
         .fxnTablePtr = &SPICC26XXDMA_fxnTable,
         .object      = &spiCC26XXDMAObjects[1],
         .hwAttrs     = &spiCC26XXDMAHWAttrs[1]
    },
    {NULL, NULL, NULL}
};

SPI_Configアレイには、ハードウェアSPIコントローラごとに別々のエントリがあります。各エントリには、fxnTablePtr、object、およびhwAttrsという3つのフィールドがあります。 'fxnTablePtr'は、ドライバAPIのデバイス固有の実装を指すポイントテーブルです。

「オブジェクト」は、ドライバーの状態、転送モード、ドライバーのコールバック関数などの情報を追跡します。この「オブジェクト」はドライバによって自動的に管理されます。

'hwAttrs'は、実際のハードウェアリソースマッピングデータ、例えばSPI信号のIOピン、ハードウェア割り込み番号、SPIコントローラのベースアドレスなどを格納します。 'hwAttrs'のほとんどのフィールドはあらかじめ定義されており、変更できません。インターフェイスのIOピンは、ユーザのケースに基づいて自由に割り当てることができます。 注:CC26XX MCUは、IOピンのいずれかを周辺機能に割り当てることができる特定の周辺機能からIOピンをデカップリングします。

もちろん、実際のIOピンは 'board.h'で最初に定義する必要があります。

#define ADC_CSN_1                             IOID_1
#define ADC_SCK_1                             IOID_2
#define ADC_MISO_1                            IOID_3
#define ADC_MOSI_1                            IOID_4
#define ADC_CSN_0                             IOID_5
#define ADC_SCK_0                             IOID_6
#define ADC_MISO_0                            IOID_7
#define ADC_MOSI_0                            IOID_8

その結果、ハードウェアリソースマッピングを設定した後、開発者はSPIインタフェースを介して外部センサチップと最終的に通信することができます。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow