“NRF52832DK协议栈实验”的版本间的差异

来自谷雨文档中心
跳转至: 导航搜索
硬件说明
实验现象
 
(未显示2个用户的16个中间版本)
第1行: 第1行:
=== 蓝牙协议简介 ===
 
 
==== 简介 ====
 
 
我们的蓝牙协议实验部分,将会给大家带来最直观的蓝牙协议部分的学习,我们通过拆分的方式,带领大家深入了解蓝牙协议的主要功能部分。
 
我们的蓝牙协议实验部分,将会给大家带来最直观的蓝牙协议部分的学习,我们通过拆分的方式,带领大家深入了解蓝牙协议的主要功能部分。
  
第52行: 第49行:
 
|MTU大小⑤
 
|MTU大小⑤
 
|
 
|
|}
+
|}{{Note|text=实验源码位于百度云盘归档资料中:归档资料/1-协议栈SDK/谷雨实验源码包/,代码的使用请参考《NRF52832DK入门手册》的蓝牙协议栈SDK一节|type=tips}}
  
 
==== 扫描参数① ====
 
==== 扫描参数① ====
第233行: 第230行:
 
|主机MTU大小配置实验
 
|主机MTU大小配置实验
 
|-
 
|-
|1.9_ble_central_profile_nus
+
|1.9_ble_central_profile_led
 +
|主机服务Client实验(Write/Read属性)
 +
|-
 +
|1.0_ble_central_profile_btn
 +
|主机服务Client实验(Notify属性)
 +
|-
 +
|1.11_ble_central_profile_nus
 
|主机获取NUS服务实验
 
|主机获取NUS服务实验
 
|-
 
|-
|1.10_ble_central_nus_communication
+
|1.12_ble_central_nus_communication
 
|主机利用NUS服务收发通信实验
 
|主机利用NUS服务收发通信实验
 
|-
 
|-
第266行: 第269行:
 
|从机MTU大小配置实验
 
|从机MTU大小配置实验
 
|-
 
|-
|2.9_ble_peripheral_profile_nus
+
|2.9_ble_peripheral_profile_led
 +
|从机服务Server实验(Write属性)
 +
|-
 +
|2.10_ble_peripheral_profile_btn
 +
|从机服务Server实验(Notify属性)
 +
|-
 +
|2.11_ble_peripheral_profile_nus
 
|从机注册NUS服务实验
 
|从机注册NUS服务实验
 
|-
 
|-
|2.10_ble_peripheral_nus_communication
+
|2.12_ble_peripheral_nus_communication
 
|从机利用NUS服务收发通信实验
 
|从机利用NUS服务收发通信实验
 
|}
 
|}
第555行: 第564行:
 
==== 实验现象 ====
 
==== 实验现象 ====
 
我们打开J-Link RTT Viewer,选择我们的Jlink仿真器,可以看到log打印如下。
 
我们打开J-Link RTT Viewer,选择我们的Jlink仿真器,可以看到log打印如下。
[[文件:Nrf rtt 11.png|边框|居中|无框|656x656像素]]
+
[[文件:Nrf rtt 11.png|边框|居中|无框|656x656像素]]{{Note|text=注意:如果设置了输出,但是RTT中并没有打印,可以尝试关闭RTT Viewer软件重连,另外需要检查sdk_config.h中的NRF_LOG_ENABLED 已时能,即 #define NRF_LOG_ENABLED 1|type=info}}
  
 
==== 工程及源码讲解 ====
 
==== 工程及源码讲解 ====
第2,233行: 第2,242行:
  
 
4、当主从机配置的MTU大小不同时,我们以小的数值作准。
 
4、当主从机配置的MTU大小不同时,我们以小的数值作准。
===LED控制实验===
+
===Write/Read属性服务实验===
 
====实验简介====
 
====实验简介====
我们的蓝牙实战实验将从蓝牙控制IO输出高低电平,以此来控制开发板上LED点亮和熄灭开始。
+
通过前面实验的学习,我们已经能够完成主机成功连接从机,并且可以配置我们想要的连接参数(连接间隔、MTU等)。所以有了前面的铺垫,从这一章节开始,我们会给大家讲解如何进行主从机的通信。本来我们应该给大家继续在串口透传的程序基础上继续添加服务功能,但由于NUS服务相对比较复杂,所以我们会插入两篇章节(本章节及下一章节),分别给大家讲解LED的Write属性服务,以及BTN的Notify属性服务,然后再切回串口透传例程继续讲解,方便大家学习。
 +
 
 +
谈到通信,就不得不给大家介绍一个名叫“GATT”的好同志,因为就是他帮我们管理蓝牙的通信,那么对于GATT而言,当两个设备成功连接之后,他们分别作为一下两个设备之一:
 +
 
 +
⊙GATT服务器:包含特性数据库的设备(比如控制LED数据的是LED特性,传输UART数据的是UART特性)。可以通过一个GATT客户端写入或者读取数据
 +
 
 +
⊙GATT客户端:向GATT服务器写入数据或者读取数据的设备
 +
 
 +
其中我们的从机设备一般是作为GATT服务器去提供服务的,主机设备作为GATT客户端去向服务的特性数据库中写入或者读取数据。
 +
 
 +
{{Note|text=GATT特征值属性包含如下4个:
 +
 
 +
Write(写)、Read(读)、Notify(通知)、Indicate(暗示)
 +
 
 +
这4个属性当中,其中大家最常用的是3个,通俗的来讲:Write属性是用于主机给从机发送数据;Read属性是用于主机读取从机的数据;
 +
Notify属性是用于从机给主机发送数据|type=info}}我们本章的实验,会给大家带来其中的write属性及Read属性的介绍,也就是教大家主机如何给从机发送数据,以及主机如何读取从机的数据。其中包含了从机的服务注册,以及主机的服务发现,相对于前面章节的内容,可能难度会大上一些,大家一定耐心查看,这个章节很重要。
  
在这个实验中,我们主要会展示一下,有关特征值的write以及read属性,其中由于read属性相对而言使用的较少。我们主要会给大家介绍write属性,也就是主机给从机发送数据的属性。
 
 
====实验现象====
 
====实验现象====
手机端通过nordic的app"nrf master control panel",发起对设备的扫描和连接,连接成功之后,我们通过UUID FFF1给开发板发送数据。
+
主机设备流程:
  
例如发送0x00,0x00,0x00,0x00给开发板,此时开发板的4个LED灯均被点亮;同样的我们给将某位数据改成0x01,对应的LED就会熄灭。
+
1、扫描符合我们连接过滤要求的从机设备(根据LED服务的UUID过滤)
 +
 
 +
2、成功连接我们的从机设备,并且更新连接参数和MTU
 +
 
 +
3、发现服务,成功发现Ghostyu LED Service
 +
 
 +
4、主机分别按下4个按键,给从机发送数据控制从机设备的4个LED依次点亮,并且同时从从机读取我们刚刚发送的数据
 +
[[文件:Nrf52832dk-ble-gattwirte2.png|边框|居中|无框|680x680像素]]从机设备流程:
 +
 
 +
1、开启广播
 +
 
 +
2、被主机成功连接,并交互连接参数
 +
 
 +
3、等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)
 +
 
 +
4、主机按下按键,从机接收到相应的LED状态数据并打印,并根据这个数据控制板子上的LED点亮[[文件:Nrf52832dk-ble-gattwirte1.png|边框|居中|无框|684x684像素]]
  
 
====工程及源码讲解====
 
====工程及源码讲解====
第2,247行: 第2,285行:
 
===== 主机部分 =====
 
===== 主机部分 =====
  
===== 从机部分 =====
+
====== gy_profile_led_c.c\.h与main.c ======
 +
我们首先查看一下主机的服务客户端文件,里面包含了好几个函数,我们挨个介绍一下这些函数的功能。
 +
 
 +
第一个还是客户端的初始化函数,对应从机的ble_led_init()注册服务的函数,我们需要利用ble_db_discovery_evt_register()函数注册一个待会服务发现的UUID。
  
======gy_profile_led.c\.h======
+
这里我们需要注意一下,不论是服务的发现,还是某一个特征的发现,都是需要根据UUID来判断的。这边可以看到我们注册UUID发现是和从机的服务中一样的(LED_UUID_BASE以及LED_UUID_SERVICE)。<syntaxhighlight lang="c" line="1" start="83">
我们首先查看一下他的服务文件,也就是gy_profile_led,我们我们就是通过这个服务来接收手机端发送的LED控制数据的。
+
//******************************************************************************
 +
// fn :ble_led_c_init
 +
//
 +
// brief : LED服务客户端初始化函数
 +
//
 +
// param : p_ble_led_c -> 指向LED客户端结构的指针
 +
//        p_ble_led_c_init -> 指向LED初始化结构的指针
 +
//
 +
// return : none
 +
uint32_t ble_led_c_init(ble_led_c_t * p_ble_led_c, ble_led_c_init_t * p_ble_led_c_init)
 +
{
 +
    uint32_t      err_code;
 +
    ble_uuid_t    led_uuid;
 +
    ble_uuid128_t led_base_uuid = {LED_UUID_BASE};
  
可以看到这个服务初始化ble_led_init函数中对于服务以及他的特征值属性的初始化过程,首先我们先初始化一个回调(p_led->led_write_handler = p_led_init->led_write_handler;),这个回调是用来将gy_profile_led这一层的数据,上传给mian文件去处理。
+
    VERIFY_PARAM_NOT_NULL(p_ble_led_c);
 +
    VERIFY_PARAM_NOT_NULL(p_ble_led_c_init);
 +
    VERIFY_PARAM_NOT_NULL(p_ble_led_c_init->evt_handler);
  
接下来是服务的添加,首先是调用sd_ble_uuid_vs_add去添加服务,然后给这个ble_uuid的服务的参数赋值,最后调用sd_ble_gatts_service_add函数去注册这个服务,这边我们注册的服务句柄是p_led->service_handle。
+
    p_ble_led_c->peer_led_db.led_handle        = BLE_GATT_HANDLE_INVALID;
 +
    p_ble_led_c->conn_handle                    = BLE_CONN_HANDLE_INVALID;
 +
    p_ble_led_c->evt_handler                    = p_ble_led_c_init->evt_handler;
  
注册完服务之后,我们就要开始添加我们的特征值characteristic,特征值的添加也是一样的,首先配置特征值的参数,这些参数中我们主要关注一下ble_gatt_char_props_t,这个参数是用来定义特征值的属性的,可以看到我们的属性有如下几种:<syntaxhighlight lang="c">
+
    err_code = sd_ble_uuid_vs_add(&led_base_uuid, &p_ble_led_c->uuid_type);
/**@brief GATT Characteristic Properties. */
+
    if (err_code != NRF_SUCCESS)
typedef struct
+
    {
{
+
        return err_code;
  /* Standard properties */
+
    }
  uint8_t broadcast      :1; /**< 广播 */
+
    VERIFY_SUCCESS(err_code);
  uint8_t read            :1; /**< 读 */
+
 
  uint8_t write_wo_resp  :1; /**< 写指令 */
+
    led_uuid.type = p_ble_led_c->uuid_type;
  uint8_t write          :1; /**< 写*/
+
    led_uuid.uuid = LED_UUID_SERVICE;
  uint8_t notify          :1; /**< 通知*/
+
 
  uint8_t indicate        :1; /**< 暗示 */
+
    return ble_db_discovery_evt_register(&led_uuid);
  uint8_t auth_signed_wr  :1; /**< 签名写指令 */
+
}
} ble_gatt_char_props_t;
+
</syntaxhighlight>看完初始化函数,我们接下来得看下当我们注册好客服端,并且在main函数中调用发现函数之后,底层如何给我们返回,这里我们需要结合main.c文件的内容一起给大家介绍。
</syntaxhighlight>因为我们的led的服务,是手机写数据给开发板控制LED,所以我们要定义特征值写使能(add_char_params.char_props.write = 1;),另外当我们需要知道上次是发送的什么控制数据给开发板时,我们需要读一下数据,所以这边我们同样定义一下读使能(add_char_params.char_props.read  = 1;),当我们配置完特征值的属性之后,我们调用characteristic_add函数,去向刚刚注册的p_led->service_handle服务中添加我们的特征值。<syntaxhighlight lang="c" line="1" start="53">
+
 
//******************************************************************************
+
在mian函数的中,我们可以看到注册了服务发现的功能,也就是数据库发现db_discovery_init();,并且注册了GATT初始化gatt_init();,以及我们的LED服务的客户端初始化led_c_init();<syntaxhighlight lang="c" line="1" start="527">
// fn :ble_led_init
+
//******************************************************************
 +
// fn : main
 
//
 
//
// brief : 初始化LED服务
+
// brief : 主函数
 
//
 
//
// param : p_led -> led服务结构体
+
// param : none
//        p_led_init -> led服务初始化结构体
 
 
//
 
//
// return : uint32_t -> 成功返回SUCCESS,其他返回ERR NO.
+
// return : none
uint32_t ble_led_init(ble_led_t * p_led, const ble_led_init_t * p_led_init)
+
int main(void)
 
{
 
{
     uint32_t              err_code;
+
     // 初始化
     ble_uuid_t           ble_uuid;
+
    log_init();            // 初始化LOG打印,由RTT工作
     ble_add_char_params_t add_char_params;
+
    timer_init();          // 初始化定时器
 +
    GPIOTE_Init();          // 初始化IO
 +
    BTN_Init(btn_evt_handler_t); // 初始化按键 
 +
    power_management_init();// 初始化电源控制
 +
    ble_stack_init();      // 初始化BLE栈堆
 +
    db_discovery_init();    // 初始化数据库发现(用于发现服务)
 +
    gatt_init();            // 初始化GATT
 +
    led_c_init();           // 初始化LED_C
 +
     scan_init();           // 初始化扫描
 +
   
 +
    // 打印例程名称
 +
    NRF_LOG_INFO("demo0:simple central");
 +
   
 +
    scan_start();           // 开始扫描
 +
   
 +
    // 进入主循环
 +
     for (;;)
 +
    {
 +
        idle_state_handle();  // 空闲状态处理
 +
    }
 +
}
 +
</syntaxhighlight>当我们初始化好以上说明的3个函数(具体每个函数的代码,大家可以看下代码中的注册),一旦我们主机发现我们的从机,并成功连接之后,会进入BLE_GAP_EVT_CONNECTED状态。
  
    // 初始化服务结构体
+
在这个状态下,我们就需要开始我们的服务发现了,调用ble_db_discovery_start()函数开始发现服务。<syntaxhighlight lang="c" line="1" start="345">
     p_led->led_write_handler = p_led_init->led_write_handler;
+
//******************************************************************
 
+
// fn : ble_evt_handler
     // 添加服务(128bit UUID)
+
//
    ble_uuid128_t base_uuid = {LED_UUID_BASE};
+
// brief : BLE事件回调
    err_code = sd_ble_uuid_vs_add(&base_uuid, &p_led->uuid_type);
+
// details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
    VERIFY_SUCCESS(err_code);
+
//
 
+
// param : ble_evt_t  事件类型
    ble_uuid.type = p_led->uuid_type;
+
//        p_context  未使用
    ble_uuid.uuid = LED_UUID_SERVICE;
+
//
 +
// return : none
 +
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
 +
{
 +
    ret_code_t            err_code;
 +
     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
 +
    ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
 +
   
 +
    switch (p_ble_evt->header.evt_id)
 +
     {
 +
        // 连接
 +
        case BLE_GAP_EVT_CONNECTED:
 +
            NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
 +
                        Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
 +
                        p_gap_evt->conn_handle,
 +
                        p_connected_evt->conn_params.min_conn_interval,
 +
                        p_connected_evt->conn_params.max_conn_interval,
 +
                        p_connected_evt->conn_params.slave_latency,
 +
                        p_connected_evt->conn_params.conn_sup_timeout
 +
                        );
 +
            m_conn_handle = p_gap_evt->conn_handle;
 +
           
 +
            err_code = ble_led_c_handles_assign(&m_ble_led_c, m_conn_handle, NULL);
 +
            APP_ERROR_CHECK(err_code);
  
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_led->service_handle);
+
            // 开始发现服务,NUS客户端等待发现结果
     VERIFY_SUCCESS(err_code);
+
            err_code = ble_db_discovery_start(&m_db_disc, p_ble_evt->evt.gap_evt.conn_handle);
 +
            APP_ERROR_CHECK(err_code);
 +
            break;
 +
</syntaxhighlight>当成功发现服务之后,会进入db_disc_handler回调函数,在这个回调函数之中,因为我们这个工程仅需要处理led的服务,所以我们调用ble_led_c_on_db_disc_evt去发现led相关的特征值内容,其中会携带我们的ble_db_discovery_evt_t参数(底层返回的所有和服务数据库相关的信息都在这个参数里面)。<syntaxhighlight lang="c" line="1" start="316">
 +
//******************************************************************
 +
// fn : db_disc_handler
 +
//
 +
// brief : 用于处理数据库发现事件的函数
 +
// details : 此函数是一个回调函数,用于处理来自数据库发现模块的事件。
 +
//          根据发现的UUID,此功能将事件转发到各自的服务。
 +
//
 +
// param : p_event -> 指向数据库发现事件的指针
 +
//
 +
// return : none
 +
static void db_disc_handler(ble_db_discovery_evt_t * p_evt)
 +
{
 +
     ble_led_c_on_db_disc_evt(&m_ble_led_c, p_evt);
 +
}
 +
</syntaxhighlight>所以接下来,我们需要先判断一下,底层返回的ble_db_discovery_evt_t中携带的类型是否是BLE_DB_DISCOVERY_COMPLETE,也就是数据库成功的完成发现,且发现的UUID是LED_UUID_SERVICE。
 +
 
 +
如果确实成功的发现我们的LED服务,接下来我们就需要从服务中取出我们需要的特征值,也就是LED_UUID_CHAR。我们需要从这个特征值当中获取我们用于通信的句柄(handle_value)。
  
     // 添加LED特征值(属性是Write和Read、长度是4)
+
当我们一切都是按照正确的流程跑完,可以看到在这个函数的最后,它会给我们返回一个p_ble_led_c->evt_handler(p_ble_led_c, &evt);,也就是向mian.c文件中给我们一个回调(ble_led_c_init初始化函数时注册的回调),其中携带的任务参数类型是BLE_LED_C_EVT_DISCOVERY_COMPLETE。<syntaxhighlight lang="c" line="1" start="32">
     memset(&add_char_params, 0, sizeof(add_char_params));
+
//******************************************************************************
    add_char_params.uuid             = LED_UUID_CHAR;
+
// fn :ble_led_c_on_db_disc_evt
    add_char_params.uuid_type        = p_led->uuid_type;
+
//
    add_char_params.init_len        = LED_UUID_CHAR_LEN;
+
// brief : 处理led服务发现的函数
    add_char_params.max_len          = LED_UUID_CHAR_LEN;
+
//
    add_char_params.char_props.read  = 1;
+
// param : p_ble_led_c -> 指向LED客户端结构的指针
    add_char_params.char_props.write = 1;
+
//        p_evt -> 指向从数据库发现模块接收到的事件的指针
 +
//
 +
// return : none
 +
void ble_led_c_on_db_disc_evt(ble_led_c_t * p_ble_led_c, ble_db_discovery_evt_t const * p_evt)
 +
{
 +
     // 判断LED服务是否发现完成
 +
     if (p_evt->evt_type == BLE_DB_DISCOVERY_COMPLETE &&
 +
        p_evt->params.discovered_db.srv_uuid.uuid == LED_UUID_SERVICE &&
 +
        p_evt->params.discovered_db.srv_uuid.type == p_ble_led_c->uuid_type)
 +
    {
 +
        ble_led_c_evt_t evt;
 +
 
 +
        evt.evt_type    = BLE_LED_C_EVT_DISCOVERY_COMPLETE;
 +
        evt.conn_handle = p_evt->conn_handle;
 +
 
 +
        for (uint32_t i = 0; i < p_evt->params.discovered_db.char_count; i++)
 +
        {
 +
            const ble_gatt_db_char_t * p_char = &(p_evt->params.discovered_db.charateristics[i]);
 +
            switch (p_char->characteristic.uuid.uuid)
 +
            {
 +
                // 根据LED特征值的UUID,获取我们句柄handle_value
 +
                case LED_UUID_CHAR:
 +
                    evt.params.peer_db.led_handle = p_char->characteristic.handle_value;
 +
                    break;
 +
 
 +
                default:
 +
                    break;
 +
            }
 +
        }
  
    add_char_params.read_access  = SEC_OPEN;
+
        NRF_LOG_DEBUG("Led Button Service discovered at peer.");
    add_char_params.write_access = SEC_OPEN;
+
       
 +
        // 如果实例是在db_discovery之前分配的,则分配db_handles
 +
        if (p_ble_led_c->conn_handle != BLE_CONN_HANDLE_INVALID)
 +
        {
 +
            if (p_ble_led_c->peer_led_db.led_handle        == BLE_GATT_HANDLE_INVALID)
 +
            {
 +
                p_ble_led_c->peer_led_db = evt.params.peer_db;
 +
            }
 +
        }
  
    return characteristic_add(p_led->service_handle, &add_char_params, &p_led->led_char_handles);
+
        p_ble_led_c->evt_handler(p_ble_led_c, &evt);
 +
    }
 
}
 
}
</syntaxhighlight>看完服务的初始化,我们来看下我们的服务注册之后是怎么来进行工作的,首先我们看下ble_led_on_ble_evt这个函数,这个函数在我们mian函数中注册BLE_LED_DEF(m_led);实例的时候被引用。<syntaxhighlight lang="c" line="1" start="15">
+
</syntaxhighlight>那么接下来,我们再去看一下mian.c中此回调函数下的处理。
//******************************************************************************
+
 
// fn :BLE_LED_DEF
+
在ble_led_c_evt_handler回调函数下,我们判断传入的事件类型,可以看到正是刚刚的BLE_LED_C_EVT_DISCOVERY_COMPLETE事件,也就是代表我们已经成功的获取了我们指定服务(LED_UUID_SERVICE)下的指定特征值(LED_UUID_CHAR)的句柄(handle_value)。
 +
 
 +
然后我们调用ble_led_c_handles_assign函数,去将我们的连接句柄connHandle以及特征值句柄handle_value,绑定给p_ble_led_c实例。<syntaxhighlight lang="c" line="1" start="272">
 +
//******************************************************************
 +
// fn : ble_led_c_evt_handler
 
//
 
//
// brief : 初始化LED服务实例
+
// brief : LED服务事件
 
//
 
//
// param : _name -> 实例的名称
+
// param : none
 +
//
 +
// return : none               
 +
static void ble_led_c_evt_handler(ble_led_c_t * p_ble_led_c, ble_led_c_evt_t * p_evt)
 +
{
 +
    ret_code_t err_code;
 +
 
 +
    switch (p_evt->evt_type)
 +
    {
 +
        case BLE_LED_C_EVT_DISCOVERY_COMPLETE:
 +
            NRF_LOG_INFO("Discovery complete.");
 +
            err_code = ble_led_c_handles_assign(&m_ble_led_c, p_evt->conn_handle, &p_evt->params.peer_db);
 +
            APP_ERROR_CHECK(err_code);
 +
            NRF_LOG_INFO("Connected to device with Ghostyu LED Service.");
 +
            break;
 +
        default:
 +
            break;
 +
    }
 +
}
 +
</syntaxhighlight>当上述的流程都正确跑完,我们就可以进行最后一步的行动,也就是发送数据,在这个例程当中我们是利用按键触发来发送对应的LED的状态变化。
 +
 
 +
我们到mian.c中,查看按键触发会调用的btn_evt_handler_t回调函数,在这个函数中,我们最后会调用LED服务数据发送的功能函数ble_led_led_status_send。<syntaxhighlight lang="c" line="1" start="495">
 +
//******************************************************************
 +
// fn : btn_evt_handler_t
 +
//
 +
// brief : 按键触发回调函数
 +
//
 +
// param : butState -> 当前的按键值
 
//
 
//
 
// return : none
 
// return : none
#define BLE_LED_DEF(_name)                                                                         \
+
void btn_evt_handler_t (uint8_t butState)
static ble_led_t _name;                                                                             \
+
{
NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                 \
+
  uint8_t buf[LED_UUID_CHAR_LEN] = {0x01,0x01,0x01,0x01};
                    BLE_LED_BLE_OBSERVER_PRIO,                                                     \
+
  switch(butState)
                    ble_led_on_ble_evt, &_name)
+
  {
</syntaxhighlight>这个m_led实例注册,涉及到NRF_SDH_BLE_OBSERVER的使用,简单的理解就是利用这个注册了实例之后,当底层有GAP或者GATT消息返回的时候,就会触发ble_led_on_ble_evt函数。
+
    case BUTTON_1:
 +
      buf[0] = 0x00;
 +
      break;
 +
    case BUTTON_2:
 +
      buf[1] = 0x00;
 +
      break;
 +
    case BUTTON_3:
 +
      buf[2] = 0x00;
 +
      break;
 +
    case BUTTON_4:
 +
      buf[3] = 0x00;
 +
      break;
 +
    default:
 +
      break;
 +
  }
 +
  ble_led_status_send(&m_ble_led_c,buf,LED_UUID_CHAR_LEN);    // 发送Wirte属性数据包
 +
  ble_led_status_read(&m_ble_led_c);                          // 发送Read属性的读取消息
 +
}
 +
</syntaxhighlight>最后我们来分析一下这个发送函数,是如何使用我们刚刚一大圈代码处理,最终得到的connhandle以及handle_value的。
 +
 
 +
首先先判断下数据的长度,是不是符合我们的特征值的长度限制(不能超过我们定义的特征值的大小,否则返回参数错误),这个判断是很有必要的!
 +
 
 +
接下来我们判断一下connhandle是否为0xffff(BLE_CONN_HANDLE_INVALID),也就是尚未连接任何设备,如果没有连接,则返回状态无效。
  
因为我们LED服务这里需要接收手机端write的LED控制数据,所以我们在事件判断中,判断是否出现GATT Write事件,一旦出现了,我们调用on_write函数去处理这个事件。<syntaxhighlight lang="c" line="1" start="28">
+
最后我们定义了ble_gattc_write_params_t结构体用于赋值我们需要发送的数据,其中值得注意的是.handle  = p_ble_led_c->peer_led_db.led_handle,这个就是我们刚刚获得的handle_value(特征值句柄),其他参数大家依葫芦画瓢,比较好理解,就不给大家介绍了。最终我们调用sd_ble_gattc_write函数将数据发送出去。<syntaxhighlight lang="c" line="1" start="148">
 
//******************************************************************************
 
//******************************************************************************
// fn :ble_led_on_ble_evt
+
// fn :ble_led_led_status_send
 
//
 
//
// brief : BLE事件处理函数
+
// brief : LED状态控制函数
 
//
 
//
// param : p_ble_evt -> ble事件
+
// param : p_ble_led_c -> 指向要关联的LED结构实例的指针
//        p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
+
//        p_string -> 发送的LED相关的数据
 +
//        length -> 发送的LED相关的数据长度
 
//
 
//
 
// return : none
 
// return : none
void ble_led_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
+
uint32_t ble_led_led_status_send(ble_led_c_t * p_ble_led_c, uint8_t * p_string, uint16_t length)
 
{
 
{
     ble_led_t * p_led = (ble_led_t *)p_context;
+
     VERIFY_PARAM_NOT_NULL(p_ble_led_c);
  
     switch (p_ble_evt->header.evt_id)
+
     if (length > LED_UUID_CHAR_LEN)
 
     {
 
     {
         // GATT Client Write事件
+
         NRF_LOG_WARNING("Content too long.");
         case BLE_GATTS_EVT_WRITE:
+
        return NRF_ERROR_INVALID_PARAM;
            on_write(p_led, p_ble_evt);
+
    }
            break;
+
    if (p_ble_led_c->conn_handle == BLE_CONN_HANDLE_INVALID)
 +
    {
 +
        return NRF_ERROR_INVALID_STATE;
 +
    }
 +
 
 +
    ble_gattc_write_params_t const write_params =
 +
    {
 +
        .write_op = BLE_GATT_OP_WRITE_CMD,
 +
        .flags    = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
 +
        .handle  = p_ble_led_c->peer_led_db.led_handle,
 +
        .offset  = 0,
 +
        .len      = length,
 +
         .p_value  = p_string
 +
    };
 +
   
 +
    return sd_ble_gattc_write(p_ble_led_c->conn_handle, &write_params);
 +
}
 +
</syntaxhighlight>上面一部分已经将我们的write属性的使用都讲解完了,最后我们再来看下Read属性部分的内容。
 +
 
 +
首先是我们还是在main文件的按键回调函数中调用的ble_led_status_read(&m_ble_led_c);函数,去读取从机特征值中的数据的,这里我们直接分析一下这个函数。
  
        default:
+
可以看到函数内容很简单,只调用了一个sd_ble_gattc_read函数去读取,包含的参数内容分别是我们的connhandle以及handle_value。<syntaxhighlight lang="c" line="1" start="209">
            break;
+
//******************************************************************************
     }
+
// fn :ble_led_status_read
 +
//
 +
// brief : 读取LED特征值
 +
//
 +
// param : p_ble_led_c -> 指向要关联的LED结构实例的指针
 +
//
 +
// return : none
 +
uint32_t ble_led_status_read(ble_led_c_t * p_ble_led_c)
 +
{
 +
    VERIFY_PARAM_NOT_NULL(p_ble_led_c);
 +
     return sd_ble_gattc_read(p_ble_led_c->conn_handle,p_ble_led_c->peer_led_db.led_handle,0);
 
}
 
}
</syntaxhighlight>on_write函数用于处理接收的write数据,我们判断一下接收的数据是否符合我们的要求,如果符合,那么我们通过初始化函数中的回调函数,将接收到的值上传到main函数中去处理。<syntaxhighlight lang="c" line="1" start="7">
+
</syntaxhighlight>当我们成功Read之后,底层的sotfdevice会通过ble_led_c_on_ble_evt函数给我们返回BLE_GATTC_EVT_READ_RSP事件。<syntaxhighlight lang="c" line="1" start="140">
 
//******************************************************************************
 
//******************************************************************************
// fn :on_write
+
// fn :ble_led_c_on_ble_evt
 
//
 
//
// brief : 处理Write事件的函数。
+
// brief : BLE事件处理函数
 
//
 
//
// param : p_led -> led服务结构体
+
// param : p_ble_evt -> ble事件
//        p_ble_evt -> ble事件
+
//        p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
 
//
 
//
 
// return : none
 
// return : none
static void on_write(ble_led_t * p_led, ble_evt_t const * p_ble_evt)
+
void ble_led_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
 
{
 
{
    ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
+
     if ((p_context == NULL) || (p_ble_evt == NULL))
 
 
     if (   (p_evt_write->handle == p_led->led_char_handles.value_handle)
 
        && (p_evt_write->len <= LED_UUID_CHAR_LEN)
 
        && (p_led->led_write_handler != NULL))
 
 
     {
 
     {
         p_led->led_write_handler((uint8_t*)p_evt_write->data);
+
         return;
 +
    }
 +
 
 +
    ble_led_c_t * p_ble_led_c = (ble_led_c_t *)p_context;
 +
 
 +
    switch (p_ble_evt->header.evt_id)
 +
    {
 +
        case BLE_GAP_EVT_DISCONNECTED:
 +
            on_disconnected(p_ble_led_c, p_ble_evt);
 +
            break;
 +
        case BLE_GATTC_EVT_READ_RSP:
 +
            on_read(p_ble_led_c, p_ble_evt);
 +
          break;
 +
         
 +
        default:
 +
            break;
 
     }
 
     }
 
}
 
}
</syntaxhighlight>
+
</syntaxhighlight>在BLE_GATTC_EVT_READ_RSP事件中,我们调用on_read函数去处理我们读取的值,我们将读取到的值,通过RTT LOG打印出来。<syntaxhighlight lang="c" line="1" start="32">
======gy_serial_led.c\.h======
+
//******************************************************************************
有关外设处理,请大家查看基础实验部分
+
// fn :on_read
======main.c======
 
main文件中也不给大家全部介绍了,这个和蓝牙协议实验部分是重合的,我们只关注实验改动的部分。
 
 
 
我们看下服务初始化的部分,可以看到调用了我们gy_profile_led中的ble_led_init函数初始化注册了我们的LED服务,并且通用注册了一个回调函数。
 
 
 
在这个回调函数led_write_handler中,我们可以获取到gy_profile_led中上传上来的接收到的wirte数据,并且利用这个数据进行LED的控制。<syntaxhighlight lang="c" line="1" start="195">
 
//******************************************************************
 
// fn : nus_data_handler
 
 
//
 
//
// brief : 用于处理来自Nordic UART服务的数据的功能
+
// brief : 处理read事件的函数。
// details : 该功能将处理从Nordic UART BLE服务接收的数据并将其发送到UART模块
 
 
//
 
//
// param : ble_nus_evt_t -> nus事件
+
// param : p_ble_led_c -> led服务结构体
 +
//        p_ble_evt -> ble事件
 
//
 
//
 
// return : none
 
// return : none
static void led_write_handler(uint8_t * new_state)
+
static void on_read(ble_led_c_t * p_ble_led_c, ble_evt_t const * p_ble_evt)
 
{
 
{
     NRF_LOG_INFO("Recive State:%02X,%02X,%02X,%02X",new_state[0],new_state[1],new_state[2],new_state[3]);
+
     if (p_ble_led_c->conn_handle == p_ble_evt->evt.gap_evt.conn_handle)
     LED_Control(BSP_LED_0, new_state[0]);
+
    {
    LED_Control(BSP_LED_1, new_state[1]);
+
      NRF_LOG_INFO("Recive State:%02X,%02X,%02X,%02X",
    LED_Control(BSP_LED_2, new_state[2]);
+
                  p_ble_evt->evt.gattc_evt.params.read_rsp.data[0],
    LED_Control(BSP_LED_3, new_state[3]);
+
                  p_ble_evt->evt.gattc_evt.params.read_rsp.data[1],
 +
                  p_ble_evt->evt.gattc_evt.params.read_rsp.data[2],
 +
                  p_ble_evt->evt.gattc_evt.params.read_rsp.data[3]);
 +
     }
 
}
 
}
 +
</syntaxhighlight>
 +
 +
===== 从机部分 =====
  
//******************************************************************
+
======gy_profile_led.c\.h======
// fn : services_init
+
我们首先查看一下他的服务文件,也就是gy_profile_led,我们我们就是通过这个服务来接收主机发送的LED控制数据的。
 +
 
 +
可以看到这个服务初始化ble_led_init函数中对于服务以及他的特征值属性的初始化过程,首先我们先初始化一个回调(p_led->led_write_handler = p_led_init->led_write_handler;),这个回调是用来将gy_profile_led这一层的数据,上传给mian文件去处理。
 +
 
 +
接下来是服务的添加,首先是调用sd_ble_uuid_vs_add去添加服务,然后给这个ble_uuid的服务的参数赋值,最后调用sd_ble_gatts_service_add函数去注册这个服务,这边我们注册的服务句柄是p_led->service_handle。
 +
 
 +
注册完服务之后,我们就要开始添加我们的特征值characteristic,特征值的添加也是一样的,首先配置特征值的参数,这些参数中我们主要关注一下ble_gatt_char_props_t,这个参数是用来定义特征值的属性的,可以看到我们的属性有如下几种:<syntaxhighlight lang="c">
 +
/**@brief GATT Characteristic Properties. */
 +
typedef struct
 +
{
 +
  /* Standard properties */
 +
  uint8_t broadcast      :1; /**< 广播 */
 +
  uint8_t read            :1; /**< 读 */
 +
  uint8_t write_wo_resp  :1; /**< 写指令 */
 +
  uint8_t write          :1; /**< 写*/
 +
  uint8_t notify          :1; /**< 通知*/
 +
  uint8_t indicate        :1; /**< 暗示 */
 +
  uint8_t auth_signed_wr  :1; /**< 签名写指令 */
 +
} ble_gatt_char_props_t;
 +
</syntaxhighlight>因为我们的led的服务,是手机写数据给开发板控制LED,所以我们要定义特征值写使能(add_char_params.char_props.write = 1;),另外当我们需要知道上次是发送的什么控制数据给开发板时,我们需要读一下数据,所以这边我们同样定义一下读使能(add_char_params.char_props.read  = 1;),当我们配置完特征值的属性之后,我们调用characteristic_add函数,去向刚刚注册的p_led->service_handle服务中添加我们的特征值。<syntaxhighlight lang="c" line="1" start="53">
 +
//******************************************************************************
 +
// fn :ble_led_init
 
//
 
//
// brief : 初始化复位(本例程展示NUS:Nordic Uart Service)
+
// brief : 初始化LED服务
 
//
 
//
// param : none
+
// param : p_led -> led服务结构体
 +
//        p_led_init -> led服务初始化结构体
 
//
 
//
// return : none
+
// return : uint32_t -> 成功返回SUCCESS,其他返回ERR NO.
static void services_init(void)
+
uint32_t ble_led_init(ble_led_t * p_led, const ble_led_init_t * p_led_init)
 
{
 
{
     uint32_t           err_code;
+
     uint32_t             err_code;
     ble_led_init_t     led_init;
+
     ble_uuid_t            ble_uuid;
 +
     ble_add_char_params_t add_char_params;
  
     // Initialize NUS.
+
     // 初始化服务结构体
     memset(&led_init, 0, sizeof(led_init));
+
     p_led->led_write_handler = p_led_init->led_write_handler;
  
     led_init.led_write_handler = led_write_handler;
+
     // 添加服务(128bit UUID)
 +
    ble_uuid128_t base_uuid = {LED_UUID_BASE};
 +
    err_code = sd_ble_uuid_vs_add(&base_uuid, &p_led->uuid_type);
 +
    VERIFY_SUCCESS(err_code);
  
     err_code = ble_led_init(&m_led, &led_init);
+
     ble_uuid.type = p_led->uuid_type;
     APP_ERROR_CHECK(err_code);
+
     ble_uuid.uuid = LED_UUID_SERVICE;
}
 
</syntaxhighlight>
 
  
==== <span> 实验总结</span> ====
+
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_led->service_handle);
 +
    VERIFY_SUCCESS(err_code);
  
=== NUS服务获取实验 ===
+
    // 添加LED特征值(属性是Write和Read、长度是4)
 +
    memset(&add_char_params, 0, sizeof(add_char_params));
 +
    add_char_params.uuid            = LED_UUID_CHAR;
 +
    add_char_params.uuid_type        = p_led->uuid_type;
 +
    add_char_params.init_len        = LED_UUID_CHAR_LEN;
 +
    add_char_params.max_len          = LED_UUID_CHAR_LEN;
 +
    add_char_params.char_props.read  = 1;
 +
    add_char_params.char_props.write = 1;
  
==== 实验简介 ====
+
    add_char_params.read_access  = SEC_OPEN;
经过之前的学习,我们已经完成了蓝牙的广播、扫描、连接,并且知道了如何更新MTU以及连接参数。
+
    add_char_params.write_access = SEC_OPEN;
  
那么当前我们仅剩下的便是连接之后的通信了,谈到蓝牙的通信,那么我们必然是绕不开蓝牙的服务的。
+
    return characteristic_add(p_led->service_handle, &add_char_params, &p_led->led_char_handles);
 
+
}
这一章节我们便给大家介绍一下从机设备如何注册一个服务,以及主机设备如何去获取服务,这一实验我们仅从应用层给大家介绍服务的添加及获取,具体的服务的实现,我们将在以后的实验中再给大家介绍。
+
</syntaxhighlight>看完服务的初始化,我们来看下我们的服务注册之后是怎么来进行工作的,首先我们看下ble_led_on_ble_evt这个函数,这个函数在我们mian函数中注册BLE_LED_DEF(m_led);实例的时候被引用。<syntaxhighlight lang="c" line="1" start="15">
 
+
//******************************************************************************
==== 实验现象 ====
+
// fn :BLE_LED_DEF
主机上电之后,首先扫描并打印符合过滤的从机设备信息,然后发起连接,连接成功之后打印连接参数、连接句柄(connhanlde)等信息。
+
//
 +
// brief : 初始化LED服务实例
 +
//
 +
// param : _name -> 实例的名称
 +
//
 +
// return : none
 +
#define BLE_LED_DEF(_name)                                                                          \
 +
static ble_led_t _name;                                                                            \
 +
NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                \
 +
                    BLE_LED_BLE_OBSERVER_PRIO,                                                    \
 +
                    ble_led_on_ble_evt, &_name)
 +
</syntaxhighlight>这个m_led实例注册,涉及到NRF_SDH_BLE_OBSERVER的使用,简单的理解就是利用这个注册了实例之后,当底层有GAP或者GATT消息返回的时候,就会触发ble_led_on_ble_evt函数。
  
当连接完成之后,首先更新了MTU大小,然后就去发起服务的扫描,最终实验现象上体现的就是发现完成,成功通过NUS(Nordic UART Service)连接到设备。
+
因为我们LED服务这里需要接收手机端write的LED控制数据,所以我们在事件判断中,判断是否出现GATT Write事件,一旦出现了,我们调用on_write函数去处理这个事件。<syntaxhighlight lang="c" line="1" start="28">
[[文件:Nrf rtt 19.png|边框|居中|无框|648x648像素]]
+
//******************************************************************************
从机设备上电广播,当被主机连接之后,会打印主机的设备信息以及连接参数,最终会打印更新后的MTU大小。
+
// fn :ble_led_on_ble_evt
[[文件:Nrf tft 29.png|边框|居中|无框|657x657像素]]
 
 
 
==== 工程及源码讲解 ====
 
 
 
===== 主机部分 =====
 
主机部分主要有两点需要我们关注一下,一个是MTU大小为什么会是244,另一个就是我们本实验的重点,如何去发起对服务的发现。
 
 
 
====== gatt_init()函数 ======
 
首先我们看一下MTU大小的配置,我们可以先进入nrf_ble_gatt_init()初始化函数中,可以看到p_gatt->att_mtu_desired_central = NRF_SDH_BLE_GATT_MAX_MTU_SIZE这样的一个参数赋值,我们再前往查看NRF_SDH_BLE_GATT_MAX_MTU_SIZE的数值,可以看到在sdh_config.h文件中,我们将其定义为247,然后我们再减去NUS的3字节占用,所以实验现象中打印的Data len是244。<syntaxhighlight lang="c" line="1" start="226">
 
//******************************************************************
 
// fn : gatt_init
 
 
//
 
//
// brief : 初始化GATT
+
// brief : BLE事件处理函数
 
//
 
//
// param : none
+
// param : p_ble_evt -> ble事件
 +
//        p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
 
//
 
//
 
// return : none
 
// return : none
void gatt_init(void)
+
void ble_led_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
 
{
 
{
     ret_code_t err_code;
+
     ble_led_t * p_led = (ble_led_t *)p_context;
  
     err_code = nrf_ble_gatt_init(&m_gatt, gatt_evt_handler);
+
     switch (p_ble_evt->header.evt_id)
     APP_ERROR_CHECK(err_code);
+
     {
 +
        // GATT Client Write事件
 +
        case BLE_GATTS_EVT_WRITE:
 +
            on_write(p_led, p_ble_evt);
 +
            break;
 +
 
 +
        default:
 +
            break;
 +
    }
 
}
 
}
</syntaxhighlight>
+
</syntaxhighlight>on_write函数用于处理接收的write数据,我们判断一下接收的数据是否符合我们的要求,如果符合,那么我们通过初始化函数中的回调函数,将接收到的值上传到main函数中去处理。<syntaxhighlight lang="c" line="1" start="7">
 
+
//******************************************************************************
====== nus_c_init()函数 ======
+
// fn :on_write
这个主机的nus client初始化函数,其在ble_nus_c_init()函数中,最终去发起了对服务的发现。然后我们定义了一个回调函数,用于处理服务发现和服务处理的事件。<syntaxhighlight lang="c" line="1" start="279">
 
//******************************************************************
 
// fn : nus_c_init
 
 
//
 
//
// brief : 初始化NUS客户端(Nordic UART Service client)
+
// brief : 处理Write事件的函数。
 
//
 
//
// param : none
+
// param : p_led -> led服务结构体
 +
//        p_ble_evt -> ble事件
 
//
 
//
 
// return : none
 
// return : none
static void nus_c_init(void)
+
static void on_write(ble_led_t * p_led, ble_evt_t const * p_ble_evt)
 
{
 
{
     ret_code_t      err_code;
+
     ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
    ble_nus_c_init_t init;
 
  
     init.evt_handler = ble_nus_c_evt_handler;
+
     if (  (p_evt_write->handle == p_led->led_char_handles.value_handle)
 
+
        && (p_evt_write->len <= LED_UUID_CHAR_LEN)
    err_code = ble_nus_c_init(&m_ble_nus_c, &init);
+
        && (p_led->led_write_handler != NULL))
     APP_ERROR_CHECK(err_code);
+
     {
 +
        p_led->led_write_handler((uint8_t*)p_evt_write->data);
 +
    }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
======main.c======
 +
main文件中也不给大家全部介绍了,这个和蓝牙协议实验部分是重合的,我们只关注实验改动的部分。
  
====== ble_nus_c_evt_handler()函数 ======
+
我们看下服务初始化的部分,可以看到调用了我们gy_profile_led中的ble_led_init函数初始化注册了我们的LED服务,并且通用注册了一个回调函数。
NUS服务相关的回调函数,这个回调中包含了3个事件ID。
 
  
BLE_NUS_C_EVT_DISCOVERY_COMPLETE:服务发现完成,这个事件返回之后,我们需要调用ble_nus_c_handles_assign()函数给服务分配handle,然后调用ble_nus_c_tx_notif_enable()函数使能NUS服务的TX特征值的通知。
+
在这个回调函数led_write_handler中,我们可以获取到gy_profile_led中上传上来的接收到的wirte数据,并且利用这个数据进行LED的控制。<syntaxhighlight lang="c" line="1" start="195">
 +
//******************************************************************
 +
// fn : nus_data_handler
 +
//
 +
// brief : 用于处理来自Nordic UART服务的数据的功能
 +
// details : 该功能将处理从Nordic UART BLE服务接收的数据并将其发送到UART模块
 +
//
 +
// param : ble_nus_evt_t -> nus事件
 +
//
 +
// return : none
 +
static void led_write_handler(uint8_t * new_state)
 +
{
 +
    NRF_LOG_INFO("Recive State:%02X,%02X,%02X,%02X",new_state[0],new_state[1],new_state[2],new_state[3]);
 +
    LED_Control(BSP_LED_0, new_state[0]);
 +
    LED_Control(BSP_LED_1, new_state[1]);
 +
    LED_Control(BSP_LED_2, new_state[2]);
 +
    LED_Control(BSP_LED_3, new_state[3]);
 +
}
  
BLE_NUS_C_EVT_NUS_TX_EVT:NUS服务的TX特征值事件,这个事件返回,代表了TX特征值有数据通知到来。
+
//******************************************************************
 
+
// fn : services_init
BLE_NUS_C_EVT_DISCONNECTED:NUS断开事件,服务获取异常断开。<syntaxhighlight lang="c" line="1" start="242">
+
//
//******************************************************************
+
// brief : 初始化复位(本例程展示NUS:Nordic Uart Service)
// fn : ble_nus_c_evt_handler
+
//
//
+
// param : none
// brief : NUS事件
+
//
//
+
// return : none
// param : none
+
static void services_init(void)
//
+
{
// return : none
+
    uint32_t          err_code;
static void ble_nus_c_evt_handler(ble_nus_c_t * p_ble_nus_c, ble_nus_c_evt_t const * p_ble_nus_evt)
+
    ble_led_init_t    led_init;
{
+
 
     ret_code_t err_code;
+
    // Initialize NUS.
 
+
    memset(&led_init, 0, sizeof(led_init));
     switch (p_ble_nus_evt->evt_type)
+
 
     {
+
    led_init.led_write_handler = led_write_handler;
         case BLE_NUS_C_EVT_DISCOVERY_COMPLETE:
+
 
             NRF_LOG_INFO("Discovery complete.");
+
    err_code = ble_led_init(&m_led, &led_init);
             err_code = ble_nus_c_handles_assign(p_ble_nus_c, p_ble_nus_evt->conn_handle, &p_ble_nus_evt->handles);
+
    APP_ERROR_CHECK(err_code);
             APP_ERROR_CHECK(err_code);
+
}
 
+
</syntaxhighlight>
             err_code = ble_nus_c_tx_notif_enable(p_ble_nus_c);
+
 
             APP_ERROR_CHECK(err_code);
+
==== <span> 实验总结</span> ====
             NRF_LOG_INFO("Connected to device with Nordic UART Service.");
+
通过这一章节的学习,我们需要掌握下面3个要点。
             break;
+
 
 
+
1、从机如何注册一个自定的服务,并且在服务下添加自己的特征值功能
         case BLE_NUS_C_EVT_NUS_TX_EVT:
+
 
             NRF_LOG_DEBUG("Receiving data.");
+
2、主机如何针对指定的从机服务,去获取这个服务以及服务下指定特征值的句柄
             NRF_LOG_HEXDUMP_DEBUG(p_ble_nus_evt->p_data, p_ble_nus_evt->data_len);
+
 
             break;
+
3、主机如何通过Write属性,向从机发送数据
 
+
 
        case BLE_NUS_C_EVT_DISCONNECTED:
+
===Notify属性服务实验===
             NRF_LOG_INFO("Disconnected.");
+
====实验简介====
             break;
+
通过Write属性服务实验的学习,我们已经知道了从机设备如何注册一个自定义服务(自定义的LED服务),然后主机如何去发现这个服务,并且利用这个服务的特征值与从机设备进行通信,控制从机设备的LED点亮。
         default:
+
 
             break;
+
也就是说,我们已经学会了如何通过主机给从机发送数据,所以接下来我们本章节将会给大家讲解从机如何通过notify属性给主机发送数据。
     }
+
====实验现象====
}
+
主机设备流程:
</syntaxhighlight>
+
 
 
+
1、扫描符合我们连接过滤要求的从机设备(根据LED服务的UUID过滤)
===== 从机部分 =====
+
 
从机部分有关MTU 244的初始化和主机部分相同,这边不再重复,我们主要关注一下NUS服务的初始化注册。
+
2、成功连接我们的从机设备,并且更新连接参数和MTU
 
+
 
====== services_init()函数 ======
+
3、发现服务,成功发现Ghostyu BTN Service
服务初始化函数中,我们调用ble_nus_init()函数初始化了NUS服务,这样初始化完成之后,我们从机的服务列表中,就会将NUS添加。
+
 
 
+
4、成功使用了从机服务的notify功能
并且我们定义了一个nus_data_handler()回调函数,用于处理NUS服务的事件(这个回调的事件我们本章节用不到,所以我们这边先不介绍,留着下一章节讲解)。<syntaxhighlight lang="c" line="1" start="223">
+
[[文件:Nrf52832dk-ble-gattnotify2.png|边框|居中|无框|680x680像素]]
//******************************************************************
+
从机设备流程:
// fn : services_init
+
 
//
+
1、开启广播
// brief : 初始化服务(本例程展示NUS:Nordic Uart Service)
+
 
 +
2、被主机成功连接,并交互连接参数
 +
 
 +
3、等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)
 +
 
 +
4、等待主机成功使能notify功能
 +
 
 +
5、从机分别按下4个按键,给主机发送相应的notify数据包,控制主机LED点亮
 +
[[文件:Nrf52832dk-ble-gattnotify1.png|边框|居中|无框|695x695px]]
 +
 
 +
====工程及源码讲解====
 +
 
 +
===== 主机部分 =====
 +
 
 +
====== gy_profile_btn_c.c\.h及mian.c ======
 +
这个实验主机部分的代码和上一章节的Write的主机是类似的,他的整个的服务发现流程是一样的,唯一不同的点是由于从机需要发送notify数据,所以我们的主机需要去使能从机的notify的功能。
 +
 
 +
所以我们来看一下主机是在哪边,以怎样的方式去使能从机的notify。以及最终是怎么接收来自从机的数据的。
 +
 
 +
首先看下使能notify的部分,通过上一实验的学习,我们已经知道当我们自己成功跑完获取服务的流程,最后会给main函数中的服务初始化的回调返回成功的事件。我们看下成功发现服务的事件BLE_BTN_C_EVT_DISCOVERY_COMPLETE,里面首先还是调用ble_btn_c_handles_assign函数将获取的句柄值和我们m_ble_btn_c实例绑定起来。然后就去调用ble_btn_c_tx_notif_enable函数去使能从机的notify功能。<syntaxhighlight lang="c" line="1" start="272">
 +
//******************************************************************
 +
// fn : ble_btn_c_evt_handler
 +
//
 +
// brief : BTN服务事件
 +
//
 +
// param : none
 +
//
 +
// return : none                
 +
static void ble_btn_c_evt_handler(ble_btn_c_t * p_ble_btn_c, ble_btn_c_evt_t * p_evt)
 +
{
 +
     ret_code_t err_code;
 +
 
 +
     switch (p_evt->evt_type)
 +
     {
 +
         case BLE_BTN_C_EVT_DISCOVERY_COMPLETE:
 +
             NRF_LOG_INFO("Discovery complete.");
 +
             err_code = ble_btn_c_handles_assign(&m_ble_btn_c, p_evt->conn_handle, &p_evt->params.peer_db);
 +
             APP_ERROR_CHECK(err_code);
 +
            NRF_LOG_INFO("Connected to device with Ghostyu BTN Service.");
 +
           
 +
             err_code = ble_btn_c_tx_notif_enable(&m_ble_btn_c);
 +
             APP_ERROR_CHECK(err_code);
 +
             NRF_LOG_INFO("Enable notification.");
 +
            break;
 +
 
 +
</syntaxhighlight>我们来看下使能notify的函数的代码,不难发现其实就是一个write功能,不过不是上一个实验向我们的handle_value去发送数据,而是向cccd_handle去发送了一个0x01(BLE_GATT_HVX_NOTIFICATION),0x00的数据。<syntaxhighlight lang="c" line="1" start="179">
 +
//******************************************************************************
 +
// fn :cccd_configure
 +
//
 +
// brief : 用于向CCCD发送数据,控制使能或者禁能notify功能
 +
//
 +
// param : conn_handle -> 连接的句柄
 +
//        conn_handle -> CCCD的句柄
 +
//        enable -> 使能或者禁能标识
 +
//
 +
// return : none
 +
static uint32_t cccd_configure(uint16_t conn_handle, uint16_t cccd_handle, bool enable)
 +
{
 +
    uint8_t buf[BLE_CCCD_VALUE_LEN];
 +
 
 +
    buf[0] = enable ? BLE_GATT_HVX_NOTIFICATION : 0;
 +
    buf[1] = 0;
 +
 
 +
    ble_gattc_write_params_t const write_params =
 +
    {
 +
        .write_op = BLE_GATT_OP_WRITE_REQ,
 +
        .flags    = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
 +
        .handle  = cccd_handle,
 +
        .offset  = 0,
 +
        .len      = sizeof(buf),
 +
        .p_value  = buf
 +
    };
 +
 
 +
    return sd_ble_gattc_write(conn_handle, &write_params);
 +
}
 +
 
 +
//******************************************************************************
 +
// fn :ble_btn_c_tx_notif_enable
 +
//
 +
// brief : 用于使能从机btn服务的notify功能
 +
//
 +
// param : p_ble_btn_c -> 指向要关联的BTN结构实例的指针
 +
//
 +
// return : none
 +
uint32_t ble_btn_c_tx_notif_enable(ble_btn_c_t * p_ble_btn_c)
 +
{
 +
    VERIFY_PARAM_NOT_NULL(p_ble_btn_c);
 +
 
 +
    if ( (p_ble_btn_c->conn_handle == BLE_CONN_HANDLE_INVALID)
 +
      ||(p_ble_btn_c->peer_btn_db.cccd_handle == BLE_GATT_HANDLE_INVALID)
 +
      )
 +
    {
 +
        return NRF_ERROR_INVALID_STATE;
 +
    }
 +
    return cccd_configure(p_ble_btn_c->conn_handle,p_ble_btn_c->peer_btn_db.cccd_handle, true);
 +
}
 +
</syntaxhighlight>看完了使能notify的函数,我们来看下接收从机数据的处理部分,首先是看到ble_btn_c_on_ble_evt函数,这个函数在我们调用BLE_BTN_C_DEF(m_ble_btn_c);注册实例的时候,就已经创建好了,用于接收底层的softdevice的消息返回。
 +
 
 +
我们看下其中的BLE_GATTC_EVT_HVX(Handle Value Notification or Indication event)事件,在这个事件下我们接收到从机发送给我们的数据。<syntaxhighlight lang="c" line="1" start="146">
 +
//******************************************************************************
 +
// fn :ble_btn_c_on_ble_evt
 +
//
 +
// brief : BLE事件处理函数
 +
//
 +
// param : p_ble_evt -> ble事件
 +
//        p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
 +
//
 +
// return : none
 +
void ble_btn_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
 +
{
 +
    if ((p_context == NULL) || (p_ble_evt == NULL))
 +
    {
 +
        return;
 +
    }
 +
 
 +
    ble_btn_c_t * p_ble_btn_c = (ble_btn_c_t *)p_context;
 +
 
 +
    switch (p_ble_evt->header.evt_id)
 +
    {
 +
        case BLE_GATTC_EVT_HVX:
 +
            on_hvx(p_ble_btn_c, p_ble_evt);
 +
            break;
 +
     
 +
        case BLE_GAP_EVT_DISCONNECTED:
 +
            on_disconnected(p_ble_btn_c, p_ble_evt);
 +
             break;
 +
 
 +
        default:
 +
            break;
 +
    }
 +
}
 +
</syntaxhighlight>然后我们看下对于接收到的从机数据的处理,首先还是一样的,我们需要判断一下数据的来源是不是我们btn_handle。当确认都是正确的,然后我们将接收的数据复制给ble_btn_c_evt_t,然后通过它的回调上传到我们的main文件中,携带的事件ID为BLE_BTN_C_EVT_BTN_TX_EVT。<syntaxhighlight lang="c" line="1" start="32">
 +
//******************************************************************************
 +
// fn :on_hvx
 +
//
 +
// brief : 用于处理从SoftDevice接收到的通知
 +
//
 +
// param : p_ble_btn_c -> 指向BTN Client结构的指针
 +
//        p_ble_evt -> 指向接收到的BLE事件的指针
 +
//
 +
// return : none
 +
static void on_hvx(ble_btn_c_t * p_ble_btn_c, ble_evt_t const * p_ble_evt)
 +
{
 +
    if (  (p_ble_btn_c->peer_btn_db.btn_handle != BLE_GATT_HANDLE_INVALID)
 +
        && (p_ble_evt->evt.gattc_evt.params.hvx.handle == p_ble_btn_c->peer_btn_db.btn_handle)
 +
        && (p_ble_btn_c->evt_handler != NULL))
 +
    {
 +
        ble_btn_c_evt_t ble_btn_c_evt;
 +
 
 +
        ble_btn_c_evt.evt_type = BLE_BTN_C_EVT_BTN_TX_EVT;
 +
        ble_btn_c_evt.p_data  = (uint8_t *)p_ble_evt->evt.gattc_evt.params.hvx.data;
 +
        ble_btn_c_evt.data_len = p_ble_evt->evt.gattc_evt.params.hvx.len;
 +
 
 +
        p_ble_btn_c->evt_handler(p_ble_btn_c, &ble_btn_c_evt);
 +
        NRF_LOG_DEBUG("Client sending data.");
 +
    }
 +
}
 +
</syntaxhighlight>接下来返回到我们的main文件中,我们在ble_btn_c_evt_handler回调中可以看到BLE_BTN_C_EVT_BTN_TX_EVT事件的处理,我们将接收到的从机数据用于控制相应的LED灯点亮。<syntaxhighlight lang="c" line="1" start="297">
 +
         case BLE_BTN_C_EVT_BTN_TX_EVT:
 +
             NRF_LOG_DEBUG("Receiving data.");
 +
             NRF_LOG_HEXDUMP_DEBUG(p_evt->p_data, p_evt->data_len);
 +
             LED_Control(BSP_LED_0, p_evt->p_data[0]);
 +
            LED_Control(BSP_LED_1, p_evt->p_data[1]);
 +
            LED_Control(BSP_LED_2, p_evt->p_data[2]);
 +
             LED_Control(BSP_LED_3, p_evt->p_data[3]);
 +
             break;
 +
           
 +
         default:
 +
             break;
 +
     }
 +
}
 +
</syntaxhighlight>
 +
 
 +
===== 从机部分 =====
 +
 
 +
======gy_profile_btn.c\.h======
 +
首先我们还是先看一下服务配置文件,首先还是注册一下服务,注册的服务句柄是p_btn->service_handle。服务注册完成之后,我们注册按键的特征值,可以看到我们分别使能了按键的notify通知属性(add_char_params.char_props.notify = 1;),并且同样使能了read属性(add_char_params.char_props.read  = 1;)。
 +
 
 +
这里我们需要注意的是下面的cccd_write_access参数被使能,上一实验大家都好理解需要使能write_access和read_access,因为我们需要使用读写,那么为什么这个例程要使能cccd_write_access呢,这边给大家简单说明一下CCCD。{{Note|text=Client Characteristic Configuration Descriptor(CCCD)是客户端特征配置描述符。当主机向CCCD中写入0x0001,此时使能notify;当写入0x0000时,此时禁止notify。
 +
在nordic的协议栈当中,他的这个notify使能是交给用户自己处理的,也是说即便主机没有向cccd中写入0x0001去使能notify,我们同样可以直接利用notify去发送数据,只能这样不符合规范。|type=info}}<syntaxhighlight lang="c" line="1" start="50">
 +
//******************************************************************************
 +
// fn :ble_btn_init
 +
//
 +
// brief : 初始化BTN服务
 +
//
 +
// param : p_btn -> btn服务结构体
 +
//
 +
// return : uint32_t -> 成功返回SUCCESS,其他返回ERR NO.
 +
uint32_t ble_btn_init(ble_btn_t * p_btn)
 +
{
 +
    uint32_t              err_code;
 +
    ble_uuid_t            ble_uuid;
 +
    ble_add_char_params_t add_char_params;
 +
 
 +
    // 添加服务(128bit UUID)
 +
    ble_uuid128_t base_uuid = {BTN_UUID_BASE};
 +
    err_code = sd_ble_uuid_vs_add(&base_uuid, &p_btn->uuid_type);
 +
    VERIFY_SUCCESS(err_code);
 +
 
 +
    ble_uuid.type = p_btn->uuid_type;
 +
    ble_uuid.uuid = BTN_UUID_SERVICE;
 +
 
 +
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_btn->service_handle);
 +
    VERIFY_SUCCESS(err_code);
 +
 
 +
    // 添加BTN特征值(属性是Write和Read、长度是4)
 +
    memset(&add_char_params, 0, sizeof(add_char_params));
 +
    add_char_params.uuid            = BTN_UUID_CHAR;
 +
    add_char_params.uuid_type        = p_btn->uuid_type;
 +
    add_char_params.init_len        = BTN_UUID_CHAR_LEN;
 +
    add_char_params.max_len          = BTN_UUID_CHAR_LEN;
 +
    add_char_params.char_props.read  = 1;
 +
    add_char_params.char_props.notify = 1;
 +
   
 +
    add_char_params.read_access  = SEC_OPEN;
 +
    add_char_params.cccd_write_access = SEC_OPEN;
 +
 
 +
    return characteristic_add(p_btn->service_handle, &add_char_params, &p_btn->btn_char_handles);
 +
}
 +
</syntaxhighlight>服务部分剩下的处理流程和上一实验是类型的,只不过上一个实验是处理的wirte属性,而这个实验是处理notify属性。
 +
 
 +
首先在BLE事件处理的函数中,我们应该要处理CCCD_Write的数据的,所以在由softdevice返回消息的ble_btn_on_ble_evt函数中,我们需要处理一下BLE_GATTS_EVT_WRITE事件。<syntaxhighlight lang="c" line="1" start="30">
 +
//******************************************************************************
 +
// fn :ble_led_on_ble_evt
 +
//
 +
// brief : BLE事件处理函数
 +
//
 +
// param : p_ble_evt -> ble事件
 +
//        p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
 +
//
 +
// return : none
 +
//******************************************************************************
 +
// fn :ble_btn_on_ble_evt
 +
//
 +
// brief : BLE事件处理函数
 +
//
 +
// param : p_ble_evt -> ble事件
 +
//        p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
 +
//
 +
// return : none
 +
void ble_btn_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
 +
{
 +
    ble_btn_t * p_btn = (ble_btn_t *)p_context;
 +
 
 +
    switch (p_ble_evt->header.evt_id)
 +
    {
 +
        case BLE_GATTS_EVT_WRITE:
 +
            on_write(p_btn, p_ble_evt);
 +
            break;
 +
           
 +
        default:
 +
            break;
 +
    }
 +
}
 +
</syntaxhighlight>在这个on_write函数中,我们接收到了主机发送过来的使能从机notify的数据,我们需要判断一下接收的数据的句柄是不是cccd_handle,以及接收的数据长度是不是2字节(使能数据:01 00)。如果成功使能了notify,那么我们打印"notification enabled"。<syntaxhighlight lang="c" line="1" start="10">
 +
static void on_write(ble_btn_t * p_btn, ble_evt_t const * p_ble_evt)
 +
{
 +
    ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
 +
   
 +
    if ((p_evt_write->handle == p_btn->btn_char_handles.cccd_handle) &&
 +
        (p_evt_write->len == 2))
 +
    {
 +
      if (ble_srv_is_notification_enabled(p_evt_write->data))
 +
      {
 +
          p_client.is_notification_enabled = true;
 +
          NRF_LOG_INFO("notification enabled");
 +
      }
 +
      else
 +
      {
 +
          p_client.is_notification_enabled = false;
 +
          NRF_LOG_INFO("notification disabled");
 +
      }
 +
    }
 +
}
 +
</syntaxhighlight>除了上述的3个函数,我们的从机服务文件,就只剩下一个发送notify数据的函数了。首先我们一定要先判断一下是否已经使能的notify使能,并且判断数据长度是否符合要求。
 +
 
 +
下面这个函数,就是我们notify发送数据的函数,他的参数我们只需要配置4个。
 +
 
 +
type配置为BLE_GATT_HVX_NOTIFICATION,代表是notify属性的数据;
 +
 
 +
handle我们需要配置为我们按键特征值的value.handle,代表的是按键特征值的Value这个列表的句柄;
 +
 
 +
剩下的p_data和p_len就是我们需要发送的数据以及数据的长度。<syntaxhighlight lang="c" line="1" start="95">
 +
//******************************************************************************
 +
// fn :ble_btn_data_send
 +
//
 +
// brief : 处理按键按下,状态更新的事件
 +
//
 +
// param : p_btn -> btn结构体
 +
//        p_data -> 数据指针
 +
//        p_length -> 数据长度
 +
//        conn_handle -> 连接的句柄
 +
//
 +
// return : none
 +
uint32_t ble_btn_data_send(ble_btn_t *p_btn, uint8_t *p_data, uint16_t p_length, uint16_t conn_handle)
 +
{
 +
 
 +
    ble_gatts_hvx_params_t    hvx_params;
 +
 
 +
    VERIFY_PARAM_NOT_NULL(p_btn);
 +
 
 +
    if (conn_handle == BLE_CONN_HANDLE_INVALID)
 +
    {
 +
        return NRF_ERROR_NOT_FOUND;
 +
    }
 +
 
 +
    if (!p_client.is_notification_enabled)
 +
    {
 +
        return NRF_ERROR_INVALID_STATE;
 +
    }
 +
 
 +
    if (p_length > BTN_UUID_CHAR_LEN)
 +
    {
 +
        return NRF_ERROR_INVALID_PARAM;
 +
    }
 +
 
 +
    memset(&hvx_params, 0, sizeof(hvx_params));
 +
    hvx_params.type  = BLE_GATT_HVX_NOTIFICATION;
 +
    hvx_params.handle = p_btn->btn_char_handles.value_handle;
 +
    hvx_params.p_data = p_data;
 +
    hvx_params.p_len  = &p_length;
 +
 
 +
    return sd_ble_gatts_hvx(conn_handle, &hvx_params);
 +
}
 +
</syntaxhighlight>
 +
======main.c======
 +
首先我们还是需要添加一下服务初始化函数。其他的处理都是在gy_profile_btn文件中,所以main文件中就只剩下一个按键触发后调用notify发送的部分。<syntaxhighlight lang="c" line="1" start="194">
 +
//******************************************************************
 +
// fn : services_init
 +
//
 +
// brief : 初始化复位(本例程展示NUS:Nordic Uart Service)
 
//
 
//
 
// param : none
 
// param : none
第2,554行: 第3,193行:
 
{
 
{
 
     uint32_t          err_code;
 
     uint32_t          err_code;
    ble_nus_init_t    nus_init;
 
 
    // Initialize NUS.
 
    memset(&nus_init, 0, sizeof(nus_init));
 
 
    nus_init.data_handler = nus_data_handler;
 
  
     err_code = ble_nus_init(&m_nus, &nus_init);
+
     err_code = ble_btn_init(&m_btn);
 
     APP_ERROR_CHECK(err_code);
 
     APP_ERROR_CHECK(err_code);
 
}
 
}
</syntaxhighlight>
+
</syntaxhighlight>当有按键按下时,最终会将按键消息传递到这个回调中进行处理,我们根据按键触发的消息,对相应的buf值进行修改,最后调用ble_btn_data_send函数将数据发送给主机。<syntaxhighlight lang="c" line="1" start="351">
 
 
==== 实验总结 ====
 
经过j这一章节的学习,大家需要了解的要点如下:
 
 
 
1、从机如何初始化注册一个服务,不仅仅是NUS,包含了协议栈SDK中的已有的服务
 
 
 
2、主机如何去发起对连接的从机的服务的发现
 
 
 
3、了解服务相关的回调函数中的事件的含义
 
 
 
=== NUS通信实验 ===
 
 
 
==== 实验简介 ====
 
上一章节,我们已经学习过了如何注册和发现一个服务,那么这一章节,我们将给大家介绍,如何利用这个服务去完成通信。
 
 
 
服务中的特征值的属性分为如下4种:Write写、Read读、Notify通知、Indicate暗示。
 
 
 
这边我们只给大家介绍Write与Notify,Write属性是主机向从机写,Notify属性是从机通知主机(通知属性的作用与否,需要由主机获取服务后使能)。
 
 
 
==== 实验现象 ====
 
这一章的实验,打印的“乱七八糟”的内容比较多,原因在于我们调高了LOG打印的等级,我们将等级调成了DEBUG档位(最高级别),有关LOG等级的说明请大家查看本手册的LOG打印实验说明。
 
 
 
其中整个的流程,还是上电先扫描,然后发起连接,或者连接参数,更新MTU大小,最后再获取NUS服务。
 
 
 
当成功获取NUS服务之后,我们向从机Wirte“AAA”。
 
[[文件:Nrf rtt 110.png|边框|居中|无框|878x878像素]]
 
主机上电等待连接,连接成功后,获取连接参数,更新MTU大小。
 
 
 
当被主机使能Notify之后,我们向主机Notify“BBB”。
 
[[文件:Nrf rtt 210.png|边框|居中|无框|656x656像素]]
 
 
 
==== 工程及源码讲解 ====
 
 
 
===== 主机部分 =====
 
源码讲解的部分,我们接着上一章的NUS服务获取之后,开始说明,我们是怎么进行数据的收发的。
 
 
 
====== ble_nus_c_evt_handler()函数 ======
 
主机的nus client回调函数,相对于上一章节,在BLE_NUS_C_EVT_DISCOVERY_COMPLETE服务发现完成的事件中,我们开启了一个周期定时器任务。
 
 
 
这边需要注意,我们接收到从机Notify的数据,是由BLE_NUS_C_EVT_NUS_TX_EVT事件返回,我们可以在这边获取接收的数据。<syntaxhighlight lang="c" line="1" start="244">
 
//******************************************************************
 
// fn : ble_nus_c_evt_handler
 
//
 
// brief : NUS事件
 
//
 
// param : none
 
//
 
// return : none
 
static void ble_nus_c_evt_handler(ble_nus_c_t * p_ble_nus_c, ble_nus_c_evt_t const * p_ble_nus_evt)
 
{
 
    ret_code_t err_code;
 
 
 
    switch (p_ble_nus_evt->evt_type)
 
    {
 
        case BLE_NUS_C_EVT_DISCOVERY_COMPLETE:
 
            NRF_LOG_INFO("Discovery complete.");
 
            err_code = ble_nus_c_handles_assign(p_ble_nus_c, p_ble_nus_evt->conn_handle, &p_ble_nus_evt->handles);
 
            APP_ERROR_CHECK(err_code);
 
 
 
            err_code = ble_nus_c_tx_notif_enable(p_ble_nus_c);
 
            APP_ERROR_CHECK(err_code);
 
            NRF_LOG_INFO("Connected to device with Nordic UART Service.");
 
           
 
            app_timer_start(m_timer_nus, APP_TIMER_TICKS(1000), NULL);
 
            break;
 
 
 
        case BLE_NUS_C_EVT_NUS_TX_EVT:
 
            NRF_LOG_DEBUG("Receiving data.");
 
            NRF_LOG_HEXDUMP_DEBUG(p_ble_nus_evt->p_data, p_ble_nus_evt->data_len);
 
            break;
 
 
 
        case BLE_NUS_C_EVT_DISCONNECTED:
 
            NRF_LOG_INFO("Disconnected.");
 
            break;
 
        default:
 
            break;
 
    }
 
}
 
 
 
</syntaxhighlight>那么接下来我们看一下定时器任务中处理了什么,可以看到我们初始化了一个周期定时器,这个定时器是1s执行一次,没过1s之后,就会返回一次nus_timeout_handler。
 
 
 
在这个nus_timeout_handler函数中,我们调用ble_nus_c_string_send()函数,去向从机设备发送AAA。<syntaxhighlight lang="c" line="1" start="458">
 
 
//******************************************************************
 
//******************************************************************
// fn : nus_timeout_handler
+
// fn : btn_evt_handler_t
 
//
 
//
// brief : NUS定时器超时任务
+
// brief : 按键触发回调函数
 
//  
 
//  
// param : p_event -> 指向数据库发现事件的指针
+
// param : butState -> 当前的按键值
 
//
 
//
 
// return : none
 
// return : none
static void nus_timeout_handler(void * p_context)
+
void btn_evt_handler_t (uint8_t butState)
 
{
 
{
    ble_nus_c_string_send(&m_ble_nus_c, "AAA", 3);
+
  uint8_t buf[BTN_UUID_CHAR_LEN] = {0x01,0x01,0x01,0x01};
}
+
  switch(butState)
 
+
  {
//******************************************************************
+
    case BUTTON_1:
// fn : timer_init
+
      buf[0] = 0x00;
//
+
      break;
// brief : 初始化定时器
+
    case BUTTON_2:
//
+
      buf[1] = 0x00;
// param : none
+
      break;
//
+
    case BUTTON_3:
// return : none
+
      buf[2] = 0x00;
static void timer_init(void)
+
      break;
{
+
     case BUTTON_4:
     ret_code_t err_code = app_timer_init();
+
      buf[3] = 0x00;
     APP_ERROR_CHECK(err_code);
+
      break;
   
+
     default:
    err_code = app_timer_create(&m_timer_nus,APP_TIMER_MODE_REPEATED,nus_timeout_handler);
+
      break;
    APP_ERROR_CHECK(err_code);
+
  }
 +
  ble_btn_data_send(&m_btn, buf, BTN_UUID_CHAR_LEN, m_conn_handle);
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
===== 从机部分 =====
+
==== <span> </span>实验总结 ====
我们从NUS的回调函数中,可以看到,我们利用BLE_NUS_EVT_RX_DATA事件接收主机Write的数据。
+
经过本章内容的学习,我们需要对notify属性的服务有一定的了解,主要掌握以下3点:
 
 
而当我们的Notification被使能的时候,也就是BLE_NUS_EVT_COMM_STARTED事件返回,我们通用会开启一个定时器任务。<syntaxhighlight lang="c" line="1" start="195">
 
//******************************************************************
 
// fn : nus_data_handler
 
//
 
// brief : 用于处理来自Nordic UART服务的数据的功能
 
// details : 该功能将处理从Nordic UART BLE服务接收的数据并将其发送到UART模块
 
//
 
// param : ble_nus_evt_t -> nus事件
 
//
 
// return : none
 
static void nus_data_handler(ble_nus_evt_t * p_evt)
 
{
 
    if (p_evt->type == BLE_NUS_EVT_RX_DATA)
 
    {
 
        NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
 
        NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
 
    }
 
    else if (p_evt->type == BLE_NUS_EVT_TX_RDY)
 
    {
 
        NRF_LOG_DEBUG(" Service is ready to accept new data to be transmitted..");
 
    }
 
    else if(p_evt->type == BLE_NUS_EVT_COMM_STARTED)
 
    {
 
      NRF_LOG_DEBUG("NUS Notification Enable.");
 
      app_timer_start(m_timer_nus, APP_TIMER_TICKS(1000), NULL);
 
    }
 
    else if (p_evt->type == BLE_NUS_EVT_COMM_STOPPED)
 
    {
 
        NRF_LOG_DEBUG("NUS Notification Disable.");
 
    }
 
}
 
</syntaxhighlight>接下来我们同样看一下我们的定时器任务,我们初始化了一个周期的定时器任务,这个定时器任务在上方BLE_NUS_EVT_COMM_STARTED事件中被设置为1s周期。
 
 
 
也就是每过1s都会进入一次nus_timeout_handler()函数,在这个函数中,我们调用ble_nus_data_send()函数,向主机Notify数据。<syntaxhighlight lang="c" line="1" start="343">
 
//******************************************************************
 
// fn : nus_timeout_handler
 
//
 
// brief : NUS定时器超时任务
 
//
 
// param : p_event -> 指向数据库发现事件的指针
 
//
 
// return : none
 
static void nus_timeout_handler(void * p_context)
 
 
    uint16_t len = 3;
 
    ble_nus_data_send(&m_nus, "BBB", &len, m_conn_handle);
 
}
 
  
//******************************************************************
+
1、我们需要了解主机如何去使能从机的notify功能
// fn : timers_init
 
//
 
// brief : 初始化定时器功能
 
//
 
// param : none
 
//
 
// return : none
 
static void timers_init(void)
 
{
 
    ret_code_t err_code = app_timer_init();
 
    APP_ERROR_CHECK(err_code);
 
   
 
    err_code = app_timer_create(&m_timer_nus,APP_TIMER_MODE_REPEATED,nus_timeout_handler);
 
    APP_ERROR_CHECK(err_code);
 
}
 
</syntaxhighlight>
 
  
==== 实验总结 ====
+
2、从机如何创建一个支持notify功能的特征值服务
这一章的学习,我们是建立在已经获取了NUS服务的基础上进行的,所以掌握的要点也比较少。
 
  
1、主机如何接收和Write数据
+
3、从机如何发送一个notify功能的数据包给主机
  
2、从机如何接收和Notify数据
 
 
[[分类:NRF52832DK]]
 
[[分类:NRF52832DK]]
 
[[分类:实验手册]]
 
[[分类:实验手册]]

2020年5月20日 (三) 14:14的最新版本

我们的蓝牙协议实验部分,将会给大家带来最直观的蓝牙协议部分的学习,我们通过拆分的方式,带领大家深入了解蓝牙协议的主要功能部分。

在进行蓝牙协议实验学习之前,我们有一些基础但十分重要的蓝牙协议介绍分享给大家,这个部分的蓝牙协议不像蓝牙协议手册那样,对每个参数进行说明,洋洋洒洒会有好几千页的说明文档,这样大家也很难去看完并记忆。

我们针对的仅仅是大家在利用协议栈开发个人需求的时候,绕不开的蓝牙协议部分的内容进行介绍。首先我们先分析一下蓝牙主从机通信,都有哪些重要点。

主机部分:扫描、发起连接、发现服务、通信。

从机部分:广播、注册服务、被连接、通信。

蓝牙主从机通信协议要点
主机 从机
扫描 连接 服务 通信 广播 服务 连接 通信
扫描参数① 连接参数③ 服务句柄⑦ 属性Write⑨ 广播数据② 服务⑥ 连接参数③ 属性Notify⑨
连接句柄④ 使能通知⑧ 连接句柄④
MTU大小⑤ MTU大小⑤
Icon-tips.png
实验源码位于百度云盘归档资料中:归档资料/1-协议栈SDK/谷雨实验源码包/,代码的使用请参考《NRF52832DK入门手册》的蓝牙协议栈SDK一节

目录

1 扫描参数①

1.1 主机扫描核心参数

主机扫描核心参数主要是4个,也就是说这4个参数是不可获取的,必须要配置的。分别是扫描间隔interval、扫描窗口window、扫描持续时长duration、扫描模式active。

扫描间隔interval:两个连续的扫描窗口的起始时间的时间差,这个很好理解,就是第一次扫描起始时间和第二次扫描起始时间的时间差。

扫描窗口window:就是指的单次扫描的时间。

扫描持续时长duration:我们发起一次扫描持续的时间,发起一次扫描包含了N个扫描窗口。

所以我们的扫描参数需要满足如下条件:

1、duration ≥ interval ≥ window

2、协议规定window和interval不能超过10.24s

协议栈中一些特殊参数情况的处理:

1、duration设置为0,持续扫描。也就是说设置为0代表无穷大

2、duration小于interval,则至少广播一次

扫描模式:

1、active配置1,主动扫描模式,可获取从设备的广播数据以及扫描回调数据

2、active配置0,被动扫描模式,只可以获取从设备的广播数据

BLE技术 扫描窗口和扫描间隔.jpg
1.2 主机扫描特殊应用参数

extended:这个是用于BLE5.0协议中新增的大广播包数据,定义为1,才可以获取到外部大广播包

filter_policy:过滤扫描的参数,这个是用于我们扫描的时候,过滤出我们想要的设备

scan_phys:扫描的PHYs,这个同样是BLE5.0协议中新增的物理层协议,分别BLE_GAP_PHY_1MBPS、BLE_GAP_PHY_2MBPS等

channel_mask:扫描的信道(暂时不清楚此参数如何使用)

2 广播数据②

2.1 BLE4.x广播数据

BLE4.x的蓝牙广播数据包,最大是31byte,遵循的方式如下,首先是数据的长度、紧接着是数据类型,最后才是数据内容。

数据长度:某一个类型的数据长度,注意长度包含数据类型及数据内容。

数据类型:BLE协议规定的数据类型,例如可发现标志flag是0x01,local name是0x09。

数据内容:用户自定义数据。

Icon-tips.png
包含广播数据和扫描回调数据:

所有的广播数据内容,用户都是可以自定义,但一定要符合上面所说的数据结构,不然BLE设备无法正确识别。

广播包的数据格式
数据长度 数据类型 数据内容
0x02 0x01(flag 可发现标志) 0x06
0x06 0x09(name 本地名称) 0x4759303031(“GY001”)
... ... ...
2.2 BLE5.x新增大广播包数据

3 连接参数③

参数如下:

Connection Interval连接间隔:在BLE连接中,使用跳频方案。两个设备在特定时间仅在特定频道上彼此发送和接收数据。这些设备稍后在新的通道(协议栈的链路层处理通道切换)上通过这个约定的时间相遇。这次用于收发数据的相遇称为连接事件。如果没有要发送或接收的应用数据,则两台设备交换链路层数据来维护连接。两个连接事件之间的时间跨度,是以1.25 ms为单位,连接间隔的范围从最小值6(7.5 ms)到最大值3200(4.0 s)。

不同的应用可能需要不同的连接间隔。

连接参数1.jpg

Slave Latency从机延迟,此参数为从机(外设设备)提供跳过多个连接事件的能力。这种能力给外设设备更多的灵活性。如果外设没有要发送的数据,则可以跳过连接事件,保持睡眠并节省电量。外设设备选择是否在每个连接事件时间点上唤醒。虽然外设可以跳过连接事件,但不能超出从延迟参数允许的最大值。

连接参数2.jpg

Supervision Time-out监控超时,是两次成功连接事件之间的最长时间。如果在此时间内没有成功的连接事件,设备将终止连接并返回到未连接状态。该参数值以10 ms为单位,监控超时值可以从最小值10(100 ms)到3200(32.0 s)。超时必须大于有效的连接间隔。

3.1 Effective Connection Interval有效连接间隔

有效连接间隔等于两个连接事件之间的时间跨度,假设从机跳过最大数量的连接事件,且允许从机延迟(如果从机延迟设置为0,则有效连接间隔等于实际连接间隔,)。

从机延迟表示可以跳过的最大事件数。该数字的范围可以从最小值0(意味着不能跳过连接事件)到最大值499。最大值不能使有效连接间隔(见下列公式)大于16秒。间隔可以使用以下公式计算:

Effective Connection Interval = (Connection Interval) × (1 + [Slave Latency])

Consider the following example:

  • Connection Interval: 80 (100 ms)
  • Slave Latency: 4
  • Effective Connection Interval: (100 ms) × (1 + 4) = 500 ms

当没有数据从从机发送到主机时,从机每500ms一个连接事件交互一次。

3.2 连接参数的注意事项

在许多应用中,从机跳过最大连接事件数。选择正确的连接参数组在低功耗蓝牙设备的功率优化中起重要作用。以下列表给出了连接参数设置中权衡的总体概述。

减少连接间隔如下:

  • 增加两个设备的功耗
  • 增加双向吞吐量
  • 减少任一方向发送数据的时间

增加连接间隔如下:

  • 降低两个设备的功耗
  • 降低双向吞吐量
  • 增加任一方向发送数据的时间

减少从机延迟(或将其设置为零)如下:

  • 增加外围设备的功耗
  • 减少外围设备接收从中央设备发送的数据的时间

增加从机延迟如下:

  • 在周边没有数据发送期间,可以降低外设的功耗到主机设备
  • 增加外设设备接收从主机设备发送的数据的时间

4 连接句柄④

设备的连接句柄范围是从0x0000-0xFFFD,当然我们实际使用,也连接不了这么多的设备。

连接的句柄是按照连接的先后顺序分配,开发者无法自己指定,也就是说第一个与之连接的设备将会被分配连接句柄(connHandle)0x0000,第二个是0x0001,以此类推。

为什么句柄的最终范围是0xFFFD,因为后面的0xFFFE与0xFFFF是有特殊用于含义的。

0xFFFE:代表的是正在连接的设备的句柄。当我们发起对一个设备的连接,但是迟迟没有连接上,我们可以调用断开连接的函数,利用这个0xFFFE句柄断开这个连接。

0xFFFF:断开的句柄。当设备与之断开连接之后,句柄就会返回为0xFFFF。

5 MTU大小⑤

MTU的大小,在BLE4.0的是时候,最大是只有27byte,当更新到BLE4.1向后,我们支持的MTU最大是251字节。

低功耗蓝牙协议栈支持链路层L2CAP PDU的分片和重组。这种分段支持允许构建在L2CAP之上的L2CAP和更高级协议(如属性协议(ATT))使用较大的有效负载大小,并减少与较大数据事务相关的开销。当使用分片时,较大的分组被分割成多个链路层分组,并由对等体设备的链路层重新组合。

MTU1.jpg

L2CAP PDU的大小还定义了属性协议最大传输单元(ATT_MTU)的大小。默认情况下,LE设备假设L2CAP PDU的大小为27字节,这对应于可以在单个连接事件数据包中传输的LE数据包的最大大小。在这种情况下,L2CAP协议头为4字节,导致ATT_MTU的默认大小为23。

注意: 使用LE数据长度分机功能时,LE包的长度最多可达251字节。

6 服务⑥

7 服务句柄⑦

8 使能通知⑧

9 属性⑨

10 蓝牙协议实验目录

蓝牙协议实验部分,我们借由串口透传实验,一步一步拆分,给大家介绍蓝牙的协议方面。

下面是我们的实验列表,里面的实验是循序渐进的进行深入讲解蓝牙协议的,建议大家不要跳跃学习。

实验列表
主机实验编号 主机实验内容
1.0_ble_central_pm 主机低功耗实验
1.1_ble_central_log 主机LOG打印实验
1.2_ble_central_scan_all 主机通用扫描实验
1.3_ble_central_scan_filter 主机过滤扫描实验
1.4_ble_central_scan_whitelist 主机白名单扫描实验
1.5_ble_central_conn_all 主机通用连接实验
1.6_ble_central_conn_filter 主机过滤连接实验
1.7_ble_central_update_connParam 主机连接参数更新实验
1.8_ble_central_update_mtu 主机MTU大小配置实验
1.9_ble_central_profile_led 主机服务Client实验(Write/Read属性)
1.0_ble_central_profile_btn 主机服务Client实验(Notify属性)
1.11_ble_central_profile_nus 主机获取NUS服务实验
1.12_ble_central_nus_communication 主机利用NUS服务收发通信实验
2.0_ble_peripheral_pm 从机低功耗实验
2.1_ble_peripheral_log 从机LOG打印实验
2.2_ble_peripheral_adv_all 从机通用广播实验
2.3_ble_peripheral_adv_filter 从机过滤广播实验
2.4_ble_peripheral_adv_whitelist 从机白名单广播实验
2.5_ble_peripheral_conn_all 从机通用连接实验
2.6_ble_peripheral_conn_filter 从机过滤连接实验
2.7_ble_peripheral_update_connParam 从机连接参数请求更新实验
2.8_ble_peripheral_update_mtu 从机MTU大小配置实验
2.9_ble_peripheral_profile_led 从机服务Server实验(Write属性)
2.10_ble_peripheral_profile_btn 从机服务Server实验(Notify属性)
2.11_ble_peripheral_profile_nus 从机注册NUS服务实验
2.12_ble_peripheral_nus_communication 从机利用NUS服务收发通信实验

11 样例工程结构简介

我们首先借由协议栈自带的串口透传例程,给大家说明一下蓝牙工程的结构,我们打开工程,查看workspace部分(每一个Group都是同等地位,以下说明不分先后)。

Workspace group.png

Application:主要就两个文件,一个是主函数 main.c 文件,这个文件是大家后续编程主要修改的文件。另一个 sdk_config.h 配置文件,NRF52相关的蓝牙参数、外设参数等等,我们在使用之前都需要在这个文件中配置或者是使能。

Workspace application.png

Board Definition:nordic给我们用户实现好的有关按键和LED灯的控制的文件,这个文件主要是nordic方便自己例程的展示实现的,在后续的编程中大家可以选择使用,或者自己根据个人需要重新实现。

Workspace boarddefinition.png

Board Support:这个分组和Board Definition是一起作用的,就是利用按键和LED灯进行一些功能的展示,比如按键的外部中断唤醒,比如用LED来指示当前蓝牙的状态等。也是一样的,后续编程中大家可以根据个人需要选择保留还是自己实现。

Workspace boardsupport.png

None:这个分组也是包含了两个文件,这两个文件都是和芯片相关的。arm_startup_nrf52.s 是芯片的启动文件,这个文件配置了芯片启动时的堆栈空间,中断向量等等参数。system_nrf52.c 文件是芯片的系统文件,配置了芯片的RAM、时钟、射频以及引脚端口等等。这两个文件都是芯片的必要文件,所以每一个工程中都是需要包含的。

Workspace none.png

nRF_BLE:这个分组包含的是蓝牙协议相关的配置文件,也就是我们协议栈实验部分主要要讲解的内容。主要有如下几个部分,广播、连接、扫描等等,由于我们展示的例程是串口透传从机,所以这边看不到扫描相关的文件。

Workspace nrfble.png

nRF_BLE_Services:这个分组是用于存放我们的蓝牙profile服务文件,像我们串口透传例程,就包含了NUS(nordic uart service)的通用配置文件。

Workspace nrfbleservices.png

nRF_Drivers:这个分组包含的是外设驱动文件,其中前缀是nrf开头的代表的是老的驱动文件,nrfx代表的新的驱动文件,我们当前使用的SDK15.2兼容新旧两种外设驱动文件。

Workspace nrfdrivers.png

nRF_Libraries:库函数文件的分组,里面包含了两个大类。一个是以app为前缀的文件,这部分是留给我们用户在应用层调用的库文件,基本上是按键、时钟等等一些外设的库。另一部分是以nrf为前缀的库文件,这个是和芯片相关的库,包括内存分配、打印以及电源管理等等。

Workspace nrflibraries.png

nRF_Log:这个是nordic做好的一个打印调试信息的功能分组,主要分为两个,一个是利用Jlink仿真器实现的RTT,另一个则是利用串口打印。这个部分的功能在下面的实验中有讲解。

Workspace nrflog.png

nRF_Segger_RTT:这个部分是Segger公司实现好的RTT的驱动文件,我们使用的时候只需要将文件添加到我们的工程中,然后调用API接口就行,这样我们就能将调试信息通过Jlink打印到RTT Viewer显示。这个部分的功能是配置上面的Log打印使用的,方便我们开发者调试程序。

Workspace nrfseggerrtt.png

nRF_SoftDevice:这里包含的文件主要是配置协议栈初始化的时候协议栈的参数设定,由于协议栈实际上是不开源的,而是留下了配置接口,这些配置接口通过客户配置相关协议栈的参数来设置协议栈运行状态。也就是在我们的stack初始化部分去初始化softdevice。

Workspace softdevice.png

UTF8/UTF16 convert:utf8与utf16之间的转换。

Workspace utf8utf16.png

Output:文件输出分组,里面存放了两个文件,一个是图像数据文件.map,一个是可执行的.out文件。

Workspace output.png

12 主从机最小工程(低功耗实验)

12.1 实验简介

低功耗实验1.0_ble_central_pm与2.0_ble_peripheral_pm,这两个实验给大家带来的是最精简的主机以及从机例程,精简到什么程度呢,只保留了协议栈初始化以及电源管理部分。利用此实验,大家可以测试一下我们的BLE工程进入低功耗模式下的功耗情况。

12.2 实验现象

我们将万用表串联到电路中,并且打到电流档,此时我们可以看到功耗如下。

12.3 工程及源码讲解

12.3.1 主机部分
12.3.1.1 main()函数

首先我们查看一下main.c文件,在此文件的mian()函数中,我们首先初始化了电源管理模块,然后初始化了BLE栈堆,最后在while大循环中我们调用空闲状态处理的函数。

接下来我们分别针对这三个部分进行介绍。

170 //******************************************************************
171 // fn : main
172 //
173 // brief : 主函数
174 //
175 // param : none
176 //
177 // return : none
178 int main(void)
179 {
180     // 初始化
181     power_management_init();// 初始化电源控制
182     ble_stack_init();       // 初始化BLE栈堆
183     
184     // 进入主循环
185     for (;;)
186     {
187         idle_state_handle();   // 空闲状态处理
188     }
189 }
12.3.1.2 ble_stack_init()函数及ble_evt_handler()回调函数

我们查看一下BLE协议栈初始化,这个部分是一个格式化的东西。

首先调用nrf_sdh_enable_request()函数请求使能softdevice,原因在于ble协议、时钟、错误的回调以及中断的配置等,都需要这个sd(softdevice)支持。

接下来我们要配置默认的ble协议,主要包含了RAM起始地址、本工程的角色(根据设备支持连接的角色来判断)、MTU的大小及UUID和属性表大小。

RAM的起始地址在下面的位置可以看到,我们打开\ble_ghostyu\1.0_ble_central_pm\LaunchIOT\s132\iar下的ble_app_ghostyu_iar_nRF5x.icf文件(将此文件拖到IAR中就可以打开,可以看到__ICFEDIT_region_RAM_start__ = 0x200029e0)。

然后我们携带RAM起始地址,使能BLE协议栈。

在最后,我们注册了一个ble_evt_handler回调,在这个回调中我们处理BLE的事件返回。

102 //******************************************************************
103 // fn : ble_stack_init
104 //
105 // brief : 用于初始化BLE协议栈
106 // details : 初始化SoftDevice、BLE事件中断
107 //
108 // param : none
109 //
110 // return : none
111 static void ble_stack_init(void)
112 {
113     ret_code_t err_code;
114 
115     // SD使能请求,配置时钟,配置错误回调,中断(中断优先级栈堆默认设置)
116     err_code = nrf_sdh_enable_request();
117     APP_ERROR_CHECK(err_code);
118 
119     // SD默认配置(如下),SD RAM起始地址配置(0x200029e0)
120     // 作为从机时的最大连接数量0
121     // 作为主机时的最大连接数据1(本工程是主机)
122     // 初始化MTU大小23
123     // 供应商特定的UUID数量1
124     // 属性表大小248(必须是4的倍数,以字节为单位)
125     // 配置服务更改特性数量0
126     uint32_t ram_start = 0;
127     err_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
128     APP_ERROR_CHECK(err_code);
129 
130     // 使能BLE栈堆
131     err_code = nrf_sdh_ble_enable(&ram_start);
132     APP_ERROR_CHECK(err_code);
133 
134     // 注册BLE事件的处理程序,所有BLE的事件都将分派ble_evt_handler回调
135     NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
136 }

ble_evt_handler回调函数,用于处理BLE的事件回调,包含了COMMON、GAP、GATT Client、GATT Server、L2CAP等多种事件类型。在这个例程中,我们只给大家保留了最基础的两个GAP状态,分别为连接状态和断开连接状态。

 68 //******************************************************************
 69 // fn : ble_evt_handler
 70 //
 71 // brief : BLE事件回调
 72 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
 73 //
 74 // param : ble_evt_t  事件类型
 75 //         p_context  未使用
 76 //
 77 // return : none
 78 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
 79 {
 80     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
 81 
 82     switch (p_ble_evt->header.evt_id)
 83     {
 84         // 连接
 85         case BLE_GAP_EVT_CONNECTED:
 86             NRF_LOG_INFO("Connected. conn_handle: 0x%x",p_gap_evt->conn_handle);
 87             break;
 88 
 89         // 断开连接
 90         case BLE_GAP_EVT_DISCONNECTED:
 91 
 92             NRF_LOG_INFO("Disconnected. conn_handle: 0x%x, reason: 0x%x",
 93                          p_gap_evt->conn_handle,
 94                          p_gap_evt->params.disconnected.reason);
 95             break;
 96 
 97         default:
 98             break;
 99     }
100 }
12.3.1.3 power_management_init()及idle_state_handle()函数

power_management_init()函数调用底层的nrf_pwr_mgmt_init()函数去初始化电源管理的部分。

idle_state_handle()函数调用底层的nrf_pwr_mgmt_run()函数,用于处理空闲状态的功能(处理完所有的挂起事件,然后进入休眠,直到下一个事件发生)。

138 //******************************************************************
139 // fn : power_management_init
140 //
141 // brief : 初始化电源管理
142 //
143 // param : none
144 //
145 // return : none
146 static void power_management_init(void)
147 {
148     ret_code_t err_code;
149     err_code = nrf_pwr_mgmt_init();
150     APP_ERROR_CHECK(err_code);
151 }
152 
153 //******************************************************************
154 // fn : idle_state_handle
155 //
156 // brief : 处理空闲状态的功能(用于主循环)
157 // details : 处理任何挂起的日志操作,然后休眠直到下一个事件发生
158 //
159 // param : none
160 //
161 // return : none
162 static void idle_state_handle(void)
163 {
164     if (NRF_LOG_PROCESS() == false)
165     {
166         nrf_pwr_mgmt_run();
167     }
168 }
12.3.2 从机部分
12.3.2.1 ble_stack_init()函数

从机部分大体上是和主机一样的,在nordic的协议栈例程中,(如果大家对BLE协议有一定的了解或者使用过其他厂家的BLE芯片)我们可以发现,nordic为了简化BLE的开发难度,可谓是不择手段,他减掉了很多的ble协议相关的内容(这里的减掉指的是放到底层处理,不需要开发者去配置),这其中就包含了GAP Role,也就是蓝牙的角色。

ble_satck_init()函数在主机与从机中,唯一不同的点在初始化参数配置。

 99 //******************************************************************
100 // fn : ble_stack_init
101 //
102 // brief : 用于初始化BLE协议栈
103 // details : 初始化SoftDevice、BLE事件中断
104 //
105 // param : none
106 //
107 // return : none
108 static void ble_stack_init(void)
109 {
110     ret_code_t err_code;
111 
112     // SD使能请求,配置时钟,配置错误回调,中断(中断优先级栈堆默认设置)
113     err_code = nrf_sdh_enable_request();
114     APP_ERROR_CHECK(err_code);
115 
116     // SD默认配置(如下),SD RAM起始地址配置(0x20002a98)
117     // 作为从机时的最大连接数量1(本工程为从机)
118     // 作为主机时的最大连接数据0
119     // 初始化MTU大小23
120     // 供应商特定的UUID数量1
121     // 属性表大小248(必须是4的倍数,以字节为单位)
122     // 配置服务更改特性数量0
123     uint32_t ram_start = 0;
124     err_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
125     APP_ERROR_CHECK(err_code);
126 
127     // 使能BLE栈堆
128     err_code = nrf_sdh_ble_enable(&ram_start);
129     APP_ERROR_CHECK(err_code);
130 
131     // 注册BLE事件的处理程序,所有BLE的事件都将分派ble_evt_handler回调
132     NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
133 }

主机初始化时设置的是作为主机时最大连接数量1,从机初始化时设置的是作为从机时最大连接数量1。这个配置的宏定义是在sdk_config.h文件中。我们go to define,进入nrf_sdh_ble_default_cfg_set()默认参数配置函数中查看,可以找到如下部分。

NRF_SDH_BLE_PERIPHERAL_LINK_COUNT与NRF_SDH_BLE_CENTRAL_LINK_COUNT,在定义最大连接设备数量的同时,也决定了它本身的角色属性。

BLE中我们称Central中心设备为主机(发起连接的设备)、Peripheral外部设备为从机(广播等待被连接的设备)。

103 ret_code_t nrf_sdh_ble_default_cfg_set(uint8_t conn_cfg_tag, uint32_t * p_ram_start)
104 {
105     uint32_t ret_code;
106 
107     ...
108 
109     // Configure the connection roles.
110     memset(&ble_cfg, 0, sizeof(ble_cfg));
111     ble_cfg.gap_cfg.role_count_cfg.periph_role_count  = NRF_SDH_BLE_PERIPHERAL_LINK_COUNT;
112 #ifndef S112
113     ble_cfg.gap_cfg.role_count_cfg.central_role_count = NRF_SDH_BLE_CENTRAL_LINK_COUNT;
114     ble_cfg.gap_cfg.role_count_cfg.central_sec_count  = MIN(NRF_SDH_BLE_CENTRAL_LINK_COUNT,
115                                                             BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT);
116 #endif
117 
118    ...
119 
120     return NRF_SUCCESS;
121 }

12.4 实验总结

在低功耗主从机的学习中,我们可以了解到最精简的主从机工程。也就是只包含了softdevice与ble协议栈初始化、以及电源管理初始化。

重点问题:

1.了解如何定义一个BLE工程是主机还是从机,或者其他的属性(主从一体等等)。

2.了解如何进行低功耗处理(main()函数中while循环的idle_state_handle空闲任务处理函数)。

13 LOG打印实验

13.1 实验简介

LOG打印实验1.1_ble_central_log与2.1_ble_peripheral_log,LOG打印实验是在低功耗实验的基础上,新增了LOG打印部分。

那么为什么我们需要添加log功能,主要是因为在开发的过程中,我们几乎很难一次调通我们需求的功能,这个时候我们就需要一种好的方式去帮助我们调通,我们出现问题点在哪(查找bug),以及我们的流程进行到哪边了(流程监视)。说到这里,大家会说为什么不使用在线调试的方式解决问题呢,下面谈一谈我个人的使用感觉(仅针对BLE)。

何时使用在线调试:

1.针对细小的问题点,例如某个参数的数值是否正确

2.针对不影响程序整机蓝牙功能运行的问题点,例如外设功能异常

何时使用log:

1.不能使用在线调试功能的时候(例如蓝牙连接状态下的通信调试,由于连接参数的限制,如果在线打断点调试,会导致异常断开)

2.监测关键节点信息(有些问题会出现在一些特殊情况下,可能需要监测N多次,才会出现一次异常)

Icon-info.png
注意:我们的LOG工程,仅针对RTT部分,没有介绍UART,UART的使用大家可以查看基础实验部分

13.2 实验现象

我们打开J-Link RTT Viewer,选择我们的Jlink仿真器,可以看到log打印如下。

Nrf rtt 11.png
Icon-info.png
注意:如果设置了输出,但是RTT中并没有打印,可以尝试关闭RTT Viewer软件重连,另外需要检查sdk_config.h中的NRF_LOG_ENABLED 已时能,即 #define NRF_LOG_ENABLED 1

13.3 工程及源码讲解

13.3.1 主机部分
13.3.1.1 工程说明

我们在工程中,新增加了nRF_Log与nRF_Segger_RTT分组,这两个分组中分别包含了log打印的相关函数,以及RTT的相关函数。

Icon-info.png
使用RTT,可以从目标微控制器输出信息,并以非常高的速度向应用程序发送输入,而不会影响目标的实时行为。

SEGGER RTT可与任何J-Link模型和任何支持目标处理器一起使用,后者允许后台存储器访问,即Cortex-M和RX目标。

RTT在两个方向上支持多个通道,直到主机和目标,可以用于不同的目的,并为用户提供最大的自由。

默认实现每个方向使用一个通道,用于可打印的终端输入和输出。使用J-Link RTT Viewer,该通道可用于多个“虚拟”终端,允许使用一个目标缓冲区打印到多个窗口(例如一个用于标准输出,一个用于错误输出,一个用于调试输出)。例如,可以使用附加的up(到主机)通道来发送分析或事件跟踪数据。

13.3.1.2 main()函数

log打印实验相对于前一章节的低功耗实验,新增的功能并不多,我们仅仅添加了LOG的RTT打印功能,这边我们首先还是看下mian函数。

可以看到,我们新增了log_init()的LOG初始化函数。

188 //******************************************************************
189 // fn : main
190 //
191 // brief : 主函数
192 //
193 // param : none
194 //
195 // return : none
196 int main(void)
197 {
198     // 初始化
199     log_init();             // 初始化LOG打印,由RTT工作
200     power_management_init();// 初始化电源控制
201     ble_stack_init();       // 初始化BLE栈堆
202 
203     // 打印例程名称
204     NRF_LOG_INFO("1.1_ble_central_log");
205     
206     // 进入主循环
207     for (;;)
208     {
209         idle_state_handle();   // 空闲状态处理
210     }
211 }
13.3.1.3 log_init()函数以及底层调用

我们查看到log_init()函数,先调用NRF_LOG_INIT()函数初始化LOG,然后我们调用NRF_LOG_DEFAULT_BACKENDS_INIT()函数来决定LOG的底层调用。

140 //******************************************************************
141 // fn : log_init
142 //
143 // brief : 初始化log打印
144 //
145 // param : none
146 //
147 // return : none
148 static void log_init(void)
149 {
150     ret_code_t err_code = NRF_LOG_INIT(NULL);
151     APP_ERROR_CHECK(err_code);
152 
153     NRF_LOG_DEFAULT_BACKENDS_INIT();
154 }

接下来我们追踪NRF_LOG_DEFAULT_BACKENDS_INIT()函数,在这个函数中,我们会引用到RTT功能。 首先我们go to define,找到NRF_LOG_DEFAULT_BACKENDS_INIT()函数的定义,sdk_config.h中我们定义了NRF_LOG_ENABLED为1,也就是使能LOG打印。

61 /**
62  * @def NRF_LOG_DEFAULT_BACKENDS_INIT
63  * @brief Macro for initializing default backends.
64  *
65  * Each backend enabled in configuration is initialized and added as a backend to the logger.
66  */
67 #if NRF_LOG_ENABLED
68 #define NRF_LOG_DEFAULT_BACKENDS_INIT() nrf_log_default_backends_init()
69 #else
70 #define NRF_LOG_DEFAULT_BACKENDS_INIT()
71 #endif

接下来我们追踪nrf_log_default_backends_init()函数,在sdk_config.h中,定义了NRF_LOG_BACKEND_RTT_ENABLED为1,也就是使能了RTT打印。

在这个地方,我们会调用nrf_log_backend_rtt_init()函数初始化RTT,然后调用nrf_log_backend_add()添加新的后端接口,并调用nrf_log_backend_enable()将这个新的后端接口使能。

这样处理之后,我们便可以使用面向RTT的log功能了。

Icon-info.png
RTT部分的源码,这个大家有兴趣的可以自行了解,我们使用RTT功能的时候,只需要将RTT分组下的SEGGER_RTT_XX.c的三个文件添加到工程即可,不一定要了解这个源码
58 void nrf_log_default_backends_init(void)
59 {
60     int32_t backend_id = -1;
61     (void)backend_id;
62 #if defined(NRF_LOG_BACKEND_RTT_ENABLED) && NRF_LOG_BACKEND_RTT_ENABLED
63     nrf_log_backend_rtt_init();
64     backend_id = nrf_log_backend_add(&rtt_log_backend, NRF_LOG_SEVERITY_DEBUG);
65     ASSERT(backend_id >= 0);
66     nrf_log_backend_enable(&rtt_log_backend);
67 #endif
68 
69 #if defined(NRF_LOG_BACKEND_UART_ENABLED) && NRF_LOG_BACKEND_UART_ENABLED
70     nrf_log_backend_uart_init();
71     backend_id = nrf_log_backend_add(&uart_log_backend, NRF_LOG_SEVERITY_DEBUG);
72     ASSERT(backend_id >= 0);
73     nrf_log_backend_enable(&uart_log_backend);
74 #endif
75 }
13.3.1.4 NRF_LOG_XX()函数说明

上面说明了如何去初始化LOG向RTT打印的功能,下面我们将给大家介绍一下LOG打印的函数,毕竟这个才是我们真正要用到的部分。

LOG打印的函数一共有4个,分别为打印ERROR、WARNING、INFO、DEBUG。他们的功能看字面意思就可以明白,分别是打印错误、警告、用户信息、调试信息。

111 #define NRF_LOG_ERROR(...)                     NRF_LOG_INTERNAL_ERROR(__VA_ARGS__)
112 #define NRF_LOG_WARNING(...)                   NRF_LOG_INTERNAL_WARNING( __VA_ARGS__)
113 #define NRF_LOG_INFO(...)                      NRF_LOG_INTERNAL_INFO( __VA_ARGS__)
114 #define NRF_LOG_DEBUG(...)                     NRF_LOG_INTERNAL_DEBUG( __VA_ARGS__)

底层的打印函数大家自己go to define查看,我这边给大家各举一个例子,方便大家使用。 其实他们的使用方式,都是和printf一样的,只是打印消息的级别不同而已。printf函数的使用大家不清楚的,可以百度查看一下。

1 NRF_LOG_ERROR("sd_ble_cfg_set() returned %s when attempting to set BLE_CONN_CFG_GAP.",
2                       nrf_strerror_get(ret_code));
3                       
4 NRF_LOG_WARNING("Change the RAM start location from 0x%x to 0x%x.",
5                       app_ram_start_link, *p_app_ram_start);
6                       
7 NRF_LOG_INFO("Shutdown started. Type %d", m_pwr_mgmt_evt);
8 
9 NRF_LOG_DEBUG("BLE event: 0x%x.", p_ble_evt->header.evt_id);

上面一段话和大家说了nordic协议栈中4个标准的LOG打印函数,他们其实拥有相同的功能,但是打印的级别不同(针对不同的调试阶段和目的),我们我们如何控制这个打印的级别呢,也是在我们的sdk_config.h中,大家可以看到NRF_LOG_DEFAULT_LEVEL。默认的只能打印到info,如果大家需要使用debug打印,需要修改此处的值为4。

7569 // <o> NRF_LOG_DEFAULT_LEVEL  - Default Severity level
7570  
7571 // <0=> Off 
7572 // <1=> Error 
7573 // <2=> Warning 
7574 // <3=> Info 
7575 // <4=> Debug 
7576 
7577 #ifndef NRF_LOG_DEFAULT_LEVEL
7578 #define NRF_LOG_DEFAULT_LEVEL 3
7579 #endif
13.3.2 从机部分

从机部分与主机部分新增log功能相同,这边不再赘述。

13.4 实验总结

LOG打印实验,大家需要掌握的三个要点如下:

1.了解什么是RTT。

2.了解nordic协议栈中LOG面向RTT功能的初始化、及打印函数的使用。

3.要善于使用log功能,这样有利于我们程序的流程开发及异常问题的查找和处理。

14 通用广播与扫描实验

14.1 实验简介

通用扫描实验1.2_ble_central_scan_all与2.2_ble_peripheral_adv_all,给大家带来的是主机的扫描功能展示,以及从机的广播功能展示。

也就是从这一实验开始,我们才是真正进入到BLE协议的学习实验,我们将按照扫描、连接、获取服务、通信的流程,在接下来的实验中给大家介绍BLE协议。

大家都清楚,低功耗蓝牙主从机间交互数据的方式一般来说是有两种,一种是连接之后通信(这个是蓝牙的主要功能),另一种就是本实验带来的主机扫描获取从机的广播数据。而上述的两种方式,不管是哪一种,都是需要扫描功能的,毕竟连接通信之前,我们的主机也是需要扫描到从机设备才可以发起连接。

14.2 实验现象

主机log打印如下,先打印当前例程的名称1.2_ble_central_scan_all,然后将会打印扫描到的从机设备的信息,格式如下:

Device MAC 0x010203040506         // 从机设备MAC地址

adv data: 0x0102...xx...xx          // 从机设备广播数据

scan data: 0x0102...xx...xx         // 从机设备扫描回调数据

rssi: -xxdBm                        // 从机设备相当于当前主机的信号强度
Nrf rtt 12.png

主机log打印如下,先打印当前例程的名称2.2_ble_peripheral_adv_all。

Nrf rtt 22.png

14.3 工程及源码讲解

14.3.1 工程说明

相对于LOG打印的工程,我们新增了nRF_BLE分组,这个分组下包含的就是BLE协议相关的文件,我们在本实验中只使用了nrf_ble_scan.c下的扫描功能函数。

Nrf group nrf ble.png
14.3.2 主机部分
14.3.2.1 main()函数

在mian函数中,我们新增了扫描功能的scan_init()初始化函数,并且在初始化所有功能之后,我们调用了发起扫描的函数scan_start()。

296 //******************************************************************
297 // fn : main
298 //
299 // brief : 主函数
300 //
301 // param : none
302 //
303 // return : none
304 int main(void)
305 {
306     // 初始化
307     log_init();             // 初始化LOG打印,由RTT工作
308     power_management_init();// 初始化电源控制
309     ble_stack_init();       // 初始化BLE栈堆
310     scan_init();            // 初始化扫描
311     
312     // 打印例程名称
313     NRF_LOG_INFO("1.2_ble_central_scan_all");
314     
315     scan_start();           // 开始扫描
316     
317     // 进入主循环
318     for (;;)
319     {
320         idle_state_handle();   // 空闲状态处理
321     }
322 }
14.3.2.2 scan_init()函数及scan_evt_handler()回调函数

首先我们看一下我们的扫描功能的初始化函数,可以看到我们最终调用的是库函数中的nrf_ble_scan_init()去初始化我们扫描,在这个函数中,有两个参数需要我们关注,一个是init_scan(这个参数携带的我们对于扫描的设置),另一个是scan_evt_handler(当我们扫描到设备之后,将会由这个回调函数返回事件信息给我们)。

155 //******************************************************************
156 // fn : scan_init
157 //
158 // brief : 初始化扫描(未设置扫描数据限制)
159 //
160 // param : none
161 //
162 // return : none
163 static void scan_init(void)
164 {
165     ret_code_t          err_code;
166     nrf_ble_scan_init_t init_scan;
167 
168     // 清空扫描结构体参数
169     memset(&init_scan, 0, sizeof(init_scan));
170     
171     // 配置扫描的参数
172     init_scan.p_scan_param = &m_scan_params;
173     
174     // 初始化扫描
175     err_code = nrf_ble_scan_init(&m_scan, &init_scan, scan_evt_handler);
176     APP_ERROR_CHECK(err_code);
177 }

首先我们看一下扫描的参数设置,这边我们设置为主动扫描,扫描间隔是100ms,扫描窗口是50ms,扫描的持续时间设置为0(设置为0,则一直扫描,除非我们调用停止扫描的函数),另外我们设置扫描附近的所有BLE设备(不做扫描限制)。

Icon-info.png
扫描模式分为两种:

1.被动扫描:只扫描从机设备的广播数据

2.主动扫描:扫描从机设备的广播数据以及扫描回调数据

55 // 定义扫描参数
56 static ble_gap_scan_params_t m_scan_params = 
57 {
58     .active        = 1,                            // 1为主动扫描,可获得广播数据及扫描回调数据
59     .interval      = NRF_BLE_SCAN_SCAN_INTERVAL,   // 扫描间隔:160*0.625 = 100ms  
60     .window        = NRF_BLE_SCAN_SCAN_WINDOW,     // 扫描窗口:80*0.625 = 50ms   
61     .timeout       = NRF_BLE_SCAN_SCAN_DURATION,   // 扫描持续的时间:设置为0,一直扫描,直到明确的停止扫描
62     .filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL,   // 扫描所有BLE设备,不做限制
63     .scan_phys     = BLE_GAP_PHY_1MBPS,            // 扫描1MBPS的PHY
64 };

接下来我们查看一下我们处理扫描回调事件的函数,因为我们这个实验带给大家的是扫描附近所有的BLE广播设备,所以可以看到,我们判断的设备ID为NRF_BLE_SCAN_EVT_NOT_FOUND,这个ID代表的未被过滤的扫描数据。 在这个事件ID下,我们根据p_scan_evt->params.p_not_found->type.scan_response判断扫描到的数据是广播数据还是扫描回调数据,并且我们可以从这个消息结构体当中获取到上述的两包数据、以及设备的MAC、设备的RSSI信号强度等等。并且我们将扫描到的这些信息通过LOG向RTT格式化打印出来。

 99 //******************************************************************
100 // fn : scan_evt_handler
101 //
102 // brief : 处理扫描回调事件
103 //
104 // param : scan_evt_t  扫描事件结构体
105 //
106 // return : none
107 static void scan_evt_handler(scan_evt_t const * p_scan_evt)
108 {
109     switch(p_scan_evt->scan_evt_id)
110     {
111         // 未过滤的扫描数据
112         case NRF_BLE_SCAN_EVT_NOT_FOUND:
113         {
114           // 判断是否为扫描回调数据
115           if(p_scan_evt->params.p_not_found->type.scan_response)
116           {
117             if(p_scan_evt->params.p_not_found->data.len)    // 存在扫描回调数据
118             {
119               NRF_LOG_INFO("scan data:  %s",
120                             Util_convertHex2Str(
121                             p_scan_evt->params.p_not_found->data.p_data,
122                             p_scan_evt->params.p_not_found->data.len));
123             }
124             else
125             {
126               NRF_LOG_INFO("scan data:  %s","NONE");
127             }
128             NRF_LOG_INFO("rssi:  %ddBm",p_scan_evt->params.p_not_found->rssi);
129           }
130           else  // 否则为广播数据
131           {
132             // 打印扫描的设备MAC
133             NRF_LOG_INFO("Device MAC:  %s",
134                          Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.p_not_found->peer_addr.addr));
135             
136             if(p_scan_evt->params.p_not_found->data.len)    // 存在广播数据
137             {
138               NRF_LOG_INFO("adv data:  %s",
139                             Util_convertHex2Str(
140                             p_scan_evt->params.p_not_found->data.p_data,
141                             p_scan_evt->params.p_not_found->data.len));
142             }
143             else
144             {
145               NRF_LOG_INFO("adv data:  %s","NONE");
146             }
147           }
148         } break;
149 
150         default:
151            break;
152     }
153 }
14.3.2.3 scan_start()函数

scan_start()发起扫描的函数,调用的是nordic提供的底层的nrf_ble_scan_start()函数实现的。

83 //******************************************************************
84 // fn : scan_start
85 //
86 // brief : 开始扫描
87 //
88 // param : none
89 //
90 // return : none
91 static void scan_start(void)
92 {
93     ret_code_t ret;
94 
95     ret = nrf_ble_scan_start(&m_scan);
96     APP_ERROR_CHECK(ret);
97 }
14.3.3 从机部分
14.3.3.1 工程说明

相对于LOG实验,从机工程也是新增了nRF_BLE的分组,但是我们可以看到看到分组下的文件与主机工程是不同的。

主机部分主要是扫描和获取服务相关,从机部分则是广播、连接参数和服务注册相关。

Nrf group nrf ble p.png
14.3.3.2 main()函数

在mian()函数,我们新增了广播初始化的advertising_init()函数、以及发起广播的advertising_start()函数。并且我们还初始化了GAP,调用的是gap_params_init()函数。

本来我们这一实验,只是想给大家带来广播和广播数据配置的展示的,但是为了能够让大家更直观的“看到”设备,所以我们便需要在广播数据中携带设备名称,那么问题来了,在nordic的协议栈中,除非我们去修改他提供的ble_advdata.c文件,不然我们没法直接去配置这个广播数据中的设备名称(这边给大家一个建议,除非对于协议栈的理解有把握,否则尽可能的不要去修改它)。

为了完成上述的添加设备名称的宏愿,所以我们只能“委曲求全”的添加了gap的初始化。

260 //******************************************************************
261 // fn : main
262 //
263 // brief : 主函数
264 //
265 // param : none
266 //
267 // return : none
268 int main(void)
269 {
270     // 初始化
271     log_init();             // 初始化LOG打印,由RTT工作
272     power_management_init();// 初始化电源控制
273     ble_stack_init();       // 初始化BLE栈堆
274     gap_params_init();      // 初始化GAP
275     advertising_init();     // 初始化广播
276     
277     // 打印例程名称
278     NRF_LOG_INFO("2.2_ble_peripheral_adv_all");
279 
280     advertising_start();    // 开启广播
281     
282     // 进入主循环
283     for (;;)
284     {
285         idle_state_handle();  // 空闲状态处理
286     }
287 }
14.3.3.3 gap_params_init()函数

由于广播数据介绍的篇幅会大一些,所以我们先给大家说明一下gap_params_init()函数。

在这个gap初始化的函数中,我们设置了设备名称设备的名称为GY-NRF52832。

52 #define DEVICE_NAME  "GY-NRF52832"
124 //******************************************************************
125 // fn : gap_params_init
126 //
127 // brief : 初始化GAP
128 // details : 此功能将设置设备的所有必需的GAP(通用访问配置文件)参数。它还设置权限和外观。
129 //
130 // param : none
131 //
132 // return : none
133 static void gap_params_init(void)
134 {
135     uint32_t                err_code;
136 
137     // 设置设备名称
138     err_code = sd_ble_gap_device_name_set(NULL,
139                                           (const uint8_t *) DEVICE_NAME,
140                                           strlen(DEVICE_NAME));
141     APP_ERROR_CHECK(err_code);
142 }
14.3.3.4 advertising_init()函数
Icon-info.png
在这里给大家科普一下,BLE的广播数据和扫描回调数据都是由用户自定义的,对于数据的内容没有任何的限制。我们只需要遵循下面的广播数据结构要求就行。

advdata[] = {

 长度A,类型A,数据A,
 长度B,类型B,数据B

}

有关广播数据中类型的定义见sdk离线文档:file:///E:/project-nordic/nRF5_SDK_15.2.0_offline_doc/s132/group___b_l_e___g_a_p___a_d___t_y_p_e___d_e_f_i_n_i_t_i_o_n_s.html

在我们的广播数据初始化函数中,我们首先定义了init.advdata.name_type为BLE_ADVDATA_FULL_NAME,这意味着我们的广播数据中将携带有刚刚GAP初始化中的全部设备名称。

然后我们定义了init.srdata.include_ble_device_addr为true,这意味着我们的扫描回调设备中将会携带有设备的MAC地址。

接下来我们设置了快慢广播,如下代码配置的结果是先快速广播18s(周期40ms),再慢速广播18s(周期100ms),最后停止广播。

最后我们调用nordic提供的广播初始化函数ble_advertising_init()。

Icon-info.png
我们展示的仅仅是nordic配置广播和扫描回调数据中的部分,除了上述的名称和设备地址。广播数据中还可以携带设备的类型、信号发射强度、连接参数、uuid等等,具体的大家可以go to define,查看ble_advdata_t结构体参数说明。
 85 //******************************************************************
 86 // fn : advertising_init
 87 //
 88 // brief : 用于初始化广播
 89 //
 90 // param : none
 91 //
 92 // return : none
 93 static void advertising_init(void)
 94 {
 95     uint32_t               err_code;
 96     ble_advertising_init_t init;
 97 
 98     memset(&init, 0, sizeof(init));
 99 
100     // 广播数据包含所有设备名称(FULL NAME)
101     init.advdata.name_type = BLE_ADVDATA_FULL_NAME;
102 //    // 广播数据只包含部分设备名称(SHORT NAME,长度为6)
103 //    init.advdata.name_type =  BLE_ADVDATA_SHORT_NAME;
104 //    init.advdata.short_name_len = 6;
105     
106     // 扫描回调数据中包含设备MAC地址
107     init.srdata.include_ble_device_addr = true;
108     
109     
110     // 配置广播周期,先快速广播18s(周期40ms),再慢速广播18s(周期100ms),最后停止广播
111     init.config.ble_adv_fast_enabled  = true;
112     init.config.ble_adv_fast_interval = 64;       // 64*0.625 = 40ms
113     init.config.ble_adv_fast_timeout  = 1800;     // 1800*10ms = 18s
114     init.config.ble_adv_slow_enabled  = true;
115     init.config.ble_adv_slow_interval = 160;      // 160*0.625 = 100ms
116     init.config.ble_adv_slow_timeout  = 1800;     // 1800*10ms = 18s
117     
118     err_code = ble_advertising_init(&m_advertising, &init);
119     APP_ERROR_CHECK(err_code);
120 
121     ble_advertising_conn_cfg_tag_set(&m_advertising, APP_BLE_CONN_CFG_TAG);
122 }
14.3.3.5 advertising_start()函数

advertising_start()作为我们开启广播功能的函数,我们调用了nordic协议栈中的ble_advertising_start()函数去实现功能。

71 //******************************************************************
72 // fn : advertising_start
73 //
74 // brief : 用于开启广播
75 //
76 // param : none
77 //
78 // return : none
79 static void advertising_start(void)
80 {
81     uint32_t err_code = ble_advertising_start(&m_advertising, BLE_ADV_MODE_FAST);
82     APP_ERROR_CHECK(err_code);
83 }

14.4 实验总结

经过通用扫描实验的学习,我们需要掌握的蓝牙协议有如下几点。

主机部分:

1.如何配置扫描的参数

2.如何发起扫描,并触类旁通,自行了解如何停止(大家自己了解一下,很容易)

3.如何获取扫描到的附近的BLE从机设备的信息

从机部分:

1.如何配置从机的广播数据、以及广播的参数(从机广播部分)

2.了解如何设置从机设备的名称(这个是正式项目经常需要使用到的)

3.了解如何开启从机广播,并触类旁通,自行了解如何关闭广播(大家自己了解一下,很容易)

15 带过滤的扫描与广播实验

15.1 实验简介

过滤扫描实验1.3_ble_central_scan_filter与2.3_ble_peripheral_adv_filter。经过通用扫描实验的学习,大家应该对于蓝牙的扫描和广播有一定的了解了。那么在我们实际的项目使用中,可能会出现由于我们的附近可能存在的BLE设备太多,导致我们去扫描的时候,没法在第一时间找到我们想要的设备的情况,那么这个时候,我们有没有好的处理方法呢。

上面的疑问答案是肯定的,因为我们在扫描的时候,可以先判断一下扫描到的广播数据或者扫描回调数据,仅返回我们需要的设备(也就是做一些数据的判断和限制),这样就可以快速的找到我们的设备。nordic对这部分处理的很细心,在BLE协议的扫描和广播中,都给我们开发者留好了限制的数据属性,我们以scan为例,可以看到限制的广播数据如下几种:名称,简称,地址,UUID以及容貌。

Icon-info.png
typedef enum

{

   SCAN_NAME_FILTER,       /**< Filter for names. */
   SCAN_SHORT_NAME_FILTER, /**< Filter for short names. */
   SCAN_ADDR_FILTER,       /**< Filter for addresses. */
   SCAN_UUID_FILTER,       /**< Filter for UUIDs. */
   SCAN_APPEARANCE_FILTER, /**< Filter for appearances. */

} nrf_ble_scan_filter_type_t;

15.2 实验现象

主机部分,可以看到上电先打印1.3_ble_central_scan_filter字样,然后会周期打印扫描到的2.3例程的从机设备信息(MAC、扫描回调数据、RSSI)。

在扫描回调的数据的末尾,我们可以看到03030100的字样,这个就是我们实验限制扫描的UUID。

Icon-info.png
03,// UUID数据长度

03,// 16bit UUID数据类型

01,00 // UUID 0x0001,这边是低位在前

Nrf rtt 13.png

从机部分,可以看到上电先打印2.3_ble_peripheral_adv_filter字样。

Nrf rtt 23.png

15.3 工程及源码讲解

15.3.1 主机工程
15.3.1.1 工程说明

本工程相对于上一章节的通用扫描实现,改动较小,只是对扫描返回的设备做出限制,不再像之前那样返回所有扫描到的设备。

所以我们的修改内容集中在scan_init()函数以及scan_evt_handler()回调函数当中。

15.3.1.2 scan_init()函数

相对于通用扫描的主机程序,我们在限制扫描的初始化函数当中,新增了nrf_ble_scan_filter_set()函数用于限制扫描的设备,在这个例程中,我们给大家展示的是限制UUID扫描,大家也可以根据其他信息去进行限制。

我们限制了扫描的UUID是16bit的0x0001,并且调用nrf_ble_scan_filters_enable()使能了这个限制。

65 // 定义扫描限制的UUID
66 static ble_uuid_t const m_nus_uuid = {BLE_UUID_NUS_SERVICE, BLE_UUID_TYPE_BLE};
162 //******************************************************************
163 // fn : scan_init
164 //
165 // brief : 初始化扫描(未设置扫描数据限制)
166 //
167 // param : none
168 //
169 // return : none
170 static void scan_init(void)
171 {
172     ret_code_t          err_code;
173     nrf_ble_scan_init_t init_scan;
174 
175     // 清空扫描结构体参数
176     memset(&init_scan, 0, sizeof(init_scan));
177     
178     // 配置扫描的参数
179     init_scan.p_scan_param = &m_scan_params;
180 
181     // 初始化扫描
182     err_code = nrf_ble_scan_init(&m_scan, &init_scan, scan_evt_handler);
183     APP_ERROR_CHECK(err_code);
184     
185     // 设置扫描的UUID限制
186     err_code = nrf_ble_scan_filter_set(&m_scan, SCAN_UUID_FILTER, &m_nus_uuid);
187     APP_ERROR_CHECK(err_code);
188 
189     // 使能扫描的UUID限制
190     err_code = nrf_ble_scan_filters_enable(&m_scan, NRF_BLE_SCAN_UUID_FILTER, false);
191     APP_ERROR_CHECK(err_code);
192 }
15.3.1.3 scan_evt_handler()回调函数

大家可以看到,我们对于扫描回调的事件ID判断进行了更改,上一章的通用扫描我们的判断的是NRF_BLE_SCAN_EVT_NOT_FOUND,这一章的限制扫描我们判断的是NRF_BLE_SCAN_EVT_FILTER_MATCH。

由于上面的一系列对于扫描UUID的限制操作,所以我们的1.3主机实验的现象,才会仅扫描并返回给我们的2.3实验的从机设备。

为了给大家更直观的找到限制的UUID,我们这边临时注视掉了广播数据的打印,只保留了包含了UUID的扫描回调数据打印。

102 //******************************************************************
103 // fn : scan_evt_handler
104 //
105 // brief : 处理扫描回调事件
106 //
107 // param : scan_evt_t  扫描事件结构体
108 //
109 // return : none
110 static void scan_evt_handler(scan_evt_t const * p_scan_evt)
111 {
112     switch(p_scan_evt->scan_evt_id)
113     {
114         // 匹配的扫描数据(也就是过滤之后的)
115         case NRF_BLE_SCAN_EVT_FILTER_MATCH:
116         {
117           // 下面这一段我们只保留了扫描回调数据获取的部分,因为从机筛选广播的UUID在扫描回调数据
118           // 判断是否为扫描回调数据
119           if(p_scan_evt->params.filter_match.p_adv_report->type.scan_response)
120           {
121             NRF_LOG_INFO("Device MAC:  %s",
122                          Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.filter_match.p_adv_report->peer_addr.addr));
123             
124             if(p_scan_evt->params.filter_match.p_adv_report->data.len)    // 存在扫描回调数据
125             {
126               NRF_LOG_INFO("scan data:  %s",
127                             Util_convertHex2Str(
128                             p_scan_evt->params.filter_match.p_adv_report->data.p_data,
129                             p_scan_evt->params.filter_match.p_adv_report->data.len));
130             }
131             else
132             {
133               NRF_LOG_INFO("scan data:  %s","NONE");
134             }
135             NRF_LOG_INFO("rssi:  %ddBm",p_scan_evt->params.filter_match.p_adv_report->rssi);
136           }
137 //          else  // 否则为广播数据
138 //          {
139 //            // 打印扫描的设备MAC
140 //            NRF_LOG_INFO("Device MAC:  %s",
141 //                         Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.filter_match.p_adv_report->peer_addr.addr));
142 //            
143 //            if(p_scan_evt->params.filter_match.p_adv_report->data.len)    // 存在广播数据
144 //            {
145 //              NRF_LOG_INFO("adv data:  %s",
146 //                            Util_convertHex2Str(
147 //                            p_scan_evt->params.filter_match.p_adv_report->data.p_data,
148 //                            p_scan_evt->params.filter_match.p_adv_report->data.len));
149 //            }
150 //            else
151 //            {
152 //              NRF_LOG_INFO("adv data:  %s","NONE");
153 //            }
154 //          }
155         } break;
156 
157         default:
158            break;
159     }
160 }
15.3.2 从机部分
15.3.2.1 工程说明

从机部分相对于通用广播的从机,我们也仅仅增加了UUID的广播数据。所以这边我们查看一下广播数据初始化的函数advertising_init()。

15.3.2.2 advertising_init()

大家可以看到,在广播数据初始化函数中,我们在init.srdata中增加了uuids_complete的定义,这样我们的广播数据中就会携带16bit的UUID 0x0001数据(这个数据在m_adv_uuids中被定义)。

53 // 定义广播的UUID
54 static ble_uuid_t m_adv_uuids[] = {{BLE_UUID_NUS_SERVICE, BLE_UUID_TYPE_BLE}};
 87 //******************************************************************
 88 // fn : advertising_init
 89 //
 90 // brief : 用于初始化广播
 91 //
 92 // param : none
 93 //
 94 // return : none
 95 static void advertising_init(void)
 96 {
 97     uint32_t               err_code;
 98     ble_advertising_init_t init;
 99 
100     memset(&init, 0, sizeof(init));
101 
102     // 广播数据包含所有设备名称(FULL NAME)
103     init.advdata.name_type = BLE_ADVDATA_FULL_NAME;
104 //    // 广播数据只包含部分设备名称(SHORT NAME,长度为6)
105 //    init.advdata.name_type =  BLE_ADVDATA_SHORT_NAME;
106 //    init.advdata.short_name_len = 6;
107     
108     // 扫描回调数据中包含16bit UUID:0x0001
109     init.srdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
110     init.srdata.uuids_complete.p_uuids  = m_adv_uuids;
111     
112     // 扫描回调数据中包含设备MAC地址
113     init.srdata.include_ble_device_addr = true;
114     
115     // 配置广播周期,先快速广播18s(周期40ms),再慢速广播18s(周期100ms),最后停止广播
116     init.config.ble_adv_fast_enabled  = true;
117     init.config.ble_adv_fast_interval = 64;       // 64*0.625 = 40ms
118     init.config.ble_adv_fast_timeout  = 1800;     // 1800*10ms = 18s
119     init.config.ble_adv_slow_enabled  = true;
120     init.config.ble_adv_slow_interval = 160;      // 160*0.625 = 100ms
121     init.config.ble_adv_slow_timeout  = 18000;    // 18000*10ms = 180s
122     
123     err_code = ble_advertising_init(&m_advertising, &init);
124     APP_ERROR_CHECK(err_code);
125 
126     ble_advertising_conn_cfg_tag_set(&m_advertising, APP_BLE_CONN_CFG_TAG);
127 }

15.4 实验总结

经过这个扫描限制实验的学习,大家需要掌握的要点。

主机部分:

1.如何添加一个扫描的限制并使能

2.限制扫描的回调事件ID是NRF_BLE_SCAN_EVT_FILTER_MATCH

3.自行编程,完成通过名称、地址等其他限制的扫描

从机部分:

1.如何添加个人的广播数据,例如本章节的UUID(本质上还是广播数据的配置)

16 白名单扫描实验

16.1 实验简介

白名单扫描实验1.4_ble_central_scan_whitelist与2.4_ble_peripheral_adv_whitelist。有关扫描的实验,在第一章的通用扫描中,我们了解到可以主机发起扫描,可以获取从机设备的一些信息,包含广播数据和扫描回调数据,以及从机设备的MAC。在第二章的限制扫描中,我们已经学习过如果根据广播数据和扫描回调数据去获取我们指定的从机设备。那么在这一章节我们将给大家带来,根据从机设备的MAC地址,去限制扫描。

Icon-info.png
注意:白名单扫描是根据扫描到的设备MAC,而不是指的广播数据中携带的MAC(假设我们将MAC地址加入到广播数据中)。

16.2 实验现象

主机部分,上电打印例程名称1.4_ble_central_scan_whitelist,如果成功设置白名单,则打印Successfully set whitelist!,然后如果附近有白名单中的设备,则会打印扫描到的白名单设备的信息。

Nrf rtt 14.png

从机部分相较于前两章的内容,几乎没有改动,这边不做讲解。因为白名单的限制,是由主机部分完成。

16.3 工程及源码讲解

16.3.1 工程说明

本实验的修改内容集中在主机的扫描初始化以及扫描的回调事件处理函数中,所以下面只有主机部分的代码讲解,而没有从机部分的。

16.3.2 主机部分
16.3.2.1 scan_init()函数

首先我们查看一下主机扫描的初始化函数,我们可以看到filter_policy过滤扫描参数,我们配置为BLE_GAP_SCAN_FP_WHITELIST,也就是白名单扫描模式。

启用这个白名单模式,如果成功,则会在扫描回调事件处理函数中给我们返回NRF_BLE_SCAN_EVT_WHITELIST_REQUEST请求开启白名单的事件ID。

54 // 定义扫描参数
55 static ble_gap_scan_params_t m_scan_params = 
56 {
57     .active        = 1,                            // 1为主动扫描,可获得广播数据及扫描回调数据
58     .interval      = NRF_BLE_SCAN_SCAN_INTERVAL,   // 扫描间隔:160*0.625 = 100ms  
59     .window        = NRF_BLE_SCAN_SCAN_WINDOW,     // 扫描窗口:80*0.625 = 50ms   
60     .timeout       = NRF_BLE_SCAN_SCAN_DURATION,   // 扫描持续的时间:设置为0,一直扫描,直到明确的停止扫描
61     .filter_policy = BLE_GAP_SCAN_FP_WHITELIST,    // 扫描白名单设备
62     .scan_phys     = BLE_GAP_PHY_1MBPS,            // 扫描1MBPS的PHY
63 };
181 //******************************************************************
182 // fn : scan_init
183 //
184 // brief : 初始化扫描(未设置扫描数据限制)
185 //
186 // param : none
187 //
188 // return : none
189 static void scan_init(void)
190 {
191     ret_code_t          err_code;
192     nrf_ble_scan_init_t init_scan;
193 
194     // 清空扫描结构体参数
195     memset(&init_scan, 0, sizeof(init_scan));
196     
197     // 配置扫描的参数
198     init_scan.p_scan_param = &m_scan_params;
199     
200     // 初始化扫描
201     err_code = nrf_ble_scan_init(&m_scan, &init_scan, scan_evt_handler);
202     APP_ERROR_CHECK(err_code);
203 }
16.3.2.2 scan_evt_handler()函数

可以看到,当我们接收到NRF_BLE_SCAN_EVT_WHITELIST_REQUEST事件时,我们将去调用sd_ble_gap_whitelist_set函数设置我们的白名单设备。

我们这边首先获取了从机的MAC地址:0xD363BFCE5C46,然后将其添加到白名单设备列表当中。

当我们扫描到0xD363BFCE5C46后将打印他的广播和扫描回调数据、以及型号强度。

Icon-tips.png
这个地方,需要大家设置为自己的从设备MAC,不然无法使用此例程
 99 //******************************************************************
100 // fn : scan_evt_handler
101 //
102 // brief : 处理扫描回调事件
103 //
104 // param : scan_evt_t  扫描事件结构体
105 //
106 // return : none
107 static void scan_evt_handler(scan_evt_t const * p_scan_evt)
108 {
109     uint32_t err_code;
110     ble_gap_addr_t peer_addr;
111     ble_gap_addr_t const * p_peer_addr;
112     switch(p_scan_evt->scan_evt_id)
113     {
114         // 白名单设置请求
115         case NRF_BLE_SCAN_EVT_WHITELIST_REQUEST:
116         {
117           memset(&peer_addr, 0x00, sizeof(peer_addr));
118           peer_addr.addr_id_peer = 1;
119           peer_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_STATIC;
120           peer_addr.addr[5] = 0xD3;
121           peer_addr.addr[4] = 0x63;
122           peer_addr.addr[3] = 0xBF;
123           peer_addr.addr[2] = 0xCE;
124           peer_addr.addr[1] = 0x5C;
125           peer_addr.addr[0] = 0x46;
126           p_peer_addr = &peer_addr; 
127           
128           // 设置白名单
129           err_code = sd_ble_gap_whitelist_set(&p_peer_addr, 0x01);
130           if (err_code == NRF_SUCCESS)
131           {
132             NRF_LOG_INFO("Successfully set whitelist!");
133           }
134           APP_ERROR_CHECK(err_code);
135         }break;
136         
137         // 扫描到的白名单设备数据
138         case NRF_BLE_SCAN_EVT_WHITELIST_ADV_REPORT:
139         {
140           // 判断是否为扫描回调数据
141           if(p_scan_evt->params.p_whitelist_adv_report->type.scan_response)
142           {
143             if(p_scan_evt->params.p_whitelist_adv_report->data.len)    // 存在扫描回调数据
144             {
145               NRF_LOG_INFO("scan data:  %s",
146                             Util_convertHex2Str(
147                             p_scan_evt->params.p_whitelist_adv_report->data.p_data,
148                             p_scan_evt->params.p_whitelist_adv_report->data.len));
149             }
150             else
151             {
152               NRF_LOG_INFO("scan data:  %s","NONE");
153             }
154             NRF_LOG_INFO("rssi:  %ddBm",p_scan_evt->params.p_whitelist_adv_report->rssi);
155           }
156           else  // 否则为广播数据
157           {
158             // 打印扫描的设备MAC
159             NRF_LOG_INFO("Device MAC:  %s",
160                          Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.p_whitelist_adv_report->peer_addr.addr));
161             
162             if(p_scan_evt->params.p_whitelist_adv_report->data.len)    // 存在广播数据
163             {
164               NRF_LOG_INFO("adv data:  %s",
165                             Util_convertHex2Str(
166                             p_scan_evt->params.p_whitelist_adv_report->data.p_data,
167                             p_scan_evt->params.p_whitelist_adv_report->data.len));
168             }
169             else
170             {
171               NRF_LOG_INFO("adv data:  %s","NONE");
172             }
173           }
174         } break;
175 
176         default:
177            break;
178     }
179 }
16.3.3 从机部分

16.4 实验总结

经过白名单实验的学习,我们需要明白以下几点:

1.我们使用白名单功能的前提是我们知道对方设备的MAC。

2.白名单功能只需要主机完成

3.了解NRF_BLE_SCAN_EVT_WHITELIST_REQUEST事件以及sd_ble_gap_whitelist_set()函数

17 通用连接实验

17.1 实验简介

通用连接实验1.5_ble_central_conn_all与2.5_ble_peripheral_conn_all。从这一实验开始,我们将给大家介绍蓝牙连接相关的协议,由此对于蓝牙协议的学习进入到第二阶段。

17.2 实验现象

主机部分,先打印实验名称1.5_ble_central_conn_all,然后开始扫描周围的从机设备。当我们将两块开发板靠近的时候(当RSSI大于-30dBm),则会发起连接。然后打印连接成功的消息,并且打印连接参数。

Icon-tips.png
Connected. conn_DevAddr: 0xD363BFCE5C46 // 连接的对方设备MAC地址

conn_handle: 0x0000 // 连接的句柄,第一个连接的设备是0x0000,第二个是0x0001,以此类推

conn_Param: 24,24,0,400 // 连接参数

Nrf rtt 15.png

从机部分,先打印实验名称2.5_ble_peripheral_conn_all,当被主机成功连接之后,会打印连接成功的消息;当断开连接之后,会打印断开连接的句柄,并且会打印断开连接的原因。

Icon-tips.png
Connected. conn_DevAddr: 0xE51F64D6BFF4 // 连接的对方设备的MAC

Connected. conn_handle: 0x0000 // 连接的句柄

Connected. conn_Param: 24,24,0,400 // 连接参数


Disconnected. conn_handle: 0x0, reason: 0x0008 // 断开连接的设备句柄,以及断开的原因

Nrf rtt 25.png

17.3 工程及源码讲解

17.3.1 主机部分
17.3.1.1 工程说明

通用连接主机工程,是在通用扫描主机工程的基础上,新增了连接的过程,主要的修改集中在扫描事件处理函数,以及ble协议栈事件处理函数。

17.3.1.2 scan_evt_handler()函数

定义连接的参数,这个参数将在sd_ble_gap_connect()函数中被调用。

65 // 定义连接参数
66 static ble_gap_conn_params_t m_conn_params = 
67 {
68     .min_conn_interval  = MSEC_TO_UNITS(NRF_BLE_SCAN_MIN_CONNECTION_INTERVAL, UNIT_1_25_MS),  // 最小连接间隔7.5ms
69     .max_conn_interval  = MSEC_TO_UNITS(NRF_BLE_SCAN_MAX_CONNECTION_INTERVAL, UNIT_1_25_MS),  // 最大连接间隔30ms  
70     .slave_latency      = NRF_BLE_SCAN_SLAVE_LATENCY,                                         // 隐藏周期0 
71     .conn_sup_timeout   = MSEC_TO_UNITS(NRF_BLE_SCAN_SUPERVISION_TIMEOUT, UNIT_10_MS),        // 超时时间4000ms 
72 };

我们可以看到,在扫描回调事件处理的函数中,前面一部分还是和之前一样的,就是获取扫描到的从机设备的信息。

在159行,我们对扫描到的从机设备的RSSI进行了判断,我们人为定义,当设备的RSSI大于-30dBm的时候,也就是信号强度非常好,我们就默认连接该设备。

首先,我们先定义准备连接的从机设备的地址和信息,配置m_addr.addr_type为BLE_GAP_ADDR_TYPE_RANDOM_STATIC,配置m_addr.addr为扫描到的设备地址p_scan_evt->params.p_not_found->peer_addr.addr。

配置好准备连接的设备的MAC信息,在发起连接之前,我们一定要先调用nrf_ble_scan_stop()函数去停止扫描,不然会出错。

停止扫描之后,我们调用sd_ble_gap_connect()函数,发起对从机设备的连接。

107 //******************************************************************
108 // fn : scan_evt_handler
109 //
110 // brief : 处理扫描回调事件
111 //
112 // param : scan_evt_t  扫描事件结构体
113 //
114 // return : none
115 static void scan_evt_handler(scan_evt_t const * p_scan_evt)
116 {
117     switch(p_scan_evt->scan_evt_id)
118     {
119         // 未过滤的扫描数据
120         case NRF_BLE_SCAN_EVT_NOT_FOUND:
121         {
122           // 判断是否为扫描回调数据
123           if(p_scan_evt->params.p_not_found->type.scan_response)
124           {
125             if(p_scan_evt->params.p_not_found->data.len)    // 存在扫描回调数据
126             {
127               NRF_LOG_INFO("scan data:  %s",
128                             Util_convertHex2Str(
129                             p_scan_evt->params.p_not_found->data.p_data,
130                             p_scan_evt->params.p_not_found->data.len));
131             }
132             else
133             {
134               NRF_LOG_INFO("scan data:  %s","NONE");
135             }
136             NRF_LOG_INFO("rssi:  %ddBm",p_scan_evt->params.p_not_found->rssi);
137           }
138           else  // 否则为广播数据
139           {
140             // 打印扫描的设备MAC
141             NRF_LOG_INFO("Device MAC:  %s",
142                          Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.p_not_found->peer_addr.addr));
143             
144             if(p_scan_evt->params.p_not_found->data.len)    // 存在广播数据
145             {
146               NRF_LOG_INFO("adv data:  %s",
147                             Util_convertHex2Str(
148                             p_scan_evt->params.p_not_found->data.p_data,
149                             p_scan_evt->params.p_not_found->data.len));
150             }
151             else
152             {
153               NRF_LOG_INFO("adv data:  %s","NONE");
154             }
155           }
156           
157           
158           // 如果扫描到的设备信号强度大于-30dBm
159           if(p_scan_evt->params.p_not_found->rssi > (-30))
160           {
161             ret_code_t          err_code;
162             
163             // 配置准备连接的设备MAC
164             ble_gap_addr_t m_addr;
165             m_addr.addr_id_peer = 1;
166             m_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_STATIC;
167             memcpy(m_addr.addr,p_scan_evt->params.p_not_found->peer_addr.addr,BLE_GAP_ADDR_LEN);
168             
169             // 停止扫描
170             nrf_ble_scan_stop();
171             // 发起连接
172             err_code = sd_ble_gap_connect(&m_addr,&m_scan_params,&m_conn_params,APP_BLE_CONN_CFG_TAG);
173             APP_ERROR_CHECK(err_code);
174           }
175           
176         } break;
177 
178         default:
179            break;
180     }
181 }
17.3.1.3 ble_evt_handler()函数

当我们的主机设备进行连接,或者断开连接的时候,BLE的事件回调中,都会给我们状态事件提示。

连接成功的状态BLE_GAP_EVT_CONNECTED,我们可以在这边获取到连接的设备的MAC、连接参数等等。

断开连接的状态BLE_GAP_EVT_DISCONNECTED,我们可以获取到断开的连接的原因等等。

210 //******************************************************************
211 // fn : ble_evt_handler
212 //
213 // brief : BLE事件回调
214 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
215 //
216 // param : ble_evt_t  事件类型
217 //         p_context  未使用
218 //
219 // return : none
220 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
221 {
222     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
223     ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
224     switch (p_ble_evt->header.evt_id)
225     {
226         // 连接
227         case BLE_GAP_EVT_CONNECTED:
228             NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
229                          Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
230                          p_gap_evt->conn_handle,
231                          p_connected_evt->conn_params.min_conn_interval,
232                          p_connected_evt->conn_params.max_conn_interval,
233                          p_connected_evt->conn_params.slave_latency,
234                          p_connected_evt->conn_params.conn_sup_timeout
235                          );
236             break;
237         // 断开连接
238         case BLE_GAP_EVT_DISCONNECTED:
239             NRF_LOG_INFO("Disconnected. conn_handle: 0x%x, reason: 0x%04x",
240                          p_gap_evt->conn_handle,
241                          p_gap_evt->params.disconnected.reason);
242             // 如果需要异常断开重连,可以打开下面的注释
243             // scan_start();  // 重新开始扫描
244             break;
245 
246         default:
247             break;
248     }
249 }
17.3.2 从机部分
17.3.2.1 工程说明

通用连接的从机工程,是在通用广播的从机工程的基础上修改的。

由于从机设备是一直保持广播,等待被连接的,所以相对于主机程序,我们就不用去找发起连接的函数以及对方设备的MAC了。因此我们只需要关注BLE协议的回调函数的事件返回。

17.3.2.2 ble_evt_handler()函数

从机的连接与断开的状态和主机是一样的,能够获得的对方设备的信息也是一样的。

连接成功的状态BLE_GAP_EVT_CONNECTED,我们可以在这边获取到连接的设备的MAC、连接参数等等。

断开连接的状态BLE_GAP_EVT_DISCONNECTED,我们可以获取到断开的连接的原因等等。

148 //******************************************************************
149 // fn : ble_evt_handler
150 //
151 // brief : BLE事件回调
152 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
153 //
154 // param : ble_evt_t  事件类型
155 //         p_context  未使用
156 //
157 // return : none
158 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
159 {
160     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
161     ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
162     switch (p_ble_evt->header.evt_id)
163     {
164         // 连接
165         case BLE_GAP_EVT_CONNECTED:
166             NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
167                          Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
168                          p_gap_evt->conn_handle,
169                          p_connected_evt->conn_params.min_conn_interval,
170                          p_connected_evt->conn_params.max_conn_interval,
171                          p_connected_evt->conn_params.slave_latency,
172                          p_connected_evt->conn_params.conn_sup_timeout
173                          );
174             break;
175         // 断开连接
176         case BLE_GAP_EVT_DISCONNECTED:
177             NRF_LOG_INFO("Disconnected. conn_handle: 0x%x, reason: 0x%04x",
178                          p_gap_evt->conn_handle,
179                          p_gap_evt->params.disconnected.reason);
180             break;
181 
182         default:
183             // No implementation needed.
184             break;
185     }
186 }

17.4 实验总结

经过主从机通用连接的实验,我们需要弄懂的问题点如下:

主机部分:

1.主机部分一定要先发起扫描,当扫描到设备之后,再去发起连接。(不扫描直接发起连接,如果周围没有这个从设备,会进入异常,并只能RST恢复)

2.主机发起连接的时候,是利用从机的MAC地址进行连接

3.连接以及断开连接的状态返回,可以获取对方设备信息以及断开原因

从机部分:

1.从机是一直广播等待被动连接的设备

2.连接以及断开连接的状态返回,可以获取对方设备信息以及断开原因

18 过滤连接实验

18.1 实验简介

过滤连接实验1.6_ble_central_conn_filter与2.6_ble_peripheral_conn_filter。

过滤连接实验是从过滤扫描的实验集成下来的,它的目的是和过滤扫描实验一样的,也是为了快速的从一堆BLE设备中,找到我们的设备,并与之快速建立连接。

18.2 实验现象

主机上电后先打印实验名称1.6_ble_central_conn_filter,然后发起扫描,如果扫描到符合过滤的从机设备,则会打印扫描到的设备信息,然后发起连接,连接成功后会打印连接的一些信息。

Nrf rtt 16.png

从机上电后先打印实验名称2.6_ble_peripheral_conn_filter,如果被主机扫描连接之后,则会打印连接的信息。

Nrf rtt 26.png

18.3 工程及源码讲解

18.3.1 主机部分
18.3.1.1 工程说明

过滤连接,我们主要关注的是扫描初始化中对于过滤连接设备的设置,以及过滤连接成功或者失败返回的扫描回调函数的事件ID。

18.3.1.2 scan_init()函数

连接参数定义,用于配置init_scan.p_conn_param参数,也就是我们的过滤扫描之后发起连接的连接参数。

65 // 定义连接参数
66 static ble_gap_conn_params_t m_conn_params = 
67 {
68     .min_conn_interval  = MSEC_TO_UNITS(NRF_BLE_SCAN_MIN_CONNECTION_INTERVAL, UNIT_1_25_MS),  // 最小连接间隔7.5ms
69     .max_conn_interval  = MSEC_TO_UNITS(NRF_BLE_SCAN_MAX_CONNECTION_INTERVAL, UNIT_1_25_MS),  // 最大连接间隔30ms  
70     .slave_latency      = NRF_BLE_SCAN_SLAVE_LATENCY,                                         // 隐藏周期0 
71     .conn_sup_timeout   = MSEC_TO_UNITS(NRF_BLE_SCAN_SUPERVISION_TIMEOUT, UNIT_10_MS),        // 超时时间4000ms 
72 };

在扫描初始化函数中,我们可以看到我们初始化了init_scan.connect_if_match = 1,然后我们还配置了init_scan.p_conn_param = &m_conn_params,后面的限制UUID是和过滤扫描一样的。 这样配置好之后,我们发起扫描,一旦扫描到我们的过滤后的设备,就会使用我们配置好的连接参数,对这个从机设备发起连接。连接成功或者失败,都会在扫描回调函数中进行事件通知。

182 //******************************************************************
183 // fn : scan_init
184 //
185 // brief : 初始化扫描(未设置扫描数据限制)
186 //
187 // param : none
188 //
189 // return : none
190 static void scan_init(void)
191 {
192     ret_code_t          err_code;
193     nrf_ble_scan_init_t init_scan;
194 
195     // 清空扫描结构体参数
196     memset(&init_scan, 0, sizeof(init_scan));
197     
198     init_scan.connect_if_match = 1;
199     init_scan.conn_cfg_tag = APP_BLE_CONN_CFG_TAG;
200     
201     // 配置扫描的参数
202     init_scan.p_scan_param = &m_scan_params;
203 
204     // 配置连接的参数
205     init_scan.p_conn_param = &m_conn_params;
206     
207     // 初始化扫描
208     err_code = nrf_ble_scan_init(&m_scan, &init_scan, scan_evt_handler);
209     APP_ERROR_CHECK(err_code);
210     
211     // 设置扫描的UUID限制
212     err_code = nrf_ble_scan_filter_set(&m_scan, SCAN_UUID_FILTER, &m_nus_uuid);
213     APP_ERROR_CHECK(err_code);
214 
215     // 使能扫描的UUID限制
216     err_code = nrf_ble_scan_filters_enable(&m_scan, NRF_BLE_SCAN_UUID_FILTER, false);
217     APP_ERROR_CHECK(err_code);
218 }
18.3.1.3 scan_evt_handler()扫描回调函数

在扫描回调函数中,过滤扫描的事件是和之前一样的,我们不做介绍。

我们关注一下事件ID,NRF_BLE_SCAN_EVT_CONNECTED与NRF_BLE_SCAN_EVT_CONNECTING_ERROR,当我们设置init_scan.connect_if_match = 1后,如果扫描到并且成功连接上目标从设备,则返回连接成功的ID,如果失败了,则返回连接ERR。

Icon-info.png
注意:由于这两个事件返回的信息和ble协议栈返回的事件ID携带的信息相同,所以这边我们不重复打印。

case NRF_BLE_SCAN_EVT_CONNECTED: { NRF_LOG_INFO("SCAN CONNECTED!");

// NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",

// Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.connected.p_connected->peer_addr.addr),

// p_scan_evt->params.connected.conn_handle,

// p_scan_evt->params.connected.p_connected->conn_params.min_conn_interval,

// p_scan_evt->params.connected.p_connected->conn_params.max_conn_interval,

// p_scan_evt->params.connected.p_connected->conn_params.slave_latency,

// p_scan_evt->params.connected.p_connected->conn_params.conn_sup_timeout

// );

}break;

case NRF_BLE_SCAN_EVT_CONNECTING_ERROR: { NRF_LOG_INFO("SCAN CONNECTING ERROR!");

// NRF_LOG_INFO("Disconnected. reason: 0x%04x",

// p_scan_evt->params.connecting_err.err_code);

}break;

111 //******************************************************************
112 // fn : scan_evt_handler
113 //
114 // brief : 处理扫描回调事件
115 //
116 // param : scan_evt_t  扫描事件结构体
117 //
118 // return : none
119 static void scan_evt_handler(scan_evt_t const * p_scan_evt)
120 {
121     switch(p_scan_evt->scan_evt_id)
122     {
123         // 匹配的扫描数据(也就是过滤之后的)
124         case NRF_BLE_SCAN_EVT_FILTER_MATCH:
125         {
126         ...
127         } break;
128 
129         
130         case NRF_BLE_SCAN_EVT_CONNECTED:
131         {
132             NRF_LOG_INFO("SCAN CONNECTED!"); 
133 //            NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
134 //                         Util_convertBdAddr2Str((uint8_t*)p_scan_evt->params.connected.p_connected->peer_addr.addr),
135 //                         p_scan_evt->params.connected.conn_handle,
136 //                         p_scan_evt->params.connected.p_connected->conn_params.min_conn_interval,
137 //                         p_scan_evt->params.connected.p_connected->conn_params.max_conn_interval,
138 //                         p_scan_evt->params.connected.p_connected->conn_params.slave_latency,
139 //                         p_scan_evt->params.connected.p_connected->conn_params.conn_sup_timeout
140 //                         );
141         }break;
142         
143         case NRF_BLE_SCAN_EVT_CONNECTING_ERROR:
144         {
145             NRF_LOG_INFO("SCAN CONNECTING ERROR!");
146 //            NRF_LOG_INFO("Disconnected. reason: 0x%04x",
147 //                         p_scan_evt->params.connecting_err.err_code);
148         }break;
149         
150         default:
151            break;
152     }
153 }
18.3.2 从机部分

从机部分就不给大家介绍了,和前面的过滤广播的例程是重复的,仅修改了连接之后获取并打印对方设备的信息。

18.4 实验总结

经过本实验的学习,大家需要了解到如何让主机在扫描的时候,去对过滤后的从机设备发起连接。

1.了解init_scan.connect_if_match参数的功能。

2.了解扫描回调事件ID中的NRF_BLE_SCAN_EVT_CONNECTED与NRF_BLE_SCAN_EVT_CONNECTING_ERROR。

19 连接参数更新实验

19.1 实验简介

经过前两章的主从机扫描并连接的实验学习后,我们不难发现,连接之后主从机之间一个很重要的参数(返回连接成功的状态下,打印的数据),那就是连接参数。

有关连接参数的详细说明,请大家查看本手册的第一章节有关蓝牙协议的介绍。

Icon-info.png
主从机连接参数的配置流程:

1、主从机连接成功之后,我们将以主机携带的连接参数,作为主从机连接的参数

2、从机可以发起更新连接参数的请求

3、主机接收到从机的参数更新请求,发起参数更新

4、更新成功,将以新的参数作为连接参数

19.2 实验现象

主机上电后发起扫描,一旦扫描到我们过滤的设备之后,就会发起连接。

连接成功后,打印连接的handle,以及主从机之间的连接参数。

当从机发起连接参数更新请求后,我们使用从机申请的连接参数,去发起连接参数的更新。

Nrf rtt 17.png

从机上电后广播,被主机连接后打印连接的handle,以及连接参数。

5000ms后,从机发起参数更新请求,主机接收请求后发起更新,更新成功后,从机打印新的连接参数。

Nrf rtt 27.png

19.3 工程及源码讲解

19.3.1 主机部分

主机部分大部分和之前的过滤连接是一样的,唯一的区别在于接收从机连接参数更新请求,并发起更新。

19.3.1.1 ble_evt_handler()回调函数

在BLE事件回调中可以看到两个新增的事件处理,一个是连接参数更新请求BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST,另一个是连接参数更新BLE_GAP_EVT_CONN_PARAM_UPDATE。

BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:连接参数更新请求,这个状态是主机接收到从机发起更新参数的请求时返回,在这个状态下,我们可以获取从机申请更新的参数数值,并且调用sd_ble_gap_conn_param_update()函数去发起连接参数的更新。

BLE_GAP_EVT_CONN_PARAM_UPDATE:连接参数更新,当连接参数完成更新的时候,会返回这个状态,在这个状态中,我们可以获取到更新完成后的新的连接参数。

202 //******************************************************************
203 // fn : ble_evt_handler
204 //
205 // brief : BLE事件回调
206 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
207 //
208 // param : ble_evt_t  事件类型
209 //         p_context  未使用
210 //
211 // return : none
212 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
213 {
214     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
215     ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
216     
217     switch (p_ble_evt->header.evt_id)
218     {
219         // 连接
220         case BLE_GAP_EVT_CONNECTED:
221             NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
222                          Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
223                          p_gap_evt->conn_handle,
224                          p_connected_evt->conn_params.min_conn_interval,
225                          p_connected_evt->conn_params.max_conn_interval,
226                          p_connected_evt->conn_params.slave_latency,
227                          p_connected_evt->conn_params.conn_sup_timeout
228                          );
229             break;
230 
231         // 断开连接
232         case BLE_GAP_EVT_DISCONNECTED:
233             NRF_LOG_INFO("Disconnected. conn_handle: 0x%x, reason: 0x%04x",
234                          p_gap_evt->conn_handle,
235                          p_gap_evt->params.disconnected.reason);
236             // 如果需要异常断开重连,可以打开下面的注释
237             // scan_start();  // 重新开始扫描
238             break;
239 
240         // 连接参数更新请求
241         case BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
242             NRF_LOG_INFO("conn_Param Update  Request");
243             
244             sd_ble_gap_conn_param_update(p_gap_evt->conn_handle,
245                                                     &p_gap_evt->params.conn_param_update_request.conn_params);
246             break;
247             
248         // 连接参数更新
249         case BLE_GAP_EVT_CONN_PARAM_UPDATE:
250             NRF_LOG_INFO("conn_Param Update: %d,%d,%d,%d",
251                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.min_conn_interval,
252                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.max_conn_interval,
253                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.slave_latency,
254                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.conn_sup_timeout
255                          );
256             break;
257             
258         default:
259             break;
260     }
261 }
19.3.2 从机部分

从机部分,我们主要关注两个地方,一个是连接参数的更新请求conn_params_init()函数,另一个是和主机部分一样的,连接参数更新成功的状态返回。

19.3.2.1 conn_params_init()函数

连接参数初始化函数,在这个函数中,我们配置了新的连接参数,以及连接参数更新的时间及尝试的次数,并且包含了两个回调函数,一个是更新是否成功的返回,一个是携带的连接参数是否正确的返回。

我们配置了第一次更新的时间为连接成功后5s(由frist_xx_delay控制),后面的几次也是间隔5s(由next_xx_delay控制),然后尝试更新次数为3。这个尝试次数3的含义是,如果申请了3次更新,都未成功,那么将从on_conn_params_evt回调中返回更新失败。

245 //******************************************************************
246 // fn : conn_params_init
247 //
248 // brief : 初始化连接参数模块的功能
249 //
250 // param : none
251 //
252 // return : none
253 static void conn_params_init(void)
254 {
255     uint32_t               err_code;
256     ble_conn_params_init_t cp_init;
257 
258     memset(&cp_init, 0, sizeof(cp_init));
259 
260     cp_init.p_conn_params                  = &m_conn_params;
261     cp_init.first_conn_params_update_delay = APP_TIMER_TICKS(5000);
262     cp_init.next_conn_params_update_delay  = APP_TIMER_TICKS(5000);
263     cp_init.max_conn_params_update_count   = 3;
264     cp_init.start_on_notify_cccd_handle    = BLE_GATT_HANDLE_INVALID;
265     cp_init.disconnect_on_fail             = false;
266     cp_init.evt_handler                    = on_conn_params_evt;
267     cp_init.error_handler                  = conn_params_error_handler;
268 
269     err_code = ble_conn_params_init(&cp_init);
270     APP_ERROR_CHECK(err_code);
271 }

可以看到,我们新的连接参数,连接间隔40ms,隐藏周期0,超时时间4s。

57 // 定义连接参数(为了展示连接参数的更新,我们设置连接间隔为固定的20ms)
58 static ble_gap_conn_params_t m_conn_params = 
59 {
60     .min_conn_interval  = MSEC_TO_UNITS(40, UNIT_1_25_MS),  // 最小连接间隔40ms
61     .max_conn_interval  = MSEC_TO_UNITS(40, UNIT_1_25_MS),  // 最大连接间隔40ms  
62     .slave_latency      = 0,                                // 隐藏周期0 
63     .conn_sup_timeout   = MSEC_TO_UNITS(2000, UNIT_10_MS),  // 超时时间4000ms 
64 };

在on_conn_params_evt()回调函数中,我们可以看到连接参数更新成功或者失败的返回。

这边需要注意的问题点如下,以我们这个实验为例,例如我们的参数更新请求会尝试3次:

1、如果第一次就更新成功,那么会直接返回更新SUCCESS,并且后面2次更新请求将不会再起作用

2、如果三次更新都不成功(这个不成功,可能是更新失败,可能是主机不响应等等),那么才会返回更新failed

也就是3次只要有一次更新成功就认为我们更新完成了,如果3次都失败,才会认为失败。

211 //******************************************************************
212 // fn : on_conn_params_evt
213 //
214 // brief : 处理连接更新参数的事件回调
215 //
216 // param : p_evt -> 接收到的连接模块返回的任务事件
217 //
218 // return : none
219 static void on_conn_params_evt(ble_conn_params_evt_t * p_evt)
220 {
221     if (p_evt->evt_type == BLE_CONN_PARAMS_EVT_SUCCEEDED)
222     {
223         NRF_LOG_INFO("connParam Update Success"); 
224     }
225       
226     if (p_evt->evt_type == BLE_CONN_PARAMS_EVT_FAILED)
227     {
228         NRF_LOG_INFO("connParam Update Failed"); 
229     }
230 }

这边的conn_params_error_handler()错误回调函数,当我们申请更新的连接参数不符合协议时才会返回,也就是提示我们连接参数异常。

232 //******************************************************************
233 // fn : conn_params_error_handler
234 //
235 // brief : 处理连接更新参数异常的事件回调
236 //
237 // param : nrf_error -> 异常标志
238 //
239 // return : none
240 static void conn_params_error_handler(uint32_t nrf_error)
241 {
242     APP_ERROR_HANDLER(nrf_error);
243 }
19.3.2.2 ble_evt_handler()回调函数

BLE事件回调函数,和主机一样的,当我们成功更新连接参数之后,返回BLE_GAP_EVT_CONN_PARAM_UPDATE事件,在这个事件中我们可以获取新的连接参数。

159 //******************************************************************
160 // fn : ble_evt_handler
161 //
162 // brief : BLE事件回调
163 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
164 //
165 // param : ble_evt_t  事件类型
166 //         p_context  未使用
167 //
168 // return : none
169 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
170 {
171     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
172     ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
173     
174     switch (p_ble_evt->header.evt_id)
175     {
176         // 连接
177         case BLE_GAP_EVT_CONNECTED:
178             NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
179                          Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
180                          p_gap_evt->conn_handle,
181                          p_connected_evt->conn_params.min_conn_interval,
182                          p_connected_evt->conn_params.max_conn_interval,
183                          p_connected_evt->conn_params.slave_latency,
184                          p_connected_evt->conn_params.conn_sup_timeout
185                          );
186             break;
187 
188         // 断开连接
189         case BLE_GAP_EVT_DISCONNECTED:
190             NRF_LOG_INFO("Disconnected. conn_handle: 0x%x, reason: 0x%04x",
191                          p_gap_evt->conn_handle,
192                          p_gap_evt->params.disconnected.reason);
193             break;
194             
195         // 连接参数更新
196         case BLE_GAP_EVT_CONN_PARAM_UPDATE:
197             NRF_LOG_INFO("conn_Param Update: %d,%d,%d,%d",
198                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.min_conn_interval,
199                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.max_conn_interval,
200                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.slave_latency,
201                          p_ble_evt->evt.gap_evt.params.conn_param_update.conn_params.conn_sup_timeout
202                          );
203             break;
204             
205         default:
206           
207             break;
208     }
209 }

19.4 实验总结

这一章的实验,想要交给大家的内容是如何配置连接参数。需要了解的重点如下:

1、了解连接参数的含义,明白连接参数越小,通信的速率越快。

2、了解主从机之间的连接参数的配置流程。

3、了解从机如何申请更新连接参数,ble_conn_params_init()函数。

4、了解主机如何更新连接参数,sd_ble_gap_conn_param_update()函数。

20 MTU更新实验

20.1 实验简介

MTU是我们的最大传输单元(Maximum Transmission Unit),也就是我们大家常说的一包数据最大可以多少字节。

在BLE4.1往后的协议中,为了提高BLE的通信速率,我们可以配置MTU最大为251byte,但是这个251byte中有4字节的BLE通信数据包的协议头。也就是我们用户的一包数据最大是247字节,但是在nordic的NUS(串口服务)中,他又占用了3个字节作为特殊用途,这样我们用户可用的数据包长度就仅剩下244字节。

这边我们需要注意的是,BLE协议头的4字节我们是不能占用的,但是例如NUS服务的3字节(这个相当于用户的私有协议,不是必须存在)。

Icon-tips.png
1、主机和从机都可以主动配置MTU大小

2、如果主从机配置的MTU大小不同,则取小的数值

20.2 实验现象

主机上电之后扫描周围的从机,一旦扫描到符合的过滤的从设备,就会打印扫描到的信息,并且发起连接,连接成功之后,打印连接的参数以及对方设备的信息。

连接成功之后,会打印Data len的大小,也就是我们的MTU。

Nrf rtt 18.png

从机上电之后广播,被主机连接之后,打印主机的设备信息以及连接参数。

并且会打印Data len的大小,也就是我们的MTU。

Nrf tft 28.png

20.3 工程及源码讲解

20.3.1 主机部分

主机部分,我们仅仅新增了有关MTU大小设置的部分,也就是在我们的gatt_init()函数中。

20.3.1.1 gatt_init()函数

在gatt初始化的函数中,我们调用nrf_ble_gatt_att_mtu_central_set()函数去配置我们的MTU大小,在主机中我们配置MTU大小为100byte。

并且最终的MTU大小的gatt的事件回调函数中展示,携带的任务ID为NRF_BLE_GATT_EVT_ATT_MTU_UPDATED。

224 //******************************************************************
225 // fn : gatt_init
226 //
227 // brief : 初始化GATT
228 //
229 // param : none
230 //
231 // return : none
232 void gatt_init(void)
233 {
234     ret_code_t err_code;
235 
236     err_code = nrf_ble_gatt_init(&m_gatt, gatt_evt_handler);
237     APP_ERROR_CHECK(err_code);
238     
239     err_code = nrf_ble_gatt_att_mtu_central_set(&m_gatt, 100);
240     APP_ERROR_CHECK(err_code);
241 }
20.3.1.2 gatt_evt_handler()回调函数

在gatt的事件回调函数中,我们接收到任务ID为NRF_BLE_GATT_EVT_ATT_MTU_UPDATED的事件之后,可以获取到我们的MTU大小。

这边打印的Data len,是我们MTU减掉NUS占用的3个特殊字节之后的大小。

205 //******************************************************************
206 // fn : gatt_evt_handler
207 //
208 // brief : GATT事件回调
209 //
210 // param : p_gatt  gatt类型结构体
211 //         p_evt  gatt事件
212 //
213 // return : none
214 void gatt_evt_handler(nrf_ble_gatt_t * p_gatt, nrf_ble_gatt_evt_t const * p_evt)
215 {
216     if ((m_conn_handle == p_evt->conn_handle) && (p_evt->evt_id == NRF_BLE_GATT_EVT_ATT_MTU_UPDATED))
217     {
218         NRF_LOG_INFO("Data len is set to 0x%X(%d)", 
219                      p_evt->params.att_mtu_effective - OPCODE_LENGTH - HANDLE_LENGTH, 
220                      p_evt->params.att_mtu_effective - OPCODE_LENGTH - HANDLE_LENGTH);
221     }
222 }
20.3.2 从机部分

从机部分与主机部分是一样的,我们都是新增了一个gatt的初始化。

20.3.2.1 gatt_init()函数

在从机的gatt初始化函数中,我们配置MTU的大小是80字节(由于主机是100字节,所以最终的MTU大小已从机的80字节为准)。

174 //******************************************************************
175 // fn : gatt_init
176 //
177 // brief : 初始化GATT
178 //
179 // param : none
180 //
181 // return : none
182 void gatt_init(void)
183 {
184     ret_code_t err_code;
185 
186     err_code = nrf_ble_gatt_init(&m_gatt, gatt_evt_handler);
187     APP_ERROR_CHECK(err_code);
188 
189     err_code = nrf_ble_gatt_att_mtu_periph_set(&m_gatt, 80);
190     APP_ERROR_CHECK(err_code);
191 }
20.3.2.2 gatt_evt_handler()回调函数

当我们在gatt的回调函数中,接收到NRF_BLE_GATT_EVT_ATT_MTU_UPDATED的事件ID,我们可以获取到当前的MTU的大小。

这边的Data len是MTU减掉NUS中的3个特殊字节后的大小。

155 //******************************************************************
156 // fn : gatt_evt_handler
157 //
158 // brief : GATT事件回调
159 //
160 // param : p_gatt  gatt类型结构体
161 //         p_evt  gatt事件
162 //
163 // return : none
164 void gatt_evt_handler(nrf_ble_gatt_t * p_gatt, nrf_ble_gatt_evt_t const * p_evt)
165 {
166     if ((m_conn_handle == p_evt->conn_handle) && (p_evt->evt_id == NRF_BLE_GATT_EVT_ATT_MTU_UPDATED))
167     {
168         NRF_LOG_INFO("Data len is set to 0x%X(%d)", 
169                      p_evt->params.att_mtu_effective - OPCODE_LENGTH - HANDLE_LENGTH, 
170                      p_evt->params.att_mtu_effective - OPCODE_LENGTH - HANDLE_LENGTH);
171     }
172 }

20.4 实验总结

MTU的大小和连接参数,与我们的通信速率息息相关。有关MTU大小的配置,有如下几个要点:

1、主机和从机都可以配置MTU大小

2、大部分情况下,我们根据从机的服务来确定MTU大小

3、MTU越大,我们可以发送的数据包就越大

4、当主从机配置的MTU大小不同时,我们以小的数值作准。

21 Write/Read属性服务实验

21.1 实验简介

通过前面实验的学习,我们已经能够完成主机成功连接从机,并且可以配置我们想要的连接参数(连接间隔、MTU等)。所以有了前面的铺垫,从这一章节开始,我们会给大家讲解如何进行主从机的通信。本来我们应该给大家继续在串口透传的程序基础上继续添加服务功能,但由于NUS服务相对比较复杂,所以我们会插入两篇章节(本章节及下一章节),分别给大家讲解LED的Write属性服务,以及BTN的Notify属性服务,然后再切回串口透传例程继续讲解,方便大家学习。

谈到通信,就不得不给大家介绍一个名叫“GATT”的好同志,因为就是他帮我们管理蓝牙的通信,那么对于GATT而言,当两个设备成功连接之后,他们分别作为一下两个设备之一:

⊙GATT服务器:包含特性数据库的设备(比如控制LED数据的是LED特性,传输UART数据的是UART特性)。可以通过一个GATT客户端写入或者读取数据

⊙GATT客户端:向GATT服务器写入数据或者读取数据的设备

其中我们的从机设备一般是作为GATT服务器去提供服务的,主机设备作为GATT客户端去向服务的特性数据库中写入或者读取数据。

Icon-info.png
GATT特征值属性包含如下4个:

Write(写)、Read(读)、Notify(通知)、Indicate(暗示)

这4个属性当中,其中大家最常用的是3个,通俗的来讲:Write属性是用于主机给从机发送数据;Read属性是用于主机读取从机的数据; Notify属性是用于从机给主机发送数据

我们本章的实验,会给大家带来其中的write属性及Read属性的介绍,也就是教大家主机如何给从机发送数据,以及主机如何读取从机的数据。其中包含了从机的服务注册,以及主机的服务发现,相对于前面章节的内容,可能难度会大上一些,大家一定耐心查看,这个章节很重要。

21.2 实验现象

主机设备流程:

1、扫描符合我们连接过滤要求的从机设备(根据LED服务的UUID过滤)

2、成功连接我们的从机设备,并且更新连接参数和MTU

3、发现服务,成功发现Ghostyu LED Service

4、主机分别按下4个按键,给从机发送数据控制从机设备的4个LED依次点亮,并且同时从从机读取我们刚刚发送的数据

Nrf52832dk-ble-gattwirte2.png

从机设备流程:

1、开启广播

2、被主机成功连接,并交互连接参数

3、等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)

4、主机按下按键,从机接收到相应的LED状态数据并打印,并根据这个数据控制板子上的LED点亮

Nrf52832dk-ble-gattwirte1.png

21.3 工程及源码讲解

21.3.1 主机部分
21.3.1.1 gy_profile_led_c.c\.h与main.c

我们首先查看一下主机的服务客户端文件,里面包含了好几个函数,我们挨个介绍一下这些函数的功能。

第一个还是客户端的初始化函数,对应从机的ble_led_init()注册服务的函数,我们需要利用ble_db_discovery_evt_register()函数注册一个待会服务发现的UUID。

这里我们需要注意一下,不论是服务的发现,还是某一个特征的发现,都是需要根据UUID来判断的。这边可以看到我们注册UUID发现是和从机的服务中一样的(LED_UUID_BASE以及LED_UUID_SERVICE)。

 83 //******************************************************************************
 84 // fn :ble_led_c_init
 85 //
 86 // brief : LED服务客户端初始化函数
 87 //
 88 // param : p_ble_led_c -> 指向LED客户端结构的指针
 89 //         p_ble_led_c_init -> 指向LED初始化结构的指针
 90 //
 91 // return : none
 92 uint32_t ble_led_c_init(ble_led_c_t * p_ble_led_c, ble_led_c_init_t * p_ble_led_c_init)
 93 {
 94     uint32_t      err_code;
 95     ble_uuid_t    led_uuid;
 96     ble_uuid128_t led_base_uuid = {LED_UUID_BASE};
 97 
 98     VERIFY_PARAM_NOT_NULL(p_ble_led_c);
 99     VERIFY_PARAM_NOT_NULL(p_ble_led_c_init);
100     VERIFY_PARAM_NOT_NULL(p_ble_led_c_init->evt_handler);
101 
102     p_ble_led_c->peer_led_db.led_handle         = BLE_GATT_HANDLE_INVALID;
103     p_ble_led_c->conn_handle                    = BLE_CONN_HANDLE_INVALID;
104     p_ble_led_c->evt_handler                    = p_ble_led_c_init->evt_handler;
105 
106     err_code = sd_ble_uuid_vs_add(&led_base_uuid, &p_ble_led_c->uuid_type);
107     if (err_code != NRF_SUCCESS)
108     {
109         return err_code;
110     }
111     VERIFY_SUCCESS(err_code);
112 
113     led_uuid.type = p_ble_led_c->uuid_type;
114     led_uuid.uuid = LED_UUID_SERVICE;
115 
116     return ble_db_discovery_evt_register(&led_uuid);
117 }

看完初始化函数,我们接下来得看下当我们注册好客服端,并且在main函数中调用发现函数之后,底层如何给我们返回,这里我们需要结合main.c文件的内容一起给大家介绍。 在mian函数的中,我们可以看到注册了服务发现的功能,也就是数据库发现db_discovery_init();,并且注册了GATT初始化gatt_init();,以及我们的LED服务的客户端初始化led_c_init();。

527 //******************************************************************
528 // fn : main
529 //
530 // brief : 主函数
531 //
532 // param : none
533 //
534 // return : none
535 int main(void)
536 {
537     // 初始化
538     log_init();             // 初始化LOG打印,由RTT工作
539     timer_init();           // 初始化定时器
540     GPIOTE_Init();          // 初始化IO
541     BTN_Init(btn_evt_handler_t); // 初始化按键  
542     power_management_init();// 初始化电源控制
543     ble_stack_init();       // 初始化BLE栈堆
544     db_discovery_init();    // 初始化数据库发现(用于发现服务)
545     gatt_init();            // 初始化GATT
546     led_c_init();           // 初始化LED_C
547     scan_init();            // 初始化扫描
548     
549     // 打印例程名称
550     NRF_LOG_INFO("demo0:simple central");
551     
552     scan_start();           // 开始扫描
553     
554     // 进入主循环
555     for (;;)
556     {
557         idle_state_handle();   // 空闲状态处理
558     }
559 }

当我们初始化好以上说明的3个函数(具体每个函数的代码,大家可以看下代码中的注册),一旦我们主机发现我们的从机,并成功连接之后,会进入BLE_GAP_EVT_CONNECTED状态。 在这个状态下,我们就需要开始我们的服务发现了,调用ble_db_discovery_start()函数开始发现服务。

345 //******************************************************************
346 // fn : ble_evt_handler
347 //
348 // brief : BLE事件回调
349 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
350 //
351 // param : ble_evt_t  事件类型
352 //         p_context  未使用
353 //
354 // return : none
355 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
356 {
357     ret_code_t            err_code;
358     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
359     ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
360     
361     switch (p_ble_evt->header.evt_id)
362     {
363         // 连接
364         case BLE_GAP_EVT_CONNECTED:
365             NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
366                          Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
367                          p_gap_evt->conn_handle,
368                          p_connected_evt->conn_params.min_conn_interval,
369                          p_connected_evt->conn_params.max_conn_interval,
370                          p_connected_evt->conn_params.slave_latency,
371                          p_connected_evt->conn_params.conn_sup_timeout
372                          );
373             m_conn_handle = p_gap_evt->conn_handle;
374             
375             err_code = ble_led_c_handles_assign(&m_ble_led_c, m_conn_handle, NULL);
376             APP_ERROR_CHECK(err_code);
377 
378             // 开始发现服务,NUS客户端等待发现结果
379             err_code = ble_db_discovery_start(&m_db_disc, p_ble_evt->evt.gap_evt.conn_handle);
380             APP_ERROR_CHECK(err_code);
381             break;

当成功发现服务之后,会进入db_disc_handler回调函数,在这个回调函数之中,因为我们这个工程仅需要处理led的服务,所以我们调用ble_led_c_on_db_disc_evt去发现led相关的特征值内容,其中会携带我们的ble_db_discovery_evt_t参数(底层返回的所有和服务数据库相关的信息都在这个参数里面)。

316 //******************************************************************
317 // fn : db_disc_handler
318 //
319 // brief : 用于处理数据库发现事件的函数
320 // details : 此函数是一个回调函数,用于处理来自数据库发现模块的事件。
321 //           根据发现的UUID,此功能将事件转发到各自的服务。
322 // 
323 // param : p_event -> 指向数据库发现事件的指针
324 //
325 // return : none
326 static void db_disc_handler(ble_db_discovery_evt_t * p_evt)
327 {
328     ble_led_c_on_db_disc_evt(&m_ble_led_c, p_evt);
329 }

所以接下来,我们需要先判断一下,底层返回的ble_db_discovery_evt_t中携带的类型是否是BLE_DB_DISCOVERY_COMPLETE,也就是数据库成功的完成发现,且发现的UUID是LED_UUID_SERVICE。

如果确实成功的发现我们的LED服务,接下来我们就需要从服务中取出我们需要的特征值,也就是LED_UUID_CHAR。我们需要从这个特征值当中获取我们用于通信的句柄(handle_value)。

当我们一切都是按照正确的流程跑完,可以看到在这个函数的最后,它会给我们返回一个p_ble_led_c->evt_handler(p_ble_led_c, &evt);,也就是向mian.c文件中给我们一个回调(ble_led_c_init初始化函数时注册的回调),其中携带的任务参数类型是BLE_LED_C_EVT_DISCOVERY_COMPLETE。

32 //******************************************************************************
33 // fn :ble_led_c_on_db_disc_evt
34 //
35 // brief : 处理led服务发现的函数
36 //
37 // param : p_ble_led_c -> 指向LED客户端结构的指针
38 //         p_evt -> 指向从数据库发现模块接收到的事件的指针
39 //
40 // return : none
41 void ble_led_c_on_db_disc_evt(ble_led_c_t * p_ble_led_c, ble_db_discovery_evt_t const * p_evt)
42 {
43     // 判断LED服务是否发现完成
44     if (p_evt->evt_type == BLE_DB_DISCOVERY_COMPLETE &&
45         p_evt->params.discovered_db.srv_uuid.uuid == LED_UUID_SERVICE &&
46         p_evt->params.discovered_db.srv_uuid.type == p_ble_led_c->uuid_type)
47     {
48         ble_led_c_evt_t evt;
49 
50         evt.evt_type    = BLE_LED_C_EVT_DISCOVERY_COMPLETE;
51         evt.conn_handle = p_evt->conn_handle;
52 
53         for (uint32_t i = 0; i < p_evt->params.discovered_db.char_count; i++)
54         {
55             const ble_gatt_db_char_t * p_char = &(p_evt->params.discovered_db.charateristics[i]);
56             switch (p_char->characteristic.uuid.uuid)
57             {
58                 // 根据LED特征值的UUID,获取我们句柄handle_value
59                 case LED_UUID_CHAR:
60                     evt.params.peer_db.led_handle = p_char->characteristic.handle_value;
61                     break;
62 
63                 default:
64                     break;
65             }
66         }
67 
68         NRF_LOG_DEBUG("Led Button Service discovered at peer.");
69         
70         // 如果实例是在db_discovery之前分配的,则分配db_handles
71         if (p_ble_led_c->conn_handle != BLE_CONN_HANDLE_INVALID)
72         {
73             if (p_ble_led_c->peer_led_db.led_handle         == BLE_GATT_HANDLE_INVALID)
74             {
75                 p_ble_led_c->peer_led_db = evt.params.peer_db;
76             }
77         }
78 
79         p_ble_led_c->evt_handler(p_ble_led_c, &evt);
80     }
81 }

那么接下来,我们再去看一下mian.c中此回调函数下的处理。

在ble_led_c_evt_handler回调函数下,我们判断传入的事件类型,可以看到正是刚刚的BLE_LED_C_EVT_DISCOVERY_COMPLETE事件,也就是代表我们已经成功的获取了我们指定服务(LED_UUID_SERVICE)下的指定特征值(LED_UUID_CHAR)的句柄(handle_value)。

然后我们调用ble_led_c_handles_assign函数,去将我们的连接句柄connHandle以及特征值句柄handle_value,绑定给p_ble_led_c实例。

272 //******************************************************************
273 // fn : ble_led_c_evt_handler
274 //
275 // brief : LED服务事件
276 //
277 // param : none
278 //
279 // return : none                 
280 static void ble_led_c_evt_handler(ble_led_c_t * p_ble_led_c, ble_led_c_evt_t * p_evt)
281 {
282     ret_code_t err_code;
283 
284     switch (p_evt->evt_type)
285     {
286         case BLE_LED_C_EVT_DISCOVERY_COMPLETE:
287             NRF_LOG_INFO("Discovery complete.");
288             err_code = ble_led_c_handles_assign(&m_ble_led_c, p_evt->conn_handle, &p_evt->params.peer_db);
289             APP_ERROR_CHECK(err_code);
290             NRF_LOG_INFO("Connected to device with Ghostyu LED Service.");
291             break;
292         default:
293             break;
294     }
295 }

当上述的流程都正确跑完,我们就可以进行最后一步的行动,也就是发送数据,在这个例程当中我们是利用按键触发来发送对应的LED的状态变化。 我们到mian.c中,查看按键触发会调用的btn_evt_handler_t回调函数,在这个函数中,我们最后会调用LED服务数据发送的功能函数ble_led_led_status_send。

495 //******************************************************************
496 // fn : btn_evt_handler_t
497 //
498 // brief : 按键触发回调函数
499 // 
500 // param : butState -> 当前的按键值
501 //
502 // return : none
503 void btn_evt_handler_t (uint8_t butState)
504 {
505   uint8_t buf[LED_UUID_CHAR_LEN] = {0x01,0x01,0x01,0x01};
506   switch(butState)
507   {
508     case BUTTON_1:
509       buf[0] = 0x00;
510       break;
511     case BUTTON_2:
512       buf[1] = 0x00;
513       break;
514     case BUTTON_3:
515       buf[2] = 0x00;
516       break;
517     case BUTTON_4:
518       buf[3] = 0x00;
519       break;
520     default:
521       break;
522   }
523   ble_led_status_send(&m_ble_led_c,buf,LED_UUID_CHAR_LEN);    // 发送Wirte属性数据包
524   ble_led_status_read(&m_ble_led_c);                          // 发送Read属性的读取消息
525 }

最后我们来分析一下这个发送函数,是如何使用我们刚刚一大圈代码处理,最终得到的connhandle以及handle_value的。

首先先判断下数据的长度,是不是符合我们的特征值的长度限制(不能超过我们定义的特征值的大小,否则返回参数错误),这个判断是很有必要的!

接下来我们判断一下connhandle是否为0xffff(BLE_CONN_HANDLE_INVALID),也就是尚未连接任何设备,如果没有连接,则返回状态无效。

最后我们定义了ble_gattc_write_params_t结构体用于赋值我们需要发送的数据,其中值得注意的是.handle = p_ble_led_c->peer_led_db.led_handle,这个就是我们刚刚获得的handle_value(特征值句柄),其他参数大家依葫芦画瓢,比较好理解,就不给大家介绍了。最终我们调用sd_ble_gattc_write函数将数据发送出去。

148 //******************************************************************************
149 // fn :ble_led_led_status_send
150 //
151 // brief : LED状态控制函数
152 //
153 // param : p_ble_led_c -> 指向要关联的LED结构实例的指针
154 //         p_string -> 发送的LED相关的数据
155 //         length -> 发送的LED相关的数据长度
156 //
157 // return : none
158 uint32_t ble_led_led_status_send(ble_led_c_t * p_ble_led_c, uint8_t * p_string, uint16_t length)
159 {
160     VERIFY_PARAM_NOT_NULL(p_ble_led_c);
161 
162     if (length > LED_UUID_CHAR_LEN)
163     {
164         NRF_LOG_WARNING("Content too long.");
165         return NRF_ERROR_INVALID_PARAM;
166     }
167     if (p_ble_led_c->conn_handle == BLE_CONN_HANDLE_INVALID)
168     {
169         return NRF_ERROR_INVALID_STATE;
170     }
171 
172     ble_gattc_write_params_t const write_params =
173     {
174         .write_op = BLE_GATT_OP_WRITE_CMD,
175         .flags    = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
176         .handle   = p_ble_led_c->peer_led_db.led_handle,
177         .offset   = 0,
178         .len      = length,
179         .p_value  = p_string
180     };
181     
182     return sd_ble_gattc_write(p_ble_led_c->conn_handle, &write_params);
183 }

上面一部分已经将我们的write属性的使用都讲解完了,最后我们再来看下Read属性部分的内容。

首先是我们还是在main文件的按键回调函数中调用的ble_led_status_read(&m_ble_led_c);函数,去读取从机特征值中的数据的,这里我们直接分析一下这个函数。

可以看到函数内容很简单,只调用了一个sd_ble_gattc_read函数去读取,包含的参数内容分别是我们的connhandle以及handle_value。

209 //******************************************************************************
210 // fn :ble_led_status_read
211 //
212 // brief : 读取LED特征值
213 //
214 // param : p_ble_led_c -> 指向要关联的LED结构实例的指针
215 //
216 // return : none
217 uint32_t ble_led_status_read(ble_led_c_t * p_ble_led_c)
218 {
219     VERIFY_PARAM_NOT_NULL(p_ble_led_c);
220     return sd_ble_gattc_read(p_ble_led_c->conn_handle,p_ble_led_c->peer_led_db.led_handle,0);
221 }

当我们成功Read之后,底层的sotfdevice会通过ble_led_c_on_ble_evt函数给我们返回BLE_GATTC_EVT_READ_RSP事件。

140 //******************************************************************************
141 // fn :ble_led_c_on_ble_evt
142 //
143 // brief : BLE事件处理函数
144 //
145 // param : p_ble_evt -> ble事件
146 //         p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
147 //
148 // return : none
149 void ble_led_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
150 {
151     if ((p_context == NULL) || (p_ble_evt == NULL))
152     {
153         return;
154     }
155 
156     ble_led_c_t * p_ble_led_c = (ble_led_c_t *)p_context;
157 
158     switch (p_ble_evt->header.evt_id)
159     {
160         case BLE_GAP_EVT_DISCONNECTED:
161             on_disconnected(p_ble_led_c, p_ble_evt);
162             break;
163         case BLE_GATTC_EVT_READ_RSP:
164             on_read(p_ble_led_c, p_ble_evt);
165           break;
166           
167         default:
168             break;
169     }
170 }

在BLE_GATTC_EVT_READ_RSP事件中,我们调用on_read函数去处理我们读取的值,我们将读取到的值,通过RTT LOG打印出来。

32 //******************************************************************************
33 // fn :on_read
34 //
35 // brief : 处理read事件的函数。
36 //
37 // param : p_ble_led_c -> led服务结构体
38 //         p_ble_evt -> ble事件
39 //
40 // return : none
41 static void on_read(ble_led_c_t * p_ble_led_c, ble_evt_t const * p_ble_evt)
42 {
43     if (p_ble_led_c->conn_handle == p_ble_evt->evt.gap_evt.conn_handle)
44     {
45       NRF_LOG_INFO("Recive State:%02X,%02X,%02X,%02X",
46                    p_ble_evt->evt.gattc_evt.params.read_rsp.data[0],
47                    p_ble_evt->evt.gattc_evt.params.read_rsp.data[1],
48                    p_ble_evt->evt.gattc_evt.params.read_rsp.data[2],
49                    p_ble_evt->evt.gattc_evt.params.read_rsp.data[3]);
50     }
51 }
21.3.2 从机部分
21.3.2.1 gy_profile_led.c\.h

我们首先查看一下他的服务文件,也就是gy_profile_led,我们我们就是通过这个服务来接收主机发送的LED控制数据的。

可以看到这个服务初始化ble_led_init函数中对于服务以及他的特征值属性的初始化过程,首先我们先初始化一个回调(p_led->led_write_handler = p_led_init->led_write_handler;),这个回调是用来将gy_profile_led这一层的数据,上传给mian文件去处理。

接下来是服务的添加,首先是调用sd_ble_uuid_vs_add去添加服务,然后给这个ble_uuid的服务的参数赋值,最后调用sd_ble_gatts_service_add函数去注册这个服务,这边我们注册的服务句柄是p_led->service_handle。

注册完服务之后,我们就要开始添加我们的特征值characteristic,特征值的添加也是一样的,首先配置特征值的参数,这些参数中我们主要关注一下ble_gatt_char_props_t,这个参数是用来定义特征值的属性的,可以看到我们的属性有如下几种:

/**@brief GATT Characteristic Properties. */
typedef struct
{
  /* Standard properties */
  uint8_t broadcast       :1; /**< 广播 */
  uint8_t read            :1; /**< 读 */
  uint8_t write_wo_resp   :1; /**< 写指令 */
  uint8_t write           :1; /**< 写*/
  uint8_t notify          :1; /**< 通知*/
  uint8_t indicate        :1; /**< 暗示 */
  uint8_t auth_signed_wr  :1; /**< 签名写指令 */
} ble_gatt_char_props_t;

因为我们的led的服务,是手机写数据给开发板控制LED,所以我们要定义特征值写使能(add_char_params.char_props.write = 1;),另外当我们需要知道上次是发送的什么控制数据给开发板时,我们需要读一下数据,所以这边我们同样定义一下读使能(add_char_params.char_props.read = 1;),当我们配置完特征值的属性之后,我们调用characteristic_add函数,去向刚刚注册的p_led->service_handle服务中添加我们的特征值。

53 //******************************************************************************
54 // fn :ble_led_init
55 //
56 // brief : 初始化LED服务
57 //
58 // param : p_led -> led服务结构体
59 //         p_led_init -> led服务初始化结构体
60 //
61 // return : uint32_t -> 成功返回SUCCESS,其他返回ERR NO.
62 uint32_t ble_led_init(ble_led_t * p_led, const ble_led_init_t * p_led_init)
63 {
64     uint32_t              err_code;
65     ble_uuid_t            ble_uuid;
66     ble_add_char_params_t add_char_params;
67 
68     // 初始化服务结构体
69     p_led->led_write_handler = p_led_init->led_write_handler;
70 
71     // 添加服务(128bit UUID)
72     ble_uuid128_t base_uuid = {LED_UUID_BASE};
73     err_code = sd_ble_uuid_vs_add(&base_uuid, &p_led->uuid_type);
74     VERIFY_SUCCESS(err_code);
75 
76     ble_uuid.type = p_led->uuid_type;
77     ble_uuid.uuid = LED_UUID_SERVICE;
78 
79     err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_led->service_handle);
80     VERIFY_SUCCESS(err_code);
81 
82     // 添加LED特征值(属性是Write和Read、长度是4)
83     memset(&add_char_params, 0, sizeof(add_char_params));
84     add_char_params.uuid             = LED_UUID_CHAR;
85     add_char_params.uuid_type        = p_led->uuid_type;
86     add_char_params.init_len         = LED_UUID_CHAR_LEN;
87     add_char_params.max_len          = LED_UUID_CHAR_LEN;
88     add_char_params.char_props.read  = 1;
89     add_char_params.char_props.write = 1;
90 
91     add_char_params.read_access  = SEC_OPEN;
92     add_char_params.write_access = SEC_OPEN;
93 
94     return characteristic_add(p_led->service_handle, &add_char_params, &p_led->led_char_handles);
95 }

看完服务的初始化,我们来看下我们的服务注册之后是怎么来进行工作的,首先我们看下ble_led_on_ble_evt这个函数,这个函数在我们mian函数中注册BLE_LED_DEF(m_led);实例的时候被引用。

15 //******************************************************************************
16 // fn :BLE_LED_DEF
17 //
18 // brief : 初始化LED服务实例
19 //
20 // param : _name -> 实例的名称
21 //
22 // return : none
23 #define BLE_LED_DEF(_name)                                                                          \
24 static ble_led_t _name;                                                                             \
25 NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                 \
26                      BLE_LED_BLE_OBSERVER_PRIO,                                                     \
27                      ble_led_on_ble_evt, &_name)

这个m_led实例注册,涉及到NRF_SDH_BLE_OBSERVER的使用,简单的理解就是利用这个注册了实例之后,当底层有GAP或者GATT消息返回的时候,就会触发ble_led_on_ble_evt函数。 因为我们LED服务这里需要接收手机端write的LED控制数据,所以我们在事件判断中,判断是否出现GATT Write事件,一旦出现了,我们调用on_write函数去处理这个事件。

28 //******************************************************************************
29 // fn :ble_led_on_ble_evt
30 //
31 // brief : BLE事件处理函数
32 //
33 // param : p_ble_evt -> ble事件
34 //         p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
35 //
36 // return : none
37 void ble_led_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
38 {
39     ble_led_t * p_led = (ble_led_t *)p_context;
40 
41     switch (p_ble_evt->header.evt_id)
42     {
43         // GATT Client Write事件
44         case BLE_GATTS_EVT_WRITE:
45             on_write(p_led, p_ble_evt);
46             break;
47 
48         default:
49             break;
50     }
51 }

on_write函数用于处理接收的write数据,我们判断一下接收的数据是否符合我们的要求,如果符合,那么我们通过初始化函数中的回调函数,将接收到的值上传到main函数中去处理。

 7 //******************************************************************************
 8 // fn :on_write
 9 //
10 // brief : 处理Write事件的函数。
11 //
12 // param : p_led -> led服务结构体
13 //         p_ble_evt -> ble事件
14 //
15 // return : none
16 static void on_write(ble_led_t * p_led, ble_evt_t const * p_ble_evt)
17 {
18     ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
19 
20     if (   (p_evt_write->handle == p_led->led_char_handles.value_handle)
21         && (p_evt_write->len <= LED_UUID_CHAR_LEN)
22         && (p_led->led_write_handler != NULL))
23     {
24         p_led->led_write_handler((uint8_t*)p_evt_write->data);
25     }
26 }
21.3.2.2 main.c

main文件中也不给大家全部介绍了,这个和蓝牙协议实验部分是重合的,我们只关注实验改动的部分。

我们看下服务初始化的部分,可以看到调用了我们gy_profile_led中的ble_led_init函数初始化注册了我们的LED服务,并且通用注册了一个回调函数。

在这个回调函数led_write_handler中,我们可以获取到gy_profile_led中上传上来的接收到的wirte数据,并且利用这个数据进行LED的控制。

195 //******************************************************************
196 // fn : nus_data_handler
197 //
198 // brief : 用于处理来自Nordic UART服务的数据的功能
199 // details : 该功能将处理从Nordic UART BLE服务接收的数据并将其发送到UART模块
200 //
201 // param : ble_nus_evt_t -> nus事件
202 //
203 // return : none
204 static void led_write_handler(uint8_t * new_state)
205 {
206     NRF_LOG_INFO("Recive State:%02X,%02X,%02X,%02X",new_state[0],new_state[1],new_state[2],new_state[3]);
207     LED_Control(BSP_LED_0, new_state[0]);
208     LED_Control(BSP_LED_1, new_state[1]);
209     LED_Control(BSP_LED_2, new_state[2]);
210     LED_Control(BSP_LED_3, new_state[3]);
211 }
212 
213 //******************************************************************
214 // fn : services_init
215 //
216 // brief : 初始化复位(本例程展示NUS:Nordic Uart Service)
217 //
218 // param : none
219 //
220 // return : none
221 static void services_init(void)
222 {
223     uint32_t           err_code;
224     ble_led_init_t     led_init;
225 
226     // Initialize NUS.
227     memset(&led_init, 0, sizeof(led_init));
228 
229     led_init.led_write_handler = led_write_handler;
230 
231     err_code = ble_led_init(&m_led, &led_init);
232     APP_ERROR_CHECK(err_code);
233 }

21.4 实验总结

通过这一章节的学习,我们需要掌握下面3个要点。

1、从机如何注册一个自定的服务,并且在服务下添加自己的特征值功能

2、主机如何针对指定的从机服务,去获取这个服务以及服务下指定特征值的句柄

3、主机如何通过Write属性,向从机发送数据

22 Notify属性服务实验

22.1 实验简介

通过Write属性服务实验的学习,我们已经知道了从机设备如何注册一个自定义服务(自定义的LED服务),然后主机如何去发现这个服务,并且利用这个服务的特征值与从机设备进行通信,控制从机设备的LED点亮。

也就是说,我们已经学会了如何通过主机给从机发送数据,所以接下来我们本章节将会给大家讲解从机如何通过notify属性给主机发送数据。

22.2 实验现象

主机设备流程:

1、扫描符合我们连接过滤要求的从机设备(根据LED服务的UUID过滤)

2、成功连接我们的从机设备,并且更新连接参数和MTU

3、发现服务,成功发现Ghostyu BTN Service

4、成功使用了从机服务的notify功能

Nrf52832dk-ble-gattnotify2.png

从机设备流程:

1、开启广播

2、被主机成功连接,并交互连接参数

3、等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)

4、等待主机成功使能notify功能

5、从机分别按下4个按键,给主机发送相应的notify数据包,控制主机LED点亮

Nrf52832dk-ble-gattnotify1.png

22.3 工程及源码讲解

22.3.1 主机部分
22.3.1.1 gy_profile_btn_c.c\.h及mian.c

这个实验主机部分的代码和上一章节的Write的主机是类似的,他的整个的服务发现流程是一样的,唯一不同的点是由于从机需要发送notify数据,所以我们的主机需要去使能从机的notify的功能。

所以我们来看一下主机是在哪边,以怎样的方式去使能从机的notify。以及最终是怎么接收来自从机的数据的。

首先看下使能notify的部分,通过上一实验的学习,我们已经知道当我们自己成功跑完获取服务的流程,最后会给main函数中的服务初始化的回调返回成功的事件。我们看下成功发现服务的事件BLE_BTN_C_EVT_DISCOVERY_COMPLETE,里面首先还是调用ble_btn_c_handles_assign函数将获取的句柄值和我们m_ble_btn_c实例绑定起来。然后就去调用ble_btn_c_tx_notif_enable函数去使能从机的notify功能。

272 //******************************************************************
273 // fn : ble_btn_c_evt_handler
274 //
275 // brief : BTN服务事件
276 //
277 // param : none
278 //
279 // return : none                 
280 static void ble_btn_c_evt_handler(ble_btn_c_t * p_ble_btn_c, ble_btn_c_evt_t * p_evt)
281 {
282     ret_code_t err_code;
283 
284     switch (p_evt->evt_type)
285     {
286         case BLE_BTN_C_EVT_DISCOVERY_COMPLETE:
287             NRF_LOG_INFO("Discovery complete.");
288             err_code = ble_btn_c_handles_assign(&m_ble_btn_c, p_evt->conn_handle, &p_evt->params.peer_db);
289             APP_ERROR_CHECK(err_code);
290             NRF_LOG_INFO("Connected to device with Ghostyu BTN Service.");
291             
292             err_code = ble_btn_c_tx_notif_enable(&m_ble_btn_c);
293             APP_ERROR_CHECK(err_code);  
294             NRF_LOG_INFO("Enable notification.");
295             break;

我们来看下使能notify的函数的代码,不难发现其实就是一个write功能,不过不是上一个实验向我们的handle_value去发送数据,而是向cccd_handle去发送了一个0x01(BLE_GATT_HVX_NOTIFICATION),0x00的数据。

179 //******************************************************************************
180 // fn :cccd_configure
181 //
182 // brief : 用于向CCCD发送数据,控制使能或者禁能notify功能
183 //
184 // param : conn_handle -> 连接的句柄
185 //         conn_handle -> CCCD的句柄
186 //         enable -> 使能或者禁能标识
187 //
188 // return : none
189 static uint32_t cccd_configure(uint16_t conn_handle, uint16_t cccd_handle, bool enable)
190 {
191     uint8_t buf[BLE_CCCD_VALUE_LEN];
192 
193     buf[0] = enable ? BLE_GATT_HVX_NOTIFICATION : 0;
194     buf[1] = 0;
195 
196     ble_gattc_write_params_t const write_params =
197     {
198         .write_op = BLE_GATT_OP_WRITE_REQ,
199         .flags    = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
200         .handle   = cccd_handle,
201         .offset   = 0,
202         .len      = sizeof(buf),
203         .p_value  = buf
204     };
205 
206     return sd_ble_gattc_write(conn_handle, &write_params);
207 }
208 
209 //******************************************************************************
210 // fn :ble_btn_c_tx_notif_enable
211 //
212 // brief : 用于使能从机btn服务的notify功能
213 //
214 // param : p_ble_btn_c -> 指向要关联的BTN结构实例的指针
215 //
216 // return : none
217 uint32_t ble_btn_c_tx_notif_enable(ble_btn_c_t * p_ble_btn_c)
218 {
219     VERIFY_PARAM_NOT_NULL(p_ble_btn_c);
220 
221     if ( (p_ble_btn_c->conn_handle == BLE_CONN_HANDLE_INVALID)
222        ||(p_ble_btn_c->peer_btn_db.cccd_handle == BLE_GATT_HANDLE_INVALID)
223        )
224     {
225         return NRF_ERROR_INVALID_STATE;
226     }
227     return cccd_configure(p_ble_btn_c->conn_handle,p_ble_btn_c->peer_btn_db.cccd_handle, true);
228 }

看完了使能notify的函数,我们来看下接收从机数据的处理部分,首先是看到ble_btn_c_on_ble_evt函数,这个函数在我们调用BLE_BTN_C_DEF(m_ble_btn_c);注册实例的时候,就已经创建好了,用于接收底层的softdevice的消息返回。 我们看下其中的BLE_GATTC_EVT_HVX(Handle Value Notification or Indication event)事件,在这个事件下我们接收到从机发送给我们的数据。

146 //******************************************************************************
147 // fn :ble_btn_c_on_ble_evt
148 //
149 // brief : BLE事件处理函数
150 //
151 // param : p_ble_evt -> ble事件
152 //         p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
153 //
154 // return : none
155 void ble_btn_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
156 {
157     if ((p_context == NULL) || (p_ble_evt == NULL))
158     {
159         return;
160     }
161 
162     ble_btn_c_t * p_ble_btn_c = (ble_btn_c_t *)p_context;
163 
164     switch (p_ble_evt->header.evt_id)
165     {
166         case BLE_GATTC_EVT_HVX:
167             on_hvx(p_ble_btn_c, p_ble_evt);
168             break;
169       
170         case BLE_GAP_EVT_DISCONNECTED:
171             on_disconnected(p_ble_btn_c, p_ble_evt);
172             break;
173 
174         default:
175             break;
176     }
177 }

然后我们看下对于接收到的从机数据的处理,首先还是一样的,我们需要判断一下数据的来源是不是我们btn_handle。当确认都是正确的,然后我们将接收的数据复制给ble_btn_c_evt_t,然后通过它的回调上传到我们的main文件中,携带的事件ID为BLE_BTN_C_EVT_BTN_TX_EVT。

32 //******************************************************************************
33 // fn :on_hvx
34 //
35 // brief : 用于处理从SoftDevice接收到的通知
36 //
37 // param : p_ble_btn_c -> 指向BTN Client结构的指针
38 //         p_ble_evt -> 指向接收到的BLE事件的指针
39 //
40 // return : none
41 static void on_hvx(ble_btn_c_t * p_ble_btn_c, ble_evt_t const * p_ble_evt)
42 {
43     if (   (p_ble_btn_c->peer_btn_db.btn_handle != BLE_GATT_HANDLE_INVALID)
44         && (p_ble_evt->evt.gattc_evt.params.hvx.handle == p_ble_btn_c->peer_btn_db.btn_handle)
45         && (p_ble_btn_c->evt_handler != NULL))
46     {
47         ble_btn_c_evt_t ble_btn_c_evt;
48 
49         ble_btn_c_evt.evt_type = BLE_BTN_C_EVT_BTN_TX_EVT;
50         ble_btn_c_evt.p_data   = (uint8_t *)p_ble_evt->evt.gattc_evt.params.hvx.data;
51         ble_btn_c_evt.data_len = p_ble_evt->evt.gattc_evt.params.hvx.len;
52 
53         p_ble_btn_c->evt_handler(p_ble_btn_c, &ble_btn_c_evt);
54         NRF_LOG_DEBUG("Client sending data.");
55     }
56 }

接下来返回到我们的main文件中,我们在ble_btn_c_evt_handler回调中可以看到BLE_BTN_C_EVT_BTN_TX_EVT事件的处理,我们将接收到的从机数据用于控制相应的LED灯点亮。

297         case BLE_BTN_C_EVT_BTN_TX_EVT:
298             NRF_LOG_DEBUG("Receiving data.");
299             NRF_LOG_HEXDUMP_DEBUG(p_evt->p_data, p_evt->data_len);
300             LED_Control(BSP_LED_0, p_evt->p_data[0]);
301             LED_Control(BSP_LED_1, p_evt->p_data[1]);
302             LED_Control(BSP_LED_2, p_evt->p_data[2]);
303             LED_Control(BSP_LED_3, p_evt->p_data[3]);
304             break;
305             
306         default:
307             break;
308     }
309 }
22.3.2 从机部分
22.3.2.1 gy_profile_btn.c\.h

首先我们还是先看一下服务配置文件,首先还是注册一下服务,注册的服务句柄是p_btn->service_handle。服务注册完成之后,我们注册按键的特征值,可以看到我们分别使能了按键的notify通知属性(add_char_params.char_props.notify = 1;),并且同样使能了read属性(add_char_params.char_props.read = 1;)。

这里我们需要注意的是下面的cccd_write_access参数被使能,上一实验大家都好理解需要使能write_access和read_access,因为我们需要使用读写,那么为什么这个例程要使能cccd_write_access呢,这边给大家简单说明一下CCCD。

Icon-info.png
Client Characteristic Configuration Descriptor(CCCD)是客户端特征配置描述符。当主机向CCCD中写入0x0001,此时使能notify;当写入0x0000时,此时禁止notify。

在nordic的协议栈当中,他的这个notify使能是交给用户自己处理的,也是说即便主机没有向cccd中写入0x0001去使能notify,我们同样可以直接利用notify去发送数据,只能这样不符合规范。

50 //******************************************************************************
51 // fn :ble_btn_init
52 //
53 // brief : 初始化BTN服务
54 //
55 // param : p_btn -> btn服务结构体
56 //
57 // return : uint32_t -> 成功返回SUCCESS,其他返回ERR NO.
58 uint32_t ble_btn_init(ble_btn_t * p_btn)
59 {
60     uint32_t              err_code;
61     ble_uuid_t            ble_uuid;
62     ble_add_char_params_t add_char_params;
63 
64     // 添加服务(128bit UUID)
65     ble_uuid128_t base_uuid = {BTN_UUID_BASE};
66     err_code = sd_ble_uuid_vs_add(&base_uuid, &p_btn->uuid_type);
67     VERIFY_SUCCESS(err_code);
68 
69     ble_uuid.type = p_btn->uuid_type;
70     ble_uuid.uuid = BTN_UUID_SERVICE;
71 
72     err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_btn->service_handle);
73     VERIFY_SUCCESS(err_code);
74 
75     // 添加BTN特征值(属性是Write和Read、长度是4)
76     memset(&add_char_params, 0, sizeof(add_char_params));
77     add_char_params.uuid             = BTN_UUID_CHAR;
78     add_char_params.uuid_type        = p_btn->uuid_type;
79     add_char_params.init_len         = BTN_UUID_CHAR_LEN;
80     add_char_params.max_len          = BTN_UUID_CHAR_LEN;
81     add_char_params.char_props.read  = 1;
82     add_char_params.char_props.notify = 1;
83     
84     add_char_params.read_access  = SEC_OPEN;
85     add_char_params.cccd_write_access = SEC_OPEN;
86 
87     return characteristic_add(p_btn->service_handle, &add_char_params, &p_btn->btn_char_handles);
88 }

服务部分剩下的处理流程和上一实验是类型的,只不过上一个实验是处理的wirte属性,而这个实验是处理notify属性。 首先在BLE事件处理的函数中,我们应该要处理CCCD_Write的数据的,所以在由softdevice返回消息的ble_btn_on_ble_evt函数中,我们需要处理一下BLE_GATTS_EVT_WRITE事件。

30 //******************************************************************************
31 // fn :ble_led_on_ble_evt
32 //
33 // brief : BLE事件处理函数
34 //
35 // param : p_ble_evt -> ble事件
36 //         p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
37 //
38 // return : none
39 //******************************************************************************
40 // fn :ble_btn_on_ble_evt
41 //
42 // brief : BLE事件处理函数
43 //
44 // param : p_ble_evt -> ble事件
45 //         p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
46 //
47 // return : none
48 void ble_btn_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
49 {
50     ble_btn_t * p_btn = (ble_btn_t *)p_context;
51   
52     switch (p_ble_evt->header.evt_id)
53     {
54         case BLE_GATTS_EVT_WRITE:
55             on_write(p_btn, p_ble_evt);
56             break;
57             
58         default:
59             break;
60     }
61 }

在这个on_write函数中,我们接收到了主机发送过来的使能从机notify的数据,我们需要判断一下接收的数据的句柄是不是cccd_handle,以及接收的数据长度是不是2字节(使能数据:01 00)。如果成功使能了notify,那么我们打印"notification enabled"。

10 static void on_write(ble_btn_t * p_btn, ble_evt_t const * p_ble_evt)
11 {
12     ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
13     
14     if ((p_evt_write->handle == p_btn->btn_char_handles.cccd_handle) &&
15         (p_evt_write->len == 2))
16     {
17       if (ble_srv_is_notification_enabled(p_evt_write->data))
18       {
19           p_client.is_notification_enabled = true;
20           NRF_LOG_INFO("notification enabled");
21       }
22       else
23       {
24           p_client.is_notification_enabled = false;
25           NRF_LOG_INFO("notification disabled");
26       }
27     }
28 }

除了上述的3个函数,我们的从机服务文件,就只剩下一个发送notify数据的函数了。首先我们一定要先判断一下是否已经使能的notify使能,并且判断数据长度是否符合要求。

下面这个函数,就是我们notify发送数据的函数,他的参数我们只需要配置4个。

type配置为BLE_GATT_HVX_NOTIFICATION,代表是notify属性的数据;

handle我们需要配置为我们按键特征值的value.handle,代表的是按键特征值的Value这个列表的句柄;

剩下的p_data和p_len就是我们需要发送的数据以及数据的长度。

 95 //******************************************************************************
 96 // fn :ble_btn_data_send
 97 //
 98 // brief : 处理按键按下,状态更新的事件
 99 //
100 // param : p_btn -> btn结构体
101 //         p_data -> 数据指针
102 //         p_length -> 数据长度
103 //         conn_handle -> 连接的句柄
104 //
105 // return : none
106 uint32_t ble_btn_data_send(ble_btn_t *p_btn, uint8_t *p_data, uint16_t p_length, uint16_t conn_handle)
107 {
108 
109     ble_gatts_hvx_params_t     hvx_params;
110 
111     VERIFY_PARAM_NOT_NULL(p_btn);
112 
113     if (conn_handle == BLE_CONN_HANDLE_INVALID)
114     {
115         return NRF_ERROR_NOT_FOUND;
116     }
117 
118     if (!p_client.is_notification_enabled)
119     {
120         return NRF_ERROR_INVALID_STATE;
121     }
122 
123     if (p_length > BTN_UUID_CHAR_LEN)
124     {
125         return NRF_ERROR_INVALID_PARAM;
126     }
127 
128     memset(&hvx_params, 0, sizeof(hvx_params));
129     hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
130     hvx_params.handle = p_btn->btn_char_handles.value_handle;
131     hvx_params.p_data = p_data;
132     hvx_params.p_len  = &p_length;
133 
134     return sd_ble_gatts_hvx(conn_handle, &hvx_params);
135 }
22.3.2.2 main.c

首先我们还是需要添加一下服务初始化函数。其他的处理都是在gy_profile_btn文件中,所以main文件中就只剩下一个按键触发后调用notify发送的部分。

194 //******************************************************************
195 // fn : services_init
196 //
197 // brief : 初始化复位(本例程展示NUS:Nordic Uart Service)
198 //
199 // param : none
200 //
201 // return : none
202 static void services_init(void)
203 {
204     uint32_t           err_code;
205 
206     err_code = ble_btn_init(&m_btn);
207     APP_ERROR_CHECK(err_code);
208 }

当有按键按下时,最终会将按键消息传递到这个回调中进行处理,我们根据按键触发的消息,对相应的buf值进行修改,最后调用ble_btn_data_send函数将数据发送给主机。

351 //******************************************************************
352 // fn : btn_evt_handler_t
353 //
354 // brief : 按键触发回调函数
355 // 
356 // param : butState -> 当前的按键值
357 //
358 // return : none
359 void btn_evt_handler_t (uint8_t butState)
360 {
361   uint8_t buf[BTN_UUID_CHAR_LEN] = {0x01,0x01,0x01,0x01};
362   switch(butState)
363   {
364     case BUTTON_1:
365       buf[0] = 0x00;
366       break;
367     case BUTTON_2:
368       buf[1] = 0x00;
369       break;
370     case BUTTON_3:
371       buf[2] = 0x00;
372       break;
373     case BUTTON_4:
374       buf[3] = 0x00;
375       break;
376     default:
377       break;
378   }
379   ble_btn_data_send(&m_btn, buf, BTN_UUID_CHAR_LEN, m_conn_handle);
380 }

22.4 实验总结

经过本章内容的学习,我们需要对notify属性的服务有一定的了解,主要掌握以下3点:

1、我们需要了解主机如何去使能从机的notify功能

2、从机如何创建一个支持notify功能的特征值服务

3、从机如何发送一个notify功能的数据包给主机

本PDF由谷雨文档中心自动生成,点击下方链接阅读最新内容。

取自“http://doc.iotxx.com/index.php?title=NRF52832DK协议栈实验&oldid=2877