LED Button 应用示例是为了让你学习如何在nRF51822上开发BLE应用,它是一个简单的演示通过BLE的指示功能进行通信的BLE应用。当它运行时,你可以通过集中器触发nRF51822上LED的输出,并且当在nRF51822上的按键被按下时集中器将会收到一个通知。
这个应用通过一个服务被建立,这个服务包括2个特性:LED特性和按键特性。LED特性:通过没有回应的写远程操作LED的亮灭。按键特性:当按键被按下或释放时,将会发送一个通知。
下面的章节将对这个应用是如何工作的进行一个简单的介绍,以帮助你理解和使用这些代码。
这个示例代码分为3个文件:
l main.c
l ble_lbs.c
l ble_lbs.h
这个结构和SDK中其他的示例是一样的,在main.c中实现了应用的行为,分离的服务文件(ble_lbs.c,ble_lbs.h)实现了服务本身和它的行为。所有的输入输出的处理都是在应用层中进行的。
一个运行在nRF51822上的应用能够与以下几个部分进行交互:
l 硬件寄存器
寄存器在nrf51.h中定义,用到的常量在nrf51_bitfields.h中定义。在LED BUTTON应用中没有关于使用它们的示例。
l SDK模块
SDK模块包含了所有BLE开发的所有文件,从封装了硬件寄存器的基本头文件如nrf_gpio.h,到提供给应用层的复杂的功能函数如app_timer或者ble_conn_params。
l 协议栈Softdevice 函数
用来配置或触发Softdevice中的行为,所有Softdevice函数的前缀都是以“sd_”开头。
在nRF51822上运行一个标准的BLE应用的基本流程是:初始化所有需要的部分,开始广播,进入省电模式,等待BLE事件发生。当接收到一个事件发生时,它将会被发送到BLE的服务和模块中,这个事件是但不仅限于以下一种事件:
l 当一个对等设备连接到nRF51822时
l 当一个对等设备写入到一个特性时
l 广播超时事件通知
这种流程让应用变得模块化,一个服务通常可以通过初始化加入一个应用之中,并且保证当事件发生时事件的句柄被调用。
建议在你开始查看示例代码之前先对它进行编译。编译之后,你可以在任何函数、变量、类型、定义上点击鼠标右键,通过选择Go To Definition Of(跳转到定义体)或者Go To Reference To(跳转到引用体)选项跳转到目标定义所在的地方。
Go To Definition Of跳转到真正的实现方法(即源代码文件),Go To Reference To跳转到它的头文件内的声明中,这意味着你不能跳转到SoftDevice 中函数的定义体中,因为这些函数没有源码。但是,你可以跳转到他们的引用体,那里可以告诉你有关如何使用的方法说明。你可以使用这个有用的功能熟悉API函数和示例工程。
这个应用笔记介绍了如何建立一个LED Button应用的步骤,另外,完整的示例代码在GitHub上的Git资源库中提供,你可以看到代码的历史版本以及它是如何被开发的。每一部分都有它自己的标签,在本文的末尾将告诉你如何查阅这些代码。
在GitHub上可以找到本工程的代码:https://github.com/NordicSemiconductor/nrf51-ble-app-lbs
本应用例程需要使用nRF51822 Evaluation Kit开发板,当然,可以经过适当的修改也可以在Development Kit.开发板上运行。
因为nRF51822 Evaluation Kit开发板上已经有板载SEGGER芯片,因此你可以使用USB线就能直接开始工作。
有很多模式样板代码需要用于开始创建一个应用和服务,所以第一步就是从SDK中复制代码:
1. 进入Board\nrf6310\s110\ble_app_template文件夹
2. 把这个文件夹复制到Board\pca10001\s110\,并重新命名为:ble_app_lbs
3. 进入文件夹ble_app_lbs中的arm文件夹,把工程文件名从ble_app_template 改为 ble_app_lbs。
SDK没有一个服务的模板,但是有一个现成的电池服务,这个服务是一个简单的首要服务,非常适合开始创建一个服务的模板。因此需要进行如下步骤:
1. 从Source/ble/ble_services复制ble_bas.c到Board/pca10001/ble/ble_app_lbs/
2. 从Include/ble/ble_services复制ble_bas.h到Board/pca10001/ble/ble_app_lbs/
3. 把ble_bas.c重命名为ble_lbs.c,把ble_bas.h重命名为ble_lbs.h
4. 在Keil左边的工程窗口中双击Services文件夹,选择添加ble_lbs.c文件。这样就把ble_lbs.c文件添加到了你的keil工程中。
因为这个是一个定制的服务,最好把它放在应用层文件夹中,以代替放在SDK 的服务文件夹中。
这个服务采用通用的方式实现,因此可以很容易重用于其他应用。它能够使应用程序通过初始化就能使用这个服务、处理事件、提供输入输出的实现。实现其他预定义服务的方法也是类似的。
ble_lbs.h头文件实现了各种数据结构、应用需要实现的事件句柄和以下3个API函数:
uint32_t ble_bas_init(ble_bas_t * p_bas, const ble_bas_init_t * p_bas_init); void ble_bas_on_ble_evt(ble_bas_t * p_bas, ble_evt_t * p_ble_evt); uint32_t ble_bas_battery_level_update(ble_bas_t * p_bas, uint8_t battery_level); |
注意:本文档中代码片段的注释已经被去掉
在上面的代码中,ble_bas_t用于引用这个服务实例,在后面还会用到。而ble_bas_init_t用于初始化参数,后面不会再用到。所有的API函数使用一个指向服务实例的指针作为第一个输入参数。
LED Button服务的API 函数也是同样的设计,步骤如下:
uint32_t ble_lbs_init(ble_lbs_t * p_lbs, const ble_lbs_init_t * p_lbs_init); void ble_lbs_on_ble_evt(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt); uint32_t ble_lbs_on_button_change(ble_lbs_t * p_lbs, uint8_t button_state); |
这里有2个数据结构需要实现:ble_lbs_t 和 ble_lbs_init_t。
在第25页4.4.1节“API设计”中,有一些用到的数据结构还没有定义:ble_lbs_t 和 ble_lbs_init_t.,我们可以基于电池服务的数据结构体实现类似的结构体,电池服务的数据结构体如下:
typedef struct { ble_bas_evt_handler_t evt_handler; bool support_notification; ble_report_ref_t * p_report_ref; uint8_t initial_batt_level; ble_cccd_security_mode_t battery_level_char_attr_md; ble_gap_conn_sec_mode_t battery_level_report_read_perm; } ble_bas_init_t; |
typedef struct ble_bas_s { ble_bas_evt_handler_t evt_handler; uint16_t service_handle; ble_gatts_char_handles_t battery_level_handles; uint16_t report_ref_handle; uint8_t battery_level_last; uint16_t conn_handle; bool is_notification_supported; } ble_bas_t; |
以上的代码中的初始化结构体包含服务的事件句柄,一些选项参数,初始化值,安全模式,服务结构体包含服务的声明,如句柄,当前电量值,通知功能是否打开等。
电池服务使用一个通用的事件句柄让应用程序知道什么时候开始和停止读取电池电量。LED Button 服务不依赖于任何启动或停止,所以只使用一个函数作为回调函数,当LED特性被写入时被调用。
这个句柄是初始化中唯一有效的参数,也是初始化结构体中唯一的成员。
typedef struct { ble_lbs_led_write_handler_t led_write_handler; } ble_lbs_init_t; |
在这个结构体中,函数类型的定义如下(在头文件中必须在ble_lbs_init_t定义之前添加,代替之前已经存在的事件句柄定义):
typedef void (*ble_lbs_led_write_handler_t) (ble_lbs_t * p_lbs, uint8_t new_state); |
然而,下面的参数参数还需要定义:
l 服务的句柄
l 特性的句柄
l 连接的句柄
l UUID类型
l LED写的回调函数
服务结构体定义如下:
typedef struct ble_lbs_s { uint16_t service_handle; ble_gatts_char_handles_t led_char_handles; ble_gatts_char_handles_t button_char_handles; uint8_t uuid_type; uint16_t conn_handle; } ble_lbs_t; |
可以删除ble_lbs.h文件中有关电池服务事件的定义。
进入服务初始化函数services_init(),函数ble_lbs_init()被调用。参数你不需要改变。
p_lbs->led_write_handler = p_lbs_init->led_write_handler; |
UUID需要重新设置,因为本服务中将要使用一个定制(私有)的UUID,以代替蓝牙技术联盟所定义的UUID。
首先,先定义一个基本UUID,一种方式是采用nRFgo Studio来生成:
这就产生了一个随机的UUID,可以用于你的定制服务中。
新产生的基本UUID必须以数组的形式包含在源代码中,但是只需要在一个地方用到:
#define LBS_UUID_BASE { 0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF,0x12, 0x12, 0x00, 0x00, 0x00, 0x00 } #define LBS_UUID_SERVICE 0x1523 #define LBS_UUID_LED_CHAR 0x1525 #define LBS_UUID_BUTTON_CHAR 0x1524 |
在服务初始化中:
ble_uuid128_t base_uuid = LBS_UUID_BASE; err_code = sd_ble_uuid_vs_add(&base_uuid, &p_lbs->uuid_type); if (err_code != NRF_SUCCESS) { return err_code; } |
以上代码段为加入一个定制的基本UUID到协议栈中,并且保存了返回的UUID类型。
ble_uuid.type = p_lbs->uuid_type; ble_uuid.uuid = LBS_UUID_SERVICE; err_code = sd_ble_gatts_service_add( BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_lbs->service_handle); if (err_code != NRF_SUCCESS) { return err_code; } |
以上代码只是添加了一个空的服务,所以还必须添加特性到服务中,下面的章节将介绍如何添加特性。
本服务将要实现2个特性,一个是控制LED的状态,一个是反馈按键的状态。这两个功能需要创建并增加2个特性到ble_lbs.c中实现,我们先从按键特性说起。
按键特性在有按键状态改变的时候有通知事件,同时也允许对等设备读取这个按键状态。这非常类似于在电池服务中电量特性的特征,因此你可以采用以下方式实现:
在电池服务的初始化中有一个标志设置了CCCD的安全模式,它存储在ble_gap_conn_sec_mode_t结构体中,这个结构体使用在头文件ble_gap.h中定义的宏BLE_GAP_CONN_SEC_MODE来设置,根据不同的安全等级定义了不同的宏,你可以根据属性的需要进行选择。
1. 使用宏BLE_GAP_CONN_SEC_MODE_SET_OPEN把CCCD设置成对任何连接和加密都是可读可写的模式。
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm); ... memset(&attr_md, 0, sizeof(attr_md)); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm); BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&attr_md.write_perm); |
ble_uuid.type = p_lbs->uuid_type; ble_uuid.uuid = LBS_UUID_BUTTON_CHAR; |
return ble_gatts_characteristic_add( p_lbs->service_handle, &char_md, &attr_char_value, &p_lbs->button_char_handles ); |
删除#endif以上的内容后,你就可以编译工程了,根据编译的警告删除没有用到的变量(initial_battery_level, encoded_report_ref, init_len, err_code)。
LED状态特性需要能够可读可写,但没有任何通知功能:
char_md.char_props.write = 1; |
ble_uuid.type = p_lbs->uuid_type; ble_uuid.uuid = LBS_UUID_LED_CHAR; |
保存返回的变量led_char_handles(LED特性的句柄),代替button_char_handles的位置,最后调用的函数如下:
return ble_gatts_characteristic_add( p_lbs->service_handle, &char_md, &attr_char_value, &p_lbs->led_char_handles ); |
编译之后,你会发现有一些参数没有使用,删除它们。
创建了增加特性的函数之后,你可以在服务初始化的末尾调用它们,如下面的例子:
// Add characteristics err_code = button_char_add(p_lbs, p_lbs_init); if (err_code != NRF_SUCCESS) { return err_code; } err_code = led_char_add(p_lbs, p_lbs_init); if (err_code != NRF_SUCCESS) { return err_code; } return NRF_SUCCESS; |
因为任何错误都会导致函数提前退出,因此当你到达函数的末尾时可以认为初始化成功了。
如果你想现在就进行测试,你可以跳到第37页第4.5.1节“为evaluation kit开发板修改模块”和第40页第4.5.3节“包含服务”,完成之后,最后的测试在第47页第5章“应用测试”中介绍。测试时你会发现,你可以连接到这个设备并且可以发现所有的服务,但是其他的功能不能工作,因此还需要在这个服务中实现处理协议栈事件和按键处理。
当协议栈需要通知应用程序一些有关它的事情的时候,协议栈事件就发生了,例如当写入特性或是描述符时。对应于本应用,你需要写入LED特性,为了让通知功能更好地工作,你需要保存连接句柄,通过这个句柄,你可以在连接事件和断开事件中实现某些操作。
作为API的一部分,你可以定义一个函数ble_lbs_on_ble_evt用来处理协议栈事件,可以使用简单的switch-case语句通过返回事件头部的id号来区分不同的事件,并进行不同的处理。
电池服务已经保存了连接句柄,但没有改变之前采用Find and Replace来查找时需要注意一些。
现有的事件句柄监听CCCD写的操作并把它发送应用层的电池服务的事件句柄,但是,在本应用中,不需要这样。
原有的代码中实现了发送通知的方法,但是如果CCCD没有使能,sd_ble_gatts_hvx()(通知或者指示回调函数)将不允许发送通知,因此你不需要在应用层中进行检测,而是交给协议栈SoftDevice中去检测。
在on_write()函数中,你可以删除与通知功能相关的代码。
当LED特性被写入的时候,你添加到数据结构的函数指针将会通知到应用层,你可以在on_write()函数中实现这样的功能。
当接收到一个写事件时,验证这个写事件是发生在对应的特性上是一个基本的任务,包括验证数据的长度,回调函数是否已设置。如果所有这些都是正确的,则回调函数将会调用,并且把已经写入的值作为输入参数。因此,on_write()函数的内容将会是这样:
ble_gatts_evt_write_t * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write; if ((p_evt_write->handle == p_lbs->led_char_handles.value_handle) && (p_evt_write->len == 1) && (p_lbs->led_write_handler != NULL)) { p_lbs->led_write_handler(p_lbs, p_evt_write->data[0]); } |
真正触发LED的操作在于应用层,这样的设计让服务很容易重用,虽然这只是针对于LED和按键的服务。
你已经添加了一个回调API函数让服务知道按键何时被按下,但还没有全部实现,因此你需要从头文件中通过复制来添加,在处理按键按下的时候,你需要给对等设备发送一个通知以告知它新的按键状态。协议栈SoftDevice API函数sd_ble_gatts_hvx来完成这个事情,它需要连接句柄和结构体ble_gatts_hvx_params_t作为输入参数,它管理一个值被通知的整个过程。
在结构体ble_gatts_hvx_params_t中,你需要设置为通知模式还是指示模式,用哪一个属性的句柄用来进行通知(本例中使用值的句柄),新的值以及值的长度。方法如下:
uint32_t ble_lbs_on_button_change(ble_lbs_t * p_lbs, uint8_t button_state) { ble_gatts_hvx_params_t params; uint16_t len = sizeof(button_state); memset(¶ms, 0, sizeof(params)); params.type = BLE_GATT_HVX_NOTIFICATION; params.handle = p_lbs->button_char_handles.value_handle; params.p_data = &button_state; params.p_len = &len; return sd_ble_gatts_hvx(p_lbs->conn_handle, ¶ms); } |
也可以使用sd_ble_gatts_value_set()函数一次性设置特性的值,当通知的时候调用sd_ble_gatts_hvx()不需要设置值和值的长度。但是,没必要使用sd_ble_gatts_hvx()做任何事情,它不过类似于一个清洁工的角色。使用函数sd_ble_gatts_value_set()更新一个可读(但不能通知)的值,因为这个函数不通过空中发送数据包。
按照本服务的实现方法,本例子可以应用在其他应用中。
需要使用模板进行一些修改以适应于evaluation kit开发板,以代替Development Kit开发板。
首先,你需要更改定义板子的宏定义:
1. 打开工程文件,进入到“Target options”标签和“C/C++”标签。
2. 把BOARD_NRF6310更改为BOARD_PCA10001,如下图所示:
注意:如果你用development kit开发板代替evaluation kit开发板,跳到步骤4和步骤5,并且把ble_app template拷贝到Board\nrf6310\s110\ble_app_lbs下,你便建立了一个工程。
你需要在main.c中删除一些引脚定义,因为在evaluation kit开发板上,没有一个单独的LED用于言断。
#define LEDBUTTON_LED_PIN_NO LED_0 |
nrf_gpio_cfg_output(LEDBUTTON_LED_PIN_NO); |
void app_error_handler(uint32_t error_code, uint32_t line_num, const uint8_t * p_file_name) { // [Comment removed from snippet for brevity] ble_debug_assert_handler(error_code, line_num, p_file_name); // On assert, the system can only recover with a reset. //NVIC_SystemReset(); } |
当你运行程序在调试模式下的时候,如果出现错误你可以停止cpu,并能够发现文件中哪一行发生了错误。你可用SoftDevice中的APP_ERROR_CHECK()宏来检测返回的错误代码得到有关错误的信息。如图5所示的例子.
在实际的产品中,通常使用复位来恢复错误,但在开发中,日记更重要。
建议在工程中更改蓝牙设备的名字,可以通过改变main.c中的宏DEVICE_NAME来实现,例如可以改成“LedButtonDemo”。
SDK提供了调度模块,这个模块提供了把事件处理和中断处理转移到main函数中进行的机制,它保证了所有的中断处理都能快速被执行。
在开始使用的模板中,默认使能了调度功能。如果你不想使用它,你可以删除它的初始化和main循环中对它的调用(scheduler_init(), app_sched_execute()),设置SDK的模块初始化函数中相关的标识使用调度的参数为假(softdevice_handler, app_timer,app_button)。
关于调度的更多详情,请查阅nRF51 SDK文档。
为了使用你创建的服务,你需要在模板中添加一些代码。
在mian.c文件中,必须出现调用services_init()函数来初始化LED Button 服务:
#include "ble_lbs.h" |
2. 如果没有添加源文件,则添加源文件到工程中:在工程窗口的左边在Services文件上点击右键,单击Add file,选择ble_lbs.c文件。
3. 在main.c中添加服务的数据结构作为全局静态变量:
static ble_lbs_t m_lbs; |
注意:事件通过在main.c中使用静态变量的方式被保存,m_lbs作为指针指向的变量经常会出现,指向它的指针为p_lbs。
static void services_init(void) { uint32_t err_code; ble_lbs_ init_t init; init.led_write_handler = led_write_handler; err_code = ble_lbs_init(&m_lbs, &init); APP_ERROR_CHECK(err_code); } |
在第35页4.4.4.3节“处理LED特性写”中,我们在服务结构体中设置了led_write_handler,当LED特性被写入的时候将会调用。这个回调函数通过上面的初始化结构体被设置,但这个回调函数还没有实现。
Static void led_write_handler(ble_lbs_t * p_lbs, uint8_t led_state) { if (led_state) { nrf_gpio_pin_set(LEDBUTTON_LED_PIN_NO); } else { nrf_gpio_pin_clear(LEDBUTTON_LED_PIN_NO); } } |
static void ble_evt_dispatch(ble_evt_t * p_ble_evt) { on_ble_evt(p_ble_evt); ble_conn_params_on_ble_evt(p_ble_evt); ble_lbs_on_ble_evt(&m_lbs, p_ble_evt); } |
现在你可以进行测试了,见第47页第5章“应用测试”,使用Master Control Panel,你可以写“1”到LED特性中来点亮LED。
为了完成本应用,你需要定义如何处理按键按下,你可以使用SDK提供的app_button模块,这个模块将会提供当按键按下和释放时的一个回调函数。
在按键初始化buttons_init()里,设置你需要的按键,在这个示例中使用了evaluation kit开发板上的button 1。
#define LEDBUTTON_BUTTON_PIN_NO BUTTON_1 |
static app_button_cfg_t buttons[] = { {WAKEUP_BUTTON_PIN, false, NRF_GPIO_PIN_PULLUP, NULL}, {LEDBUTTON_BUTTON_PIN_NO, false, NRF_GPIO_PIN_PULLUP, button_event_handler}, }; APP_BUTTON_INIT( buttons, sizeof(buttons) / sizeof(buttons[0]), BUTTON_DETECTION_DELAY, true); |
在evaluation kit开发板上的按键是低电平有效,所以第2个参数为false,但它没有外部上拉电阻,因此你需要使用NRF_GPIO_PIN_PULLUP来使能内部上拉电阻。唤醒按键wakeup_button没有声明,所以它的回调函数设置为NULL。完成之后,你就已经完成按键模块的初始化了。
static void button_event_handler(uint8_t pin_no, uint8_t button action) { uint32_t err_code; switch(pin_no) { case LEDBUTTON_BUTTON_PIN_NO: err_code = ble_lbs_on_button_change(&m_lbs, button_action); if (err_code != NRF_SUCCESS && err_code != BLE_ERROR_INVALID_CONN_HANDLE && err_code != NRF_ERROR_INVALID_STATE) { APP_ERROR_CHECK(err_code); } break; default: APP_ERROR_HANDLER(pin_no); break; } } |
在上面的代码中,我们忽略可能客户端CCCD还没有被写入,或者我们没有正确连接所带来的错误。
为了添加已经定义的函数,你需要确定按键模块已经被使能。默认情况下,应用模板建议连接事件和断开连接事件中来完成使能和禁止,这里我们也使用这种方式。取消对app_button_enable() 和 app_button_disable()的注释:
switch (p_ble_evt->header.evt_id) { case BLE_GAP_EVT_CONNECTED: … /* … */ err_code = app_button_enable(); break; case BLE_GAP_EVT_DISCONNECTED: … /* … */ err_code = app_button_disable(); if (err_code == NRF_SUCCESS) { advertising_start(); } break; |
现在你可以进行测试了,见第47页第5章“应用测试”的描述,然而为了让集中器在扫描的时候能够更容易区分不同的设备,有必要在广播数据包中加入本服务的UUID。
4.5.6 加入本服务的UUID到广播数据包中
在广播数据包中包含服务UUID,可以使集中器利用这个信息决定是否进行连接。在第8页第2.1.2节“广播”中所提到,一个广播数据包最多可携带31字节,如果需要更多的数据需要传输,可以使用扫描回应发送。
你需要增加一个定制的16位的UUID到扫描回应数据包中,因为广播数据包已经没有可用的空间了。
广播数据的设置在main.c的advertising_init()中,设置广播数据结构体,并调用ble_advdata_set()来设置,它使用2个相同的数据类型的参数,一个是广播数据包,一个是扫描回应数据包。你必须添加一个数据结构作为扫描回应的参数。
服务UUID设置为LBS_UUID_SERVICE,类型使用结构体ble_lbs_t中的uuid_type,广播数据包的初始化如下:
static void advertising_init(void) { uint32_t err_code; ble_advdata_t advdata; ble_advdata_t scanrsp; uint8_t flags = BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE;
// YOUR_JOB: Use UUIDs for service(s) used in your application. ble_uuid_t adv_uuids[] = {{LBS_UUID_SERVICE, m_lbs.uuid_type}};
// Build and set advertising data memset(&advdata, 0, sizeof(advdata));
advdata.name_type = BLE_ADVDATA_FULL_NAME; advdata.include_appearance = true; advdata.flags.size = sizeof(flags); advdata.flags.p_data = &flags;
memset(&scanrsp, 0, sizeof(scanrsp)); scanrsp.uuids_complete.uuid_cnt = sizeof(adv_uuids) / sizeof(adv_uuids[0]); scanrsp.uuids_complete.p_uuids = adv_uuids;
err_code = ble_advdata_set(&advdata, &scanrsp); APP_ERROR_CHECK(err_code); } |
因为m_lbs结构体的uuid_type在这里被使用,所以确保它在services_init()中已经被设置,并保证它在advertising_init()之前被调用,在main中:
int main(void) { … services_init(); advertising_init(); … |
到此,这个应用已经建立完成。