打开主菜单

谷雨文档中心 β

NRF52832DK协议栈高级实验

工程编号 实验名称 工程简介
3.0_ble_peripheral_profile_led LED控制实验 实现了LED服务,可以通过手机控制LED灯的点亮和熄灭
3.1_ble_peripheral_profile_btn 按键控制实验 实现了按键服务,可以将按键按下的状态发送给手机
3.2_ble_peripheral_beacon 基站(信标)实验 BEACON格式广播
3.3_ble_peripheral_nus 从机NUS实验(串口透传) 实现了NUS服务,可以实现主从机的串口透传
3.4_ble_central_nus 主机NUS实验(串口透传) 实现了NUS服务,可以实现主从机的串口透传
3.5_ble_peripheral_multi 一从多主实验 一个从机支持被多个主机连接
3.6_ble_central_multi 一主多从实验 一个主机支持连接多个从机
3.7_ble_central_peripheral 主从一体实验 异常
3.8_ble_hid_takephoto 自拍实验 利用BLE HID协议,蓝牙模块控制手机照相机拍照
3.9_ble_hid_musicplayer 音乐播放器实验 利用BLE HID协议,蓝牙模块控制手机音乐播放器
Icon-tips.png
实验源码位于百度云盘归档资料中:归档资料/1-协议栈SDK/谷雨实验源码包/,代码的使用请参考《NRF52832DK入门手册》的蓝牙协议栈SDK一节

目录

1 LED控制实验

1.1 实验简介

我们的蓝牙实战实验将从蓝牙控制IO输出高低电平,以此来控制开发板上LED点亮和熄灭开始。

在这个实验中,我们主要会展示一下,有关特征值的write以及read属性,其中由于read属性相对而言使用的较少。我们主要会给大家介绍write属性,也就是主机给从机发送数据的属性。

1.2 硬件说明

nRF52DK开发板采用与Nordic官方开发板相同的指示灯电路,原则协议栈例程可以直接控制指示灯,无需修改代码。

指示灯与芯片引脚对应关系如下表格。

网络标号 芯片引脚号 连接方式
LED1 P0.17 直连,低电平亮灯
LED2 P0.18 直连,低电平亮灯
LED3 P0.19 直连,低电平亮灯
LED4 P0.20 直连,低电平亮灯
 

1.3 实验现象

手机端通过nordic的app"nrf master control panel",发起对设备的扫描和连接,连接成功之后,我们通过UUID FFF1给开发板发送数据。

例如发送0x00,0x00,0x00,0x00给开发板,此时开发板的4个LED灯均被点亮;同样的我们给将某位数据改成0x01,对应的LED就会熄灭。

1.4 源码讲解

1.4.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 }
1.4.2 gy_serial_led.c\.h

有关外设处理,请大家查看基础实验部分

1.4.3 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 }

2 按键控制实验

2.1 实验简介

按键控制实验,就是开发板上的按键按下,然后我们将通过蓝牙上报手机是哪一个按键被按下。

在上一个LED控制实验中,我们主要讲了Write属性,也就是主机给从机发送数据。在这个实验中,我们将会给大家展示从机如何给主机发送数据,也就是notify属性的使用。

2.2 硬件说明

nRF52DK开发板采用与Nordic官方开发板相同的按键电路,原则协议栈例程可以直接使用按键,无需修改代码。

按键与芯片引脚对应关系如下表格。

网络标号 芯片引脚号 连接方式
BTN1 P0.13 直连,下降沿(低电平)触发
BTN2 P0.14 直连,下降沿(低电平)触发
BTN3 P0.15 直连,下降沿(低电平)触发
BTN4 P0.16 直连,下降沿(低电平)触发
 

2.3 实验现象

首先还是使用nordic的app "nrf master control panel"去连接我们的开发板,连接成功之后,我们点击UUID FFE1特征值的使用notify的按钮(三个向下箭头的图标)。

使能了notify之后,我们分别按下开发板上的按键S1-S4,可以看到手机上显示对应的上报状态数据。

2.4 源码讲解

2.4.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。

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的数据的,但是这边我们偷懒了,不去做处理,验证一下主机不发送数据使能notify,我们也可以通过notify发送数据出去。

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

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

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

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

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

 7 //******************************************************************************
 8 // fn :ble_lbs_on_button_change
 9 //
10 // brief : 处理按键按下,状态更新的事件
11 //
12 // param : conn_handle -> 连接的句柄
13 //         p_btn -> btn结构体
14 //         button_state -> 按键状态
15 //
16 // return : none
17 uint32_t ble_lbs_on_button_change(uint16_t conn_handle, ble_btn_t * p_btn, uint8_t *button_state)
18 {
19     ble_gatts_hvx_params_t params;
20     uint16_t len = BTN_UUID_CHAR_LEN;
21 
22     memset(&params, 0, sizeof(params));
23     params.type   = BLE_GATT_HVX_NOTIFICATION;
24     params.handle = p_btn->btn_char_handles.value_handle;
25     params.p_data = button_state;
26     params.p_len  = &len;
27 
28     return sd_ble_gatts_hvx(conn_handle, &params);
29 }
2.4.2 gy_serial_btn.c\.h

按键的外设处理驱动文件,这个详细的说明也是请大家查看基础实验部分的,这里我们只关注一下按键被触发,然后通过notify函数,去向手机发送通知。

41 //******************************************************************
42 // fn : btn_timeout_handler
43 //
44 // brief : 按键定时器超时任务
45 // 
46 // param : p_event -> 指向数据库发现事件的指针
47 //
48 // return : none
49 static void btn_timeout_handler(void * p_context)
50 { 
51   uint8_t btnStateBuf[BTN_UUID_CHAR_LEN] = {0};
52   
53   if(nrf_drv_gpiote_in_is_set(BUTTON_1) == 0)
54   {
55     NRF_LOG_INFO("BTN1");
56     btnStateBuf[0] = 0x01;
57     btnState = 1;
58   }
59 
60   if(nrf_drv_gpiote_in_is_set(BUTTON_2) == 0)
61   {
62     NRF_LOG_INFO("BTN2");
63     btnStateBuf[1] = 0x01;
64     btnState = 1;
65   }
66   
67   if(nrf_drv_gpiote_in_is_set(BUTTON_3) == 0)
68   {
69     NRF_LOG_INFO("BTN3");
70     btnStateBuf[2] = 0x01;
71     btnState = 1;
72   }
73   
74   if(nrf_drv_gpiote_in_is_set(BUTTON_4) == 0)
75   {
76     NRF_LOG_INFO("BTN4");
77     btnStateBuf[3] = 0x01;
78     btnState = 1;
79   }
80   
81   if(btnState)
82   {
83     ble_lbs_on_button_change(0x0000, &m_btn, btnStateBuf);
84   }
85   btnState = 0;
86 }

2.5 main.c

由于这个例程的按键通知我们是放到了按键驱动文件中处理,所以在main函数当中我们只需要去初始化一下这个服务就可以了。

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 }

3 基站(信标)实验

3.1 实验简介

无线技术中的信标是广播小块信息的概念。 信息可以是任何东西,范围从环境数据(温度,气压,湿度等)到微位置数据(资产跟踪,零售等)或方向数据(加速,旋转等)。

传输的数据通常是静态的,但也可以是动态的并随时间变化。 通过使用蓝牙低功耗,可以将信标设计为在单个纽扣电池上运行多年。 本应用报告介绍了信标的概念以及如何开始实施信标解决方案。 本文档中的命名约定可以概括为使用蓝牙低能耗技术广告来广播信息的信标,该技术可被称为蓝牙低功耗。

3.2 实验现象

3.3 源码讲解

首先我们先定义beacon相关的数据,其中我们用户需要关注的主要有3个参数,UUID、Major以及Minor,其他的参数大家可以理解为固定的格式(格式固定,但数据内容不固定,可能有不同的厂商信息)。另外还有一个值得我们关注的数据,那就是APP_COMPANY_IDENTIFIER,如果我们定义此参数为0x004C(也就是Apple id),那么我们的基站设备就被成为iBeacon。

54 // BEACON数据
55 #define APP_BEACON_INFO_LENGTH          0x17                     // BEACON数据总长度
56 #define APP_ADV_DATA_LENGTH             0x15                     // BEACON特殊字节的长度
57 #define APP_DEVICE_TYPE                 0x02                     // 字节0x02代表这个设备是BEACON
58 #define APP_MEASURED_RSSI               0xC3                     // BEACON在1米距离处的信号强度
59 #define APP_COMPANY_IDENTIFIER          0x004C                   // 004C代表的是Apple id(只有这个ID,设备才会叫iBeacon)
60 #define APP_MAJOR_VALUE                 0x27, 0x12               // major
61 #define APP_MINOR_VALUE                 0x0B, 0x86               // minor
62 #define APP_BEACON_UUID                 0xFD,0xA5,0x06,0x93, \
63                                         0xA4,0xE2,0x4F,0xB1, \
64                                         0xAF,0xCF,0xC6,0xEB, \
65                                         0x07,0x64,0x78,0x25      // UUID
66 
67 // BEACON数据数组,用于初始化广播数据内容
68 static uint8_t m_beacon_info[APP_BEACON_INFO_LENGTH] = 
69 {
70     APP_DEVICE_TYPE,
71     APP_ADV_DATA_LENGTH, 
72     APP_BEACON_UUID,
73     APP_MAJOR_VALUE,
74     APP_MINOR_VALUE,
75     APP_MEASURED_RSSI
76 };

定义好基站的数据之后,我们需要把它添加到广播数据中,那么这边唯一的疑问的就是我们应该把基站数据作为什么type来添加。 从下面的代码中,我们不难看出,基站的数据是作为p_manuf_specific_data(也就是用户自定义)数据来添加的,在Beacon数据添加的时候,大家可以看到只有company_identifier是被独立出来配置的,这里我们将其配置为apple id,所以最终我们的设备类型会是iBeacon。

110 //******************************************************************
111 // fn : advertising_init
112 //
113 // brief : 用于初始化广播
114 //
115 // param : none
116 //
117 // return : none
118 static void advertising_init(void)
119 {
120     uint32_t               err_code;
121     ble_advertising_init_t init;
122     ble_advdata_manuf_data_t manuf_specific_data;
123     
124     memset(&init, 0, sizeof(init));
125 
126     // 扫描回调数据包含短设备名称(方便大家看app显示BEACON标志)
127     init.srdata.name_type = BLE_ADVDATA_SHORT_NAME;
128     init.srdata.short_name_len = 9;
129    
130     // 配置广播数据的flag,设置为beacon,配置为BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED
131     init.advdata.flags = BLE_GAP_ADV_FLAG_LE_GENERAL_DISC_MODE|BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED;
132     
133     // 配置广播数据中的BEACON数据
134     manuf_specific_data.company_identifier = APP_COMPANY_IDENTIFIER;
135     manuf_specific_data.data.p_data = (uint8_t *) m_beacon_info;
136     manuf_specific_data.data.size   = APP_BEACON_INFO_LENGTH;
137     init.advdata.p_manuf_specific_data = &manuf_specific_data;
138 
139     // 配置广播周期,40ms快速广播,超时参数配置为0代表一直广播
140     init.config.ble_adv_fast_enabled  = true;
141     init.config.ble_adv_fast_interval = 64;   // 64*0.625 = 40ms
142     init.config.ble_adv_fast_timeout  = 0;    // 1800*10ms = 18s
143 
144     err_code = ble_advertising_init(&m_advertising, &init);
145     APP_ERROR_CHECK(err_code);
146 
147     ble_advertising_conn_cfg_tag_set(&m_advertising, APP_BLE_CONN_CFG_TAG);
148 }

4 从机NUS实验

4.1 实验简介

NUS(Nordic Uart Service)实验,他本质上是和开始的LED与按键的实验是一样的,都是新建一个服务文件,注册好我们需要使用的特征值功能,然后传输数据。不过是要比他们稍微复杂一点,因为我们串口的数据是涉及到收发两个部分(也就是既有write也有notify),所以我们在注册特征值的时候,是要注册两个,一个负责write,一个负责notify。

剩下的就是串口外设的处理,我们这边对于串口驱动不做介绍,大家可以去看基础实验部分的说明。我们在这个实验中仅给大家讲解串口RX收到的数据如何通知(notify)给手机,手机下发(write)的数据如何通过串口TX打印显示出来。

4.2 硬件说明

串口一向是所有开发板必需的功能之一,nRF52DK开发板采用CH340C芯片,将芯片的UART信号转换为USB接口信号,方便电脑使用USB来虚拟一个串口调试。

CH340C外围电路简单,且不需要外部晶振,nRF52DK开发板采用与Nordic官方开发板相同的串口收发引脚,如下表格所示。

网络标号 芯片引脚号 连接方式
CH340_TX P0.08(nRF_RX信号) 通过SW1拨动开关,选择连接
CH340_RX P0.06(nRF_TX信号) 通过SW1拨动开关,选择连接
 

图中P5引出了CH340C的流控制引脚,但是没有连接任何线路,所需要使用流控制,可自行连接测试。

4.3 实验现象

4.4 源码讲解

4.4.1 ble_nus.c\.h

先看下nus服务的初始化函数,首先我们还是先初始化了一个回调p_nus_init->data_handler,这个回调用于将nus层的数据传递给mian文件使用。

接下来我们初始化nus服务,还是和以前一样的,经过给参数的赋值,最终我们调用sd_ble_gatts_service_add函数注册了一个句柄为p_nus->service_handle的服务。

最后我们来注册我们需要使用的特征值characteristic,这边我们需要注册两个,一个是TX(发送),一个是RX(接收)。

首先我们看一下RX特征值的添加,我们配置他的属性是write,因为手机(主机)发送数据是通过write的方式;接下来我们看下TX特征值,我们做为从机设备,发送数据给手机(主机)使用的是notify方式,我们依次将这两个特征值添加到服务当中。

248 uint32_t ble_nus_init(ble_nus_t * p_nus, ble_nus_init_t const * p_nus_init)
249 {
250     ret_code_t            err_code;
251     ble_uuid_t            ble_uuid;
252     ble_uuid128_t         nus_base_uuid = NUS_BASE_UUID;
253     ble_add_char_params_t add_char_params;
254 
255     VERIFY_PARAM_NOT_NULL(p_nus);
256     VERIFY_PARAM_NOT_NULL(p_nus_init);
257 
258     // Initialize the service structure.
259     p_nus->data_handler = p_nus_init->data_handler;
260 
261     /**@snippet [Adding proprietary Service to the SoftDevice] */
262     // Add a custom base UUID.
263     err_code = sd_ble_uuid_vs_add(&nus_base_uuid, &p_nus->uuid_type);
264     VERIFY_SUCCESS(err_code);
265 
266     ble_uuid.type = p_nus->uuid_type;
267     ble_uuid.uuid = BLE_UUID_NUS_SERVICE;
268 
269     // Add the service.
270     err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
271                                         &ble_uuid,
272                                         &p_nus->service_handle);
273     /**@snippet [Adding proprietary Service to the SoftDevice] */
274     VERIFY_SUCCESS(err_code);
275 
276     // Add the RX Characteristic.
277     memset(&add_char_params, 0, sizeof(add_char_params));
278     add_char_params.uuid                     = BLE_UUID_NUS_RX_CHARACTERISTIC;
279     add_char_params.uuid_type                = p_nus->uuid_type;
280     add_char_params.max_len                  = BLE_NUS_MAX_RX_CHAR_LEN;
281     add_char_params.init_len                 = sizeof(uint8_t);
282     add_char_params.is_var_len               = true;
283     add_char_params.char_props.write         = 1;
284     add_char_params.char_props.write_wo_resp = 1;
285 
286     add_char_params.read_access  = SEC_OPEN;
287     add_char_params.write_access = SEC_OPEN;
288 
289     err_code = characteristic_add(p_nus->service_handle, &add_char_params, &p_nus->rx_handles);
290     if (err_code != NRF_SUCCESS)
291     {
292         return err_code;
293     }
294 
295     // Add the TX Characteristic.
296     /**@snippet [Adding proprietary characteristic to the SoftDevice] */
297     memset(&add_char_params, 0, sizeof(add_char_params));
298     add_char_params.uuid              = BLE_UUID_NUS_TX_CHARACTERISTIC;
299     add_char_params.uuid_type         = p_nus->uuid_type;
300     add_char_params.max_len           = BLE_NUS_MAX_TX_CHAR_LEN;
301     add_char_params.init_len          = sizeof(uint8_t);
302     add_char_params.is_var_len        = true;
303     add_char_params.char_props.notify = 1;
304 
305     add_char_params.read_access       = SEC_OPEN;
306     add_char_params.write_access      = SEC_OPEN;
307     add_char_params.cccd_write_access = SEC_OPEN;
308 
309     return characteristic_add(p_nus->service_handle, &add_char_params, &p_nus->tx_handles);
310     /**@snippet [Adding proprietary characteristic to the SoftDevice] */
311 }

看完nus服务初始化之后,我们需要来看下ble_nus_on_ble_evt函数,这个函数在LED实验中有过说明,它的注册是在main文件中,我们注册nus实例的时候注册好的,功能是用来接收底层返回的GAP与GATT事件。 从代码中可以看到,我们这个实验需要接收的底层返回有3个,分别是:连接成功的GAP事件(BLE_GAP_EVT_CONNECTED)、write属性的GATT事件(BLE_GATTS_EVT_WRITE)以及notify通知传输完成的事件(BLE_GATTS_EVT_HVN_TX_COMPLETE)。

218 void ble_nus_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
219 {
220     if ((p_context == NULL) || (p_ble_evt == NULL))
221     {
222         return;
223     }
224 
225     ble_nus_t * p_nus = (ble_nus_t *)p_context;
226 
227     switch (p_ble_evt->header.evt_id)
228     {
229         case BLE_GAP_EVT_CONNECTED:
230             on_connect(p_nus, p_ble_evt);
231             break;
232 
233         case BLE_GATTS_EVT_WRITE:
234             on_write(p_nus, p_ble_evt);
235             break;
236 
237         case BLE_GATTS_EVT_HVN_TX_COMPLETE:
238             on_hvx_tx_complete(p_nus, p_ble_evt);
239             break;
240 
241         default:
242             // No implementation needed.
243             break;
244     }
245 }

首先我们看下GAP连接成功事件,我们处理了哪些消息,在连接成功指令,我们首先是调用blcm_link_ctx_get函数去获取蓝牙连接的相关信息(这个暂时没有明白它的具体意义,因为获取到的p_client并没有被任何地方使用)。 然后我们调用sd_ble_gatts_value_get函数去获取CCCD中的gatts_val数据,然后我们调用ble_srv_is_notification_enabled对这个数据进行判断,如果得到的gatts_val数据是0x0001,那么代表了主机已经使能了我们从机的notify功能,那么我们通过p_nus->data_handler向mian文件传递BLE_NUS_EVT_COMM_STARTED事件。

 72 static void on_connect(ble_nus_t * p_nus, ble_evt_t const * p_ble_evt)
 73 {
 74     ret_code_t                 err_code;
 75     ble_nus_evt_t              evt;
 76     ble_gatts_value_t          gatts_val;
 77     uint8_t                    cccd_value[2];
 78     ble_nus_client_context_t * p_client = NULL;
 79 
 80     err_code = blcm_link_ctx_get(p_nus->p_link_ctx_storage,
 81                                  p_ble_evt->evt.gap_evt.conn_handle,
 82                                  (void *) &p_client);
 83     if (err_code != NRF_SUCCESS)
 84     {
 85         NRF_LOG_ERROR("Link context for 0x%02X connection handle could not be fetched.",
 86                       p_ble_evt->evt.gap_evt.conn_handle);
 87     }
 88 
 89     /* Check the hosts CCCD value to inform of readiness to send data using the RX characteristic */
 90     memset(&gatts_val, 0, sizeof(ble_gatts_value_t));
 91     gatts_val.p_value = cccd_value;
 92     gatts_val.len     = sizeof(cccd_value);
 93     gatts_val.offset  = 0;
 94 
 95     err_code = sd_ble_gatts_value_get(p_ble_evt->evt.gap_evt.conn_handle,
 96                                       p_nus->rx_handles.cccd_handle,
 97                                       &gatts_val);
 98 
 99     if ((err_code == NRF_SUCCESS)     &&
100         (p_nus->data_handler != NULL) &&
101         ble_srv_is_notification_enabled(gatts_val.p_value))
102     {
103         if (p_client != NULL)
104         {
105             p_client->is_notification_enabled = true;
106         }
107 
108         memset(&evt, 0, sizeof(ble_nus_evt_t));
109         evt.type        = BLE_NUS_EVT_COMM_STARTED;
110         evt.p_nus       = p_nus;
111         evt.conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
112         evt.p_link_ctx  = p_client;
113 
114         p_nus->data_handler(&evt);
115     }
116 }

接下来我们查看一下接收到的主机数据的处理,对于接收到的主机数据,我们分两个部分判断,一个是CCCD写的数据(p_nus->tx_handles.cccd_handle),一个是特征值RX的数据(p_nus->rx_handles.value_handle)。

CCCD的数据,我们接收到之后,去判断一下是使能notify还是禁能notify(也就是数据是0x0000还是0x0001)。如果是使能,则向main文件返回BLE_NUS_EVT_COMM_STARTED事件,如果是禁能,则向mian文件返回BLE_NUS_EVT_COMM_STOPPED事件。

RX特征值,是用于接收主机wirte的数据(也就是我们用户通信的数据),我们将接收的数据赋值给回调任务(evt.params.rx_data.p_data = p_evt_write->data; evt.params.rx_data.length = p_evt_write->len;),然后向mian文件返回BLE_NUS_EVT_RX_DATA事件。

124 static void on_write(ble_nus_t * p_nus, ble_evt_t const * p_ble_evt)
125 {
126     ret_code_t                    err_code;
127     ble_nus_evt_t                 evt;
128     ble_nus_client_context_t    * p_client;
129     ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
130 
131     err_code = blcm_link_ctx_get(p_nus->p_link_ctx_storage,
132                                  p_ble_evt->evt.gatts_evt.conn_handle,
133                                  (void *) &p_client);
134     if (err_code != NRF_SUCCESS)
135     {
136         NRF_LOG_ERROR("Link context for 0x%02X connection handle could not be fetched.",
137                       p_ble_evt->evt.gatts_evt.conn_handle);
138     }
139 
140     memset(&evt, 0, sizeof(ble_nus_evt_t));
141     evt.p_nus       = p_nus;
142     evt.conn_handle = p_ble_evt->evt.gatts_evt.conn_handle;
143     evt.p_link_ctx  = p_client;
144 
145     if ((p_evt_write->handle == p_nus->tx_handles.cccd_handle) &&
146         (p_evt_write->len == 2))
147     {
148         if (p_client != NULL)
149         {
150             if (ble_srv_is_notification_enabled(p_evt_write->data))
151             {
152                 p_client->is_notification_enabled = true;
153                 evt.type                          = BLE_NUS_EVT_COMM_STARTED;
154             }
155             else
156             {
157                 p_client->is_notification_enabled = false;
158                 evt.type                          = BLE_NUS_EVT_COMM_STOPPED;
159             }
160 
161             if (p_nus->data_handler != NULL)
162             {
163                 p_nus->data_handler(&evt);
164             }
165 
166         }
167     }
168     else if ((p_evt_write->handle == p_nus->rx_handles.value_handle) &&
169              (p_nus->data_handler != NULL))
170     {
171         evt.type                  = BLE_NUS_EVT_RX_DATA;
172         evt.params.rx_data.p_data = p_evt_write->data;
173         evt.params.rx_data.length = p_evt_write->len;
174 
175         p_nus->data_handler(&evt);
176     }
177     else
178     {
179         // Do Nothing. This event is not relevant for this service.
180     }
181 }

最后一个,也就是我们的BLE_GATTS_EVT_HVN_TX_COMPLETE事件的任务处理,这个事件代表的是我们notify数据向主机通知完成。所以这里我们也没有什么需要处理的,只需要向mian文件返回一个BLE_NUS_EVT_TX_RDY事件,代表我们发送完成了(notify方式发送)。

189 static void on_hvx_tx_complete(ble_nus_t * p_nus, ble_evt_t const * p_ble_evt)
190 {
191     ret_code_t                 err_code;
192     ble_nus_evt_t              evt;
193     ble_nus_client_context_t * p_client;
194 
195     err_code = blcm_link_ctx_get(p_nus->p_link_ctx_storage,
196                                  p_ble_evt->evt.gatts_evt.conn_handle,
197                                  (void *) &p_client);
198     if (err_code != NRF_SUCCESS)
199     {
200         NRF_LOG_ERROR("Link context for 0x%02X connection handle could not be fetched.",
201                       p_ble_evt->evt.gatts_evt.conn_handle);
202         return;
203     }
204 
205     if (p_client->is_notification_enabled)
206     {
207         memset(&evt, 0, sizeof(ble_nus_evt_t));
208         evt.type        = BLE_NUS_EVT_TX_RDY;
209         evt.p_nus       = p_nus;
210         evt.conn_handle = p_ble_evt->evt.gatts_evt.conn_handle;
211         evt.p_link_ctx  = p_client;
212 
213         p_nus->data_handler(&evt);
214     }
215 }

上面的部分介绍完之后,整个ble_nus.c文件就只剩下一个函数还没有介绍,也就是ble_nus_data_send函数,这个函数是预留给大家在mian文件中调用,去notify数据的。 在发送数据之前,我们首先先分别判断了一下设备是否连接了,notify属性是否被使能,发送的数据长度是否符合要求。以上都通过之后,我们将会给ble_gatts_hvx_params_t的参数赋值,最后调用sd_ble_gatts_hvx函数发送。

314 uint32_t ble_nus_data_send(ble_nus_t * p_nus,
315                            uint8_t   * p_data,
316                            uint16_t  * p_length,
317                            uint16_t    conn_handle)
318 {
319     ret_code_t                 err_code;
320     ble_gatts_hvx_params_t     hvx_params;
321     ble_nus_client_context_t * p_client;
322 
323     VERIFY_PARAM_NOT_NULL(p_nus);
324 
325     err_code = blcm_link_ctx_get(p_nus->p_link_ctx_storage, conn_handle, (void *) &p_client);
326     VERIFY_SUCCESS(err_code);
327 
328     if ((conn_handle == BLE_CONN_HANDLE_INVALID) || (p_client == NULL))
329     {
330         return NRF_ERROR_NOT_FOUND;
331     }
332 
333     if (!p_client->is_notification_enabled)
334     {
335         return NRF_ERROR_INVALID_STATE;
336     }
337 
338     if (*p_length > BLE_NUS_MAX_DATA_LEN)
339     {
340         return NRF_ERROR_INVALID_PARAM;
341     }
342 
343     memset(&hvx_params, 0, sizeof(hvx_params));
344 
345     hvx_params.handle = p_nus->tx_handles.value_handle;
346     hvx_params.p_data = p_data;
347     hvx_params.p_len  = p_length;
348     hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
349 
350     return sd_ble_gatts_hvx(conn_handle, &hvx_params);
351 }
4.4.2 mian.c

mian文件中的内容,很多都是和之前重复的,这边我们只说明两个部分。一个是nus服务的回调函数,也就是刚刚ble_nus.c中向main文件回调的一些事件的处理;另一部分就是接收到串口的数据,调用刚刚最后说明的notify发送函数去向主机传输数据的部分。

首先看下nus服务的回调函数,其中处理了4个事件,正是刚刚我们从ble_nus.c文件中回调上来的。

BLE_NUS_EVT_RX_DATA事件中,我们将接收的主机write数据,通过串口打印显示。

其他的三个事件中,分别提示了各自代表的意义。

212 //******************************************************************
213 // fn : nus_data_handler
214 //
215 // brief : 用于处理来自Nordic UART服务的数据的功能
216 // details : 该功能将处理从Nordic UART BLE服务接收的数据并将其发送到UART模块
217 //
218 // param : ble_nus_evt_t -> nus事件
219 //
220 // return : none
221 static void nus_data_handler(ble_nus_evt_t * p_evt)
222 {
223     if (p_evt->type == BLE_NUS_EVT_RX_DATA)
224     {
225         UART_Write((uint8_t*)p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
226         NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
227         NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
228     }
229     else if (p_evt->type == BLE_NUS_EVT_TX_RDY)
230     {
231         NRF_LOG_DEBUG(" Service is ready to accept new data to be transmitted..");
232     }
233     else if(p_evt->type == BLE_NUS_EVT_COMM_STARTED)
234     {
235        NRF_LOG_DEBUG("NUS Notification Enable.");
236        //app_timer_start(m_timer_nus, APP_TIMER_TICKS(10), NULL);
237     }
238     else if (p_evt->type == BLE_NUS_EVT_COMM_STOPPED)
239     {
240         NRF_LOG_DEBUG("NUS Notification Disable.");
241     }
242 }

接下来我们看下串口回调函数,与前面的nus服务的回调类似,不过这个回调是处理串口上传的数据。 我们接收到串口的数据,并且调用ble_nus_data_send函数去将接收的数据,传输给主机。

434 // 串口回调函数
435 void APP_UartEvtHandle(uart_evt_t* pEvt)
436 {
437     uint16_t len = 0;
438     switch(pEvt->evt_type)
439     {
440       case UART_EVT_RX_TIMEOUT:
441       case UART_EVT_RX_OVERFLOW:
442         len = UART_Read(Buf,pEvt->status);
443         ble_nus_data_send(&m_nus, Buf, &len, m_conn_handle);      //从串口发出
444       break;
445     }
446 }

5 主机NUS实验

5.1 实验简介

NUS_C(Nordic Uart Service Client)实验,这个实验就是针对NUS进行处理的。

这点是nordic的特色,之前我们做ti的CC25和CC26开发,从机的服务文件是一样需要大家根据自己需求实现,而主机部分的客户端处理,则是在一个大的事件回调中处理,开发者只要知道从机服务的UUID即可。而在nordic的工程中,我们可以看到,所有的服务都有其对应的客户端服务处理的文件,这样的好处在于一对一处理,我们对于细节上的处理能够做的更好,缺点在于如果大家自己新增服务,那么还需要编辑一个对应的客户端文件,这样增加了大家的开发难度。

剩下的就是串口外设的处理,我们这边对于串口驱动不做介绍,大家可以去看基础实验部分的说明。我们在这个实验中仅给大家讲解串口RX收到的数据如何写(Write)给从机,从机通知(notify)的数据,主机如何接收并通过串口TX打印出来显示。

5.2 硬件说明

串口一向是所有开发板必需的功能之一,nRF52DK开发板采用CH340C芯片,将芯片的UART信号转换为USB接口信号,方便电脑使用USB来虚拟一个串口调试。

CH340C外围电路简单,且不需要外部晶振,nRF52DK开发板采用与Nordic官方开发板相同的串口收发引脚,如下表格所示。

网络标号 芯片引脚号 连接方式
CH340_TX P0.08(nRF_RX信号) 通过SW1拨动开关,选择连接
CH340_RX P0.06(nRF_TX信号) 通过SW1拨动开关,选择连接
 

图中P5引出了CH340C的流控制引脚,但是没有连接任何线路,所需要使用流控制,可自行连接测试。

5.3 实验现象

5.4 源码讲解

5.4.1 ble_nus.c\.h

NUS_C客户端相对于NUS服务,功能部分要多一些,因为除了客户端初始化以及通信等回调事件的处理,多了一个部分,也就是发起服务扫描的回调函数处理。

首先是第一部分,也就是客户端初始化,以及底层回调的GAP和GATT事件的处理。

NUS客户端初始化函数,我们首先同样是先调用sd_ble_uuid_vs_add函数,根据我们的NUS_BASE_UUID,去确定一下客户端的UUID Type(p_ble_nus_c->uuid_type)。

然后我们分别初始化了p_ble_nus_c的连接句柄(conn_handle)、NUS服务的TX和RX的句柄,以及配置了一个回调函数p_ble_nus_c->evt_handler。

121 uint32_t ble_nus_c_init(ble_nus_c_t * p_ble_nus_c, ble_nus_c_init_t * p_ble_nus_c_init)
122 {
123     uint32_t      err_code;
124     ble_uuid_t    uart_uuid;
125     ble_uuid128_t nus_base_uuid = NUS_BASE_UUID;
126 
127     VERIFY_PARAM_NOT_NULL(p_ble_nus_c);
128     VERIFY_PARAM_NOT_NULL(p_ble_nus_c_init);
129 
130     err_code = sd_ble_uuid_vs_add(&nus_base_uuid, &p_ble_nus_c->uuid_type);
131     VERIFY_SUCCESS(err_code);
132 
133     uart_uuid.type = p_ble_nus_c->uuid_type;
134     uart_uuid.uuid = BLE_UUID_NUS_SERVICE;
135 
136     p_ble_nus_c->conn_handle           = BLE_CONN_HANDLE_INVALID;
137     p_ble_nus_c->evt_handler           = p_ble_nus_c_init->evt_handler;
138     p_ble_nus_c->handles.nus_tx_handle = BLE_GATT_HANDLE_INVALID;
139     p_ble_nus_c->handles.nus_rx_handle = BLE_GATT_HANDLE_INVALID;
140 
141     return ble_db_discovery_evt_register(&uart_uuid);
142 }

看完初始化函数,接下来我们还是一样的来看下ble_nus_c_on_ble_evt函数,这个是nus服务的部分是一样的,都是mian文件中注册nus_c实例的时候就初始化好的,用于接收底层返回的GAP和GATT消息事件。 这里面我们初始化了两个事件,一个是BLE_GATTC_EVT_HVX,用于接收notify数据的事件,另一个处理的是BLE_GAP_EVT_DISCONNECTED,这个事件处理的是,如果断开连接,我们需要将nus_c的状态改成断开状态。

144 void ble_nus_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
145 {
146     ble_nus_c_t * p_ble_nus_c = (ble_nus_c_t *)p_context;
147 
148     if ((p_ble_nus_c == NULL) || (p_ble_evt == NULL))
149     {
150         return;
151     }
152 
153     if ( (p_ble_nus_c->conn_handle != BLE_CONN_HANDLE_INVALID)
154        &&(p_ble_nus_c->conn_handle != p_ble_evt->evt.gap_evt.conn_handle)
155        )
156     {
157         return;
158     }
159 
160     switch (p_ble_evt->header.evt_id)
161     {
162         case BLE_GATTC_EVT_HVX:
163             on_hvx(p_ble_nus_c, p_ble_evt);
164             break;
165 
166         case BLE_GAP_EVT_DISCONNECTED:
167             if (p_ble_evt->evt.gap_evt.conn_handle == p_ble_nus_c->conn_handle
168                     && p_ble_nus_c->evt_handler != NULL)
169             {
170                 ble_nus_c_evt_t nus_c_evt;
171 
172                 nus_c_evt.evt_type = BLE_NUS_C_EVT_DISCONNECTED;
173 
174                 p_ble_nus_c->conn_handle = BLE_CONN_HANDLE_INVALID;
175                 p_ble_nus_c->evt_handler(p_ble_nus_c, &nus_c_evt);
176             }
177             break;
178 
179         default:
180             // No implementation needed.
181             break;
182     }
183 }

接下来我们顺带的看一下on_hvx函数,我们接收到底层的BLE_GATTC_EVT_HVX事件之后,调用这个函数进行处理。首先是获取p_ble_evt携带的数据内容和长度,并且将ble_nus_c_evt.evt_type的任务ID改成是BLE_NUS_C_EVT_NUS_TX_EVT,这样我们会向main文件回调我们的事件和数据。

103 static void on_hvx(ble_nus_c_t * p_ble_nus_c, ble_evt_t const * p_ble_evt)
104 {
105     // HVX can only occur from client sending.
106     if (   (p_ble_nus_c->handles.nus_tx_handle != BLE_GATT_HANDLE_INVALID)
107         && (p_ble_evt->evt.gattc_evt.params.hvx.handle == p_ble_nus_c->handles.nus_tx_handle)
108         && (p_ble_nus_c->evt_handler != NULL))
109     {
110         ble_nus_c_evt_t ble_nus_c_evt;
111 
112         ble_nus_c_evt.evt_type = BLE_NUS_C_EVT_NUS_TX_EVT;
113         ble_nus_c_evt.p_data   = (uint8_t *)p_ble_evt->evt.gattc_evt.params.hvx.data;
114         ble_nus_c_evt.data_len = p_ble_evt->evt.gattc_evt.params.hvx.len;
115 
116         p_ble_nus_c->evt_handler(p_ble_nus_c, &ble_nus_c_evt);
117         NRF_LOG_DEBUG("Client sending data.");
118     }
119 }

接下来的三个函数我们就放到一起说明了,功能比较好理解。

cccd_configure和ble_nus_c_tx_notif_enable函数,从名称上就不难看出,他们俩是用来配置我们的CCCD的,也就是使能NUS的notify功能。

ble_nus_c_string_send函数,这个是主机的write功能函数,就是调用这个函数向从机写数据。

185 /**@brief Function for creating a message for writing to the CCCD. */
186 static uint32_t cccd_configure(uint16_t conn_handle, uint16_t cccd_handle, bool enable)
187 {
188     uint8_t buf[BLE_CCCD_VALUE_LEN];
189 
190     buf[0] = enable ? BLE_GATT_HVX_NOTIFICATION : 0;
191     buf[1] = 0;
192 
193     ble_gattc_write_params_t const write_params =
194     {
195         .write_op = BLE_GATT_OP_WRITE_REQ,
196         .flags    = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
197         .handle   = cccd_handle,
198         .offset   = 0,
199         .len      = sizeof(buf),
200         .p_value  = buf
201     };
202 
203     return sd_ble_gattc_write(conn_handle, &write_params);
204 }
205 
206 
207 uint32_t ble_nus_c_tx_notif_enable(ble_nus_c_t * p_ble_nus_c)
208 {
209     VERIFY_PARAM_NOT_NULL(p_ble_nus_c);
210 
211     if ( (p_ble_nus_c->conn_handle == BLE_CONN_HANDLE_INVALID)
212        ||(p_ble_nus_c->handles.nus_tx_cccd_handle == BLE_GATT_HANDLE_INVALID)
213        )
214     {
215         return NRF_ERROR_INVALID_STATE;
216     }
217     return cccd_configure(p_ble_nus_c->conn_handle,p_ble_nus_c->handles.nus_tx_cccd_handle, true);
218 }
219 
220 
221 uint32_t ble_nus_c_string_send(ble_nus_c_t * p_ble_nus_c, uint8_t * p_string, uint16_t length)
222 {
223     VERIFY_PARAM_NOT_NULL(p_ble_nus_c);
224 
225     if (length > BLE_NUS_MAX_DATA_LEN)
226     {
227         NRF_LOG_WARNING("Content too long.");
228         return NRF_ERROR_INVALID_PARAM;
229     }
230     if (p_ble_nus_c->conn_handle == BLE_CONN_HANDLE_INVALID)
231     {
232         NRF_LOG_WARNING("Connection handle invalid.");
233         return NRF_ERROR_INVALID_STATE;
234     }
235 
236     ble_gattc_write_params_t const write_params =
237     {
238         .write_op = BLE_GATT_OP_WRITE_CMD,
239         .flags    = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
240         .handle   = p_ble_nus_c->handles.nus_rx_handle,
241         .offset   = 0,
242         .len      = length,
243         .p_value  = p_string
244     };
245 
246     return sd_ble_gattc_write(p_ble_nus_c->conn_handle, &write_params);
247 }

了解完第一部分,我们来看下第二部分的内容,也就是发现服务的部分。

当我们在main文件中完成一系列获取服务的流程之后,最终会进入下面这个函数处理服务,可以看到我们分别根据TX和RX这两个特征值的UUID,获取了对应的handle。

并且会向main文件中返回一个BLE_NUS_C_EVT_DISCOVERY_COMPLETE事件,代表的是服务获取完成。

55 void ble_nus_c_on_db_disc_evt(ble_nus_c_t * p_ble_nus_c, ble_db_discovery_evt_t * p_evt)
56 {
57     ble_nus_c_evt_t nus_c_evt;
58     memset(&nus_c_evt,0,sizeof(ble_nus_c_evt_t));
59 
60     ble_gatt_db_char_t * p_chars = p_evt->params.discovered_db.charateristics;
61 
62     // Check if the NUS was discovered.
63     if (    (p_evt->evt_type == BLE_DB_DISCOVERY_COMPLETE)
64         &&  (p_evt->params.discovered_db.srv_uuid.uuid == BLE_UUID_NUS_SERVICE)
65         &&  (p_evt->params.discovered_db.srv_uuid.type == p_ble_nus_c->uuid_type))
66     {
67         for (uint32_t i = 0; i < p_evt->params.discovered_db.char_count; i++)
68         {
69             switch (p_chars[i].characteristic.uuid.uuid)
70             {
71                 case BLE_UUID_NUS_RX_CHARACTERISTIC:
72                     nus_c_evt.handles.nus_rx_handle = p_chars[i].characteristic.handle_value;
73                     break;
74 
75                 case BLE_UUID_NUS_TX_CHARACTERISTIC:
76                     nus_c_evt.handles.nus_tx_handle = p_chars[i].characteristic.handle_value;
77                     nus_c_evt.handles.nus_tx_cccd_handle = p_chars[i].cccd_handle;
78                     break;
79 
80                 default:
81                     break;
82             }
83         }
84         if (p_ble_nus_c->evt_handler != NULL)
85         {
86             nus_c_evt.conn_handle = p_evt->conn_handle;
87             nus_c_evt.evt_type    = BLE_NUS_C_EVT_DISCOVERY_COMPLETE;
88             p_ble_nus_c->evt_handler(p_ble_nus_c, &nus_c_evt);
89         }
90     }
91 }

最后我们看下ble_nus_c_handles_assign函数,这个函数是用于给nus_c来分配句柄的。 他是在上面返回给mian函数的BLE_NUS_C_EVT_DISCOVERY_COMPLETE事件中进行处理,这个时候底层的handle句柄已经获取完成,这边是给我们定义的句柄结构体中的参数赋值。

250 uint32_t ble_nus_c_handles_assign(ble_nus_c_t               * p_ble_nus,
251                                   uint16_t                    conn_handle,
252                                   ble_nus_c_handles_t const * p_peer_handles)
253 {
254     VERIFY_PARAM_NOT_NULL(p_ble_nus);
255 
256     p_ble_nus->conn_handle = conn_handle;
257     if (p_peer_handles != NULL)
258     {
259         p_ble_nus->handles.nus_tx_cccd_handle = p_peer_handles->nus_tx_cccd_handle;
260         p_ble_nus->handles.nus_tx_handle      = p_peer_handles->nus_tx_handle;
261         p_ble_nus->handles.nus_rx_handle      = p_peer_handles->nus_rx_handle;
262     }
263     return NRF_SUCCESS;
264 }
5.4.2 main.c

mian文件中,主要也是有两个部分,一个是注册NUS_C的,一个是用于NUS服务发现的。我们从main函数开始看起。

mian函数中,我们分别初始化了数据库发现(db_discovery_init)以及nus_c(nus_c_init)。

542 //******************************************************************
543 // fn : main
544 //
545 // brief : 主函数
546 //
547 // param : none
548 //
549 // return : none
550 int main(void)
551 {
552     // 初始化
553     log_init();             // 初始化LOG打印,由RTT工作
554     timer_init();           // 初始化定时器
555     db_discovery_init();    // 初始化数据库发现
556     power_management_init();// 初始化电源控制
557     ble_stack_init();       // 初始化BLE栈堆
558     gatt_init();            // 初始化GATT
559     nus_c_init();           // 初始化NUS
560     scan_init();            // 初始化扫描
561     
562     // 打印例程名称
563     //NRF_LOG_INFO("demo0: simple central");
564     
565     // 初始化外设
566     UART_Init(APP_UartEvtHandle);   //初始化串口
567     UART_Write("3.4_ble_central_nus", sizeof("3.4_ble_central_nus"));
568     
569     
570     scan_start();           // 开始扫描
571     
572     // 进入主循环
573     for (;;)
574     {
575         idle_state_handle();   // 空闲状态处理
576     }
577 }

我们看一下NUS_C的初始化函数,以及NUS_C的回调函数,可以看到都是调用的我们刚刚说的ble_nus_c.c中的函数去处理的,这里也没有太多的东西需要重复描述,我们主要看一下回调函数中的两个事件处理的内容。

BLE_NUS_C_EVT_DISCOVERY_COMPLETE,这个是我们服务发现完成之后返回的事件,所以我们可以在这个事件中调用ble_nus_c_handles_assign去获取特征值的句柄,并且可以调用ble_nus_c_tx_notif_enable使能从机的notify属性。

BLE_NUS_C_EVT_NUS_TX_EVT事件中,我们接收从机notify的数据,并通过串口打印出来。

250 //******************************************************************
251 // fn : ble_nus_c_evt_handler
252 //
253 // brief : NUS事件
254 //
255 // param : none
256 //
257 // return : none
258 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)
259 {
260     ret_code_t err_code;
261 
262     switch (p_ble_nus_evt->evt_type)
263     {
264         case BLE_NUS_C_EVT_DISCOVERY_COMPLETE:
265             NRF_LOG_INFO("Discovery complete.");
266             err_code = ble_nus_c_handles_assign(p_ble_nus_c, p_ble_nus_evt->conn_handle, &p_ble_nus_evt->handles);
267             APP_ERROR_CHECK(err_code);
268 
269             err_code = ble_nus_c_tx_notif_enable(p_ble_nus_c);
270             APP_ERROR_CHECK(err_code);
271             NRF_LOG_INFO("Connected to device with Nordic UART Service.");
272             
273             app_timer_start(m_timer_nus, APP_TIMER_TICKS(1000), NULL);
274             break;
275 
276         case BLE_NUS_C_EVT_NUS_TX_EVT:
277             UART_Write((uint8_t*)p_ble_nus_evt->p_data, p_ble_nus_evt->data_len);
278             NRF_LOG_DEBUG("Receiving data.");
279             NRF_LOG_HEXDUMP_DEBUG(p_ble_nus_evt->p_data, p_ble_nus_evt->data_len);
280             break;
281 
282         case BLE_NUS_C_EVT_DISCONNECTED:
283             NRF_LOG_INFO("Disconnected.");
284             break;
285         default:
286             break;
287     }
288 }
289 
290 //******************************************************************
291 // fn : nus_c_init
292 //
293 // brief : 初始化NUS客户端(Nordic UART Service client)
294 //
295 // param : none
296 //
297 // return : none
298 static void nus_c_init(void)
299 {
300     ret_code_t       err_code;
301     ble_nus_c_init_t init;
302 
303     init.evt_handler = ble_nus_c_evt_handler;
304 
305     err_code = ble_nus_c_init(&m_ble_nus_c, &init);
306     APP_ERROR_CHECK(err_code);
307 }

接下来我们看一下初始化以及发起服务获取部分,首先是初始化函数,我们调用ble_db_discovery_init函数初始化了服务发现的功能,并且注册了一个回调函数db_disc_handler。这个回调函数中我们将对发现的服务进行功能判断,以及相应的参数赋值或者其他处理,处理调用的函数也是ble_nus_c.c中的ble_nus_c_on_db_disc_evt。

309 //******************************************************************
310 // fn : db_disc_handler
311 //
312 // brief : 用于处理数据库发现事件的函数
313 // details : 此函数是一个回调函数,用于处理来自数据库发现模块的事件。
314 //           根据发现的UUID,此功能将事件转发到各自的服务。
315 // 
316 // param : p_event -> 指向数据库发现事件的指针
317 //
318 // return : none
319 static void db_disc_handler(ble_db_discovery_evt_t * p_evt)
320 {
321     ble_nus_c_on_db_disc_evt(&m_ble_nus_c, p_evt);
322 }
323 
324 //******************************************************************
325 // fn : db_discovery_init
326 //
327 // brief : 用于初始化数据库发现模块的函数
328 //
329 // param : none
330 //
331 // return : none
332 static void db_discovery_init(void)
333 {
334     ret_code_t err_code = ble_db_discovery_init(db_disc_handler);
335     APP_ERROR_CHECK(err_code);
336 }

上面的发现服务初始化找到了,最终返回的消息事件也知道怎么处理了,那么我们是在哪边去开始服务发现这个过程的呢。 我们可以看到在ble协议栈任务回调事件中,当我们连接成功之后(BLE_GAP_EVT_CONNECTED事件返回),我们调用了ble_db_discovery_start函数去发现服务。

338 //******************************************************************
339 // fn : ble_evt_handler
340 //
341 // brief : BLE事件回调
342 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
343 //
344 // param : ble_evt_t  事件类型
345 //         p_context  未使用
346 //
347 // return : none
348 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
349 {
350     ret_code_t            err_code;
351     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
352     ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
353     
354     switch (p_ble_evt->header.evt_id)
355     {
356         // 连接
357         case BLE_GAP_EVT_CONNECTED:
358             UART_Write("Connected", sizeof("Connected"));
359           
360             NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
361                          Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
362                          p_gap_evt->conn_handle,
363                          p_connected_evt->conn_params.min_conn_interval,
364                          p_connected_evt->conn_params.max_conn_interval,
365                          p_connected_evt->conn_params.slave_latency,
366                          p_connected_evt->conn_params.conn_sup_timeout
367                          );
368             m_conn_handle = p_gap_evt->conn_handle;
369 
370             // 开始发现服务,NUS客户端等待发现结果
371             err_code = ble_db_discovery_start(&m_db_disc, p_ble_evt->evt.gap_evt.conn_handle);
372             APP_ERROR_CHECK(err_code);
373             
374             break;
375 
376         // 断开连接
377         case BLE_GAP_EVT_DISCONNECTED:
378             UART_Write("Disconnected", sizeof("Disconnected"));
379           
380             NRF_LOG_INFO("Disconnected. conn_handle: 0x%x, reason: 0x%04x",
381                          p_gap_evt->conn_handle,
382                          p_gap_evt->params.disconnected.reason);
383             m_conn_handle = BLE_CONN_HANDLE_INVALID;
384             
385             // 如果需要异常断开重连,可以打开下面的注释
386             // scan_start();  // 重新开始扫描
387             break;
388 
389         // 连接参数更新请求
390         case BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
391             NRF_LOG_INFO("conn_Param Update REQUEST: %d,%d,%d,%d",
392                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.min_conn_interval,
393                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.max_conn_interval,
394                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.slave_latency,
395                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.conn_sup_timeout
396                          );
397             break;
398         
399         case BLE_GAP_EVT_CONN_PARAM_UPDATE:
400           {
401             NRF_LOG_INFO("conn_Param Update Success: %d,%d,%d,%d",
402                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.min_conn_interval,
403                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.max_conn_interval,
404                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.slave_latency,
405                          p_ble_evt->evt.gap_evt.params.conn_param_update_request.conn_params.conn_sup_timeout
406                          );
407           }break;
408           
409         default:
410             break;
411     }
412 }

6 一从多主实验

6.1 实验简介

一主多从以及一从多主,这个在大家的实际场景使用中,经常会要用到,所以我们将会在接下来的两个例程依次给大家做介绍。

首先是给大家带来的我们本实验的一从多主,顾名思义就是一个从机设备支持被多个主机设备同时连接。那么我们建立这样一对多的连接方式,代码的处理过程会变得非常麻烦吗,实际上并不是这样,对于我们开发者而言,如果大家对于连接句柄(conn_handle)的理解已经比较透彻,那么增加的难度真的很低。

6.2 硬件说明

串口一向是所有开发板必需的功能之一,nRF52DK开发板采用CH340C芯片,将芯片的UART信号转换为USB接口信号,方便电脑使用USB来虚拟一个串口调试。

CH340C外围电路简单,且不需要外部晶振,nRF52DK开发板采用与Nordic官方开发板相同的串口收发引脚,如下表格所示。

网络标号 芯片引脚号 连接方式
CH340_TX P0.08(nRF_RX信号) 通过SW1拨动开关,选择连接
CH340_RX P0.06(nRF_TX信号) 通过SW1拨动开关,选择连接
 

图中P5引出了CH340C的流控制引脚,但是没有连接任何线路,所需要使用流控制,可自行连接测试。

6.3 实验现象

6.4 源码讲解

6.4.1 mian.c

mian文件中,我们首先看一下mian函数,可以看到并没有因为要建立多个链接而新增什么功能的初始化,这就说明我们只需要在原有的功能基础上去修改一些东西。那么接下来我们带大家一起看下我们修改了哪些地方。

第一点,ble协议栈初始化,咋一看好像没有什么改动,实际上在我们的nrf_sdh_ble_default_cfg_set初始化函数中,我们已经配置了本从机最大支持被4个主机连接,这个配置是的sdk_conifg.h中去修改了宏定义。

11965 // <o> NRF_SDH_BLE_PERIPHERAL_LINK_COUNT - Maximum number of peripheral links. 
11966 #ifndef NRF_SDH_BLE_PERIPHERAL_LINK_COUNT
11967 #define NRF_SDH_BLE_PERIPHERAL_LINK_COUNT 4
11968 #endif
11969 
11970 // <o> NRF_SDH_BLE_CENTRAL_LINK_COUNT - Maximum number of central links. 
11971 #ifndef NRF_SDH_BLE_CENTRAL_LINK_COUNT
11972 #define NRF_SDH_BLE_CENTRAL_LINK_COUNT 0
11973 #endif
11974 
11975 // <o> NRF_SDH_BLE_TOTAL_LINK_COUNT - Total link count. 
11976 // <i> Maximum number of total concurrent connections using the default configuration.
11977 
11978 #ifndef NRF_SDH_BLE_TOTAL_LINK_COUNT
11979 #define NRF_SDH_BLE_TOTAL_LINK_COUNT 4
11980 #endif
501 //******************************************************************
502 // fn : ble_stack_init
503 //
504 // brief : 用于初始化BLE协议栈
505 // details : 初始化SoftDevice、BLE事件中断
506 //
507 // param : none
508 //
509 // return : none
510 static void ble_stack_init(void)
511 {
512     ret_code_t err_code;
513 
514     // SD使能请求,配置时钟,配置错误回调,中断(中断优先级栈堆默认设置)
515     err_code = nrf_sdh_enable_request();
516     APP_ERROR_CHECK(err_code);
517 
518     // SD默认配置
519     uint32_t ram_start = 0;
520     err_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
521     APP_ERROR_CHECK(err_code);
522 
523     // 使能BLE栈堆
524     err_code = nrf_sdh_ble_enable(&ram_start);
525     APP_ERROR_CHECK(err_code);
526 
527     // 注册BLE事件的处理程序,所有BLE的事件都将分派ble_evt_handler回调
528     NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
529 }

初始化中我们已经定义好了最大支持被4个主机连接,接下来就轮到了GAP状态处理的部分,我们看一下BLE协议栈的回调函数,可以看到里面还是只处理了连接(BLE_GAP_EVT_CONNECTED)和断开(BLE_GAP_EVT_DISCONNECTED)这两个状态。

473 //******************************************************************
474 // fn : ble_evt_handler
475 //
476 // brief : BLE事件回调
477 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
478 //
479 // param : ble_evt_t  事件类型
480 //         p_context  未使用
481 //
482 // return : none
483 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
484 {
485     switch (p_ble_evt->header.evt_id)
486     {
487         case BLE_GAP_EVT_CONNECTED:
488             on_connected(&p_ble_evt->evt.gap_evt);
489             break;
490 
491         case BLE_GAP_EVT_DISCONNECTED:
492             on_disconnected(&p_ble_evt->evt.gap_evt);
493             break;
494 
495         default:
496             // No implementation needed.
497             break;
498     }
499 }

接下来我们分别来看一下连接成功的处理函数on_connected以及断开的处理函数on_disconnected.

首先看一下连接的处理函数,相对于以前只支持一个连接的例程,我们这边新增了一个nrf_ble_qwr_conn_handle_assign函数处理,这个函数的功能是用于将连接句柄分配给Queued Writes模块的给定实例。

并且我们需要对当前连接的总数量进行判断,如果以及连接的主机数量少于4个,则继续开启广播,等待其他主机继续发起连接;如果连接的主机数量达到4个,则不再开启广播。

407 //******************************************************************
408 // fn : on_connected
409 //
410 // brief : 用于处理Connected事件的函数。
411 //
412 // param : p_gap_evt -> 从BLE堆栈收到的GAP事件
413 //
414 // return : none
415 static void on_connected(const ble_gap_evt_t * const p_gap_evt)
416 {
417     ret_code_t  err_code;
418     uint32_t    periph_link_cnt = ble_conn_state_peripheral_conn_count(); // Number of peripheral links.
419 
420     UART_Printf("Connection with link 0x%x established.\n", p_gap_evt->conn_handle);
421 
422     // Assign connection handle to available instance of QWR module.
423     for (uint32_t i = 0; i < NRF_SDH_BLE_PERIPHERAL_LINK_COUNT; i++)
424     {
425         if (m_qwr[i].conn_handle == BLE_CONN_HANDLE_INVALID)
426         {
427             err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr[i], p_gap_evt->conn_handle);
428             APP_ERROR_CHECK(err_code);
429             break;
430         }
431     }
432 
433     if (periph_link_cnt == NRF_SDH_BLE_PERIPHERAL_LINK_COUNT)
434     {
435 
436     }
437     else
438     {
439         // Continue advertising. More connections can be established because the maximum link count has not been reached.
440         advertising_start();
441     }
442 }

然后我们再看一下断开连接的处理函数,先判断下是否有设备连接(periph_link_cnt == 0),如果没有设备连接,则返回err。然后我们再判断一下,当前连接数是否是最大连接数量减1(periph_link_cnt == (NRF_SDH_BLE_PERIPHERAL_LINK_COUNT - 1)),也就是说刚刚从满数量连接少了一个,这个时候我要需要重新开启广播,等待连接。

444 //******************************************************************
445 // fn : on_disconnected
446 //
447 // brief : 用于处理Disconnected事件的函数。
448 //
449 // param : p_gap_evt -> 从BLE堆栈收到的GAP事件
450 //
451 // return : none
452 static void on_disconnected(ble_gap_evt_t const * const p_gap_evt)
453 {
454     ret_code_t  err_code;
455     uint32_t    periph_link_cnt = ble_conn_state_peripheral_conn_count(); // Number of peripheral links.
456 
457     UART_Printf("Connection 0x%x has been disconnected. Reason: 0x%X\n",
458                  p_gap_evt->conn_handle,
459                  p_gap_evt->params.disconnected.reason);
460 
461     if (periph_link_cnt == 0)
462     {
463         APP_ERROR_CHECK(err_code);
464     }
465 
466     if (periph_link_cnt == (NRF_SDH_BLE_PERIPHERAL_LINK_COUNT - 1))
467     {
468         // Advertising is not running when all connections are taken, and must therefore be started.
469         advertising_start();
470     }
471 }

讲完了一从多主连接相关的改变,我们接下来来说明一下多主连接的情况下,服务的处理。 首先看下服务初始化函数,这里我们主要是Queue Write初始化时候,需要根据最大连接数量初始化多个m_qwr实例。NUS的初始化部分是不需要改变的。

314 //******************************************************************
315 // fn : services_init
316 //
317 // brief : 初始化复位(本例程展示NUS:Nordic Uart Service)
318 //
319 // param : none
320 //
321 // return : none
322 static void services_init(void)
323 {
324     ret_code_t         err_code;
325     ble_nus_init_t     nus_init;
326     nrf_ble_qwr_init_t qwr_init = {0};
327 
328     // Initialize Queued Write Module instances.
329     qwr_init.error_handler = nrf_qwr_error_handler;
330 
331     for (uint32_t i = 0; i < LINK_TOTAL; i++)
332     {
333         err_code = nrf_ble_qwr_init(&m_qwr[i], &qwr_init);
334         APP_ERROR_CHECK(err_code);
335     }
336 
337     // Initialize NUS.
338     memset(&nus_init, 0, sizeof(nus_init));
339 
340     nus_init.data_handler = nus_data_handler;
341 
342     err_code = ble_nus_init(&m_nus, &nus_init);
343     APP_ERROR_CHECK(err_code);
344 
345     ble_conn_state_init();
346 }

其他就只剩下数据收发部分,我们得考虑有多个连接的设备。从上面一路看下来,我们不难看出,所谓的多主连接,实际上只是对conn_handle的处理(我们宏定义需要修改最大连接数量)。

281 //******************************************************************
282 // fn : nus_data_handler
283 //
284 // brief : 用于处理来自Nordic UART服务的数据的功能
285 // details : 该功能将处理从Nordic UART BLE服务接收的数据并将其发送到UART模块
286 //
287 // param : ble_nus_evt_t -> nus事件
288 //
289 // return : none
290 static void nus_data_handler(ble_nus_evt_t * p_evt)
291 {
292     if (p_evt->type == BLE_NUS_EVT_RX_DATA)
293     {
294         UART_Printf("Recive on connection handle 0x%04x.\n", p_evt->conn_handle);
295         UART_Write((uint8_t*)p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
296         
297         NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
298         NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
299     }
300     else if (p_evt->type == BLE_NUS_EVT_TX_RDY)
301     {
302         NRF_LOG_DEBUG(" Service is ready to accept new data to be transmitted..");
303     }
304     else if(p_evt->type == BLE_NUS_EVT_COMM_STARTED)
305     {
306        NRF_LOG_DEBUG("NUS Notification Enable.");
307     }
308     else if (p_evt->type == BLE_NUS_EVT_COMM_STOPPED)
309     {
310         NRF_LOG_DEBUG("NUS Notification Disable.");
311     }
312 }
580 // 串口回调函数
581 void APP_UartEvtHandle(uart_evt_t* pEvt)
582 {
583     uint16_t len = 0;
584     ble_conn_state_conn_handle_list_t conn_handles = ble_conn_state_periph_handles();
585     
586     switch(pEvt->evt_type)
587     {
588       case UART_EVT_RX_TIMEOUT:
589       case UART_EVT_RX_OVERFLOW:
590         len = UART_Read(Buf,pEvt->status);
591         if(len > BLE_NUS_MAX_DATA_LEN) break;
592         for (uint8_t i = 0; i < conn_handles.len; i++)
593         {
594           UART_Printf("Sent on connection handle 0x%04x.\n", conn_handles.conn_handles[i]);
595           ble_nus_data_send(&m_nus, Buf, &len, conn_handles.conn_handles[i]); 
596         }
597       break;
598     }
599 }

7 一主多从实验

7.1 实验简介

介绍完一从多主的实验,我们来看下一主多从的,从上一个实验的源码讲解中我们不难看出,所谓的多路连接,主要处理的就是连接的句柄。

而在我们的一主都从实验中,大家需要多注意下服务句柄的分配部分,因为涉及到多个从机的服务,所以我们对于句柄的分配,则相关内容要多一些。

7.2 硬件说明

串口一向是所有开发板必需的功能之一,nRF52DK开发板采用CH340C芯片,将芯片的UART信号转换为USB接口信号,方便电脑使用USB来虚拟一个串口调试。

CH340C外围电路简单,且不需要外部晶振,nRF52DK开发板采用与Nordic官方开发板相同的串口收发引脚,如下表格所示。

网络标号 芯片引脚号 连接方式
CH340_TX P0.08(nRF_RX信号) 通过SW1拨动开关,选择连接
CH340_RX P0.06(nRF_TX信号) 通过SW1拨动开关,选择连接
 

图中P5引出了CH340C的流控制引脚,但是没有连接任何线路,所需要使用流控制,可自行连接测试。

7.3 实验现象

7.4 源码讲解

7.4.1 mian.c

首先还是一样的,我们先看下ble协议栈初始化的部分,我们定义了可以连接的从机最大数量为8。

12065 // <o> NRF_SDH_BLE_PERIPHERAL_LINK_COUNT - Maximum number of peripheral links. 
12066 #ifndef NRF_SDH_BLE_PERIPHERAL_LINK_COUNT
12067 #define NRF_SDH_BLE_PERIPHERAL_LINK_COUNT 0
12068 #endif
12069 
12070 // <o> NRF_SDH_BLE_CENTRAL_LINK_COUNT - Maximum number of central links. 
12071 #ifndef NRF_SDH_BLE_CENTRAL_LINK_COUNT
12072 #define NRF_SDH_BLE_CENTRAL_LINK_COUNT 8
12073 #endif
12074 
12075 // <o> NRF_SDH_BLE_TOTAL_LINK_COUNT - Total link count. 
12076 // <i> Maximum number of total concurrent connections using the default configuration.
12077 
12078 #ifndef NRF_SDH_BLE_TOTAL_LINK_COUNT
12079 #define NRF_SDH_BLE_TOTAL_LINK_COUNT 8
12080 #endif
314 //******************************************************************
315 // fn : ble_evt_handler
316 //
317 // brief : BLE事件回调
318 // details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
319 //
320 // param : ble_evt_t  事件类型
321 //         p_context  未使用
322 //
323 // return : none
324 static void ble_stack_init(void)
325 {
326     ret_code_t err_code;
327 
328     err_code = nrf_sdh_enable_request();
329     APP_ERROR_CHECK(err_code);
330 
331     // Configure the BLE stack using the default settings.
332     // Fetch the start address of the application RAM.
333     uint32_t ram_start = 0;
334     err_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
335     APP_ERROR_CHECK(err_code);
336 
337     // Enable BLE stack.
338     err_code = nrf_sdh_ble_enable(&ram_start);
339     APP_ERROR_CHECK(err_code);
340 
341     // Register a handler for BLE events.
342     NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
343 }

接下来我们通用查看一下ble协议栈的回调函数,看下其中对于连接和断开等GAP状态的处理。

BLE_GAP_EVT_CONNECTED连接状态,我们首先调用ble_nus_c_handles_assign函数去分配句柄,但实际上由于第三个参数为NULL,这里我们等于是给nus服务句柄置0。接下来我们调用ble_db_discovery_start函数去发现服务(由于有多个连接,所以我们需要根据当前连接的设备句柄参数去发现服务),成功发现NUS服务之后,会给我们在ble_nus_c_evt_handler回调函数中返回BLE_NUS_C_EVT_DISCOVERY_COMPLETE事件。

BLE_GAP_EVT_DISCONNECTED断开状态,先判断了是否有设备连接,如果没有就直接返回err,并且在最后重新开启扫描。

207 //******************************************************************
208 // fn : db_disc_handler
209 //
210 // brief : 用于处理数据库发现事件的函数
211 // details : 此函数是一个回调函数,用于处理来自数据库发现模块的事件。
212 //           根据发现的UUID,此功能将事件转发到各自的服务。
213 // 
214 // param : p_event -> 指向数据库发现事件的指针
215 //
216 // return : none
217 static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
218 {
219     ret_code_t err_code;
220 
221     // For readability.
222     ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
223 
224     switch (p_ble_evt->header.evt_id)
225     {
226         // Upon connection, check which peripheral is connected, initiate DB
227         // discovery, update LEDs status, and resume scanning, if necessary.
228         case BLE_GAP_EVT_CONNECTED:
229         {
230             UART_Printf("Connection 0x%x established, starting DB discovery.",
231                          p_gap_evt->conn_handle);
232 
233             APP_ERROR_CHECK_BOOL(p_gap_evt->conn_handle < NRF_SDH_BLE_CENTRAL_LINK_COUNT);
234 
235             err_code = ble_nus_c_handles_assign(&m_ble_nus_c[p_gap_evt->conn_handle],
236                                                 p_gap_evt->conn_handle,
237                                                 NULL);
238             APP_ERROR_CHECK(err_code);
239 
240             err_code = ble_db_discovery_start(&m_db_disc[p_gap_evt->conn_handle],
241                                               p_gap_evt->conn_handle);
242             if (err_code != NRF_ERROR_BUSY)
243             {
244                 APP_ERROR_CHECK(err_code);
245             }
246 
247             // Update LEDs status and check whether it is needed to look for more
248             // peripherals to connect to.
249             if (ble_conn_state_central_conn_count() == NRF_SDH_BLE_CENTRAL_LINK_COUNT)
250             {
251                 
252             }
253             else
254             {
255                 // Resume scanning.
256                 scan_start();
257             }
258         } break; // BLE_GAP_EVT_CONNECTED
259 
260         // Upon disconnection, reset the connection handle of the peer that disconnected, update
261         // the LEDs status and start scanning again.
262         case BLE_GAP_EVT_DISCONNECTED:
263         {
264             UART_Printf("LBS central link 0x%x disconnected (reason: 0x%x)",
265                          p_gap_evt->conn_handle,
266                          p_gap_evt->params.disconnected.reason);
267 
268             if (ble_conn_state_central_conn_count() == 0)
269             {
270                 APP_ERROR_CHECK(err_code);
271             }
272 
273             // Start scanning.
274             scan_start();
275         } break;
276 
277         case BLE_GAP_EVT_TIMEOUT:
278         {
279             // Timeout for scanning is not specified, so only the connection requests can time out.
280             if (p_gap_evt->params.timeout.src == BLE_GAP_TIMEOUT_SRC_CONN)
281             {
282                 NRF_LOG_DEBUG("Connection request timed out.");
283             }
284         } break;
285 
286         default:
287             // No implementation needed.
288             break;
289     }
290 }

然后我们看下nus_c的初始化,不同于从机设备的多路连接(因为从机自己提供服务,服务是唯一的),主机连接的多个从机有可能存在服务的不同,所以我们需要初始化多个m_ble_nus_c的实例。 同样的在nus_c_init的初始化函数中,我们需要对这些m_ble_nus_c的实例都进行初始化操作。

51 BLE_NUS_C_ARRAY_DEF(m_ble_nus_c, NRF_SDH_BLE_CENTRAL_LINK_COUNT);
286 //******************************************************************
287 // fn : nus_c_init
288 //
289 // brief : 初始化NUS客户端(Nordic UART Service client)
290 //
291 // param : none
292 //
293 // return : none
294 static void nus_c_init(void)
295 {
296     ret_code_t       err_code;
297     ble_nus_c_init_t init;
298 
299     init.evt_handler = ble_nus_c_evt_handler;
300 
301     for (uint32_t i = 0; i < NRF_SDH_BLE_CENTRAL_LINK_COUNT; i++)
302     {
303         err_code = ble_nus_c_init(&m_ble_nus_c[i], &init);
304         APP_ERROR_CHECK(err_code);
305     }
306 }

看完nus_c的初始化函数,我们来看下他的回调函数的事件处理。

BLE_NUS_C_EVT_DISCOVERY_COMPLETE事件是服务发现完成后返回,我们可以在这个事件中去调用ble_nus_c_handles_assign给对应的连接的从设备的服务分配句柄,并且去使能他的notify。

BLE_NUS_C_EVT_NUS_TX_EVT事件是接收到从机的notify数据时返回,我们可以从这里获取到从设备的数据,并且打印出来显示。

168 //******************************************************************
169 // fn : ble_nus_c_evt_handler
170 //
171 // brief : NUS事件
172 //
173 // param : none
174 //
175 // return : none
176 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)
177 {
178     ret_code_t err_code;
179 
180     switch (p_ble_nus_evt->evt_type)
181     {
182         case BLE_NUS_C_EVT_DISCOVERY_COMPLETE:
183             NRF_LOG_INFO("Discovery complete.");
184             err_code = ble_nus_c_handles_assign(p_ble_nus_c, p_ble_nus_evt->conn_handle, &p_ble_nus_evt->handles);
185             APP_ERROR_CHECK(err_code);
186 
187             err_code = ble_nus_c_tx_notif_enable(p_ble_nus_c);
188             APP_ERROR_CHECK(err_code);
189             NRF_LOG_INFO("Connected to device with Nordic UART Service.");
190             break;
191 
192         case BLE_NUS_C_EVT_NUS_TX_EVT:
193             UART_Write((uint8_t*)p_ble_nus_evt->p_data, p_ble_nus_evt->data_len);
194             NRF_LOG_DEBUG("Receiving data.");
195             NRF_LOG_HEXDUMP_DEBUG(p_ble_nus_evt->p_data, p_ble_nus_evt->data_len);
196             break;
197 
198         case BLE_NUS_C_EVT_DISCONNECTED:
199             NRF_LOG_INFO("Disconnected.");
200             break;
201         default:
202             break;
203     }
204 }

最后是串口回调函数,我们需要在这边处理接收到的串口数据,利用ble_nus_c_string_send函数发送给与我们主机连接的从机设备,由于是多路连接,所以我们需要一个循环来向多个m_ble_nus_c发送数据(就是像多个从机设备发送)。

447 // 串口回调函数
448 void APP_UartEvtHandle(uart_evt_t* pEvt)
449 {
450     uint16_t len = 0;
451 
452     switch(pEvt->evt_type)
453     {
454       case UART_EVT_RX_TIMEOUT:
455       case UART_EVT_RX_OVERFLOW:
456         len = UART_Read(Buf,pEvt->status);
457         for (uint8_t i = 0; i < NRF_SDH_BLE_CENTRAL_LINK_COUNT; i++)
458         {
459           //UART_Printf("Sent on connection handle 0x%04x.\n",0);
460           ble_nus_c_string_send(&m_ble_nus_c[i], Buf, len); 
461         }
462       break;
463     }
464 }

8 主从一体实验

9 自拍实验

9.1 实验简介

HID(human Interface device),即人机交互设备,常见的有鼠标,键盘,游戏手柄,等等。一般有线方式都是通过USB连线连接到机器设备,作为用户输入设备。在蓝牙技术中,HID设备的接入就是无线的了。但 BLE HID 规范是以 USB HID 规范为基础的 ,不然设备驱动,兼容等问题都会有很多麻烦了。蓝牙中有HID, HOGP profile,但是只是在蓝牙数据通信上做的规范,HID具体含义相关,还是需要看USB相关的HID文档。提供的HID示例均符合USB HID使用表:http://www.usb.org/developers/hidpage

注意:原本的HID协议,鼠标作为输入端设备,PC电脑作为接入端。而在BLE上,蓝牙开发板是作为输入端,手机是作为接入端,这样的方式,就导致蓝牙开发板发送数据给手机都是作为input数据。

9.2 硬件说明

nRF52DK开发板采用与Nordic官方开发板相同的按键电路,原则协议栈例程可以直接使用按键,无需修改代码。

按键与芯片引脚对应关系如下表格。

网络标号 芯片引脚号 连接方式
BTN1 P0.13 直连,下降沿(低电平)触发
BTN2 P0.14 直连,下降沿(低电平)触发
BTN3 P0.15 直连,下降沿(低电平)触发
BTN4 P0.16 直连,下降沿(低电平)触发
 

9.3 实验现象

9.4 源码讲解

源码讲解部分,我们主要围绕HID服务部分的处理来介绍。分别是hid服务的初始化,mouse控制数据的说明,以及hid服务发送数据的方式。

9.4.1 mian.c

首先我们找到工程的服务初始化函数,在里面包含了我们的qwr初始化,以及我们的hid服务的初始化。

593 //******************************************************************
594 // fn : services_init
595 //
596 // brief : 由应用层调用的初始化服务的函数
597 //
598 // param : none
599 //
600 // return : none
601 static void services_init(void)
602 {
603     qwr_init();
604     hids_init();
605 }

然后我们追踪到我们的hid初始化函数,可以看到里面的代码处理可以分为3个部分。

第一部分:rep_map_data数组,这个数组中定义了3个报告(report)

Report ID 1:描述的是鼠标按键以及滚轮的控制

Report ID 2:描述的是鼠标(指针)的控制

Report ID 3:描述的是音乐播放按键的控制,包含了音量的加减、上一首、下一首的切换,以及暂停/恢复等。

第二部分:p_input_report起头的代码,分别对上面的3个报告进行了初始化,这边都是配置的简易加密(just work),也就是配对的时候不需要加密。并且均配置为BLE_HIDS_REP_TYPE_INPUT,也就是由开发板发送数据给手机。

第三部分:最后是ble_hids_init_t起头的代码,也就是对hid服务的结构体进行初始化,由于我们是要用鼠标按键的功能,所以配置mouse为true,keyfob为flase,同样的也都是简单加密。

由于我们这个例程是自拍功能(也就是控制手机的音量加减按键),所以我们仅针对Report ID 3进行讲解。具体的HID协议这边我们就不给大家说明了,可以查看实验简介中提供的USB HID链接说明。在report3中,我们用户使用一个字节的数据进行音乐播放的控制,这个字节的8个bit位控制的内容分别如下:

bit 功能
0(0x01) Play/Pause 开始/暂停(音乐播放控制)
1(0x02) AL Consumer Control Configuration 用于将通用控件与特定的消费类设备或软件播放器相关联,以接收控件输入,而不考虑用户的关注点。 例如,一组传输控件可能与DVD-ROM播放器相关联,即使没有用户关注,该DVD-ROM播放器也将接收静音输入(没有深入研究,不清楚具体含义)
2(0x04) Scan Next Track 下一首(音乐播放控制)
3(0x08) Scan Previous Track 上一首(音乐播放控制)
4(0x10) Volume Down 音量减
5(0x20) Volume Up 音量加
6(0x40) AC Forward 加载上一个文档(功能测试不可用)
7(0x80) AC Back 载入下一个文件(切换手机应用,功能可用)
432 //******************************************************************
433 // fn : hids_init
434 //
435 // brief : 初始化HID服务
436 //
437 // param : none
438 //
439 // return : none
440 static void hids_init(void)
441 {
442     ret_code_t                err_code;
443     ble_hids_init_t           hids_init_obj;
444     ble_hids_inp_rep_init_t * p_input_report;
445     uint8_t                   hid_info_flags;
446 
447     static ble_hids_inp_rep_init_t inp_rep_array[INPUT_REPORT_COUNT];
448     static uint8_t rep_map_data[] =
449     {
450         0x05, 0x01, // Usage Page (Generic Desktop)
451         0x09, 0x02, // Usage (Mouse)
452 
453         0xA1, 0x01, // Collection (Application)
454 
455         // Report ID 1: Mouse buttons + scroll/pan
456         0x85, 0x01,       // Report Id 1
457         0x09, 0x01,       // Usage (Pointer)
458         0xA1, 0x00,       // Collection (Physical)
459         0x95, 0x05,       // Report Count (3)
460         0x75, 0x01,       // Report Size (1)
461         0x05, 0x09,       // Usage Page (Buttons)
462         0x19, 0x01,       // Usage Minimum (01)
463         0x29, 0x05,       // Usage Maximum (05)
464         0x15, 0x00,       // Logical Minimum (0)
465         0x25, 0x01,       // Logical Maximum (1)
466         0x81, 0x02,       // Input (Data, Variable, Absolute)
467         0x95, 0x01,       // Report Count (1)
468         0x75, 0x03,       // Report Size (3)
469         0x81, 0x01,       // Input (Constant) for padding
470         0x75, 0x08,       // Report Size (8)
471         0x95, 0x01,       // Report Count (1)
472         0x05, 0x01,       // Usage Page (Generic Desktop)
473         0x09, 0x38,       // Usage (Wheel)
474         0x15, 0x81,       // Logical Minimum (-127)
475         0x25, 0x7F,       // Logical Maximum (127)
476         0x81, 0x06,       // Input (Data, Variable, Relative)
477         0x05, 0x0C,       // Usage Page (Consumer)
478         0x0A, 0x38, 0x02, // Usage (AC Pan)
479         0x95, 0x01,       // Report Count (1)
480         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
481         0xC0,             // End Collection (Physical)
482 
483         // Report ID 2: Mouse motion
484         0x85, 0x02,       // Report Id 2
485         0x09, 0x01,       // Usage (Pointer)
486         0xA1, 0x00,       // Collection (Physical)
487         0x75, 0x0C,       // Report Size (12)
488         0x95, 0x02,       // Report Count (2)
489         0x05, 0x01,       // Usage Page (Generic Desktop)
490         0x09, 0x30,       // Usage (X)
491         0x09, 0x31,       // Usage (Y)
492         0x16, 0x01, 0xF8, // Logical maximum (2047)
493         0x26, 0xFF, 0x07, // Logical minimum (-2047)
494         0x81, 0x06,       // Input (Data, Variable, Relative)
495         0xC0,             // End Collection (Physical)
496         0xC0,             // End Collection (Application)
497 
498         // Report ID 3: Advanced buttons
499         0x05, 0x0C,       // Usage Page (Consumer)
500         0x09, 0x01,       // Usage (Consumer Control)
501         0xA1, 0x01,       // Collection (Application)
502         0x85, 0x03,       // Report Id (3)
503         0x15, 0x00,       // Logical minimum (0)
504         0x25, 0x01,       // Logical maximum (1)
505         0x75, 0x01,       // Report Size (1)
506         0x95, 0x01,       // Report Count (1)
507 
508         0x09, 0xCD,       // Usage (Play/Pause)
509         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
510         0x0A, 0x83, 0x01, // Usage (AL Consumer Control Configuration)
511         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
512         0x09, 0xB5,       // Usage (Scan Next Track)
513         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
514         0x09, 0xB6,       // Usage (Scan Previous Track)
515         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
516 
517         0x09, 0xEA,       // Usage (Volume Down)
518         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
519         0x09, 0xE9,       // Usage (Volume Up)
520         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
521         0x0A, 0x25, 0x02, // Usage (AC Forward)
522         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
523         0x0A, 0x24, 0x02, // Usage (AC Back)
524         0x81, 0x06,       // Input (Data,Value,Relative,Bit Field)
525         0xC0              // End Collection
526     };
527 
528     memset(inp_rep_array, 0, sizeof(inp_rep_array));
529     // Initialize HID Service.
530     p_input_report                      = &inp_rep_array[INPUT_REP_BUTTONS_INDEX];
531     p_input_report->max_len             = INPUT_REP_BUTTONS_LEN;
532     p_input_report->rep_ref.report_id   = INPUT_REP_REF_BUTTONS_ID;
533     p_input_report->rep_ref.report_type = BLE_HIDS_REP_TYPE_INPUT;
534 
535     p_input_report->sec.cccd_wr = SEC_JUST_WORKS;
536     p_input_report->sec.wr      = SEC_JUST_WORKS;
537     p_input_report->sec.rd      = SEC_JUST_WORKS;
538 
539     p_input_report                      = &inp_rep_array[INPUT_REP_MOVEMENT_INDEX];
540     p_input_report->max_len             = INPUT_REP_MOVEMENT_LEN;
541     p_input_report->rep_ref.report_id   = INPUT_REP_REF_MOVEMENT_ID;
542     p_input_report->rep_ref.report_type = BLE_HIDS_REP_TYPE_INPUT;
543 
544     p_input_report->sec.cccd_wr = SEC_JUST_WORKS;
545     p_input_report->sec.wr      = SEC_JUST_WORKS;
546     p_input_report->sec.rd      = SEC_JUST_WORKS;
547 
548     p_input_report                      = &inp_rep_array[INPUT_REP_MPLAYER_INDEX];
549     p_input_report->max_len             = INPUT_REP_MEDIA_PLAYER_LEN;
550     p_input_report->rep_ref.report_id   = INPUT_REP_REF_MPLAYER_ID;
551     p_input_report->rep_ref.report_type = BLE_HIDS_REP_TYPE_INPUT;
552 
553     p_input_report->sec.cccd_wr = SEC_JUST_WORKS;
554     p_input_report->sec.wr      = SEC_JUST_WORKS;
555     p_input_report->sec.rd      = SEC_JUST_WORKS;
556 
557     hid_info_flags = HID_INFO_FLAG_REMOTE_WAKE_MSK | HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK;
558 
559     memset(&hids_init_obj, 0, sizeof(hids_init_obj));
560 
561     hids_init_obj.error_handler                  = service_error_handler;
562     hids_init_obj.is_kb                          = false;
563     hids_init_obj.is_mouse                       = true;
564     hids_init_obj.inp_rep_count                  = INPUT_REPORT_COUNT;
565     hids_init_obj.p_inp_rep_array                = inp_rep_array;
566     hids_init_obj.outp_rep_count                 = 0;
567     hids_init_obj.p_outp_rep_array               = NULL;
568     hids_init_obj.feature_rep_count              = 0;
569     hids_init_obj.p_feature_rep_array            = NULL;
570     hids_init_obj.rep_map.data_len               = sizeof(rep_map_data);
571     hids_init_obj.rep_map.p_data                 = rep_map_data;
572     hids_init_obj.hid_information.bcd_hid        = BASE_USB_HID_SPEC_VERSION;
573     hids_init_obj.hid_information.b_country_code = 0;
574     hids_init_obj.hid_information.flags          = hid_info_flags;
575     hids_init_obj.included_services_count        = 0;
576     hids_init_obj.p_included_services_array      = NULL;
577 
578     hids_init_obj.rep_map.rd_sec         = SEC_JUST_WORKS;
579     hids_init_obj.hid_information.rd_sec = SEC_JUST_WORKS;
580 
581     hids_init_obj.boot_mouse_inp_rep_sec.cccd_wr = SEC_JUST_WORKS;
582     hids_init_obj.boot_mouse_inp_rep_sec.wr      = SEC_JUST_WORKS;
583     hids_init_obj.boot_mouse_inp_rep_sec.rd      = SEC_JUST_WORKS;
584 
585     hids_init_obj.protocol_mode_rd_sec = SEC_JUST_WORKS;
586     hids_init_obj.protocol_mode_wr_sec = SEC_JUST_WORKS;
587     hids_init_obj.ctrl_point_wr_sec    = SEC_JUST_WORKS;
588 
589     err_code = ble_hids_init(&m_hids, &hids_init_obj);
590     APP_ERROR_CHECK(err_code);
591 }

看完了hid服务的初始化之后,我们来看下如何发送hid服务的数据,也就是mouse控制的数据,这里我们以开发板上的按键S1和S2来作为mouse音量键控制的展示。

首先是按键S1,我们按下后,进入到如下处理函数。我们定义一个buffer,并且给它赋值0x10(0001 0000),也就是bit4赋值为1,然后调用ble_hids_inp_rep_send函数将这个数据发送给手机,此时控制的是手机的音量减按键。

然后是按键S1,我们按下后,进入到如下处理函数。我们定义一个buffer,并且给它赋值0x20(0010 0000),也就是bit5赋值为1,然后调用ble_hids_inp_rep_send函数将这个数据发送给手机,此时控制的是手机的音量加按键。

而如果我们打开手机的照相机功能,音量的加减按键都是控制照相机拍照。

1012 //******************************************************************
1013 // fn : bsp_event_handler
1014 //
1015 // brief : 处理来自BSP模块的事件的函数
1016 //
1017 // param : event -> 事件按下按钮产生的事件
1018 //
1019 // return : none
1020 static void bsp_event_handler(bsp_event_t event)
1021 {
1022     ret_code_t err_code;
1023 
1024     switch (event)
1025     {
1026         case BSP_EVENT_KEY_0:
1027             if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
1028             {
1029                 //mouse_movement_send(-MOVEMENT_SPEED, 0);
1030                 uint8_t buffer[INPUT_REP_MEDIA_PLAYER_LEN];
1031 
1032                 APP_ERROR_CHECK_BOOL(INPUT_REP_MEDIA_PLAYER_LEN == 1);
1033 
1034                 buffer[0] = 0x10;
1035                 err_code = ble_hids_inp_rep_send(&m_hids,
1036                                                  INPUT_REP_MPLAYER_INDEX,
1037                                                  INPUT_REP_MEDIA_PLAYER_LEN,
1038                                                  buffer,
1039                                                  m_conn_handle);
1040                 if (err_code != NRF_ERROR_INVALID_STATE)
1041                 {
1042                     APP_ERROR_CHECK(err_code);
1043                 }
1044             }
1045             break;
1046 
1047         case BSP_EVENT_KEY_1:
1048             if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
1049             {
1050                 //mouse_movement_send(0, -MOVEMENT_SPEED);
1051                 uint8_t buffer[INPUT_REP_MEDIA_PLAYER_LEN];
1052 
1053                 APP_ERROR_CHECK_BOOL(INPUT_REP_MEDIA_PLAYER_LEN == 1);
1054 
1055                 buffer[0] = 0x20;
1056                 err_code = ble_hids_inp_rep_send(&m_hids,
1057                                                  INPUT_REP_MPLAYER_INDEX,
1058                                                  INPUT_REP_MEDIA_PLAYER_LEN,
1059                                                  buffer,
1060                                                  m_conn_handle);
1061                 if (err_code != NRF_ERROR_INVALID_STATE)
1062                 {
1063                     APP_ERROR_CHECK(err_code);
1064                 }
1065             }
1066             break;
1067             
1068         default:
1069             break;
1070     }
1071 }

10 音乐播放器实验

10.1 实验简介

上一个自拍实验,我们已经给大家介绍了BLE HID的含义以及使用方式(进行本实验之前,请大家一定先看自拍实验内容讲解)。

大家都知道蓝牙耳机与蓝牙音响是蓝牙最常用的方向,而这两个应用都是和手机音乐播放器相关的控制,自拍实验中我们已经了解到了如何控制手机音量的加减,所以这一章节我们补充性的给大家带来音乐播放的其他3个按键控制(开始/暂停、下一首、上一首)。

10.2 硬件说明

nRF52DK开发板采用与Nordic官方开发板相同的按键电路,原则协议栈例程可以直接使用按键,无需修改代码。

按键与芯片引脚对应关系如下表格。

网络标号 芯片引脚号 连接方式
BTN1 P0.13 直连,下降沿(低电平)触发
BTN2 P0.14 直连,下降沿(低电平)触发
BTN3 P0.15 直连,下降沿(低电平)触发
BTN4 P0.16 直连,下降沿(低电平)触发
 

10.3 实验现象

10.4 源码讲解

本实验和自拍实验的主体代码都是相同,唯一不同的只有按键触发下的hid控制。

10.4.1 mian.c

按键按下,我们分别看下每个按键对应的hid功能。

S1:控制音乐的暂停与播放

S2:控制音乐切换下一首

S3:控制音乐切换上一首

S4:控制手机应用app切换(与本实验无关,仅做功能展示)

1012 //******************************************************************
1013 // fn : bsp_event_handler
1014 //
1015 // brief : 处理来自BSP模块的事件的函数
1016 //
1017 // param : event -> 事件按下按钮产生的事件
1018 //
1019 // return : none
1020 static void bsp_event_handler(bsp_event_t event)
1021 {
1022     ret_code_t err_code;
1023 
1024     switch (event)
1025     {
1026         case BSP_EVENT_KEY_0:
1027             if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
1028             {
1029                 //mouse_movement_send(-MOVEMENT_SPEED, 0);
1030                 uint8_t buffer[INPUT_REP_MEDIA_PLAYER_LEN];
1031 
1032                 APP_ERROR_CHECK_BOOL(INPUT_REP_MEDIA_PLAYER_LEN == 1);
1033 
1034                 buffer[0] = 0x01;   // 开始/暂停
1035                 err_code = ble_hids_inp_rep_send(&m_hids,
1036                                                  INPUT_REP_MPLAYER_INDEX,
1037                                                  INPUT_REP_MEDIA_PLAYER_LEN,
1038                                                  buffer,
1039                                                  m_conn_handle);
1040                 if (err_code != NRF_ERROR_INVALID_STATE)
1041                 {
1042                     APP_ERROR_CHECK(err_code);
1043                 }
1044             }
1045             break;
1046 
1047         case BSP_EVENT_KEY_1:
1048             if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
1049             {
1050                 //mouse_movement_send(0, -MOVEMENT_SPEED);
1051                 uint8_t buffer[INPUT_REP_MEDIA_PLAYER_LEN];
1052 
1053                 APP_ERROR_CHECK_BOOL(INPUT_REP_MEDIA_PLAYER_LEN == 1);
1054 
1055                 buffer[0] = 0x04;   // 下一首
1056                 err_code = ble_hids_inp_rep_send(&m_hids,
1057                                                  INPUT_REP_MPLAYER_INDEX,
1058                                                  INPUT_REP_MEDIA_PLAYER_LEN,
1059                                                  buffer,
1060                                                  m_conn_handle);
1061                 if (err_code != NRF_ERROR_INVALID_STATE)
1062                 {
1063                     APP_ERROR_CHECK(err_code);
1064                 }
1065             }
1066             break;
1067 
1068         case BSP_EVENT_KEY_2:
1069             if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
1070             {
1071                 //mouse_movement_send(0, -MOVEMENT_SPEED);
1072                 uint8_t buffer[INPUT_REP_MEDIA_PLAYER_LEN];
1073 
1074                 APP_ERROR_CHECK_BOOL(INPUT_REP_MEDIA_PLAYER_LEN == 1);
1075 
1076                 buffer[0] = 0x08;   // 上一首
1077                 err_code = ble_hids_inp_rep_send(&m_hids,
1078                                                  INPUT_REP_MPLAYER_INDEX,
1079                                                  INPUT_REP_MEDIA_PLAYER_LEN,
1080                                                  buffer,
1081                                                  m_conn_handle);
1082                 if (err_code != NRF_ERROR_INVALID_STATE)
1083                 {
1084                     APP_ERROR_CHECK(err_code);
1085                 }
1086             }
1087             break;
1088             
1089         case BSP_EVENT_KEY_3:
1090             if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
1091             {
1092                 //mouse_movement_send(0, -MOVEMENT_SPEED);
1093                 uint8_t buffer[INPUT_REP_MEDIA_PLAYER_LEN];
1094 
1095                 APP_ERROR_CHECK_BOOL(INPUT_REP_MEDIA_PLAYER_LEN == 1);
1096 
1097                 buffer[0] = 0x80;   // 切换手机应用(切换已经打开的app)
1098                 err_code = ble_hids_inp_rep_send(&m_hids,
1099                                                  INPUT_REP_MPLAYER_INDEX,
1100                                                  INPUT_REP_MEDIA_PLAYER_LEN,
1101                                                  buffer,
1102                                                  m_conn_handle);
1103                 if (err_code != NRF_ERROR_INVALID_STATE)
1104                 {
1105                     APP_ERROR_CHECK(err_code);
1106                 }
1107             }
1108             break;
1109             
1110         default:
1111             break;
1112     }
1113 }