NRF52832DK协议栈实验
我们的蓝牙协议实验部分,将会给大家带来最直观的蓝牙协议部分的学习,我们通过拆分的方式,带领大家深入了解蓝牙协议的主要功能部分。
在进行蓝牙协议实验学习之前,我们有一些基础但十分重要的蓝牙协议介绍分享给大家,这个部分的蓝牙协议不像蓝牙协议手册那样,对每个参数进行说明,洋洋洒洒会有好几千页的说明文档,这样大家也很难去看完并记忆。
我们针对的仅仅是大家在利用协议栈开发个人需求的时候,绕不开的蓝牙协议部分的内容进行介绍。首先我们先分析一下蓝牙主从机通信,都有哪些重要点。
主机部分:扫描、发起连接、发现服务、通信。
从机部分:广播、注册服务、被连接、通信。
主机 | 从机 | ||||||
---|---|---|---|---|---|---|---|
扫描 | 连接 | 服务 | 通信 | 广播 | 服务 | 连接 | 通信 |
扫描参数① | 连接参数③ | 服务句柄⑦ | 属性Write⑨ | 广播数据② | 服务⑥ | 连接参数③ | 属性Notify⑨ |
连接句柄④ | 使能通知⑧ | 连接句柄④ | |||||
MTU大小⑤ | MTU大小⑤ |
目录
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,被动扫描模式,只可以获取从设备的广播数据
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。
数据内容:用户自定义数据。
数据长度 | 数据类型 | 数据内容 |
---|---|---|
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)。
不同的应用可能需要不同的连接间隔。
Slave Latency从机延迟,此参数为从机(外设设备)提供跳过多个连接事件的能力。这种能力给外设设备更多的灵活性。如果外设没有要发送的数据,则可以跳过连接事件,保持睡眠并节省电量。外设设备选择是否在每个连接事件时间点上唤醒。虽然外设可以跳过连接事件,但不能超出从延迟参数允许的最大值。
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))使用较大的有效负载大小,并减少与较大数据事务相关的开销。当使用分片时,较大的分组被分割成多个链路层分组,并由对等体设备的链路层重新组合。
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都是同等地位,以下说明不分先后)。
Application:主要就两个文件,一个是主函数 main.c 文件,这个文件是大家后续编程主要修改的文件。另一个 sdk_config.h 配置文件,NRF52相关的蓝牙参数、外设参数等等,我们在使用之前都需要在这个文件中配置或者是使能。
Board Definition:nordic给我们用户实现好的有关按键和LED灯的控制的文件,这个文件主要是nordic方便自己例程的展示实现的,在后续的编程中大家可以选择使用,或者自己根据个人需要重新实现。
Board Support:这个分组和Board Definition是一起作用的,就是利用按键和LED灯进行一些功能的展示,比如按键的外部中断唤醒,比如用LED来指示当前蓝牙的状态等。也是一样的,后续编程中大家可以根据个人需要选择保留还是自己实现。
None:这个分组也是包含了两个文件,这两个文件都是和芯片相关的。arm_startup_nrf52.s 是芯片的启动文件,这个文件配置了芯片启动时的堆栈空间,中断向量等等参数。system_nrf52.c 文件是芯片的系统文件,配置了芯片的RAM、时钟、射频以及引脚端口等等。这两个文件都是芯片的必要文件,所以每一个工程中都是需要包含的。
nRF_BLE:这个分组包含的是蓝牙协议相关的配置文件,也就是我们协议栈实验部分主要要讲解的内容。主要有如下几个部分,广播、连接、扫描等等,由于我们展示的例程是串口透传从机,所以这边看不到扫描相关的文件。
nRF_BLE_Services:这个分组是用于存放我们的蓝牙profile服务文件,像我们串口透传例程,就包含了NUS(nordic uart service)的通用配置文件。
nRF_Drivers:这个分组包含的是外设驱动文件,其中前缀是nrf开头的代表的是老的驱动文件,nrfx代表的新的驱动文件,我们当前使用的SDK15.2兼容新旧两种外设驱动文件。
nRF_Libraries:库函数文件的分组,里面包含了两个大类。一个是以app为前缀的文件,这部分是留给我们用户在应用层调用的库文件,基本上是按键、时钟等等一些外设的库。另一部分是以nrf为前缀的库文件,这个是和芯片相关的库,包括内存分配、打印以及电源管理等等。
nRF_Log:这个是nordic做好的一个打印调试信息的功能分组,主要分为两个,一个是利用Jlink仿真器实现的RTT,另一个则是利用串口打印。这个部分的功能在下面的实验中有讲解。
nRF_Segger_RTT:这个部分是Segger公司实现好的RTT的驱动文件,我们使用的时候只需要将文件添加到我们的工程中,然后调用API接口就行,这样我们就能将调试信息通过Jlink打印到RTT Viewer显示。这个部分的功能是配置上面的Log打印使用的,方便我们开发者调试程序。
nRF_SoftDevice:这里包含的文件主要是配置协议栈初始化的时候协议栈的参数设定,由于协议栈实际上是不开源的,而是留下了配置接口,这些配置接口通过客户配置相关协议栈的参数来设置协议栈运行状态。也就是在我们的stack初始化部分去初始化softdevice。
UTF8/UTF16 convert:utf8与utf16之间的转换。
Output:文件输出分组,里面存放了两个文件,一个是图像数据文件.map,一个是可执行的.out文件。
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多次,才会出现一次异常)
13.2 实验现象
我们打开J-Link RTT Viewer,选择我们的Jlink仿真器,可以看到log打印如下。
注意:如果设置了输出,但是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的相关函数。
使用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功能了。
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 // 从机设备相当于当前主机的信号强度
主机log打印如下,先打印当前例程的名称2.2_ble_peripheral_adv_all。
14.3 工程及源码讲解
14.3.1 工程说明
相对于LOG打印的工程,我们新增了nRF_BLE分组,这个分组下包含的就是BLE协议相关的文件,我们在本实验中只使用了nrf_ble_scan.c下的扫描功能函数。
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设备(不做扫描限制)。
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的分组,但是我们可以看到看到分组下的文件与主机工程是不同的。
主机部分主要是扫描和获取服务相关,从机部分则是广播、连接参数和服务注册相关。
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()函数
在这里给大家科普一下,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()。
我们展示的仅仅是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以及容貌。
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。
从机部分,可以看到上电先打印2.3_ble_peripheral_adv_filter字样。
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地址,去限制扫描。
16.2 实验现象
主机部分,上电打印例程名称1.4_ble_central_scan_whitelist,如果成功设置白名单,则打印Successfully set whitelist!,然后如果附近有白名单中的设备,则会打印扫描到的白名单设备的信息。
从机部分相较于前两章的内容,几乎没有改动,这边不做讲解。因为白名单的限制,是由主机部分完成。
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后将打印他的广播和扫描回调数据、以及型号强度。
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),则会发起连接。然后打印连接成功的消息,并且打印连接参数。
Connected. conn_DevAddr: 0xD363BFCE5C46 // 连接的对方设备MAC地址
conn_handle: 0x0000 // 连接的句柄,第一个连接的设备是0x0000,第二个是0x0001,以此类推 conn_Param: 24,24,0,400 // 连接参数 |
从机部分,先打印实验名称2.5_ble_peripheral_conn_all,当被主机成功连接之后,会打印连接成功的消息;当断开连接之后,会打印断开连接的句柄,并且会打印断开连接的原因。
Connected. conn_DevAddr: 0xE51F64D6BFF4 // 连接的对方设备的MAC
Connected. conn_handle: 0x0000 // 连接的句柄 Connected. conn_Param: 24,24,0,400 // 连接参数
|
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,然后发起扫描,如果扫描到符合过滤的从机设备,则会打印扫描到的设备信息,然后发起连接,连接成功后会打印连接的一些信息。
从机上电后先打印实验名称2.6_ble_peripheral_conn_filter,如果被主机扫描连接之后,则会打印连接的信息。
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。
注意:由于这两个事件返回的信息和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 实验简介
经过前两章的主从机扫描并连接的实验学习后,我们不难发现,连接之后主从机之间一个很重要的参数(返回连接成功的状态下,打印的数据),那就是连接参数。
有关连接参数的详细说明,请大家查看本手册的第一章节有关蓝牙协议的介绍。
主从机连接参数的配置流程:
1、主从机连接成功之后,我们将以主机携带的连接参数,作为主从机连接的参数 2、从机可以发起更新连接参数的请求 3、主机接收到从机的参数更新请求,发起参数更新 4、更新成功,将以新的参数作为连接参数 |
19.2 实验现象
主机上电后发起扫描,一旦扫描到我们过滤的设备之后,就会发起连接。
连接成功后,打印连接的handle,以及主从机之间的连接参数。
当从机发起连接参数更新请求后,我们使用从机申请的连接参数,去发起连接参数的更新。
从机上电后广播,被主机连接后打印连接的handle,以及连接参数。
5000ms后,从机发起参数更新请求,主机接收请求后发起更新,更新成功后,从机打印新的连接参数。
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字节(这个相当于用户的私有协议,不是必须存在)。
20.2 实验现象
主机上电之后扫描周围的从机,一旦扫描到符合的过滤的从设备,就会打印扫描到的信息,并且发起连接,连接成功之后,打印连接的参数以及对方设备的信息。
连接成功之后,会打印Data len的大小,也就是我们的MTU。
从机上电之后广播,被主机连接之后,打印主机的设备信息以及连接参数。
并且会打印Data len的大小,也就是我们的MTU。
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客户端去向服务的特性数据库中写入或者读取数据。
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依次点亮,并且同时从从机读取我们刚刚发送的数据
从机设备流程:
1、开启广播
2、被主机成功连接,并交互连接参数
3、等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)
4、主机按下按键,从机接收到相应的LED状态数据并打印,并根据这个数据控制板子上的LED点亮
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功能
从机设备流程:
1、开启广播
2、被主机成功连接,并交互连接参数
3、等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)
4、等待主机成功使能notify功能
5、从机分别按下4个按键,给主机发送相应的notify数据包,控制主机LED点亮
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。
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功能的数据包给主机