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 的完整数据流:

特征读取操作数据流(Read Request / Response) 客户端 (GATT Client) 服务端 (GATT Server) ① 构造 Read Request ② ATT PDU: 0x0A + Handle + Auth ③ 解析 Handle 查找属性 ④ 构造 Read Response ⑤ ATT PDU: 0x0B + Value (MTU-1 bytes) ⑥ 解析 Value 更新缓存

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 后,经过以下步骤处理:

  1. 协议栈接收:HCI 层收到 ATT PDU,递交给 att_read_req() 函数。
  2. 句柄验证:检查 handle 是否在已注册属性表中。
  3. 权限检查:验证读取权限(加密、认证、授权等)。
  4. 调用用户回调:触发注册在属性上的 struct bt_gatt_attr::read 回调。
  5. 构造响应:将回调返回的数据打包成 Read Response PDU。
  6. 发送:通过 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 → Server0x0A (Read Request)Opcode (1B) + Handle (2B)0-2Opcode + 特征值句柄(小端序)
Server → Client0x0B (Read Response)Opcode (1B) + Value (0~MTU-1 B)0~NOpcode + 有效数据(无长度前缀)
Client → Server0x0C (Read Blob Request)Opcode (1B) + Handle (2B) + Offset (2B)0-4用于读取长特征值(> 22 bytes)
Server → Client0x0D (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 是最直观的排错手段。