连接到 BLE 从站设备

介绍

德州仪器(TI)的 CC26XX 系列 SoC 是面向蓝牙低功耗(BLE)应用的现成无线 MCU。除了 MCU 之外,TI 还提供了一个完整的软件堆栈 ,可提供必要的 API 和示例代码,帮助开发人员快速开始使用工具链。但是,对于初学者来说,总是存在一个问题,即在一长串参考文档和代码之前从哪里开始。本说明旨在记录启动第一个项目所需的必要步骤。

简单外设配置文件是 BLE 堆栈的 Hello World 示例,其中 MCU 充当上游主机的 BLE 外设,或 BLE 服务客户端,如 PC 和智能手机。常见的现实应用包括:蓝牙耳机,蓝牙温度传感器等。

在开始之前,我们首先需要收集基本的软件和硬件工具,以便进行编程和调试。

  1. BLE 堆栈

    从官方网站下载并安装 TI 的 BLE-STACK-2-2-0。假设它安装在默认位置“C:\ ti”中。

  2. IDE - 有两种选择:

  • 适用于 ARM 的 IAR Embedded Workbench。这是一个商业工具,具有 30 天的免费评估期。

  • TI 的 Code Composer Studio(CCS)。TI 的官方 IDE 并提供免费许可。在本例中,我们将使用 CCS V6.1.3

  1. 硬件编程工具

    推荐 TI 的 XDS100 USB 接口 JTAG 器件。

在 CCS 中导入示例项目

Simple Peripheral Profile 示例代码随 BLE-Stack 安装一起提供。按照以下步骤将此示例项目导入 CCS。

  1. 启动 CCS,创建工作区文件夹。然后文件 - >导入。在选择导入源下,选择“Code Compose Studio - > CCS Projects”选项,然后单击下一步https://i.stack.imgur.com/nfUSF.jpg
  2. 浏览到’C:\ ti \ simplelink \ ble_sdk_2_02_00_31 \ examples \ cc2650em \ simple_peripheral \ ccs’。将发现两个项目。选择全部并勾选下面的两个选项。然后单击完成。通过将项目复制到工作空间,你可以保持原始项目设置不会进行以下所有修改。 https://i.stack.imgur.com/x45lU.jpg

Simple Peripheral Profile 示例包括两个项目:

  • simple_peripheral_cc2650em_app
  • simple_peripheral_cc2650em_stack

‘cc2650em’是 TI cc2650 评估板的代号。 _stack 项目包括 TI 的 BEL-Stack-2-2-0 的代码和二进制文件,它处理蓝牙广告,握手,频率同步等。这是代码的一部分,相对稳定,不想成为大部分时间都被开发人员所感动。 _app 项目是开发人员实现自己的任务和 BLE 服务的地方。

构建和下载

单击菜单’Project-> Build All’来构建两个项目。如果编译器在链接时报告某种内部错误,请尝试通过以下方式禁用链接器的’compress_dwarf’选项:

  • 右键单击该项目,然后选择 Propoerties
  • 在“Build-> ARM Linker”中,单击 Edit Flags 按钮。
  • 将最后一个选项修改为’–compress_dwarf = off’。

成功构建两个项目后,分别单击“Run-> debug”将堆栈和应用程序映像下载到 MCU。

触摸代码

为了能够对示例代码进行积极的修改,开发人员必须获得有关 BLE 堆栈的分层结构的详细知识。对于温度读取/通知等基本任务,我们只关注两个文件:PROFILES / simple_gatt_profile.c(.h) 和 Application / simple_peripheral.c(.h)

simple_gatt_profile.c

所有蓝牙应用程序都提供某种类型的服务,每种服务都包含一组特征。简单的外设配置文件定义了一个简单的服务,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 0xFFF5 特点 5 只读

这五个特征具有不同的属性,并作为各种用户案例的示例。例如,MCU 可以使用 simplePeripheralChar4 向其客户端(上游主机)通知信息的变化。

要定义蓝牙服务,必须构建属性表。

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

在注册服务时,开发人员必须为特征的读取写入授权提供三个回调函数。我们可以在示例代码中找到回调函数列表。

/*********************************************************************
 * 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。类似地,在写入请求时将调用 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 个参数,这些参数在标题注释中进行了解释。该功能首先检查属性的访问权限,例如它是否具有读取权限。然后通过测试条件’if(offset> 0)‘来检查这是否是更大的 blob 读取请求的段读取。显然,该函数暂时不支持 blob 读取。接下来,提取所请求属性的 UUID。有两种类型的 UUID:16 位和 128 位。虽然示例代码使用 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;
    }

写回调函数类似,只是存在 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

TI 的 BLE 堆栈运行在精简的多线程 OS 层之上。要向 MCU 添加工作负载,开发人员必须首先创建任务。simple_peripheral.c 演示了自定义任务的基本结构,包括任务的创建,初始化和内务管理。从温度读数和通知等基本任务开始,我们将重点关注以下几个关键功能。

文件的开头定义了一组可影响蓝牙连接行为的参数。

// 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 一起定义蓝牙连接的连接间隔,即一对设备交换信息的频率。较低的连接间隔意味着响应更快,但功耗也更高。

参数 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 通过蓝牙连接将信息传递给服务客户端。BLE 堆栈负责维护无线连接到通过蓝牙链路传输消息的所有低级别作业。所有开发人员需要做的是收集特定于应用程序的数据并将其更新为服务表中的相应特征。

最后,当对允许写入的特性执行写请求时,将引发回调函数。

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 堆栈负责通过蓝牙连接将信息从服务表传送到对等设备(反之亦然)。