NRF 52832 ble_app_blinky 官方示例 part1

ble_app_blinky 是Nordic 为BLE从设备设计的官方示例,主要内容为用户自己设计service。
工程目录位于
\DeviceDownload\nRF5SDK153059ac345\nRF5_SDK_15.3.0_59ac345\examples\ble_peripheral\ble_app_blinky
工程目录如下
在这里插入图片描述

写在添加Service前的内容
以下内容摘抄博客园iini大佬的博客。有兴趣的搜索原文查看。
蓝牙软件的结构图,基本上任何蓝牙软件都是使用这个架构。
在这里插入图片描述
Application应用层:
我们开发一个蓝牙产品或者实现某种功能的设备时,开发主要部分位于应用层。
Profiles:初略理解为包含一个或者多个Service的集合。其中有蓝牙官方组织规定的Profiles,用户也可以自己设计属于自己的
专属Profiles。发布的GATT规范列表,包括警告通知(alert notificantion),血压测量(blood pressure),心率(heart rate),电池(battery)等等

非标准蓝牙任务规范profile,也称为私有任务。是供应商自定义的任务,在蓝牙SIG小组内未定义的任务规范,比如本例所谈的蓝牙LED灯任务。

协议栈:
通用访问规范(Generic Access Profile,GAP):
GAP是应用层能够直接访问BLE协议栈的最底层,它包括管理广播和连接事件的有关参数。GAP模块代表了所有蓝牙设备的共用基础功能,如传输,协议或者应用规范所使用的模式和访问过程。GAP的服务包括设备发现,连接方式,安全,认证,关联模型和服务发现等。

通用属性配置文件(Generic Attribute profile,GATT)
GATT层是传输真正数据所在的层。包括了一个数据传输和存储框架以及其基本操作。
GTTA定义了两类角色:
服务器(server)和客户端(client)

BLE采用了client/server (C/S) 架构来进行数据交互,C/S架构是一种非常常见的架构,在我们身边随处可见,比如我们经常用到的浏览器和服务器也是一种C/S架构,这其中浏览器是客户端client,服务器是服务端server,server比如淘宝服务器,提供商品信息,广告,社交等服务,而浏览器就是客户端,比如微软的IE,就可以用来请求这些服务,并使用server提供的服务。BLE与此类似,一般而言设备提供服务,因此设备是server,手机使用设备提供的服务,因此手机是client。比如蓝牙体温计,它可以提供 “体温” 数据服务,因此是一个server,而手机则可以请求“体温”数据以显示在手机上,因此手机是一个client。

服务是以数据为载体的,所以说server提供服务其实就是提供各种有价值的数据。
在这里插入图片描述
客户端要访问某一个数据,就发送一个request/请求(其实就是一条命令或者PDU),服务端再把该数据返回给客户端(一条response/响应命令或者PDU),这就是C/S架构。提供数据的一方为服务端,请求数据的一段为客户端。这里和蓝牙的主机从机是不同的概念。

蓝牙本质是一个通讯协议,主要目的是用来传输数据。所以一切围绕数据展开,是我认为的BLE学习方法。

数据是什么,单纯来说就是一些二进制比特位。比如蓝牙耳机的电量值,显然是手机作为客户端向蓝牙耳机服务端发送一个请求,要求耳机告诉手机你的电量值是多少。那么客户端,服务端,数据,请求,应答。都出现了。

把一些表示有逻辑意义的信息字节组织起来,就会变成一条条的数据,这样的数据称为 attribute,把attribute翻译成数据条目。
对于服务端而言,他提供的就时很多这样的数据条目,可以像表格一样一条一条的罗列出来。
假设一个班级有20个小朋友,每个人的年龄是服务端提供的数据。所以服务端可以提供20条数据。然而每个年龄如何喝小朋友的名字挂钩,这需要一些额外的信息。所以信息无法以非常单纯的数字形式存在。或包含很多用于表示信息类型,索引,权限的额外数据。
在这里插入图片描述
Attribute handle,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。就像我们打开excle 最前面有个表格的行号,只要我们知道行号,就能找到这条数据,逻辑上的索引值。

Attribute type,Attribute类型,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。UUID有点类似身份证,每个人都有一个唯一身份证号。

Attribute value,就是数据真正的值,0到512字节长。
Attribute permissions,Attribute的权限属性,Open直接可以读或者写,No Access,禁止读或者写,Authentication,需要配对才能读或者写等等

一个应用包含很多数据条目,所有的attribute组成一个database,也称为attribute table,设备支持的服务不同,attribute table就不同。一个attribute table示例如下所示:
在这里插入图片描述
HANDLE 是一个自然增长的序列,在添加service的时候按照初始化顺序,一个一个添加到协议栈中。
TYPE:是一些UUID,有些SIG蓝牙官方组织会定义好,有些事用户自己定义。

ATT,全称attribute protocol(数据交互协议)。说到底,ATT是由一群ATT命令组成,就是上文所述的request(请求)和response(响应)命令,ATT也是蓝牙空口包中的最上层,也就是说,ATT就是大家对蓝牙数据包进行分析的最多的地方。

ATT命令,正式称谓ATT PDU(Protocol Data Unit,协议数据交互单元)包括4类:读,写,notify(通知)和indicate(指示)。这些命令又可以分成两种:如果它需要response,那么会在相应命令后面加上request;相反,如果它只需要ACK而不需要response,那么它的后面就不会带request。这里要特别强调一点,ATT所有命令都是“必达”的,也就是说每个命令发出去之后,会立马等ACK信息,如果收到了ACK包,发送方认为命令完成;否则发送方会一直重传该命令直到超时导致BLE连接断开。换句话说,只要你的BLE连接没有断开,那么你之前发送的数据包,不管它是用什么ATT PDU来发送的,它肯定被对方收到了。我估计很多人对此会产生疑问,因为他们经常碰到丢包的情况,其实大家经常碰到的“丢包”,不是空中把包丢了或者包在空中被干扰了,而是大家发送的代码写得有问题,导致你要发送的包没有被安全送达到协议栈射频FIFO中,从而出现所谓的“丢包”。以后大家碰到丢包情况,请先检查你的代码,保证你的数据包正确完整安全地送达到协议栈射频FIFO中,只要数据包放到了协议栈射频FIFO中,蓝牙协议栈就能保证该数据包“必达”对方。既然每个ATT命令都必达对方,那么还需要request类型的命令做什么?如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。换句话说,不带request的命令,虽然协议栈底层确保了该命令必达对方,但应用层其实并不知道(私有实现方法除外),当你需要实现一个通信序列的时候,这种命令就显得不足了。而采用request/response方式的命令对,request命令发出去之后,必须等到相应的response命令回复才能进行下一步操作,比如发送下一个request命令,这样应用层可以严格按照规定逻辑执行一系列的操作,这个在很多应用场合是非常有用的。Request/response命令对还有一个副作用:大大降低通信的有效速率(吞吐率),因为request/response命令必须在不同的连接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就导一个数据包的发送需要跨两个连接间隔甚至更多。而不带request后缀的ATT命令就没有这个限制,ACK可以在同一个连接间隔中回复,这样一个连接间隔中可以同时发出多个数据包,这样将大大提高通信速率。

不带request的命令只有2个:write command和notification,其余的命令都是带request:所有 read命令,所有write 命令,find命令以及indicate命令,完整的ATT命令(ATT PDU)列表如下所示:
在这里插入图片描述
在这里插入图片描述

GATT,全称generic attribute profile,对数据进行一般化/抽象化的子规范,说白了就是对数据进行逻辑化表达的规定。前面说过了,attribute是一条一条的数据,那么这条数据表示什么?如何对其进行分类?这就是GATT要做的事情,GATT将对数据赋予含义,并呈现一定的逻辑结构。Service和characteristic就是GATT层定义的,前面说过,server端提供服务,服务就是数据,而数据就是一条一条的attribute,而service和characteristic就是数据的逻辑呈现,或者说用户能看到的数据最终都转化为service和characteristic。比如,一个数据 “37” ,有可能是说体温“37度”,也有可能是说心率“37次”或者湿度“37%”,因此必须对数据进行分类和定义。

在这里插入图片描述
那service/characteristic和attribute之间到底是一个怎么样的关系?如前所述,service/characteristic是attribute的逻辑表现形式,而attribute是service/characteristic具体实现方式。尤其要注意的是,一条characteristic不是对应一条attribute,而是由多条attribute组成。虽然一个数据最有价值的部分是它的值(value),但是仅有value是不够的,比如27,到底是表示27°温度还是27%湿度;如果表示的是温度,那么它的单位是摄氏度还是华氏度,同时每个数据还有相应的读写属性以及权限属性,

因此一个characteristic包含三种类型的数据条目(attribute):characteristic声明条目(declaration attribute),characteristic值条目(value attribute)以及characteristic描述符条目(descriptor attribute)(一个characteristic可以有多个描述符条目)
在这里插入图片描述

由于一个service可以包含多个characteristic,characteristic declaration就是每个characteristic的分界符,解析时一旦遇到characteristic declaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value attribute的读写属性等。Characteristic value就是数据的值了,它也是一个单独的attribute,这个比较好理解就不再说了。Characteristic descriptor就是数据的额外信息,比如温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。Descriptor属于可选条目,也就是说,一个characteristic可以不包含任何一条descriptor。这里着重提一种特殊的descriptor:CCCD。一般而言,都是client来访问server的characteristic,即通过ATT读或者写PDU访问相关数据。如果server想直接把自己的characteristic的值告诉client,就需要通过notify或者indicate PDU,跟其他PDU相比,这2个PDU是由server自己决定什么时候开始传送,而不是被动接受client的命令请求。但client毕竟是客户啊,它得有自主权,所以引入了一个CCCD来帮助client控制server的行为。client可以通过禁止CCCD以不接收 notify或者indicate命令,client也可以通过使能CCCD以允许notify或者indicate命令。重新总结一下,当CCCD使能的情况下,server可以随时notify或者indicate数据给client;当CCCD禁止的时候,哪怕server有数据,它也不能notify或者indicate给client。这里强调一下,当characteristic具有notify或者indicate操作功能时,蓝牙规范要求必须为其添加CCCD attribute
在这里插入图片描述
所谓开发蓝牙应用程序,其实就是开发service和characteristic。

下载ble_app_blinky 历程到NRF52832 DK开发板上,使用NRF CONNECT 连接开发板。
在这里插入图片描述
在这里插入图片描述

可以看到"PRIMARY SERVICE"的字符其本身是一个条目,描述的是服务本身。
任何显示出来的数据都是来自服务端,所以看到的字符都是数据,都是数据条目。
BUTTON 表示 characteristic ,包含好几个数据条目 ,“BUTTON” 字符本身就是描述符。 type wei UUID.权限是NOTIFY,READ。descriptor 是可以配置的,说明可以使能CCCD。

本节最后,本节需要深刻理解数据条目Attribute,Characteristic,Service。
蓝牙开发本质是在方案商的SDK下,进行应用开发。一般的公司不会进行蓝牙协议栈的开发。

##第二部分

ble_app_blinky 官方示例 的开端,也是离不开数据。
那么顺着数据的思路在分析一下

首先一个application 就是提供一些数据给客户端,所以把这个历程所有的数据归类到一个服务中
这个服务有两个逻辑功能,一个是LED和按键。按照逻辑上可以把服务中所有的数据分为两类也就是说有两个Characteristic就可以满足需求

Characteristic 1:LED 能够进行数据的写入,服务端收到数据执行相应的LED开启关闭任务,视乎有个回调函数。
Value:LED state

Characteristic 2:BUTTON 主动告知客户端,开发板上按键的状态
Value:Press State

这样我们把这个application所有的数据已经用数据条目attribute的方式理了一次,对照一下下面的表格,看看还缺少什么

在这里插入图片描述
显然缺少了 attribute Type ----自己定义的UUID,因为SIG 小组并没有规定LED profile显然这是一个自定义的profile。

好的显然我们需要使用一个128位的自定义UUID,在SDK中如何设计呢
在这里插入图片描述
函数

uint32_t sd_ble_uuid_vs_add	(	ble_uuid128_t const * 	p_vs_uuid,
uint8_t * 	p_uuid_type 
)	

p_vs_uuid 指向一个128位的UUID数据
p_uuid_type 表示返回p_vs_uuid 指向数据的UUID类型,为输出值
在这里插入图片描述
这样我们定义好自己定义的UUID后就把attribute 内容准备好了,创建相应的变量就好。
所以我们要设计一个数据结构体,把application 的内容都涵盖进去。

首先一个应用可能包含多个Service所以需要有一个service_handle;
两个 Characteristic 。
一个UUID Type
LED需要根据写入的数据执行开关LED操作,这里需要一个led事件处理函数。

/**@brief LED Button Service structure. This structure contains various status information for the service. */
struct ble_lbs_s
{
    uint16_t                    service_handle;      /**< Handle of LED Button Service (as provided by the BLE stack). */
    ble_gatts_char_handles_t    led_char_handles;    /**< Handles related to the LED Characteristic. */
    ble_gatts_char_handles_t    button_char_handles; /**< Handles related to the Button Characteristic. */
    uint8_t                     uuid_type;           /**< UUID type for the LED Button Service. */
    ble_lbs_led_write_handler_t led_write_handler;   /**< Event handler to be called when the LED Characteristic is written. */
};

下一步
创建上面的结构体标量初始化,结构体变量
SDK用了一种比较特殊的方式,宏定义来创建。

BLE_LBS_DEF(m_lbs);   
#define BLE_LBS_DEF(_name)                                                                          \
static ble_lbs_t _name;                                                                             \
NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                 \
                     BLE_LBS_BLE_OBSERVER_PRIO,                                                     \
                     ble_lbs_on_ble_evt, &_name)

创建了ble_lbs_t 结构体变量,注册了,lbs ble事件处理函数

加下来开始初始化服务

/**@brief Function for initializing services that will be used by the application.
 */
static void services_init(void)
{
    ret_code_t         err_code;
    ble_lbs_init_t     init     = {0};
    nrf_ble_qwr_init_t qwr_init = {0};

    // Initialize Queued Write Module.
    qwr_init.error_handler = nrf_qwr_error_handler;

    err_code = nrf_ble_qwr_init(&m_qwr, &qwr_init);
    APP_ERROR_CHECK(err_code);

    // Initialize LBS.
    init.led_write_handler = led_write_handler;

    err_code = ble_lbs_init(&m_lbs, &init); //使用init对m_lbs进行初始化,init 为 0
    APP_ERROR_CHECK(err_code);
}
uint32_t ble_lbs_init(ble_lbs_t * p_lbs, const ble_lbs_init_t * p_lbs_init)
{
    uint32_t              err_code;
    ble_uuid_t            ble_uuid;
    ble_add_char_params_t add_char_params;

    // Initialize service structure.
    p_lbs->led_write_handler = p_lbs_init->led_write_handler;

    // Add service.
    ble_uuid128_t base_uuid = {LBS_UUID_BASE};
    err_code = sd_ble_uuid_vs_add(&base_uuid, &p_lbs->uuid_type); //保存返回的UUID类型,base uuid类型
    VERIFY_SUCCESS(err_code);
    // 服务名称 attribute  uuid,表示存在一条数据,用来表示服务本身
    ble_uuid.type = p_lbs->uuid_type;
    ble_uuid.uuid = LBS_UUID_SERVICE;
    
    //调用SDK服务添加API
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_lbs->service_handle);
    VERIFY_SUCCESS(err_code);

    // 服务添加完成后,添加服务下面的特征属性characteristic,依照SDK的characteristic结构体进行
    // Add Button characteristic.
    memset(&add_char_params, 0, sizeof(add_char_params));
    add_char_params.uuid              = LBS_UUID_BUTTON_CHAR;
    add_char_params.uuid_type         = p_lbs->uuid_type;
    add_char_params.init_len          = sizeof(uint8_t);//数据初始长度
    add_char_params.max_len           = sizeof(uint8_t);//允许的最大长度
    add_char_params.char_props.read   = 1;              
    add_char_params.char_props.notify = 1;

    add_char_params.read_access       = SEC_OPEN;
    add_char_params.cccd_write_access = SEC_OPEN;

    err_code = characteristic_add(p_lbs->service_handle,
                                  &add_char_params,
                                  &p_lbs->button_char_handles);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    //依次添加  characteristic
    // Add LED characteristic.
    memset(&add_char_params, 0, sizeof(add_char_params));
    add_char_params.uuid             = LBS_UUID_LED_CHAR;
    add_char_params.uuid_type        = p_lbs->uuid_type;
    add_char_params.init_len         = sizeof(uint8_t);
    add_char_params.max_len          = sizeof(uint8_t);
    add_char_params.char_props.read  = 1;
    add_char_params.char_props.write = 1;

    add_char_params.read_access  = SEC_OPEN;
    add_char_params.write_access = SEC_OPEN;

    return characteristic_add(p_lbs->service_handle, &add_char_params, &p_lbs->led_char_handles);
}

经过以上的步骤,创建了服务,和服务下面的特征属性,以及特征属性的数据条目。其关联是使用service_handle进行的。这样协议栈知道那几个characteristic是属于哪个服务的。

当协议栈需要通知应用程序一些有关它的事情的时候,协议栈事件就发生了,例如当写入特性或是描述符时。对应于本应用,你需要写入LED特性,为了让通知功能更好地工作,你需要保存连接句柄,通过这个句柄,你可以在连接事件和断开事件中实现某些操作。
作为 API的一部分,你可以定义一个函数 ble_lbs_on_ble_evt用来处理协议栈事件,可以使用简单的switch-case语句通过返回事件头部的id号来区分不同的事件,并进行不同的处理。

void ble_lbs_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
    ble_lbs_t * p_lbs = (ble_lbs_t *)p_context;

    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GATTS_EVT_WRITE:
            on_write(p_lbs, p_ble_evt);
            break;

        default:
            // No implementation needed.
            break;
    }
}

/**@brief Function for handling the Write event.
 *
 * @param[in] p_lbs      LED Button Service structure.
 * @param[in] p_ble_evt  Event received from the BLE stack.
 */
static void on_write(ble_lbs_t * p_lbs, ble_evt_t const * p_ble_evt)
{
    ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;

    if (   (p_evt_write->handle == p_lbs->led_char_handles.value_handle)
        && (p_evt_write->len == 1)
        && (p_lbs->led_write_handler != NULL))
    {
        p_lbs->led_write_handler(p_ble_evt->evt.gap_evt.conn_handle, p_lbs, p_evt_write->data[0]);
    }
}

// led_write_handler 在服务初始化的时候进行了指定
/**@brief Function for handling write events to the LED characteristic.
 *
 * @param[in] p_lbs     Instance of LED Button Service to which the write applies.
 * @param[in] led_state Written/desired state of the LED.
 */
static void led_write_handler(uint16_t conn_handle, ble_lbs_t * p_lbs, uint8_t led_state)
{
    if (led_state)
    {
        bsp_board_led_on(LEDBUTTON_LED);
        NRF_LOG_INFO("Received LED ON!");
    }
    else
    {
        bsp_board_led_off(LEDBUTTON_LED);
        NRF_LOG_INFO("Received LED OFF!");
    }
}
uint32_t ble_lbs_on_button_change(uint16_t conn_handle, ble_lbs_t * p_lbs, uint8_t button_state)
{
    ble_gatts_hvx_params_t params;
    uint16_t len = sizeof(button_state);

    memset(&params, 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(conn_handle, &params);
}

当协议栈接收到写事件的时候就会调用事件处理函数,p_evt_write->data[0] 为客户端写入的值。

到这代码视乎已经完成了。

再来看几个问题,客户端发起读的时候进行了哪些动作
读写操作也许就application开发另外一个重点了

在上面的led_write_handler函数中,我们处理了来自客户端的数据。通过事件的方式完成。
application 所有的操作都是建立在API和事件上的。事件是来自协议栈的反馈。调用API是应用层把自己相应操作发送给协议栈执行。
这所谓执行和反馈,构成了application的全部。

void ble_lbs_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
    ble_lbs_t * p_lbs = (ble_lbs_t *)p_context;
	//添加这样的一行代码,观察协议栈发送给应用层的所有事件
	NRF_LOG_ERROR(" BLE EVENT :%x ,%d ",p_ble_evt->header.evt_id,p_ble_evt->header.evt_id);

    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GATTS_EVT_WRITE:
            on_write(p_lbs, p_ble_evt);
            break;

        default:
            // No implementation needed.
            break;
    }
}

Read
需要看是否进行了授权

1.无需授权的Read
在这里插入图片描述
协议栈会直接回复读取的值,没有应用层事件
2需要授权的read
在这里插入图片描述
ATT TABLE(在初始化service添加服务的时候,数据条目会逐条加入TABLE中)中存在的值。
协议栈收到Read 请求后悔产生RW AUTHORIZE 请求,并且告诉应用层Value值。
应用层可以重新设定返回的值app_value,如果权限通过则协议栈发送app_value 给客户端。
如果权限错误那么返回错误给客户端。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值