4. 特征读取操作实现:客户端发起 Read Request、服务端处理流程、响应构造与 ATT PDU 格式
4.1 章节概述
特征读取(Read Characteristic)是最基础的 GATT 操作之一。本章将聚焦于客户端发起 Read Request、服务端接收与处理请求、构造响应并返回 ATT PDU 的完整流程。正确理解 ATT PDU 结构是调试 BLE 数据通信的关键。
关键概念:Read Request 使用 ATT 命令码
0x0A,响应使用 0x0B(Read Response)。每次读取最多返回 MTU - 1 字节数据(默认 MTU=23 时最多 22 字节)。
4.2 读取操作角色与数据流
以下 SVG 描述了从客户端发起 Read Request 到服务端返回 Read Response 的完整数据流:
4.3 客户端发起 Read Request
4.3.1 关键 API(host 层)
在 Zephyr / nRF Connect SDK 中,读取特征值使用以下 API:
// 异步读取 - 通过回调返回数据
int bt_gatt_read(struct bt_conn *conn, struct bt_gatt_read_params *params);
// 参数结构体定义
struct bt_gatt_read_params {
bt_gatt_read_func_t func; // 读取完成回调
bt_gatt_attr_func_t handle_rsp; // 处理响应的回调
uint8_t handle_count; // 要读取的 handle 数量
uint16_t *handles; // handle 数组
uint8_t variable; // 可变长度读取标志
};
注意:
bt_gatt_read_params.handles 必须保持有效,直到回调返回。建议使用全局或静态分配的数组。
4.3.2 读取示例(长连接模式)
// 读取单个特征值
static struct bt_gatt_read_params read_params;
static uint16_t target_handle = 0x0012; // 假设特征值句柄
static void read_cb(struct bt_conn *conn, uint8_t err,
struct bt_gatt_read_params *params,
const void *data, uint16_t length)
{
if (err) {
printk("Read failed: err 0x%02x\n", err);
return;
}
printk("Read succeeded: length=%u\n", length);
printk("Data: %s\n", (const char *)data);
// 清除读取参数,避免悬挂指针
memset(&read_params, 0, sizeof(read_params));
}
void read_characteristic(struct bt_conn *conn, uint16_t handle)
{
read_params.func = read_cb;
read_params.handle_count = 1;
read_params.handles = &target_handle;
read_params.variable = 0; // 非长读取
int err = bt_gatt_read(conn, &read_params);
if (err) {
printk("bt_gatt_read failed: %d\n", err);
}
}
4.4 服务端处理流程(Zephyr 驱动层)
服务端收到 Read Request 后,经过以下步骤处理:
- 协议栈接收:HCI 层收到 ATT PDU,递交给
att_read_req()函数。 - 句柄验证:检查 handle 是否在已注册属性表中。
- 权限检查:验证读取权限(加密、认证、授权等)。
- 调用用户回调:触发注册在属性上的
struct bt_gatt_attr::read回调。 - 构造响应:将回调返回的数据打包成 Read Response PDU。
- 发送:通过 L2CAP 层发送回客户端。
4.4.1 服务端属性注册示例
// 服务端定义特征属性,注册读取回调
static ssize_t read_my_char(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
// 假设数据存储在 attr->user_data 中
const char *value = (const char *)attr->user_data;
return bt_gatt_attr_read(conn, attr, buf, len, offset, value, strlen(value));
}
BT_GATT_SERVICE_DEFINE(my_service,
BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_MY_CHAR,
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
read_my_char, NULL, "Hello BLE"),
);
4.5 ATT PDU 格式详解
| 方向 | ATT 命令码 | PDU 字段 | 字节偏移 | 说明 |
|---|---|---|---|---|
| Client → Server | 0x0A (Read Request) | Opcode (1B) + Handle (2B) | 0-2 | Opcode + 特征值句柄(小端序) |
| Server → Client | 0x0B (Read Response) | Opcode (1B) + Value (0~MTU-1 B) | 0~N | Opcode + 有效数据(无长度前缀) |
| Client → Server | 0x0C (Read Blob Request) | Opcode (1B) + Handle (2B) + Offset (2B) | 0-4 | 用于读取长特征值(> 22 bytes) |
| Server → Client | 0x0D (Read Blob Response) | Opcode (1B) + Value (0~MTU-1 B) | 0~N | 带偏移的读取响应 |
常见错误:ATT PDU 不包含数据长度字段!接收方通过 L2CAP 帧的完整长度减去 1(Opcode 长度)来推断数据长度。因此 MTU 不匹配可能直接导致数据截断或解析错误。
4.6 响应构造与回调实现
4.6.1 使用 bt_gatt_attr_read 辅助函数
服务端推荐使用 bt_gatt_attr_read() 构造响应,该函数自动处理 Length 检查与偏移:
/**
* 通用读取回调辅助函数
* @param conn 当前连接
* @param attr 属性指针(通过 user_data 存放数据)
* @param buf 响应缓冲区(协议栈提供)
* @param len 缓冲区剩余长度
* @param offset 客户端请求的偏移量(Read Blob 时使用)
* @param value 实际数据指针
* @param value_len 实际数据长度
* @return 实际写入 buf 的字节数,或负值表示错误
*/
ssize_t bt_gatt_attr_read(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf, uint16_t len,
uint16_t offset,
const void *value, uint16_t value_len);
4.6.2 手动构造响应(用于调试或特殊逻辑)
// 服务端手动构造 Read Response(不推荐生产环境使用)
ssize_t my_custom_read(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
// 检查是否在 MTU 范围内
if (offset) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
uint8_t response_data[] = {0x01, 0x02, 0x03, 0x04}; // 示例数据
uint16_t data_len = sizeof(response_data);
// 确保不超过 MTU-1
if (len < data_len) {
data_len = len;
}
// 直接复制数据到 buf(协议栈会自动添加 Opcode 0x0B)
memcpy(buf, response_data, data_len);
return data_len;
}
调试建议:使用 nRF Connect 或 LightBlue 的 Logger 捕获 ATT PDU 原始字节。Read Request 典型十六进制格式:
0A 12 00(Handle=0x0012)。Read Response:0B 48 65 6C 6C 6F(返回 "Hello")。
4.7 常见错误与避坑总结
| 错误 | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| 句柄无效 | 客户端收到 ATT_ERR_INVALID_HANDLE (0x01) | Handle 不在服务端属性表中 | 使用 bt_gatt_attr_find_by_handle 验证 |
| 权限不足 | 客户端收到 ATT_ERR_READ_NOT_PERMITTED (0x02) | 属性未设置 BT_GATT_PERM_READ | 在 BT_GATT_CHARACTERISTIC 宏中添加权限位 |
| 数据截断 | 客户端收到不完整数据 | MTU 过小,数据被 L2CAP 层截断 | 使用 MTU Exchange 或采用 Read Blob(偏移读取) |
| 回调未实现 | 服务端返回 ATT_ERR_UNLIKELY_ERROR (0x0E) | 注册特征时 read 回调为 NULL | 确保 BT_GATT_CHARACTERISTIC 第4个参数非空 |
| 数据格式错误 | 客户端解析乱码 | 返回的数据包含非预期字节序或编码 | 明确约定 UTF-8 / 二进制 / 小端序,并在回调中统一处理 |
4.8 性能与稳定性优化
- 减少 ATT 事务:批量读取多个特征时,使用
Read Multiple Request(命令码0x0E)合并为一个 PDU。 - 缓存策略:服务端在
read回调中不要执行阻塞操作(如 Flash 擦写、I2C 等待)。将最新值预取到 RAM,确保读取 O(1) 完成。 - MTU 协商:客户端主动发起 MTU Exchange(命令
0x02),增大 MTU 可减少长数据读取的分片次数。 - 安全性:对敏感特征,在
read回调中通过bt_conn_get_security()校验连接加密等级,避免未授权读取。
4.9 本节要点
- Read Request / Response 是 同步事务:客户端发出请求后,必须等待服务器响应才能发起下一个 ATT 请求。
- ATT PDU 中 没有数据长度字段,数据长度由 L2CAP 帧长度自动推断。
- 服务端必须正确实现
read回调,并处理offset支持长特征读取。 - 使用
bt_gatt_attr_read辅助函数可以减少手动处理边界条件的错误。 - 调试时捕获原始 ATT PDU 是最直观的排错手段。