3 概览
3.1 基本原则
- BLE 协议栈尽最大努力执行各项任务,比如
- 将发射功率设置为 \(100dBm\),协议栈将以最大功率进行发射
- 广播、连接并发时,部分广播事件、连接事件可能因需要执行其它任务而被忽略(跳过)
- 极端情况下,当协议栈可能会主动终止某些任务并上报相应的事件
- 连接模式下已进入 BLE 协议栈的数据包不会丢失、总是可以送达,除非连接断开
3.2 协议栈架构
Controller、Host 以两个任务(或者线程)的形式运行,HCI 接口经过特殊设计,尽量减少内存数据的复制。 Host 的架构如图 3.1 所示,主要通过 GAP、ATT、GATT Client、SM 等 4 个模块为开发者提供操作接口。
Host 任务的伪代码如下:
void host_task(void)
{
();
host_initwhile (true)
{
if (recv_msg(msg) != 0) continue;
(msg);
process_msg}
}
也就是说 Host 任务是由各种消息驱动的。这些消息既包括来自 Controller 的事件、ACL 数据,
也包含软件定时器消息和用户发送的消息(btstack_push_user_msg
和 btstack_push_user_runnable
)。
process_msg
的伪代码如下:
void process_msg(msg)
{
switch (msg)
{
case HCI Event:
();
调用各个回调break;
case ACL 数据:
();
调用各个回调break;
case 软件定时器:
();
超时处理break;
case 用户消息:
();
弹出用户消息事件break;
case 用户执行体:
();
运行用户执行体break;
}
}
各个模块(包含协议内部模块及 App3)通过注册回调函数以响应这些事件。 有的模块在处理这些消息时又会产生其它的新事件, 为了响应这些新事件可以再向这些模块注册回调函数。也就是说,Host 内部各个模块(以及 App)通过消息、回调函数耦合在一起。 例如:
hci_add_event_handler
通过这个函数可以注册一个能够监听所有 HCI 事件的回调;
att_server_init
向 ATT Server 模块注册用以响应特征读写的回调函数。
3.3 通信模型
通信是指由一地向另一地进行信息的传输与交换,其目的是传输信息、削减另一地的不确定性。对于 BLE 而言,主要有两种通信方式: 一对多的广播、一对一的连接。低功耗蓝牙定义了四种角色:广播发送方称为广播者(Broadcaster),接收方称为观察者(Observer); 连接的发起方称为主角色(新名称为中心角色,Central),接受方称为从角色(新名称为外围角色,Periperhal)。
蓝牙规范定义了广播数据的格式、AD 类型,见图 3.2。如,AD 类型 0x09
表示设备名称,其后面的 AD 数据域是一个 UTF-8 字符串。
另请参阅“广播数据”一节。
对于连接模式,低功耗蓝牙定义了特征(Characteristic)和值(Value)这两个概念,进而组织成服务(Service),再由服务组成配置(Profile)。 特征的标识用 UUID 标识。客户端发现了服务器支持的服务后,就可以读写特征的值,或者订阅特征4。 显然特征不见得支持所有这些操作,因此,蓝牙核心规范又为特征定义了属性(Property)、 描述符(Discriptor)等装饰物,以说明特征所支持的操作、需要的权限。UUID 长度为 \(16\) 字节,为了提高传输效率,BLE 定义了一系列特殊的 UUID, 它们的区别仅在于 2 个字节,另外 \(14\) 个字节相同:
0x0000xxxx-0000-1000-8000-00805F9B34FB
这两个字节就是所谓的 16-bit UUID,由蓝牙特别兴趣小组公司负责管理、分配5。
3.4 回调函数事件包
协议栈的各种回调函数遵循类似的原型,其输入称为事件包:
typedef void (*btstack_packet_handler_t) (
// 事件包类型
uint8_t packet_type,
// 关联的信道(一般指蓝牙连接句柄)
uint16_t channel,
// 事件包内容
const uint8_t *packet,
// 事件包内容的长度
uint16_t size);
包类型 packet_type
的取值如下:
HCI_EVENT_PACKET
:HCI 事件包(常用)这个类型的事件包是个“大杂烩”,多个模块弹出的事件包都会使用这个类型。
HCI_ACL_DATA_PACKET
:Controller 上报的 ACL 数据这个类型的事件包只会被通过
hci_register_acl_packet_handler
注册的 ACL 数据回调函数收到。HCI_COMPLETED_SDU_PACKET
:来自 LE 信用信道的完整 SDU这个类型的事件包只会被通过
l2cap_register_service
注册的 L2CAP 服务回调函数收到。L2CAP_EVENT_PACKET
:来自 L2CAP 的事件包这个类型的事件包只会被通过
l2cap_add_event_handler
注册的 L2CAP 事件回调函数收到。
3.5 事件包的解析
下面只介绍 HCI_EVENT_PACKET
事件包的解析,其它几种类型不常用。
首先使用 hci_event_packet_get_type(packet)
获取事件代码,根据事件代码的不同,后续的处理大不相同。
常用的几种事件代码如下。
BTSTACK_EVENT_STATE
:蓝牙协议栈事件一般用于响应协议栈初始化:
if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) break; // App 初始化
HCI_EVENT_LE_META
:BLE 元事件这个元事件下辖多个子事件。先通过
hci_event_le_meta_get_subevent_code(packet)
获得子事件代码,然后通过decode_hci_le_meta_event(packet, sub_event_type)
宏得到子事件的内容。sub_event_type
为子事件内容对应的数据类型,各字段与蓝牙核心规范里的定义一致6。协议栈的版本及初始化流程导致下列子事件不会出现,只会出现对应的扩展过的、功能更全面的事件:
HCI_SUBEVENT_LE_CONNECTION_COMPLETE
5.3 及以下改用
HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE
; 5.4 及以上改用HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE_V2
HCI_SUBEVENT_LE_ADVERTISING_REPORT
改用
HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT
协议栈可能报告的子事件及对应的
sub_event_type
类型如下:HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE
连接参数更新(
le_meta_event_conn_update_complete_t
)HCI_SUBEVENT_LE_READ_REMOTE_USED_FEATURES_COMPLETE
读取对端特性(
le_meta_event_read_remote_feature_complete_t
)HCI_SUBEVENT_LE_LONG_TERM_KEY_REQUEST
请求 LTK(
le_meta_event_long_term_key_request_t
)HCI_SUBEVENT_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_COMPLETE
远端连接参数请求(
le_meta_event_remote_conn_param_request_t
)HCI_SUBEVENT_LE_DATA_LENGTH_CHANGE_EVENT
数据包长度改变(
le_meta_event_data_length_changed_t
)HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE
(5.3 及以下)连接建立(
le_meta_event_enh_create_conn_complete_t
)HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE_V2
(5.4 及以上)连接建立(
le_meta_event_enh_create_conn_complete_v2_t
),与HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE
相比,增加了 PAwR 的相关信息HCI_SUBEVENT_LE_DIRECT_ADVERTISING_REPORT
定向广播报告(
le_meta_directed_adv_report_t
)HCI_SUBEVENT_LE_PHY_UPDATE_COMPLETE
PHY 更新完成(
le_meta_phy_update_complete_t
)HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT
扩展广播报告(
le_meta_event_ext_adv_report_t
)HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED
(5.3 及以下)周期广播同步建立(
le_meta_event_periodic_adv_sync_established_t
)HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_V2
(5.4 及以上)周期广播同步建立(
le_meta_event_periodic_adv_sync_established_v2_t
),与HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED
相比,增加了 PAwR 的相关信息HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_REPORT
(5.3 及以下)周期广播报告(
le_meta_event_periodic_adv_report_t
)HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_REPORT_V2
(5.4 及以上)周期广播报告(
le_meta_event_periodic_adv_report_v2_t
),与HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_REPORT
相比,增加了 PAwR 的相关信息HCI_SUBEVENT_LE_PERIODIC_ADVERTISING_SYNC_LOST
周期广播同步丢失(
le_meta_event_periodic_adv_sync_lost_t
)HCI_SUBEVENT_LE_SCAN_TIMEOUT
扫描超时
HCI_SUBEVENT_LE_ADVERTISING_SET_TERMINATED
广播停止(
le_meta_adv_set_terminated_t
)如果是由于广播是由于建立了连接而停止,那么从这个事件里可以得到广播句柄和连接句柄的对应关系。
HCI_SUBEVENT_LE_SCAN_REQUEST_RECEIVED
收到扫描请求(
le_meta_scan_req_received_t
)HCI_SUBEVENT_LE_CHANNEL_SELECTION_ALGORITHM
信道选择算法(
le_meta_ch_sel_algo_t
)HCI_SUBEVENT_LE_CONNECTIONLESS_IQ_REPORT
无连接 IQ 报告(
le_meta_connless_iq_report_t
)HCI_SUBEVENT_LE_CONNECTION_IQ_REPORT
有连接 IQ 报告(
le_meta_conn_iq_report_t
)HCI_SUBEVENT_LE_CTE_REQ_FAILED
CTE 请求失败(
le_meta_cte_req_failed_t
)HCI_SUBEVENT_LE_PRD_ADV_SYNC_TRANSFER_RCVD
(5.3 及以下)周期广播转移请求(
le_meta_prd_adv_sync_transfer_recv_t
)HCI_SUBEVENT_LE_PRD_ADV_SYNC_TRANSFER_RCVD_V2
(5.4 及以上)周期广播转移请求(
le_meta_prd_adv_sync_transfer_recv_v2_t
),与HCI_SUBEVENT_LE_PRD_ADV_SYNC_TRANSFER_RCVD
相比,增加了 PAwR 的相关信息HCI_SUBEVENT_LE_REQUEST_PEER_SCA
对端 SCA 请求完成(
le_meta_request_peer_sca_complete_t
)HCI_SUBEVENT_LE_PATH_LOSS_THRESHOLD
路损门限报告(
le_meta_path_loss_threshold_t
)HCI_SUBEVENT_LE_TRANSMIT_POWER_REPORTING
发射功率报告(
le_meta_tx_power_reporting_t
)HCI_SUBEVENT_LE_SUBRATE_CHANGE
减速模式改变(
le_meta_subrate_change_t
)HCI_SUBEVENT_PRD_ADV_SUBEVT_DATA_REQ
PAwR 子事件数据请求(
le_mete_event_prd_adv_subevent_data_req_t
)HCI_SUBEVENT_PRD_ADV_RSP_REPORT
PAwR 子事件响应(
le_mete_event_prd_adv_rsp_report_t
)HCI_SUBEVENT_LE_VENDOR_PRO_CONNECTIONLESS_IQ_REPORT
私有无连接 IQ 报告(
le_meta_pro_connless_iq_report_t
)
HCI_EVENT_DISCONNECTION_COMPLETE
:连接断开事件通过
decode_hci_event_disconn_complete(packet)
解析事件内容:typedef struct event_disconn_complete { // 状态码 uint8_t status; // 连接句柄 uint16_t conn_handle; // 原因 uint8_t reason; } event_disconn_complete_t;
HCI_EVENT_COMMAND_COMPLETE
:HCI 命令完成事件通过
hci_event_command_complete_get_command_opcode(packet)
获得 HCI 命令码。 通过hci_event_command_complete_get_return_parameters(packet)
获得 Controller 返回的参数,其中第 1 个字节为命令完成的状态, \(0\) 表示没有错误,详见“Controller 错误码”。其它参数需要根据命令码做具体分析。HCI_EVENT_COMMAND_STATUS
:HCI 命令状态事件有些 HCI 命令可以立即完成,得到结果,Controller 会上报相应的
HCI_EVENT_COMMAND_COMPLETE
。有些 HCI 命令则需要一定时间才能完成,比如发起连接, Controller 收到这样的命令后不上报HCI_EVENT_COMMAND_COMPLETE
,而是上报HCI_EVENT_COMMAND_STATUS
。通过
hci_event_command_status_get_command_opcode(packet)
获得 HCI 命令码。 通过hci_event_command_status_get_status(packet)
获得状态,\(0\) 表示没有错误,详见“Controller 错误码”。BTSTACK_EVENT_USER_MSG
:来自btstack_push_user_msg
的用户消息
3.6 Controller 错误码
表 3.1 列出了 Controller 使用的部分错误码。
错误码 | 含义 |
---|---|
0x00 | 无错误 |
0x01 | 未知的命令 |
0x02 | 未知的连接句柄 |
0x03 | 硬件错误 |
0x05 | 鉴权失败 |
0x06 | PIN 或密钥缺失 |
0x07 | 超出内存容量 |
0x08 | 连接超时 |
0x09 | 连接数目达到极限 |
0x0B | 连接已存在 |
0x0C | 命令不允许 |
0x11 | 不支持的特性或参数值 |
0x12 | HCI 命令参数错误 |
0x13 | 远端用户断开连接 |
0x14 | 远端设备因资源紧张断开连接 |
0x15 | 远端设备因关机断开连接 |
0x16 | 本机断开连接 |
0x1A | 不支持的远端或 LMP 特性 |
0x1E | 非法的 LMP 或 LL 参数 |
0x1F | 未指定的错误 |
0x20 | 不支持的 LMP 或 LL 参数 |
0x22 | LMP 或 LL 响应超时 |
0x23 | LMP 错误(会话冲突) |
0x28 | 时机已过 |
0x2E | 不支持信道分类 |
0x2F | 安全特性不足 |
0x3A | Controller 正忙 |
0x3C | 定向广播超时 |
0x3D | 因 MIC 错误而断开连接 |
0x3E | 连接无法建立 |
0x43 | 到达极限 |
0x44 | Host 已取消 |
0x45 | 数据包太长 |
0x46 | 太晚了 |
0x47 | 太早了 |
0xFE | 任务调度失败(私有错误码) |
0xFF | 其它错误(私有错误码) |
3.7 ATT 错误码
表 3.2 列出了蓝牙核心规范定义的 ATT 错误码。
错误码 | 含义 |
---|---|
0x00 | 无错误。 |
0x01 | 无效句柄。 |
0x02 | 指定的特征不可读。 |
0x03 | 指定的特征不可写。 |
0x04 | PDU 不合法。 |
0x05 | 身份认证不足。在读写该特征之前需要进行身份认证。 |
0x06 | ATT 服务器不支持该请求。 |
0x07 | Offset 参数超出范围。 |
0x08 | 授权不足。在读写该特征之前需要获得授权。 |
0x09 | Prepare Write 队列已满。 |
0x0A | 未指定指定的特征。 |
0x0B | 特征太长,无法通过 ATT_READ_BLOB_REQ PDU 读取。 |
0x0C | 密钥长度太短。 |
0x0D | 特征的长度不合法。 |
0x0E | 不常见的错误。 |
0x0F | 加密不足。在读写该特征之前需要开启加密。 |
0x10 | 不支持的组类型。 |
0x11 | 资源不足,无法完成请求。 |
0x12 | 数据库失步。服务器要求客户端重新发现数据库。 |
0x13 | 特征的参数值非法。 |
0x80~0x9F | 应用层错误码。 |
0xE0~0xFF | Profile 或 服务的错误码7。 |
宏定义 ATT_ERROR_....
与 0x01 ~ 0x13 错误码一一对应。
3.8 Controller 特性定义
比特位置 | 链路层特性 |
---|---|
0 | LE Encryption |
1 | Connection Parameters Request |
2 | Extended Reject Indication |
3 | Slave-initiated Features Exchange |
4 | LE Ping |
5 | LE Data Packet Length Extension |
6 | LL Privacy |
7 | Extended Scanner Filter Policies |
8 | LE 2M PHY |
9 | Stable Modulation Index - Transmitter |
10 | Stable Modulation Index - Receiver |
11 | LE Coded PHY |
12 | LE Extended Advertising |
13 | LE Periodic Advertising |
14 | Channel Selection Algorithm #2 |
15 | LE Power Class 1 |
16 | Minimum Number of Used Channels Procedure |
17 | Connection CTE Request |
18 | Connection CTE Response |
19 | Connectionless CTE Transmitter |
20 | Connectionless CTE Receiver |
21 | Antenna Switching During CTE Transmission (AoD) |
22 | Antenna Switching During CTE Reception (AoA) |
23 | Receiving Constant Tone Extensions |
24 | Periodic Advertising Sync Transfer - Sender |
25 | Periodic Advertising Sync Transfer - Recipient |
26 | Sleep Clock Accuracy Updates |
27 | Remote Public Key Validation |
28 | Connected Isochronous Stream - Master |
29 | Connected Isochronous Stream - Slave |
30 | Isochronous Broadcaster |
31 | Synchronized Receiver |
32 | Isochronous Channels (Host Support) |
33 | LE Power Control Request |
34 | LE Power Control Request |
35 | LE Path Loss Monitoring |
36 | Periodic Advertising ADI |
37 | Connection Subrating |
38 | Connection Subrating (Host Support) |
39 | Channel Classification |
两个比特位置 33 和 34 合并表示 LE 功控请求特性,只能同为 0 或同为 1。 这是因为蓝牙规范 5.2 的特性定义有误,5.3 加以修正,只得以两个比特表示同一特性。
3.10 白名单
在广播、扫描或者建立连接时,都可能用到白名单。操作白名单的 API 共有 3 个:
gap_clear_white_lists
:清空白名单gap_add_whitelist
:添加一个设备地址gap_remove_whitelist
:删除一个设备地址
3.11 异步特性
Host API 绝大多数都是异步非阻塞操作,比如调用 gap_set_ext_adv_enable()
并不立即使能广播:这个函数只会给 Controller
发送一条 HCI 消息8,Controller 接收消息、完成处理之后才会真正开始广播。
这种异步特性可能使得实现某些功能的代码冗长、零散,开发者可以考虑使用其它语言9, 或者重新封装 Host API,使其变为同步操作(参考 “同步版 API”一节)。
3.12 线程安全性
Host 是线程不安全的,除下列函数之外的所有 API,如无特殊说明都必须在 Host 任务的上下文内调用。 当其它任务需要调用 Host API 时,必须触发一段处于 Host 任务上下文内的代码,再在这段代码里调用 Host API。 下列函数恰是为该功能设置:
btstack_push_user_msg
通过
btstack_push_user_msg
向 Host 发送一条消息,消息的回调处理处于 Host 任务上下文内, 在这里可以调用 Host API。btstack_push_user_runnable
通过
btstack_push_user_runnable
向 Host 发送一个可执行体(函数指针及参数),Host 任务在其上下文内运行这个可执行体。这个可执行体可以自由调用 Host API。 SDK 提供的工具模块btstack_mt.c
利用这个函数实现了一套“线程安全的 API”。
开发者在其它任务里直接调用 Host API,即便发现功能正常,也仅是偶然现象,无法保证总是正常。
3.13 BLE 设备地址
BLE 设备地址长度为 6 字节,外加 1 个比特表示地址类型。BLE 规范定义了若干种地址类型:
公共地址(Public Address,地址类型为 0)
指从 IEEE 注册机构(IEEE Registration Authority)获得的全球唯一的 EUI-48 地址。
随机地址(Random Address,地址类型为 1)
以下几种随机地址类型通过最高的 2 个比特区分。
静态设备地址(最高 2 个比特为 0b11)
可随机生成,可以每次上电后重新生成10,但是整个上电周期内不能改变。
私有地址
共有两种私有地址。
可解析私有地址(最高 2 个比特为 0b01)
不可解析私有地址(最高 2 个比特为 0b00)
多数情况下四处广播设备的公共地址或者静态地址显然不是一个好主意,使用私有地址可有效地保护隐私。 有了可解析地址的概念后,设备的公共地址、静态地址就从逻辑上变成身份地址(Identity Address)。
INGCHIPS 918xx/916xx 系列芯片没有公共地址,只能通过编程配置随机地址。
使用蓝牙地址时要注意字节顺序。协议栈遵循下面的基本规律:
- API 使用大端模式
- BLE 元事件使用小端模式11
使用 reverse_bd_addr
可以翻转地址的字节顺序:
void reverse_bd_addr(
// 待翻转的地址
const uint8_t *src,
// 输出(不能与 src 相同)
uint8_t * dest);
3.14 解析列表与隐私
当使用链路层隐私(LL Privacy)特性12时,需要用到解析列表。与之相关的 API 如下:
gap_add_dev_to_resolving_list
:向列表中添加一条记录。一条记录包含 4 项数据:对端设备身份地址及地址类型,本端及对端的 IRK;gap_remove_dev_from_resolving_list
:删除列表中的一条记录;gap_clear_resolving_list
:清空列表;gap_read_peer_resolving_addr
:读取与某条记录关联的对端设备的可解析地址;gap_read_local_resolving_addr
:读取与某条记录关联的本端设备的可解析地址;gap_set_addr_resolution_enable
:使能或禁用地址解析功能;gap_set_resolvable_private_addr_timeout
:设置可解析地址超时时间。超过此时间后,Controller 会重新随机生成可解析地址;gap_set_privacy_mode
:设置某对端设备的隐私模式。
设备要保护自己的隐私,就必须保证不在任何广播 PDU 里发送其身份地址。使用解析列表里的某个记录时,如果本端 IRK 有效(即不是全部为 0),
则本端使用可解析地址,隐私得到保护,反之则只能使用身份地址,泄露隐私;如果对端 IRK 有效,则只接受可解析地址,
对端可在保护了隐私的前提下与本端正常通信,反之则只能接受、识别对端的身份地址,对端为了与本端通信必须泄露隐私。
如果将隐私模式设置为 PRIVACY_MODE_DEVICE
,那么当对端 IRK 有效时,Controller 仍会接受对端的身份地址。
也就是说,保证地址解析列表每条记录里的本端和对端的 IRK 都是有效的,并使能地址解析,可有效地保护隐私,详见 链路层隐私。
PRIVACY_MODE_DEVICE
仅可用于对端设备完全无法支持可解析地址的情况。