BLE(蓝牙低功耗)
1 什么是蓝牙低功耗?
BLE是蓝牙低功耗的简称(Bluetooth Low Energy)。BLE技术是低成本、短距离、可互操作的鲁棒性无线技术,工作在免许可的2.4GHz ISM射频频段。它从一开始就设计为超低功耗无线技术。它利用许多智能手段最大限度地降低功耗。
低成本,低功耗快速启动,瞬间连接。最快3ms连接。传输距离的提高。高安全性。使用AES-128加密算法进行数据报加密认证。
2 BLE体系结构
蓝牙地功耗包含三个部分:控制器、主机和应用程序
应用 应用程序 主机 通用访问配置文件(GAP)通用属性配置文件(GATT)属性协议(ATT) 安全管理器(SM)逻辑链路控制器和适配协议(L2CAP) 控制器 主机控制器接口(HCL)链路层(LL)物理层(PHY)
3 BLE广播、扫描和连接事件
3.1 广播事件
通用广播:最常用的广播方式,可以被扫描,接受到连接请求时可以作为从设备进入一个连接
定向广播:针对于快速建立连接的需求,定向广播会占满整个广播信道,数据净荷只包含广播者和发起者地址,发起者收到发给自己的定向广播后,会立即发送连接请求。
不可连接广播:广播数据,而不进入连接态。
可发现广播:不可连接,但可以响应扫描。
BLE广播间隔:是指两次广播事件之间的最小时间间隔,一般取值范围在20ms-10.24s之间,链路层会在每次广播事件期间产生一个随机广播延时时间(0ms-10ms)。
3.2 扫描事件
每次扫描,设备打开接收器去监听广播设备,这称为一个扫描事件,扫描事件有两个事件参数扫描窗口和扫描间隔。
扫描窗口(scan window):一次扫描进行的时间宽度。
扫描间隔(scan interval):两个连续的扫描窗口的起始时间之间的时间差,包括扫描休息的时间和扫描进行的时间。
注意:
- 扫描窗口和扫描间隔设置的时间不能大于**10.24s**。- 扫描窗口设置的值不能大于扫描间隔的值- 如果扫描窗口 = 扫描间隔的话,说明主机一直在进行扫描。
3.3 连接事件
一个连接事件是指主设备和从设备之间相互发送数据包的过程
所有的数据交换都是通过连接事件来完成的
每个事件发生在某个数据通道(0-36)。
一个连接中,主从设备依靠连接事件交换数据。
设备连接后,无论有无数据收发,连接事件都在按照设置的参数周而复始的进行着,直到一方停止响应。
主机与从机可在单次连接事件进行多次数据传输:
1)连接参数
- 连接间隔:必须是1.25ms的倍数,范围是从最小值6(7.5ms)到最大值3200(4.0s)。- 从机延迟:这个参数描述了从机跳过连接事件的次数。这使外围设备具有一定的灵活性,如果它不具有任何数据传送,它可以选择跳过连接事件,并保持睡眠,从而提供了一些积蓄力量。这一决定取决于外围设备。- 监督超时:这是两个成功的连接事件之间间隔的最大值。如果超过这个时间还未出现成功的连接事件,那么设备将会考虑失去连接,返回一个未连接状态。这个参数使用10ms的步进(10ms的倍数)。监督超时时间从最小10(100ms)到最大3200(32.0s)。同时超时时间必须大于有效连接时间。2)连接参数和功耗、传输速度的关系不同的应用也许需要不同的连接间隔,一个长时间的连接间隔将会节约更多的能量,因为设备可以在两个连接事件之间睡眠更长的时间。但是它会导致数据发送不及时,如果有数据要发送,那么它只能够在下一次连接事件到来时才能被发送。- 短连接间隔 ---> 高功耗,高数据吞吐量,发送等待时间短。- 长连接间隔 ---> 低功耗,低数据吞吐量,发送等待时间长。- 低或者0潜伏值 ---> 从机在没有数据发送的情况下高功耗,从机可以快速的收到主机的数据- 高潜伏值 ---> 从机在没有数据发送的情况下可以低功耗,从机无法及时收到主机的数据,但是主机能及时 收到从机的数据;
4 BLE程序结构
一个BLE程序通常需要至少包含4个必要部分:系统初始化、启动、空闲管理和事件处理。
4.1 系统初始化
初始化部分用于将系统配置为预设状态,等待启动操作启动系统。初始化包含了十个部分,其中日志打印和指示灯不是必须的。
日志打印:用于输出调试信息,日志打印不是BLE程序运行必须的,但是它是一个“不可或缺”的部件。
APP定时器初始化:初始化APP定时器,用于实现各种定时任务。尤其是要注意APP定时器是基于RTC1的软件定时器,并不是BLE运行的“时钟”。但是因为连接参数协商程序模块使用了APP定时器,所以即使应用程序没有用到APP定时器,也要执行APP初始化。
板卡外设初始化:板卡上电后,需要将板上的外设初始化到自己想要的状态。板卡外设初始化可以根据具体的需求来操作,比如本例中我们只用到了LED指示BLE工作状态。那么只初始化LED就可以了。
BLE相关初始化:包含广播、首选连接参数、GAP层、服务等初始化配置,通过配置将具体的配置参数传递给SoftDevice,当我们启动协议栈后,整个BLE部分的程序就会按照我们设定的参数运行。
4.2 启动
对于外围设备来说,启动的是广播,启动之后,系统开始按照配置的广播间隔对外广播数据,等待中心设备连接。对于中心设备来说,启动的是扫描,按照配置的参数扫描周围的设备。
4.3 空闲管理
空闲管理的作用主要是为了实现低功耗,BLE程序运行时,CPU并不是总是在运行的,空闲管理就是让CPU在没有指令执行时进入到低功耗模式等待事件唤醒,从而降低功耗,延长电池使用事件。空闲管理同事还处理挂起的日志打印信息,如果程序中使用了日志打印功能。
4.4 事件处理
应用程序通过注册“事件监视者”监视BLE协议栈事件,当协议栈有事件产生时,会告知事件监视者,应该程序通过事件监视者的时间处理函数获取和处理时间,如外围设备和中心设备连接后,SoftDevice会提交“连接事件”,应用程序即可在事件处理函数中接收到该事件并执行相应动作。
5 编写代码
5.1 日志打印
5.1.1 日志开关
nRF52823的SDK中,日志开关时在“sdk_config.h”中。NRF_LOG_ENABLE是总日志开关,nrf_log_module_configuration是各个程序模块的日志开关。
5.1.2 日志级别
Off:关闭日志输出。 Error:日志只输出错误信息,函数:NRF_LOG_ERROR。 Warning:日志只输出警告信息,函数:NRF_LOG_WARNING。 Info:日志只输出基本信息, 函数:NRF_LOG_INFO。 Debug:日志只输出调试信息,函数:NRF_LOG_DEBUG。5.1.3 加入Log
使用Log时,首先要引入Log的头文件,然后初始化Log程序模块,并在主循环中加入Log信息的输出处理,之后就可以通过Log输出函数打印信息。
头文件:
#include "nrf_log.h"#include "nrf_log_ctrl.h"#include "nrf_log_default_backends.h"
初始化:
static void log_init(void){// 初始化log程序模块ret_code_t err_code = NRF_LOG_INIT(NULL);APP_ERROR_CHECK(err_code);// 设置log输出终端(根据sdk_config.h中的配置设置终端为UART或者RTT)NRF_LOG_DEFAULT_BACKENDS_INIT();}
信息处理:
if(NRF_LOG_PROCESS() == false){// 其他功能代码}
5.1.4 Log配置(RTT作为输出终端)
打开“sdk_config.h”文件,切换到“Configuration Wizard”,主要配置项目如下图,其他配置默认即可。
修改好配置后,编译工程并下载到开发板运行。之后还需要配置JLINK-RTT,配置好之后,才能接受LOG打印的信息,步骤如下。
1.启动JLINK-RTT Viewer
2.配置JLINK-RTT Viewer
连接JLINK:选择USB目标设备:选择nRF52832-xxAA调试接口:必须选择SWD 3.信息显示
配置信息完成后,点击确定“确定”按钮即进入JLINK-RTT Viewer界面
5.2 初始化APP定时器
5.2.1 APP定时器的作用
APP定时器基于实时计数器RTC1的软件定时器,APP定时器允许用户同时创建多个定时任务,APP定时器在RTC1终端处理程序中检查超时并执行超时处理程序的调用,在软件中断(SWIO)处理定时列表,RTC1中断和SWIO均使用低中断优先级APP_LOW。
在BLE的程序中,APP定时器的用途主要有以下几种:
1.按键消抖以及实现长按和短按2.定时驱动LED指示灯,和BLE事件配合实现BLE的状态指示3.连接参数更新,连接成功后,开启APP定时器,超时后进行连接参数更新4.用户创建定时任务,实现各种定时任务,如创建一个超时时间为1秒的APP定时器作为实时时钟的时基
因为BLE中的连接参数更新使用了APP定时器,所以,及时BLE程序中不用按键、指示灯,也不创建用户定时任务,APP定时器也需要初始化。
5.2.2 加入APP定时器初始化代码
使用定时器时,需要引入头文件“app_timer.h”,然后,初始化APP定时器模块,之后即可创建用户定时任务,实现定时。
头文件:
#include "app_timer.h"
初始化:
// 初始化APP定时器模块static void timers_init(void){// 初始化APP定时器模块ret_code_t err_code = app_timer_init();// 检查返回值APP_ERROR_CHECK(err_code);// 加入创建用户定时任务的代码,创建用户定时任务。}
5.3 初始化指示灯
初始化LED时,调用库函数bsp_init(),函数参数使用BSP_INIT_LEDS即可初始化开发板上的4个LED指示灯,代码如下:
初始化LED:
static void leds_init(void){// 初始化BSP指示灯ret_code_t err_code = bsp_init(BSP_INIT_LEDS, NULL);// 检查函数返回的错误代码APP_ERROR_CHECK(err_code);}
5.4 初始化和运行电源管理
SoftDevice实现了一个简单易用的SoftDevice Power API,用于优化电源管理。启用SoftDevice时,应用程序必须使用此API以确保实现正确的功能。
电源管理运行后,在使用API等待应用程序事件时,只要SoftDevice不是用CPU,CPU就会进入IDLE状态,此时,SoftDevice直接处理的中断不会唤醒应用程序。应用程序中断将按预期唤醒应用程序。另外,当系统进入System Off模式时,使用电源管理模块的API可以确保在断电之前停止SoftDevice服务。
使用电源管理功能时,首先要初始化电源管理模块,之后在主函数中运行电源管理,这样,当CPU空闲时会执行电源管理API,进入低功耗模式等待事件唤醒,从而实现低功耗。
5.4.1 初始化电源管理模块
电源管理模块通过调用nrf_pwr_mgmt_init()函数初始化
初始化
static void power_management_init(void){ret_code_t err_code;// 初始化电源管理err_code = nrf_pwr_mamt_init(); /***************************函数原型:ret_code_t nrf_pwr_mgmt_init(void)函数功能:初始化电源管理模块。根据配置,此功能在系统控制块(SCB)中设置SEVONPEND,SoftDevice的中断优先级高于SVC,此操作不安全参 数:无返回值:NRF_SUCCESS:启动广播成功。NRF_ERROR_INVALID_STATE:广播程序模块没有初始化。***************************/// 检查函数返回的错误代码APP_ERROR_CHECK(err_code)}
5.4.2 运行电源管理
运行电源管理时通过调用函数nrf_pwr_mgmt_run()执行的,该函数必须放到主循环里面执行,以确保CPU空闲时即可运行电源管理。
运行电源管理
// 空闲状态处理函数。如果没有挂起的日志操作,则睡眠知道下一个事件发生后唤醒系统static void idle_state_handle(void){// 处理挂起的logif(NRF_LOG_PROCESS() == false){// 运行电源管理,该函数需要放到主函数循环里面执行nrf_pwr_mgmt_run();}}
5.5 BLE协议栈初始化
5.5.1 初始化步骤
使用BLE功能之前,需要先初始化BLE协议栈,BLE协议栈初始化包括是能SoftDevice、配置BLE协议栈参数、使能BLE协议栈和注册BLE协议栈事件回调函数。
初始化BLE协议栈:
static void ble_stack_init(void){ret_code_t err_code;// 请求使能SoftDevice,该函数会根据sdk_config.h文件中低频时钟的设置来配置低频时钟err_code = nrf_sdh_enable_request();APP_ERROR_CHECK(err_code);// 定义保存应用程序RAM起始地址的变量uint32_t ram_start = 0;// 使用sdk_config.h文件的默认参数配置协议栈,获取应用程序RAM起始地址// 赋值给变量ram_starterr_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);APP_ERR_CHECK(err_code);// 使能BLE协议栈err_code = nrf_sdh_ble_enable(&ram_start);APP_ERROR_CHECK(err_code);// 注册BLE事件回调函数NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);}
1. 使能SoftDevice
从上面的代码中,我们可以看到,初始化BLE协议栈时,首先必须使能SoftDevice,使能SoftDevice时用的库函数时nrf_sdh_enable_request(),该函数在使能SoftDevice时同事根据sdk_config.h文件中低频时钟的设置来配置低频时钟。
函数原型:
// 函数原型:ret_code_t nrf_sdh_enable_request(void);// 函数功能:// 请求使能SoftDevice。// 该函数向所有使用NRF_SDH_REQUEST_OBSERVER宏注册的所有观察者发布NRF_SDH_EVT_ENABLE_REQUEST请求,// 观察者可能会确认/也可能不确认请求,// 如果所有的观察者都确认,则SoftDevice使能成功,// 否则,进程将被停止,// 而未确认的观察者如果准备好后会负责通过调用“nrf_sdh_request_continue”函数重启进程// 参数:无// 返回值://NRF_SUCCESS:初始化成功。//NRF_ERROR_INVALID_STATE:协议栈已经使能。
2. 配置BLE协议栈的参数
SoftDevice使能后,接下来需要配置BLE协议栈的参数,这是通过库函数nrf_sdh_ble_default_cfg_set()完成的,该函数一方面使用sdk_config.h文件中设置的默认参数配置协议栈,另一方面获取MDK中设置的应用程序的RAM其实地址,并通过函数的参数ram_start返回应用程序RAM起始地址,后面使能BLE协议栈时会用到该地址。
函数原型:
// 函数原型:ret_code_t nrf_sdh_ble_default_cfg_set(uint8_t conn_cfg_tag,uint32_t* p_ram_start);// 函数功能://设置默认的BLE协议栈配置//并将RAM起始地址保存到传入的参数p_ram_start中// 参数:uint8_t conn_cfg_tag:配置标记。uint32_t* p_ram_start:应用程序RAM起始地址。// 返回值:NRF_SUCCESS:初始化成功。NRF_ERROR_INVALID_STATE:协议栈已经使能。
3. 使能BLE协议栈
BLE协议栈参数配置完成后,即可通过nrf_sdh_ble_enable()库函数使能BLE协议栈和分配内容,该函数会对RAM起始地址和大小进行检查,并通过log打印出检查结果。
函数原型:
// 函数原型:ret_code_t nrf_sdh_ble_enable(uint32_t *p_app_arm_start);// 函数功能://配置和使能BLE协议栈// 参数://uint32_t *p_app_arm_start:应用程序RAM起始地址。// 返回值://NRF_SUCCESS:BLE协议栈初始化成功。//NRF_ERROR_INVALID_STATE:BLE协议栈已经初始化,不能再进行初始化。//NRF_ERROR_INVALID_ADDR:提供无效的或未对齐的指针。//NEF_ERROR_NO_MEM:内存不足。
4. 注册事件监视者
程序运行过程中,应用程序需要实时获取自己感兴趣的协议栈的事件以进行相应的处理,也就是应用程序需要“监视”协议栈的事件,所以在使能BLE协议栈后,需要注册事件监视者,当协议栈有时间产生时,会告知事件监视者,事件监视者则会采取相应的行动。以蓝牙连接断开事件为例,其执行流程如下:
应用程序注册BLE协议栈事件监视者。协议栈检测到蓝牙连接断开(BLE_GAP_EVT_DISCONNECTED)事件。协议栈告知事件监视者蓝牙连接断开事件事件监视者处理蓝牙断开事件(事件处理函数中处理蓝牙断开事件)
事件监视者使用宏NEF_SDH_BLE_OBSERVER注册,该宏描述如下,共有四个参数,其中“_prio”是固定的数值,应用程序注册事件监视者时该参数的值时3,应用程序不能修改该数值。
// 宏:#define NRF_SDH_BLE_OBSERVER(_name,_prio,_handler,_context)// 功能描述:该宏用于注册事件监视者,程序模块如果想接受事件通知,就必须使用该宏注册事件处理程序// 参数:_name:事件监视者名称。_prio:事件监视者事件处理函数的优先级,数字越小,优先级越高。_handler:BLE事件句柄_context:事件处理函数参数
注册事件监视者时应用程序需要提供事件处理函数,用于接收和处理BLE事件,BLE事件处理函数编写格式如下,函数名称必须和注册的名称一样。
static void ble_evt_handler(ble_evt_t const *p_ble_evt, void *p_context){ret_code_t err_code = NRF_SUCCESS;// 判断BLE事件类型,根据事件类型执行相应操作switch(p_ble_evt->header.evt_id){// 断开连接事件NRF_LOG_INFO("Disconnected.");break;// 添加处理其它BLE事件的代码default:break;}}
对于BLE工程模板,只需要处理连接事件、连接断开事件、PHY更新事件和超时时间这几个基本的BLE事件即可,代码清单如下。
static void ble_evt_handler(ble_evt_t const *p_ble_evt, void *p_context){ret_code_t err_code = NRF_SUCCESS;// 判断BLE事件类型,根据事件类型执行相应的操作switch(p_ble_evt->header.evt_id){// 断开连接事件case BLE_GAP_EVT_DISCONNECTED:// 打印提示信息NRF_LOG_INFO("Disconnected.");break;// 连接事件case BLE_GAP_EVT_CONNECTED:NRF_LOG_INFO("Connected.");// 设置指示灯状态为连接状态,即指示灯D1常亮err_code = bsp_indication_set(BSP_INDICATE_CONNECTED);APP_ERROR_CHECK(err_code);// 保存连接句柄m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;// 将连接句柄分配给派对写入实例,分配后派对写入实例和该连接关联,// 这样,当有多个连接的时候,通过关联不同的派对写入实例,很方便单独处理各个连接err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);APP_ERROR_CHECK(err_code);break;// PHY更新事件case BLE_GAP_EVT_PHY_UPDATE_REQUEST:{NRF_LOG_DEBUG("PHY update request.");ble_gap_phys_t const phys = {.rx_phys = BLE_GAP_PHY_AUTO,.tx_phys = BLE_GAP_PHY_AUTO,};// 响应PHY更新规程err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);APP_ERROR_CHECK(err_code);} break;// GATT客户端超时事件case BLE_GATTC_EVT_TIMEOUT: NRF_LOG_DEBUG("GATT Client Timeout.");err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,BLE_HCI_REMOTE_USER_TERMINATED_CONNECTTON);APP_ERROR_CHECK(err_code);// GATT服务器超时事件case BLE_FATTS_EVT_TIMEOUT:NRF_LOG_DEBUG("GATT Server Timeout.");// 断开当前连接err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);APP_ERROR_CHECK(err_code);break;default:break;}}
在BLE协议栈初始化的过程中,和开发者关系比较大的有2个方面:配置低频时钟源和分配内存,低频时钟源设计到硬件电路的设计,加入我们的硬件电路没有设计外部32.768KHz晶体的话,就必须要配置使用内部RC作为低频时钟源,因此我们要掌握配置低频时钟源的方法。分配内存涉及到协议栈和应用程序对片内RAM的占用,BLE程序中片内RAM是分为两部分的,一部分给SoftDevice使用,另外一部分给应用程序使用,为了合理使用内存,我们需要在使能BLE协议栈时调整内存的分配,接下来我们着重看一下这两部分的操作。
5.5.2 配置低频时钟
SoftDevice开放了低频时钟的配置接口,允许上层应用程序配置低频时钟源(LFCLK时钟源),应用程序也必须为SoftDevice配置低频时钟源,可配置的低频时钟源有2种:
1)片内32.768KHzRC振荡源。
2)片外32.768KHz晶体。
它们的区别如下:
使用内部RC振荡源:缺点是进度稍差,优点是不会增加硬件成本,同时不会占用PCB空间,设备可以设计的更小巧。
使用外部32.768KHz晶振:优点是精度高,缺点是会增加设备成本和占用PCB空间。
低频时钟是在nrf_sdh_enable_request()函数中配置的,进入该函数,可以看到配置的代码如下,定义了一个时钟配置结构体clock_lf_cfg并对其变量指定初始化,初始化时使用的宏时在“sdk_config.h”文件中定义的,之后调用API函数sd_soft_device_enable()使能SoftDevice时,使用该结构体作为sd_soft_device_enable()函数参数,由该API函数完成低频时钟配置。
低频时钟配置代码:
// 低频时钟配置nrf_clock_lf_cfg_t const clock_lf_cfg = {.soure= NRF_SDH_CLOCK_LF_SRC, // 时钟源.rc_ctiv = NRF_SDH_CLOCK_LF_RC_CTIV, // SoftDevice校准定时器间隔// SoftDevice在恒温下校准定时器的间隔,即温度不变的情况下校准定时器的间隔.rc_temp_ctiv = NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,.accuracy = NRF_SDH_CLOCK_LF_ACCURACY // 外部时钟的精度}
上述代码中的4个用于初始化的宏时钟配置结构体成员变量的宏定义在“sdk_config.h”文件中,通过修改这4个宏的值即可设置低频时钟源,默认配置使用的是外部32.768KHz晶体,这4个宏的作用如下:
1)NRF_SDH_CLOCK_LF_SRC:低频时钟源,可设置为外部晶体或内部RC
NRF_CLOCK_LF_SRC_RC:使用内部RC作为低频时钟源。
NRF_CLOCK_LF_SRC_XTAL:使用外部32.768KHz晶体作为低频时钟源。
2)NRF_SDH_CLOCK_LF_RC_CTIV:校准间隔,低频时钟源设置为使用内部RC时推荐值为16,低频时钟源设置为外部晶体时该值为0。
3)NRF_SDH_CLOCK_LF_RC_TEMP_CTIV:SoftDevice在恒温下校准定时器的间隔,即温度不变的情况下校准定时器的间隔,低频时钟源设置为使用内部RC时推荐值为2,设置为外部晶体时该值为0.
4)NRF_SDH_CLOCK_LF_ACCURACY:外部时钟精度,可设置的值如下:
NRF_CLOCK_LF_ACCURACY_500_PPM
NRF_CLOCK_LF_ACCURACY_250_PPM
NRF_CLOCK_LF_ACCURACY_150_PPM
NRF_CLOCK_LF_ACCURACY_100_PPM
NRF_CLOCK_LF_ACCURACY_75_PPM
NRF_CLOCK_LF_ACCURACY_50_PPM
NRF_CLOCK_LF_ACCURACY_30_PPM
NRF_CLOCK_LF_ACCURACY_20_PPM
NRF_CLOCK_LF_ACCURACY_10_PPM
低频时钟源设置为内部RC时设置为NRF_CLOCK_LF_ACCURACY_500PPM,低频时钟源设置为外部晶体时,根据外部晶体的实际参数从上述可设置值中选择对应的值。
1.使用外部32.768KHz晶体作为低频时钟源时“sdk_config.h文件中的配置如下:”
2. 使用内部RC作为低频时钟源时“sdk_config.h”文件中的配置如下:
5.5.3 BLE协议栈使能和内存配置
BLE程序中,RAM是分为两部分的,一部分保留给SoftDevice使用,另外一部分给应用程序使用。RAM分配是在MDK的工程配置中进行的,BLE工程模板中RAM分配如下图所示,通过设置RAM起始地址的大小确定分配给应用程序的RAM空间以及保留给SoftDevice的RAM空间。我们知道nRF52832的片内RAM起始地址是0x20000000,下图中的设置即将0x20000000 - 0x2000224F这一RAM空间分配给了SoftDevice,从0x20002250开始的大小为0xDDB0的RAM空间分配给了应用程序。
BLE工程模板相当于BLE的“最小系统”,所以保留给SoftDevice的RAM空间时最小的,也就是BLE工程模板保留给SoftDevice的RAM空间时SoftDevice对RAM的最小需求。当我们在BLE工程模板中加入其他功能的时候,如加入服务、配对绑定、自定义UUID等等,或者是一个功能比较多的BLE程序中去掉某些功能的时候,协议栈和应用程序对RAM的占用通常会改变,这时候,可能会出现下面三种情况:
RAM起始地址设置过小,程序无法运行。
RAM起始地址设置过大,保留给协议栈的RAM空间足够,程序正常运行,我们希望调整内存分配,以节省RAM,从而能为应用程序分配更多的RAM。
RAM起始地址设置过大,保留给栈协议的RAM空间足够,程序正常运行,我们不关心RAM是否浪费。
对应解决方案:
由上文可以看到,对于RAM分配,我们需要的是通过log获取系统输出的RAM分配信息,根据该信息去修改MDK中RAM的分配。为了获取该提示信息,我们需要做到一下两点。
为了SoftDevice保留的RAM空间能让SoftDevice运行,即RAM的起始地址设置的不小于BLE工程模板中的设置,这样及时程序不能正常运行,但是可以运行到通过Log输出RAM分配提示信息。
开启Log,从而获取系统输出的RAM提示信息。
5.6 配置GAP参数
GAP时蓝牙里面相对比较复杂的概念,主要需要关注的是:GAP的作用、GAP的角色、GAP安全模式和GAP定义的服务(GAO服务)。
5.6.1 GAP作用
GAP(Generic Access Profile),通用访问配置文件。GAP定义了设备如何彼此发现、建立连接以及如何实现绑定,同时描述了设备如何成为广播者和观察者,并且实现无需连接的传输。同时,GAP定义了如何用不同类型的地址来实现隐私性和可解析性。
GAP的作用就是定义以下4个方面:
GAP角色。可发现性模式和规程。连接模式和规程安全模式和规程
从学习GAP开始,会接触到2个非常重要的概念:模式(Mode)和规程(Procedure)。他们用来描述设备的行为,定义和区别如下:
模式(Mode):模式描述的是设备的工作状态,当一个设备被配置为按照某种方式操作一段较长的时间时,称为模式。如广播模式,表示设备正处于广播状态,一般会持续很长时间。
规程(Procedure):规程描述的是在有限的时间内进行特定的操作,如连接参数更新规程,它是在较短的时间内执行了连续参数更新的操作。
5.6.2 GAP角色
BLE为设备在物理传输定义了4中GAP角色,一个设备可以支持多个GAP角色,如既可以是广播者,也可以同时时外围设备:
广播者:广播发送者,不是可连接的设备。观察者:扫描广播,不能够启动连接。外围设备:广播发送者,是可连接的设备,连接后成为从设备。中心设备:扫描广播启动连接,连接后成为主设备。
要理解GAP为什么分为4种角色,就要知道蓝牙标准制定时的考虑。我们知道BLE主打低功耗、所以BLE体系结构中,为了最大可能优化设备,节省功耗,所有的层都采用了非对称的设计,对于物理层的无线装置,可以时者3中形式。
芯片只有发射机:只能发射无线信号,不能接收无线信号,硬件成本低。芯片只有接收机:只能接受无线信号,不能发射无线信号,硬件成本低。芯片同时具有接收机和发射机:既可以接收无线信号,也可以发射无线信号,硬件成本较高。
5.6.3 GAP安全模式
GAP定义了两类安全模式和4个安全规程,它们被服务用于描述所需的安全几倍,每个服务都可以根据自己对安全性的需求使用它们来进行描述。
安全模式是针对服务的,比如对于广播者和观察者,它们仅是一个对外广播数据,一个扫描、获取广播数据,它们没有服务,所以不需要安全模式。对于外围设备和中心设备,安全模式也是可选的,不是必须的,他取决于服务对安全性的需求。
5.6.4 GAP服务
GAP层定义了GAP服务,它的作用时用来确定设备的信息,GAP服务包含了5个特征:设备名称、外观特征、外围设备首选连接参数、中心设备地址解析和可解析私有地址。正如前文所述,广播者和观察者没有服务,当然不能包含GAP服务,外围设备和中心设备对GAP读物时强制包含的。
对于每一个GAP角色,当其包含GAP服务时,对于GAP的特征的需求也是不一样的,对于设备名称和外观特征,外围设备和中心设备时强制包含的,对于外围设备首选连接参数,外围设备时可选择是否包含的,儿中心设备时不能包含的,对于中心设备地址解析和可解析私有地址时有条件包含的。
Device Name特征
Device Name(设备名称)特征应包含UTF-8字符串的设备名称,一个设备只允许有一个设备名称,长度范围是0~248字节。如果设备时可发现的,设备名称特征应不需要认证或授权即可读,如果设备是不可发现的,设备名称在没有认证或授权时应不可读。
GAP设备名称特征操作需要掌握两个API函数:sd_ble_gap_device_name_set()和sd_ble_gap_device_name_get(),其中sd_ble_gap_device_name_set()用于设置GAP设备名称,sd_ble_gap_device_name_get()用于读取GAP设备名称,函数原型如下:
// 函数原型:uint32_t sd_ble_gap_device_name_set(ble_gap_conn_sec_mode_t const* p_write_perm,uint8_t const* p_dev_name,uint16_t len);// 函数功能://设置GAP设备的名称。//注意:如果设备名称时存储在Flash中的,不能修改,函数会返回“NRF_ERROR_FORBIDDEN”。// 参数://p_write_perm:指向RTC应用程序实例。//p_dev_name:指向UTF-8编码、非空终止的字符串、//len:指向的UTF-8编码、非空终止的字符串的长度,不能超过BLE_GAP_DEVNAME_MAX_LEN定义的长度。// 返回值:// NRF_SUCCESS:GAP设备名称和许可设置成功。// NRF_ERROR_INVALID_ADDR:提供了无效的指针。// NRF_ERROR_INVALID_PARAM:提供无效的参数。// NRF_ERROR_DATA_SIZE:提供了无效的数据长度。// NRF_ERROR_FORBIDDEN:设备名称不是可写的。
GAP设备名称设置函数中涉及到了非空终止的字符串的概念和计算机设备名称长度的方法,这是我们操作时需要注意的。
非空终止的字符串(NULL-terminated string):C语言中以’\0’作为字符串的结束标志,’\0’在ASCLL称为NUL,非空终止的字符串即不包含’\0’的字符串。
计算机设备名称字符串长度:计算长度时,需要获取的是不包含结束符’\0’的字符串长度。所以使用strlen()函数来计算字符串长度,strlen()计算字符串的长度时,返回的长度不包括结束符’\0’。
GAP设备名称设置示例:首先要定义设备的字符串,可以使用宏定义,也可以使用数组,同时要设置GAP的安全模式,这里设置的是无安全性,然后调用API函数sd_ble_gap_device_name_set()设置设备名称。
设置GAP设备名称示例:
#define DEVICE_NAME "BLE_Template" // 宏定义设备名称字符串ble_gap_conn_sec_mode_t sec_mode;// 设置GAP的安全模式BLE_GAP_CONN_MODE_SET_OPEN(&sec_mode);// 设置GAP设备名称err_code = sd_ble_gap_device_name_set(&sec_mode,(const uint8_t *)DEVICE_NAME,strlen(DEVICE_NAME););// 检查函数返回的错误代码APP_ERROR_CHECK(err_code);
获取设备名称函数原型:
// 函数原型:uint32_t sd_ble_gap_device_name_get(uint8_t* p_dev_name,uint16_t* p_len,);// 函数功能://读取GAP设备名称。//注意:如果获取的设备名称比提供的缓冲大,P_len返回的是实际获取的设备名称的长度,//而缓存中只能根据自身大小保存部分设备名称信息,应用程序可以根据这些信息来调整缓存的大小。// 参数://p_dev_name:指向用于存放获取的设备名称字符串的缓存,获取的设备名称时UTF-8编码、非空终止的字符串。//len:输入时提供用来保存设备名称的缓存的大小,输出时返回的是实际获取的设备名称的长度。// 返回值://NRF_SUCCESS:获取GAP设备名称成功。//NRF_ERROR_INVALID_ADDR:提供了无效的指针。//NRF_ERROR_DATA_SIZE:提供了无效的数据长度。
GAP设备名称读取示例:首先需要定义一个数组用来保存读取的设备名称,之后调用API函数sd_ble_gap_device_name_get()读取,读取时需要指定读取的长度。这里需要注意数组长度(name_buf)和指定读取的长度(name_len)的关系。调用sd_ble_gap_device_name_get()读取时,name_len向函数传递了应用程序提供的用来保存设备名称的缓存的长度,读取后,函数name_len返回的实际读取的设备名称的长度,这时候,如果读取的实际长度超过了name_len,多出的部分不会保存到name_buf数组。
读取GAP设备名称示例:
// 保存读取的GAP设备名称,注意数组的长度uint8_t name_buf[15];// 指定读取的长度uint16_t name_len = 15;// 读取GAP设备名称err_code = sd_ble_gap_device_name_get(name_buf, &name_len);// 读取成功,串口打印出设备名称if(err_code == NRF_SUCCESS){printf("%s\r\n", name_buf);}
Appearance特征
外观是一个16位的数值,外观由SIG定义,用来列举设备的外观样式,一个设备只允许有一个外观特征。
外观指示设备是普通手机、鼠标、键盘等等,通常被用来在用户界面的设备旁显示设备的小图标,通过图标向用户展示设备。
外观的编码可以在BLE工程文件的“ble_types.h”头文件中查到。
GAP外观特征操作需要掌握两个API函数:sd_ble_gap_appearance_set()和sd_ble_gap_appearance_get(),其中sd_ble_gap_appearance_set()用于设置GAP外观特征,sd_ble_gap_appearance_get()用于读取GAP外观特征,函数原型如下:
获取设备特征外观函数原型:
// 函数原型:uint32_t sd_ble_gap_appearance_set(uint16_t appearance);// 函数功能://设置GAP设备名称。// 参数://appearance:外观(16位),这只的数值参考ISG的编码文件。// 返回值://NRF_SUCCESS:GAP设备名称和许可设置成功。//NRF_ERROR_INVALID_PARAM:提供了无效的参数。
设置GAP外观特征示例:
// 设置GAP外观特征为手机err_code = sd_ble_gap_appearance_set(BLE_APPEARANCE_GENERIC_PHONE);// 检查函数返回的错误代码APP_ERROR_CHECK(err_code);
设置设备特征外观函数原型:
// 函数原型:uint32_t sd_ble_gap_appearance_set(uint16_t appearance);// 函数功能://读取GAP外观特征。// 参数://appearance:指向外观(16位)的变量。// 返回值://NRF_SUCCESS:读取外观特征成功。//NRF_ERROR_INVALID_ADDR:提供了无效的指针。
读取GAP外观特征示例:
// 用于保存读取的外观特征,因为外观特征死16位的,所以定义16位的变量uint16_t my_appearance;// 读取外观特征err_code = sd_ble_appearance_get(&appearance);// 读取成功后,打印外观编码if(err_code == NRF_SUCCESS){NRF_LOG_INFO("%d\r\n");}
PPCP特征
外围设备首选连接参数(The Peripheral Preferred Connection Parameters(PPCP))特征包含了外围设备倾向的连接参数,一个设备只允许有一个外围设备首选连接参数。外围设备首选连接参数必须可读,而且必须不可写。
为什么不适用固定的连接参数,而需要外围设备提供首选连接参数?因为外围设备和中心设备建立连接的时候,对于主机来说,并不一定能适应外围设备的连接参数,这时候,主机就可以以此为参考来调整连接参数,而不用去猜测这些参数。
外围设备首选连接包含4个项目,每个项目2个字节共8个字节,其数据格式如下表所示。
连接间隔:必须是1.25ms的倍数,范围是从最小值6(7.5ms)到最大值3200(4.0s)。(包含最小连接间隔和最大连接间隔)
从机延迟:这个参数描述了从机跳过连接事件的次数。这使外围设备具有一定的灵活性,如果它不具有任何数据传送,它可以选择跳过连接事件,并保持睡眠,从而提供了一些积蓄力量。这一决定取决于外围设备。
监督超时:这是两个成功的连接事件之间间隔的最大值。如果超过这个时间还未出现成功的连接事件,那么设备将会考虑失去连接,返回一个未连接状态。这个参数使用10ms的步进(10ms的倍数)。监督超时时间从最小10(100ms)到最大3200(32.0s)。同时超时时间必须大于有效连接时间。
我们可以看到连续间隔是一个范围而不是一个固定的数值,连接间隔为什么不用固定的值而用范围?
这是因为从设备连接间隔范围代表外围设备倾向的连接间隔,连接参数完全由主设备控制,范围的目的时为了能让主机去选择一个主机认为合适的,如果把最大值和最小值设置成相同的,那么主机就会只用你设定的这个值。如果十个范围的话,通常主机会选择接近最大值的值或者直接用最大值的值。
外围设备首选连接参数操作需要掌握两个API函数:sd_ble_gap_ppcp_set()和sd_ble_gap_ppcp_get(),其中sd_ble_gap_ppcp_set()用于设置外围首选连接参数,sd_ble_gap_ppcp_get()用于读取外围设备首选连接参数,函数原型如下:
设置外围首先连接参数函数原型:
// 函数原型:uint32_t sd_ble_gap_ppcp_set(ble_gap_conn_params_t const* p_conn_params);// 函数功能://设置外围设备首选连接参数。// 参数://p_conn_params:指向保存连接参数的结构体变量。// 返回值://NRF_SUCCESS:设置外围设备首选连接参数成功。//NRF_ERROR_INVALID_ADDR:提供了无效的指针。//NRF_ERROR_INVALID_PARAM:提供了无效的参数。
设置外围设备首选连接参数示例:
// 定义连接参数结构体变量ble_gap_conn_params_t gap_conn_params;// 设置首选连接参数,设置前先清零gap_conn_params变量memset(&gap_conn_params, 0, sizeof(gap_conn_params));gap_conn_params.min_conn_interval = MIN_CONN_INTERVAL; // 最小连接间隔gap_conn_params.max_conn_interval = MAX_CONN_INTERVAL; // 最大连接间隔gap_conn_params.slace_latency= SLAVE_LATENCY;// 从机延迟gap_conn_params.conn_sup_timeout = CONN_SUP_TIMEOUT; // 监督超时// 调用协议栈API配置GAP参数err_code = sd_ble_gap_ppcp_set(&gap_conn_params);// 检查函数返回的错误代码APP_ERROR_CHECK(err_code);
获得外围首先连接参数函数原型:
// 函数原型:uint32_t sd_ble_gap_ppcp_get(ble_gap_conn_params_t*p_conn_params);// 函数功能://获得外围设备首选连接参数。// 参数://p_conn_params:指向用来保存读取的外围设备首选连接参数的结构体变量。// 返回值://NRF_SUCCESS:获取外围设备首选连接参数成功。//NRF_ERROR_INVALID_ADDR:提供了无效的指针。
5.6.5 代码示例
GAP初始化示例:
static void gap_params_init(void){ret_code_t err_code;// 定义连接参数结构体变量ble_gap_conn_params_t gpa_conn_params;ble_gap_conn_sec_mode_t sec_mode;// 设置GAP的安全模式,即设置设备名称特征的写权限,这里设置的是安全模式1等级11,// 即无安全性BLE_GAP_CONN_SSEC_MODE_SET_OPEN(&sec_mode);// 设置GAP设备名称err_code = sd_ble_gap_device_name_set(&sec_mode,(const uint8_t *)DEVICE_NAME,strlen(DEVICE_NAME););// 检查函数返回的错误代码APP_ERROR_CHECK(err_code);// 如果需要设置外观特征,在这里使用如下的代码设置/*err_code = sd_ble_gap_appearance_set(BLE_APPEARANCE_);APP_ERROR_CHECK(err_code);*/// 设置首选连接参数,设置钱先清零gap_conn_paramsmemset(&gap_conn_params, 0, sizeof(gap_conn_params));gap_conn_params.min_conn_interval = MIN_CONN_INTERVAL; // 最小连接间隔gap_conn_params.max_conn_interval = MAX_CONN_INTERVAL; // 最大连接间隔gap_conn_params.slace_latency= SLAVE_LATENCY;// 从机延迟gap_conn_params.conn_sup_timeout = CONN_SUP_TIMEOUT; // 监督超时// 调用协议栈API配置GAP参数err_code = sd_ble_gap_ppcp_set(&gap_conn_params);APP_ERROR_CHECK(err_code);}
5.7 GATT初始化
GATT程序模块的作用时用于协商和跟踪GATT连接参数和更新数据长度,在BLE程序中,初始化话GATT程序模块只需要调用库函数nrf_ble_gatt_init()即可完成初始化,函数原型如下:
// 函数原型:ret_code_t nrf_ble_gatt_init(nrf_ble_gatt_t* p_gatt,nrf_ble_gatt_evt_handler_t evt_handler);// 函数功能://初始化GATT模块。// 参数://evt_handler:事件句柄。//p_gatt:指向GATT结构体变量。// 返回值://NRF_SUCCESS:操作成功。//NRF_ERROR_NULL:指针p_gatt为NULL。
初始化GATT示例:
NRF_BLE_GATT_DEF(m_gatt); // 定义GATT模块实例static void gatt_init(void){// 初始化GATT程序模块ret_code_t err_code = nrf_ble_gatt_init(&m_gatt, NULL);// 检查函数返回的错误代码APP_ERROR_CHECK(err_code);}
5.8 广播初始化
广播者对外广播的目的时为了向周边的设备显示自己的存在和“我是谁”,所以广播包中需要按照规定的格式写入数据。
为什么要按照规定的格式来写入数据?
因为不按照规定的格式来写入,广播保温就毫无规则可言,那么对端设备就无法理解广播中数据的意义,从而无法解析广播报文,那么BLE也就失去了它的一大优势“互操作性”。设备每次广播时,会在3个广播信道上发送相同的保温。这些报文被称为一个广播事件。除了定向报文以外,其他广播事件均可以选择“20ms~10.28s”不等的间隔。
广播初始化中,需要完成的是配置广播数据、广播模式和广播间隔,同时应用程序要提供广播事件处理函数用于接收广播事件。广播初始化通过调用库函数ble_advertising_init()完成,该函数原型如下:
// 函数原型:uint32_t ble_advertising_init(ble_advertising_t* const p_advertising,ble_advertising_init_t_const *const p_init);// 函数功能://广播初始化函数。该函数对所需的广播数据进行编码并将其传递给协议栈。// 参数://p_advertising:广播模块实例。必须由应用程序提供,广播木块实例在该函数中初始化//之后作为一个特定的模块实例使用。//p_init:指向广播初始化结构体。// 返回值://NRF_SUCCESS:广播初始化成功。//NRF_ERROR_INVALID_PARAM:p_init指向的广播初始化结构体中包含了无效的广播配置数据。
5.8.1 配置广播数据和模式
广播数据广播初始化中需要配置广播包中包含那些数据,一般情况下,建议广播包中至少包含:设备名称、Flags、外观和首要服务的UUID。对于BLE工程模板,因为仅有两个必须的服务(GAP服务和GATT服务),没有其他的服务,所以这里我们在广播数据中只加入设备名称、外观和Flags。