3. 特征值存储与访问机制:本地GATT数据库实现

特征值是 GATT 服务中最核心的数据单元。嵌入式 BLE 协议栈需要在本地维护一份“GATT 数据库”,用于存储每个特征值的句柄、UUID、权限、缓存值等属性。本章聚焦两种典型的本地存储结构——静态数组与动态链表,并讲解如何注册读写回调函数,使上层应用能够响应 ATT 读写请求。

3.1 本地 GATT 数据库的基本组成

一个特征值在数据库中至少需要记录以下字段:

字段类型说明
handleuint16_tATT 句柄,唯一标识该特征值
uuiduint16_t 或 128bit特征值 UUID(如 0x2A19 表示电池电量)
permissionsbitmask读/写/通知/指示权限
valueuint8_t 数组当前缓存的特征值数据
value_lenuint16_t有效数据长度
value_max_lenuint16_t存储缓冲区最大容量
user_cbfunction pointer读写回调函数指针
设计要点: 权限字段需同时包含“安全权限”(如是否需要加密、MITM)和“操作类型”(读、写、通知、指示)。常用实现采用 8 位或 16 位位图。

3.2 基于静态数组的 GATT 数据库

对于资源受限的 MCU(Cortex-M0,<64KB Flash),推荐在编译期定义固定大小的特征值表。每个特征值在数组中占用一个槽位。

3.2.1 数组定义与初始化

typedef struct {
    uint16_t handle;
    uint16_t uuid;
    uint8_t  permissions;
    uint8_t  value[32];       // 最大32字节
    uint16_t value_len;
    uint16_t value_max_len;
    int (*read_cb)(uint16_t conn_handle, uint8_t *buf, uint16_t *len);
    int (*write_cb)(uint16_t conn_handle, const uint8_t *buf, uint16_t len);
} gatt_char_t;

#define MAX_CHARS 8
static gatt_char_t gatt_db[MAX_CHARS];
static uint8_t char_count = 0;

3.2.2 注册特征值

static int battery_level_read(uint16_t conn_handle, uint8_t *buf, uint16_t *len) {
    // 读取硬件ADC,填充buf,设置*len
    *len = 1;
    buf[0] = get_battery_level();  // 0~100
    return 0;  // 成功
}

static int battery_level_write(uint16_t conn_handle, const uint8_t *buf, uint16_t len) {
    // 一般电池特征值不允许写,返回错误
    return -1;  // 不支持写
}

void app_register_battery_characteristic(void) {
    gatt_char_t *ch = &gatt_db[char_count++];
    ch->handle = 0x0010;
    ch->uuid   = 0x2A19;
    ch->permissions = GATT_PERM_READ;
    ch->value_len    = 0;
    ch->value_max_len = 1;
    ch->read_cb  = battery_level_read;
    ch->write_cb = battery_level_write;
}

3.3 基于动态链表的 GATT 数据库

当特征值数量不确定,或需要运行时动态添加/删除服务时,使用单链表或双向链表更灵活。代价是每增加一个特征值需要 malloc 一个节点。

typedef struct gatt_char_node {
    gatt_char_t data;
    struct gatt_char_node *next;
} gatt_char_node_t;

static gatt_char_node_t *gatt_db_head = NULL;

int gatt_db_add(gatt_char_t *char_data) {
    gatt_char_node_t *node = malloc(sizeof(gatt_char_node_t));
    if (!node) return -1;
    memcpy(&node->data, char_data, sizeof(gatt_char_t));
    node->next = gatt_db_head;
    gatt_db_head = node;
    return 0;
}
适用场景提示: 静态数组适合 OTA 升级后特征值不变的应用(如健康手环);动态链表适合支持多种配置文件的通用 BLE 模块(如传感器网关)。

3.4 特征值读写回调函数注册机制

BLE 协议栈在收到 ATT Read Request 或 Write Request 时,需要调用用户提供的回调函数来获取最新值或写入数据。注册回调通常有两种方式:

  • 全局回调表:协议栈直接扫描数据库,根据句柄找到对应的回调并调用。
  • 句柄映射表:额外维护一个句柄→回调指针的哈希表或二分查找表。

以下示例展示如何在协议栈的 ATT 处理层中调用注册的回调:

// 协议栈内部:收到ATT读请求时的分发函数
int att_read_handler(uint16_t conn_handle, uint16_t attr_handle) {
    for (int i = 0; i < char_count; i++) {
        if (gatt_db[i].handle == attr_handle) {
            if (gatt_db[i].read_cb) {
                uint8_t buf[32];
                uint16_t len = 0;
                int ret = gatt_db[i].read_cb(conn_handle, buf, &len);
                if (ret == 0) {
                    // 将buf通过ATT Read Response发送回对端
                    att_send_read_rsp(conn_handle, attr_handle, buf, len);
                } else {
                    att_send_error_rsp(conn_handle, attr_handle, ATT_ERR_READ_NOT_PERMITTED);
                }
            }
            break;
        }
    }
    return 0;
}

3.5 本地 GATT 数据库访问流程

本地GATT数据库读写流程 1. 协议栈收到ATT请求 2. 查找GATT数据库句柄 3. 检查权限位 4a. 读请求 → 调用read_cb 4b. 写请求 → 调用write_cb 5a. 填充数据并发送响应 5b. 更新缓存值并应答 权限检查失败时→发送错误响应(ATT Error Response),不调用回调

3.6 调试与验证

使用串口日志或 BLE Sniffer 验证数据库行为:

[ATT] Read Request handle=0x0010 length=0
[GATT] lookup handle=0x0010 -> found idx=2
[GATT] permission check: READ allowed
[GATT] calling read_cb (battery_level_read)
[GATT] read_cb returns 0, data=[0x64] len=1
[ATT] Read Response handle=0x0010 data=[0x64]

3.7 常见错误与避坑指南

错误现象可能原因解决措施
对方读特征值返回错误码 0x0A(Attribute Not Found)GATT 数据库中未注册该句柄,或句柄被覆盖检查数组/链表初始化,确保句柄唯一且未越界
写特征值后数据不更新write_cb 未写回 value 缓冲区回调中更新 ch->value 和 ch->value_len
多次注册后内存溢出(动态链表)未释放旧节点,或忘记 free增加节点销毁函数,在服务删除时释放
权限检查始终通过permissions 位掩码定义错误使用 #define 定义常量,与协议栈枚举对齐

3.8 性能与稳定性优化建议

  • 数组优先: 若无动态增减需求,优先使用静态数组,避免堆碎片。
  • 查找加速: 若特征值超过 20 个,将数组按句柄排序,使用二分查找代替线性扫描。
  • 原子操作: 如果回调在中断上下文被调用,需要保护 gatt_db 的访问(如禁用全局中断或使用临界区)。
  • 缓存一致性: 若特征值由传感器循环更新,务必在 write_cb 中同步更新缓存值,并触发通知(如果需要)。
特别注意: 回调函数中不应执行阻塞操作(如延时、打印大量日志),否则会阻塞 ATT 协议栈,导致连接断开或超时。若需要较长时间处理,应启动异步任务,立即返回 pending 状态码。