Поиск…


Подключение к ведомым устройствам BLE

Вступление

Texas Instruments (TI) CC26XX серии SoCs - это легкодоступные беспроводные микроконтроллеры, предназначенные для приложений с низкой энергией Bluetooth (BLE). Наряду с MCU, TI предлагает полноценный стековый программный стек, который предоставляет необходимые коды API и образцов, чтобы быстро помочь разработчикам начать работу с цепочкой инструментов. Тем не менее, для новичков всегда возникает вопрос, где начать с длинного списка справочного документа и кодов. Эта записка направлена ​​на то, чтобы записать необходимые шаги, которые необходимо выполнить, чтобы начать первый проект.

Простой профиль периферии - это пример «Hello World» стека BLE, где MCU действует как периферийное устройство BLE для хостов, расположенных выше по потоку, или клиентов службы BLE, таких как ПК и смартфоны. Обычные приложения в реальном мире: Bluetooth-наушники, датчик температуры Bluetooth и т. Д.

Перед запуском мы сначала должны собрать базовые программные и аппаратные средства для программирования и отладки.

  1. Стек BLE

    Загрузите и установите TI BLE-STACK-2-2-0 с официального сайта. Предположим, что он установлен в папку по умолчанию 'C: \ ti'.

  2. IDE - есть два варианта:

  • IAR Embedded Workbench для ARM. Это коммерческий инструмент с 30-дневным свободным периодом оценки.

  • Студия Code Composer для TI (CCS). Официальная IDE TI и бесплатная лицензия. В этом примере мы будем использовать CCS V6.1.3

  1. Аппаратный программный инструмент

    Рекомендовать устройство JTAG для USB-интерфейса XDS100 от TI.

Пример проекта импорта в CCS

Образец кода Simple Peripheral Profile поставляется с установкой BLE-Stack. Выполните следующие шаги, чтобы импортировать этот примерный проект в CCS.

  1. Запустите CCS, создайте папку рабочего пространства. Затем File-> Import. В разделе «Выбор источника импорта» выберите «Проект сборки кода» -> «Проекты CCS» и нажмите «Далее». Импорт проекта CCS
  2. Перейдите к 'C: \ ti \ simplelink \ ble_sdk_2_02_00_31 \ examples \ cc2650em \ simple_peripheral \ ccs'. Два проекта будут обнаружены. Выделите все и отметьте оба варианта ниже. Затем нажмите «Готово». Скопировав проекты в рабочее пространство, вы оставите исходный проект без изменений для всех последующих модификаций. введите описание изображения здесь

Пример простого периферийного профиля включает два проекта:

  • simple_peripheral_cc2650em_app
  • simple_peripheral_cc2650em_stack

'cc2650em' - это кодовое имя для оценочной платы cc2650 от TI. Проект _stack включает в себя коды и двоичные файлы BEL-Stack-2-2-0 от TI, который обрабатывает рекламу Bluetooth, подтверждение связи, частотную синхронизацию и т. Д. Это часть кода, который относительно стабилен и не хочет быть которые чаще всего затрагивают разработчики. В проекте _app разработчики реализуют свои собственные задачи и службу BLE.

Создание и загрузка

Чтобы создать оба проекта, нажмите «Project-> Build All» меню. Если компилятор сообщает о какой-либо внутренней ошибке при компоновке, попробуйте отключить опцию 'compress_dwarf' для компоновщика:

  • щелкните правой кнопкой мыши проект и выберите «Propoerties».
  • в 'Build-> ARM Linker', нажмите кнопку «Редактировать флаги».
  • измените последний параметр на «--compress_dwarf = off».

После успешного создания обоих проектов нажмите «Run-> debug» отдельно, чтобы загрузить как изображения стека, так и приложения в MCU.

Коснитесь кода

Чтобы иметь возможность вносить агрессивные изменения в примерный код, разработчики должны получить подробные сведения о многоуровневой структуре стека BLE. Для элементарных задач, таких как чтение / уведомление температуры, мы можем сосредоточиться только на двух файлах: PROFILES / simple_gatt_profile.c (.h) и Application / simple_peripheral.c (.h)

simple_gatt_profile.c

Все приложения Bluetooth предлагают определенный тип обслуживания, каждый из которых состоит из набора характеристик. Простой периферийный профиль определяет один простой сервис с UUID 0xFFF0, который состоит из 5 характеристик. Эта услуга указана в файле 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 Только для чтения

Пять характеристик имеют разные свойства и служат примерами для различных случаев пользователей. Например, 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 );

При регистрации услуги разработчики должны предоставить три функции обратного вызова для «Read», «Write» и «Authorization» характеристик. В образце кода мы можем найти список функций обратного вызова.

/*********************************************************************
 * 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
};

Таким образом, simpleProfile_ReadAttrCB будет вызван после того, как клиент службы отправит запрос на чтение через соединение Bluetooth. Аналогично, simpleProfile_WriteAttrCB вызывается при выполнении запроса на запись. Понимание этих двух функций является ключом к успеху настройки проекта.

Ниже приведена функция обратного вызова.

/*********************************************************************
 * @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 параметров, которые объясняются в комментариях заголовка. Функция начинается с проверки разрешения доступа к атрибуту, например, есть ли у него разрешение на чтение. Затем он проверяет, является ли это сегментом, читаемым большим запросом на считывание BLOB, проверяя условие «if (offset> 0)». Очевидно, что функция пока не поддерживает blob. Затем извлекается UUID запрашиваемого атрибута. Существует два типа UUID: 16-бит и 128-бит. Хотя примерный код определяет все характеристики с использованием 16-разрядных UUID, 128-битный UUID является более универсальным и более широко используется в восходящих хостах, таких как ПК и смартфоны. Поэтому несколько строк кода используются для преобразования 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;
    }

Функция обратного вызова записи аналогична, за исключением того, что существует специальный тип записи с UUID из GATT_CLIENT_CHAR_CFG_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

Стек BLE TI работает поверх большого многопоточного уровня ОС. Чтобы добавить рабочую нагрузку в 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. И при тайм-ауте вызывается функция SimpleBLEPeripheral_clockHandler с параметром SBP_PERIODIC_EVT. Затем событие синхронизации может быть вызвано

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);
}

Эта функция устанавливает флаг в векторе событий и активирует приложение из списка задач ОС. Обратите внимание, что мы не выполняем какую-либо конкретную рабочую нагрузку пользователя в этой функции обратного вызова, потому что это НЕ рекомендуется. Пользовательская рабочая нагрузка часто включает вызовы API стека BLE. Выполнение вызовов API BLE-стека внутри функций обратного вызова часто приводит к системным исключениям. Вместо этого мы устанавливаем флаг в векторе событий задачи и ожидаем, что он будет обработан позже в контексте приложения. Точкой входа для примерной задачи является 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 и т. Д. Затем мы вызываем API SimpleProfile_SetParameter () для передачи информации для обслуживания клиентов через соединение 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 для выполнения любой полезной работы GPIO беспроводного MCU почти всегда задействованы. Например, чтобы считывать температуру с внешнего датчика, может потребоваться функциональность ADC для контактов GPIO. Микроконтроллер CC2640 от TI имеет максимум 31 GPIO, учитывая разные типы упаковки.

В аппаратной части CC2640 предоставляет богатый набор периферийных функций, таких как ADC, UARTS, SPI, SSI, I2C и т. Д. На стороне программного обеспечения стек BLE TI пытается предложить единый независимый от устройства интерфейс драйвера для разных периферийных устройств. Единый интерфейс драйвера может улучшить вероятность повторного использования кода, но, с другой стороны, он также увеличивает наклон кривой обучения. В этой заметке мы используем контроллер SPI в качестве примера и показываем, как интегрировать драйвер программного обеспечения в пользовательские приложения.

Базовый поток драйвера SPI

В стеке BLE TI периферийный драйвер часто состоит из трех частей: независимой от устройства спецификации API-интерфейсов драйвера; специфичную для устройства реализацию API-интерфейсов драйвера и сопоставление аппаратного ресурса.

Для контроллера SPI его реализация драйвера включает три файла:

  • <ti / drivers / SPI.h> - это независимая от устройства спецификация API
  • <ti / drivers / spi / SPICC26XXDMA.h> - это специфичная для CC2640 реализация API
  • <ti / drivers / dma / UDMACC26XX.h> - это драйвер uDMA, требуемый драйвером SPI

(Примечание: лучший документ для периферийных драйверов стека BLE TI в основном может быть найден в их заголовочных файлах, таких как SPICC26XXDMA.h в этом случае)

Чтобы начать использовать SPI-контроллер, давайте сначала создадим пользовательский файл 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 работает в режиме master с битрейтом 800 кбит / с и использует неблокирующий метод для обработки каждой транзакции, так что когда транзакция будет завершена, будет вызвана функция обратного вызова sbp_spiCallback.

Наконец, вызов SPI_open () открывает аппаратный SPI-контроллер и возвращает дескриптор для последующих транзакций SPI. SPI_open () принимает два аргумента, первый - идентификатор SPI-контроллера. CC2640 имеет два аппаратных SPI-контроллера на кристалле, поэтому эти аргументы ID будут либо 0, либо 1, как определено ниже. Второй аргумент - это требуемые параметры для контроллера 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);
}

Конфигурация контактов ввода / вывода

До сих пор кажется довольно простым в использовании драйвера SPI. Но подождите, как можно подключить вызовы API программного обеспечения к физическим SPI-сигналам? Это делается с помощью трех структур данных: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 и SPI_Config. Обычно они создаются в другом месте, например, «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. «FxnTablePtr» - это таблица точек, которая указывает на специфические для устройства реализации API-интерфейса драйвера.

«Объект» отслеживает информацию, такую ​​как состояние драйвера, режим передачи, функция обратного вызова для драйвера. Этот «объект» автоматически поддерживается драйвером.

«HwAttrs» хранит фактические данные сопоставления аппаратного ресурса, например, выводы IO для сигналов SPI, номер прерывания аппаратного обеспечения, базовый адрес контроллера SPI и т. Д. Большинство полей «hwAttrs» предварительно определены и не могут быть изменены. В то время как контакты IO интерфейса могут свободно назначаться на основе пользовательских случаев. Примечание: MCU CC26XX отделяют контакты 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